[Bps-public-commit] smokingit branch, anna, updated. 9c237d295c453d70d7034376560c3e0a63a99bcb

Thomas Sibley trs at bestpractical.com
Fri Jun 14 15:59:17 EDT 2013


The branch, anna has been updated
       via  9c237d295c453d70d7034376560c3e0a63a99bcb (commit)
       via  a01916ee887fcc90bec5af5bebc2a8f29709f74c (commit)
       via  b910a45af1c8f560c141ebb5e86c5e901cc03ebd (commit)
       via  a793ddf127151c98a5bdc7e00419b6ed75d7ddef (commit)
       via  4a7b928a183faeb8130880e6ad694044d71d1ae2 (commit)
       via  63e489054aac8c7466bf6daa37b1671b1e5e029d (commit)
       via  abc0d0752a64ac3d3c9631c0104ace21d59744c6 (commit)
       via  4f30dc24b318300dfb77d5b7ea306aec6dc8e216 (commit)
       via  f06ac63499842276541c008f4a0727166d5a8c36 (commit)
       via  5eab33fb8094a9d3501eb2474f2be19d20308f16 (commit)
      from  a7d78bbc126f0bfd853abf20c6759afd5662c19f (commit)

Summary of changes:
 Makefile.PL                                  |   5 +
 app.psgi                                     |   2 +-
 bin/anna                                     |   5 +
 bin/miniircd                                 | 841 +++++++++++++++++++++++++++
 lib/Smokingit/IRC.pm                         |  59 +-
 lib/Smokingit/Model/Project.pm               |  12 +-
 lib/Smokingit/Model/SmokeResultCollection.pm |  24 +
 7 files changed, 932 insertions(+), 16 deletions(-)
 create mode 100755 bin/miniircd

- Log -----------------------------------------------------------------
commit 5eab33fb8094a9d3501eb2474f2be19d20308f16
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:53:48 2013 -0700

    Add deps we require

diff --git a/Makefile.PL b/Makefile.PL
index 66cb8fd..525b8db 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -4,5 +4,10 @@ name        'Smokingit';
 version     '0.01';
 requires    'Jifty' => '1.01209';
 requires    'Git::PurePerl';
+requires    'IM::Engine';
+requires    'String::IRC';
+
+# requires IM::Engine from git://github.com/sartak/IM-Engine.git
+# requires Web::Hippie from git://github.com/alexmv/Web-Hippie.git
 
 WriteAll;

commit f06ac63499842276541c008f4a0727166d5a8c36
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Thu Jun 13 18:20:27 2013 -0700

    Convenience methods to find queued tests

