1
0
Fork 0

Change add_server behaviour, fix IRC parameters parsing, can use with Python statement for managing server scope

This commit is contained in:
nemunaire 2014-10-09 07:37:52 +02:00
parent f9ee1fe898
commit 4dd837cf4b
8 changed files with 249 additions and 68 deletions

19
bot.py
View File

@ -25,7 +25,7 @@ import threading
import time
import uuid
__version__ = '3.4.dev1'
__version__ = '3.4.dev2'
__author__ = 'nemunaire'
from consumer import Consumer, EventConsumer, MessageConsumer
@ -33,8 +33,6 @@ from event import ModuleEvent
from hooks.messagehook import MessageHook
from hooks.manager import HooksManager
from networkbot import NetworkBot
from server.IRC import IRC as IRCServer
from server.DCC import DCC
logger = logging.getLogger("nemubot.bot")
@ -312,13 +310,20 @@ class Bot(threading.Thread):
c.start()
def add_server(self, node, nick, owner, realname):
"""Add a new server to the context"""
srv = IRCServer(node, nick, owner, realname)
def add_server(self, srv, autoconnect=False):
"""Add a new server to the context
Arguments:
srv -- a concrete AbstractServer instance
autoconnect -- connect after add?
"""
if srv.id not in self.servers:
self.servers[srv.id] = srv
srv.open()
if autoconnect:
srv.open()
return True
else:
return False

View File

@ -19,8 +19,9 @@
import traceback
import sys
from networkbot import NetworkBot
from hooks import hook
from message import TextMessage
from networkbot import NetworkBot
nemubotversion = 3.4
NODATA = True
@ -198,7 +199,7 @@ def send(data, toks, context, prompt):
print ("send: not enough arguments.")
return
srv.send_msg_final(chan, toks[rd])
srv.send_response(TextMessage(" ".join(toks[rd:]), server=None, to=[chan]))
return "done"
@hook("prompt_cmd", "zap")

View File

@ -1,8 +1,8 @@
#!/usr/bin/python3
#!/usr/bin/env python3.2
# -*- coding: utf-8 -*-
# Nemubot is a modulable IRC bot, built around XML configuration files.
# Copyright (C) 2012 Mercier Pierre-Olivier
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2014 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
@ -21,7 +21,6 @@ import imp
import logging
import os
import sys
import traceback
import bot
import prompt

View File

@ -19,10 +19,12 @@
import imp
import logging
import os
import xmlparser
logger = logging.getLogger("nemubot.prompt.builtins")
from server.IRC import IRC as IRCServer
import xmlparser
def end(toks, context, prompt):
"""Quit the prompt for reload or exit"""
if toks[0] == "refresh":
@ -67,16 +69,58 @@ def load_file(filename, context):
or config.getName() == "nemubotconfig"):
# Preset each server in this file
for server in config.getNodes("server"):
ip = server["ip"] if server.hasAttribute("ip") else config["ip"]
nick = server["nick"] if server.hasAttribute("nick") else config["nick"]
owner = server["owner"] if server.hasAttribute("owner") else config["owner"]
realname = server["realname"] if server.hasAttribute("realname") else config["realname"]
if context.add_server(server, nick, owner, realname):
print("Server `%s:%s' successfully added." %
(server["host"], server["port"]))
opts = {
"host": server["host"],
"ssl": server.hasAttribute("ssl") and server["ssl"].lower() == "true",
"nick": server["nick"] if server.hasAttribute("nick") else config["nick"],
"owner": server["owner"] if server.hasAttribute("owner") else config["owner"],
}
# Optional keyword arguments
for optional_opt in [ "port", "realname", "password", "encoding", "caps" ]:
if server.hasAttribute(optional_opt):
opts[optional_opt] = server[optional_opt]
elif optional_opt in config:
opts[optional_opt] = config[optional_opt]
# Command to send on connection
if "on_connect" in server:
def on_connect():
yield server["on_connect"]
opts["on_connect"] = on_connect
# Channels to autojoin on connection
if server.hasNode("channel"):
opts["channels"] = list()
for chn in server.getNodes("channel"):
opts["channels"].append((chn["name"], chn["password"]) if chn["password"] is not None else chn["name"])
# Server/client capabilities
if "caps" in server or "caps" in config:
capsl = (server["caps"] if server.hasAttribute("caps") else config["caps"]).lower()
if capsl == "no" or capsl == "off" or capsl == "false":
opts["caps"] = None
else:
opts["caps"] = capsl.split(',')
else:
print("Server `%s:%s' already added, skiped." %
(server["host"], server["port"]))
opts["caps"] = list()
# Bind the protocol asked to the corresponding implementation
if "protocol" not in server or server["protocol"] == "irc":
srvcls = IRCServer
else:
raise Exception("Unhandled protocol '%s'" % server["protocol"])
# Initialize the server
srv = srvcls(**opts)
# Add the server in the context
if context.add_server(srv,
"autoconnect" in server and server["autoconnect"].lower() != "false"):
print("Server '%s' successfully added." % srv.id)
else:
print("Can't add server '%s'." % srv.id)
# Load module and their configuration
for mod in config.getNodes("module"):

