Index: trunk/debs/ircecho/debian/ircecho.default |
— | — | @@ -0,0 +1,11 @@ |
| 2 | +# Defaults for ircecho initscript |
| 3 | +# sourced by /etc/init.d/ircecho |
| 4 | +# installed at /etc/default/ircecho by the maintainer scripts |
| 5 | + |
| 6 | +# |
| 7 | +# This is a POSIX shell fragment |
| 8 | +# |
| 9 | +INFILE="" |
| 10 | +NICK="" |
| 11 | +CHANS="" |
| 12 | +SERVER="" |
Index: trunk/debs/ircecho/debian/control |
— | — | @@ -0,0 +1,11 @@ |
| 2 | +Source: ircecho |
| 3 | +Section: net |
| 4 | +Priority: extra |
| 5 | +Maintainer: Ryan Lane <rlane@wikimedia.org> |
| 6 | +Build-Depends: debhelper (>= 4) |
| 7 | +Standards-Version: 1.0 |
| 8 | + |
| 9 | +Package: ircecho |
| 10 | +Depends: python-pyinotify |
| 11 | +Architecture: all |
| 12 | +Description: IRC bot for echoing from stdin to IRC. |
Index: trunk/debs/ircecho/debian/dirs |
— | — | @@ -0,0 +1,3 @@ |
| 2 | +usr/ircecho |
| 3 | +usr/ircecho/bin |
| 4 | +usr/ircecho/lib |
Index: trunk/debs/ircecho/debian/compat |
— | — | @@ -0,0 +1 @@ |
| 2 | +6 |
Index: trunk/debs/ircecho/debian/install |
— | — | @@ -0,0 +1,3 @@ |
| 2 | +ircecho /usr/ircecho/bin |
| 3 | +ircbot.py /usr/ircecho/lib |
| 4 | +irclib.py /usr/ircecho/lib |
Index: trunk/debs/ircecho/debian/changelog |
— | — | @@ -0,0 +1,11 @@ |
| 2 | +ircecho (1.0-1) lucid-wikimedia; urgency=low |
| 3 | + |
| 4 | + * Fix init script |
| 5 | + |
| 6 | + -- Ryan Lane <rlane@wikimedia.org> Mon, 4 Apr 2011 23:50:00 +0000 |
| 7 | + |
| 8 | +ircecho (1.0) lucid-wikimedia; urgency=low |
| 9 | + |
| 10 | + * Initial package |
| 11 | + |
| 12 | + -- Ryan Lane <rlane@wikimedia.org> Wed, 30 Mar 2011 21:35:28 +0000 |
Index: trunk/debs/ircecho/debian/copyright |
— | — | @@ -0,0 +1,23 @@ |
| 2 | +This package was debianized by Ryan Lane <rlane@wikimedia.org> on |
| 3 | +Fri, 1 Apr 2011 21:35:28 +0000. |
| 4 | + |
| 5 | +It was downloaded from http://svn.wikimedia.org/viewvc/mediawiki/trunk/tools/ircecho |
| 6 | + |
| 7 | +Upstream Author(s): |
| 8 | + |
| 9 | + Kate Turner |
| 10 | + |
| 11 | +Copyright: |
| 12 | + |
| 13 | + Copyright (C) 2011 Kate Turner |
| 14 | + |
| 15 | +License: |
| 16 | + |
| 17 | + Public Domain |
| 18 | + |
| 19 | +The Debian packaging is: |
| 20 | + |
| 21 | + Copyright (C) 2011 Ryan Lane <rlane@wikimedia.org> |
| 22 | + |
| 23 | +and is licensed under the GPL version 2, |
| 24 | +see `/usr/share/common-licenses/GPL-2'. |
Index: trunk/debs/ircecho/debian/rules |
— | — | @@ -0,0 +1,86 @@ |
| 2 | +#!/usr/bin/make -f |
| 3 | +# -*- makefile -*- |
| 4 | +# Sample debian/rules that uses debhelper. |
| 5 | +# This file was originally written by Joey Hess and Craig Small. |
| 6 | +# As a special exception, when this file is copied by dh-make into a |
| 7 | +# dh-make output file, you may use that output file without restriction. |
| 8 | +# This special exception was added by Craig Small in version 0.37 of dh-make. |
| 9 | + |
| 10 | +# Uncomment this to turn on verbose mode. |
| 11 | +export DH_VERBOSE=1 |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | + |
| 16 | + |
| 17 | +configure: configure-stamp |
| 18 | +configure-stamp: |
| 19 | + dh_testdir |
| 20 | + # Add here commands to configure the package. |
| 21 | + |
| 22 | + touch configure-stamp |
| 23 | + |
| 24 | + |
| 25 | +build: build-stamp |
| 26 | + |
| 27 | +build-stamp: configure-stamp |
| 28 | + dh_testdir |
| 29 | + |
| 30 | + # Add here commands to compile the package. |
| 31 | + touch $@ |
| 32 | + |
| 33 | +clean: |
| 34 | + dh_testdir |
| 35 | + dh_testroot |
| 36 | + rm -f build-stamp configure-stamp |
| 37 | + |
| 38 | + # Add here commands to clean up after the build process. |
| 39 | + rm -rf build |
| 40 | + |
| 41 | + dh_clean |
| 42 | + |
| 43 | +install: build |
| 44 | + dh_testdir |
| 45 | + dh_testroot |
| 46 | + #dh_prep |
| 47 | + dh_installdirs |
| 48 | + |
| 49 | +# Build architecture-independent files here. |
| 50 | +binary-indep: install |
| 51 | + dh_testdir |
| 52 | + dh_testroot |
| 53 | + dh_installinit -n |
| 54 | + |
| 55 | +# Build architecture-dependent files here. |
| 56 | +binary-arch: install |
| 57 | + dh_testdir |
| 58 | + dh_testroot |
| 59 | +# dh_installchangelogs |
| 60 | +# dh_installdocs |
| 61 | +# dh_installexamples |
| 62 | + dh_install |
| 63 | +# dh_installmenu |
| 64 | +# dh_installdebconf |
| 65 | +# dh_installlogrotate |
| 66 | +# dh_installemacsen |
| 67 | +# dh_installpam |
| 68 | +# dh_installmime |
| 69 | +# dh_python |
| 70 | +# dh_installinit |
| 71 | +# dh_installcron |
| 72 | +# dh_installinfo |
| 73 | +# dh_installman |
| 74 | + dh_link |
| 75 | + dh_strip |
| 76 | + dh_compress |
| 77 | + dh_fixperms |
| 78 | +# dh_perl |
| 79 | +# dh_makeshlibs |
| 80 | + dh_installdeb |
| 81 | + dh_shlibdeps |
| 82 | + dh_gencontrol |
| 83 | + dh_md5sums |
| 84 | + dh_builddeb |
| 85 | + |
| 86 | +binary: binary-indep binary-arch |
| 87 | +.PHONY: build clean binary-indep binary-arch binary install configure |
Property changes on: trunk/debs/ircecho/debian/rules |
___________________________________________________________________ |
Added: svn:executable |
1 | 88 | + * |
Index: trunk/debs/ircecho/debian/ircecho.init |
— | — | @@ -0,0 +1,142 @@ |
| 2 | +#! /bin/sh |
| 3 | +### BEGIN INIT INFO |
| 4 | +# Provides: irc-echoer |
| 5 | +# Required-Start: $remote_fs $syslog |
| 6 | +# Required-Stop: $remote_fs $syslog |
| 7 | +# Default-Start: 2 3 4 5 |
| 8 | +# Default-Stop: 0 1 6 |
| 9 | +# Short-Description: Input to IRC echoer |
| 10 | +# Description: Input to IRC echoer |
| 11 | +### END INIT INFO |
| 12 | + |
| 13 | +# Author: Ryan Lane <rlane@wikimedia.org> |
| 14 | +# |
| 15 | +# Do NOT "set -e" |
| 16 | + |
| 17 | +PATH=/sbin:/usr/sbin:/bin:/usr/bin |
| 18 | +DESC="Input to IRC echoer" |
| 19 | +DAEMON="/usr/ircecho/bin/ircecho" |
| 20 | +USER="nobody" |
| 21 | +SCRIPTNAME="/etc/init.d/ircecho" |
| 22 | +NAME="ircecho" |
| 23 | + |
| 24 | +# Exit if the package is not installed |
| 25 | +[ -x "$DAEMON" ] || exit 0 |
| 26 | + |
| 27 | +# Read configuration variable file if it is present |
| 28 | +[ -r /etc/default/$NAME ] && . /etc/default/$NAME |
| 29 | + |
| 30 | +# Load the VERBOSE setting and other rcS variables |
| 31 | +. /lib/init/vars.sh |
| 32 | + |
| 33 | +# Define LSB log_* functions. |
| 34 | +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. |
| 35 | +. /lib/lsb/init-functions |
| 36 | + |
| 37 | +# |
| 38 | +# Function that starts the daemon/service |
| 39 | +# |
| 40 | +do_start() |
| 41 | +{ |
| 42 | + # Return |
| 43 | + # 0 if daemon has been started |
| 44 | + # 1 if daemon was already running |
| 45 | + # 2 if daemon could not be started |
| 46 | + start-stop-daemon --start -m --pidfile=/var/run/ircecho.pid -b --quiet -c $USER --exec $DAEMON --test -- --infile=$INFILE $CHANS $NICK $SERVER > /dev/null \ |
| 47 | + || return 1 |
| 48 | + start-stop-daemon --start -m --pidfile=/var/run/ircecho.pid -b --quiet -c $USER --exec $DAEMON -- --infile=$INFILE $CHANS $NICK $SERVER \ |
| 49 | + || return 2 |
| 50 | + # Add code here, if necessary, that waits for the process to be ready |
| 51 | + # to handle requests from services started subsequently which depend |
| 52 | + # on this one. As a last resort, sleep for some time. |
| 53 | +} |
| 54 | + |
| 55 | +# |
| 56 | +# Function that stops the daemon/service |
| 57 | +# |
| 58 | +do_stop() |
| 59 | +{ |
| 60 | + # Return |
| 61 | + # 0 if daemon has been stopped |
| 62 | + # 1 if daemon was already stopped |
| 63 | + # 2 if daemon could not be stopped |
| 64 | + # other if a failure occurred |
| 65 | + start-stop-daemon --stop --pidfile=/var/run/ircecho.pid --signal 9 --quiet |
| 66 | + RETVAL="$?" |
| 67 | + [ "$RETVAL" = 2 ] && return 2 |
| 68 | + return "$RETVAL" |
| 69 | +} |
| 70 | + |
| 71 | +# |
| 72 | +# Function that sends a SIGHUP to the daemon/service |
| 73 | +# |
| 74 | +do_reload() { |
| 75 | + # |
| 76 | + # If the daemon can reload its configuration without |
| 77 | + # restarting (for example, when it is sent a SIGHUP), |
| 78 | + # then implement that here. |
| 79 | + # |
| 80 | + do_stop |
| 81 | + do_start |
| 82 | + return 0 |
| 83 | +} |
| 84 | + |
| 85 | +case "$1" in |
| 86 | + start) |
| 87 | + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" |
| 88 | + do_start |
| 89 | + case "$?" in |
| 90 | + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; |
| 91 | + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; |
| 92 | + esac |
| 93 | + ;; |
| 94 | + stop) |
| 95 | + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" |
| 96 | + do_stop |
| 97 | + case "$?" in |
| 98 | + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; |
| 99 | + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; |
| 100 | + esac |
| 101 | + ;; |
| 102 | + status) |
| 103 | + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? |
| 104 | + ;; |
| 105 | + #reload|force-reload) |
| 106 | + # |
| 107 | + # If do_reload() is not implemented then leave this commented out |
| 108 | + # and leave 'force-reload' as an alias for 'restart'. |
| 109 | + # |
| 110 | + #log_daemon_msg "Reloading $DESC" "$NAME" |
| 111 | + #do_reload |
| 112 | + #log_end_msg $? |
| 113 | + #;; |
| 114 | + restart|force-reload) |
| 115 | + # |
| 116 | + # If the "reload" option is implemented then remove the |
| 117 | + # 'force-reload' alias |
| 118 | + # |
| 119 | + log_daemon_msg "Restarting $DESC" "$NAME" |
| 120 | + do_stop |
| 121 | + case "$?" in |
| 122 | + 0|1) |
| 123 | + do_start |
| 124 | + case "$?" in |
| 125 | + 0) log_end_msg 0 ;; |
| 126 | + 1) log_end_msg 1 ;; # Old process is still running |
| 127 | + *) log_end_msg 1 ;; # Failed to start |
| 128 | + esac |
| 129 | + ;; |
| 130 | + *) |
| 131 | + # Failed to stop |
| 132 | + log_end_msg 1 |
| 133 | + ;; |
| 134 | + esac |
| 135 | + ;; |
| 136 | + *) |
| 137 | + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 |
| 138 | + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 |
| 139 | + exit 3 |
| 140 | + ;; |
| 141 | +esac |
| 142 | + |
| 143 | +: |
Index: trunk/debs/ircecho/ircbot.py |
— | — | @@ -0,0 +1,432 @@ |
| 2 | +# Copyright (C) 1999--2002 Joel Rosdahl |
| 3 | +# |
| 4 | +# This library is free software; you can redistribute it and/or |
| 5 | +# modify it under the terms of the GNU Lesser General Public |
| 6 | +# License as published by the Free Software Foundation; either |
| 7 | +# version 2.1 of the License, or (at your option) any later version. |
| 8 | +# |
| 9 | +# This library is distributed in the hope that it will be useful, |
| 10 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 12 | +# Lesser General Public License for more details. |
| 13 | +# |
| 14 | +# You should have received a copy of the GNU Lesser General Public |
| 15 | +# License along with this library; if not, write to the Free Software |
| 16 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 17 | +# |
| 18 | +# Joel Rosdahl <joel@rosdahl.net> |
| 19 | +# |
| 20 | +# $Id: ircbot.py,v 1.14 2003/10/29 21:11:07 jrosdahl Exp $ |
| 21 | + |
| 22 | +"""ircbot -- Simple IRC bot library. |
| 23 | + |
| 24 | +This module contains a single-server IRC bot class that can be used to |
| 25 | +write simpler bots. |
| 26 | +""" |
| 27 | + |
| 28 | +import sys |
| 29 | +import string |
| 30 | +from UserDict import UserDict |
| 31 | + |
| 32 | +from irclib import SimpleIRCClient |
| 33 | +from irclib import nm_to_n, irc_lower, all_events |
| 34 | +from irclib import parse_channel_modes, is_channel |
| 35 | +from irclib import ServerConnectionError |
| 36 | + |
| 37 | +class SingleServerIRCBot(SimpleIRCClient): |
| 38 | + """A single-server IRC bot class. |
| 39 | + |
| 40 | + The bot tries to reconnect if it is disconnected. |
| 41 | + |
| 42 | + The bot keeps track of the channels it has joined, the other |
| 43 | + clients that are present in the channels and which of those that |
| 44 | + have operator or voice modes. The "database" is kept in the |
| 45 | + self.channels attribute, which is an IRCDict of Channels. |
| 46 | + """ |
| 47 | + def __init__(self, server_list, nickname, realname, reconnection_interval=60): |
| 48 | + """Constructor for SingleServerIRCBot objects. |
| 49 | + |
| 50 | + Arguments: |
| 51 | + |
| 52 | + server_list -- A list of tuples (server, port) that |
| 53 | + defines which servers the bot should try to |
| 54 | + connect to. |
| 55 | + |
| 56 | + nickname -- The bot's nickname. |
| 57 | + |
| 58 | + realname -- The bot's realname. |
| 59 | + |
| 60 | + reconnection_interval -- How long the bot should wait |
| 61 | + before trying to reconnect. |
| 62 | + |
| 63 | + dcc_connections -- A list of initiated/accepted DCC |
| 64 | + connections. |
| 65 | + """ |
| 66 | + |
| 67 | + SimpleIRCClient.__init__(self) |
| 68 | + self.channels = IRCDict() |
| 69 | + self.server_list = server_list |
| 70 | + if not reconnection_interval or reconnection_interval < 0: |
| 71 | + reconnection_interval = 2**31 |
| 72 | + self.reconnection_interval = reconnection_interval |
| 73 | + |
| 74 | + self._nickname = nickname |
| 75 | + self._realname = realname |
| 76 | + for i in ["disconnect", "join", "kick", "mode", |
| 77 | + "namreply", "nick", "part", "quit"]: |
| 78 | + self.connection.add_global_handler(i, |
| 79 | + getattr(self, "_on_" + i), |
| 80 | + -10) |
| 81 | + def _connected_checker(self): |
| 82 | + """[Internal]""" |
| 83 | + if not self.connection.is_connected(): |
| 84 | + self.connection.execute_delayed(self.reconnection_interval, |
| 85 | + self._connected_checker) |
| 86 | + self.jump_server() |
| 87 | + |
| 88 | + def _connect(self): |
| 89 | + """[Internal]""" |
| 90 | + password = None |
| 91 | + if len(self.server_list[0]) > 2: |
| 92 | + password = self.server_list[0][2] |
| 93 | + try: |
| 94 | + self.connect(self.server_list[0][0], |
| 95 | + self.server_list[0][1], |
| 96 | + self._nickname, |
| 97 | + password, |
| 98 | + ircname=self._realname) |
| 99 | + except ServerConnectionError: |
| 100 | + pass |
| 101 | + |
| 102 | + def _on_disconnect(self, c, e): |
| 103 | + """[Internal]""" |
| 104 | + self.channels = IRCDict() |
| 105 | + self.connection.execute_delayed(self.reconnection_interval, |
| 106 | + self._connected_checker) |
| 107 | + |
| 108 | + def _on_join(self, c, e): |
| 109 | + """[Internal]""" |
| 110 | + ch = e.target() |
| 111 | + nick = nm_to_n(e.source()) |
| 112 | + if nick == c.get_nickname(): |
| 113 | + self.channels[ch] = Channel() |
| 114 | + self.channels[ch].add_user(nick) |
| 115 | + |
| 116 | + def _on_kick(self, c, e): |
| 117 | + """[Internal]""" |
| 118 | + nick = e.arguments()[0] |
| 119 | + channel = e.target() |
| 120 | + |
| 121 | + if nick == c.get_nickname(): |
| 122 | + del self.channels[channel] |
| 123 | + else: |
| 124 | + self.channels[channel].remove_user(nick) |
| 125 | + |
| 126 | + def _on_mode(self, c, e): |
| 127 | + """[Internal]""" |
| 128 | + modes = parse_channel_modes(string.join(e.arguments())) |
| 129 | + t = e.target() |
| 130 | + if is_channel(t): |
| 131 | + ch = self.channels[t] |
| 132 | + for mode in modes: |
| 133 | + if mode[0] == "+": |
| 134 | + f = ch.set_mode |
| 135 | + else: |
| 136 | + f = ch.clear_mode |
| 137 | + f(mode[1], mode[2]) |
| 138 | + else: |
| 139 | + # Mode on self... XXX |
| 140 | + pass |
| 141 | + |
| 142 | + def _on_namreply(self, c, e): |
| 143 | + """[Internal]""" |
| 144 | + |
| 145 | + # e.arguments()[0] == "=" (why?) |
| 146 | + # e.arguments()[1] == channel |
| 147 | + # e.arguments()[2] == nick list |
| 148 | + |
| 149 | + ch = e.arguments()[1] |
| 150 | + for nick in string.split(e.arguments()[2]): |
| 151 | + if nick[0] == "@": |
| 152 | + nick = nick[1:] |
| 153 | + self.channels[ch].set_mode("o", nick) |
| 154 | + elif nick[0] == "+": |
| 155 | + nick = nick[1:] |
| 156 | + self.channels[ch].set_mode("v", nick) |
| 157 | + self.channels[ch].add_user(nick) |
| 158 | + |
| 159 | + def _on_nick(self, c, e): |
| 160 | + """[Internal]""" |
| 161 | + before = nm_to_n(e.source()) |
| 162 | + after = e.target() |
| 163 | + for ch in self.channels.values(): |
| 164 | + if ch.has_user(before): |
| 165 | + ch.change_nick(before, after) |
| 166 | + |
| 167 | + def _on_part(self, c, e): |
| 168 | + """[Internal]""" |
| 169 | + nick = nm_to_n(e.source()) |
| 170 | + channel = e.target() |
| 171 | + |
| 172 | + if nick == c.get_nickname(): |
| 173 | + del self.channels[channel] |
| 174 | + else: |
| 175 | + self.channels[channel].remove_user(nick) |
| 176 | + |
| 177 | + def _on_quit(self, c, e): |
| 178 | + """[Internal]""" |
| 179 | + nick = nm_to_n(e.source()) |
| 180 | + for ch in self.channels.values(): |
| 181 | + if ch.has_user(nick): |
| 182 | + ch.remove_user(nick) |
| 183 | + |
| 184 | + def die(self, msg="Bye, cruel world!"): |
| 185 | + """Let the bot die. |
| 186 | + |
| 187 | + Arguments: |
| 188 | + |
| 189 | + msg -- Quit message. |
| 190 | + """ |
| 191 | + self.connection.quit(msg) |
| 192 | + sys.exit(0) |
| 193 | + |
| 194 | + def disconnect(self, msg="I'll be back!"): |
| 195 | + """Disconnect the bot. |
| 196 | + |
| 197 | + The bot will try to reconnect after a while. |
| 198 | + |
| 199 | + Arguments: |
| 200 | + |
| 201 | + msg -- Quit message. |
| 202 | + """ |
| 203 | + self.connection.quit(msg) |
| 204 | + |
| 205 | + def get_version(self): |
| 206 | + """Returns the bot version. |
| 207 | + |
| 208 | + Used when answering a CTCP VERSION request. |
| 209 | + """ |
| 210 | + return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>" |
| 211 | + |
| 212 | + def jump_server(self): |
| 213 | + """Connect to a new server, possibly disconnecting from the current. |
| 214 | + |
| 215 | + The bot will skip to next server in the server_list each time |
| 216 | + jump_server is called. |
| 217 | + """ |
| 218 | + if self.connection.is_connected(): |
| 219 | + self.connection.quit("Jumping servers") |
| 220 | + self.server_list.append(self.server_list.pop(0)) |
| 221 | + self._connect() |
| 222 | + |
| 223 | + def on_ctcp(self, c, e): |
| 224 | + """Default handler for ctcp events. |
| 225 | + |
| 226 | + Replies to VERSION and PING requests and relays DCC requests |
| 227 | + to the on_dccchat method. |
| 228 | + """ |
| 229 | + if e.arguments()[0] == "VERSION": |
| 230 | + c.ctcp_reply(nm_to_n(e.source()), |
| 231 | + "VERSION " + self.get_version()) |
| 232 | + elif e.arguments()[0] == "PING": |
| 233 | + if len(e.arguments()) > 1: |
| 234 | + c.ctcp_reply(nm_to_n(e.source()), |
| 235 | + "PING " + e.arguments()[1]) |
| 236 | + elif e.arguments()[0] == "DCC" and e.arguments()[1] == "CHAT": |
| 237 | + self.on_dccchat(c, e) |
| 238 | + |
| 239 | + def on_dccchat(self, c, e): |
| 240 | + pass |
| 241 | + |
| 242 | + def start(self): |
| 243 | + """Start the bot.""" |
| 244 | + self._connect() |
| 245 | + SimpleIRCClient.start(self) |
| 246 | + |
| 247 | + |
| 248 | +class IRCDict: |
| 249 | + """A dictionary suitable for storing IRC-related things. |
| 250 | + |
| 251 | + Dictionary keys a and b are considered equal if and only if |
| 252 | + irc_lower(a) == irc_lower(b) |
| 253 | + |
| 254 | + Otherwise, it should behave exactly as a normal dictionary. |
| 255 | + """ |
| 256 | + |
| 257 | + def __init__(self, dict=None): |
| 258 | + self.data = {} |
| 259 | + self.canon_keys = {} # Canonical keys |
| 260 | + if dict is not None: |
| 261 | + self.update(dict) |
| 262 | + def __repr__(self): |
| 263 | + return repr(self.data) |
| 264 | + def __cmp__(self, dict): |
| 265 | + if isinstance(dict, IRCDict): |
| 266 | + return cmp(self.data, dict.data) |
| 267 | + else: |
| 268 | + return cmp(self.data, dict) |
| 269 | + def __len__(self): |
| 270 | + return len(self.data) |
| 271 | + def __getitem__(self, key): |
| 272 | + return self.data[self.canon_keys[irc_lower(key)]] |
| 273 | + def __setitem__(self, key, item): |
| 274 | + if self.has_key(key): |
| 275 | + del self[key] |
| 276 | + self.data[key] = item |
| 277 | + self.canon_keys[irc_lower(key)] = key |
| 278 | + def __delitem__(self, key): |
| 279 | + ck = irc_lower(key) |
| 280 | + del self.data[self.canon_keys[ck]] |
| 281 | + del self.canon_keys[ck] |
| 282 | + def clear(self): |
| 283 | + self.data.clear() |
| 284 | + self.canon_keys.clear() |
| 285 | + def copy(self): |
| 286 | + if self.__class__ is UserDict: |
| 287 | + return UserDict(self.data) |
| 288 | + import copy |
| 289 | + return copy.copy(self) |
| 290 | + def keys(self): |
| 291 | + return self.data.keys() |
| 292 | + def items(self): |
| 293 | + return self.data.items() |
| 294 | + def values(self): |
| 295 | + return self.data.values() |
| 296 | + def has_key(self, key): |
| 297 | + return self.canon_keys.has_key(irc_lower(key)) |
| 298 | + def update(self, dict): |
| 299 | + for k, v in dict.items(): |
| 300 | + self.data[k] = v |
| 301 | + def get(self, key, failobj=None): |
| 302 | + return self.data.get(key, failobj) |
| 303 | + |
| 304 | + |
| 305 | +class Channel: |
| 306 | + """A class for keeping information about an IRC channel. |
| 307 | + |
| 308 | + This class can be improved a lot. |
| 309 | + """ |
| 310 | + |
| 311 | + def __init__(self): |
| 312 | + self.userdict = IRCDict() |
| 313 | + self.operdict = IRCDict() |
| 314 | + self.voiceddict = IRCDict() |
| 315 | + self.modes = {} |
| 316 | + |
| 317 | + def users(self): |
| 318 | + """Returns an unsorted list of the channel's users.""" |
| 319 | + return self.userdict.keys() |
| 320 | + |
| 321 | + def opers(self): |
| 322 | + """Returns an unsorted list of the channel's operators.""" |
| 323 | + return self.operdict.keys() |
| 324 | + |
| 325 | + def voiced(self): |
| 326 | + """Returns an unsorted list of the persons that have voice |
| 327 | + mode set in the channel.""" |
| 328 | + return self.voiceddict.keys() |
| 329 | + |
| 330 | + def has_user(self, nick): |
| 331 | + """Check whether the channel has a user.""" |
| 332 | + return self.userdict.has_key(nick) |
| 333 | + |
| 334 | + def is_oper(self, nick): |
| 335 | + """Check whether a user has operator status in the channel.""" |
| 336 | + return self.operdict.has_key(nick) |
| 337 | + |
| 338 | + def is_voiced(self, nick): |
| 339 | + """Check whether a user has voice mode set in the channel.""" |
| 340 | + return self.voiceddict.has_key(nick) |
| 341 | + |
| 342 | + def add_user(self, nick): |
| 343 | + self.userdict[nick] = 1 |
| 344 | + |
| 345 | + def remove_user(self, nick): |
| 346 | + for d in self.userdict, self.operdict, self.voiceddict: |
| 347 | + if d.has_key(nick): |
| 348 | + del d[nick] |
| 349 | + |
| 350 | + def change_nick(self, before, after): |
| 351 | + self.userdict[after] = 1 |
| 352 | + del self.userdict[before] |
| 353 | + if self.operdict.has_key(before): |
| 354 | + self.operdict[after] = 1 |
| 355 | + del self.operdict[before] |
| 356 | + if self.voiceddict.has_key(before): |
| 357 | + self.voiceddict[after] = 1 |
| 358 | + del self.voiceddict[before] |
| 359 | + |
| 360 | + def set_mode(self, mode, value=None): |
| 361 | + """Set mode on the channel. |
| 362 | + |
| 363 | + Arguments: |
| 364 | + |
| 365 | + mode -- The mode (a single-character string). |
| 366 | + |
| 367 | + value -- Value |
| 368 | + """ |
| 369 | + if mode == "o": |
| 370 | + self.operdict[value] = 1 |
| 371 | + elif mode == "v": |
| 372 | + self.voiceddict[value] = 1 |
| 373 | + else: |
| 374 | + self.modes[mode] = value |
| 375 | + |
| 376 | + def clear_mode(self, mode, value=None): |
| 377 | + """Clear mode on the channel. |
| 378 | + |
| 379 | + Arguments: |
| 380 | + |
| 381 | + mode -- The mode (a single-character string). |
| 382 | + |
| 383 | + value -- Value |
| 384 | + """ |
| 385 | + try: |
| 386 | + if mode == "o": |
| 387 | + del self.operdict[value] |
| 388 | + elif mode == "v": |
| 389 | + del self.voiceddict[value] |
| 390 | + else: |
| 391 | + del self.modes[mode] |
| 392 | + except KeyError: |
| 393 | + pass |
| 394 | + |
| 395 | + def has_mode(self, mode): |
| 396 | + return mode in self.modes |
| 397 | + |
| 398 | + def is_moderated(self): |
| 399 | + return self.has_mode("m") |
| 400 | + |
| 401 | + def is_secret(self): |
| 402 | + return self.has_mode("s") |
| 403 | + |
| 404 | + def is_protected(self): |
| 405 | + return self.has_mode("p") |
| 406 | + |
| 407 | + def has_topic_lock(self): |
| 408 | + return self.has_mode("t") |
| 409 | + |
| 410 | + def is_invite_only(self): |
| 411 | + return self.has_mode("i") |
| 412 | + |
| 413 | + def has_message_from_outside_protection(self): |
| 414 | + # Eh... What should it be called, really? |
| 415 | + return self.has_mode("n") |
| 416 | + |
| 417 | + def has_limit(self): |
| 418 | + return self.has_mode("l") |
| 419 | + |
| 420 | + def limit(self): |
| 421 | + if self.has_limit(): |
| 422 | + return self.modes[l] |
| 423 | + else: |
| 424 | + return None |
| 425 | + |
| 426 | + def has_key(self): |
| 427 | + return self.has_mode("k") |
| 428 | + |
| 429 | + def key(self): |
| 430 | + if self.has_key(): |
| 431 | + return self.modes["k"] |
| 432 | + else: |
| 433 | + return None |
Index: trunk/debs/ircecho/irclib.py |
— | — | @@ -0,0 +1,1554 @@ |
| 2 | +# Copyright (C) 1999--2002 Joel Rosdahl |
| 3 | +# |
| 4 | +# This library is free software; you can redistribute it and/or |
| 5 | +# modify it under the terms of the GNU Lesser General Public |
| 6 | +# License as published by the Free Software Foundation; either |
| 7 | +# version 2.1 of the License, or (at your option) any later version. |
| 8 | +# |
| 9 | +# This library is distributed in the hope that it will be useful, |
| 10 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 12 | +# Lesser General Public License for more details. |
| 13 | +# |
| 14 | +# You should have received a copy of the GNU Lesser General Public |
| 15 | +# License along with this library; if not, write to the Free Software |
| 16 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 17 | +# |
| 18 | +# Joel Rosdahl <joel@rosdahl.net> |
| 19 | +# |
| 20 | +# $Id: irclib.py,v 1.23 2004/07/09 08:54:40 jrosdahl Exp $ |
| 21 | + |
| 22 | +"""irclib -- Internet Relay Chat (IRC) protocol client library. |
| 23 | + |
| 24 | +This library is intended to encapsulate the IRC protocol at a quite |
| 25 | +low level. It provides an event-driven IRC client framework. It has |
| 26 | +a fairly thorough support for the basic IRC protocol and CTCP, but DCC |
| 27 | +connection support is not yet implemented. |
| 28 | + |
| 29 | +In order to understand how to make an IRC client, I'm afraid you more |
| 30 | +or less must understand the IRC specifications. They are available |
| 31 | +here: [IRC specifications]. |
| 32 | + |
| 33 | +The main features of the IRC client framework are: |
| 34 | + |
| 35 | + * Abstraction of the IRC protocol. |
| 36 | + * Handles multiple simultaneous IRC server connections. |
| 37 | + * Handles server PONGing transparently. |
| 38 | + * Messages to the IRC server are done by calling methods on an IRC |
| 39 | + connection object. |
| 40 | + * Messages from an IRC server triggers events, which can be caught |
| 41 | + by event handlers. |
| 42 | + * Reading from and writing to IRC server sockets are normally done |
| 43 | + by an internal select() loop, but the select()ing may be done by |
| 44 | + an external main loop. |
| 45 | + * Functions can be registered to execute at specified times by the |
| 46 | + event-loop. |
| 47 | + * Decodes CTCP tagging correctly (hopefully); I haven't seen any |
| 48 | + other IRC client implementation that handles the CTCP |
| 49 | + specification subtilties. |
| 50 | + * A kind of simple, single-server, object-oriented IRC client class |
| 51 | + that dispatches events to instance methods is included. |
| 52 | + |
| 53 | +Current limitations: |
| 54 | + |
| 55 | + * The IRC protocol shines through the abstraction a bit too much. |
| 56 | + * Data is not written asynchronously to the server, i.e. the write() |
| 57 | + may block if the TCP buffers are stuffed. |
| 58 | + * There are no support for DCC connections. |
| 59 | + * The author haven't even read RFC 2810, 2811, 2812 and 2813. |
| 60 | + * Like most projects, documentation is lacking... |
| 61 | + |
| 62 | +Since I seldom use IRC anymore, I will probably not work much on the |
| 63 | +library. If you want to help or continue developing the library, |
| 64 | +please contact me (Joel Rosdahl <joel@rosdahl.net>). |
| 65 | + |
| 66 | +.. [IRC specifications] http://www.irchelp.org/irchelp/rfc/ |
| 67 | +""" |
| 68 | + |
| 69 | +import bisect |
| 70 | +import re |
| 71 | +import select |
| 72 | +import socket |
| 73 | +import string |
| 74 | +import sys |
| 75 | +import time |
| 76 | +import types |
| 77 | + |
| 78 | +VERSION = 0, 4, 2 |
| 79 | +DEBUG = 0 |
| 80 | + |
| 81 | +# TODO |
| 82 | +# ---- |
| 83 | +# (maybe) thread safety |
| 84 | +# (maybe) color parser convenience functions |
| 85 | +# documentation (including all event types) |
| 86 | +# (maybe) add awareness of different types of ircds |
| 87 | +# send data asynchronously to the server (and DCC connections) |
| 88 | +# (maybe) automatically close unused, passive DCC connections after a while |
| 89 | + |
| 90 | +# NOTES |
| 91 | +# ----- |
| 92 | +# connection.quit() only sends QUIT to the server. |
| 93 | +# ERROR from the server triggers the error event and the disconnect event. |
| 94 | +# dropping of the connection triggers the disconnect event. |
| 95 | + |
| 96 | +class IRCError(Exception): |
| 97 | + """Represents an IRC exception.""" |
| 98 | + pass |
| 99 | + |
| 100 | + |
| 101 | +class IRC: |
| 102 | + """Class that handles one or several IRC server connections. |
| 103 | + |
| 104 | + When an IRC object has been instantiated, it can be used to create |
| 105 | + Connection objects that represent the IRC connections. The |
| 106 | + responsibility of the IRC object is to provide an event-driven |
| 107 | + framework for the connections and to keep the connections alive. |
| 108 | + It runs a select loop to poll each connection's TCP socket and |
| 109 | + hands over the sockets with incoming data for processing by the |
| 110 | + corresponding connection. |
| 111 | + |
| 112 | + The methods of most interest for an IRC client writer are server, |
| 113 | + add_global_handler, remove_global_handler, execute_at, |
| 114 | + execute_delayed, process_once and process_forever. |
| 115 | + |
| 116 | + Here is an example: |
| 117 | + |
| 118 | + irc = irclib.IRC() |
| 119 | + server = irc.server() |
| 120 | + server.connect(\"irc.some.where\", 6667, \"my_nickname\") |
| 121 | + server.privmsg(\"a_nickname\", \"Hi there!\") |
| 122 | + server.process_forever() |
| 123 | + |
| 124 | + This will connect to the IRC server irc.some.where on port 6667 |
| 125 | + using the nickname my_nickname and send the message \"Hi there!\" |
| 126 | + to the nickname a_nickname. |
| 127 | + """ |
| 128 | + |
| 129 | + def __init__(self, fn_to_add_socket=None, |
| 130 | + fn_to_remove_socket=None, |
| 131 | + fn_to_add_timeout=None): |
| 132 | + """Constructor for IRC objects. |
| 133 | + |
| 134 | + Optional arguments are fn_to_add_socket, fn_to_remove_socket |
| 135 | + and fn_to_add_timeout. The first two specify functions that |
| 136 | + will be called with a socket object as argument when the IRC |
| 137 | + object wants to be notified (or stop being notified) of data |
| 138 | + coming on a new socket. When new data arrives, the method |
| 139 | + process_data should be called. Similarly, fn_to_add_timeout |
| 140 | + is called with a number of seconds (a floating point number) |
| 141 | + as first argument when the IRC object wants to receive a |
| 142 | + notification (by calling the process_timeout method). So, if |
| 143 | + e.g. the argument is 42.17, the object wants the |
| 144 | + process_timeout method to be called after 42 seconds and 170 |
| 145 | + milliseconds. |
| 146 | + |
| 147 | + The three arguments mainly exist to be able to use an external |
| 148 | + main loop (for example Tkinter's or PyGTK's main app loop) |
| 149 | + instead of calling the process_forever method. |
| 150 | + |
| 151 | + An alternative is to just call ServerConnection.process_once() |
| 152 | + once in a while. |
| 153 | + """ |
| 154 | + |
| 155 | + if fn_to_add_socket and fn_to_remove_socket: |
| 156 | + self.fn_to_add_socket = fn_to_add_socket |
| 157 | + self.fn_to_remove_socket = fn_to_remove_socket |
| 158 | + else: |
| 159 | + self.fn_to_add_socket = None |
| 160 | + self.fn_to_remove_socket = None |
| 161 | + |
| 162 | + self.fn_to_add_timeout = fn_to_add_timeout |
| 163 | + self.connections = [] |
| 164 | + self.handlers = {} |
| 165 | + self.delayed_commands = [] # list of tuples in the format (time, function, arguments) |
| 166 | + |
| 167 | + self.add_global_handler("ping", _ping_ponger, -42) |
| 168 | + |
| 169 | + def server(self): |
| 170 | + """Creates and returns a ServerConnection object.""" |
| 171 | + |
| 172 | + c = ServerConnection(self) |
| 173 | + self.connections.append(c) |
| 174 | + return c |
| 175 | + |
| 176 | + def process_data(self, sockets): |
| 177 | + """Called when there is more data to read on connection sockets. |
| 178 | + |
| 179 | + Arguments: |
| 180 | + |
| 181 | + sockets -- A list of socket objects. |
| 182 | + |
| 183 | + See documentation for IRC.__init__. |
| 184 | + """ |
| 185 | + for s in sockets: |
| 186 | + for c in self.connections: |
| 187 | + if s == c._get_socket(): |
| 188 | + c.process_data() |
| 189 | + |
| 190 | + def process_timeout(self): |
| 191 | + """Called when a timeout notification is due. |
| 192 | + |
| 193 | + See documentation for IRC.__init__. |
| 194 | + """ |
| 195 | + t = time.time() |
| 196 | + while self.delayed_commands: |
| 197 | + if t >= self.delayed_commands[0][0]: |
| 198 | + apply(self.delayed_commands[0][1], self.delayed_commands[0][2]) |
| 199 | + del self.delayed_commands[0] |
| 200 | + else: |
| 201 | + break |
| 202 | + |
| 203 | + def process_once(self, timeout=0): |
| 204 | + """Process data from connections once. |
| 205 | + |
| 206 | + Arguments: |
| 207 | + |
| 208 | + timeout -- How long the select() call should wait if no |
| 209 | + data is available. |
| 210 | + |
| 211 | + This method should be called periodically to check and process |
| 212 | + incoming data, if there are any. If that seems boring, look |
| 213 | + at the process_forever method. |
| 214 | + """ |
| 215 | + sockets = map(lambda x: x._get_socket(), self.connections) |
| 216 | + sockets = filter(lambda x: x != None, sockets) |
| 217 | + if sockets: |
| 218 | + (i, o, e) = select.select(sockets, [], [], timeout) |
| 219 | + self.process_data(i) |
| 220 | + else: |
| 221 | + time.sleep(timeout) |
| 222 | + self.process_timeout() |
| 223 | + |
| 224 | + def process_forever(self, timeout=0.2): |
| 225 | + """Run an infinite loop, processing data from connections. |
| 226 | + |
| 227 | + This method repeatedly calls process_once. |
| 228 | + |
| 229 | + Arguments: |
| 230 | + |
| 231 | + timeout -- Parameter to pass to process_once. |
| 232 | + """ |
| 233 | + while 1: |
| 234 | + self.process_once(timeout) |
| 235 | + |
| 236 | + def disconnect_all(self, message=""): |
| 237 | + """Disconnects all connections.""" |
| 238 | + for c in self.connections: |
| 239 | + c.quit(message) |
| 240 | + c.disconnect(message) |
| 241 | + |
| 242 | + def add_global_handler(self, event, handler, priority=0): |
| 243 | + """Adds a global handler function for a specific event type. |
| 244 | + |
| 245 | + Arguments: |
| 246 | + |
| 247 | + event -- Event type (a string). Check the values of the |
| 248 | + numeric_events dictionary in irclib.py for possible event |
| 249 | + types. |
| 250 | + |
| 251 | + handler -- Callback function. |
| 252 | + |
| 253 | + priority -- A number (the lower number, the higher priority). |
| 254 | + |
| 255 | + The handler function is called whenever the specified event is |
| 256 | + triggered in any of the connections. See documentation for |
| 257 | + the Event class. |
| 258 | + |
| 259 | + The handler functions are called in priority order (lowest |
| 260 | + number is highest priority). If a handler function returns |
| 261 | + \"NO MORE\", no more handlers will be called. |
| 262 | + """ |
| 263 | + |
| 264 | + if not self.handlers.has_key(event): |
| 265 | + self.handlers[event] = [] |
| 266 | + bisect.insort(self.handlers[event], ((priority, handler))) |
| 267 | + |
| 268 | + def remove_global_handler(self, event, handler): |
| 269 | + """Removes a global handler function. |
| 270 | + |
| 271 | + Arguments: |
| 272 | + |
| 273 | + event -- Event type (a string). |
| 274 | + |
| 275 | + handler -- Callback function. |
| 276 | + |
| 277 | + Returns 1 on success, otherwise 0. |
| 278 | + """ |
| 279 | + if not self.handlers.has_key(event): |
| 280 | + return 0 |
| 281 | + for h in self.handlers[event]: |
| 282 | + if handler == h[1]: |
| 283 | + self.handlers[event].remove(h) |
| 284 | + return 1 |
| 285 | + |
| 286 | + def execute_at(self, at, function, arguments=()): |
| 287 | + """Execute a function at a specified time. |
| 288 | + |
| 289 | + Arguments: |
| 290 | + |
| 291 | + at -- Execute at this time (standard \"time_t\" time). |
| 292 | + |
| 293 | + function -- Function to call. |
| 294 | + |
| 295 | + arguments -- Arguments to give the function. |
| 296 | + """ |
| 297 | + self.execute_delayed(at-time.time(), function, arguments) |
| 298 | + |
| 299 | + def execute_delayed(self, delay, function, arguments=()): |
| 300 | + """Execute a function after a specified time. |
| 301 | + |
| 302 | + Arguments: |
| 303 | + |
| 304 | + delay -- How many seconds to wait. |
| 305 | + |
| 306 | + function -- Function to call. |
| 307 | + |
| 308 | + arguments -- Arguments to give the function. |
| 309 | + """ |
| 310 | + bisect.insort(self.delayed_commands, (delay+time.time(), function, arguments)) |
| 311 | + if self.fn_to_add_timeout: |
| 312 | + self.fn_to_add_timeout(delay) |
| 313 | + |
| 314 | + def dcc(self, dcctype="chat"): |
| 315 | + """Creates and returns a DCCConnection object. |
| 316 | + |
| 317 | + Arguments: |
| 318 | + |
| 319 | + dcctype -- "chat" for DCC CHAT connections or "raw" for |
| 320 | + DCC SEND (or other DCC types). If "chat", |
| 321 | + incoming data will be split in newline-separated |
| 322 | + chunks. If "raw", incoming data is not touched. |
| 323 | + """ |
| 324 | + c = DCCConnection(self, dcctype) |
| 325 | + self.connections.append(c) |
| 326 | + return c |
| 327 | + |
| 328 | + def _handle_event(self, connection, event): |
| 329 | + """[Internal]""" |
| 330 | + h = self.handlers |
| 331 | + for handler in h.get("all_events", []) + h.get(event.eventtype(), []): |
| 332 | + if handler[1](connection, event) == "NO MORE": |
| 333 | + return |
| 334 | + |
| 335 | + def _remove_connection(self, connection): |
| 336 | + """[Internal]""" |
| 337 | + self.connections.remove(connection) |
| 338 | + if self.fn_to_remove_socket: |
| 339 | + self.fn_to_remove_socket(connection._get_socket()) |
| 340 | + |
| 341 | +_rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?") |
| 342 | + |
| 343 | + |
| 344 | +class Connection: |
| 345 | + """Base class for IRC connections. |
| 346 | + |
| 347 | + Must be overridden. |
| 348 | + """ |
| 349 | + def __init__(self, irclibobj): |
| 350 | + self.irclibobj = irclibobj |
| 351 | + |
| 352 | + def _get_socket(): |
| 353 | + raise IRCError, "Not overridden" |
| 354 | + |
| 355 | + ############################## |
| 356 | + ### Convenience wrappers. |
| 357 | + |
| 358 | + def execute_at(self, at, function, arguments=()): |
| 359 | + self.irclibobj.execute_at(at, function, arguments) |
| 360 | + |
| 361 | + def execute_delayed(self, delay, function, arguments=()): |
| 362 | + self.irclibobj.execute_delayed(delay, function, arguments) |
| 363 | + |
| 364 | + |
| 365 | +class ServerConnectionError(IRCError): |
| 366 | + pass |
| 367 | + |
| 368 | +class ServerNotConnectedError(ServerConnectionError): |
| 369 | + pass |
| 370 | + |
| 371 | + |
| 372 | +# Huh!? Crrrrazy EFNet doesn't follow the RFC: their ircd seems to |
| 373 | +# use \n as message separator! :P |
| 374 | +_linesep_regexp = re.compile("\r?\n") |
| 375 | + |
| 376 | +class ServerConnection(Connection): |
| 377 | + """This class represents an IRC server connection. |
| 378 | + |
| 379 | + ServerConnection objects are instantiated by calling the server |
| 380 | + method on an IRC object. |
| 381 | + """ |
| 382 | + |
| 383 | + def __init__(self, irclibobj): |
| 384 | + Connection.__init__(self, irclibobj) |
| 385 | + self.connected = 0 # Not connected yet. |
| 386 | + self.connect_time = 0 |
| 387 | + |
| 388 | + def connect(self, server, port, nickname, password=None, username=None, |
| 389 | + ircname=None, localaddress="", localport=0): |
| 390 | + """Connect/reconnect to a server. |
| 391 | + |
| 392 | + Arguments: |
| 393 | + |
| 394 | + server -- Server name. |
| 395 | + |
| 396 | + port -- Port number. |
| 397 | + |
| 398 | + nickname -- The nickname. |
| 399 | + |
| 400 | + password -- Password (if any). |
| 401 | + |
| 402 | + username -- The username. |
| 403 | + |
| 404 | + ircname -- The IRC name ("realname"). |
| 405 | + |
| 406 | + localaddress -- Bind the connection to a specific local IP address. |
| 407 | + |
| 408 | + localport -- Bind the connection to a specific local port. |
| 409 | + |
| 410 | + This function can be called to reconnect a closed connection. |
| 411 | + |
| 412 | + Returns the ServerConnection object. |
| 413 | + """ |
| 414 | + |
| 415 | + # flood control |
| 416 | + delay = self.connect_time - time.time() + 60 |
| 417 | + if self.connect_time and delay > 0: |
| 418 | + time.sleep(delay) |
| 419 | + |
| 420 | + self.connect_time = time.time() |
| 421 | + |
| 422 | + if self.connected: |
| 423 | + self.quit("Changing server") |
| 424 | + |
| 425 | + self.socket = None |
| 426 | + self.previous_buffer = "" |
| 427 | + self.handlers = {} |
| 428 | + self.real_server_name = "" |
| 429 | + self.real_nickname = nickname |
| 430 | + self.server = server |
| 431 | + self.port = port |
| 432 | + self.nickname = nickname |
| 433 | + self.username = username or nickname |
| 434 | + self.ircname = ircname or nickname |
| 435 | + self.password = password |
| 436 | + self.localaddress = localaddress |
| 437 | + self.localport = localport |
| 438 | + self.localhost = socket.gethostname() |
| 439 | + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 440 | + try: |
| 441 | + self.socket.bind((self.localaddress, self.localport)) |
| 442 | + self.socket.connect((self.server, self.port)) |
| 443 | + except socket.error, x: |
| 444 | + raise ServerConnectionError, "Couldn't connect to socket: %s" % x |
| 445 | + self.connected = 1 |
| 446 | + if self.irclibobj.fn_to_add_socket: |
| 447 | + self.irclibobj.fn_to_add_socket(self.socket) |
| 448 | + |
| 449 | + # Log on... |
| 450 | + if self.password: |
| 451 | + self.pass_(self.password) |
| 452 | + self.nick(self.nickname) |
| 453 | + self.user(self.username, self.ircname) |
| 454 | + return self |
| 455 | + |
| 456 | + def close(self): |
| 457 | + """Close the connection. |
| 458 | + |
| 459 | + This method closes the connection permanently; after it has |
| 460 | + been called, the object is unusable. |
| 461 | + """ |
| 462 | + |
| 463 | + self.disconnect("Closing object") |
| 464 | + self.irclibobj._remove_connection(self) |
| 465 | + |
| 466 | + def _get_socket(self): |
| 467 | + """[Internal]""" |
| 468 | + return self.socket |
| 469 | + |
| 470 | + def get_server_name(self): |
| 471 | + """Get the (real) server name. |
| 472 | + |
| 473 | + This method returns the (real) server name, or, more |
| 474 | + specifically, what the server calls itself. |
| 475 | + """ |
| 476 | + |
| 477 | + if self.real_server_name: |
| 478 | + return self.real_server_name |
| 479 | + else: |
| 480 | + return "" |
| 481 | + |
| 482 | + def get_nickname(self): |
| 483 | + """Get the (real) nick name. |
| 484 | + |
| 485 | + This method returns the (real) nickname. The library keeps |
| 486 | + track of nick changes, so it might not be the nick name that |
| 487 | + was passed to the connect() method. """ |
| 488 | + |
| 489 | + return self.real_nickname |
| 490 | + |
| 491 | + def process_data(self): |
| 492 | + """[Internal]""" |
| 493 | + |
| 494 | + try: |
| 495 | + new_data = self.socket.recv(2**14) |
| 496 | + except socket.error, x: |
| 497 | + # The server hung up. |
| 498 | + self.disconnect("Connection reset by peer") |
| 499 | + return |
| 500 | + if not new_data: |
| 501 | + # Read nothing: connection must be down. |
| 502 | + self.disconnect("Connection reset by peer") |
| 503 | + return |
| 504 | + |
| 505 | + lines = _linesep_regexp.split(self.previous_buffer + new_data) |
| 506 | + |
| 507 | + # Save the last, unfinished line. |
| 508 | + self.previous_buffer = lines[-1] |
| 509 | + lines = lines[:-1] |
| 510 | + |
| 511 | + for line in lines: |
| 512 | + if DEBUG: |
| 513 | + print "FROM SERVER:", line |
| 514 | + |
| 515 | + if not line: |
| 516 | + continue |
| 517 | + |
| 518 | + prefix = None |
| 519 | + command = None |
| 520 | + arguments = None |
| 521 | + self._handle_event(Event("all_raw_messages", |
| 522 | + self.get_server_name(), |
| 523 | + None, |
| 524 | + [line])) |
| 525 | + |
| 526 | + m = _rfc_1459_command_regexp.match(line) |
| 527 | + if m.group("prefix"): |
| 528 | + prefix = m.group("prefix") |
| 529 | + if not self.real_server_name: |
| 530 | + self.real_server_name = prefix |
| 531 | + |
| 532 | + if m.group("command"): |
| 533 | + command = string.lower(m.group("command")) |
| 534 | + |
| 535 | + if m.group("argument"): |
| 536 | + a = string.split(m.group("argument"), " :", 1) |
| 537 | + arguments = string.split(a[0]) |
| 538 | + if len(a) == 2: |
| 539 | + arguments.append(a[1]) |
| 540 | + |
| 541 | + if command == "nick": |
| 542 | + if nm_to_n(prefix) == self.real_nickname: |
| 543 | + self.real_nickname = arguments[0] |
| 544 | + elif command == "001": |
| 545 | + # Record the nickname in case the client changed nick |
| 546 | + # in a nicknameinuse callback. |
| 547 | + self.real_nickname = arguments[0] |
| 548 | + |
| 549 | + if command in ["privmsg", "notice"]: |
| 550 | + target, message = arguments[0], arguments[1] |
| 551 | + messages = _ctcp_dequote(message) |
| 552 | + |
| 553 | + if command == "privmsg": |
| 554 | + if is_channel(target): |
| 555 | + command = "pubmsg" |
| 556 | + else: |
| 557 | + if is_channel(target): |
| 558 | + command = "pubnotice" |
| 559 | + else: |
| 560 | + command = "privnotice" |
| 561 | + |
| 562 | + for m in messages: |
| 563 | + if type(m) is types.TupleType: |
| 564 | + if command in ["privmsg", "pubmsg"]: |
| 565 | + command = "ctcp" |
| 566 | + else: |
| 567 | + command = "ctcpreply" |
| 568 | + |
| 569 | + m = list(m) |
| 570 | + if DEBUG: |
| 571 | + print "command: %s, source: %s, target: %s, arguments: %s" % ( |
| 572 | + command, prefix, target, m) |
| 573 | + self._handle_event(Event(command, prefix, target, m)) |
| 574 | + else: |
| 575 | + if DEBUG: |
| 576 | + print "command: %s, source: %s, target: %s, arguments: %s" % ( |
| 577 | + command, prefix, target, [m]) |
| 578 | + self._handle_event(Event(command, prefix, target, [m])) |
| 579 | + else: |
| 580 | + target = None |
| 581 | + |
| 582 | + if command == "quit": |
| 583 | + arguments = [arguments[0]] |
| 584 | + elif command == "ping": |
| 585 | + target = arguments[0] |
| 586 | + else: |
| 587 | + target = arguments[0] |
| 588 | + arguments = arguments[1:] |
| 589 | + |
| 590 | + if command == "mode": |
| 591 | + if not is_channel(target): |
| 592 | + command = "umode" |
| 593 | + |
| 594 | + # Translate numerics into more readable strings. |
| 595 | + if numeric_events.has_key(command): |
| 596 | + command = numeric_events[command] |
| 597 | + |
| 598 | + if DEBUG: |
| 599 | + print "command: %s, source: %s, target: %s, arguments: %s" % ( |
| 600 | + command, prefix, target, arguments) |
| 601 | + self._handle_event(Event(command, prefix, target, arguments)) |
| 602 | + |
| 603 | + def _handle_event(self, event): |
| 604 | + """[Internal]""" |
| 605 | + self.irclibobj._handle_event(self, event) |
| 606 | + if self.handlers.has_key(event.eventtype()): |
| 607 | + for fn in self.handlers[event.eventtype()]: |
| 608 | + fn(self, event) |
| 609 | + |
| 610 | + def is_connected(self): |
| 611 | + """Return connection status. |
| 612 | + |
| 613 | + Returns true if connected, otherwise false. |
| 614 | + """ |
| 615 | + return self.connected |
| 616 | + |
| 617 | + def add_global_handler(self, *args): |
| 618 | + """Add global handler. |
| 619 | + |
| 620 | + See documentation for IRC.add_global_handler. |
| 621 | + """ |
| 622 | + apply(self.irclibobj.add_global_handler, args) |
| 623 | + |
| 624 | + def remove_global_handler(self, *args): |
| 625 | + """Remove global handler. |
| 626 | + |
| 627 | + See documentation for IRC.remove_global_handler. |
| 628 | + """ |
| 629 | + apply(self.irclibobj.remove_global_handler, args) |
| 630 | + |
| 631 | + def action(self, target, action): |
| 632 | + """Send a CTCP ACTION command.""" |
| 633 | + self.ctcp("ACTION", target, action) |
| 634 | + |
| 635 | + def admin(self, server=""): |
| 636 | + """Send an ADMIN command.""" |
| 637 | + self.send_raw(string.strip(string.join(["ADMIN", server]))) |
| 638 | + |
| 639 | + def ctcp(self, ctcptype, target, parameter=""): |
| 640 | + """Send a CTCP command.""" |
| 641 | + ctcptype = string.upper(ctcptype) |
| 642 | + self.privmsg(target, "\001%s%s\001" % (ctcptype, parameter and (" " + parameter) or "")) |
| 643 | + |
| 644 | + def ctcp_reply(self, target, parameter): |
| 645 | + """Send a CTCP REPLY command.""" |
| 646 | + self.notice(target, "\001%s\001" % parameter) |
| 647 | + |
| 648 | + def disconnect(self, message=""): |
| 649 | + """Hang up the connection. |
| 650 | + |
| 651 | + Arguments: |
| 652 | + |
| 653 | + message -- Quit message. |
| 654 | + """ |
| 655 | + if not self.connected: |
| 656 | + return |
| 657 | + |
| 658 | + self.connected = 0 |
| 659 | + try: |
| 660 | + self.socket.close() |
| 661 | + except socket.error, x: |
| 662 | + pass |
| 663 | + self.socket = None |
| 664 | + self._handle_event(Event("disconnect", self.server, "", [message])) |
| 665 | + |
| 666 | + def globops(self, text): |
| 667 | + """Send a GLOBOPS command.""" |
| 668 | + self.send_raw("GLOBOPS :" + text) |
| 669 | + |
| 670 | + def info(self, server=""): |
| 671 | + """Send an INFO command.""" |
| 672 | + self.send_raw(string.strip(string.join(["INFO", server]))) |
| 673 | + |
| 674 | + def invite(self, nick, channel): |
| 675 | + """Send an INVITE command.""" |
| 676 | + self.send_raw(string.strip(string.join(["INVITE", nick, channel]))) |
| 677 | + |
| 678 | + def ison(self, nicks): |
| 679 | + """Send an ISON command. |
| 680 | + |
| 681 | + Arguments: |
| 682 | + |
| 683 | + nicks -- List of nicks. |
| 684 | + """ |
| 685 | + self.send_raw("ISON " + string.join(nicks, " ")) |
| 686 | + |
| 687 | + def join(self, channel, key=""): |
| 688 | + """Send a JOIN command.""" |
| 689 | + self.send_raw("JOIN %s%s" % (channel, (key and (" " + key)))) |
| 690 | + |
| 691 | + def kick(self, channel, nick, comment=""): |
| 692 | + """Send a KICK command.""" |
| 693 | + self.send_raw("KICK %s %s%s" % (channel, nick, (comment and (" :" + comment)))) |
| 694 | + |
| 695 | + def links(self, remote_server="", server_mask=""): |
| 696 | + """Send a LINKS command.""" |
| 697 | + command = "LINKS" |
| 698 | + if remote_server: |
| 699 | + command = command + " " + remote_server |
| 700 | + if server_mask: |
| 701 | + command = command + " " + server_mask |
| 702 | + self.send_raw(command) |
| 703 | + |
| 704 | + def list(self, channels=None, server=""): |
| 705 | + """Send a LIST command.""" |
| 706 | + command = "LIST" |
| 707 | + if channels: |
| 708 | + command = command + " " + string.join(channels, ",") |
| 709 | + if server: |
| 710 | + command = command + " " + server |
| 711 | + self.send_raw(command) |
| 712 | + |
| 713 | + def lusers(self, server=""): |
| 714 | + """Send a LUSERS command.""" |
| 715 | + self.send_raw("LUSERS" + (server and (" " + server))) |
| 716 | + |
| 717 | + def mode(self, target, command): |
| 718 | + """Send a MODE command.""" |
| 719 | + self.send_raw("MODE %s %s" % (target, command)) |
| 720 | + |
| 721 | + def motd(self, server=""): |
| 722 | + """Send an MOTD command.""" |
| 723 | + self.send_raw("MOTD" + (server and (" " + server))) |
| 724 | + |
| 725 | + def names(self, channels=None): |
| 726 | + """Send a NAMES command.""" |
| 727 | + self.send_raw("NAMES" + (channels and (" " + string.join(channels, ",")) or "")) |
| 728 | + |
| 729 | + def nick(self, newnick): |
| 730 | + """Send a NICK command.""" |
| 731 | + self.send_raw("NICK " + newnick) |
| 732 | + |
| 733 | + def notice(self, target, text): |
| 734 | + """Send a NOTICE command.""" |
| 735 | + # Should limit len(text) here! |
| 736 | + self.send_raw("NOTICE %s :%s" % (target, text)) |
| 737 | + |
| 738 | + def oper(self, nick, password): |
| 739 | + """Send an OPER command.""" |
| 740 | + self.send_raw("OPER %s %s" % (nick, password)) |
| 741 | + |
| 742 | + def part(self, channels): |
| 743 | + """Send a PART command.""" |
| 744 | + if type(channels) == types.StringType: |
| 745 | + self.send_raw("PART " + channels) |
| 746 | + else: |
| 747 | + self.send_raw("PART " + string.join(channels, ",")) |
| 748 | + |
| 749 | + def pass_(self, password): |
| 750 | + """Send a PASS command.""" |
| 751 | + self.send_raw("PASS " + password) |
| 752 | + |
| 753 | + def ping(self, target, target2=""): |
| 754 | + """Send a PING command.""" |
| 755 | + self.send_raw("PING %s%s" % (target, target2 and (" " + target2))) |
| 756 | + |
| 757 | + def pong(self, target, target2=""): |
| 758 | + """Send a PONG command.""" |
| 759 | + self.send_raw("PONG %s%s" % (target, target2 and (" " + target2))) |
| 760 | + |
| 761 | + def privmsg(self, target, text): |
| 762 | + """Send a PRIVMSG command.""" |
| 763 | + # Should limit len(text) here! |
| 764 | + self.send_raw("PRIVMSG %s :%s" % (target, text)) |
| 765 | + |
| 766 | + def privmsg_many(self, targets, text): |
| 767 | + """Send a PRIVMSG command to multiple targets.""" |
| 768 | + # Should limit len(text) here! |
| 769 | + self.send_raw("PRIVMSG %s :%s" % (string.join(targets, ","), text)) |
| 770 | + |
| 771 | + def quit(self, message=""): |
| 772 | + """Send a QUIT command.""" |
| 773 | + self.send_raw("QUIT" + (message and (" :" + message))) |
| 774 | + |
| 775 | + def sconnect(self, target, port="", server=""): |
| 776 | + """Send an SCONNECT command.""" |
| 777 | + self.send_raw("CONNECT %s%s%s" % (target, |
| 778 | + port and (" " + port), |
| 779 | + server and (" " + server))) |
| 780 | + |
| 781 | + def send_raw(self, string): |
| 782 | + """Send raw string to the server. |
| 783 | + |
| 784 | + The string will be padded with appropriate CR LF. |
| 785 | + """ |
| 786 | + if self.socket is None: |
| 787 | + raise ServerNotConnectedError, "Not connected." |
| 788 | + try: |
| 789 | + self.socket.send(string + "\r\n") |
| 790 | + if DEBUG: |
| 791 | + print "TO SERVER:", string |
| 792 | + except socket.error, x: |
| 793 | + # Ouch! |
| 794 | + self.disconnect("Connection reset by peer.") |
| 795 | + |
| 796 | + def squit(self, server, comment=""): |
| 797 | + """Send an SQUIT command.""" |
| 798 | + self.send_raw("SQUIT %s%s" % (server, comment and (" :" + comment))) |
| 799 | + |
| 800 | + def stats(self, statstype, server=""): |
| 801 | + """Send a STATS command.""" |
| 802 | + self.send_raw("STATS %s%s" % (statstype, server and (" " + server))) |
| 803 | + |
| 804 | + def time(self, server=""): |
| 805 | + """Send a TIME command.""" |
| 806 | + self.send_raw("TIME" + (server and (" " + server))) |
| 807 | + |
| 808 | + def topic(self, channel, new_topic=None): |
| 809 | + """Send a TOPIC command.""" |
| 810 | + if new_topic == None: |
| 811 | + self.send_raw("TOPIC " + channel) |
| 812 | + else: |
| 813 | + self.send_raw("TOPIC %s :%s" % (channel, new_topic)) |
| 814 | + |
| 815 | + def trace(self, target=""): |
| 816 | + """Send a TRACE command.""" |
| 817 | + self.send_raw("TRACE" + (target and (" " + target))) |
| 818 | + |
| 819 | + def user(self, username, realname): |
| 820 | + """Send a USER command.""" |
| 821 | + self.send_raw("USER %s 0 * :%s" % (username, realname)) |
| 822 | + |
| 823 | + def userhost(self, nicks): |
| 824 | + """Send a USERHOST command.""" |
| 825 | + self.send_raw("USERHOST " + string.join(nicks, ",")) |
| 826 | + |
| 827 | + def users(self, server=""): |
| 828 | + """Send a USERS command.""" |
| 829 | + self.send_raw("USERS" + (server and (" " + server))) |
| 830 | + |
| 831 | + def version(self, server=""): |
| 832 | + """Send a VERSION command.""" |
| 833 | + self.send_raw("VERSION" + (server and (" " + server))) |
| 834 | + |
| 835 | + def wallops(self, text): |
| 836 | + """Send a WALLOPS command.""" |
| 837 | + self.send_raw("WALLOPS :" + text) |
| 838 | + |
| 839 | + def who(self, target="", op=""): |
| 840 | + """Send a WHO command.""" |
| 841 | + self.send_raw("WHO%s%s" % (target and (" " + target), op and (" o"))) |
| 842 | + |
| 843 | + def whois(self, targets): |
| 844 | + """Send a WHOIS command.""" |
| 845 | + self.send_raw("WHOIS " + string.join(targets, ",")) |
| 846 | + |
| 847 | + def whowas(self, nick, max="", server=""): |
| 848 | + """Send a WHOWAS command.""" |
| 849 | + self.send_raw("WHOWAS %s%s%s" % (nick, |
| 850 | + max and (" " + max), |
| 851 | + server and (" " + server))) |
| 852 | + |
| 853 | + |
| 854 | +class DCCConnectionError(IRCError): |
| 855 | + pass |
| 856 | + |
| 857 | + |
| 858 | +class DCCConnection(Connection): |
| 859 | + """This class represents a DCC connection. |
| 860 | + |
| 861 | + DCCConnection objects are instantiated by calling the dcc |
| 862 | + method on an IRC object. |
| 863 | + """ |
| 864 | + def __init__(self, irclibobj, dcctype): |
| 865 | + Connection.__init__(self, irclibobj) |
| 866 | + self.connected = 0 |
| 867 | + self.passive = 0 |
| 868 | + self.dcctype = dcctype |
| 869 | + self.peeraddress = None |
| 870 | + self.peerport = None |
| 871 | + |
| 872 | + def connect(self, address, port): |
| 873 | + """Connect/reconnect to a DCC peer. |
| 874 | + |
| 875 | + Arguments: |
| 876 | + address -- Host/IP address of the peer. |
| 877 | + |
| 878 | + port -- The port number to connect to. |
| 879 | + |
| 880 | + Returns the DCCConnection object. |
| 881 | + """ |
| 882 | + self.peeraddress = socket.gethostbyname(address) |
| 883 | + self.peerport = port |
| 884 | + self.socket = None |
| 885 | + self.previous_buffer = "" |
| 886 | + self.handlers = {} |
| 887 | + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 888 | + self.passive = 0 |
| 889 | + try: |
| 890 | + self.socket.connect((self.peeraddress, self.peerport)) |
| 891 | + except socket.error, x: |
| 892 | + raise DCCConnectionError, "Couldn't connect to socket: %s" % x |
| 893 | + self.connected = 1 |
| 894 | + if self.irclibobj.fn_to_add_socket: |
| 895 | + self.irclibobj.fn_to_add_socket(self.socket) |
| 896 | + return self |
| 897 | + |
| 898 | + def listen(self): |
| 899 | + """Wait for a connection/reconnection from a DCC peer. |
| 900 | + |
| 901 | + Returns the DCCConnection object. |
| 902 | + |
| 903 | + The local IP address and port are available as |
| 904 | + self.localaddress and self.localport. After connection from a |
| 905 | + peer, the peer address and port are available as |
| 906 | + self.peeraddress and self.peerport. |
| 907 | + """ |
| 908 | + self.previous_buffer = "" |
| 909 | + self.handlers = {} |
| 910 | + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 911 | + self.passive = 1 |
| 912 | + try: |
| 913 | + self.socket.bind((socket.gethostbyname(socket.gethostname()), 0)) |
| 914 | + self.localaddress, self.localport = self.socket.getsockname() |
| 915 | + self.socket.listen(10) |
| 916 | + except socket.error, x: |
| 917 | + raise DCCConnectionError, "Couldn't bind socket: %s" % x |
| 918 | + return self |
| 919 | + |
| 920 | + def disconnect(self, message=""): |
| 921 | + """Hang up the connection and close the object. |
| 922 | + |
| 923 | + Arguments: |
| 924 | + |
| 925 | + message -- Quit message. |
| 926 | + """ |
| 927 | + if not self.connected: |
| 928 | + return |
| 929 | + |
| 930 | + self.connected = 0 |
| 931 | + try: |
| 932 | + self.socket.close() |
| 933 | + except socket.error, x: |
| 934 | + pass |
| 935 | + self.socket = None |
| 936 | + self.irclibobj._handle_event( |
| 937 | + self, |
| 938 | + Event("dcc_disconnect", self.peeraddress, "", [message])) |
| 939 | + self.irclibobj._remove_connection(self) |
| 940 | + |
| 941 | + def process_data(self): |
| 942 | + """[Internal]""" |
| 943 | + |
| 944 | + if self.passive and not self.connected: |
| 945 | + conn, (self.peeraddress, self.peerport) = self.socket.accept() |
| 946 | + self.socket.close() |
| 947 | + self.socket = conn |
| 948 | + self.connected = 1 |
| 949 | + if DEBUG: |
| 950 | + print "DCC connection from %s:%d" % ( |
| 951 | + self.peeraddress, self.peerport) |
| 952 | + self.irclibobj._handle_event( |
| 953 | + self, |
| 954 | + Event("dcc_connect", self.peeraddress, None, None)) |
| 955 | + return |
| 956 | + |
| 957 | + try: |
| 958 | + new_data = self.socket.recv(2**14) |
| 959 | + except socket.error, x: |
| 960 | + # The server hung up. |
| 961 | + self.disconnect("Connection reset by peer") |
| 962 | + return |
| 963 | + if not new_data: |
| 964 | + # Read nothing: connection must be down. |
| 965 | + self.disconnect("Connection reset by peer") |
| 966 | + return |
| 967 | + |
| 968 | + if self.dcctype == "chat": |
| 969 | + # The specification says lines are terminated with LF, but |
| 970 | + # it seems safer to handle CR LF terminations too. |
| 971 | + chunks = _linesep_regexp.split(self.previous_buffer + new_data) |
| 972 | + |
| 973 | + # Save the last, unfinished line. |
| 974 | + self.previous_buffer = chunks[-1] |
| 975 | + if len(self.previous_buffer) > 2**14: |
| 976 | + # Bad peer! Naughty peer! |
| 977 | + self.disconnect() |
| 978 | + return |
| 979 | + chunks = chunks[:-1] |
| 980 | + else: |
| 981 | + chunks = [new_data] |
| 982 | + |
| 983 | + command = "dccmsg" |
| 984 | + prefix = self.peeraddress |
| 985 | + target = None |
| 986 | + for chunk in chunks: |
| 987 | + if DEBUG: |
| 988 | + print "FROM PEER:", chunk |
| 989 | + arguments = [chunk] |
| 990 | + if DEBUG: |
| 991 | + print "command: %s, source: %s, target: %s, arguments: %s" % ( |
| 992 | + command, prefix, target, arguments) |
| 993 | + self.irclibobj._handle_event( |
| 994 | + self, |
| 995 | + Event(command, prefix, target, arguments)) |
| 996 | + |
| 997 | + def _get_socket(self): |
| 998 | + """[Internal]""" |
| 999 | + return self.socket |
| 1000 | + |
| 1001 | + def privmsg(self, string): |
| 1002 | + """Send data to DCC peer. |
| 1003 | + |
| 1004 | + The string will be padded with appropriate LF if it's a DCC |
| 1005 | + CHAT session. |
| 1006 | + """ |
| 1007 | + try: |
| 1008 | + self.socket.send(string) |
| 1009 | + if self.dcctype == "chat": |
| 1010 | + self.socket.send("\n") |
| 1011 | + if DEBUG: |
| 1012 | + print "TO PEER: %s\n" % string |
| 1013 | + except socket.error, x: |
| 1014 | + # Ouch! |
| 1015 | + self.disconnect("Connection reset by peer.") |
| 1016 | + |
| 1017 | +class SimpleIRCClient: |
| 1018 | + """A simple single-server IRC client class. |
| 1019 | + |
| 1020 | + This is an example of an object-oriented wrapper of the IRC |
| 1021 | + framework. A real IRC client can be made by subclassing this |
| 1022 | + class and adding appropriate methods. |
| 1023 | + |
| 1024 | + The method on_join will be called when a "join" event is created |
| 1025 | + (which is done when the server sends a JOIN messsage/command), |
| 1026 | + on_privmsg will be called for "privmsg" events, and so on. The |
| 1027 | + handler methods get two arguments: the connection object (same as |
| 1028 | + self.connection) and the event object. |
| 1029 | + |
| 1030 | + Instance attributes that can be used by sub classes: |
| 1031 | + |
| 1032 | + ircobj -- The IRC instance. |
| 1033 | + |
| 1034 | + connection -- The ServerConnection instance. |
| 1035 | + |
| 1036 | + dcc_connections -- A list of DCCConnection instances. |
| 1037 | + """ |
| 1038 | + def __init__(self): |
| 1039 | + self.ircobj = IRC() |
| 1040 | + self.connection = self.ircobj.server() |
| 1041 | + self.dcc_connections = [] |
| 1042 | + self.ircobj.add_global_handler("all_events", self._dispatcher, -10) |
| 1043 | + self.ircobj.add_global_handler("dcc_disconnect", self._dcc_disconnect, -10) |
| 1044 | + |
| 1045 | + def _dispatcher(self, c, e): |
| 1046 | + """[Internal]""" |
| 1047 | + m = "on_" + e.eventtype() |
| 1048 | + if hasattr(self, m): |
| 1049 | + getattr(self, m)(c, e) |
| 1050 | + |
| 1051 | + def _dcc_disconnect(self, c, e): |
| 1052 | + self.dcc_connections.remove(c) |
| 1053 | + |
| 1054 | + def connect(self, server, port, nickname, password=None, username=None, |
| 1055 | + ircname=None, localaddress="", localport=0): |
| 1056 | + """Connect/reconnect to a server. |
| 1057 | + |
| 1058 | + Arguments: |
| 1059 | + |
| 1060 | + server -- Server name. |
| 1061 | + |
| 1062 | + port -- Port number. |
| 1063 | + |
| 1064 | + nickname -- The nickname. |
| 1065 | + |
| 1066 | + password -- Password (if any). |
| 1067 | + |
| 1068 | + username -- The username. |
| 1069 | + |
| 1070 | + ircname -- The IRC name. |
| 1071 | + |
| 1072 | + localaddress -- Bind the connection to a specific local IP address. |
| 1073 | + |
| 1074 | + localport -- Bind the connection to a specific local port. |
| 1075 | + |
| 1076 | + This function can be called to reconnect a closed connection. |
| 1077 | + """ |
| 1078 | + self.connection.connect(server, port, nickname, |
| 1079 | + password, username, ircname, |
| 1080 | + localaddress, localport) |
| 1081 | + |
| 1082 | + def dcc_connect(self, address, port, dcctype="chat"): |
| 1083 | + """Connect to a DCC peer. |
| 1084 | + |
| 1085 | + Arguments: |
| 1086 | + |
| 1087 | + address -- IP address of the peer. |
| 1088 | + |
| 1089 | + port -- Port to connect to. |
| 1090 | + |
| 1091 | + Returns a DCCConnection instance. |
| 1092 | + """ |
| 1093 | + dcc = self.ircobj.dcc(dcctype) |
| 1094 | + self.dcc_connections.append(dcc) |
| 1095 | + dcc.connect(address, port) |
| 1096 | + return dcc |
| 1097 | + |
| 1098 | + def dcc_listen(self, dcctype="chat"): |
| 1099 | + """Listen for connections from a DCC peer. |
| 1100 | + |
| 1101 | + Returns a DCCConnection instance. |
| 1102 | + """ |
| 1103 | + dcc = self.ircobj.dcc(dcctype) |
| 1104 | + self.dcc_connections.append(dcc) |
| 1105 | + dcc.listen() |
| 1106 | + return dcc |
| 1107 | + |
| 1108 | + def start(self): |
| 1109 | + """Start the IRC client.""" |
| 1110 | + self.ircobj.process_forever() |
| 1111 | + |
| 1112 | + |
| 1113 | +class Event: |
| 1114 | + """Class representing an IRC event.""" |
| 1115 | + def __init__(self, eventtype, source, target, arguments=None): |
| 1116 | + """Constructor of Event objects. |
| 1117 | + |
| 1118 | + Arguments: |
| 1119 | + |
| 1120 | + eventtype -- A string describing the event. |
| 1121 | + |
| 1122 | + source -- The originator of the event (a nick mask or a server). XXX Correct? |
| 1123 | + |
| 1124 | + target -- The target of the event (a nick or a channel). XXX Correct? |
| 1125 | + |
| 1126 | + arguments -- Any event specific arguments. |
| 1127 | + """ |
| 1128 | + self._eventtype = eventtype |
| 1129 | + self._source = source |
| 1130 | + self._target = target |
| 1131 | + if arguments: |
| 1132 | + self._arguments = arguments |
| 1133 | + else: |
| 1134 | + self._arguments = [] |
| 1135 | + |
| 1136 | + def eventtype(self): |
| 1137 | + """Get the event type.""" |
| 1138 | + return self._eventtype |
| 1139 | + |
| 1140 | + def source(self): |
| 1141 | + """Get the event source.""" |
| 1142 | + return self._source |
| 1143 | + |
| 1144 | + def target(self): |
| 1145 | + """Get the event target.""" |
| 1146 | + return self._target |
| 1147 | + |
| 1148 | + def arguments(self): |
| 1149 | + """Get the event arguments.""" |
| 1150 | + return self._arguments |
| 1151 | + |
| 1152 | +_LOW_LEVEL_QUOTE = "\020" |
| 1153 | +_CTCP_LEVEL_QUOTE = "\134" |
| 1154 | +_CTCP_DELIMITER = "\001" |
| 1155 | + |
| 1156 | +_low_level_mapping = { |
| 1157 | + "0": "\000", |
| 1158 | + "n": "\n", |
| 1159 | + "r": "\r", |
| 1160 | + _LOW_LEVEL_QUOTE: _LOW_LEVEL_QUOTE |
| 1161 | +} |
| 1162 | + |
| 1163 | +_low_level_regexp = re.compile(_LOW_LEVEL_QUOTE + "(.)") |
| 1164 | + |
| 1165 | +def mask_matches(nick, mask): |
| 1166 | + """Check if a nick matches a mask. |
| 1167 | + |
| 1168 | + Returns true if the nick matches, otherwise false. |
| 1169 | + """ |
| 1170 | + nick = irc_lower(nick) |
| 1171 | + mask = irc_lower(mask) |
| 1172 | + mask = string.replace(mask, "\\", "\\\\") |
| 1173 | + for ch in ".$|[](){}+": |
| 1174 | + mask = string.replace(mask, ch, "\\" + ch) |
| 1175 | + mask = string.replace(mask, "?", ".") |
| 1176 | + mask = string.replace(mask, "*", ".*") |
| 1177 | + r = re.compile(mask, re.IGNORECASE) |
| 1178 | + return r.match(nick) |
| 1179 | + |
| 1180 | +_alpha = "abcdefghijklmnopqrstuvwxyz" |
| 1181 | +_special = "-[]\\`^{}" |
| 1182 | +nick_characters = _alpha + string.upper(_alpha) + string.digits + _special |
| 1183 | +_ircstring_translation = string.maketrans(string.upper(_alpha) + "[]\\^", |
| 1184 | + _alpha + "{}|~") |
| 1185 | + |
| 1186 | +def irc_lower(s): |
| 1187 | + """Returns a lowercased string. |
| 1188 | + |
| 1189 | + The definition of lowercased comes from the IRC specification (RFC |
| 1190 | + 1459). |
| 1191 | + """ |
| 1192 | + return string.translate(s, _ircstring_translation) |
| 1193 | + |
| 1194 | +def _ctcp_dequote(message): |
| 1195 | + """[Internal] Dequote a message according to CTCP specifications. |
| 1196 | + |
| 1197 | + The function returns a list where each element can be either a |
| 1198 | + string (normal message) or a tuple of one or two strings (tagged |
| 1199 | + messages). If a tuple has only one element (ie is a singleton), |
| 1200 | + that element is the tag; otherwise the tuple has two elements: the |
| 1201 | + tag and the data. |
| 1202 | + |
| 1203 | + Arguments: |
| 1204 | + |
| 1205 | + message -- The message to be decoded. |
| 1206 | + """ |
| 1207 | + |
| 1208 | + def _low_level_replace(match_obj): |
| 1209 | + ch = match_obj.group(1) |
| 1210 | + |
| 1211 | + # If low_level_mapping doesn't have the character as key, we |
| 1212 | + # should just return the character. |
| 1213 | + return _low_level_mapping.get(ch, ch) |
| 1214 | + |
| 1215 | + if _LOW_LEVEL_QUOTE in message: |
| 1216 | + # Yup, there was a quote. Release the dequoter, man! |
| 1217 | + message = _low_level_regexp.sub(_low_level_replace, message) |
| 1218 | + |
| 1219 | + if _CTCP_DELIMITER not in message: |
| 1220 | + return [message] |
| 1221 | + else: |
| 1222 | + # Split it into parts. (Does any IRC client actually *use* |
| 1223 | + # CTCP stacking like this?) |
| 1224 | + chunks = string.split(message, _CTCP_DELIMITER) |
| 1225 | + |
| 1226 | + messages = [] |
| 1227 | + i = 0 |
| 1228 | + while i < len(chunks)-1: |
| 1229 | + # Add message if it's non-empty. |
| 1230 | + if len(chunks[i]) > 0: |
| 1231 | + messages.append(chunks[i]) |
| 1232 | + |
| 1233 | + if i < len(chunks)-2: |
| 1234 | + # Aye! CTCP tagged data ahead! |
| 1235 | + messages.append(tuple(string.split(chunks[i+1], " ", 1))) |
| 1236 | + |
| 1237 | + i = i + 2 |
| 1238 | + |
| 1239 | + if len(chunks) % 2 == 0: |
| 1240 | + # Hey, a lonely _CTCP_DELIMITER at the end! This means |
| 1241 | + # that the last chunk, including the delimiter, is a |
| 1242 | + # normal message! (This is according to the CTCP |
| 1243 | + # specification.) |
| 1244 | + messages.append(_CTCP_DELIMITER + chunks[-1]) |
| 1245 | + |
| 1246 | + return messages |
| 1247 | + |
| 1248 | +def is_channel(string): |
| 1249 | + """Check if a string is a channel name. |
| 1250 | + |
| 1251 | + Returns true if the argument is a channel name, otherwise false. |
| 1252 | + """ |
| 1253 | + return string and string[0] in "#&+!" |
| 1254 | + |
| 1255 | +def ip_numstr_to_quad(num): |
| 1256 | + """Convert an IP number as an integer given in ASCII |
| 1257 | + representation (e.g. '3232235521') to an IP address string |
| 1258 | + (e.g. '192.168.0.1').""" |
| 1259 | + n = long(num) |
| 1260 | + p = map(str, map(int, [n >> 24 & 0xFF, n >> 16 & 0xFF, |
| 1261 | + n >> 8 & 0xFF, n & 0xFF])) |
| 1262 | + return string.join(p, ".") |
| 1263 | + |
| 1264 | +def ip_quad_to_numstr(quad): |
| 1265 | + """Convert an IP address string (e.g. '192.168.0.1') to an IP |
| 1266 | + number as an integer given in ASCII representation |
| 1267 | + (e.g. '3232235521').""" |
| 1268 | + p = map(long, string.split(quad, ".")) |
| 1269 | + s = str((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]) |
| 1270 | + if s[-1] == "L": |
| 1271 | + s = s[:-1] |
| 1272 | + return s |
| 1273 | + |
| 1274 | +def nm_to_n(s): |
| 1275 | + """Get the nick part of a nickmask. |
| 1276 | + |
| 1277 | + (The source of an Event is a nickmask.) |
| 1278 | + """ |
| 1279 | + return string.split(s, "!")[0] |
| 1280 | + |
| 1281 | +def nm_to_uh(s): |
| 1282 | + """Get the userhost part of a nickmask. |
| 1283 | + |
| 1284 | + (The source of an Event is a nickmask.) |
| 1285 | + """ |
| 1286 | + return string.split(s, "!")[1] |
| 1287 | + |
| 1288 | +def nm_to_h(s): |
| 1289 | + """Get the host part of a nickmask. |
| 1290 | + |
| 1291 | + (The source of an Event is a nickmask.) |
| 1292 | + """ |
| 1293 | + return string.split(s, "@")[1] |
| 1294 | + |
| 1295 | +def nm_to_u(s): |
| 1296 | + """Get the user part of a nickmask. |
| 1297 | + |
| 1298 | + (The source of an Event is a nickmask.) |
| 1299 | + """ |
| 1300 | + s = string.split(s, "!")[1] |
| 1301 | + return string.split(s, "@")[0] |
| 1302 | + |
| 1303 | +def parse_nick_modes(mode_string): |
| 1304 | + """Parse a nick mode string. |
| 1305 | + |
| 1306 | + The function returns a list of lists with three members: sign, |
| 1307 | + mode and argument. The sign is \"+\" or \"-\". The argument is |
| 1308 | + always None. |
| 1309 | + |
| 1310 | + Example: |
| 1311 | + |
| 1312 | + >>> irclib.parse_nick_modes(\"+ab-c\") |
| 1313 | + [['+', 'a', None], ['+', 'b', None], ['-', 'c', None]] |
| 1314 | + """ |
| 1315 | + |
| 1316 | + return _parse_modes(mode_string, "") |
| 1317 | + |
| 1318 | +def parse_channel_modes(mode_string): |
| 1319 | + """Parse a channel mode string. |
| 1320 | + |
| 1321 | + The function returns a list of lists with three members: sign, |
| 1322 | + mode and argument. The sign is \"+\" or \"-\". The argument is |
| 1323 | + None if mode isn't one of \"b\", \"k\", \"l\", \"v\" or \"o\". |
| 1324 | + |
| 1325 | + Example: |
| 1326 | + |
| 1327 | + >>> irclib.parse_channel_modes(\"+ab-c foo\") |
| 1328 | + [['+', 'a', None], ['+', 'b', 'foo'], ['-', 'c', None]] |
| 1329 | + """ |
| 1330 | + |
| 1331 | + return _parse_modes(mode_string, "bklvo") |
| 1332 | + |
| 1333 | +def _parse_modes(mode_string, unary_modes=""): |
| 1334 | + """[Internal]""" |
| 1335 | + modes = [] |
| 1336 | + arg_count = 0 |
| 1337 | + |
| 1338 | + # State variable. |
| 1339 | + sign = "" |
| 1340 | + |
| 1341 | + a = string.split(mode_string) |
| 1342 | + if len(a) == 0: |
| 1343 | + return [] |
| 1344 | + else: |
| 1345 | + mode_part, args = a[0], a[1:] |
| 1346 | + |
| 1347 | + if mode_part[0] not in "+-": |
| 1348 | + return [] |
| 1349 | + for ch in mode_part: |
| 1350 | + if ch in "+-": |
| 1351 | + sign = ch |
| 1352 | + elif ch == " ": |
| 1353 | + collecting_arguments = 1 |
| 1354 | + elif ch in unary_modes: |
| 1355 | + if len(args) >= arg_count + 1: |
| 1356 | + modes.append([sign, ch, args[arg_count]]) |
| 1357 | + arg_count = arg_count + 1 |
| 1358 | + else: |
| 1359 | + modes.append([sign, ch, None]) |
| 1360 | + else: |
| 1361 | + modes.append([sign, ch, None]) |
| 1362 | + return modes |
| 1363 | + |
| 1364 | +def _ping_ponger(connection, event): |
| 1365 | + """[Internal]""" |
| 1366 | + connection.pong(event.target()) |
| 1367 | + |
| 1368 | +# Numeric table mostly stolen from the Perl IRC module (Net::IRC). |
| 1369 | +numeric_events = { |
| 1370 | + "001": "welcome", |
| 1371 | + "002": "yourhost", |
| 1372 | + "003": "created", |
| 1373 | + "004": "myinfo", |
| 1374 | + "005": "featurelist", # XXX |
| 1375 | + "200": "tracelink", |
| 1376 | + "201": "traceconnecting", |
| 1377 | + "202": "tracehandshake", |
| 1378 | + "203": "traceunknown", |
| 1379 | + "204": "traceoperator", |
| 1380 | + "205": "traceuser", |
| 1381 | + "206": "traceserver", |
| 1382 | + "207": "traceservice", |
| 1383 | + "208": "tracenewtype", |
| 1384 | + "209": "traceclass", |
| 1385 | + "210": "tracereconnect", |
| 1386 | + "211": "statslinkinfo", |
| 1387 | + "212": "statscommands", |
| 1388 | + "213": "statscline", |
| 1389 | + "214": "statsnline", |
| 1390 | + "215": "statsiline", |
| 1391 | + "216": "statskline", |
| 1392 | + "217": "statsqline", |
| 1393 | + "218": "statsyline", |
| 1394 | + "219": "endofstats", |
| 1395 | + "221": "umodeis", |
| 1396 | + "231": "serviceinfo", |
| 1397 | + "232": "endofservices", |
| 1398 | + "233": "service", |
| 1399 | + "234": "servlist", |
| 1400 | + "235": "servlistend", |
| 1401 | + "241": "statslline", |
| 1402 | + "242": "statsuptime", |
| 1403 | + "243": "statsoline", |
| 1404 | + "244": "statshline", |
| 1405 | + "250": "luserconns", |
| 1406 | + "251": "luserclient", |
| 1407 | + "252": "luserop", |
| 1408 | + "253": "luserunknown", |
| 1409 | + "254": "luserchannels", |
| 1410 | + "255": "luserme", |
| 1411 | + "256": "adminme", |
| 1412 | + "257": "adminloc1", |
| 1413 | + "258": "adminloc2", |
| 1414 | + "259": "adminemail", |
| 1415 | + "261": "tracelog", |
| 1416 | + "262": "endoftrace", |
| 1417 | + "263": "tryagain", |
| 1418 | + "265": "n_local", |
| 1419 | + "266": "n_global", |
| 1420 | + "300": "none", |
| 1421 | + "301": "away", |
| 1422 | + "302": "userhost", |
| 1423 | + "303": "ison", |
| 1424 | + "305": "unaway", |
| 1425 | + "306": "nowaway", |
| 1426 | + "311": "whoisuser", |
| 1427 | + "312": "whoisserver", |
| 1428 | + "313": "whoisoperator", |
| 1429 | + "314": "whowasuser", |
| 1430 | + "315": "endofwho", |
| 1431 | + "316": "whoischanop", |
| 1432 | + "317": "whoisidle", |
| 1433 | + "318": "endofwhois", |
| 1434 | + "319": "whoischannels", |
| 1435 | + "321": "liststart", |
| 1436 | + "322": "list", |
| 1437 | + "323": "listend", |
| 1438 | + "324": "channelmodeis", |
| 1439 | + "329": "channelcreate", |
| 1440 | + "331": "notopic", |
| 1441 | + "332": "topic", |
| 1442 | + "333": "topicinfo", |
| 1443 | + "341": "inviting", |
| 1444 | + "342": "summoning", |
| 1445 | + "346": "invitelist", |
| 1446 | + "347": "endofinvitelist", |
| 1447 | + "348": "exceptlist", |
| 1448 | + "349": "endofexceptlist", |
| 1449 | + "351": "version", |
| 1450 | + "352": "whoreply", |
| 1451 | + "353": "namreply", |
| 1452 | + "361": "killdone", |
| 1453 | + "362": "closing", |
| 1454 | + "363": "closeend", |
| 1455 | + "364": "links", |
| 1456 | + "365": "endoflinks", |
| 1457 | + "366": "endofnames", |
| 1458 | + "367": "banlist", |
| 1459 | + "368": "endofbanlist", |
| 1460 | + "369": "endofwhowas", |
| 1461 | + "371": "info", |
| 1462 | + "372": "motd", |
| 1463 | + "373": "infostart", |
| 1464 | + "374": "endofinfo", |
| 1465 | + "375": "motdstart", |
| 1466 | + "376": "endofmotd", |
| 1467 | + "377": "motd2", # 1997-10-16 -- tkil |
| 1468 | + "381": "youreoper", |
| 1469 | + "382": "rehashing", |
| 1470 | + "384": "myportis", |
| 1471 | + "391": "time", |
| 1472 | + "392": "usersstart", |
| 1473 | + "393": "users", |
| 1474 | + "394": "endofusers", |
| 1475 | + "395": "nousers", |
| 1476 | + "401": "nosuchnick", |
| 1477 | + "402": "nosuchserver", |
| 1478 | + "403": "nosuchchannel", |
| 1479 | + "404": "cannotsendtochan", |
| 1480 | + "405": "toomanychannels", |
| 1481 | + "406": "wasnosuchnick", |
| 1482 | + "407": "toomanytargets", |
| 1483 | + "409": "noorigin", |
| 1484 | + "411": "norecipient", |
| 1485 | + "412": "notexttosend", |
| 1486 | + "413": "notoplevel", |
| 1487 | + "414": "wildtoplevel", |
| 1488 | + "421": "unknowncommand", |
| 1489 | + "422": "nomotd", |
| 1490 | + "423": "noadmininfo", |
| 1491 | + "424": "fileerror", |
| 1492 | + "431": "nonicknamegiven", |
| 1493 | + "432": "erroneusnickname", # Thiss iz how its speld in thee RFC. |
| 1494 | + "433": "nicknameinuse", |
| 1495 | + "436": "nickcollision", |
| 1496 | + "437": "unavailresource", # "Nick temporally unavailable" |
| 1497 | + "441": "usernotinchannel", |
| 1498 | + "442": "notonchannel", |
| 1499 | + "443": "useronchannel", |
| 1500 | + "444": "nologin", |
| 1501 | + "445": "summondisabled", |
| 1502 | + "446": "usersdisabled", |
| 1503 | + "451": "notregistered", |
| 1504 | + "461": "needmoreparams", |
| 1505 | + "462": "alreadyregistered", |
| 1506 | + "463": "nopermforhost", |
| 1507 | + "464": "passwdmismatch", |
| 1508 | + "465": "yourebannedcreep", # I love this one... |
| 1509 | + "466": "youwillbebanned", |
| 1510 | + "467": "keyset", |
| 1511 | + "471": "channelisfull", |
| 1512 | + "472": "unknownmode", |
| 1513 | + "473": "inviteonlychan", |
| 1514 | + "474": "bannedfromchan", |
| 1515 | + "475": "badchannelkey", |
| 1516 | + "476": "badchanmask", |
| 1517 | + "477": "nochanmodes", # "Channel doesn't support modes" |
| 1518 | + "478": "banlistfull", |
| 1519 | + "481": "noprivileges", |
| 1520 | + "482": "chanoprivsneeded", |
| 1521 | + "483": "cantkillserver", |
| 1522 | + "484": "restricted", # Connection is restricted |
| 1523 | + "485": "uniqopprivsneeded", |
| 1524 | + "491": "nooperhost", |
| 1525 | + "492": "noservicehost", |
| 1526 | + "501": "umodeunknownflag", |
| 1527 | + "502": "usersdontmatch", |
| 1528 | +} |
| 1529 | + |
| 1530 | +generated_events = [ |
| 1531 | + # Generated events |
| 1532 | + "dcc_connect", |
| 1533 | + "dcc_disconnect", |
| 1534 | + "dccmsg", |
| 1535 | + "disconnect", |
| 1536 | + "ctcp", |
| 1537 | + "ctcpreply", |
| 1538 | +] |
| 1539 | + |
| 1540 | +protocol_events = [ |
| 1541 | + # IRC protocol events |
| 1542 | + "error", |
| 1543 | + "join", |
| 1544 | + "kick", |
| 1545 | + "mode", |
| 1546 | + "part", |
| 1547 | + "ping", |
| 1548 | + "privmsg", |
| 1549 | + "privnotice", |
| 1550 | + "pubmsg", |
| 1551 | + "pubnotice", |
| 1552 | + "quit", |
| 1553 | +] |
| 1554 | + |
| 1555 | +all_events = generated_events + protocol_events + numeric_events.values() |
Index: trunk/debs/ircecho/ircecho |
— | — | @@ -0,0 +1,83 @@ |
| 2 | +#! /usr/bin/env python |
| 3 | +# |
| 4 | +# stdin -> IRC echo bot, with optional file input support. |
| 5 | +# |
| 6 | +# Written by Kate Turner <kate.turner@gmail.com>, source is in the public domain. |
| 7 | +# Modified by Ryan Lane <rlane@wikimedia.org> for watching and taking input for files. |
| 8 | +# Changes are also public domain. |
| 9 | + |
| 10 | +import sys |
| 11 | +import pyinotify |
| 12 | +from optparse import OptionParser |
| 13 | + |
| 14 | +sys.path.append('/usr/ircecho/lib') |
| 15 | + |
| 16 | +from ircbot import SingleServerIRCBot |
| 17 | +from irclib import nm_to_n |
| 18 | + |
| 19 | +import threading |
| 20 | + |
| 21 | +class EchoReader(threading.Thread): |
| 22 | + def __init__(self, bot, chans, infile=''): |
| 23 | + threading.Thread.__init__(self) |
| 24 | + self.bot = bot |
| 25 | + self.chans = chans |
| 26 | + self.infile = infile |
| 27 | + |
| 28 | + def run(self): |
| 29 | + if self.infile: |
| 30 | + print "Using infile" |
| 31 | + wm = pyinotify.WatchManager() |
| 32 | + mask = pyinotify.IN_MODIFY |
| 33 | + f = open(self.infile) |
| 34 | + # Seek to the end of the file |
| 35 | + f.seek(0,2) |
| 36 | + handler = EventHandler() |
| 37 | + handler.infile = f |
| 38 | + handler.bot = self.bot |
| 39 | + handler.chans = self.chans |
| 40 | + notifier = pyinotify.Notifier(wm, handler) |
| 41 | + wdd = wm.add_watch(self.infile, mask) |
| 42 | + notifier.loop() |
| 43 | + else: |
| 44 | + while True: |
| 45 | + try: |
| 46 | + s = raw_input() |
| 47 | + # this throws an exception if not connected. |
| 48 | + self.bot.connection.privmsg(self.chans, s) |
| 49 | + except EOFError: |
| 50 | + # Once the input is finished, the bot should exit |
| 51 | + break; |
| 52 | + except Exception: |
| 53 | + pass |
| 54 | + |
| 55 | +class EchoBot(SingleServerIRCBot): |
| 56 | + def __init__(self, chans, nickname, server): |
| 57 | + print "*** Connecting to IRC server %s..." % server |
| 58 | + SingleServerIRCBot.__init__(self, [(server, 6667)], nickname, "IRC echo bot") |
| 59 | + self.chans = chans |
| 60 | + |
| 61 | + def on_nicknameinuse(self, c, e): |
| 62 | + c.nick(c.get_nickname() + "_") |
| 63 | + |
| 64 | + def on_welcome(self, c, e): |
| 65 | + print "*** Connected" |
| 66 | + for chan in self.chans: |
| 67 | + c.join(chan) |
| 68 | + |
| 69 | +class EventHandler(pyinotify.ProcessEvent): |
| 70 | + def process_IN_MODIFY(self, event): |
| 71 | + s = self.infile.read() |
| 72 | + self.bot.connection.privmsg(self.chans, s) |
| 73 | + |
| 74 | +parser = OptionParser(conflict_handler="resolve") |
| 75 | +parser.set_usage("ircecho [--infile=<filename>] <channel> <nickname> <server>") |
| 76 | +parser.add_option("--infile", dest="infile", help="Read input from the specific file instead of from stdin") |
| 77 | +(options, args) = parser.parse_args() |
| 78 | +chans = args[0] |
| 79 | +nickname = args[1] |
| 80 | +server = args[2] |
| 81 | +bot = EchoBot([chans], nickname, server) |
| 82 | +sthr = EchoReader(bot, chans, options.infile) |
| 83 | +sthr.start() |
| 84 | +bot.start() |
Property changes on: trunk/debs/ircecho/ircecho |
___________________________________________________________________ |
Added: svn:executable |
1 | 85 | + * |