mirror of
https://github.com/jlu5/PyLink.git
synced 2024-11-01 01:09:22 +01:00
Move Irc() from main.py to classes.py
This prevents import loops (main->utils->classes->main) from the changes in the next commit.
This commit is contained in:
parent
51389b96e2
commit
090fa85a46
261
classes.py
261
classes.py
@ -1,9 +1,261 @@
|
|||||||
import threading
|
import threading
|
||||||
from random import randint
|
from random import randint
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import ssl
|
||||||
|
from collections import defaultdict
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from log import log
|
from log import log
|
||||||
import main
|
import utils
|
||||||
import time
|
|
||||||
|
### Exceptions
|
||||||
|
|
||||||
|
class NotAuthenticatedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ProtocolError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
### Internal classes (users, servers, channels)
|
||||||
|
|
||||||
|
class Irc():
|
||||||
|
def initVars(self):
|
||||||
|
self.pseudoclient = None
|
||||||
|
self.connected = threading.Event()
|
||||||
|
self.lastping = time.time()
|
||||||
|
# Server, channel, and user indexes to be populated by our protocol module
|
||||||
|
self.servers = {self.sid: IrcServer(None, self.serverdata['hostname'], internal=True)}
|
||||||
|
self.users = {}
|
||||||
|
self.channels = defaultdict(IrcChannel)
|
||||||
|
# Sets flags such as whether to use halfops, etc. The default RFC1459
|
||||||
|
# modes are implied.
|
||||||
|
self.cmodes = {'op': 'o', 'secret': 's', 'private': 'p',
|
||||||
|
'noextmsg': 'n', 'moderated': 'm', 'inviteonly': 'i',
|
||||||
|
'topiclock': 't', 'limit': 'l', 'ban': 'b',
|
||||||
|
'voice': 'v', 'key': 'k',
|
||||||
|
# Type A, B, and C modes
|
||||||
|
'*A': 'b',
|
||||||
|
'*B': 'k',
|
||||||
|
'*C': 'l',
|
||||||
|
'*D': 'imnpstr'}
|
||||||
|
self.umodes = {'invisible': 'i', 'snomask': 's', 'wallops': 'w',
|
||||||
|
'oper': 'o',
|
||||||
|
'*A': '', '*B': '', '*C': 's', '*D': 'iow'}
|
||||||
|
|
||||||
|
# This max nick length starts off as the config value, but may be
|
||||||
|
# overwritten later by the protocol module if such information is
|
||||||
|
# received. Note that only some IRCds (InspIRCd) give us nick length
|
||||||
|
# during link, so it is still required that the config value be set!
|
||||||
|
self.maxnicklen = self.serverdata['maxnicklen']
|
||||||
|
self.prefixmodes = {'o': '@', 'v': '+'}
|
||||||
|
|
||||||
|
# Uplink SID (filled in by protocol module)
|
||||||
|
self.uplink = None
|
||||||
|
self.start_ts = int(time.time())
|
||||||
|
|
||||||
|
# UID generators, for servers that need it
|
||||||
|
self.uidgen = {}
|
||||||
|
|
||||||
|
def __init__(self, netname, proto, conf):
|
||||||
|
# Initialize some variables
|
||||||
|
self.name = netname.lower()
|
||||||
|
self.conf = conf
|
||||||
|
self.serverdata = conf['servers'][netname]
|
||||||
|
self.sid = self.serverdata["sid"]
|
||||||
|
self.botdata = conf['bot']
|
||||||
|
self.proto = proto
|
||||||
|
self.pingfreq = self.serverdata.get('pingfreq') or 30
|
||||||
|
self.pingtimeout = self.pingfreq * 2
|
||||||
|
|
||||||
|
self.initVars()
|
||||||
|
|
||||||
|
self.connection_thread = threading.Thread(target = self.connect)
|
||||||
|
self.connection_thread.start()
|
||||||
|
self.pingTimer = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
ip = self.serverdata["ip"]
|
||||||
|
port = self.serverdata["port"]
|
||||||
|
while True:
|
||||||
|
self.initVars()
|
||||||
|
checks_ok = True
|
||||||
|
try:
|
||||||
|
self.socket = socket.socket()
|
||||||
|
self.socket.setblocking(0)
|
||||||
|
# Initial connection timeout is a lot smaller than the timeout after
|
||||||
|
# we've connected; this is intentional.
|
||||||
|
self.socket.settimeout(self.pingfreq)
|
||||||
|
self.ssl = self.serverdata.get('ssl')
|
||||||
|
if self.ssl:
|
||||||
|
log.info('(%s) Attempting SSL for this connection...', self.name)
|
||||||
|
certfile = self.serverdata.get('ssl_certfile')
|
||||||
|
keyfile = self.serverdata.get('ssl_keyfile')
|
||||||
|
if certfile and keyfile:
|
||||||
|
try:
|
||||||
|
self.socket = ssl.wrap_socket(self.socket,
|
||||||
|
certfile=certfile,
|
||||||
|
keyfile=keyfile)
|
||||||
|
except OSError:
|
||||||
|
log.exception('(%s) Caught OSError trying to '
|
||||||
|
'initialize the SSL connection; '
|
||||||
|
'are "ssl_certfile" and '
|
||||||
|
'"ssl_keyfile" set correctly?',
|
||||||
|
self.name)
|
||||||
|
checks_ok = False
|
||||||
|
else:
|
||||||
|
log.error('(%s) SSL certfile/keyfile was not set '
|
||||||
|
'correctly, aborting... ', self.name)
|
||||||
|
checks_ok = False
|
||||||
|
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
|
||||||
|
self.socket.connect((ip, port))
|
||||||
|
self.socket.settimeout(self.pingtimeout)
|
||||||
|
|
||||||
|
if self.ssl and checks_ok:
|
||||||
|
peercert = self.socket.getpeercert(binary_form=True)
|
||||||
|
sha1fp = hashlib.sha1(peercert).hexdigest()
|
||||||
|
expected_fp = self.serverdata.get('ssl_fingerprint')
|
||||||
|
if expected_fp:
|
||||||
|
if sha1fp != expected_fp:
|
||||||
|
log.error('(%s) Uplink\'s SSL certificate '
|
||||||
|
'fingerprint (SHA1) does not match the '
|
||||||
|
'one configured: expected %r, got %r; '
|
||||||
|
'disconnecting...', self.name,
|
||||||
|
expected_fp, sha1fp)
|
||||||
|
checks_ok = False
|
||||||
|
else:
|
||||||
|
log.info('(%s) Uplink SSL certificate fingerprint '
|
||||||
|
'(SHA1) verified: %r', self.name, sha1fp)
|
||||||
|
else:
|
||||||
|
log.info('(%s) Uplink\'s SSL certificate fingerprint '
|
||||||
|
'is %r. You can enhance the security of your '
|
||||||
|
'link by specifying this in a "ssl_fingerprint"'
|
||||||
|
' option in your server block.', self.name,
|
||||||
|
sha1fp)
|
||||||
|
|
||||||
|
if checks_ok:
|
||||||
|
self.proto.connect(self)
|
||||||
|
self.spawnMain()
|
||||||
|
log.info('(%s) Starting ping schedulers....', self.name)
|
||||||
|
self.schedulePing()
|
||||||
|
log.info('(%s) Server ready; listening for data.', self.name)
|
||||||
|
self.run()
|
||||||
|
else:
|
||||||
|
log.error('(%s) A configuration error was encountered '
|
||||||
|
'trying to set up this connection. Please check'
|
||||||
|
' your configuration file and try again.',
|
||||||
|
self.name)
|
||||||
|
except (socket.error, ProtocolError, ConnectionError) as e:
|
||||||
|
log.warning('(%s) Disconnected from IRC: %s: %s',
|
||||||
|
self.name, type(e).__name__, str(e))
|
||||||
|
self.disconnect()
|
||||||
|
autoconnect = self.serverdata.get('autoconnect')
|
||||||
|
log.debug('(%s) Autoconnect delay set to %s seconds.', self.name, autoconnect)
|
||||||
|
if autoconnect is not None and autoconnect >= 0:
|
||||||
|
log.info('(%s) Going to auto-reconnect in %s seconds.', self.name, autoconnect)
|
||||||
|
time.sleep(autoconnect)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
log.debug('(%s) Canceling pingTimer at %s due to disconnect() call', self.name, time.time())
|
||||||
|
self.connected.clear()
|
||||||
|
try:
|
||||||
|
self.socket.close()
|
||||||
|
self.pingTimer.cancel()
|
||||||
|
except: # Socket timed out during creation; ignore
|
||||||
|
pass
|
||||||
|
# Internal hook signifying that a network has disconnected.
|
||||||
|
self.callHooks([None, 'PYLINK_DISCONNECT', {}])
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
buf = b""
|
||||||
|
data = b""
|
||||||
|
while True:
|
||||||
|
data = self.socket.recv(2048)
|
||||||
|
buf += data
|
||||||
|
if self.connected.is_set() and not data:
|
||||||
|
log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name)
|
||||||
|
return
|
||||||
|
elif (time.time() - self.lastping) > self.pingtimeout:
|
||||||
|
log.warning('(%s) Connection timed out.', self.name)
|
||||||
|
return
|
||||||
|
while b'\n' in buf:
|
||||||
|
line, buf = buf.split(b'\n', 1)
|
||||||
|
line = line.strip(b'\r')
|
||||||
|
# TODO: respect other encodings?
|
||||||
|
line = line.decode("utf-8", "replace")
|
||||||
|
log.debug("(%s) <- %s", self.name, line)
|
||||||
|
hook_args = None
|
||||||
|
try:
|
||||||
|
hook_args = self.proto.handle_events(self, line)
|
||||||
|
except Exception:
|
||||||
|
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
|
||||||
|
return
|
||||||
|
# Only call our hooks if there's data to process. Handlers that support
|
||||||
|
# hooks will return a dict of parsed arguments, which can be passed on
|
||||||
|
# to plugins and the like. For example, the JOIN handler will return
|
||||||
|
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
|
||||||
|
# 'UID3']}, etc.
|
||||||
|
if hook_args is not None:
|
||||||
|
self.callHooks(hook_args)
|
||||||
|
|
||||||
|
def callHooks(self, hook_args):
|
||||||
|
numeric, command, parsed_args = hook_args
|
||||||
|
# Always make sure TS is sent.
|
||||||
|
if 'ts' not in parsed_args:
|
||||||
|
parsed_args['ts'] = int(time.time())
|
||||||
|
hook_cmd = command
|
||||||
|
hook_map = self.proto.hook_map
|
||||||
|
# Handlers can return a 'parse_as' key to send their payload to a
|
||||||
|
# different hook. An example of this is "/join 0" being interpreted
|
||||||
|
# as leaving all channels (PART).
|
||||||
|
if command in hook_map:
|
||||||
|
hook_cmd = hook_map[command]
|
||||||
|
hook_cmd = parsed_args.get('parse_as') or hook_cmd
|
||||||
|
log.debug('Parsed args %r received from %s handler (calling hook %s)', parsed_args, command, hook_cmd)
|
||||||
|
# Iterate over hooked functions, catching errors accordingly
|
||||||
|
for hook_func in utils.command_hooks[hook_cmd]:
|
||||||
|
try:
|
||||||
|
log.debug('Calling function %s', hook_func)
|
||||||
|
hook_func(self, numeric, command, parsed_args)
|
||||||
|
except Exception:
|
||||||
|
# We don't want plugins to crash our servers...
|
||||||
|
log.exception('Unhandled exception caught in %r' % hook_func)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
# Safeguard against newlines in input!! Otherwise, each line gets
|
||||||
|
# treated as a separate command, which is particularly nasty.
|
||||||
|
data = data.replace('\n', ' ')
|
||||||
|
data = data.encode("utf-8") + b"\n"
|
||||||
|
stripped_data = data.decode("utf-8").strip("\n")
|
||||||
|
log.debug("(%s) -> %s", self.name, stripped_data)
|
||||||
|
try:
|
||||||
|
self.socket.send(data)
|
||||||
|
except (OSError, AttributeError):
|
||||||
|
log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data)
|
||||||
|
|
||||||
|
def schedulePing(self):
|
||||||
|
self.proto.pingServer(self)
|
||||||
|
self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing)
|
||||||
|
self.pingTimer.daemon = True
|
||||||
|
self.pingTimer.start()
|
||||||
|
log.debug('(%s) Ping scheduled at %s', self.name, time.time())
|
||||||
|
|
||||||
|
def spawnMain(self):
|
||||||
|
nick = self.botdata.get('nick') or 'PyLink'
|
||||||
|
ident = self.botdata.get('ident') or 'pylink'
|
||||||
|
host = self.serverdata["hostname"]
|
||||||
|
log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
|
||||||
|
olduserobj = self.pseudoclient
|
||||||
|
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)})
|
||||||
|
for chan in self.serverdata['channels']:
|
||||||
|
self.proto.joinClient(self, self.pseudoclient.uid, chan)
|
||||||
|
# PyLink internal hook called when spawnMain is called and the
|
||||||
|
# contents of Irc().pseudoclient change.
|
||||||
|
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
|
||||||
|
|
||||||
class IrcUser():
|
class IrcUser():
|
||||||
def __init__(self, nick, ts, uid, ident='null', host='null',
|
def __init__(self, nick, ts, uid, ident='null', host='null',
|
||||||
@ -60,8 +312,7 @@ class IrcChannel():
|
|||||||
s.discard(target)
|
s.discard(target)
|
||||||
self.users.discard(target)
|
self.users.discard(target)
|
||||||
|
|
||||||
class ProtocolError(Exception):
|
### FakeIRC classes, used for test cases
|
||||||
pass
|
|
||||||
|
|
||||||
global testconf
|
global testconf
|
||||||
testconf = {'bot':
|
testconf = {'bot':
|
||||||
@ -86,7 +337,7 @@ testconf = {'bot':
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeIRC(main.Irc):
|
class FakeIRC(Irc):
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.messages = []
|
self.messages = []
|
||||||
self.hookargs = []
|
self.hookargs = []
|
||||||
|
246
main.py
246
main.py
@ -2,13 +2,7 @@
|
|||||||
|
|
||||||
import imp
|
import imp
|
||||||
import os
|
import os
|
||||||
import socket
|
|
||||||
import time
|
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
|
||||||
import threading
|
|
||||||
import ssl
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from log import log
|
from log import log
|
||||||
import conf
|
import conf
|
||||||
@ -16,244 +10,6 @@ import classes
|
|||||||
import utils
|
import utils
|
||||||
import coreplugin
|
import coreplugin
|
||||||
|
|
||||||
class Irc():
|
|
||||||
|
|
||||||
def initVars(self):
|
|
||||||
self.pseudoclient = None
|
|
||||||
self.connected = threading.Event()
|
|
||||||
self.lastping = time.time()
|
|
||||||
# Server, channel, and user indexes to be populated by our protocol module
|
|
||||||
self.servers = {self.sid: classes.IrcServer(None, self.serverdata['hostname'], internal=True)}
|
|
||||||
self.users = {}
|
|
||||||
self.channels = defaultdict(classes.IrcChannel)
|
|
||||||
# Sets flags such as whether to use halfops, etc. The default RFC1459
|
|
||||||
# modes are implied.
|
|
||||||
self.cmodes = {'op': 'o', 'secret': 's', 'private': 'p',
|
|
||||||
'noextmsg': 'n', 'moderated': 'm', 'inviteonly': 'i',
|
|
||||||
'topiclock': 't', 'limit': 'l', 'ban': 'b',
|
|
||||||
'voice': 'v', 'key': 'k',
|
|
||||||
# Type A, B, and C modes
|
|
||||||
'*A': 'b',
|
|
||||||
'*B': 'k',
|
|
||||||
'*C': 'l',
|
|
||||||
'*D': 'imnpstr'}
|
|
||||||
self.umodes = {'invisible': 'i', 'snomask': 's', 'wallops': 'w',
|
|
||||||
'oper': 'o',
|
|
||||||
'*A': '', '*B': '', '*C': 's', '*D': 'iow'}
|
|
||||||
|
|
||||||
# This max nick length starts off as the config value, but may be
|
|
||||||
# overwritten later by the protocol module if such information is
|
|
||||||
# received. Note that only some IRCds (InspIRCd) give us nick length
|
|
||||||
# during link, so it is still required that the config value be set!
|
|
||||||
self.maxnicklen = self.serverdata['maxnicklen']
|
|
||||||
self.prefixmodes = {'o': '@', 'v': '+'}
|
|
||||||
|
|
||||||
# Uplink SID (filled in by protocol module)
|
|
||||||
self.uplink = None
|
|
||||||
self.start_ts = int(time.time())
|
|
||||||
|
|
||||||
# UID generators, for servers that need it
|
|
||||||
self.uidgen = {}
|
|
||||||
|
|
||||||
def __init__(self, netname, proto, conf):
|
|
||||||
# Initialize some variables
|
|
||||||
self.name = netname.lower()
|
|
||||||
self.conf = conf
|
|
||||||
self.serverdata = conf['servers'][netname]
|
|
||||||
self.sid = self.serverdata["sid"]
|
|
||||||
self.botdata = conf['bot']
|
|
||||||
self.proto = proto
|
|
||||||
self.pingfreq = self.serverdata.get('pingfreq') or 30
|
|
||||||
self.pingtimeout = self.pingfreq * 2
|
|
||||||
|
|
||||||
self.initVars()
|
|
||||||
|
|
||||||
self.connection_thread = threading.Thread(target = self.connect)
|
|
||||||
self.connection_thread.start()
|
|
||||||
self.pingTimer = None
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
ip = self.serverdata["ip"]
|
|
||||||
port = self.serverdata["port"]
|
|
||||||
while True:
|
|
||||||
self.initVars()
|
|
||||||
checks_ok = True
|
|
||||||
try:
|
|
||||||
self.socket = socket.socket()
|
|
||||||
self.socket.setblocking(0)
|
|
||||||
# Initial connection timeout is a lot smaller than the timeout after
|
|
||||||
# we've connected; this is intentional.
|
|
||||||
self.socket.settimeout(self.pingfreq)
|
|
||||||
self.ssl = self.serverdata.get('ssl')
|
|
||||||
if self.ssl:
|
|
||||||
log.info('(%s) Attempting SSL for this connection...', self.name)
|
|
||||||
certfile = self.serverdata.get('ssl_certfile')
|
|
||||||
keyfile = self.serverdata.get('ssl_keyfile')
|
|
||||||
if certfile and keyfile:
|
|
||||||
try:
|
|
||||||
self.socket = ssl.wrap_socket(self.socket,
|
|
||||||
certfile=certfile,
|
|
||||||
keyfile=keyfile)
|
|
||||||
except OSError:
|
|
||||||
log.exception('(%s) Caught OSError trying to '
|
|
||||||
'initialize the SSL connection; '
|
|
||||||
'are "ssl_certfile" and '
|
|
||||||
'"ssl_keyfile" set correctly?',
|
|
||||||
self.name)
|
|
||||||
checks_ok = False
|
|
||||||
else:
|
|
||||||
log.error('(%s) SSL certfile/keyfile was not set '
|
|
||||||
'correctly, aborting... ', self.name)
|
|
||||||
checks_ok = False
|
|
||||||
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
|
|
||||||
self.socket.connect((ip, port))
|
|
||||||
self.socket.settimeout(self.pingtimeout)
|
|
||||||
|
|
||||||
if self.ssl and checks_ok:
|
|
||||||
peercert = self.socket.getpeercert(binary_form=True)
|
|
||||||
sha1fp = hashlib.sha1(peercert).hexdigest()
|
|
||||||
expected_fp = self.serverdata.get('ssl_fingerprint')
|
|
||||||
if expected_fp:
|
|
||||||
if sha1fp != expected_fp:
|
|
||||||
log.error('(%s) Uplink\'s SSL certificate '
|
|
||||||
'fingerprint (SHA1) does not match the '
|
|
||||||
'one configured: expected %r, got %r; '
|
|
||||||
'disconnecting...', self.name,
|
|
||||||
expected_fp, sha1fp)
|
|
||||||
checks_ok = False
|
|
||||||
else:
|
|
||||||
log.info('(%s) Uplink SSL certificate fingerprint '
|
|
||||||
'(SHA1) verified: %r', self.name, sha1fp)
|
|
||||||
else:
|
|
||||||
log.info('(%s) Uplink\'s SSL certificate fingerprint '
|
|
||||||
'is %r. You can enhance the security of your '
|
|
||||||
'link by specifying this in a "ssl_fingerprint"'
|
|
||||||
' option in your server block.', self.name,
|
|
||||||
sha1fp)
|
|
||||||
|
|
||||||
if checks_ok:
|
|
||||||
self.proto.connect(self)
|
|
||||||
self.spawnMain()
|
|
||||||
log.info('(%s) Starting ping schedulers....', self.name)
|
|
||||||
self.schedulePing()
|
|
||||||
log.info('(%s) Server ready; listening for data.', self.name)
|
|
||||||
self.run()
|
|
||||||
else:
|
|
||||||
log.error('(%s) A configuration error was encountered '
|
|
||||||
'trying to set up this connection. Please check'
|
|
||||||
' your configuration file and try again.',
|
|
||||||
self.name)
|
|
||||||
except (socket.error, classes.ProtocolError, ConnectionError) as e:
|
|
||||||
log.warning('(%s) Disconnected from IRC: %s: %s',
|
|
||||||
self.name, type(e).__name__, str(e))
|
|
||||||
self.disconnect()
|
|
||||||
autoconnect = self.serverdata.get('autoconnect')
|
|
||||||
log.debug('(%s) Autoconnect delay set to %s seconds.', self.name, autoconnect)
|
|
||||||
if autoconnect is not None and autoconnect >= 0:
|
|
||||||
log.info('(%s) Going to auto-reconnect in %s seconds.', self.name, autoconnect)
|
|
||||||
time.sleep(autoconnect)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
log.debug('(%s) Canceling pingTimer at %s due to disconnect() call', self.name, time.time())
|
|
||||||
self.connected.clear()
|
|
||||||
try:
|
|
||||||
self.socket.close()
|
|
||||||
self.pingTimer.cancel()
|
|
||||||
except: # Socket timed out during creation; ignore
|
|
||||||
pass
|
|
||||||
# Internal hook signifying that a network has disconnected.
|
|
||||||
self.callHooks([None, 'PYLINK_DISCONNECT', {}])
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
buf = b""
|
|
||||||
data = b""
|
|
||||||
while True:
|
|
||||||
data = self.socket.recv(2048)
|
|
||||||
buf += data
|
|
||||||
if self.connected.is_set() and not data:
|
|
||||||
log.warning('(%s) No data received and self.connected is set; disconnecting!', self.name)
|
|
||||||
return
|
|
||||||
elif (time.time() - self.lastping) > self.pingtimeout:
|
|
||||||
log.warning('(%s) Connection timed out.', self.name)
|
|
||||||
return
|
|
||||||
while b'\n' in buf:
|
|
||||||
line, buf = buf.split(b'\n', 1)
|
|
||||||
line = line.strip(b'\r')
|
|
||||||
# TODO: respect other encodings?
|
|
||||||
line = line.decode("utf-8", "replace")
|
|
||||||
log.debug("(%s) <- %s", self.name, line)
|
|
||||||
hook_args = None
|
|
||||||
try:
|
|
||||||
hook_args = self.proto.handle_events(self, line)
|
|
||||||
except Exception:
|
|
||||||
log.exception('(%s) Caught error in handle_events, disconnecting!', self.name)
|
|
||||||
return
|
|
||||||
# Only call our hooks if there's data to process. Handlers that support
|
|
||||||
# hooks will return a dict of parsed arguments, which can be passed on
|
|
||||||
# to plugins and the like. For example, the JOIN handler will return
|
|
||||||
# something like: {'channel': '#whatever', 'users': ['UID1', 'UID2',
|
|
||||||
# 'UID3']}, etc.
|
|
||||||
if hook_args is not None:
|
|
||||||
self.callHooks(hook_args)
|
|
||||||
|
|
||||||
def callHooks(self, hook_args):
|
|
||||||
numeric, command, parsed_args = hook_args
|
|
||||||
# Always make sure TS is sent.
|
|
||||||
if 'ts' not in parsed_args:
|
|
||||||
parsed_args['ts'] = int(time.time())
|
|
||||||
hook_cmd = command
|
|
||||||
hook_map = self.proto.hook_map
|
|
||||||
# Handlers can return a 'parse_as' key to send their payload to a
|
|
||||||
# different hook. An example of this is "/join 0" being interpreted
|
|
||||||
# as leaving all channels (PART).
|
|
||||||
if command in hook_map:
|
|
||||||
hook_cmd = hook_map[command]
|
|
||||||
hook_cmd = parsed_args.get('parse_as') or hook_cmd
|
|
||||||
log.debug('Parsed args %r received from %s handler (calling hook %s)', parsed_args, command, hook_cmd)
|
|
||||||
# Iterate over hooked functions, catching errors accordingly
|
|
||||||
for hook_func in utils.command_hooks[hook_cmd]:
|
|
||||||
try:
|
|
||||||
log.debug('Calling function %s', hook_func)
|
|
||||||
hook_func(self, numeric, command, parsed_args)
|
|
||||||
except Exception:
|
|
||||||
# We don't want plugins to crash our servers...
|
|
||||||
log.exception('Unhandled exception caught in %r' % hook_func)
|
|
||||||
continue
|
|
||||||
|
|
||||||
def send(self, data):
|
|
||||||
# Safeguard against newlines in input!! Otherwise, each line gets
|
|
||||||
# treated as a separate command, which is particularly nasty.
|
|
||||||
data = data.replace('\n', ' ')
|
|
||||||
data = data.encode("utf-8") + b"\n"
|
|
||||||
stripped_data = data.decode("utf-8").strip("\n")
|
|
||||||
log.debug("(%s) -> %s", self.name, stripped_data)
|
|
||||||
try:
|
|
||||||
self.socket.send(data)
|
|
||||||
except (OSError, AttributeError):
|
|
||||||
log.debug("(%s) Dropping message %r; network isn't connected!", self.name, stripped_data)
|
|
||||||
|
|
||||||
def schedulePing(self):
|
|
||||||
self.proto.pingServer(self)
|
|
||||||
self.pingTimer = threading.Timer(self.pingfreq, self.schedulePing)
|
|
||||||
self.pingTimer.daemon = True
|
|
||||||
self.pingTimer.start()
|
|
||||||
log.debug('(%s) Ping scheduled at %s', self.name, time.time())
|
|
||||||
|
|
||||||
def spawnMain(self):
|
|
||||||
nick = self.botdata.get('nick') or 'PyLink'
|
|
||||||
ident = self.botdata.get('ident') or 'pylink'
|
|
||||||
host = self.serverdata["hostname"]
|
|
||||||
log.info('(%s) Connected! Spawning main client %s.', self.name, nick)
|
|
||||||
olduserobj = self.pseudoclient
|
|
||||||
self.pseudoclient = self.proto.spawnClient(self, nick, ident, host, modes={("+o", None)})
|
|
||||||
for chan in self.serverdata['channels']:
|
|
||||||
self.proto.joinClient(self, self.pseudoclient.uid, chan)
|
|
||||||
# PyLink internal hook called when spawnMain is called and the
|
|
||||||
# contents of Irc().pseudoclient change.
|
|
||||||
self.callHooks([self.sid, 'PYLINK_SPAWNMAIN', {'olduser': olduserobj}])
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
log.info('PyLink starting...')
|
log.info('PyLink starting...')
|
||||||
if conf.conf['login']['password'] == 'changeme':
|
if conf.conf['login']['password'] == 'changeme':
|
||||||
@ -299,7 +55,7 @@ if __name__ == '__main__':
|
|||||||
log.critical('Failed to load protocol module: ImportError: %s', protoname, str(e))
|
log.critical('Failed to load protocol module: ImportError: %s', protoname, str(e))
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
else:
|
else:
|
||||||
utils.networkobjects[network] = Irc(network, proto, conf.conf)
|
utils.networkobjects[network] = classes.Irc(network, proto, conf.conf)
|
||||||
utils.started.set()
|
utils.started.set()
|
||||||
log.info("loaded plugins: %s", utils.plugins)
|
log.info("loaded plugins: %s", utils.plugins)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user