View File

@ -26,37 +26,46 @@ from channel import Channel
import message
from message.printer.IRC import IRC as IRCPrinter
from server.socket import SocketServer
import tools
class IRC(SocketServer):
def __init__(self, node, nick, owner, realname):
self.id = nick + "@" + node["host"] + ":" + node["port"]
self.printer = IRCPrinter
SocketServer.__init__(self,
node["host"],
node["port"],
node["password"],
node.hasAttribute("ssl") and node["ssl"].lower() == "true")
def __init__(self, owner, nick="nemubot", host="localhost", port=6667,
ssl=False, password=None, realname="Nemubot",
encoding="utf-8", caps=None, channels=list(),
on_connect=None):
"""Prepare a connection with an IRC server
Keyword arguments:
owner -- bot's owner
nick -- bot's nick
host -- host to join
port -- port on the host to reach
ssl -- is this server using a TLS socket
password -- if a password is required to connect to the server
realname -- the bot's realname
encoding -- the encoding used on the whole server
caps -- client capabilities to register on the server
channels -- list of channels to join on connection (if a channel is password protected, give a tuple: (channel_name, password))
on_connect -- generator to call when connection is done
"""
self.id = nick + "@" + host + ":" + port
self.printer = IRCPrinter
SocketServer.__init__(self, host=host, port=port, ssl=ssl)
self.password = password
self.nick = nick
self.owner = owner
self.realname = realname
#Keep a list of connected channels
self.encoding = encoding
# Keep a list of joined channels
self.channels = dict()
if node.hasAttribute("encoding"):
self.encoding = node["encoding"]
else:
self.encoding = "utf-8"
if node.hasAttribute("caps"):
if node["caps"].lower() == "no":
self.capabilities = None
else:
self.capabilities = node["caps"].split(",")
else:
self.capabilities = list()
# Server/client capabilities
self.capabilities = caps
# Register CTCP capabilities
self.ctcp_capabilities = dict()
@ -68,7 +77,7 @@ class IRC(SocketServer):
def _ctcp_dcc(msg, cmds):
"""Response to DCC CTCP message"""
try:
ip = srv.toIP(int(cmds[3]))
ip = tools.toIP(int(cmds[3]))
port = int(cmds[4])
conn = DCC(srv, msg.sender)
except:
@ -98,6 +107,7 @@ class IRC(SocketServer):
self.logger.debug("CTCP capabilities setup: %s", ", ".join(self.ctcp_capabilities))
# Register hooks on some IRC CMD
self.hookscmd = dict()
@ -109,14 +119,15 @@ class IRC(SocketServer):
# Respond to 001
def _on_connect(msg):
# First, send user defined command
if node.hasAttribute("on_connect"):
self.write(node["on_connect"])
if on_connect is not None:
for oc in on_connect():
self.write(oc)
# Then, JOIN some channels
for chn in node.getNodes("channel"):
if chn["password"] is not None:
self.write("JOIN %s %s" % (chn["name"], chn["password"]))
for chn in channels:
if isinstance(chn, tuple):
self.write("JOIN %s %s" % chn)
else:
self.write("JOIN %s" % chn["name"])
self.write("JOIN %s" % chn)
self.hookscmd["001"] = _on_connect
# Respond to ERROR
@ -141,9 +152,9 @@ class IRC(SocketServer):
def _on_join(msg):
if len(msg.params) == 0: return
for chname in msg.params[0].split(b","):
for chname in msg.decode(msg.params[0]).split(","):
# Register the channel
chan = Channel(msg.decode(chname))
chan = Channel(chname)
self.channels[chname] = chan
self.hookscmd["JOIN"] = _on_join
# Respond to PART
@ -197,6 +208,8 @@ class IRC(SocketServer):
self.hookscmd["PRIVMSG"] = _on_ctcp
# Open/close
def _open(self):
if SocketServer._open(self):
if self.password is not None:
@ -214,6 +227,10 @@ class IRC(SocketServer):
return SocketServer._close(self)
# Writes: as inherited
# Read
def read(self):
for line in SocketServer.read(self):
msg = IRCMessage(line, self.encoding)
@ -226,6 +243,8 @@ class IRC(SocketServer):
yield mes
# Parsing stuff
mgx = re.compile(b'''^(?:@(?P<tags>[^ ]+)\ )?
(?::(?P<prefix>
(?P<nick>[^!@ ]+)
@ -269,7 +288,7 @@ class IRCMessage:
self.cmd = self.decode(p.group("command"))
# Parse params
if p.group("params") is not None:
if p.group("params") is not None and p.group("params") != b'':
for param in p.group("params").strip().split(b' '):
self.params.append(param)
@ -278,7 +297,13 @@ class IRCMessage:
def add_tag(self, key, value=None):
"""Add an IRCv3.2 Message Tags"""
"""Add an IRCv3.2 Message Tags
Arguments:
key -- tag identifier (unique for the message)
value -- optional value for the tag
"""
# Treat special tags
if key == "time":
value = datetime.fromtimestamp(calendar.timegm(time.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")), timezone.utc)
@ -289,11 +314,17 @@ class IRCMessage:
@property
def is_ctcp(self):
"""Analyze a message, to determine if this is a CTCP one"""
return self.cmd == "PRIVMSG" and len(self.params) == 2 and len(self.params[1]) > 1 and (self.params[1][0] == 0x01 or self.params[1][1] == 0x01)
def decode(self, s):
"""Decode the content string usign a specific encoding"""
"""Decode the content string usign a specific encoding
Argument:
s -- string to decode
"""
if isinstance(s, bytes):
try:
s = s.decode()
@ -326,6 +357,12 @@ class IRCMessage:
def to_message(self, srv):
"""Convert to one of concrete implementation of AbstractMessage
Argument:
srv -- the server from the message was received
"""
if self.cmd == "PRIVMSG" or self.cmd == "NOTICE":
receivers = self.decode(self.params[0]).split(',')
@ -344,6 +381,13 @@ class IRCMessage:
else:
text = self.decode(self.params[1])
if text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":":
designated = srv.nick
text = text[len(srv.nick) + 1:].strip()
else:
designated = None
# Is this a command?
if len(text) > 1 and text[0] == '!':
text = text[1:].strip()
@ -355,10 +399,11 @@ class IRCMessage:
return message.Command(cmd=args[0], args=args[1:], **common_args)
elif text.find(srv.nick) == 0 and len(text) > len(srv.nick) + 2 and text[len(srv.nick)] == ":":
text = text[len(srv.nick) + 1:].strip()
return message.DirectAsk(designated=srv.nick, message=text, **common_args)
# Is this an ask for this bot?
elif designated is not None:
return message.DirectAsk(designated=designated, message=text, **common_args)
# Normal message
else:
return message.TextMessage(message=text, **common_args)

View File

@ -38,6 +38,9 @@ class AbstractServer(io.IOBase):
send_callback -- Callback when developper want to send a message
"""
if not hasattr(self, "id"):
raise Exception("No id defined for this server. Please set one!")
self.logger = logging.getLogger("nemubot.server." + self.id)
self._sending_queue = queue.Queue()
if send_callback is not None:
@ -46,8 +49,20 @@ class AbstractServer(io.IOBase):
self._send_callback = self._write_select
# Open/close
def __enter__(self):
self.open()
return self
def __exit__(self, type, value, traceback):
self.close()
def open(self):
"""Generic open function that register the server un _rlist in case of successful _open"""
self.logger.info("Opening connection to %s", self.id)
if self._open():
_rlist.append(self)
_xlist.append(self)
@ -55,6 +70,7 @@ class AbstractServer(io.IOBase):
def close(self):
"""Generic close function that register the server un _{r,w,x}list in case of successful _close"""
self.logger.info("Closing connection to %s", self.id)
if self._close():
if self in _rlist:
_rlist.remove(self)
@ -64,10 +80,18 @@ class AbstractServer(io.IOBase):
_xlist.remove(self)
# Writes
def write(self, message):
"""Send a message to the server using send_callback"""
"""Asynchronymously send a message to the server using send_callback
Argument:
message -- message to send
"""
self._send_callback(message)
def write_select(self):
"""Internal function used by the select function"""
try:
@ -79,20 +103,27 @@ class AbstractServer(io.IOBase):
except queue.Empty:
pass
def _write_select(self, message):
"""Send a message to the server safely through select"""
"""Send a message to the server safely through select
Argument:
message -- message to send
"""
self._sending_queue.put(self.format(message))
self.logger.debug("Message '%s' appended to Queue", message)
if self not in _wlist:
_wlist.append(self)
def exception(self):
"""Exception occurs in fd"""
print("Unhandle file descriptor exception on server " + self.id)
def send_response(self, response):
"""Send a formated Message class"""
"""Send a formated Message class
Argument:
response -- message to send
"""
if response is None:
return
@ -104,3 +135,11 @@ class AbstractServer(io.IOBase):
vprnt = self.printer()
response.accept(vprnt)
self.write(vprnt.pp)
# Exceptions
def exception(self):
"""Exception occurs in fd"""
self.logger.warning("Unhandle file descriptor exception on server %s",
self.id)

View File

@ -23,23 +23,28 @@ from server import AbstractServer
class SocketServer(AbstractServer):
def __init__(self, host, port=6667, password=None, ssl=False):
def __init__(self, host, port, ssl=False):
AbstractServer.__init__(self)
self.host = host
self.port = int(port)
self.password = password
self.ssl = ssl
self.socket = None
self.readbuffer = b''
def fileno(self):
return self.socket.fileno() if self.socket else None
@property
def connected(self):
"""Indicator of the connection aliveness"""
return self.socket is not None
# Open/close
def _open(self):
# Create the socket
self.socket = socket.socket()
@ -60,6 +65,7 @@ class SocketServer(AbstractServer):
return True
def _close(self):
self._sending_queue.join()
if self.connected:
@ -71,18 +77,27 @@ class SocketServer(AbstractServer):
self.socket = None
return True
# Write
def _write(self, cnt):
if not self.connected: return
self.socket.send(cnt)
def format(self, txt):
if isinstance(txt, bytes):
return txt + b'\r\n'
else:
return txt.encode() + b'\r\n'
# Read
def read(self):
if not self.connected: return
raw = self.socket.recv(1024)
temp = (self.readbuffer + raw).split(b'\r\n')
self.readbuffer = temp.pop()

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2014 nemunaire
#
# 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 imp
def intToIP(n):
ip = ""
for i in range(0,4):
mod = n % 256
ip = "%d.%s" % (mod, ip)
n = (n - mod) / 256
return ip[:len(ip) - 1]
def ipToInt(ip):
sum = 0
for b in ip.split("."):
sum = 256 * sum + int(b)
return sum