summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsand <daniel@spatof.org>2013-05-10 14:54:40 (GMT)
committer sand <daniel@spatof.org>2013-05-10 14:54:40 (GMT)
commit0d6762c35166b87e4ba87ac6bc6003bdcd7de1d5 (patch)
tree07dd89232429a8e012a91692e619e06cb21bd800
parent36eee3df2da83c8122824dd53f170d27710fd1da (diff)
Import del nuovo codice
-rw-r--r--BUGS.txt40
-rw-r--r--pinolo/__init__.py4
-rw-r--r--pinolo/__init__.py.OLD192
-rw-r--r--pinolo/bot.py202
-rw-r--r--pinolo/config.py147
-rw-r--r--pinolo/irc.py801
-rw-r--r--pinolo/main.py118
-rw-r--r--pinolo/options.py271
-rw-r--r--pinolo/plugins/__init__.py35
-rw-r--r--pinolo/signals.py62
-rw-r--r--pinolo/tasks.py47
-rw-r--r--pinolo/utils.py0
-rw-r--r--pinolo/utils/network.py94
-rw-r--r--pinolo/utils/text.py2
-rw-r--r--setup.py8
15 files changed, 691 insertions, 1332 deletions
diff --git a/BUGS.txt b/BUGS.txt
deleted file mode 100644
index 4d039a6..0000000
--- a/BUGS.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-00:55:50 01/02/12 pinolo.irc.azzurra INFO Reconnecting to tophost.azzurra.org:6667 (azzurra) [0/188]
-00:59:00 01/02/12 pinolo.irc.azzurra ERROR Failed connection to: tophost.azzurra.org:6667 (ETIMEDOUT Connection timed out)
-00:59:00 01/02/12 pinolo.irc.azzurra WARNING I'll be quiet for 120 seconds before trying to connect again
-01:01:03 01/02/12 pinolo.irc.azzurra INFO Connected to: tophost.azzurra.org:6667 (azzurra)
-Traceback (most recent call last):
- File "/usr/lib/pymodules/python2.6/gevent/greenlet.py", line 388, in run
- result = self._run(*self.args, **self.kwargs)
- File "/home/sand/pinolo_git/pinolo/irc.py", line 244, in connect
- self.event_loop()
- File "/home/sand/pinolo_git/pinolo/irc.py", line 354, in event_loop
- self.connect()
- File "/home/sand/pinolo_git/pinolo/irc.py", line 244, in connect
- self.event_loop()
- File "/home/sand/pinolo_git/pinolo/irc.py", line 354, in event_loop
- self.connect()
- File "/home/sand/pinolo_git/pinolo/irc.py", line 244, in connect
- self.event_loop()
- File "/home/sand/pinolo_git/pinolo/irc.py", line 280, in event_loop
- line = self.stream.readline()
- File "/usr/lib/python2.6/socket.py", line 444, in readline
- data = self._sock.recv(self._rbufsize)
- File "/usr/lib/pymodules/python2.6/gevent/socket.py", line 327, in recv
- return self._sock.recv(*args)
-error: [Errno 104] Connection reset by peer
-<Greenlet at 0x256c738: <bound method IRCClient.connect of IRCClient(name: 'azzurra')>> failed with error
-
-Traceback (most recent call last):
- File "/usr/lib/pymodules/python2.6/gevent/greenlet.py", line 388, in run
- result = self._run(*self.args, **self.kwargs)
- File "/home/sand/pinolo_git/pinolo/irc.py", line 401, in output_loop
- self.stream.flush()
- File "/usr/lib/python2.6/socket.py", line 297, in flush
- self._sock.sendall(buffer(data, write_offset, buffer_size))
- File "/usr/lib/pymodules/python2.6/gevent/socket.py", line 388, in sendall
- data_sent += self.send(data[data_sent:], flags)
- File "/usr/lib/pymodules/python2.6/gevent/socket.py", line 369, in send
- return self._sock.send(data, flags)
-error: [Errno 32] Broken pipe
-<Greenlet at 0x256c5a0: <bound method IRCClient.output_loop of IRCClient(name: 'azzurra')>> failed with error
-
diff --git a/pinolo/__init__.py b/pinolo/__init__.py
index 7d28b2a..5e8cf3a 100644
--- a/pinolo/__init__.py
+++ b/pinolo/__init__.py
@@ -1,7 +1,7 @@
-VERSION = (0, 9, 2)
+VERSION = (0, 10, 1)
STR_VERSION = '.'.join([str(x) for x in VERSION])
FULL_VERSION = "Pinolo-" + STR_VERSION
-SOURCE_URL = "http://code.dyne.org/?r=pinolo"
+SOURCE_URL = "http://git.spatof.org/pinolo.git"
USER_AGENT = "Pinolo/%s +%s" % (STR_VERSION, SOURCE_URL)
DEFAULT_DATABASE_FILENAME = 'db.sqlite'
diff --git a/pinolo/__init__.py.OLD b/pinolo/__init__.py.OLD
deleted file mode 100644
index eefe8fe..0000000
--- a/pinolo/__init__.py.OLD
+++ /dev/null
@@ -1,192 +0,0 @@
-import logging
-from collections import namedtuple
-import optparse
-
-from yapsy.IPlugin import IPlugin
-from yapsy.PluginManager import PluginManager
-
-
-# to track individual IRC users
-IRCUser = namedtuple('IRCUser', 'nickname ident hostname')
-Configuration = namedtuple('Configuration',
- 'quotes_db xapian_db pidfile servers')
-
-# OptionParser subclassed for IRC commands
-class OptionParserError(Exception): pass
-
-class MyOptionParser(optparse.OptionParser):
- """An OptionParser which raises an OptionParserError instead of sys.exit()"""
-
- def error(self, msg):
- """Raises OptionParserError with the error message"""
-
- raise OptionParserError(msg)
-
- def exit(self, status=0, msg=None):
- """Raises OptionParserError with the error message"""
- raise OptionParserError(msg)
-
- def print_help(self, file=None):
- msg = self.format_help().encode('utf-8', "replace")
- # msg = msg.split("\n")
-
- raise OptionParserError(msg)
-
-class Request(object):
- """An IRC request object.
-
- This encapsulate all the informations needed to handle a request coming from
- an IRC user; any plugin can use reply() to reply directly to the user or
- channel where the command was issued.
-
- Arguments:
- - ``client``: a Pinolo instance
- - ``author``: an IRCUser instance (user who issued the command)
- - ``channel``: channel where the command was issued, if any
- - ``reply_to``: who to respond to (channel or user)
- - ``command``: the command issued
- - ``arguments``: a list with arguments, if any (shlex splitted)
-
- """
- def __init__(self, client, author, channel, reply_to, command, arguments):
- self.client = client
- self.author = author
- self.channel = channel
- self.reply_to = reply_to
- self.command = command
- self.arguments = arguments[:]
-
- def reply(self, message, prefix=True):
- """Con questo barbatrucco posso vomitare su IRC anche le newline senza sbattimento"""
- if "\n" in message:
- for line in message.split("\n"):
- self._reply(line, prefix)
-
- else:
- self._reply(message, prefix)
-
- def _reply(self, message, prefix=True):
- """Reply to channel or user (see self.reply_to).
-
- With ``prefix`` set to False will NOT prefix the user nickname to the
- text.
- """
-
- if (self.reply_to.startswith('#')
- and prefix):
- self.client.reply(self.reply_to, "%s: %s" % (self.author.nickname,
- message))
- else:
- self.client.reply(self.reply_to, message)
-
-
-# Yapsy Plugin Categories
-class BasePlugin(IPlugin): pass
-class UndefinedPlugin(BasePlugin): pass
-class CommandPlugin(BasePlugin): pass
-
-class PluginActivationError(Exception): pass
-
-
-class MyPluginManager(PluginManager):
- """An Extended PluginManager.
-
- My PluginManager will activate() plugins passing a new parameter
- (*configuration*) which can be used to configure the plugin inizialization.
-
- A new parameter ``fail_on_error` (default: **True**) on loadPlugins() will
- raise any Exception raised during a plugin inizialization phase.
- """
-
- def activatePluginByName(self, name, configuration, category="Default"):
- """
- Activate a plugin corresponding to a given category + name.
-
- NEW PARAMETER: ``configuration``, a *dict* with the plugin
- configuration, passed to plugin's activate().
- """
-
- pta_item = self.getPluginByName(name, category)
-
- if pta_item is not None:
- plugin_to_activate = pta_item.plugin_object
-
- if plugin_to_activate is not None:
- logging.debug("Activating plugin: %s.%s"% (category,name))
- plugin_to_activate.activate(configuration)
- return plugin_to_activate
-
- return None
-
- def loadPlugins(self, callback=None, fail_on_error=True):
- """
- Load the candidate plugins that have been identified through a
- previous call to locatePlugins. For each plugin candidate
- look for its category, load it and store it in the appropriate
- slot of the ``category_mapping``.
-
- If a callback function is specified, call it before every load
- attempt. The ``plugin_info`` instance is passed as an argument to
- the callback.
-
- NEW PARAMETER: ``fail_on_error``, to raise an Exception on
- plugin loading failure.
- """
-# print "%s.loadPlugins" % self.__class__
- if not hasattr(self, '_candidates'):
- raise ValueError("locatePlugins must be called before loadPlugins")
-
- for candidate_infofile, candidate_filepath, plugin_info in self._candidates:
- # if a callback exists, call it before attempting to load
- # the plugin so that a message can be displayed to the
- # user
- if callback is not None:
- callback(plugin_info)
- # now execute the file and get its content into a
- # specific dictionnary
- candidate_globals = {"__file__":candidate_filepath+".py"}
- if "__init__" in os.path.basename(candidate_filepath):
- sys.path.append(plugin_info.path)
- try:
- #candidateMainFile = open(candidate_filepath+".py","r")
- # exec(candidateMainFile,candidate_globals)
- execfile(candidate_filepath + '.py',
- candidate_globals)
- except Exception,e:
- logging.debug("Unable to execute the code in plugin: %s" % candidate_filepath)
- logging.debug("\t The following problem occured: %s %s " % (os.linesep, e))
- if "__init__" in os.path.basename(candidate_filepath):
- sys.path.remove(plugin_info.path)
-
- # sand
- if fail_on_error:
- raise
- continue
-
- if "__init__" in os.path.basename(candidate_filepath):
- sys.path.remove(plugin_info.path)
- # now try to find and initialise the first subclass of the correct plugin interface
- for element in candidate_globals.itervalues():
- current_category = None
- for category_name in self.categories_interfaces:
- try:
- is_correct_subclass = issubclass(element, self.categories_interfaces[category_name])
- except:
- continue
- if is_correct_subclass:
- if element is not self.categories_interfaces[category_name]:
- current_category = category_name
- break
- if current_category is not None:
- if not (candidate_infofile in self._category_file_mapping[current_category]):
- # we found a new plugin: initialise it and search for the next one
- plugin_info.plugin_object = element()
- plugin_info.category = current_category
- self.category_mapping[current_category].append(plugin_info)
- self._category_file_mapping[current_category].append(candidate_infofile)
- current_category = None
- break
-
- # Remove candidates list since we don't need them any more and
- # don't need to take up the space
- delattr(self, '_candidates')
diff --git a/pinolo/bot.py b/pinolo/bot.py
new file mode 100644
index 0000000..0ee8161
--- /dev/null
+++ b/pinolo/bot.py
@@ -0,0 +1,202 @@
+# -*- encoding: utf-8 -*-
+"""
+ pinolo.bot
+ ~~~~~~~~~~
+
+ The Bot class contains functions to start and stop the bot, handle network
+ traffic and loading of plugins.
+
+ :copyright: (c) 2013 Daniel Kertesz
+ :license: BSD, see LICENSE for more details.
+"""
+import os
+import re
+import socket
+import select
+import errno
+import logging
+import Queue
+import pinolo.plugins
+from pinolo.signals import SignalDispatcher
+from pinolo.irc import IRCConnection
+
+
+log = logging.getLogger()
+
+
+class Bot(SignalDispatcher):
+ """Main Bot controller class.
+
+ Handle the network stuff, must be initialized with a configuration object.
+ """
+ def __init__(self, config):
+ SignalDispatcher.__init__(self)
+ self.config = config
+ self.connections = {}
+ self.connection_map = {}
+ self.coda = Queue.Queue()
+ self.plugins = []
+
+ for server in config['servers']:
+ server_config = config['servers'][server]
+ ircc = IRCConnection(server, server_config, self)
+ self.connections[server] = ircc
+
+ def start(self):
+ # Here we also load and activate the plugins
+ self.load_plugins()
+ self.activate_plugins()
+
+ self.signal_emit("pre_connect")
+
+ for conn_name, conn_obj in self.connections.iteritems():
+ print "Connecting to server: {0}".format(conn_name)
+ conn_obj.connect()
+
+ for conn_obj in self.connections.values():
+ self.connection_map[conn_obj.socket] = conn_obj
+
+ self.running = True
+ self.main_loop()
+
+ def main_loop(self):
+ """Main loop. Here we handle the network connections and buffers,
+ dispatching events to the IRC clients when needed."""
+
+ while self.running:
+ # For the select() call we must create two distinct groups of sockets
+ # to watch for: all the active sockets must be checked for reading, but
+ # only sockets with a non empty out-buffer will be checked for writing.
+ in_sockets = []
+ for connection in self.connections.values():
+ if connection.active:
+ in_sockets.append(connection.socket)
+
+ out_sockets = []
+ for connection in self.connections.values():
+ if len(connection.out_buffer):
+ out_sockets.append(connection.socket)
+
+ # This is ugly. XXX
+ if not in_sockets:
+ log.error("No more active connections. exiting...")
+ self.running = False
+ continue
+
+ # select() with 1 second timeout
+ readable, writable, exceptional = select.select(in_sockets, out_sockets, [], 1)
+
+ # Do the reading for the readable sockets
+ for s in readable:
+
+ # We must read data from socket until the syscall returns EAGAIN;
+ # when the OS signals EAGAIN the socket would block reading.
+ while True:
+ try:
+ chunk = s.recv(512)
+ except socket.error, e:
+ if e[0] == errno.EAGAIN:
+ break
+ else:
+ raise
+ if chunk == '':
+ self.connection_map[s].connected = False
+ self.connection_map[s].active = False
+ print "{0} disconnected".format(self.connection_map[s].name)
+ break
+ else:
+ self.connection_map[s].in_buffer += chunk
+ self.connection_map[s].check_in_buffer()
+
+ # scrive
+ for s, conn_obj in self.connection_map.iteritems():
+ # check if we got disconnected while reading from socket
+ if not conn_obj.connected:
+ continue
+
+ while len(conn_obj.out_buffer):
+ try:
+ sent = s.send(conn_obj.out_buffer)
+ except socket.error, e:
+ if e[0] == errno.EAGAIN:
+ break
+ else:
+ raise
+ conn_obj.out_buffer = conn_obj.out_buffer[sent:]
+
+ # controlla coda
+ try:
+ conn_name, goo = self.coda.get(False, 1)
+ except Queue.Empty, e:
+ pass
+ else:
+ for line in goo.split("\n"):
+ self.connections[conn_name].msg("#test", line)
+
+ # end while
+ def quit(self):
+ """Quit all connected clients"""
+ print "Dicono che devo da quitta"
+ for conn_obj in self.connections.values():
+ conn_obj.quit("Ctrl-C")
+
+ def load_plugins(self):
+ """Load all plugins from the plugins module"""
+
+ def my_import(name):
+ """Import by filename (taken from effbot)"""
+
+ m = __import__(name)
+ for n in name.split(".")[1:]:
+ m = getattr(m, n)
+ return m
+
+ plugins_dir = os.path.join(
+ os.path.abspath(os.path.dirname(__file__)),
+ "plugins")
+
+ self.signal_emit("pre_load_plugins")
+
+ for filename in os.listdir(plugins_dir):
+ if (not filename.endswith(".py") or
+ filename.startswith("_")):
+ continue
+ plugin_name = os.path.splitext(filename)[0]
+ log.debug("Loading plugin %s" % plugin_name)
+ try:
+ module = my_import("pinolo.plugins." + plugin_name)
+ # module = __import__("pinolo.plugins", plugin_name)
+ except Exception, e:
+ print "Failed to load plugin '%s'" % plugin_name
+ print "Exception: %s" % str(e)
+ import traceback
+ for line in traceback.format_exception_only(type(e), e):
+ print line
+ # continue
+ raise
+
+ self.signal_emit("plugin_loaded", plugin_name=plugin_name,
+ plugin_module=module)
+
+ self.signal_emit("post_load_plugins")
+
+ def activate_plugins(self):
+ """Call the activate method on all loaded plugins"""
+ for plugin_name, plugin_class in pinolo.plugins.registry:
+ log.debug("Activating plugin %s" % plugin_name)
+ p_obj = plugin_class(self)
+ p_obj.activate()
+ self.plugins.append(p_obj)
+ self.signal_emit("plugin_activated", plugin_name=plugin_name,
+ plugin_object=p_obj)
+
+ def deactivate_plugins(self):
+ """Call deactivate method on all the loaded plugins.
+
+ TODO: Should we also destroy the plugin objects?
+ """
+ for plugin in self.plugins:
+ plugin_name = plugin.__class__.__name__
+ plugin.deactivate()
+ self.signal_emit("plugin_deactivated", plugin_name=plugin_name,
+ plugin_object=plugin)
diff --git a/pinolo/config.py b/pinolo/config.py
index 6be7e20..bbc3188 100644
--- a/pinolo/config.py
+++ b/pinolo/config.py
@@ -1,127 +1,46 @@
-import os, sys, re
+# -*- encoding: utf-8 -*-
+"""
+ pinolo.config
+ ~~~~~~~~~~~~~
+
+ Configuration file handling. Boring stuff.
+
+ :copyright: (c) 2013 Daniel Kertesz
+ :license: BSD, see LICENSE for more details.
+"""
+import re
+import codecs
from ConfigParser import SafeConfigParser
-from collections import namedtuple
-from pinolo import DEFAULT_DATABASE_FILENAME
-class ConfigError(Exception): pass
-class ConfigFilesNotFound(ConfigError): pass
+r_comma = re.compile(r'\s*,\s+')
-class Config(object):
- def __init__(self, name, **entries):
- self.config_name = name
- self.__dict__.update(entries)
- def __repr__(self):
- return "<Config %s(%s)>" % (self.config_name,
- ', '.join(["%s = %r" % (name, value)
- for name, value in self.__dict__.iteritems()]))
-
-def unicode_or_None(obj):
- if type(obj) is str:
- return unicode(obj, 'utf-8', 'replace')
- else:
- return obj
-
-def read_config_files(filenames):
+def read_config_file(filename):
cfp = SafeConfigParser()
- ret = cfp.read(filenames)
- if not ret:
- raise RuntimeError("No config file found!")
-
- config = {}
- for section in cfp.sections():
- config[section] = dict(cfp.items(section))
-
- fix_config(config)
- general = config['general']
- cfg = GeneralConfig(nickname=general['nickname'] or 'pinolo',
- ident=general['ident'] or 'pinolo',
- realname=general['realname'] or 'Pinot di pinolo',
- datadir=general.get('datadir', os.getcwd()),
- googleapi=general.get('googleapi', None))
-
- for section in config.keys():
- if section == 'general': continue
- server = config[section]
- srv = ServerConfig(address=server['address'],
- port=server['port'],
- ssl=server.get('ssl', False),
- channels=server['channels'],
- nickserv=server.get('nickserv', None),
- password=server.get('password', None),
- nickname=server.get('nickname', cfg.nickname))
- cfg.servers[section] = srv
-
- return cfg
+ with codecs.open(filename, 'r', 'utf-8') as fd:
+ cfp.readfp(fd, filename)
-numeric_options = ('port',)
-boolean_options = ('ssl',)
-list_options = ('channels',)
-time_options = ('timeout',)
+ config = dict(cfp.items("general"))
+ config['servers'] = {}
-def boolywood(value):
- if value.lower() in ('0', 'false', 'no', 'nein', 'off'):
- return False
- return True
+ for opt in ('nicknames',):
+ if opt in config:
+ config[opt] = r_comma.split(config[opt])
-def parse_timeout(text):
- # 1h2m30s 1m30s 40s 2m
- match = re.match(r'(?:(\d*)h)?(?:(\d*)m)?(?:(\d*)s)?', text, re.I)
- if match:
- hours = int(match.group(1) or 0)
- minutes = int(match.group(2) or 0)
- seconds = int(match.group(3) or 0)
- return (hours, minutes, seconds)
- raise RuntimeError("Invalid time format")
-
-def fix_config(global_config):
- for name, config in global_config.iteritems():
- for option in numeric_options:
- if option in config:
- config[option] = int(config[option])
- for option in boolean_options:
- if option in config:
- config[option] = boolywood(config[option])
- for option in list_options:
- if option in config:
- config[option] = [x.strip() for x in config[option].split(',')]
- for option in time_options:
- if option in config:
- config[option] = parse_timeout(config[option])
-
-
-class NewConfig(object): pass
-
-class GeneralConfig(NewConfig):
- def __init__(self, nickname, ident, realname, datadir, googleapi=None):
- self.nickname = unicode_or_None(nickname)
- self.ident = unicode_or_None(ident)
- self.realname = unicode_or_None(realname)
- self.datadir = datadir
- self.googleapi = unicode_or_None(googleapi)
-
- self.servers = {}
-
-class ServerConfig(NewConfig):
- def __init__(self, address, port, ssl=False, channels=None,
- nickserv=None, password=None, nickname=None):
- self.address = address
- self.port = int(port)
- self.ssl = ssl
- if isinstance(channels, list):
- self.channels = channels[:]
- elif channels:
- self.channels = [channels]
- else:
- self.channels = []
- self.channels = [unicode(c, 'utf-8', 'replace') for c in self.channels]
+ for section in cfp.sections():
+ if not section.startswith("server:"):
+ continue
- self.nickserv = unicode_or_None(nickserv)
- self.password = unicode_or_None(password)
- self.nickname = unicode_or_None(nickname)
+ server_name = section.split(':')[1]
+ server_config = dict(cfp.items(section))
+ for opt in ('port',):
+ server_config[opt] = int(server_config[opt])
+ for opt in ('channels',):
+ server_config[opt] = r_comma.split(server_config[opt])
+
+ config['servers'][server_name] = server_config
-def database_filename(datadir):
- return "sqlite:///" + os.path.join(datadir, DEFAULT_DATABASE_FILENAME)
+ return config
diff --git a/pinolo/irc.py b/pinolo/irc.py
index 41ba590..332e160 100644
--- a/pinolo/irc.py
+++ b/pinolo/irc.py
@@ -1,32 +1,26 @@
# -*- encoding: utf-8 -*-
"""
-IRC connections handling with gevent.
+ pinolo.irc
+ ~~~~~~~~~~
-Heavily inspired by hmbot and the following gist by gihub:maxcountryman:
-https://gist.github.com/676306
+ IRC related functions and classes. It's basically a callback system, with
+ events named by the IRC command (e.g. on_PRIVMSG, on_NOTICE, etc).
+
+ :copyright: (c) 2013 Daniel Kertesz
+ :license: BSD, see LICENSE for more details.
"""
-import os
-import re
-import time
import logging
+import re
+import socket
import errno
-import gevent
-from gevent.core import timer
-from gevent import socket, ssl
-from gevent.queue import Queue
-import pinolo.plugins
-from pinolo.database import init_db
-from pinolo.prcd import moccolo_random, prcd_categories
-from pinolo.cowsay import cowsay
-from pinolo.utils.text import decode_text
-from pinolo.config import database_filename
-from pinolo.casuale import get_random_quit
-from pinolo import (FULL_VERSION, EOF_RECONNECT_TIME,
- FAILED_CONNECTION_RECONNECT_TIME,
- CONNECTION_TIMEOUT, PING_DELAY, THROTTLE_TIME,
- THROTTLE_INCREASE)
+import threading
+import Queue
+import urllib2
+from pinolo.tasks import TestTask
+log = logging.getLogger(__name__)
+
# re for usermask parsing
# usermask_re = re.compile(r'(?:([^!]+)!)?(?:([^@]+)@)?(\S+)')
# Note: some IRC events could not have a normal usermask, for example:
@@ -52,14 +46,7 @@ NEWLINE = '\r\n'
# IRC CTCP 'special' character
CTCPCHR = u'\x01'
-# Standard command aliases
-COMMAND_ALIASES = {
- 's': 'search',
-}
-
-
-class LastEvent(Exception):
- pass
+re_comma = re.compile(r'\s*,\s+')
def parse_usermask(usermask):
@@ -76,54 +63,16 @@ def parse_usermask(usermask):
class IRCUser(object):
- """This class represent common IRC informations about a user.
-
- Attributes:
-
- self.ident
- Ident string for that user.
-
- self.hostname
- Hostname for that user.
-
- self.nickname
- Nickname for that user.
- """
- def __init__(self, ident, hostname, nickname):
- self.ident, self.hostname, self.nickname = ident, hostname, nickname
+ def __init__(self, nickname, ident, hostname):
+ self.nickname = nickname
+ self.ident = ident
+ self.hostname = hostname
def __repr__(self):
- return u"<IRCUser(nickname:%s, %s@%s)>" % (
- self.nickname, self.ident, self.hostname)
+ return u"<IRCUser(%s!%s@%s)>" % (self.nickname, self.ident, self.hostname)
class IRCEvent(object):
- """Common IRC event class.
-
- Attributes:
-
- self.client
- The client instance that received the event.
-
- self.user
- The IRC user who triggered the event.
-
- self.command
- The command from the event; this can be an IRC event (e.g: PRIVMSG) or
- an internal command (e.g: !quote).
-
- self.argstr
- Arguments of `command` as a string; this can be for example the target
- of a PRVIMSG.
-
- self.args
- A list containing the splitted (by whitespace) words from argstr.
-
- self.text
- Everything after the first ':' in an IRC line; this can be, for
- example, the content of a PRVIMSG.
- """
-
def __init__(self, client, user, command, argstr, args=None, text=None):
self.client = client
self.user = user
@@ -139,537 +88,257 @@ class IRCEvent(object):
def nickname(self):
return self.user.nickname
- def reply(self, message, prefix=True):
- """Send a PRIVMSG to a user or a channel as a reply to some query.
-
- message
- The text message that will be sent; it must be an unicode string
- object!
+ @property
+ def name(self):
+ # XXX occhio
+ return "on_" + self.command.encode('utf-8')
- prefix
- If `True` `message` will be prefixed with the nickname of the user
- who triggered the event.
- """
- assert type(message) is unicode
- assert type(self.user.nickname is unicode)
+ def reply(self, message, prefix=True):
+ assert isinstance(message, unicode) is True
+ assert isinstance(self.nickname, unicode) is True
recipient = self.args[0]
- if recipient.startswith(u'#'):
+ if recipient.startswith(u"#"):
if prefix:
- message = u"%s: %s" % (self.user.nickname, message)
+ message = u"{0}: {1}".format(self.nickname, message)
self.client.msg(recipient, message)
else:
- self.client.msg(self.user.nickname, message)
+ self.client.msg(self.nickname, message)
def __repr__(self):
- return u"<IRCEvent(%r, command: %s, argstr: %s, " \
- "args: %r, text: %r)>" % (self.user, self.command, self.argstr,
- self.args, self.text)
-
-
-class IRCClient(object):
- """An IRC client with gevent.
-
- Attributes:
-
- self.name
- Name identifying this connection, usually the IRC network name.
+ return u"<IRCEvent(client=%r, user=%r, command=%r, args=%r)>" % (
+ self.client, self.user, self.command, self.args)
- self.config
- Pointer to this client configuration dict.
- self.general_config
- Pointer to the global bot configuration.
-
- self.head
- Pointer to the "head" object (the connection manager).
- """
-
- def __init__(self, name, config, general_config, head):
+class IRCConnection(object):
+ def __init__(self, name, config, bot):
+ # the connection name
self.name = name
+ # connection configuration
self.config = config
- self.general_config = general_config
- self.head = head
-
- self.nickname = self.config.nickname
- self.current_nickname = self.nickname
-
+ # pointer to manager object
+ self.bot = bot
+ # network stuff
self.socket = None
- self.stream = None
- self.throttle_out = THROTTLE_TIME
- self._last_write_time = 0
- self.logger = logging.getLogger("%s.%s" % (__name__, self.name))
- self._running = False
- self._connected = False
-
- self.ping_timer = None
- self.greenlet = None
-
- def __repr__(self):
- return "%s(name: %r)" % (
- self.__class__.__name__, self.name
- )
+ self.out_buffer = ""
+ self.in_buffer = ""
+ # are we connected, active?
+ self.connected = False
+ self.active = True
+ # nickname handling
+ self.nicknames = self.bot.config['nicknames']
+ self.current_nickname = 0
+ # la queue per i thread
+ self.coda = self.bot.coda
def connect(self):
- """Connect to the configured IRC server.
-
- In case of a connection error gevent.sleep() will be called to pause
- the client before attempting a new connection.
- """
- while True:
- try:
- self._connect()
- self._connected = True
- except socket.error, e:
- # Ad esempio:
- # e.errno == errno.ECONNREFUSED
- error_name = errno.errorcode[e.errno]
- error_desc = os.strerror(e.errno)
-
- self.logger.error(u"Failed connection to: %s:%d (%s %s)" % (
- self.config.address, self.config.port, error_name,
- error_desc))
- self.logger.warning(u"I'll be quiet for %d seconds before "
- "trying to connect again" %
- FAILED_CONNECTION_RECONNECT_TIME)
-
- self._connected = False
- gevent.sleep(FAILED_CONNECTION_RECONNECT_TIME)
- else:
- break
-
- gevent.sleep(1)
- self._running = True
- self.login_to_server()
- self.ciclo_pingo()
- self.event_loop()
-
- def _connect(self):
- """This is the real method for connecting to a IRC server.
- """
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- if self.config.ssl:
- self.socket = ssl.wrap_socket(self.socket)
- self.stream = self.socket.makefile()
- self.socket.connect((self.config.address, self.config.port))
- self.logger.info(u"Connected to: %s:%d (%s)" % (
- self.config.address, self.config.port, self.name)
- )
-
- def login_to_server(self):
- """Login to the IRC server and optionally sends the server password.
- """
- if self.config.password:
- self.send_cmd(u"PASS %s" % self.config.password)
- self.set_nickname(self.current_nickname)
- self.send_cmd(u"USER %s 8 * :%s\n" % (self.general_config.ident,
- self.general_config.realname))
-
- def set_nickname(self, nickname):
- self.current_nickname = nickname
- self.send_cmd(u"NICK %s" % nickname)
-
- def event_loop(self):
- """IRC event handling.
-
- Every line read from a IRC server will be a new event, so a Event
- object will be created with all the details of the event.
- """
- while True:
- line = None
- with gevent.Timeout(CONNECTION_TIMEOUT, False):
- line = self.stream.readline()
- if line is None:
- self.logger.warning(u"Connection timeout: "
- "%d elapsed" % CONNECTION_TIMEOUT)
- break
- # continue
-
- if line == '':
- break # EOF
- line = decode_text(line.strip())
- self.logger.debug(u"IN: %s" % line)
-
- if line.startswith(u':'):
- source, line = line[1:].split(u' ', 1)
- nickname, ident, hostname = parse_usermask(source)
- else:
- # PING :server.irc.net
- nickname, ident, hostname = (None, None, None)
-
- # Parsa il `command` e i suoi `argstr`; in caso di CTCP o !comando
- # cambia `command` adeguatamente.
- command, line = line.split(u' ', 1)
- command = command.encode('utf-8', 'replace')
- if u' :' in line:
- argstr, text = line.split(u' :', 1)
-
- # CTCP
- if (text.startswith(CTCPCHR) and text.endswith(CTCPCHR)):
- text = text[1:-1]
- old_command = command
-
- try:
- command, argstr = text.split(u' ', 1)
- except ValueError:
- command, argstr = text, u''
- text = u''
-
- if old_command == "PRIVMSG":
- command = "CTCP_" + command
- else:
- command = "CTCP_REPLY_" + command
-
- # E' un "comando" del Bot
- elif text.startswith(u'!'):
- try:
- command, text = text[1:].split(u' ', 1)
- except ValueError:
- command, text = text[1:], u''
- finally:
- command = command.encode('utf-8', 'replace')
-
- # Espande il comando con gli alias
- command = "cmd_" + COMMAND_ALIASES.get(command, command)
+ self.socket.setblocking(0)
+ try:
+ self.socket.connect((self.config['hostname'], self.config['port']))
+ except socket.error, e:
+ if isinstance(e, str):
+ raise
+ elif e[0] == errno.EINPROGRESS:
+ pass
else:
- argstr, text = line, u''
-
- args = argstr.split()
- user = IRCUser(ident, hostname, nickname)
- event = IRCEvent(self, user, command, argstr, args, text)
-
- event_name = 'on_%s' % command
- self.logger.debug(u"looking for event %s" % (event_name,))
- self.dispatch_event(event_name, event)
-
- # qui siamo a EOF! ######################
- self._connected = False
- if self._running:
- self._running = False
- self.logger.warning(u"EOF from server? Sleeping %i seconds before "
- "reconnecting" % EOF_RECONNECT_TIME)
- gevent.sleep(EOF_RECONNECT_TIME)
- self.logger.info(u"Reconnecting to %s:%d (%s)" % (
- self.config.address, self.config.port, self.name))
- self.connect()
-
- def dispatch_event(self, event_name, event):
- """Dispatch an Event object to the related methods.
-
- Method lookup will search this class and all plugin classes for a
- method named as in `event_name`.
-
- event_name
- The event name in the form "on_<event name>", for example:
- on_PRIVMSG.
-
- event
- The Event object.
- """
- for inst in [self] + self.head.plugins:
- if hasattr(inst, event_name):
- f = getattr(inst, event_name)
- try:
- f(event)
- except LastEvent:
- self.logger.debug(u"LastEvent for %s from %r" % (
- event_name, f))
- break
-
- def send_cmd(self, cmd):
- """Queue a IRC command to send to the server.
+ raise
+ self.connected = True
- cmd
- A formatted IRC command string.
+ self.nick()
+ self.ident()
- NOTA: Adesso scrive direttamente sul socket.
- """
- if not self._connected:
- self.logger.error(u"Discarding output (we aren't connected): %r" %
- (cmd,))
- return
- if isinstance(cmd, unicode):
- cmd = cmd.encode('utf-8', 'replace')
- self.stream.write(cmd + NEWLINE)
- self.stream.flush()
+ def send(self, line):
+ """Send a command to the IRC server.
- def msg(self, target, message):
- """
- Our `PRIVMSG` with flood protection.
- """
- now = time.time()
- elapsed = now - self._last_write_time
- if elapsed < self.throttle_out:
- gevent.sleep(0.5 - elapsed)
-
- self.logger.debug(u"PRVIMSG %s :%s" % (target, message))
- self.send_cmd(u"PRIVMSG %s :%s" % (target, message))
- self._last_write_time = now
-
- def msg_channels(self, msg, channels=None):
- """Send a PRIVMSG to a list of channels."""
- if channels is None:
- channels = self.config.channels[:]
- for channel in channels:
- self.msg(channel, msg)
-
- def join(self, channel):
- """Join a channel."""
- self.logger.info(u"Joining %s" % channel)
- self.send_cmd(u"JOIN %s" % channel)
- self.me(channel, u"saluta tutti")
-
- def quit(self, message="Bye"):
- """Quit this connection and kills the greenlet."""
- self.logger.info(u"QUIT requested")
- if self._running:
- self.send_cmd(u"QUIT :%s" % message)
- self._running = False # XXX
- self.stop_ciclo_pingo()
- # self.stream.close() # XXX
- self.socket.close()
- self.greenlet.kill()
-
- def notice(self, target, message):
+ NOTE: outgoing data must not be unicode!
"""
- NOTICE
+ buf = "{0}{1}".format(line, NEWLINE)
+ if isinstance(line, unicode):
+ buf = buf.encode('utf-8')
+ self.out_buffer += buf
+
+ def parse_line(self, line):
+ """Handle IRC lines
+
+ Each IRC message may consist of up to three main parts: the prefix
+ (optional), the command, and the command parameters (of which there
+ may be up to 15).
+
+ If the prefix is missing from the message, it is assumed to
+ have originated from the connection from which it was received.
+
+ :localhost NOTICE AUTH :TclIRCD-0.1a initialized, welcome.
+ NICK foobar
+ USER sand 8 * :peto peto peto
+ :localhost 001 foobar :Welcome to this IRC server foobar
+ :localhost 002 foobar :Your host is localhost, running version TclIRCD-0.1a
+ :localhost 003 foobar :This server was created ... I don't know
+ :localhost 004 foobar localhost TclIRCD-0.1a aAbBcCdDeEfFGhHiIjkKlLmMnNopPQrRsStUvVwWxXyYzZ0123459*@ bcdefFhiIklmnoPqstv
+ JOIN #test
+ :foobar!~sand@localhost JOIN :#test
+ :localhost 331 foobar #test :There isn't a topic.
+ :localhost 353 foobar = #test :@foobar
+ :localhost 366 foobar #test :End of /NAMES list.
+
+ PRIVMSG:
+ :petello!~petone@localhost PRIVMSG petone :ciao a te
+
+ PING:
+ :sand!~sand@localhost PRIVMSG petone :PING 1368113613 585318
"""
- self.logger.debug(u"NOTICE %s :%s" % (target, message))
- self.send_cmd(u"NOTICE %s :%s" % (target, message))
-
- def me(self, target, message):
- self.msg(target, u"%sACTION %s%s" % (CTCPCHR, message, CTCPCHR))
-
- def ctcp(self, target, message):
- """Generic CTCP send method."""
- self.logger.debug(u"SENT CTCP TO %s :%s" % (target, message))
- self.msg(target, u"%s%s%s" % (CTCPCHR, message, CTCPCHR))
-
- def ctcp_reply(self, target, message):
- """Reply to a CTCP."""
- self.logger.debug(u"CTCP REPLY TO %s: %s" % (target, message))
- self.notice(target, u"%s%s%s" % (CTCPCHR, message, CTCPCHR))
-
- def ctcp_ping(self, target):
- """Send a CTCP PING to a target IRC user."""
- tempo = int(time.time())
- self.ctcp(target, u"PING %d" % (tempo,))
-
- def ctcp_ping_reply(self, target, message):
- """Reply to a CTCP PING."""
- self.ctcp_reply(target, u"PING %s" % message)
-
- def nickserv_login(self):
- """Handle authentication with NickServ service."""
- self.logger.info(u"Authenticating with NickServ")
- self.msg(u'NickServ', u"IDENTIFY %s" % self.config.nickserv)
- gevent.sleep(1)
-
- def ciclo_pingo(self):
- """Set a gevent.timer that will ping ourself from time to time."""
- self.ping_timer = timer(PING_DELAY, self.pingati)
-
- def stop_ciclo_pingo(self):
- """Stop the gevent.timer created by `ciclo_pingo`."""
- if self.ping_timer is not None:
- self.ping_timer.cancel()
-
- def pingati(self):
- """Ping myself and set a new self-ping timer."""
- # verifico che siamo connessi; non e' troppo affidabile...
- if self._running:
- self.logger.debug(u"PING to myself")
- self.ctcp_ping(self.current_nickname)
- self.ciclo_pingo()
-
- def increase_throttle(self):
- """Increase the throttle of PRIVMSG sent to the server."""
- old_value = self.throttle_out
- self.throttle_out += THROTTLE_INCREASE
- self.logger.warning(u"Increasing throttle: %f -> %f" % (
- old_value, self.throttle_out))
-
- # EVENTS ##################################################################
+ print "<<< %s" % (line,)
- def on_001(self, event):
- """
- L'evento "welcome" del server IRC.
- NOTA: Non e' un `welcome` ufficiale, ma funziona.
- """
- if self.config.nickserv:
- self.nickserv_login()
+ if line.startswith(u":"):
+ # IRC message with prefix
+ source, line = line[1:].split(u" ", 1)
+ nickname, ident, hostname = parse_usermask(source)
+ else:
+ # IRC message without prefix (e.g. server's PING)
+ nickname, ident, hostname = (None, None, None)
+
+ # IRC command (e.g. PRIVMSG)
+ # command = PRIVMSG
+ # line = petone :ciao a te
+ # where "petone" is the target (so it's an argstr) and ":ciao a te"
+ # is the command text
+ command, line = line.split(u" ", 1)
+ # convert command to a string object since it will be used to lookup
+ # the handler function (and thus can't be a unicode string)
+ command = command.encode('utf-8', 'replace')
+
+ # IRC command arguments and text
+ # args will be a list of command arguments and text will be the command
+ # text, if any.
+ if u" :" in line:
+ argstr, text = line.split(u" :", 1)
+
+ # CTCP
+ if (text.startswith(CTCPCHR) and text.endswith(CTCPCHR)):
+ text = text[1:-1]
+ old_command = command
+
+ if u" " in text:
+ command, argstr = text.split(u" ", 1)
+ else:
+ command, argstr = text, u""
+ text = u""
+
+ if old_command == "PRIVMSG":
+ command = "CTCP_" + command
+ else:
+ command = "CTCP_REPLY_" + command
+
+ # E' un "comando" del Bot
+ elif text.startswith(u'!'):
+ try:
+ command, text = text[1:].split(u' ', 1)
+ except ValueError:
+ command, text = text[1:], u''
+ finally:
+ command = command.encode('utf-8', 'replace')
- for channel in self.config.channels:
- self.join(channel)
+ # Espande il comando con gli alias
+ # command = "cmd_" + COMMAND_ALIASES.get(command, command)
+ command = "cmd_{0}".format(command)
- def on_433(self, event):
- """
- Nickname is already in use.
- """
- new_nick = self.current_nickname + '_'
- self.set_nickname(new_nick)
+ else:
+ argstr, text = line, u""
+ args = argstr.split()
- def on_PING(self, event):
- """
- Server PING.
- """
- self.logger.debug(u"PING from server")
- self.send_cmd(u"PONG %s" % event.argstr)
+ user = IRCUser(nickname, ident, hostname)
+ event = IRCEvent(self, user, command, argstr, args, text)
+ self.dispatch_event(event)
- def on_PRIVMSG(self, event):
- target = event.args[0]
- private = target == self.current_nickname
+ def dispatch_event(self, event):
+ logging.debug("Dispatching event: %r" % event)
- # if event.text.startswith(self.current_nickname) or private:
- # event.reply(get_random_reply())
+ for handler in [self] + self.bot.plugins:
+ if hasattr(handler, event.name):
+ fn = getattr(handler, event.name)
+ try:
+ fn(event)
+ except Exception, e:
+ print "Exception in IRC callback {0}: {1}".format(
+ event.name, str(e))
+
+ def check_in_buffer(self):
+ """Check for complete lines in the input buffer, encode them in UTF-8
+ and pass them to the parser."""
+ lines = []
+ while "\n" in self.in_buffer:
+ nl = self.in_buffer.find("\n")
+ if nl == -1:
+ break
+ line = self.in_buffer[:nl]
+ lines.append(line)
+ self.in_buffer = self.in_buffer[nl + 1:]
- def on_CTCP_PING(self, event):
- if event.user.nickname != self.current_nickname:
- self.logger.info(u"CTCP PING from %s: %s" % (event.user.nickname,
- event.argstr))
- self.ctcp_ping_reply(event.user.nickname, event.argstr)
+ for line in lines:
+ line = line.replace("\r", "")
+
+ try:
+ line = line.decode('utf-8', 'replace')
+ except UnicodeDecodeError, e:
+ print "Invalid encoding for irc line: %s" % (line,)
+ else:
+ self.parse_line(line)
- def on_CTCP_VERSION(self, event):
- self.logger.info(u"CTCP VERSION from %s" % event.user.nickname)
- self.ctcp_reply(event.user.nickname, u"VERSION %s" % FULL_VERSION)
+ # IRC COMMANDS
- def on_NOTICE(self, event):
- pass
+ def nick(self, nickname=None):
+ if nickname is None:
+ nickname = self.nicknames[self.current_nickname]
+ self.send(u"NICK {0}".format(nickname))
- def on_KICK(self, event):
- channel = event.args[0]
- self.logger.info(u"KICKed from %s by %s (%s)" % (
- channel, event.nickname, event.text))
- gevent.sleep(1)
- self.join(channel)
+ def ident(self):
+ """Send an USER command.
- def on_ERROR(self, event):
- """
- 2011-09-05 10:51:14,156 pinolo.irc.azzurra WARNING ERROR from server: :Closing Link: my.hostname.net (Excess Flood)
- """
- # skip if it's our /quit command
- if '(Quit:' in event.argstr:
- return
- match = re.search(r"\(([^)]+)\)", event.argstr)
- if match:
- reason = match.group(1)
- reason = reason.lower()
- else:
- reason = u""
-
- if reason == u"excess flood":
- self.logger.warning(u"ERROR: Excess Flood from server!")
- self.increase_throttle()
- self.logger.warning(u"ERROR from server: %s" % event.argstr)
-
- def on_cmd_quit(self, event):
- if event.user.nickname == u'sand':
- reason = get_random_quit() if event.text == '' else event.text
- self.logger.warning(u"Global quit from %s (%s)" % (
- event.user.nickname, reason))
- self.head.shutdown(reason)
-
- def on_cmd_prcd(self, event):
- cat, moccolo = moccolo_random(event.text or None)
- if not moccolo:
- event.reply(u"La categoria non esiste!")
- else:
- event.reply(u"(%s) %s" % (cat, moccolo))
+ Parameters: <user> <mode> <unused> <realname>
+ For example:
+ USER guest 8 * :My real name
- def on_cmd_prcd_list(self, event):
- event.reply(u', '.join(prcd_categories))
+ Note: user mode '8' means "invisible"
+ """
+ self.send(u"USER {0} 8 * :{1}".format(
+ self.bot.config['ident'],
+ self.bot.config['realname']))
+
+ def join(self, channel, key=None):
+ """Join a single channel"""
+ self.send(u"JOIN {0}{1}".format(
+ channel, " " + key if key else ""))
+
+ def join_many(self, channels, keys):
+ """Join many channels"""
+ self.send(u"JOIN {0} {1}".format(
+ ",".join(channels), ",".join(keys)))
+
+ def join_all(self):
+ """Join all the configured channels"""
+ for channel in self.config['channels']:
+ self.join(channel)
- def on_cmd_PRCD(self, event):
- cat, moccolo = moccolo_random(event.text or None)
- if not moccolo:
- event.reply(u"La categoria non esiste!")
- else:
- output = cowsay(moccolo)
- for line in output:
- if line:
- event.reply(line, prefix=False)
+ def quit(self, message=None):
+ """Quit from the server"""
+ if message is None:
+ message = "Il cesso gallico"
+ self.send(u"QUIT :{0}".format(message))
- def on_cmd_pingami(self, event):
- self.ctcp_ping(event.user.nickname)
+ def msg(self, target, message):
+ """PRIVMSG"""
+ self.send(u"PRIVMSG {0} :{1}".format(target, message))
+ # IRC EVENTS
+ def on_001(self, event):
+ self.join_all()
-class BigHead(object):
- """
- Questo oggetto e' il cervellone che gestisce i plugin e tutte le
- connessioni.
- """
+ def on_cmd_saluta(self, event):
+ event.reply(u"ciao")
- def __init__(self, config):
- self.config = config # la config globale
- self.connections = {}
- self.plugins = []
- self.logger = logging.getLogger('pinolo.head')
- self.plugins_dir = os.path.join(
- os.path.abspath(os.path.dirname(__file__)), 'plugins')
- self.db_uri = database_filename(self.config.datadir)
-
- self.load_plugins()
- self.start_plugins()
- # init_db() va DOPO start_plugins() per creare eventuali tabelle del DB
- # in realta' va DOPO l'import. XXX
- init_db(self.db_uri)
- self.activate_plugins()
-
- def load_plugins(self):
- def my_import(name):
- """
- http://effbot.org/zone/import-string.htm#importing-by-filename
- """
- m = __import__(name)
- for n in name.split(".")[1:]:
- m = getattr(m, n)
- return m
-
- # NOTA: SKIPPARE I .pyc!!!
- for root, dirs, files in os.walk(self.plugins_dir):
- files = [os.path.splitext(x)[0] for x in files
- if (not x.startswith('_') and x.endswith('.py'))]
- for libname in set(files): # uniqify
- libname = "pinolo.plugins." + libname
- self.logger.info(u"Importing plugin: %s" % (libname,))
- p = my_import(libname)
-
- def start_plugins(self):
- for plugin_name, PluginClass in pinolo.plugins.registry:
- # init and append to internal list
- self.plugins.append(PluginClass(self))
- # and update aliases
- COMMAND_ALIASES.update(PluginClass.COMMAND_ALIASES.items())
-
- def activate_plugins(self):
- self.logger.debug(u"Activating plugins")
- for plugin in self.plugins:
- plugin.activate()
-
- def deactivate_plugins(self):
- self.logger.debug(u"Deactivating plugins")
- for plugin in self.plugins:
- plugin.deactivate()
-
- def shutdown(self, reason=u"quit"):
- self.logger.warning(u"Global shutdown")
- self.deactivate_plugins()
- for client in self.connections.values():
- client.quit(reason)
-
- def run(self):
- print "[*] Starting %s" % FULL_VERSION
- jobs = []
-
- for name, server in self.config.servers.iteritems():
- irc = IRCClient(name, server, self.config, self)
- self.connections[name] = irc
- job = gevent.spawn(irc.connect)
- irc.greenlet = job
- jobs.append(job)
+ def on_cmd_getta(self, event):
+ task = TestTask(self.name, self.coda)
+ task.start()
- try:
- gevent.joinall(jobs)
- except KeyboardInterrupt:
- self.shutdown(u"keyboard-interrupt")
- gevent.joinall(jobs)
+ def on_cmd_quitta(self, event):
+ self.quit()
diff --git a/pinolo/main.py b/pinolo/main.py
index 0f0b228..27d194b 100644
--- a/pinolo/main.py
+++ b/pinolo/main.py
@@ -1,48 +1,86 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
+# -*- encoding: utf-8 -*-
+import logging
+import getopt
+import sys
+from pinolo.bot import Bot
+from pinolo.config import read_config_file
-import warnings
-warnings.simplefilter('default')
-import sys
-import logging
-logging.basicConfig(level=logging.INFO,
- format="%(asctime)s %(name)s %(levelname)s %(message)s",
- datefmt="%H:%M:%S %d/%m/%y")
-logger = logging.getLogger('pinolo')
-
-from pinolo.options import Options
-from pinolo.config import read_config_files
-from pinolo.irc import BigHead
-from pinolo import FULL_VERSION
-
-optspec = """
-pinolo [options]
---
-c,config= Read configuration options from file.
-d,debug Enable debugging messages.
-unaz Load 'unicode-nazi' library to debug unicode errors.
+usage = \
"""
-header = "%s, the naughty chat bot." % FULL_VERSION
+Usage: {0} [-v] [-d] [-h] [-V] -c filename
+ {0} [--verbose] [--debug] [--help] [--version] --config filename
+"""
+
+version = "pinolo x.y"
+
+
+def fatal(msg, code=1):
+ """Print an error message and exit the program"""
+ sys.stderr.write("ERROR: %s\n" % msg)
+ sys.exit(code)
+
def main():
- print header
- o = Options(optspec)
- (options, flags, extra) = o.parse(sys.argv[1:])
-
- if not options.config:
- o.fatal("You must specify a configuration file!")
- if options.debug:
- logger.setLevel(logging.DEBUG)
- if options.unaz:
- try:
- import unicodenazi
- except ImportError:
- logger.warning("Cannot find unicode-nazi package!")
-
- config = read_config_files(options.config)
- head = BigHead(config)
- head.run()
+ """Command line entry point"""
+ opt_short = 'c:dvVh'
+ opt_long = ['config=', 'debug' , 'verbose', 'version', 'help']
+
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], opt_short, opt_long)
+ except getopt.GetoptError, e:
+ fatal(e)
+
+ options = {
+ 'config_file': None,
+ 'verbose': False,
+ 'debug': False,
+ }
+
+ for name, value in opts:
+ if name in ('-h', '--help'):
+ print usage.format(sys.argv[0])
+ sys.exit(0)
+ elif name in ('-c', '--config'):
+ options['config_file'] = value
+ elif name in ('-d', '--debug'):
+ options['debug'] = True
+ elif name in ('-v', '--verbose'):
+ options['verbose'] = True
+ elif name in ('-V', '--version'):
+ print version
+ sys.exit(0)
+
+ # Check for mandatory options
+ if not options['config_file']:
+ fatal("You must specify a configuration file")
+
+ # Set the global logging level
+ root_log = logging.getLogger()
+ if options['debug']:
+ root_log.setLevel(logging.DEBUG)
+ elif options['verbose']:
+ root_log.setLevel(logging.INFO)
+ else:
+ root_log.setLevel(logging.WARNING)
+
+ start_bot(options)
+
+
+def start_bot(options):
+ """Launch the irc bot instance"""
+ config = read_config_file(options['config_file'])
+ bot = Bot(config)
+
+ # This try block is ugly but allow us to catch the interrupt signal
+ # and still do a clean exit.
+ try:
+ bot.start()
+ except KeyboardInterrupt:
+ print "\nInterrupt, exiting.\nPress CTRL-C again to force exit"
+ bot.quit()
+ bot.main_loop()
+
if __name__ == '__main__':
main()
diff --git a/pinolo/options.py b/pinolo/options.py
deleted file mode 100644
index 9bcead7..0000000
--- a/pinolo/options.py
+++ /dev/null
@@ -1,271 +0,0 @@
-# Copyright 2011 Avery Pennarun and options.py contributors.
-# All rights reserved.
-#
-# (This license applies to this file but not necessarily the other files in
-# this package.)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in
-# the documentation and/or other materials provided with the
-# distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY AVERY PENNARUN ``AS IS'' AND ANY
-# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
-# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
-# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
-# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-"""Command-line options parser.
-With the help of an options spec string, easily parse command-line options.
-
-An options spec is made up of two parts, separated by a line with two dashes.
-The first part is the synopsis of the command and the second one specifies
-options, one per line.
-
-Each non-empty line in the synopsis gives a set of options that can be used
-together.
-
-Option flags must be at the begining of the line and multiple flags are
-separated by commas. Usually, options have a short, one character flag, and a
-longer one, but the short one can be omitted.
-
-Long option flags are used as the option's key for the OptDict produced when
-parsing options.
-
-When the flag definition is ended with an equal sign, the option takes one
-string as an argument. Otherwise, the option does not take an argument and
-corresponds to a boolean flag that is true when the option is given on the
-command line.
-
-The option's description is found at the right of its flags definition, after
-one or more spaces. The description ends at the end of the line. If the
-description contains text enclosed in square brackets, the enclosed text will
-be used as the option's default value.
-
-Options can be put in different groups. Options in the same group must be on
-consecutive lines. Groups are formed by inserting a line that begins with a
-space. The text on that line will be output after an empty line.
-"""
-import sys, os, textwrap, getopt, re, struct
-
-class OptDict:
- """Dictionary that exposes keys as attributes.
-
- Keys can bet set or accessed with a "no-" or "no_" prefix to negate the
- value.
- """
- def __init__(self):
- self._opts = {}
-
- def __setitem__(self, k, v):
- if k.startswith('no-') or k.startswith('no_'):
- k = k[3:]
- v = not v
- self._opts[k] = v
-
- def __getitem__(self, k):
- if k.startswith('no-') or k.startswith('no_'):
- return not self._opts[k[3:]]
- return self._opts[k]
-
- def __getattr__(self, k):
- return self[k]
-
-
-def _default_onabort(msg):
- sys.exit(97)
-
-
-def _intify(v):
- try:
- vv = int(v or '')
- if str(vv) == v:
- return vv
- except ValueError:
- pass
- return v
-
-
-def _atoi(v):
- try:
- return int(v or 0)
- except ValueError:
- return 0
-
-
-def _remove_negative_kv(k, v):
- if k.startswith('no-') or k.startswith('no_'):
- return k[3:], not v
- return k,v
-
-def _remove_negative_k(k):
- return _remove_negative_kv(k, None)[0]
-
-
-def _tty_width():
- s = struct.pack("HHHH", 0, 0, 0, 0)
- try:
- import fcntl, termios
- s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
- except (IOError, ImportError):
- return _atoi(os.environ.get('WIDTH')) or 70
- (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
- return xsize or 70
-
-
-class Options:
- """Option parser.
- When constructed, a string called an option spec must be given. It
- specifies the synopsis and option flags and their description. For more
- information about option specs, see the docstring at the top of this file.
-
- Two optional arguments specify an alternative parsing function and an
- alternative behaviour on abort (after having output the usage string).
-
- By default, the parser function is getopt.gnu_getopt, and the abort
- behaviour is to exit the program.
- """
- def __init__(self, optspec, optfunc=getopt.gnu_getopt,
- onabort=_default_onabort):
- self.optspec = optspec
- self._onabort = onabort
- self.optfunc = optfunc
- self._aliases = {}
- self._shortopts = 'h?'
- self._longopts = ['help', 'usage']
- self._hasparms = {}
- self._defaults = {}
- self._usagestr = self._gen_usage()
-
- def _gen_usage(self):
- out = []
- lines = self.optspec.strip().split('\n')
- lines.reverse()
- first_syn = True
- while lines:
- l = lines.pop()
- if l == '--': break
- out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l))
- first_syn = False
- out.append('\n')
- last_was_option = False
- while lines:
- l = lines.pop()
- if l.startswith(' '):
- out.append('%s%s\n' % (last_was_option and '\n' or '',
- l.lstrip()))
- last_was_option = False
- elif l:
- (flags, extra) = l.split(' ', 1)
- extra = extra.strip()
- if flags.endswith('='):
- flags = flags[:-1]
- has_parm = 1
- else:
- has_parm = 0
- g = re.search(r'\[([^\]]*)\]$', extra)
- if g:
- defval = g.group(1)
- else:
- defval = None
- flagl = flags.split(',')
- flagl_nice = []
- for _f in flagl:
- f,dvi = _remove_negative_kv(_f, _intify(defval))
- self._aliases[f] = _remove_negative_k(flagl[0])
- self._hasparms[f] = has_parm
- self._defaults[f] = dvi
- if f == '#':
- self._shortopts += '0123456789'
- flagl_nice.append('-#')
- elif len(f) == 1:
- self._shortopts += f + (has_parm and ':' or '')
- flagl_nice.append('-' + f)
- else:
- f_nice = re.sub(r'\W', '_', f)
- self._aliases[f_nice] = _remove_negative_k(flagl[0])
- self._longopts.append(f + (has_parm and '=' or ''))
- self._longopts.append('no-' + f)
- flagl_nice.append('--' + _f)
- flags_nice = ', '.join(flagl_nice)
- if has_parm:
- flags_nice += ' ...'
- prefix = ' %-20s ' % flags_nice
- argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
- initial_indent=prefix,
- subsequent_indent=' '*28))
- out.append(argtext + '\n')
- last_was_option = True
- else:
- out.append('\n')
- last_was_option = False
- return ''.join(out).rstrip() + '\n'
-
- def usage(self, msg=""):
- """Print usage string to stderr and abort."""
- sys.stderr.write(self._usagestr)
- if msg:
- sys.stderr.write(msg)
- e = self._onabort and self._onabort(msg) or None
- if e:
- raise e
-
- def fatal(self, msg):
- """Print an error message to stderr and abort with usage string."""
- msg = '\nerror: %s\n' % msg
- return self.usage(msg)
-
- def parse(self, args):
- """Parse a list of arguments and return (options, flags, extra).
-
- In the returned tuple, "options" is an OptDict with known options,
- "flags" is a list of option flags that were used on the command-line,
- and "extra" is a list of positional arguments.
- """
- try:
- (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
- except getopt.GetoptError, e:
- self.fatal(e)
-
- opt = OptDict()
-
- for k,v in self._defaults.iteritems():
- k = self._aliases[k]
- opt[k] = v
-
- for (k,v) in flags:
- k = k.lstrip('-')
- if k in ('h', '?', 'help', 'usage'):
- self.usage()
- if k.startswith('no-'):
- k = self._aliases[k[3:]]
- v = 0
- elif (self._aliases.get('#') and
- k in ('0','1','2','3','4','5','6','7','8','9')):
- v = int(k) # guaranteed to be exactly one digit
- k = self._aliases['#']
- opt['#'] = v
- else:
- k = self._aliases[k]
- if not self._hasparms[k]:
- assert(v == '')
- v = (opt._opts.get(k) or 0) + 1
- else:
- v = _intify(v)
- opt[k] = v
- for (f1,f2) in self._aliases.iteritems():
- opt[f1] = opt._opts.get(f2)
- return (opt,flags,extra)
diff --git a/pinolo/plugins/__init__.py b/pinolo/plugins/__init__.py
index a59b90f..5badf97 100644
--- a/pinolo/plugins/__init__.py
+++ b/pinolo/plugins/__init__.py
@@ -1,18 +1,37 @@
-registry = []
+# -*- encoding: utf-8 -*-
+"""
+ pinolo.plugins
+ ~~~~~~~~~~~~~~
-class Plugin(object):
+ Plugin base class with a global register storing all the plugins
+ instances.
- COMMAND_ALIASES = {}
+ :copyright: (c) 2013 Daniel Kertesz
+ :license: BSD, see LICENSE for more details.
+"""
+# This list will store tuples containing the plugin name and the plugin
+# instance for each plugin loaded.
+registry = []
+
+class Plugin(object):
+ """Base class for plugins"""
+
class __metaclass__(type):
- def __init__(cls, name, bases, dict):
- type.__init__(cls, name, bases, dict)
- registry.append((name, cls))
+ def __init__(cls, name, bases, _dict):
+ global registry
+
+ type.__init__(cls, name, bases, _dict)
+ # We must not add this class to the plugin registry!
+ if name != "Plugin":
+ registry.append((name, cls))
- def __init__(self, head):
- self.head = head
+ def __init__(self, bot):
+ """Initialize the plugin instance with a pointer to the bot object"""
+ self.bot = bot
def activate(self):
+ """Activate the plugin"""
pass
def deactivate(self):
diff --git a/pinolo/signals.py b/pinolo/signals.py
new file mode 100644
index 0000000..aed8e73
--- /dev/null
+++ b/pinolo/signals.py
@@ -0,0 +1,62 @@
+# -*- encoding: utf-8 -*-
+# Heavily inspired by signals.py from ranger (file manager).
+import weakref
+
+
+class Signal(dict):
+ stopped = False
+
+ def __init__(self, **kwargs):
+ dict.__init__(self, kwargs)
+ self.__dict__ = self
+
+ def stop(self):
+ self.stopped = True
+
+
+class SignalHandler:
+ active = True
+
+ def __init__(self, name, function, priority):
+ self._priority = max(0, min(1, priority))
+ self._name = name
+ self._function = function
+
+
+class SignalDispatcher(object):
+ def __init__(self):
+ self._signals = dict()
+
+ def signal_bind(self, name, function, priority=0.5):
+ handlers = self._signals.setdefault(name, [])
+ handler = SignalHandler(name, function, priority)
+ handlers.append(handler)
+ return handler
+
+ def signal_unbind(self, signal_handler):
+ if signal_handler._name in self._signals:
+ signal_handler._function = None
+ self._signals[signal_handler._name].remove(signal_handler)
+
+ def signal_emit(self, name, **kw):
+ if not name in self._signals:
+ return True
+ handlers = self._signals[name]
+ if not handlers:
+ return True
+
+ signal = Signal(origin=self, name=name, **kw)
+
+ for handler in tuple(handlers):
+ if handler.active:
+ try:
+ fn = handler._function
+ fn(signal)
+ except Exception, e:
+ log.error("Signal error: %s" % str(e))
+ print "Error in signal: %s" % str(e)
+
+ if signal.stopped:
+ return False
+
+ return True
diff --git a/pinolo/tasks.py b/pinolo/tasks.py
new file mode 100644
index 0000000..2b0f08e
--- /dev/null
+++ b/pinolo/tasks.py
@@ -0,0 +1,47 @@
+# -*- encoding: utf-8 -*-
+"""
+ pinolo.tasks
+ ~~~~~~~~~~~~
+
+ A Task object is a `threading.Thread` instance that will be executed
+ without blocking the main thread.
+ This is useful to perform potentially blocking actions like fecthing
+ resources via HTTP.
+
+ :copyright: (c) 2013 Daniel Kertesz
+ :license: BSD, see LICENSE for more details.
+"""
+import threading
+import urllib2
+
+
+class Task(threading.Thread):
+ """A task is an execution unit that will be run in a separate thread
+ that should not block tha main thread (handling irc connections).
+ """
+ def __init__(self, connection_name, queue, *args, **kwargs):
+ self.connection_name = connection_name
+ self.queue = queue
+ super(Task, self).__init__(*args, **kwargs)
+
+ def run(self):
+ raise RuntimeError("Must be implemented!")
+
+ def put_results(self, data):
+ """Task output will be sent to the main thread via the configured
+ queue; data should be a string containing the full output, that will
+ later be splitted on newlines."""
+ unit = (self.connection_name, data)
+ self.queue.put(unit)
+
+
+class TestTask(Task):
+ def run(self):
+ """Main execution function. This must not be called directly! You
+ must call threading.Thread.start() method."""
+
+ url = "http://www.spatof.org/blog/robots.txt"
+ resp = urllib2.urlopen(url)
+ data = resp.read()
+
+ self.put_results(data)
diff --git a/pinolo/utils.py b/pinolo/utils.py
deleted file mode 100644
index e69de29..0000000
--- a/pinolo/utils.py
+++ /dev/null
diff --git a/pinolo/utils/network.py b/pinolo/utils/network.py
deleted file mode 100644
index a4049b2..0000000
--- a/pinolo/utils/network.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-gevent related functions.
-"""
-
-import httplib
-import urllib2
-import subprocess
-import errno
-import sys
-import os
-import fcntl
-
-# import gevent
-from gevent import socket
-
-from pinolo import USER_AGENT
-
-
-class gevent_HTTPConnection(httplib.HTTPConnection):
- """
- httplib.HTTPConnection compatibile con gevent.
- - da python 2.6
-
- Per evitare monkey.patch_all():
- http://groups.google.com/group/gevent/browse_thread/thread/c20181cb066ee97e?fwc=2&pli=1
- """
- def connect(self):
- self.sock = socket.create_connection((self.host, self.port),
- self.timeout)
- if self._tunnel_host:
- self._tunnel()
-
-class gevent_HTTPHandler(urllib2.HTTPHandler):
- """
- urllib2.HTTPHandler compatibile con gevent.
- """
- def http_open(self, request):
- return self.do_open(gevent_HTTPConnection, request)
-
-def gevent_url_open(url, headers=[], data=None):
- """
- Fa una richiesta HTTP GET o POST con eventuali `headers` aggiunti.
- E' compatibile con gevent.
- """
- request = urllib2.Request(url, data)
- request.add_header('User-Agent', USER_AGENT)
-
- for name, value in headers:
- request.add_header(name, value)
-
- opener = urllib2.build_opener(gevent_HTTPHandler)
- return opener.open(request)
-
-
-def popen_communicate(args, data=''):
- """
- Communicate with the process non-blockingly.
- # from gevent/examples
- """
- p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- fcntl.fcntl(p.stdin, fcntl.F_SETFL, os.O_NONBLOCK) # make the file nonblocking
- fcntl.fcntl(p.stdout, fcntl.F_SETFL, os.O_NONBLOCK) # make the file nonblocking
-
- bytes_total = len(data)
- bytes_written = 0
- while bytes_written < bytes_total:
- try:
- # p.stdin.write() doesn't return anything, so use os.write.
- bytes_written += os.write(p.stdin.fileno(), data[bytes_written:])
- except IOError, ex:
- if ex[0] != errno.EAGAIN:
- raise
- sys.exc_clear()
- socket.wait_write(p.stdin.fileno())
-
- p.stdin.close()
-
- chunks = []
-
- while True:
- try:
- chunk = p.stdout.read(4096)
- if not chunk:
- break
- chunks.append(chunk)
- except IOError, ex:
- if ex[0] != errno.EAGAIN:
- raise
- sys.exc_clear()
- socket.wait_read(p.stdout.fileno())
-
- p.stdout.close()
- return ''.join(chunks)
diff --git a/pinolo/utils/text.py b/pinolo/utils/text.py
index 44ac5f8..b31f530 100644
--- a/pinolo/utils/text.py
+++ b/pinolo/utils/text.py
@@ -36,6 +36,7 @@ def strip_html(text):
return text # leave as is
return re.sub("(?s)<[^>]*>|&#?\w+;", fixup, text)
+
def decode_text(text):
"""Decode a given text string to an Unicode string.
@@ -50,6 +51,7 @@ def decode_text(text):
# fallback
return text.decode('utf-8', 'replace')
+
def md5(text):
"""Calculate the MD5 hash for a given text, returning an hex string."""
return hashlib.md5(text).hexdigest()
diff --git a/setup.py b/setup.py
index b7b5dd4..b8de071 100644
--- a/setup.py
+++ b/setup.py
@@ -4,16 +4,14 @@ from setuptools import setup, find_packages
setup(
name="pinolo",
- version="0.9.2",
+ version="0.10.1",
description="Pinolo, the naughty chat bot",
author="sand",
author_email="daniel@spatof.org",
- url="http://code.dyne.org/?r=pinolo",
+ url="http://git.spatof.org/pinolo.git",
# xapian!
install_requires=[
- "gevent==0.13.8",
- "greenlet==0.4.0",
- "SQLAlchemy==0.7.8",
+ "SQLAlchemy==0.7.10",
"requests==0.14.0",
"beautifulsoup4==4.1.3"
],