Add the ability to talk with other nemubot over DCC

This commit is contained in:
Némunaire 2012-08-31 05:21:19 +02:00
parent 30da270557
commit a2d9757d06
8 changed files with 350 additions and 200 deletions

186
bot.py
View File

@ -20,17 +20,19 @@ 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
ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
class Bot(BotCaps):
class Bot:
def __init__(self, servers=dict(), modules=dict(), mp=list()):
BotCaps.__init__(self, 3.2, "3.2-dev")
# Bot general informations
self.version = 3.2
self.version_txt = "3.2-dev"
# Keep global context: servers and modules
self.servers = servers
@ -44,8 +46,12 @@ class Bot(BotCaps):
self.events = list()
self.event_timer = None
# Own hooks
self.hooks = hooks.MessagesHook(self)
# Other known bots, making a bots network
self.network = dict()
self.hooks_cache = dict()
# Messages to be treated
self.msg_queue = Queue()
@ -194,6 +200,180 @@ class Bot(BotCaps):
for srv in k:
self.servers[srv].disconnect()
# Hooks cache
def create_cache(self, name):
if name not in self.hooks_cache:
if isinstance(self.hooks.__dict__[name], list):
self.hooks_cache[name] = list()
# Start by adding locals hooks
for h in self.hooks.__dict__[name]:
tpl = (h, 0, self.hooks.__dict__[name])
self.hooks_cache[name].append(tpl)
# Now, add extermal hooks
level = 0
while level == 0 or lvl_exist:
lvl_exist = False
for ext in self.network:
if len(self.network[ext].hooks) > level:
lvl_exist = True
for h in self.network[ext].hooks[level].__dict__[name]:
if h not in self.hooks_cache[name]:
self.hooks_cache[name].append((h, level + 1,
self.network[ext].hooks[level].__dict__[name]))
level += 1
elif isinstance(self.hooks.__dict__[name], dict):
self.hooks_cache[name] = dict()
# Start by adding locals hooks
for h in self.hooks.__dict__[name]:
self.hooks_cache[name][h] = (self.hooks.__dict__[name][h], 0,
self.hooks.__dict__[name])
# Now, add extermal hooks
level = 0
while level == 0 or lvl_exist:
lvl_exist = False
for ext in self.network:
if len(self.network[ext].hooks) > level:
lvl_exist = True
for h in self.network[ext].hooks[level].__dict__[name]:
if h not in self.hooks_cache[name]:
self.hooks_cache[name][h] = (self.network[ext].hooks[level].__dict__[name][h], level + 1, self.network[ext].hooks[level].__dict__[name])
level += 1
else:
raise Exception(name + " hook type unrecognized")
return self.hooks_cache[name]
# Treatment
def check_rest_times(self, store, hook):
"""Remove from store the hook if it has been executed given time"""
if hook.times == 0:
if isinstance(store, dict):
store[hook.name].remove(hook)
if len(store) == 0:
del store[hook.name]
elif isinstance(store, list):
store.remove(hook)
def treat_pre(self, msg):
"""Treat a message before all other treatment"""
for h, lvl, store in self.create_cache("all_pre"):
h.run(msg)
self.check_rest_times(store, h)
def treat_cmd(self, msg):
"""Treat a command message"""
treated = list()
# First, treat simple hook
cmd_hook = self.create_cache("cmd_hook")
if msg.cmd[0] in cmd_hook:
(hks, lvl, store) = cmd_hook[msg.cmd[0]]
for h in hks:
res = h.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, h)
# Then, treat regexp based hook
cmd_rgxp = self.create_cache("cmd_rgxp")
for hook, lvl, store in cmd_rgxp:
if hook.is_matching(msg.cmd[0], msg.channel):
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
# Finally, treat default hooks if not catched before
cmd_default = self.create_cache("cmd_default")
for hook, lvl, store in cmd_default:
if treated:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
return treated
def treat_ask(self, msg):
"""Treat an ask message"""
treated = list()
# First, treat simple hook
ask_hook = self.create_cache("ask_hook")
if msg.content in ask_hook:
hks, lvl, store = ask_hook[msg.content]
for h in hks:
res = h.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, h)
# Then, treat regexp based hook
ask_rgxp = self.create_cache("ask_rgxp")
for hook, lvl, store in ask_rgxp:
if hook.is_matching(msg.content, msg.channel):
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
# Finally, treat default hooks if not catched before
ask_default = self.create_cache("ask_default")
for hook, lvl, store in ask_default:
if treated:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
return treated
def treat_answer(self, msg):
"""Treat a normal message"""
treated = list()
# First, treat simple hook
msg_hook = self.create_cache("msg_hook")
if msg.content in msg_hook:
hks, lvl, store = msg_hook[msg.content]
for h in hks:
res = h.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, h)
# Then, treat regexp based hook
msg_rgxp = self.create_cache("msg_rgxp")
for hook, lvl, store in msg_rgxp:
if hook.is_matching(msg.content, msg.channel):
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
# Finally, treat default hooks if not catched before
msg_default = self.create_cache("msg_default")
for hook, lvl, store in msg_default:
if len(treated) > 0:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(store, hook)
return treated
def hotswap(bak):
return Bot(bak.servers, bak.modules, bak.modules_path)

