diff --git a/config.yml.example b/config.yml.example index 38a7634..0b52fdf 100644 --- a/config.yml.example +++ b/config.yml.example @@ -3,6 +3,7 @@ bot: nick: pylink user: pylink realname: PyLink Service Client + prefix: "@" login: # PyLink administrative login - Change this, or the service will not start! @@ -21,6 +22,8 @@ server: # SID - required for InspIRCd and TS6 based servers. This must be three characters long. # The first char must be a digit [0-9], and the remaining two chars may be letters [A-Z] or digits. sid: "0AL" - # Set protocol module - protocol: inspircd channel: "#pylink" + +# Plugins to load (omit the .py extension) +plugins: + - hello diff --git a/main.py b/main.py index be72442..d651562 100755 --- a/main.py +++ b/main.py @@ -7,6 +7,9 @@ import threading import socket import multiprocessing import time +import sys + +import proto print('PyLink starting...') with open("config.yml", 'r') as f: @@ -15,19 +18,6 @@ with open("config.yml", 'r') as f: # if conf['login']['password'] == 'changeme': # print("You have not set the login details correctly! Exiting...") -class IrcUser(): - def __init__(self, nick, ts, uid, ident='null', host='null', - realname='PyLink dummy client', realhost='null', - ip='0.0.0.0'): - self.nick = nick - self.ts = ts - self.uid = uid - self.ident = ident - self.host = host - self.realhost = realhost - self.ip = ip - self.realname = realname - class Irc(): def __init__(self): # Initialize some variables @@ -36,6 +26,7 @@ class Irc(): self.users = {} self.channels = {} self.name = conf['server']['netname'] + self.conf = conf self.serverdata = conf['server'] ip = self.serverdata["ip"] @@ -43,23 +34,12 @@ class Irc(): self.sid = self.serverdata["sid"] print("Connecting to network %r on %s:%s" % (self.name, ip, port)) - protoname = self.serverdata['protocol'] - # With the introduction of Python 3, relative imports are no longer - # allowed from normal applications ran from the command line. Instead, - # these imported libraries must be installed as a package using distutils - # or something similar. - # - # But I don't want that! Where PyLink is at right now (a total WIP), it is - # a lot more convenient to run the program directly from the source folder. - protocols_folder = [os.path.join(os.getcwd(), 'protocols')] - # Here, we override the module lookup and import the protocol module - # dynamically depending on which module was configured. - moduleinfo = imp.find_module(protoname, protocols_folder) - self.proto = imp.load_source(protoname, moduleinfo[1]) self.socket = socket.socket() self.socket.connect((ip, port)) - self.proto.connect(self) + proto.connect(self) self.connected = True + self.loaded = [] + self.load_plugins() self.run() def run(self): @@ -67,22 +47,33 @@ class Irc(): data = "" while self.connected: try: - data += self.socket.recv(1024).decode("utf-8") + data = self.socket.recv(4096).decode("utf-8") + buf += data if not data: break - buf += data while '\n' in buf: line, buf = buf.split('\n', 1) print("<- {}".format(line)) - self.proto.handle_events(self, line) - except socket.error: + proto.handle_events(self, line) + except socket.error as e: print('Received socket.error: %s, exiting.' % str(e)) - self.connected = False - sys.exit(1) + break + sys.exit(1) def send(self, data): data = data.encode("utf-8") + b"\n" print("-> {}".format(data.decode("utf-8").strip("\n"))) self.socket.send(data) + def load_plugins(self): + to_load = conf['plugins'] + plugins_folder = [os.path.join(os.getcwd(), 'plugins')] + # Here, we override the module lookup and import the plugins + # dynamically depending on which were configured. + for plugin in to_load: + moduleinfo = imp.find_module(plugin, plugins_folder) + self.loaded.append(imp.load_source(plugin, moduleinfo[1])) + print("loaded plugins: %s" % self.loaded) + + irc_obj = Irc() diff --git a/plugins/hello.py b/plugins/hello.py new file mode 100644 index 0000000..982796f --- /dev/null +++ b/plugins/hello.py @@ -0,0 +1,7 @@ +import sys, os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import proto + +@proto.add_cmd +def hello(irc, source, command, args): + proto._sendFromUser(irc, 'PRIVMSG %s :hello!' % source) diff --git a/protocols/inspircd.py b/proto.py similarity index 74% rename from protocols/inspircd.py rename to proto.py index 424e988..85312c2 100644 --- a/protocols/inspircd.py +++ b/proto.py @@ -2,26 +2,29 @@ import threading import socket import time import re -import string import sys +from utils import * -# TODO: make PyLink a package so I don't have to hack the import system -# like this. -from os import sys, path -sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) -from main import IrcUser +global bot_commands +# This should be a mapping of command names to functions +bot_commands = {} -# From http://www.inspircd.org/wiki/Modules/spanningtree/UUIDs.html -chars = string.digits + string.ascii_uppercase -iters = [iter(chars) for _ in range(6)] -a = [next(i) for i in iters] +class IrcUser(): + def __init__(self, nick, ts, uid, ident='null', host='null', + realname='PyLink dummy client', realhost='null', + ip='0.0.0.0'): + self.nick = nick + self.ts = ts + self.uid = uid + self.ident = ident + self.host = host + self.realhost = realhost + self.ip = ip + self.realname = realname -def next_uid(sid, level=-1): - try: - a[level] = next(iters[level]) - return sid + ''.join(a) - except StopIteration: - return UID(level-1) + def __repr__(self): + keys = [k for k in dir(self) if not k.startswith("__")] + return ','.join(["%s=%s" % (k, getattr(self, k)) for k in keys]) def _sendFromServer(irc, msg): irc.send(':%s %s' % (irc.sid, msg)) @@ -32,7 +35,7 @@ def _sendFromUser(irc, msg, user=None): irc.send(':%s %s' % (user, msg)) def _join(irc, channel): - _sendFromUser(irc, "FJOIN {channel} {ts} +nt :,{uid}".format(sid=irc.sid, + _sendFromUser(irc, "JOIN {channel} {ts} +nt :,{uid}".format(sid=irc.sid, ts=int(time.time()), uid=irc.pseudoclient.uid, channel=channel)) def _uidToNick(irc, uid): @@ -47,7 +50,7 @@ def connect(irc): irc.pseudoclient = IrcUser('PyLink', ts, uid, 'pylink', host, 'PyLink Client') irc.users['PyLink'] = irc.pseudoclient - + f = irc.send f('CAPAB START 1203') # This is hard coded atm... We should fix it eventually... @@ -71,13 +74,24 @@ def connect(irc): # :7NU PING 7NU 0AL def handle_ping(irc, servernumeric, command, args): - if args[3] == irc.sid: - _sendFromServer(irc, 'PONG %s' % args[2]) + if args[1] == irc.sid: + _sendFromServer(irc, 'PONG %s' % args[1]) -def handle_privmsg(irc, numeric, command, args): +def handle_privmsg(irc, source, command, args): # _sendFromUser(irc, 'PRIVMSG %s :hello!' % numeric) print(irc.users) - print(irc.channels) + prefix = irc.conf['bot']['prefix'] + if args[0] == irc.pseudoclient.uid: + cmd_args = args[1].split(' ', 1) + cmd = cmd_args[0] + try: + cmd_args = cmd_args[1] + except IndexError: + cmd_args = [] + try: + bot_commands[cmd](irc, source, command, args) + except KeyError: + _sendFromUser(irc, 'PRIVMSG %s :unknown command %r' % (source, cmd)) def handle_error(irc, numeric, command, args): print('Received an ERROR, killing!') @@ -89,11 +103,9 @@ def handle_fjoin(irc, servernumeric, command, args): channel = args[0] # tl;dr InspIRCd sends each user's channel data in the form of 'modeprefix(es),UID' # We'll save each user in this format too, at least for now. - print(args) users = args[-1].split() users = [x.split(',') for x in users] - print(users) - + ''' if channel not in irc.channels.keys(): irc.channels[channel]['users'] = users @@ -103,7 +115,7 @@ def handle_fjoin(irc, servernumeric, command, args): ''' def handle_uid(irc, numeric, command, args): - # :1SR UID 1SRAAAAAU 1428974823 synnero ow.my.eye.rs ow.my.eye.rs GLolol 2604:180:1::d34d:d87b 1425951245 +Wiosw +ACGKNOQXacfgklnoqvx :move along, nothing to see here! + # :70M UID 70MAAAAAB 1429934638 GL 0::1 hidden-7j810p.9mdf.lrek.0000.0000.IP gl 0::1 1429934638 +Wioswx +ACGKNOQXacfgklnoqvx :realname uid, ts, nick, realhost, host, ident, ip = args[0:7] realname = args[-1] irc.users[nick] = IrcUser(nick, ts, uid, ident, host, realname, realhost, ip) @@ -112,11 +124,13 @@ def handle_quit(irc, numeric, command, args): # :1SRAAGB4T QUIT :Quit: quit message goes here nick = _uidToNick(irc, numeric) del irc.users[nick] + ''' for k, v in irc.channels.items(): try: del irc.channels[k][users][v] except KeyError: pass + ''' def handle_events(irc, data): # Each server message looks something like this: @@ -146,6 +160,8 @@ def handle_events(irc, data): numeric = args[0] command = args[1] + args = args[2:] + print(args) except IndexError: return @@ -155,3 +171,6 @@ def handle_events(irc, data): func(irc, numeric, command, args) except KeyError: # unhandled event pass + +def add_cmd(func): + bot_commands[func.__name__.lower()] = func diff --git a/protocols/stub.py b/protocols/stub.py deleted file mode 100644 index 017b9d8..0000000 --- a/protocols/stub.py +++ /dev/null @@ -1,8 +0,0 @@ -def connect(irc): - print('%s: Using PyLink stub/testing protocol.' % irc.name) - print('Send password: %s' % irc.serverdata['sendpass']) - print('Receive password: %s' % irc.serverdata['recvpass']) - print('Server: %s:%s' % (irc.serverdata["ip"], irc.serverdata["port"])) - -def handle_events(irc, data): - print('%s: Received event: %s' % (irc.name, data)) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..f0ace13 --- /dev/null +++ b/utils.py @@ -0,0 +1,13 @@ +import string + +# From http://www.inspircd.org/wiki/Modules/spanningtree/UUIDs.html +chars = string.digits + string.ascii_uppercase +iters = [iter(chars) for _ in range(6)] +a = [next(i) for i in iters] + +def next_uid(sid, level=-1): + try: + a[level] = next(iters[level]) + return sid + ''.join(a) + except StopIteration: + return UID(level-1)