Change add_server behaviour, fix IRC parameters parsing, can use with Python statement for managing server scope
This commit is contained in:
parent
f9ee1fe898
commit
4dd837cf4b
17
bot.py
17
bot.py
@ -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
|
||||
if autoconnect:
|
||||
srv.open()
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
print("Server `%s:%s' already added, skiped." %
|
||||
(server["host"], server["port"]))
|
||||
opts["caps"] = capsl.split(',')
|
||||
else:
|
||||
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"):
|
||||
|
117
server/IRC.py
117
server/IRC.py
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user