View File

@ -1,28 +0,0 @@
# -*- 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 <http://www.gnu.org/licenses/>.
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()

View File

@ -38,7 +38,7 @@ class Consumer(threading.Thread):
# Create, parse and treat the message
try:
msg = Message(srv, raw, time, prvt)
res = msg.treat(self.context.hooks)
res = msg.treat()
except:
print ("\033[1;31mERROR:\033[0m occurred during the "
"processing of the message: %s" % raw)
@ -56,6 +56,9 @@ class Consumer(threading.Thread):
elif isinstance(res, Response):
srv.send_response(res, data)
# Inform that the message has been treated
srv.msg_treated(data)
except queue.Empty:
pass
finally:

145
hooks.py
View File

@ -19,7 +19,9 @@
from response import Response
class MessagesHook:
def __init__(self):
def __init__(self, context):
self.context = context
# Store specials hook
self.all_pre = list() # Treated before any parse
#self.all_post = list() # Treated before send message to user
@ -42,25 +44,30 @@ class MessagesHook:
def add_hook(self, store, hook):
"""Insert in the right place a hook into the given store"""
if isinstance(store, dict) and hook.name is not None:
if hook.name not in store:
store[hook.name] = list()
store[hook.name].append(hook)
elif isinstance(store, list):
store.append(hook)
if store in self.context.hooks_cache:
del self.context.hooks_cache[store]
attr = getattr(self, store)
if attr is None:
print ("Warning: unrecognized hook store type")
return
if isinstance(attr, dict) and hook.name is not None:
if hook.name not in attr:
attr[hook.name] = list()
attr[hook.name].append(hook)
elif isinstance(attr, list):
attr.append(hook)
else:
print ("Warning: unrecognized hook store type")
def register_hook_attributes(self, store, module, node):
if node.hasAttribute("name"):
self.add_hook(getattr(self, store + "_hook"), Hook(getattr(module,
node["call"]),
node["name"]))
self.add_hook(store + "_hook", Hook(getattr(module, node["call"]),
node["name"]))
elif node.hasAttribute("regexp"):
self.add_hook(getattr(self, store + "_rgxp"), Hook(getattr(module,
node["call"]),
None, None,
node["regexp"]))
self.add_hook(store + "_rgxp", Hook(getattr(module, node["call"]),
None, None, node["regexp"]))
def register_hook(self, module, node):
"""Create a hook from configuration node"""
@ -75,116 +82,6 @@ class MessagesHook:
node["type"] == "all"):
self.register_hook_attributes("answer", module, node)
def check_rest_times(self, store, hook):
"""Remove from store the hook if it has been executed given time"""
if hook.times == 0:
if isinstance(store, dict):
store[hook.name].remove(hook)
if len(store) == 0:
del store[hook.name]
elif isinstance(store, list):
store.remove(hook)
def treat_pre(self, msg):
"""Treat a message before all other treatment"""
for h in self.all_pre:
h.run(msg)
self.check_rest_times(self.all_pre, h)
def treat_cmd(self, msg):
"""Treat a command message"""
treated = list()
# First, treat simple hook
if msg.cmd[0] in self.cmd_hook:
for h in self.cmd_hook[msg.cmd[0]]:
res = h.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.cmd_hook, h)
# Then, treat regexp based hook
for hook in self.cmd_rgxp:
if hook.is_matching(msg.cmd[0], msg.channel):
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.cmd_rgxp, hook)
# Finally, treat default hooks if not catched before
for hook in self.cmd_default:
if treated:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.cmd_default, hook)
return treated
def treat_ask(self, msg):
"""Treat an ask message"""
treated = list()
# First, treat simple hook
if msg.content in self.ask_hook:
for h in self.ask_hook[msg.content]:
res = h.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.ask_hook, h)
# Then, treat regexp based hook
for hook in self.ask_rgxp:
if hook.is_matching(msg.content, msg.channel):
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.ask_rgxp, hook)
# Finally, treat default hooks if not catched before
for hook in self.ask_default:
if treated:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.ask_default, hook)
return treated
def treat_answer(self, msg):
"""Treat a normal message"""
treated = list()
# First, treat simple hook
if msg.content in self.msg_hook:
for h in self.msg_hook[msg.content]:
res = h.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.msg_hook, h)
# Then, treat regexp based hook
for hook in self.msg_rgxp:
if hook.is_matching(msg.content, msg.channel):
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.msg_rgxp, hook)
# Finally, treat default hooks if not catched before
for hook in self.msg_default:
if len(treated) > 0:
break
res = hook.run(msg)
if res is not None and res != False:
treated.append(res)
self.check_rest_times(self.msg_default, hook)
return treated
class Hook:
"""Class storing hook informations"""