diff --git a/lib/Smokingit/Model/Project.pm b/lib/Smokingit/Model/Project.pm
index 4a4d114..3a48241 100644
--- a/lib/Smokingit/Model/Project.pm
+++ b/lib/Smokingit/Model/Project.pm
@@ -109,16 +109,8 @@ sub trunk_or_relengs {
 sub planned_tests {
     my $self = shift;
     my $tests = Smokingit::Model::SmokeResultCollection->new;
-    $tests->limit(
-        column => "queue_status",
-        operator => "IS NOT",
-        value => "NULL"
-    );
+    $tests->limit_to_queued;
     $tests->limit( column => "project_id", value => $self->id );
-    $tests->order_by(
-        { column => "queued_at", order  => "asc" },
-        { column => "id",        order  => "asc" },
-    );
     $tests->prefetch( name => "commit" );
     return $tests;
 }
diff --git a/lib/Smokingit/Model/SmokeResultCollection.pm b/lib/Smokingit/Model/SmokeResultCollection.pm
index b65247d..45b9b55 100644
--- a/lib/Smokingit/Model/SmokeResultCollection.pm
+++ b/lib/Smokingit/Model/SmokeResultCollection.pm
@@ -12,4 +12,28 @@ sub implicit_clauses {
     $self->columns(@cols);
 }
 
+sub queued {
+    my $class = shift;
+       $class = ref($class) if ref($class);
+
+    my $queued = $class->new;
+    $queued->limit_to_queued;
+    $queued->prefetch( name => "project" );
+    $queued->prefetch( name => "commit" );
+    return $queued;
+}
+
+sub limit_to_queued {
+    my $self = shift;
+    $self->limit(
+        column => "queue_status",
+        operator => "IS NOT",
+        value => "NULL"
+    );
+    $self->order_by(
+        { column => "queued_at", order  => "asc" },
+        { column => "id",        order  => "asc" },
+    );
+}
+
 1;

commit 4f30dc24b318300dfb77d5b7ea306aec6dc8e216
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:53:58 2013 -0700

    Ensure an IRC host is specified

diff --git a/bin/anna b/bin/anna
index 878bba1..7d05831 100755
--- a/bin/anna
+++ b/bin/anna
@@ -14,4 +14,9 @@ unless (%config) {
     exit;
 }
 
+unless ($config{host}) {
+    print "No host specified for IRC; edit your site_config.yml\n";
+    exit;
+}
+
 Smokingit::IRC->new->run;

commit abc0d0752a64ac3d3c9631c0104ace21d59744c6
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:54:28 2013 -0700

    Finding more than 1 SHA is a problem, not more than 0

diff --git a/lib/Smokingit/IRC.pm b/lib/Smokingit/IRC.pm
index 2d424f0..0cbbde5 100644
--- a/lib/Smokingit/IRC.pm
+++ b/lib/Smokingit/IRC.pm
@@ -115,7 +115,7 @@ sub do_status {
             return error_reply(
                 $incoming => "No such SHA!"
             );
-        } elsif (@matches > 0) {
+        } elsif (@matches > 1) {
             return error_reply(
                 $incoming => "Found ".(@matches+0)." matching SHAs!",
             );

commit 63e489054aac8c7466bf6daa37b1671b1e5e029d
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:55:04 2013 -0700

    Refactor lookups of commitish tokens

diff --git a/lib/Smokingit/IRC.pm b/lib/Smokingit/IRC.pm
index 0cbbde5..9330ec7 100644
--- a/lib/Smokingit/IRC.pm
+++ b/lib/Smokingit/IRC.pm
@@ -105,6 +105,17 @@ sub do_retest {
 }
 
 sub do_status {
+    my $self     = shift;
+    my $incoming = shift;
+    my $what     = $self->lookup_commitish($incoming, @_);
+    if ($what->isa("Smokingit::Model::Commit")) {
+        return $incoming->reply( $what->short_sha . " is " . $what->status );
+    } else {
+        return $what;
+    }
+}
+
+sub lookup_commitish {
     my $self = shift;
     my ($incoming, $what) = @_;
     if ($what =~ /^\s*([a-fA-F0-9]{5,})\s*$/) {
@@ -121,7 +132,7 @@ sub do_status {
             );
         }
 
-        $what = $matches[0];
+        return $matches[0];
     } else {
         my ($project, $branch) = $what =~ /^\s*(?:(\S+):)?(\S+)\s*$/;
         my $branches = Smokingit::Model::BranchCollection->new;
@@ -152,12 +163,11 @@ sub do_status {
         }
 
         # Need to re-parse if this got any updates
-        return $self->do_status($incoming, $what)
+        return $self->lookup_commitish($incoming, $what)
             if $matches[0]->as_superuser->sync;
 
-        $what = $matches[0]->current_commit;
+        return $matches[0]->current_commit;
     }
-    return $incoming->reply( $what->short_sha . " is " . $what->status );
 }
 
 sub do_sync {

commit 4a7b928a183faeb8130880e6ad694044d71d1ae2
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:55:29 2013 -0700

    Fix failure to create master branch
    
    A few lines up we do:
    
        $has_master = delete $branches{master};
    
    so we can't expect to use $branches{master} later.

diff --git a/lib/Smokingit/Model/Project.pm b/lib/Smokingit/Model/Project.pm
index 3a48241..4c38cbd 100644
--- a/lib/Smokingit/Model/Project.pm
+++ b/lib/Smokingit/Model/Project.pm
@@ -202,7 +202,7 @@ sub sync {
         my ($ok, $msg) = $branch->create(
             project_id    => $self->id,
             name          => $name,
-            sha           => $branches{$name},
+            sha           => ($name eq "master" ? $has_master : $branches{$name}),
             status        => $status,
             long_status   => "",
             to_merge_into => undef,

commit a793ddf127151c98a5bdc7e00419b6ed75d7ddef
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:56:44 2013 -0700

    queued [commitish] command for anna

diff --git a/lib/Smokingit/IRC.pm b/lib/Smokingit/IRC.pm
index 9330ec7..bd90155 100644
--- a/lib/Smokingit/IRC.pm
+++ b/lib/Smokingit/IRC.pm
@@ -79,6 +79,8 @@ sub incoming {
         return $self->do_status($incoming, $1);
     } elsif ($msg =~ /^(?:re)?sync(?:\s+(.*))?/) {
         return $self->do_sync($incoming, $1);
+    } elsif ($msg =~ /^queued?(?:\s+(.*))?/) {
+        return $self->do_queued($incoming, $1);
     } else {
         return $incoming->reply( "What?" );
     }
@@ -195,6 +197,43 @@ sub do_sync {
     }
 }
 
+sub do_queued {
+    my $self = shift;
+    my ($incoming, $what) = @_;
+
+    if ($what) {
+        $what = $self->lookup_commitish($incoming, $what);
+        return $what unless $what->isa("Smokingit::Model::Commit");
+    }
+
+    my $queued = Smokingit::Model::SmokeResultCollection->queued;
+    my $count  = $queued->count;
+    my $msg    = "$count test". ($count == 1 ? "" : "s") ." queued";
+
+    if ($what) {
+        my ($before, $found) = (0, undef);
+        while (my $test = $queued->next) {
+            $found = 1, last if $test->commit->sha eq $what->sha;
+            $before++;
+        }
+        my $short = $what->short_sha;
+        if ($found) {
+            if ($before == 0) {
+                $msg .= "; $short first in line";
+            } elsif ($before == 1) {
+                $msg .= "; $short up next";
+            } else {
+                $msg .= "; $before test".($before == 1 ? "" : "s")
+                      . " before $short";
+            }
+        } else {
+            $msg .= "; $short not queued!";
+        }
+    }
+
+    return $incoming->reply($msg);
+}
+
 sub test_progress {
     my $self = shift;
     my $msg = shift;

commit b910a45af1c8f560c141ebb5e86c5e901cc03ebd
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:57:44 2013 -0700

    bin/start-server uses :8888

diff --git a/app.psgi b/app.psgi
index 225e9e4..68e4f65 100644
--- a/app.psgi
+++ b/app.psgi
@@ -7,7 +7,7 @@ builder {
        origins => [
            "http://smokingit.bestpractical.com",
            "https://tickets.bestpractical.com",
-           "http://localhost:8008"
+           "http://localhost:8888"
        ],
        methods => ["GET"];
 

commit a01916ee887fcc90bec5af5bebc2a8f29709f74c
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:58:13 2013 -0700

    Bundle a mini ircd for local testing

diff --git a/bin/miniircd b/bin/miniircd
new file mode 100755
index 0000000..561e1a2
--- /dev/null
+++ b/bin/miniircd
@@ -0,0 +1,841 @@
+#! /usr/bin/env python
+# Hey, Emacs! This is -*-python-*-.
+#
+# Copyright (C) 2003, 2011-2013 Joel Rosdahl
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Joel Rosdahl <joel at rosdahl.net>
+
+VERSION = "0.4"
+
+import os
+import re
+import select
+import socket
+import string
+import sys
+import tempfile
+import time
+from datetime import datetime
+from optparse import OptionParser
+
+
+def create_directory(path):
+    if not os.path.isdir(path):
+        os.makedirs(path)
+
+
+class Channel(object):
+    def __init__(self, server, name):
+        self.server = server
+        self.name = name
+        self.members = set()
+        self._topic = ""
+        self._key = None
+        if self.server.statedir:
+            self._state_path = "%s/%s" % (
+                self.server.statedir,
+                name.replace("_", "__").replace("/", "_"))
+            self._read_state()
+        else:
+            self._state_path = None
+
+    def add_member(self, client):
+        self.members.add(client)
+
+    def get_topic(self):
+        return self._topic
+
+    def set_topic(self, value):
+        self._topic = value
+        self._write_state()
+
+    topic = property(get_topic, set_topic)
+
+    def get_key(self):
+        return self._key
+
+    def set_key(self, value):
+        self._key = value
+        self._write_state()
+
+    key = property(get_key, set_key)
+
+    def remove_client(self, client):
+        self.members.discard(client)
+        if not self.members:
+            self.server.remove_channel(self)
+
+    def _read_state(self):
+        if not (self._state_path and os.path.exists(self._state_path)):
+            return
+        data = {}
+        exec(open(self._state_path), {}, data)
+        self._topic = data.get("topic", "")
+        self._key = data.get("key")
+
+    def _write_state(self):
+        if not self._state_path:
+            return
+        (fd, path) = tempfile.mkstemp(dir=os.path.dirname(self._state_path))
+        fp = os.fdopen(fd, "w")
+        fp.write("topic = %r\n" % self.topic)
+        fp.write("key = %r\n" % self.key)
+        fp.close()
+        os.rename(path, self._state_path)
+
+
+class Client(object):
+    __linesep_regexp = re.compile(r"\r?\n")
+    # The RFC limit for nicknames is 9 characters, but what the heck.
+    __valid_nickname_regexp = re.compile(
+        r"^[][\`_^{|}A-Za-z][][\`_^{|}A-Za-z0-9]{0,50}$")
+    __valid_channelname_regexp = re.compile(
+        r"^[&#+!][^\x00\x07\x0a\x0d ,:]{0,50}$")
+
+    def __init__(self, server, socket):
+        self.server = server
+        self.socket = socket
+        self.channels = {}  # irc_lower(Channel name) --> Channel
+        self.nickname = None
+        self.user = None
+        self.realname = None
+        (self.host, self.port) = socket.getpeername()
+        self.__timestamp = time.time()
+        self.__readbuffer = ""
+        self.__writebuffer = ""
+        self.__sent_ping = False
+        if self.server.password:
+            self.__handle_command = self.__pass_handler
+        else:
+            self.__handle_command = self.__registration_handler
+
+    def get_prefix(self):
+        return "%s!%s@%s" % (self.nickname, self.user, self.host)
+    prefix = property(get_prefix)
+
+    def check_aliveness(self):
+        now = time.time()
+        if self.__timestamp + 180 < now:
+            self.disconnect("ping timeout")
+            return
+        if not self.__sent_ping and self.__timestamp + 90 < now:
+            if self.__handle_command == self.__command_handler:
+                # Registered.
+                self.message("PING :%s" % self.server.name)
+                self.__sent_ping = True
+            else:
+                # Not registered.
+                self.disconnect("ping timeout")
+
+    def write_queue_size(self):
+        return len(self.__writebuffer)
+
+    def __parse_read_buffer(self):
+        lines = self.__linesep_regexp.split(self.__readbuffer)
+        self.__readbuffer = lines[-1]
+        lines = lines[:-1]
+        for line in lines:
+            if not line:
+                # Empty line. Ignore.
+                continue
+            x = line.split(" ", 1)
+            command = x[0].upper()
+            if len(x) == 1:
+                arguments = []
+            else:
+                if len(x[1]) > 0 and x[1][0] == ":":
+                    arguments = [x[1][1:]]
+                else:
+                    y = string.split(x[1], " :", 1)
+                    arguments = string.split(y[0])
+                    if len(y) == 2:
+                        arguments.append(y[1])
+            self.__handle_command(command, arguments)
+
+    def __pass_handler(self, command, arguments):
+        server = self.server
+        if command == "PASS":
+            if len(arguments) == 0:
+                self.reply_461("PASS")
+            else:
+                if arguments[0].lower() == server.password:
+                    self.__handle_command = self.__registration_handler
+                else:
+                    self.reply("464 :Password incorrect")
+        elif command == "QUIT":
+            self.disconnect("Client quit")
+            return
+
+    def __registration_handler(self, command, arguments):
+        server = self.server
+        if command == "NICK":
+            if len(arguments) < 1:
+                self.reply("431 :No nickname given")
+                return
+            nick = arguments[0]
+            if server.get_client(nick):
+                self.reply("433 * %s :Nickname is already in use" % nick)
+            elif not self.__valid_nickname_regexp.match(nick):
+                self.reply("432 * %s :Erroneous nickname" % nick)
+            else:
+                self.nickname = nick
+                server.client_changed_nickname(self, None)
+        elif command == "USER":
+            if len(arguments) < 4:
+                self.reply_461("USER")
+                return
+            self.user = arguments[0]
+            self.realname = arguments[3]
+        elif command == "QUIT":
+            self.disconnect("Client quit")
+            return
+        if self.nickname and self.user:
+            self.reply("001 %s :Hi, welcome to IRC" % self.nickname)
+            self.reply("002 %s :Your host is %s, running version miniircd-%s"
+                       % (self.nickname, server.name, VERSION))
+            self.reply("003 %s :This server was created sometime"
+                       % self.nickname)
+            self.reply("004 %s :%s miniircd-%s o o"
+                       % (self.nickname, server.name, VERSION))
+            self.send_lusers()
+            self.send_motd()
+            self.__handle_command = self.__command_handler
+
+    def __command_handler(self, command, arguments):
+        def away_handler():
+            pass
+
+        def ison_handler():
+            if len(arguments) < 1:
+                self.reply_461("ISON")
+                return
+            nicks = arguments
+            online = [n for n in nicks if server.get_client(n)]
+            self.reply("303 %s :%s" % (self.nickname, " ".join(online)))
+
+        def join_handler():
+            if len(arguments) < 1:
+                self.reply_461("JOIN")
+                return
+            if arguments[0] == "0":
+                for (channelname, channel) in self.channels.items():
+                    self.message_channel(channel, "PART", channelname, True)
+                    self.channel_log(channel, "left", meta=True)
+                    server.remove_member_from_channel(self, channelname)
+                self.channels = {}
+                return
+            channelnames = arguments[0].split(",")
+            if len(arguments) > 1:
+                keys = arguments[1].split(",")
+            else:
+                keys = []
+            keys.extend((len(channelnames) - len(keys)) * [None])
+            for (i, channelname) in enumerate(channelnames):
+                if irc_lower(channelname) in self.channels:
+                    continue
+                if not valid_channel_re.match(channelname):
+                    self.reply_403(channelname)
+                    continue
+                channel = server.get_channel(channelname)
+                if channel.key is not None and channel.key != keys[i]:
+                    self.reply(
+                        "475 %s %s :Cannot join channel (+k) - bad key"
+                        % (self.nickname, channelname))
+                    continue
+                channel.add_member(self)
+                self.channels[irc_lower(channelname)] = channel
+                self.message_channel(channel, "JOIN", channelname, True)
+                self.channel_log(channel, "joined", meta=True)
+                if channel.topic:
+                    self.reply("332 %s %s :%s"
+                               % (self.nickname, channel.name, channel.topic))
+                else:
+                    self.reply("331 %s %s :No topic is set"
+                               % (self.nickname, channel.name))
+                self.reply("353 %s = %s :%s"
+                           % (self.nickname,
+                              channelname,
+                              " ".join(sorted(x.nickname
+                                              for x in channel.members))))
+                self.reply("366 %s %s :End of NAMES list"
+                           % (self.nickname, channelname))
+
+        def list_handler():
+            if len(arguments) < 1:
+                channels = server.channels.values()
+            else:
+                channels = []
+                for channelname in arguments[0].split(","):
+                    if server.has_channel(channelname):
+                        channels.append(server.get_channel(channelname))
+            channels.sort(key=lambda x: x.name)
+            for channel in channels:
+                self.reply("322 %s %s %d :%s"
+                           % (self.nickname, channel.name,
+                              len(channel.members), channel.topic))
+            self.reply("323 %s :End of LIST" % self.nickname)
+
+        def lusers_handler():
+            self.send_lusers()
+
+        def mode_handler():
+            if len(arguments) < 1:
+                self.reply_461("MODE")
+                return
+            targetname = arguments[0]
+            if server.has_channel(targetname):
+                channel = server.get_channel(targetname)
+                if len(arguments) < 2:
+                    if channel.key:
+                        modes = "+k"
+                        if irc_lower(channel.name) in self.channels:
+                            modes += " %s" % channel.key
+                    else:
+                        modes = "+"
+                    self.reply("324 %s %s %s"
+                               % (self.nickname, targetname, modes))
+                    return
+                flag = arguments[1]
+                if flag == "+k":
+                    if len(arguments) < 3:
+                        self.reply_461("MODE")
+                        return
+                    key = arguments[2]
+                    if irc_lower(channel.name) in self.channels:
+                        channel.key = key
+                        self.message_channel(
+                            channel, "MODE", "%s +k %s" % (channel.name, key),
+                            True)
+                        self.channel_log(
+                            channel, "set channel key to %s" % key, meta=True)
+                    else:
+                        self.reply("442 %s :You're not on that channel"
+                                   % targetname)
+                elif flag == "-k":
+                    if irc_lower(channel.name) in self.channels:
+                        channel.key = None
+                        self.message_channel(
+                            channel, "MODE", "%s -k" % channel.name,
+                            True)
+                        self.channel_log(
+                            channel, "removed channel key", meta=True)
+                    else:
+                        self.reply("442 %s :You're not on that channel"
+                                   % targetname)
+                else:
+                    self.reply("472 %s %s :Unknown MODE flag"
+                               % (self.nickname, flag))
+            elif targetname == self.nickname:
+                if len(arguments) == 1:
+                    self.reply("221 %s +" % self.nickname)
+                else:
+                    self.reply("501 %s :Unknown MODE flag" % self.nickname)
+            else:
+                self.reply_403(targetname)
+
+        def motd_handler():
+            self.send_motd()
+
+        def nick_handler():
+            if len(arguments) < 1:
+                self.reply("431 :No nickname given")
+                return
+            newnick = arguments[0]
+            client = server.get_client(newnick)
+            if newnick == self.nickname:
+                pass
+            elif client and client is not self:
+                self.reply("433 %s %s :Nickname is already in use"
+                           % (self.nickname, newnick))
+            elif not self.__valid_nickname_regexp.match(newnick):
+                self.reply("432 %s %s :Erroneous Nickname"
+                           % (self.nickname, newnick))
+            else:
+                for x in self.channels.values():
+                    self.channel_log(
+                        x, "changed nickname to %s" % newnick, meta=True)
+                oldnickname = self.nickname
+                self.nickname = newnick
+                server.client_changed_nickname(self, oldnickname)
+                self.message_related(
+                    ":%s!%s@%s NICK %s"
+                    % (oldnickname, self.user, self.host, self.nickname),
+                    True)
+
+        def notice_and_privmsg_handler():
+            if len(arguments) == 0:
+                self.reply("411 %s :No recipient given (%s)"
+                           % (self.nickname, command))
+                return
+            if len(arguments) == 1:
+                self.reply("412 %s :No text to send" % self.nickname)
+                return
+            targetname = arguments[0]
+            message = arguments[1]
+            client = server.get_client(targetname)
+            if client:
+                client.message(":%s %s %s :%s"
+                               % (self.prefix, command, targetname, message))
+            elif server.has_channel(targetname):
+                channel = server.get_channel(targetname)
+                self.message_channel(
+                    channel, command, "%s :%s" % (channel.name, message))
+                self.channel_log(channel, message)
+            else:
+                self.reply("401 %s %s :No such nick/channel"
+                           % (self.nickname, targetname))
+
+        def part_handler():
+            if len(arguments) < 1:
+                self.reply_461("PART")
+                return
+            if len(arguments) > 1:
+                partmsg = arguments[1]
+            else:
+                partmsg = self.nickname
+            for channelname in arguments[0].split(","):
+                if not valid_channel_re.match(channelname):
+                    self.reply_403(channelname)
+                elif not irc_lower(channelname) in self.channels:
+                    self.reply("442 %s %s :You're not on that channel"
+                               % (self.nickname, channelname))
+                else:
+                    channel = self.channels[irc_lower(channelname)]
+                    self.message_channel(
+                        channel, "PART", "%s :%s" % (channelname, partmsg),
+                        True)
+                    self.channel_log(channel, "left (%s)" % partmsg, meta=True)
+                    del self.channels[irc_lower(channelname)]
+                    server.remove_member_from_channel(self, channelname)
+
+        def ping_handler():
+            if len(arguments) < 1:
+                self.reply("409 %s :No origin specified" % self.nickname)
+                return
+            self.reply("PONG %s :%s" % (server.name, arguments[0]))
+
+        def pong_handler():
+            pass
+
+        def quit_handler():
+            if len(arguments) < 1:
+                quitmsg = self.nickname
+            else:
+                quitmsg = arguments[0]
+            self.disconnect(quitmsg)
+
+        def topic_handler():
+            if len(arguments) < 1:
+                self.reply_461("TOPIC")
+                return
+            channelname = arguments[0]
+            channel = self.channels.get(irc_lower(channelname))
+            if channel:
+                if len(arguments) > 1:
+                    newtopic = arguments[1]
+                    channel.topic = newtopic
+                    self.message_channel(
+                        channel, "TOPIC", "%s :%s" % (channelname, newtopic),
+                        True)
+                    self.channel_log(
+                        channel, "set topic to %r" % newtopic, meta=True)
+                else:
+                    if channel.topic:
+                        self.reply("332 %s %s :%s"
+                                   % (self.nickname, channel.name,
+                                      channel.topic))
+                    else:
+                        self.reply("331 %s %s :No topic is set"
+                                   % (self.nickname, channel.name))
+            else:
+                self.reply("442 %s :You're not on that channel" % channelname)
+
+        def wallops_handler():
+            if len(arguments) < 1:
+                self.reply_461(command)
+            message = arguments[0]
+            for client in server.clients.values():
+                client.message(":%s NOTICE %s :Global notice: %s"
+                               % (self.prefix, client.nickname, message))
+
+        def who_handler():
+            if len(arguments) < 1:
+                return
+            targetname = arguments[0]
+            if server.has_channel(targetname):
+                channel = server.get_channel(targetname)
+                for member in channel.members:
+                    self.reply("352 %s %s %s %s %s %s H :0 %s"
+                               % (self.nickname, targetname, member.user,
+                                  member.host, server.name, member.nickname,
+                                  member.realname))
+                self.reply("315 %s %s :End of WHO list"
+                           % (self.nickname, targetname))
+
+        def whois_handler():
+            if len(arguments) < 1:
+                return
+            username = arguments[0]
+            user = server.get_client(username)
+            if user:
+                self.reply("311 %s %s %s %s * :%s"
+                           % (self.nickname, user.nickname, user.user,
+                              user.host, user.realname))
+                self.reply("312 %s %s %s :%s"
+                           % (self.nickname, user.nickname, server.name,
+                              server.name))
+                self.reply("319 %s %s :%s"
+                           % (self.nickname, user.nickname,
+                              " ".join(user.channels)))
+                self.reply("318 %s %s :End of WHOIS list"
+                           % (self.nickname, user.nickname))
+            else:
+                self.reply("401 %s %s :No such nick"
+                           % (self.nickname, username))
+
+        handler_table = {
+            "AWAY": away_handler,
+            "ISON": ison_handler,
+            "JOIN": join_handler,
+            "LIST": list_handler,
+            "LUSERS": lusers_handler,
+            "MODE": mode_handler,
+            "MOTD": motd_handler,
+            "NICK": nick_handler,
+            "NOTICE": notice_and_privmsg_handler,
+            "PART": part_handler,
+            "PING": ping_handler,
+            "PONG": pong_handler,
+            "PRIVMSG": notice_and_privmsg_handler,
+            "QUIT": quit_handler,
+            "TOPIC": topic_handler,
+            "WALLOPS": wallops_handler,
+            "WHO": who_handler,
+            "WHOIS": whois_handler,
+        }
+        server = self.server
+        valid_channel_re = self.__valid_channelname_regexp
+        try:
+            handler_table[command]()
+        except KeyError:
+            self.reply("421 %s %s :Unknown command" % (self.nickname, command))
+
+    def socket_readable_notification(self):
+        try:
+            data = self.socket.recv(2 ** 10)
+            self.server.print_debug(
+                "[%s:%d] -> %r" % (self.host, self.port, data))
+            quitmsg = "EOT"
+        except socket.error, x:
+            data = ""
+            quitmsg = x
+        if data:
+            self.__readbuffer += data
+            self.__parse_read_buffer()
+            self.__timestamp = time.time()
+            self.__sent_ping = False
+        else:
+            self.disconnect(quitmsg)
+
+    def socket_writable_notification(self):
+        try:
+            sent = self.socket.send(self.__writebuffer)
+            self.server.print_debug(
+                "[%s:%d] <- %r" % (
+                    self.host, self.port, self.__writebuffer[:sent]))
+            self.__writebuffer = self.__writebuffer[sent:]
+        except socket.error, x:
+            self.disconnect(x)
+
+    def disconnect(self, quitmsg):
+        self.message("ERROR :%s" % quitmsg)
+        self.server.print_info(
+            "Disconnected connection from %s:%s (%s)." % (
+                self.host, self.port, quitmsg))
+        self.socket.close()
+        self.server.remove_client(self, quitmsg)
+
+    def message(self, msg):
+        self.__writebuffer += msg + "\r\n"
+
+    def reply(self, msg):
+        self.message(":%s %s" % (self.server.name, msg))
+
+    def reply_403(self, channel):
+        self.reply("403 %s %s :No such channel" % (self.nickname, channel))
+
+    def reply_461(self, command):
+        nickname = self.nickname or "*"
+        self.reply("461 %s %s :Not enough parameters" % (nickname, command))
+
+    def message_channel(self, channel, command, message, include_self=False):
+        line = ":%s %s %s" % (self.prefix, command, message)
+        for client in channel.members:
+            if client != self or include_self:
+                client.message(line)
+
+    def channel_log(self, channel, message, meta=False):
+        if not self.server.logdir:
+            return
+        if meta:
+            format = "[%s] * %s %s\n"
+        else:
+            format = "[%s] <%s> %s\n"
+        timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
+        logname = channel.name.replace("_", "__").replace("/", "_")
+        fp = open("%s/%s.log" % (self.server.logdir, logname), "a")
+        fp.write(format % (timestamp, self.nickname, message))
+        fp.close()
+
+    def message_related(self, msg, include_self=False):
+        clients = set()
+        if include_self:
+            clients.add(self)
+        for channel in self.channels.values():
+            clients |= channel.members
+        if not include_self:
+            clients.discard(self)
+        for client in clients:
+            client.message(msg)
+
+    def send_lusers(self):
+        self.reply("251 %s :There are %d users and 0 services on 1 server"
+                   % (self.nickname, len(self.server.clients)))
+
+    def send_motd(self):
+        server = self.server
+        motdlines = server.get_motd_lines()
+        if motdlines:
+            self.reply("375 %s :- %s Message of the day -"
+                       % (self.nickname, server.name))
+            for line in motdlines:
+                self.reply("372 %s :- %s" % (self.nickname, line.rstrip()))
+            self.reply("376 %s :End of /MOTD command" % self.nickname)
+        else:
+            self.reply("422 %s :MOTD File is missing" % self.nickname)
+
+
+class Server(object):
+    def __init__(self, options):
+        self.ports = options.ports
+        self.password = options.password
+        self.motdfile = options.motd
+        self.verbose = options.verbose
+        self.debug = options.debug
+        self.logdir = options.logdir
+        self.statedir = options.statedir
+        self.name = socket.getfqdn()[:63]  # Server name limit from the RFC.
+        self.channels = {}  # irc_lower(Channel name) --> Channel instance.
+        self.clients = {}  # Socket --> Client instance.
+        self.nicknames = {}  # irc_lower(Nickname) --> Client instance.
+        if self.logdir:
+            create_directory(self.logdir)
+        if self.statedir:
+            create_directory(self.statedir)
+
+    def daemonize(self):
+        try:
+            pid = os.fork()
+            if pid > 0:
+                sys.exit(0)
+        except OSError:
+            sys.exit(1)
+        os.setsid()
+        try:
+            pid = os.fork()
+            if pid > 0:
+                self.print_info("PID: %d" % pid)
+                sys.exit(0)
+        except OSError:
+            sys.exit(1)
+        os.chdir("/")
+        os.umask(0)
+        dev_null = open("/dev/null", "r+")
+        os.dup2(dev_null.fileno(), sys.stdout.fileno())
+        os.dup2(dev_null.fileno(), sys.stderr.fileno())
+        os.dup2(dev_null.fileno(), sys.stdin.fileno())
+
+    def get_client(self, nickname):
+        return self.nicknames.get(irc_lower(nickname))
+
+    def has_channel(self, name):
+        return irc_lower(name) in self.channels
+
+    def get_channel(self, channelname):
+        if irc_lower(channelname) in self.channels:
+            channel = self.channels[irc_lower(channelname)]
+        else:
+            channel = Channel(self, channelname)
+            self.channels[irc_lower(channelname)] = channel
+        return channel
+
+    def get_motd_lines(self):
+        if self.motdfile:
+            try:
+                return open(self.motdfile).readlines()
+            except IOError:
+                return ["Could not read MOTD file %r." % self.motdfile]
+        else:
+            return []
+
+    def print_info(self, msg):
+        if self.verbose:
+            print msg
+            sys.stdout.flush()
+
+    def print_debug(self, msg):
+        if self.debug:
+            print msg
+            sys.stdout.flush()
+
+    def print_error(self, msg):
+        sys.stderr.write("%s\n" % msg)
+
+    def client_changed_nickname(self, client, oldnickname):
+        if oldnickname:
+            del self.nicknames[irc_lower(oldnickname)]
+        self.nicknames[irc_lower(client.nickname)] = client
+
+    def remove_member_from_channel(self, client, channelname):
+        if irc_lower(channelname) in self.channels:
+            channel = self.channels[irc_lower(channelname)]
+            channel.remove_client(client)
+
+    def remove_client(self, client, quitmsg):
+        client.message_related(":%s QUIT :%s" % (client.prefix, quitmsg))
+        for x in client.channels.values():
+            client.channel_log(x, "quit (%s)" % quitmsg, meta=True)
+            x.remove_client(client)
+        if client.nickname \
+               and irc_lower(client.nickname) in self.nicknames:
+            del self.nicknames[irc_lower(client.nickname)]
+        del self.clients[client.socket]
+
+    def remove_channel(self, channel):
+        del self.channels[irc_lower(channel.name)]
+
+    def start(self):
+        serversockets = []
+        for port in self.ports:
+            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+            try:
+                s.bind(("", port))
+            except socket.error, x:
+                self.print_error("Could not bind port %s: %s." % (port, x))
+                sys.exit(1)
+            s.listen(5)
+            serversockets.append(s)
+            del s
+            self.print_info("Listening on port %d." % port)
+        last_aliveness_check = time.time()
+        while True:
+            (iwtd, owtd, ewtd) = select.select(
+                serversockets + [x.socket for x in self.clients.values()],
+                [x.socket for x in self.clients.values()
+                          if x.write_queue_size() > 0],
+                [],
+                10)
+            for x in iwtd:
+                if x in self.clients:
+                    self.clients[x].socket_readable_notification()
+                else:
+                    (conn, addr) = x.accept()
+                    self.clients[conn] = Client(self, conn)
+                    self.print_info("Accepted connection from %s:%s." % (
+                        addr[0], addr[1]))
+            for x in owtd:
+                if x in self.clients: # client may have been disconnected
+                    self.clients[x].socket_writable_notification()
+            now = time.time()
+            if last_aliveness_check + 10 < now:
+                for client in self.clients.values():
+                    client.check_aliveness()
+                last_aliveness_check = now
+
+
+_alpha = "abcdefghijklmnopqrstuvwxyz"
+_ircstring_translation = string.maketrans(
+    string.upper(_alpha) + "[]\\^",
+    _alpha + "{}|~")
+
+
+def irc_lower(s):
+    return string.translate(s, _ircstring_translation)
+
+
+def main(argv):
+    op = OptionParser(
+        version=VERSION,
+        description="miniircd is a small and limited IRC server.")
+    op.add_option(
+        "-d", "--daemon",
+        action="store_true",
+        help="fork and become a daemon")
+    op.add_option(
+        "--debug",
+        action="store_true",
+        help="print debug messages to stdout")
+    op.add_option(
+        "--logdir",
+        metavar="X",
+        help="store channel log in directory X")
+    op.add_option(
+        "--motd",
+        metavar="X",
+        help="display file X as message of the day")
+    op.add_option(
+        "-p", "--password",
+        metavar="X",
+        help="require connection password X; default: no password")
+    op.add_option(
+        "--ports",
+        metavar="X",
+        help="listen to ports X (a list separated by comma or whitespace);"
+             " default: 6667")
+    op.add_option(
+        "--statedir",
+        metavar="X",
+        help="save persistent channel state (topic, key) in directory X")
+    op.add_option(
+        "--verbose",
+        action="store_true",
+        help="be verbose (print some progress messages to stdout)")
+    op.set_defaults(ports="6667")
+    (options, args) = op.parse_args(argv[1:])
+    if options.debug:
+        options.verbose = True
+    ports = []
+    for port in re.split(r"[,\s]+", options.ports):
+        try:
+            ports.append(int(port))
+        except ValueError:
+            op.error("bad port: %r" % port)
+    options.ports = ports
+    server = Server(options)
+    if options.daemon:
+        server.daemonize()
+    try:
+        server.start()
+    except KeyboardInterrupt:
+        server.print_error("Interrupted.")
+
+
+main(sys.argv)
+
+# ex:et:sw=4:ts=4

commit 9c237d295c453d70d7034376560c3e0a63a99bcb
Merge: a7d78bb a01916e
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Jun 14 12:59:01 2013 -0700

    Merge branch 'anna-queued' into anna


-----------------------------------------------------------------------



More information about the Bps-public-commit mailing list