Introducing a new nemubot architecture for servers and messages treatment: messages have no more context or server
This commit is contained in:
parent
f00dfc82f7
commit
a62940380e
53
DCC.py
53
DCC.py
@ -26,18 +26,14 @@ import time
|
||||
import traceback
|
||||
|
||||
import message
|
||||
import server
|
||||
|
||||
#Store all used ports
|
||||
PORTS = list()
|
||||
|
||||
class DCC(threading.Thread):
|
||||
class DCC(server.Server):
|
||||
def __init__(self, srv, dest, socket=None):
|
||||
self.DCC = False # Is this a DCC connection
|
||||
self.error = False # An error has occur, closing the connection?
|
||||
self.stop = False # Stop requered
|
||||
self.stopping = threading.Event() # Event to listen for full disconnection
|
||||
self.conn = socket # The socket
|
||||
self.connected = self.conn is not None # Is connected?
|
||||
self.messages = list() # Message queued before connexion
|
||||
|
||||
# Informations about the sender
|
||||
@ -64,7 +60,7 @@ class DCC(threading.Thread):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def foundPort(self):
|
||||
"""Found a free port for the connexion"""
|
||||
"""Found a free port for the connection"""
|
||||
for p in range(65432, 65535):
|
||||
if p not in PORTS:
|
||||
PORTS.append(p)
|
||||
@ -73,41 +69,18 @@ class DCC(threading.Thread):
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Gives the server identifiant"""
|
||||
return self.srv.id + "/" + self.sender
|
||||
|
||||
def setError(self, msg):
|
||||
self.error = True
|
||||
self.srv.send_msg_usr(self.sender, msg)
|
||||
|
||||
def disconnect(self):
|
||||
"""Close the connection if connected"""
|
||||
if self.connected:
|
||||
self.stop = True
|
||||
self.conn.shutdown(socket.SHUT_RDWR)
|
||||
self.stopping.wait()
|
||||
return True
|
||||
return False
|
||||
|
||||
def kill(self):
|
||||
"""Stop the loop without closing the socket"""
|
||||
if self.connected:
|
||||
self.stop = True
|
||||
self.connected = False
|
||||
#self.stopping.wait()#Compare with server before delete me
|
||||
return True
|
||||
return False
|
||||
|
||||
def launch(self, mods=None):
|
||||
"""Connect to the client if not already connected"""
|
||||
if not self.connected:
|
||||
self.stop = False
|
||||
self.start()
|
||||
|
||||
def accept_user(self, host, port):
|
||||
"""Accept a DCC connection"""
|
||||
self.conn = socket.socket()
|
||||
self.s = socket.socket()
|
||||
try:
|
||||
self.conn.connect((host, port))
|
||||
self.s.connect((host, port))
|
||||
print ('Accepted user from', host, port, "for", self.sender)
|
||||
self.connected = True
|
||||
self.stop = False
|
||||
@ -143,13 +116,13 @@ class DCC(threading.Thread):
|
||||
|
||||
s.listen(1)
|
||||
#Waiting for the client
|
||||
(self.conn, addr) = s.accept()
|
||||
(self.s, addr) = s.accept()
|
||||
print ('Connected by', addr)
|
||||
self.connected = True
|
||||
return True
|
||||
|
||||
def send_dcc_raw(self, line):
|
||||
self.conn.sendall(line + b'\n')
|
||||
self.s.sendall(line + b'\n')
|
||||
|
||||
def send_dcc(self, msg, to = None):
|
||||
"""If we talk to this user, send a message through this connection
|
||||
@ -157,7 +130,7 @@ class DCC(threading.Thread):
|
||||
if to is None or to == self.sender or to == self.nick:
|
||||
if self.error:
|
||||
self.srv.send_msg_final(self.nick, msg)
|
||||
elif not self.connected or self.conn is None:
|
||||
elif not self.connected or self.s is None:
|
||||
if not self.DCC:
|
||||
self.start()
|
||||
self.DCC = True
|
||||
@ -190,8 +163,8 @@ class DCC(threading.Thread):
|
||||
with open(self.messages, 'rb') as f:
|
||||
d = f.read(268435456) #Packets size: 256Mo
|
||||
while d:
|
||||
self.conn.sendall(d)
|
||||
self.conn.recv(4) #The client send a confirmation after each packet
|
||||
self.s.sendall(d)
|
||||
self.s.recv(4) #The client send a confirmation after each packet
|
||||
d = f.read(268435456) #Packets size: 256Mo
|
||||
|
||||
# Messages connection
|
||||
@ -211,7 +184,7 @@ class DCC(threading.Thread):
|
||||
self.nicksize = len(self.srv.nick)
|
||||
self.Bnick = self.srv.nick.encode()
|
||||
while not self.stop:
|
||||
raw = self.conn.recv(1024) #recieve server messages
|
||||
raw = self.s.recv(1024) #recieve server messages
|
||||
if not raw:
|
||||
break
|
||||
readbuffer = readbuffer + raw
|
||||
@ -222,7 +195,7 @@ class DCC(threading.Thread):
|
||||
self.treatement(line)
|
||||
|
||||
if self.connected:
|
||||
self.conn.close()
|
||||
self.s.close()
|
||||
self.connected = False
|
||||
|
||||
#Remove from DCC connections server list
|
||||
|
263
IRCServer.py
Normal file
263
IRCServer.py
Normal file
@ -0,0 +1,263 @@
|
||||
# -*- 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 errno
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
import channel
|
||||
import DCC
|
||||
import message
|
||||
import server
|
||||
import xmlparser
|
||||
|
||||
class IRCServer(server.Server):
|
||||
"""Class to interact with an IRC server"""
|
||||
|
||||
def __init__(self, node, nick, owner, realname):
|
||||
"""Initialize an IRC server
|
||||
|
||||
Arguments:
|
||||
node -- server node from XML configuration
|
||||
nick -- nick used by the bot on this server
|
||||
owner -- nick used by the bot owner on this server
|
||||
realname -- string used as realname on this server
|
||||
|
||||
"""
|
||||
server.Server.__init__(self)
|
||||
|
||||
self.node = node
|
||||
|
||||
self.nick = nick
|
||||
self.owner = owner
|
||||
self.realname = realname
|
||||
|
||||
# Listen private messages?
|
||||
self.listen_nick = True
|
||||
|
||||
self.dcc_clients = dict()
|
||||
|
||||
self.channels = dict()
|
||||
for chn in self.node.getNodes("channel"):
|
||||
chan = channel.Channel(chn, self)
|
||||
self.channels[chan.name] = chan
|
||||
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""Return the server hostname"""
|
||||
if self.node is not None and self.node.hasAttribute("server"):
|
||||
return self.node["server"]
|
||||
else:
|
||||
return "localhost"
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""Return the connection port used on this server"""
|
||||
if self.node is not None and self.node.hasAttribute("port"):
|
||||
return self.node.getInt("port")
|
||||
else:
|
||||
return "6667"
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
"""Return the password used to connect to this server"""
|
||||
if self.node is not None and self.node.hasAttribute("password"):
|
||||
return self.node["password"]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def allow_all(self):
|
||||
"""If True, treat message from all channels, not only listed one"""
|
||||
return (self.node is not None and self.node.hasAttribute("allowall")
|
||||
and self.node["allowall"] == "true")
|
||||
|
||||
@property
|
||||
def autoconnect(self):
|
||||
"""Autoconnect the server when added"""
|
||||
if self.node is not None and self.node.hasAttribute("autoconnect"):
|
||||
value = self.node["autoconnect"].lower()
|
||||
return value != "no" and value != "off" and value != "false"
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Gives the server identifiant"""
|
||||
return self.host + ":" + str(self.port)
|
||||
|
||||
def accepted_channel(self, chan, sender = None):
|
||||
"""Return True if the channel (or the user) is authorized"""
|
||||
if self.allow_all:
|
||||
return True
|
||||
elif self.listen_nick:
|
||||
return (chan in self.channels and (sender is None or sender in
|
||||
self.channels[chan].people)
|
||||
) or chan == self.nick
|
||||
else:
|
||||
return chan in self.channels and (sender is None or sender
|
||||
in self.channels[chan].people)
|
||||
|
||||
def join(self, chan, password=None):
|
||||
"""Join a channel"""
|
||||
if chan is not None and self.connected and chan not in self.channels:
|
||||
chn = xmlparser.module_state.ModuleState("channel")
|
||||
chn["name"] = chan
|
||||
chn["password"] = password
|
||||
self.node.addChild(chn)
|
||||
self.channels[chan] = channel.Channel(chn, self)
|
||||
if password is not None:
|
||||
self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode ())
|
||||
else:
|
||||
self.s.send(("JOIN %s\r\n" % chan).encode ())
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def leave(self, chan):
|
||||
"""Leave a channel"""
|
||||
if chan is not None and self.connected and chan in self.channels:
|
||||
if chan.instanceof(list):
|
||||
for c in chan:
|
||||
self.leave(c)
|
||||
else:
|
||||
self.s.send(("PART %s\r\n" % self.channels[chan].name).encode ())
|
||||
del self.channels[chan]
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# Main loop
|
||||
def run(self):
|
||||
if not self.connected:
|
||||
self.s = socket.socket() #Create the socket
|
||||
try:
|
||||
self.s.connect((self.host, self.port)) #Connect to server
|
||||
except socket.error as e:
|
||||
self.s = None
|
||||
print ("\033[1;31mError:\033[0m Unable to connect to %s:%d: %s"
|
||||
% (self.host, self.port, os.strerror(e.errno)))
|
||||
return
|
||||
self.stopping.clear()
|
||||
|
||||
if self.password != None:
|
||||
self.s.send(b"PASS " + self.password.encode () + b"\r\n")
|
||||
self.s.send(("NICK %s\r\n" % self.nick).encode ())
|
||||
self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host,
|
||||
self.realname)).encode ())
|
||||
raw = self.s.recv(1024)
|
||||
if not raw:
|
||||
print ("Unable to connect to %s:%d" % (self.host, self.port))
|
||||
return
|
||||
self.connected = True
|
||||
print ("Connection to %s:%d completed" % (self.host, self.port))
|
||||
|
||||
readbuffer = b'' #Here we store all the messages from server
|
||||
while not self.stop:
|
||||
readbuffer = readbuffer + raw
|
||||
temp = readbuffer.split(b'\n')
|
||||
readbuffer = temp.pop()
|
||||
|
||||
for line in temp:
|
||||
self.treat_msg(line)
|
||||
raw = self.s.recv(1024) #recieve server messages
|
||||
|
||||
if self.connected:
|
||||
self.s.close()
|
||||
self.connected = False
|
||||
print ("Server `%s' successfully stopped." % self.id)
|
||||
self.stopping.set()
|
||||
# Rearm Thread
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
|
||||
# Overwritted methods
|
||||
|
||||
def disconnect(self):
|
||||
"""Close the socket with the server and all DCC client connections"""
|
||||
#Close all DCC connection
|
||||
for clt in self.dcc_clients:
|
||||
self.dcc_clients[clt].disconnect()
|
||||
return server.Server.disconnect(self)
|
||||
|
||||
|
||||
|
||||
# Abstract methods
|
||||
|
||||
def send_pong(self, cnt):
|
||||
"""Send a PONG command to the server with argument 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_dcc(self, msg, to):
|
||||
"""Send a message through DCC connection"""
|
||||
if msg is not None and to is not None:
|
||||
realname = to.split("!")[1]
|
||||
if realname not in self.dcc_clients.keys():
|
||||
d = dcc.DCC(self, to)
|
||||
self.dcc_clients[realname] = d
|
||||
self.dcc_clients[realname].send_dcc(msg)
|
||||
|
||||
def send_msg_final(self, channel, line, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message without checks or format"""
|
||||
#TODO: add something for post message treatment here
|
||||
if channel == self.nick:
|
||||
print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg)
|
||||
traceback.print_stack()
|
||||
if line is not None and channel is not None:
|
||||
if self.s is None:
|
||||
print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, line))
|
||||
traceback.print_stack()
|
||||
elif len(line) < 442:
|
||||
self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ())
|
||||
else:
|
||||
print ("\033[1;35mWarning:\033[0m Message truncated due to size (%d ; max : 442) : %s" % (len(line), line))
|
||||
traceback.print_stack()
|
||||
self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"...", endl)).encode ())
|
||||
|
||||
def send_msg_usr(self, user, msg):
|
||||
"""Send a message to a user instead of a channel"""
|
||||
if user is not None and user[0] != "#":
|
||||
realname = user.split("!")[1]
|
||||
if realname in self.dcc_clients or user in self.dcc_clients:
|
||||
self.send_dcc(msg, user)
|
||||
else:
|
||||
for line in msg.split("\n"):
|
||||
if line != "":
|
||||
self.send_msg_final(user.split('!')[0], msg)
|
||||
|
||||
def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message to a channel"""
|
||||
if self.accepted_channel(channel):
|
||||
server.Server.send_msg(self, channel, msg, cmd, endl)
|
||||
|
||||
def send_msg_verified(self, sender, channel, msg, cmd = "PRIVMSG", endl = "\r\n"):
|
||||
"""Send a message to a channel, only if the source user is on this channel too"""
|
||||
if self.accepted_channel(channel, sender):
|
||||
self.send_msg_final(channel, msg, cmd, endl)
|
||||
|
||||
def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message to all channels on this server"""
|
||||
for channel in self.channels.keys():
|
||||
self.send_msg(channel, msg, cmd, endl)
|
67
bot.py
67
bot.py
@ -26,19 +26,26 @@ import consumer
|
||||
import event
|
||||
import hooks
|
||||
from networkbot import NetworkBot
|
||||
from server import Server
|
||||
from IRCServer import IRCServer
|
||||
import response
|
||||
|
||||
ID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
class Bot:
|
||||
def __init__(self, servers=dict(), modules=dict(), mp=list()):
|
||||
def __init__(self, ip, realname, mp=list()):
|
||||
# Bot general informations
|
||||
self.version = 3.2
|
||||
self.version_txt = "3.2-dev"
|
||||
self.version = 3.3
|
||||
self.version_txt = "3.3-dev"
|
||||
|
||||
# Save various informations
|
||||
self.ip = ip
|
||||
self.realname = realname
|
||||
self.ctcp_capabilities = dict()
|
||||
self.init_ctcp_capabilities()
|
||||
|
||||
# Keep global context: servers and modules
|
||||
self.servers = servers
|
||||
self.modules = modules
|
||||
self.servers = dict()
|
||||
self.modules = dict()
|
||||
|
||||
# Context paths
|
||||
self.modules_path = mp
|
||||
@ -61,6 +68,36 @@ class Bot:
|
||||
self.cnsr_thrd_size = -1
|
||||
|
||||
|
||||
def init_ctcp_capabilities(self):
|
||||
"""Reset existing CTCP capabilities to default one"""
|
||||
self.ctcp_capabilities["ACTION"] = lambda msg: print ("ACTION receive")
|
||||
self.ctcp_capabilities["CLIENTINFO"] = self._ctcp_clientinfo
|
||||
self.ctcp_capabilities["DCC"] = self._ctcp_dcc
|
||||
self.ctcp_capabilities["NEMUBOT"] = lambda srv, msg: _ctcp_response(
|
||||
msg.sender, "NEMUBOT %f" % self.version)
|
||||
self.ctcp_capabilities["TIME"] = lambda srv, msg: _ctcp_response(
|
||||
msg.sender, "TIME %s" % (datetime.now()))
|
||||
self.ctcp_capabilities["USERINFO"] = lambda srv, msg: _ctcp_response(
|
||||
msg.sender, "USERINFO %s" % self.realname)
|
||||
self.ctcp_capabilities["VERSION"] = lambda srv, msg: _ctcp_response(
|
||||
msg.sender, "VERSION nemubot v%s" % self.version_txt)
|
||||
|
||||
def _ctcp_clientinfo(self, srv, msg):
|
||||
"""Response to CLIENTINFO CTCP message"""
|
||||
return _ctcp_response(msg.sndr,
|
||||
" ".join(self.ctcp_capabilities.keys()))
|
||||
|
||||
def _ctcp_dcc(self, srv, msg):
|
||||
"""Response to DCC CTCP message"""
|
||||
ip = self.srv.toIP(int(msg.cmds[3]))
|
||||
conn = DCC(srv, msg.sender)
|
||||
if conn.accept_user(ip, int(msg.cmds[4])):
|
||||
srv.dcc_clients[conn.sender] = conn
|
||||
conn.send_dcc("Hello %s!" % conn.nick)
|
||||
else:
|
||||
print ("DCC: unable to connect to %s:%s" % (ip, msg.cmds[4]))
|
||||
|
||||
|
||||
def add_event(self, evt, eid=None):
|
||||
"""Register an event and return its identifiant for futur update"""
|
||||
if eid is None:
|
||||
@ -126,7 +163,7 @@ class Bot:
|
||||
while len(self.events)>0 and datetime.now() >= self.events[0].current:
|
||||
#print ("end timer: while")
|
||||
evt = self.events.pop(0)
|
||||
self.cnsr_queue.put_nowait(consumer.EventConsumer(self, evt))
|
||||
self.cnsr_queue.put_nowait(consumer.EventConsumer(evt))
|
||||
self.update_consumers()
|
||||
|
||||
self.update_timer()
|
||||
@ -134,11 +171,11 @@ class Bot:
|
||||
|
||||
def addServer(self, node, nick, owner, realname):
|
||||
"""Add a new server to the context"""
|
||||
srv = Server(node, nick, owner, realname)
|
||||
srv = IRCServer(node, nick, owner, realname)
|
||||
if srv.id not in self.servers:
|
||||
self.servers[srv.id] = srv
|
||||
if srv.autoconnect:
|
||||
srv.launch(self)
|
||||
srv.launch(self.receive_message)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@ -296,6 +333,16 @@ class Bot:
|
||||
self.check_rest_times(store, h)
|
||||
|
||||
|
||||
def treat_post(self, msg):
|
||||
"""Treat a message before send"""
|
||||
for h, lvl, store in self.create_cache("all_post"):
|
||||
c = h.run(msg)
|
||||
self.check_rest_times(store, h)
|
||||
if not c:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def treat_cmd(self, msg):
|
||||
"""Treat a command message"""
|
||||
treated = list()
|
||||
@ -401,6 +448,8 @@ class Bot:
|
||||
|
||||
return treated
|
||||
|
||||
def _ctcp_response(sndr, msg):
|
||||
return response.Response(sndr, msg, ctcp=True)
|
||||
|
||||
def hotswap(bak):
|
||||
return Bot(bak.servers, bak.modules, bak.modules_path)
|
||||
|
77
consumer.py
77
consumer.py
@ -17,12 +17,15 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import queue
|
||||
import re
|
||||
import threading
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
import bot
|
||||
from message import Message
|
||||
from response import Response
|
||||
import server
|
||||
|
||||
class MessageConsumer:
|
||||
"""Store a message before treating"""
|
||||
@ -33,11 +36,64 @@ class MessageConsumer:
|
||||
self.prvt = prvt
|
||||
self.data = data
|
||||
|
||||
def run(self):
|
||||
# Create, parse and treat the message
|
||||
def treat_in(self, context, msg):
|
||||
"""Treat the input message"""
|
||||
if msg.cmd == "PING":
|
||||
self.srv.send_pong(msg.content)
|
||||
else:
|
||||
# TODO: Manage credits here
|
||||
context.treat_pre(msg)
|
||||
|
||||
if msg.cmd == "PRIVMSG" and msg.ctcp:
|
||||
if msg.cmds[0] in context.ctcp_capabilities:
|
||||
return context.ctcp_capabilities[msg.cmds[0]](self.srv, msg)
|
||||
else:
|
||||
return bot._ctcp_response(msg.sender, "ERRMSG Unknown or unimplemented CTCP request")
|
||||
elif msg.cmd == "PRIVMSG" and self.srv.accepted_channel(msg.channel):
|
||||
return self.treat_prvmsg(context, msg)
|
||||
# TODO: continue here
|
||||
pass
|
||||
|
||||
def treat_prvmsg(self, context, msg):
|
||||
# Treat all messages starting with 'nemubot:' as distinct commands
|
||||
if msg.content.find("%s:"%self.srv.nick) == 0:
|
||||
# Remove the bot name
|
||||
msg.content = msg.content[len(self.srv.nick)+1:].strip()
|
||||
|
||||
# Treat ping
|
||||
if re.match(".*(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)",
|
||||
msg.content, re.I) is not None:
|
||||
return Response(msg.sender, message="pong",
|
||||
channel=msg.channel, nick=msg.nick)
|
||||
|
||||
# Ask hooks
|
||||
else:
|
||||
return context.treat_ask(self)
|
||||
|
||||
|
||||
def treat_out(self, context, res):
|
||||
"""Treat the output message"""
|
||||
# Define the destination server
|
||||
if (res.server is not None and
|
||||
res.server.instanceof(string) and res.server in context.servers):
|
||||
res.server = context.servers[res.server]
|
||||
if (res.server is not None and
|
||||
not res.server.instanceof(server.Server)):
|
||||
print ("\033[1;35mWarning:\033[0m the server defined in this "
|
||||
"response doesn't exist: %s" % (res.server))
|
||||
res.server = None
|
||||
if res.server is None:
|
||||
res.server = self.srv
|
||||
|
||||
# Sent the message only if treat_post authorize it
|
||||
if context.treat_post(res):
|
||||
res.server.send_response(res, self.data)
|
||||
|
||||
def run(self, context):
|
||||
"""Create, parse and treat the message"""
|
||||
try:
|
||||
msg = Message(self.srv, self.raw, self.time, self.prvt)
|
||||
res = msg.treat()
|
||||
res = self.treat_in(context, msg)
|
||||
except:
|
||||
print ("\033[1;31mERROR:\033[0m occurred during the "
|
||||
"processing of the message: %s" % self.raw)
|
||||
@ -51,13 +107,13 @@ class MessageConsumer:
|
||||
if isinstance(res, list):
|
||||
for r in res:
|
||||
if isinstance(r, Response):
|
||||
self.srv.send_response(r, self.data)
|
||||
self.treat_out(context, r)
|
||||
elif isinstance(r, list):
|
||||
for s in r:
|
||||
if isinstance(s, Response):
|
||||
self.srv.send_response(s, self.data)
|
||||
self.treat_out(context, s)
|
||||
elif isinstance(res, Response):
|
||||
self.srv.send_response(res, self.data)
|
||||
self.treat_out(context, res)
|
||||
|
||||
# Inform that the message has been treated
|
||||
self.srv.msg_treated(self.data)
|
||||
@ -65,12 +121,11 @@ class MessageConsumer:
|
||||
|
||||
class EventConsumer:
|
||||
"""Store a event before treating"""
|
||||
def __init__(self, context, evt, timeout=20):
|
||||
self.context = context
|
||||
def __init__(self, evt, timeout=20):
|
||||
self.evt = evt
|
||||
self.timeout = timeout
|
||||
|
||||
def run(self):
|
||||
def run(self, context):
|
||||
try:
|
||||
self.evt.launch_check()
|
||||
except:
|
||||
@ -79,7 +134,7 @@ class EventConsumer:
|
||||
traceback.print_exception(exc_type, exc_value,
|
||||
exc_traceback)
|
||||
if self.evt.next is not None:
|
||||
self.context.add_event(self.evt, self.evt.id)
|
||||
context.add_event(self.evt, self.evt.id)
|
||||
|
||||
|
||||
|
||||
@ -94,7 +149,7 @@ class Consumer(threading.Thread):
|
||||
try:
|
||||
while not self.stop:
|
||||
stm = self.context.cnsr_queue.get(True, 20)
|
||||
stm.run()
|
||||
stm.run(self.context)
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
2
hooks.py
2
hooks.py
@ -24,7 +24,7 @@ class MessagesHook:
|
||||
|
||||
# Store specials hook
|
||||
self.all_pre = list() # Treated before any parse
|
||||
#self.all_post = list() # Treated before send message to user
|
||||
self.all_post = list() # Treated before send message to user
|
||||
|
||||
# Store direct hook
|
||||
self.cmd_hook = dict()
|
||||
|
@ -136,7 +136,7 @@ class ModuleLoader(SourceLoader):
|
||||
if not hasattr(module, "nemubotversion"):
|
||||
raise ImportError("Module `%s' is not a nemubot module."%self.name)
|
||||
# Check module version
|
||||
if module.nemubotversion != self.context.version:
|
||||
if module.nemubotversion > self.context.version:
|
||||
raise ImportError("Module `%s' is not compatible with this "
|
||||
"version." % self.name)
|
||||
|
||||
|
61
message.py
61
message.py
@ -76,6 +76,14 @@ class Message:
|
||||
# Check for CTCP request
|
||||
self.ctcp = len(words[3]) > 1 and (words[3][0] == 0x01 or words[3][1] == 0x01)
|
||||
self.content = self.pickWords(words[3:])
|
||||
# If CTCP, remove 0x01
|
||||
if self.ctcp:
|
||||
self.content = self.content[1:len(self.content)-1]
|
||||
# Split content by words
|
||||
try:
|
||||
self.cmds = shlex.split(self.content.decode())
|
||||
except ValueError:
|
||||
self.cmds = self.content.decode().split(' ')
|
||||
elif self.cmd == '353' and len(words) > 3:
|
||||
for i in range(2, len(words)):
|
||||
if words[i][0] == 58:
|
||||
@ -125,8 +133,9 @@ class Message:
|
||||
def is_owner(self):
|
||||
return self.nick == self.srv.owner
|
||||
|
||||
def authorize(self):
|
||||
def authorize_DEPRECATED(self):
|
||||
"""Is nemubot listening for the sender on this channel?"""
|
||||
# TODO: deprecated
|
||||
if self.srv.isDCC(self.sender):
|
||||
return True
|
||||
elif self.realname not in CREDITS:
|
||||
@ -139,13 +148,7 @@ class Message:
|
||||
|
||||
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()
|
||||
elif self.channel in self.srv.channels:
|
||||
if self.channel in self.srv.channels:
|
||||
if self.cmd == "353":
|
||||
self.srv.channels[self.channel].parse353(self)
|
||||
elif self.cmd == "332":
|
||||
@ -166,53 +169,13 @@ class Message:
|
||||
self.srv.channels[chn].part(self.nick)
|
||||
return None
|
||||
|
||||
def parsectcp(self):
|
||||
"""Parse CTCP requests"""
|
||||
if self.content == '\x01CLIENTINFO\x01':
|
||||
self.srv.send_ctcp(self.sender, "CLIENTINFO TIME USERINFO VERSION CLIENTINFO")
|
||||
elif self.content == '\x01TIME\x01':
|
||||
self.srv.send_ctcp(self.sender, "TIME %s" % (datetime.now()))
|
||||
elif self.content == '\x01USERINFO\x01':
|
||||
self.srv.send_ctcp(self.sender, "USERINFO %s" % (self.srv.realname))
|
||||
elif self.content == '\x01VERSION\x01':
|
||||
self.srv.send_ctcp(self.sender, "VERSION nemubot v%s" % self.srv.context.version_txt)
|
||||
elif self.content[:9] == '\x01DCC CHAT':
|
||||
words = self.content[1:len(self.content) - 1].split(' ')
|
||||
ip = self.srv.toIP(int(words[3]))
|
||||
conn = DCC(self.srv, self.sender)
|
||||
if conn.accept_user(ip, int(words[4])):
|
||||
self.srv.dcc_clients[conn.sender] = conn
|
||||
conn.send_dcc("Hello %s!" % conn.nick)
|
||||
else:
|
||||
print ("DCC: unable to connect to %s:%s" % (ip, words[4]))
|
||||
elif self.content == '\x01NEMUBOT\x01':
|
||||
self.srv.send_ctcp(self.sender, "NEMUBOT %f" % self.srv.context.version)
|
||||
elif self.content[:7] != '\x01ACTION':
|
||||
print (self.content)
|
||||
self.srv.send_ctcp(self.sender, "ERRMSG Unknown or unimplemented CTCP request")
|
||||
|
||||
def reparsemsg(self):
|
||||
self.parsemsg()
|
||||
|
||||
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
|
||||
self.content = self.content[len(self.srv.nick)+1:].strip()
|
||||
messagel = self.content.lower()
|
||||
|
||||
# Treat ping
|
||||
if re.match(".*(m[' ]?entends?[ -]+tu|h?ear me|do you copy|ping)",
|
||||
messagel) is not None:
|
||||
return Response(self.sender, message="pong", channel=self.channel, nick=self.nick)
|
||||
|
||||
# Ask hooks
|
||||
else:
|
||||
return self.srv.context.treat_ask(self)
|
||||
|
||||
#Owner commands
|
||||
elif self.content[0] == '`' and self.sender == self.srv.owner:
|
||||
if self.content[0] == '`' and self.sender == self.srv.owner:
|
||||
self.cmd = self.content[1:].split(' ')
|
||||
if self.cmd[0] == "ban":
|
||||
if len(self.cmd) > 1:
|
||||
|
@ -50,12 +50,12 @@ def connect(data, toks, context, prompt):
|
||||
if len(toks) > 1:
|
||||
for s in toks[1:]:
|
||||
if s in context.servers:
|
||||
context.servers[s].launch(context)
|
||||
context.servers[s].launch(context.receive_message)
|
||||
else:
|
||||
print ("connect: server `%s' not found." % s)
|
||||
|
||||
elif prompt.selectedServer is not None:
|
||||
prompt.selectedServer.launch(context)
|
||||
prompt.selectedServer.launch(context.receive_message)
|
||||
else:
|
||||
print (" Please SELECT a server or give its name in argument.")
|
||||
|
||||
|
@ -29,7 +29,7 @@ import importer
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create bot context
|
||||
context = bot.Bot()
|
||||
context = bot.Bot(0, "FIXME")
|
||||
|
||||
# Load the prompt
|
||||
prmpt = prompt.Prompt()
|
||||
|
@ -217,6 +217,9 @@ class NetworkBot:
|
||||
elif (cmd == b'HOOK' or cmd == b'"HOOK"') and len(args) > 0: # Action requested
|
||||
self.context.receive_message(self, args[0].encode(), True, tag)
|
||||
|
||||
elif (cmd == b'NOMORE' or cmd == b'"NOMORE"') and len(args) > 0: # Reset !more feature
|
||||
if args[0] in self.srv.moremessages:
|
||||
del self.srv.moremessages[args[0]]
|
||||
|
||||
def exec_hook(self, msg):
|
||||
self.send_cmd(["HOOK", msg.raw])
|
||||
|
@ -22,12 +22,14 @@ import sys
|
||||
class Response:
|
||||
def __init__(self, sender, message=None, channel=None, nick=None, server=None,
|
||||
nomore="No more message", title=None, more="(suite) ", count=None,
|
||||
shown_first_count=-1):
|
||||
ctcp=False, shown_first_count=-1):
|
||||
self.nomore = nomore
|
||||
self.more = more
|
||||
self.rawtitle = title
|
||||
self.server = server
|
||||
self.messages = list()
|
||||
self.alone = True
|
||||
self.ctcp = ctcp
|
||||
if message is not None:
|
||||
self.append_message(message, shown_first_count=shown_first_count)
|
||||
self.elt = 0 # Next element to display
|
||||
|
234
server.py
234
server.py
@ -16,37 +16,16 @@
|
||||
# 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 errno
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
import channel
|
||||
import DCC
|
||||
import message
|
||||
import xmlparser
|
||||
|
||||
class Server(threading.Thread):
|
||||
def __init__(self, node, nick, owner, realname, socket = None):
|
||||
def __init__(self, socket = None):
|
||||
self.stop = False
|
||||
self.stopping = threading.Event()
|
||||
self.nick = nick
|
||||
self.owner = owner
|
||||
self.realname = realname
|
||||
self.s = socket
|
||||
self.connected = self.s is not None
|
||||
self.node = node
|
||||
|
||||
self.listen_nick = True
|
||||
|
||||
self.dcc_clients = dict()
|
||||
|
||||
self.channels = dict()
|
||||
for chn in node.getNodes("channel"):
|
||||
chan = channel.Channel(chn, self)
|
||||
self.channels[chan.name] = chan
|
||||
|
||||
self.moremessages = dict()
|
||||
|
||||
@ -55,41 +34,17 @@ class Server(threading.Thread):
|
||||
def isDCC(self, to=None):
|
||||
return to is not None and to in self.dcc_clients
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
if self.node.hasAttribute("server"):
|
||||
return self.node["server"]
|
||||
else:
|
||||
return "localhost"
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
if self.node.hasAttribute("port"):
|
||||
return self.node.getInt("port")
|
||||
else:
|
||||
return "6667"
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
if self.node.hasAttribute("password"):
|
||||
return self.node["password"]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def allow_all(self):
|
||||
return self.node.hasAttribute("allowall") and self.node["allowall"] == "true"
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
"""Convert common IP representation to little-endian integer representation"""
|
||||
sum = 0
|
||||
if self.node.hasAttribute("ip"):
|
||||
for b in self.node["ip"].split("."):
|
||||
sum = 256 * sum + int(b)
|
||||
ip = self.node["ip"]
|
||||
else:
|
||||
#TODO: find the external IP
|
||||
pass
|
||||
ip = "0.0.0.0"
|
||||
for b in ip.split("."):
|
||||
sum = 256 * sum + int(b)
|
||||
return sum
|
||||
|
||||
def toIP(self, input):
|
||||
@ -101,27 +56,22 @@ class Server(threading.Thread):
|
||||
input = (input - mod) / 256
|
||||
return ip[:len(ip) - 1]
|
||||
|
||||
@property
|
||||
def autoconnect(self):
|
||||
if self.node.hasAttribute("autoconnect"):
|
||||
value = self.node["autoconnect"].lower()
|
||||
return value != "no" and value != "off" and value != "false"
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.host + ":" + str(self.port)
|
||||
|
||||
def send_pong(self, cnt):
|
||||
self.s.send(("PONG %s\r\n" % cnt).encode ())
|
||||
"""Gives the server identifiant"""
|
||||
raise NotImplemented()
|
||||
|
||||
def msg_treated(self, origin):
|
||||
"""Do nothing, here for implement abstract class"""
|
||||
pass
|
||||
"""Action done on server when a message was treated"""
|
||||
raise NotImplemented()
|
||||
|
||||
def send_response(self, res, origin):
|
||||
if res.channel is not None and res.channel != self.nick:
|
||||
"""Analyse a Response and send it"""
|
||||
# TODO: how to send a CTCP message to a different person
|
||||
if res.ctcp:
|
||||
self.send_ctcp(res.sender, res.get_message())
|
||||
|
||||
elif res.channel is not None and res.channel != self.nick:
|
||||
self.send_msg(res.channel, res.get_message())
|
||||
|
||||
if not res.alone:
|
||||
@ -136,129 +86,62 @@ class Server(threading.Thread):
|
||||
"""Send a message as CTCP response"""
|
||||
if msg is not None and to is not None:
|
||||
for line in msg.split("\n"):
|
||||
if self.s is None:
|
||||
print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, msg))
|
||||
traceback.print_stack()
|
||||
elif line != "":
|
||||
self.s.send (("%s %s :\x01%s\x01%s" % (cmd, to.split("!")[0], line, endl)).encode ())
|
||||
if line != "":
|
||||
self.send_msg_final(to.split("!")[0], "\x01" + line + "\x01", cmd, endl)
|
||||
|
||||
def send_dcc(self, msg, to):
|
||||
"""Send a message through DCC connection"""
|
||||
if msg is not None and to is not None:
|
||||
realname = to.split("!")[1]
|
||||
if realname not in self.dcc_clients.keys():
|
||||
d = dcc.DCC(self, to)
|
||||
self.dcc_clients[realname] = d
|
||||
self.dcc_clients[realname].send_dcc(msg)
|
||||
|
||||
raise NotImplemented()
|
||||
|
||||
def send_msg_final(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message without checks"""
|
||||
if channel == self.nick:
|
||||
print ("\033[1;35mWarning:\033[0m Nemubot talks to himself: %s" % msg)
|
||||
traceback.print_stack()
|
||||
if msg is not None and channel is not None:
|
||||
for line in msg.split("\n"):
|
||||
if line != "":
|
||||
if self.s is None:
|
||||
print ("\033[1;35mWarning:\033[0m Attempt to send message on a non connected server: %s: %s" % (self.id, msg))
|
||||
traceback.print_stack()
|
||||
elif len(line) < 442:
|
||||
self.s.send (("%s %s :%s%s" % (cmd, channel, line, endl)).encode ())
|
||||
else:
|
||||
self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"...", endl)).encode ())
|
||||
"""Send a message without checks or format"""
|
||||
raise NotImplemented()
|
||||
|
||||
def send_msg_usr(self, user, msg):
|
||||
"""Send a message to a user instead of a channel"""
|
||||
if user is not None and user[0] != "#":
|
||||
realname = user.split("!")[1]
|
||||
if realname in self.dcc_clients:
|
||||
self.send_dcc(msg, user)
|
||||
else:
|
||||
self.send_msg_final(user.split('!')[0], msg)
|
||||
raise NotImplemented()
|
||||
|
||||
def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message to a channel"""
|
||||
if self.accepted_channel(channel):
|
||||
self.send_msg_final(channel, msg, cmd, endl)
|
||||
if msg is not None:
|
||||
for line in msg.split("\n"):
|
||||
if line != "":
|
||||
self.send_msg_final(channel, line, cmd, endl)
|
||||
|
||||
def send_msg_verified(self, sender, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message to a channel, only if the source user is on this channel too"""
|
||||
if self.accepted_channel(channel, sender):
|
||||
self.send_msg_final(channel, msg, cmd, endl)
|
||||
"""A more secure way to send messages"""
|
||||
raise NotImplemented()
|
||||
|
||||
def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"):
|
||||
"""Send a message to all channels on this server"""
|
||||
for channel in self.channels.keys():
|
||||
self.send_msg(channel, msg, cmd, endl)
|
||||
|
||||
|
||||
def accepted_channel(self, chan, sender = None):
|
||||
"""Return True if the channel (or the user) is authorized"""
|
||||
if self.allow_all:
|
||||
return True
|
||||
elif self.listen_nick:
|
||||
return (chan in self.channels and (sender is None or sender in
|
||||
self.channels[chan].people)
|
||||
) or chan == self.nick
|
||||
else:
|
||||
return chan in self.channels and (sender is None or sender
|
||||
in self.channels[chan].people)
|
||||
raise NotImplemented()
|
||||
|
||||
def disconnect(self):
|
||||
"""Close the socket with the server and all DCC client connections"""
|
||||
"""Close the socket with the server"""
|
||||
if self.connected:
|
||||
self.stop = True
|
||||
self.s.shutdown(socket.SHUT_RDWR)
|
||||
|
||||
#Close all DCC connection
|
||||
for clt in self.dcc_clients:
|
||||
self.dcc_clients[clt].disconnect()
|
||||
|
||||
self.stopping.wait()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def kill(self):
|
||||
"""Just stop the main loop, don't close the socket directly"""
|
||||
if self.connected:
|
||||
self.stop = True
|
||||
self.connected = False
|
||||
#Send a message in order to close the socket
|
||||
self.s.send(("WHO %s\r\n" % self.nick).encode ())
|
||||
self.s.send(("Bye!\r\n" % self.nick).encode ())
|
||||
self.stopping.wait()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def join(self, chan, password = None):
|
||||
"""Join a channel"""
|
||||
if chan is not None and self.connected and chan not in self.channels:
|
||||
chn = xmlparser.module_state.ModuleState("channel")
|
||||
chn["name"] = chan
|
||||
chn["password"] = password
|
||||
self.node.addChild(chn)
|
||||
self.channels[chan] = channel.Channel(chn, self)
|
||||
if password is not None:
|
||||
self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode ())
|
||||
else:
|
||||
self.s.send(("JOIN %s\r\n" % chan).encode ())
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def leave(self, chan):
|
||||
"""Leave a channel"""
|
||||
if chan is not None and self.connected and chan in self.channels:
|
||||
self.s.send(("PART %s\r\n" % self.channels[chan].name).encode ())
|
||||
del self.channels[chan]
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def launch(self, context, verb=True):
|
||||
def launch(self, receive_action, verb=True):
|
||||
"""Connect to the server if it is no yet connected"""
|
||||
self.context = context
|
||||
self._receive_action = receive_action
|
||||
if not self.connected:
|
||||
self.stop = False
|
||||
self.start()
|
||||
@ -266,54 +149,7 @@ class Server(threading.Thread):
|
||||
print (" Already connected.")
|
||||
|
||||
def treat_msg(self, line, private=False):
|
||||
self.context.receive_message(self, line, private)
|
||||
self._receive_action(self, line, private)
|
||||
|
||||
def run(self):
|
||||
if not self.connected:
|
||||
self.s = socket.socket() #Create the socket
|
||||
try:
|
||||
self.s.connect((self.host, self.port)) #Connect to server
|
||||
except socket.error as e:
|
||||
self.s = None
|
||||
print ("\033[1;31mError:\033[0m Unable to connect to %s:%d: %s"
|
||||
% (self.host, self.port, os.strerror(e.errno)))
|
||||
return
|
||||
self.stopping.clear()
|
||||
self.connected = True
|
||||
|
||||
if self.password != None:
|
||||
self.s.send(b"PASS " + self.password.encode () + b"\r\n")
|
||||
self.s.send(("NICK %s\r\n" % self.nick).encode ())
|
||||
self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host,
|
||||
self.realname)).encode ())
|
||||
print ("Connection to %s:%d completed" % (self.host, self.port))
|
||||
|
||||
if len(self.channels) > 0:
|
||||
for chn in self.channels.keys():
|
||||
if self.channels[chn].password is not None:
|
||||
self.s.send(("JOIN %s %s\r\n" % (self.channels[chn].name,
|
||||
self.channels[chn].password)).encode ())
|
||||
else:
|
||||
self.s.send(("JOIN %s\r\n" %
|
||||
self.channels[chn].name).encode ())
|
||||
print ("Listen to channels: %s" % ' '.join (self.channels.keys()))
|
||||
|
||||
readbuffer = b'' #Here we store all the messages from server
|
||||
while not self.stop:
|
||||
raw = self.s.recv(1024) #recieve server messages
|
||||
if not raw:
|
||||
break
|
||||
readbuffer = readbuffer + raw
|
||||
temp = readbuffer.split(b'\n')
|
||||
readbuffer = temp.pop()
|
||||
|
||||
for line in temp:
|
||||
self.treat_msg(line)
|
||||
|
||||
if self.connected:
|
||||
self.s.close()
|
||||
self.connected = False
|
||||
print ("Server `%s' successfully stopped." % self.id)
|
||||
self.stopping.set()
|
||||
#Rearm Thread
|
||||
threading.Thread.__init__(self)
|
||||
raise NotImplemented()
|
||||
|
Loading…
Reference in New Issue
Block a user