View File

@ -214,11 +214,11 @@ def register_hooks(module, context, prompt):
# Register legacy hooks
if hasattr(module, "parseanswer"):
context.hooks.add_hook(context.hooks.cmd_default, Hook(module.parseanswer))
context.hooks.add_hook("cmd_default", Hook(module.parseanswer))
if hasattr(module, "parseask"):
context.hooks.add_hook(context.hooks.ask_default, Hook(module.parseask))
context.hooks.add_hook("ask_default", Hook(module.parseask))
if hasattr(module, "parselisten"):
context.hooks.add_hook(context.hooks.msg_default, Hook(module.parselisten))
context.hooks.add_hook("msg_default", Hook(module.parselisten))
##########################
# #

View File

@ -43,6 +43,7 @@ def save():
class Message:
def __init__ (self, srv, line, timestamp, private = False):
self.raw = line
self.srv = srv
self.time = timestamp
self.channel = None
@ -136,14 +137,14 @@ class Message:
return False
return self.srv.accepted_channel(self.channel)
def treat(self, hooks):
def treat(self):
"""Parse and treat the message"""
if self.cmd == "PING":
self.srv.send_pong(self.content)
elif self.cmd == "PRIVMSG" and self.ctcp:
self.parsectcp()
elif self.cmd == "PRIVMSG" and self.authorize():
return self.parsemsg (hooks)
return self.parsemsg()
elif self.channel in self.srv.channels:
if self.cmd == "353":
self.srv.channels[self.channel].parse353(self)
@ -191,13 +192,10 @@ class Message:
self.srv.send_ctcp(self.sender, "ERRMSG Unknown or unimplemented CTCP request")
def reparsemsg(self):
if self.hooks is not None:
self.parsemsg(self.hooks)
else:
print ("Can't reparse message")
self.parsemsg()
def parsemsg (self, hooks):
hooks.treat_pre(self)
def parsemsg (self):
self.srv.context.treat_pre(self)
#Treat all messages starting with 'nemubot:' as distinct commands
if self.content.find("%s:"%self.srv.nick) == 0:
#Remove the bot name
@ -211,7 +209,7 @@ class Message:
# Ask hooks
else:
return hooks.treat_ask(self)
return self.srv.context.treat_ask(self)
#Owner commands
elif self.content[0] == '`' and self.sender == self.srv.owner:
@ -236,7 +234,6 @@ class Message:
#Messages stating with !
elif self.content[0] == '!' and len(self.content) > 1:
self.hooks = hooks
try:
self.cmd = shlex.split(self.content[1:])
except ValueError:
@ -277,13 +274,13 @@ class Message:
conn = DCC(self.srv, self.sender)
conn.send_file("bot_sample.xml")
else:
return hooks.treat_cmd(self)
return self.srv.context.treat_cmd(self)
else:
res = hooks.treat_answer(self)
res = self.srv.context.treat_answer(self)
# Assume the message starts with nemubot:
if res is None and self.private:
return hooks.treat_ask(self)
return self.srv.context.treat_ask(self)
return res
# def parseOwnerCmd(self, cmd):

View File

