diff --git a/bot.py b/bot.py index 2c4e58c..30af57e 100644 --- a/bot.py +++ b/bot.py @@ -20,26 +20,32 @@ from datetime import datetime from queue import Queue import threading +from botcaps import BotCaps from consumer import Consumer import event -import hooks +from networkbot import NetworkBot from server import Server -class Bot: +class Bot(BotCaps): def __init__(self, servers=dict(), modules=dict(), mp=list()): - self.version = 3.2 - self.version_txt = "3.2" + BotCaps.__init__(self, 3.2, "3.2-dev") + # Keep global context: servers and modules self.servers = servers self.modules = modules + # Context paths self.modules_path = mp self.datas_path = './datas/' - self.hooks = hooks.MessagesHook() + # Events self.events = list() self.event_timer = None + # Other known bots, making a bots network + self.network = dict() + + # Messages to be treated self.msg_queue = Queue() self.msg_thrd = list() self.msg_thrd_size = -1 @@ -74,7 +80,6 @@ class Bot: #else: # print ("Update timer: no timer left") - def end_timer(self): """Function called at the end of the timer""" #print ("end timer") @@ -149,6 +154,14 @@ class Bot: self.msg_thrd_size += 2 + def add_networkbot(self, srv, dest, dcc=None): + """Append a new bot into the network""" + id = srv.id + "/" + dest + if id not in self.network: + self.network[id] = NetworkBot(self, srv, dest, dcc) + return self.network[id] + + def quit(self, verb=False): """Save and unload modules and disconnect servers""" if self.event_timer is not None: diff --git a/botcaps.py b/botcaps.py new file mode 100644 index 0000000..624dd3c --- /dev/null +++ b/botcaps.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a modulable IRC bot, built around XML configuration files. +# Copyright (C) 2012 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import hooks + +class BotCaps: + def __init__(self, version=None, version_txt=None): + # Bot general informations + self.version = version + self.version_txt = version_txt + + # Hooks + self.hooks = hooks.MessagesHook() diff --git a/modules/cmd_server.py b/modules/cmd_server.py index 6609240..354e128 100644 --- a/modules/cmd_server.py +++ b/modules/cmd_server.py @@ -16,9 +16,20 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from networkbot import NetworkBot + nemubotversion = 3.2 NODATA = True +def getserver(toks, context, prompt): + """Choose the server in toks or prompt""" + if len(toks) > 1 and toks[0] in context.servers: + return (context.servers[toks[0]], toks[1:]) + elif prompt.selectedServer is not None: + return (prompt.selectedServer, toks) + else: + return (None, toks) + def close(data, toks, context, prompt): """Disconnect and forget (remove from the servers list) the server""" if len(toks) > 1: @@ -64,6 +75,19 @@ def disconnect(data, toks, context, prompt): else: print (" Please SELECT a server or give its name in argument.") +def discover(data, toks, context, prompt): + """Discover a new bot on a server""" + (srv, toks) = getserver(toks, context, prompt) + if srv is not None: + for name in toks[1:]: + if "!" in name: + bot = context.add_networkbot(srv, name) + bot.connect() + else: + print (" %s is not a valid fullname, for example: nemubot!nemubotV3@bot.nemunai.re") + else: + print (" Please SELECT a server or give its name in first argument.") + def hotswap(data, toks, context, prompt): """Reload a server class""" if len(toks) > 1: diff --git a/modules/cmd_server.xml b/modules/cmd_server.xml index 62c16de..7ce23e8 100644 --- a/modules/cmd_server.xml +++ b/modules/cmd_server.xml @@ -2,6 +2,7 @@ + diff --git a/networkbot.py b/networkbot.py new file mode 100644 index 0000000..0c32ac4 --- /dev/null +++ b/networkbot.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +# Nemubot is a modulable IRC bot, built around XML configuration files. +# Copyright (C) 2012 Mercier Pierre-Olivier +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import random +import shlex + +from botcaps import BotCaps +from DCC import DCC + +class NetworkBot: + def __init__(self, context, srv, dest, dcc=None): + self.context = context + self.srv = srv + self.dcc = dcc + self.dest = dest + self.infos = None + + self.my_tag = random.randint(0,255) + self.inc_tag = 0 + self.tags = dict() + + @property + def sender(self): + if self.dcc is not None: + return self.dcc.sender + return None + + @property + def nick(self): + if self.dcc is not None: + return self.dcc.nick + return None + + @property + def realname(self): + if self.dcc is not None: + return self.dcc.realname + return None + + def send_cmd(self, cmd, data=None): + """Create a tag and send the command""" + # First, define a tag + self.inc_tag = (self.inc_out + 1) % 256 + while self.inc_tag in self.tags: + self.inc_tag = (self.inc_out + 1) % 256 + tag = ("%c%c" % (self.my_tag, self.inc_tag)).encode() + + if data is not None: + self.tags[tag] = data + else: + self.tags[tag] = cmd + + # Send the command with the tag + self.send_response(tag, cmd) + + def send_response(self, tag, msg): + """Send a response with a tag""" + for line in msg.split("\n"): + self.dcc.send_dcc_raw(tag + b' ' + line.encode()) + + def send_ack(self, tag): + """Acknowledge a command""" + if tag in self.tags: + del self.tags[tag] + self.send_response(tag, "ACK") + + def connect(self): + """Making the connexion with dest through srv""" + if self.dcc is None: + self.dcc = DCC(self.srv, self.dest) + self.dcc.treatement = self.hello + self.dcc.send_dcc("NEMUBOT###") + + def disconnect(self, reason=""): + """Close the connection and remove the bot from network list""" + del self.context.network[self.dcc.id] + self.dcc.send_dcc("DISCONNECT :%s" % reason) + self.dcc.disconnect() + + def hello(self, line): + if line == b'NEMUBOT###': + self.dcc.treatement = self.treat_msg + self.send_cmd("MYTAG %c" % self.my_tag) + self.send_cmd("FETCH") + elif line != b'Hello ' + self.srv.nick.encode() + b'!': + self.disconnect("Sorry, I think you were a bot") + + def treat_msg(self, line, cmd=None): + print (line) + words = line.split(b' ') + + # Ignore invalid commands + if len(words) >= 2: + tag = words[0] + cmd = words[1] + if len(words) > 2: + args = shlex.split(line[len(tag) + len(cmd) + 2:].decode()) + else: + args = list() + + # Parse + if cmd == b'ACK': + if tag in self.tags: + del self.tags[tag] + + elif cmd == b'MYTAG' and len(args) > 0: + while args[0] == self.my_tag: + self.my_tag = random.randint(0,255) + self.send_ack(tag)