Introducing a new nemubot architecture for servers and messages treatment: messages have no more context or server

This commit is contained in:
Némunaire 2012-11-02 12:10:37 +01:00
parent f00dfc82f7
commit a62940380e
12 changed files with 471 additions and 327 deletions

53
DCC.py
View File

@ -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
View 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
View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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:

View File

@ -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.")

View File

@ -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()

View File

@ -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])

View File

@ -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
View File

@ -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()