@ -16,20 +16,27 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import random
import shlex
import urllib.parse
import zlib
from botcaps import BotCaps
from DCC import DCC
import hooks
from response import Response
class NetworkBot:
def __init__(self, context, srv, dest, dcc=None):
# General informations
self.context = context
self.srv = srv
self.dcc = dcc
self.dest = dest
self.infos = None
self.dcc = dcc # DCC connection to the other bot
self.hooks = list()
# Tags monitor
self.my_tag = random.randint(0,255)
self.inc_tag = 0
self.tags = dict()
@ -52,39 +59,64 @@ class NetworkBot:
return self.dcc.realname
return None
def isDCC(self, someone):
"""Abstract implementation"""
return True
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
self.inc_tag = (self.inc_tag + 1) % 256
while self.inc_tag in self.tags:
self.inc_tag = (self.inc_out + 1) % 256
self.inc_tag = (self.inc_tag + 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
self.tags[tag] = (cmd, data)
# Send the command with the tag
self.send_response(tag, cmd)
self.send_response_final(tag, cmd)
def send_response(self, tag, msg):
def send_response(self, res, tag):
self.send_response_final(tag, [res.sender, res.channel, res.nick, res.nomore, res.title, res.more, res.count, json.dumps(res.messages)])
def msg_treated(self, tag):
self.send_ack(tag)
def send_response_final(self, tag, msg):
"""Send a response with a tag"""
for line in msg.split("\n"):
self.dcc.send_dcc_raw(tag + b' ' + line.encode())
if isinstance(msg, list):
cnt = b''
for i in msg:
if i is None:
cnt += b' ""'
elif isinstance(i, int):
cnt += (' %d' % i).encode()
elif isinstance(i, float):
cnt += (' %f' % i).encode()
else:
cnt += b' "' + urllib.parse.quote(i).encode() + b'"'
if False and len(cnt) > 10:
cnt = b' Z ' + zlib.compress(cnt)
print (cnt)
self.dcc.send_dcc_raw(tag + cnt)
else:
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")
self.send_response_final(tag, "ACK")
def connect(self):
"""Making the connexion with dest through srv"""
if self.dcc is None:
if self.dcc is None or not self.dcc.connected:
self.dcc = DCC(self.srv, self.dest)
self.dcc.treatement = self.hello
self.dcc.send_dcc("NEMUBOT###")
else:
self.send_cmd("FETCH")
def disconnect(self, reason=""):
"""Close the connection and remove the bot from network list"""
@ -101,24 +133,89 @@ class NetworkBot:
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())
# Is it a response?
if tag in self.tags:
# Is it compressed content?
if words[1] == b'Z':
#print (line)
line = zlib.decompress(line[len(tag) + 3:])
self.response(line, tag, [urllib.parse.unquote(arg) for arg in shlex.split(line[len(tag) + 1:].decode())], self.tags[tag])
else:
args = list()
cmd = words[1]
if len(words) > 2:
args = shlex.split(line[len(tag) + len(cmd) + 2:].decode())
args = [urllib.parse.unquote(arg) for arg in args]
else:
args = list()
#print ("request:", line)
self.request(tag, cmd, args)
# Parse
if cmd == b'ACK':
if tag in self.tags:
del self.tags[tag]
def response(self, line, tag, args, t):
(cmds, data) = t
#print ("response for", cmds, ":", args)
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)
if isinstance(cmds, list):
cmd = cmds[0]
else:
cmd = cmds
cmds = list(cmd)
if args[0] == 'ACK': # Acknowledge a command
del self.tags[tag]
elif cmd == "FETCH" and len(args) >= 5:
level = int(args[1])
while len(self.hooks) <= level:
self.hooks.append(hooks.MessagesHook(self.context))
if args[2] == "": args[2] = None
if args[3] == "": args[3] = None
if args[4] == "": args[4] = list()
else: args[4] = args[4].split(',')
self.hooks[level].add_hook(args[0], hooks.Hook(self.exec_hook, args[2], None, args[3], args[4]))
elif cmd == "HOOK" and len(args) >= 8:
# Rebuild the response
if args[1] == '': args[1] = None
if args[2] == '': args[2] = None
if args[3] == '': args[3] = None
if args[4] == '': args[4] = None
if args[5] == '': args[5] = None
if args[6] == '': args[6] = None
res = Response(args[0], channel=args[1], nick=args[2], nomore=args[3], title=args[4], more=args[5], count=args[6])
for msg in json.loads(args[7]):
res.append_message(msg)
if len(res.messages) <= 1:
res.alone = True
self.srv.send_response(res, None)
def request(self, tag, cmd, args):
# Parse
if cmd == b'MYTAG' and len(args) > 0: # Inform about choosen tag
while args[0] == self.my_tag:
self.my_tag = random.randint(0,255)
self.send_ack(tag)
elif cmd == b'FETCH': # Get known commands
for name in ["cmd_hook", "ask_hook", "msg_hook"]:
elts = self.context.create_cache(name)
for elt in elts:
(hooks, lvl, store) = elts[elt]
for h in hooks:
self.send_response_final(tag, [name, lvl, elt, h.regexp, ','.join(h.channels)])
self.send_ack(tag)
elif (cmd == b'HOOK' or cmd == b'"HOOK"') and len(args) > 0: # Action requested
self.context.receive_message(self, args[0].encode(), True, tag)
def exec_hook(self, msg):
self.send_cmd(["HOOK", msg.raw])

View File

@ -110,6 +110,10 @@ class Server(threading.Thread):
def send_pong(self, cnt):
self.s.send(("PONG %s\r\n" % cnt).encode ())
def msg_treated(self, origin):
"""Do nothing, here for implement abstract class"""
pass
def send_response(self, res, origin):
if res.channel is not None and res.channel != self.nick:
self.send_msg(res.channel, res.get_message())