source: perl/modules/IRC/lib/BarnOwl/Module/IRC.pm @ ae2b830

release-1.10
Last change on this file since ae2b830 was c84c365, checked in by Jason Gross <jgross@mit.edu>, 11 years ago
remove a space
  • Property mode set to 100644
File size: 18.2 KB
RevLine 
[b38b0b2]1use strict;
2use warnings;
3
4package BarnOwl::Module::IRC;
5
6=head1 NAME
7
[2c40dc0]8BarnOwl::Module::IRC
[b38b0b2]9
10=head1 DESCRIPTION
11
[b8a3e00]12This module implements IRC support for BarnOwl.
[b38b0b2]13
14=cut
15
16use BarnOwl;
17use BarnOwl::Hooks;
18use BarnOwl::Message::IRC;
[380b1ab]19use BarnOwl::Module::IRC::Connection qw(is_private);
[ab9cd8f]20use BarnOwl::Module::IRC::Completion;
[b38b0b2]21
[8ba9313]22use AnyEvent::IRC;
[b38b0b2]23use Getopt::Long;
[9620c8d]24use Encode;
[c2866ec]25use Text::Wrap;
[b38b0b2]26
[2c40dc0]27our $VERSION = 0.02;
[b38b0b2]28
29our $irc;
30
31# Hash alias -> BarnOwl::Module::IRC::Connection object
32our %ircnets;
33
34sub startup {
[b10f340]35    BarnOwl::new_variable_string('irc:nick', {
36        default     => $ENV{USER},
37        summary     => 'The default IRC nickname',
38        description => 'By default, irc-connect will use this nick '  .
39        'when connecting to a new server. See :help irc-connect for ' .
40        'more information.'
41       });
42
43    BarnOwl::new_variable_string('irc:user', {
44        default => $ENV{USER},
45        summary => 'The IRC "username" field'
46       });
47        BarnOwl::new_variable_string('irc:name', {
48        default => "",
49        summary     => 'A short name field for IRC',
50        description => 'A short (maybe 60 or so chars) piece of text, ' .
51        'originally intended to display your real name, which people '  .
52        'often use for pithy quotes and URLs.'
53       });
[cd12307]54
[b10f340]55    BarnOwl::new_variable_bool('irc:spew', {
56        default     => 0,
57        summary     => 'Show unhandled IRC events',
58        description => 'If set, display all unrecognized IRC events as ' .
[cd12307]59        'admin messages. Intended for debugging and development use only.'
[b10f340]60       });
[f81176c]61
62    BarnOwl::new_variable_string('irc:skip', {
63        default     => 'welcome yourhost created ' .
64        'luserclient luserme luserop luserchannels',
65        summary     => 'Skip messages of these types',
66        description => 'If set, each (space-separated) message type ' .
67        'provided will be hidden and ignored if received.'
68       });
69
[c2866ec]70    BarnOwl::new_variable_int('irc:max-message-length', {
71        default     => 450,
72        summary     => 'Split messages to at most this many characters.' .
73                       "If non-positive, don't split messages",
74        description => 'If set to a positive number, any paragraph in an ' .
75                       'IRC message will be split after this many characters.'
76       });
77
[b38b0b2]78    register_commands();
[96f7b07]79    BarnOwl::filter(qw{irc type ^IRC$ or ( type ^admin$ and adminheader ^IRC$ )});
[b38b0b2]80}
81
82sub shutdown {
83    for my $conn (values %ircnets) {
[8ba9313]84        $conn->conn->disconnect('Quitting');
[b38b0b2]85    }
86}
87
[f17bb2c0]88sub quickstart {
89    return <<'END_QUICKSTART';
90@b[IRC:]
91Use ':irc-connect @b[server]' to connect to an IRC server, and
92':irc-join @b[#channel]' to join a channel. ':irc-msg @b[#channel]
93@b[message]' sends a message to a channel.
94END_QUICKSTART
95}
96
[da554da]97sub buddylist {
98    my $list = "";
99
100    for my $net (sort keys %ircnets) {
101        my $conn = $ircnets{$net};
102        my ($nick, $server) = ($conn->nick, $conn->server);
[6396c1e]103        $list .= BarnOwl::Style::boldify("IRC channels for $net ($nick\@$server)");
104        $list .= "\n";
[da554da]105
[dace02a]106        for my $chan (keys %{$conn->conn->{channel_list}}) {
[da554da]107            $list .= "  $chan\n";
108        }
109    }
110
111    return $list;
112}
113
[f81176c]114sub skip_msg {
115    my $class = shift;
116    my $type = lc shift;
117    my $skip = lc BarnOwl::getvar('irc:skip');
118    return grep {$_ eq $type} split ' ', $skip;
119}
120
[54b4a87]121=head2 mk_irc_command SUB FLAGS
122
123Return a subroutine that can be bound as a an IRC command. The
124subroutine will be called with arguments (COMMAND-NAME,
125IRC-CONNECTION, [CHANNEL], ARGV...).
126
127C<IRC-CONNECTION> and C<CHANNEL> will be inferred from arguments to
128the command and the current message if appropriate.
129
130The bitwise C<or> of zero or more C<FLAGS> can be passed in as a
131second argument to alter the behavior of the returned commands:
132
133=over 4
134
135=item C<CHANNEL_ARG>
136
137This command accepts the name of a channel. Pass in the C<CHANNEL>
138argument listed above, and die if no channel argument can be found.
139
[4f7b1f4]140=item C<CHANNEL_OR_USER>
141
142Pass the channel argument, but accept it if it's a username (e.g.
143has no hash).  Only relevant with C<CHANNEL_ARG>.
144
[54b4a87]145=item C<CHANNEL_OPTIONAL>
146
147Pass the channel argument, but don't die if not present. Only relevant
148with C<CHANNEL_ARG>.
149
[416241f]150=item C<ALLOW_DISCONNECTED>
151
152C<IRC-CONNECTION> may be a disconnected connection object that is
153currently pending a reconnect.
154
[54b4a87]155=back
156
157=cut
158
159use constant CHANNEL_ARG        => 1;
160use constant CHANNEL_OPTIONAL   => 2;
[4f7b1f4]161use constant CHANNEL_OR_USER    => 4;
[330c55a]162
[4f7b1f4]163use constant ALLOW_DISCONNECTED => 8;
[416241f]164
[b38b0b2]165sub register_commands {
[f17bb2c0]166    BarnOwl::new_command(
167        'irc-connect' => \&cmd_connect,
168        {
169            summary => 'Connect to an IRC server',
170            usage =>
[c84c365]171'irc-connect [-a ALIAS] [-s] [-p PASSWORD] [-n NICK] SERVER [port]',
[cd12307]172            description => <<END_DESCR
173Connect to an IRC server. Supported options are:
[f17bb2c0]174
175 -a <alias>          Define an alias for this server
176 -s                  Use SSL
177 -p <password>       Specify the password to use
178 -n <nick>           Use a non-default nick
179
180The -a option specifies an alias to use for this connection. This
181alias can be passed to the '-a' argument of any other IRC command to
182control which connection it operates on.
183
184For servers with hostnames of the form "irc.FOO.{com,org,...}", the
185alias will default to "FOO"; For other servers the full hostname is
186used.
187END_DESCR
188        }
189    );
190
191    BarnOwl::new_command(
[416241f]192        'irc-disconnect' => mk_irc_command( \&cmd_disconnect, ALLOW_DISCONNECTED ),
[f17bb2c0]193        {
194            summary => 'Disconnect from an IRC server',
195            usage   => 'irc-disconnect [-a ALIAS]',
196
197            description => <<END_DESCR
198Disconnect from an IRC server. You can specify a specific server with
199"-a SERVER-ALIAS" if necessary.
200END_DESCR
201        }
202    );
203
204    BarnOwl::new_command(
[4f7b1f4]205        'irc-msg' => mk_irc_command( \&cmd_msg, CHANNEL_OR_USER|CHANNEL_ARG|CHANNEL_OPTIONAL ),
[f17bb2c0]206        {
207            summary => 'Send an IRC message',
208            usage   => 'irc-msg [-a ALIAS] DESTINATION MESSAGE',
209
210            description => <<END_DESCR
[cd12307]211Send an IRC message.
[f17bb2c0]212END_DESCR
213        }
214    );
215
216    BarnOwl::new_command(
[54b4a87]217        'irc-mode' => mk_irc_command( \&cmd_mode, CHANNEL_OPTIONAL|CHANNEL_ARG ),
[f17bb2c0]218        {
219            summary => 'Change an IRC channel or user mode',
220            usage   => 'irc-mode [-a ALIAS] TARGET [+-]MODE OPTIONS',
221
222            description => <<END_DESCR
223Change the mode of an IRC user or channel.
224END_DESCR
225        }
226    );
227
228    BarnOwl::new_command(
229        'irc-join' => mk_irc_command( \&cmd_join ),
230        {
231            summary => 'Join an IRC channel',
[1b62a55]232            usage   => 'irc-join [-a ALIAS] #channel [KEY]',
[f17bb2c0]233
234            description => <<END_DESCR
235Join an IRC channel.
236END_DESCR
237        }
238    );
239
240    BarnOwl::new_command(
[54b4a87]241        'irc-part' => mk_irc_command( \&cmd_part, CHANNEL_ARG ),
[f17bb2c0]242        {
243            summary => 'Leave an IRC channel',
244            usage   => 'irc-part [-a ALIAS] #channel',
245
246            description => <<END_DESCR
247Part from an IRC channel.
248END_DESCR
249        }
250    );
251
252    BarnOwl::new_command(
253        'irc-nick' => mk_irc_command( \&cmd_nick ),
254        {
255            summary => 'Change your IRC nick on an existing connection.',
256            usage   => 'irc-nick [-a ALIAS] NEW-NICK',
257
258            description => <<END_DESCR
259Set your IRC nickname on an existing connect. To change it prior to
260connecting, adjust the `irc:nick' variable.
261END_DESCR
262        }
263    );
264
265    BarnOwl::new_command(
[54b4a87]266        'irc-names' => mk_irc_command( \&cmd_names, CHANNEL_ARG ),
[f17bb2c0]267        {
268            summary => 'View the list of users in a channel',
269            usage   => 'irc-names [-a ALIAS] #channel',
270
271            description => <<END_DESCR
272`irc-names' displays the list of users in a given channel in a pop-up
273window.
274END_DESCR
275        }
276    );
277
278    BarnOwl::new_command(
279        'irc-whois' => mk_irc_command( \&cmd_whois ),
280        {
281            summary => 'Displays information about a given IRC user',
282            usage   => 'irc-whois [-a ALIAS] NICK',
283
284            description => <<END_DESCR
285Pops up information about a given IRC user.
286END_DESCR
287        }
288    );
289
290    BarnOwl::new_command(
291        'irc-motd' => mk_irc_command( \&cmd_motd ),
292        {
293            summary => 'Displays an IRC server\'s MOTD (Message of the Day)',
294            usage   => 'irc-motd [-a ALIAS]',
295
296            description => <<END_DESCR
297Displays an IRC server's message of the day.
298END_DESCR
299        }
300    );
301
302    BarnOwl::new_command(
303        'irc-list' => \&cmd_list,
304        {
305            summary => 'Show all the active IRC connections.',
306            usage   => 'irc-list',
307
308            description => <<END_DESCR
309Show all the currently active IRC connections with their aliases and
310server names.
311END_DESCR
312        }
313    );
314
315    BarnOwl::new_command( 'irc-who'   => mk_irc_command( \&cmd_who ) );
316    BarnOwl::new_command( 'irc-stats' => mk_irc_command( \&cmd_stats ) );
317
318    BarnOwl::new_command(
[54b4a87]319        'irc-topic' => mk_irc_command( \&cmd_topic, CHANNEL_ARG ),
[f17bb2c0]320        {
321            summary => 'View or change the topic of an IRC channel',
322            usage   => 'irc-topic [-a ALIAS] #channel [TOPIC]',
323
324            description => <<END_DESCR
325Without extra arguments, fetches and displays a given channel's topic.
326
327With extra arguments, changes the target channel's topic string. This
328may require +o on some channels.
329END_DESCR
330        }
331    );
332
333    BarnOwl::new_command(
334        'irc-quote' => mk_irc_command( \&cmd_quote ),
335        {
336            summary => 'Send a raw command to the IRC servers.',
337            usage   => 'irc-quote [-a ALIAS] TEXT',
338
339            description => <<END_DESCR
340Send a raw command line to an IRC server.
341
342This can be used to perform some operation not yet supported by
343BarnOwl, or to define new IRC commands.
344END_DESCR
345        }
346    );
[b38b0b2]347}
348
[f17bb2c0]349
[167044b]350$BarnOwl::Hooks::startup->add('BarnOwl::Module::IRC::startup');
351$BarnOwl::Hooks::shutdown->add('BarnOwl::Module::IRC::shutdown');
[f17bb2c0]352$BarnOwl::Hooks::getQuickstart->add('BarnOwl::Module::IRC::quickstart');
[da554da]353$BarnOwl::Hooks::getBuddyList->add("BarnOwl::Module::IRC::buddylist");
[b38b0b2]354
355################################################################################
356######################## Owl command handlers ##################################
357################################################################################
358
359sub cmd_connect {
360    my $cmd = shift;
361
[2c40dc0]362    my $nick = BarnOwl::getvar('irc:nick');
363    my $username = BarnOwl::getvar('irc:user');
364    my $ircname = BarnOwl::getvar('irc:name');
[b38b0b2]365    my $host;
366    my $port;
367    my $alias;
368    my $ssl;
369    my $password = undef;
370
371    {
372        local @ARGV = @_;
373        GetOptions(
374            "alias=s"    => \$alias,
375            "ssl"        => \$ssl,
[2c40dc0]376            "password=s" => \$password,
[b10f340]377            "nick=s"     => \$nick,
[2c40dc0]378        );
[b38b0b2]379        $host = shift @ARGV or die("Usage: $cmd HOST\n");
380        if(!$alias) {
[f094fc4]381            if($host =~ /^(?:irc[.])?([\w-]+)[.]\w+$/) {
[b0c8011]382                $alias = $1;
383            } else {
384                $alias = $host;
385            }
[b38b0b2]386        }
387        $ssl ||= 0;
[f094fc4]388        $port = shift @ARGV || ($ssl ? 6697 : 6667);
[b38b0b2]389    }
390
[b0c8011]391    if(exists $ircnets{$alias}) {
392        die("Already connected to a server with alias '$alias'. Either disconnect or specify an alias with -a.\n");
393    }
394
[8ba9313]395    my $conn = BarnOwl::Module::IRC::Connection->new($alias, $host, $port, {
396        nick      => $nick,
397        user      => $username,
398        real      => $ircname,
399        password  => $password,
400        SSL       => $ssl,
401        timeout   => sub {0}
402       });
[851a0e0]403    $ircnets{$alias} = $conn;
[b38b0b2]404    return;
405}
406
407sub cmd_disconnect {
[ac374fc]408    my $cmd = shift;
409    my $conn = shift;
[851a0e0]410    if ($conn->conn->{socket}) {
[5c6d661]411        $conn->did_quit(1);
[8ba9313]412        $conn->conn->disconnect("Goodbye!");
[3713b86]413    } elsif ($conn->{reconnect_timer}) {
[416241f]414        BarnOwl::admin_message('IRC',
415                               "[" . $conn->alias . "] Reconnect cancelled");
416        $conn->cancel_reconnect;
[3713b86]417        delete $ircnets{$conn->alias};
[416241f]418    }
[b38b0b2]419}
420
421sub cmd_msg {
[330c55a]422    my $cmd  = shift;
423    my $conn = shift;
[e0fba58]424    my $to = shift or die("Usage: $cmd [NICK|CHANNEL]\n");
[2c40dc0]425    # handle multiple recipients?
[b38b0b2]426    if(@_) {
427        process_msg($conn, $to, join(" ", @_));
428    } else {
[744769e]429        BarnOwl::start_edit_win(BarnOwl::quote('/msg', '-a', $conn->alias, $to), sub {process_msg($conn, $to, @_)});
[b38b0b2]430    }
[48f7d12]431    return;
[b38b0b2]432}
433
434sub process_msg {
435    my $conn = shift;
436    my $to = shift;
[9a023d0]437    my $fullbody = shift;
438    my @msgs;
439    # Require the user to send in paragraphs (double-newline between) to
440    # actually send multiple PRIVMSGs, in order to play nice with autofill.
441    $fullbody =~ s/\r//g;
442    @msgs = split "\n\n", $fullbody;
443    map { tr/\n/ / } @msgs;
[c2866ec]444    # split each body at irc:max-message-length characters, if that number
445    # is positive.  Only split at space boundaries.  Start counting a-fresh
446    # at the beginning of each paragraph
447    my $max_len = BarnOwl::getvar('irc:max-message-length');
448    if ($max_len > 0) {
449        local($Text::Wrap::columns) = $max_len;
450        @msgs = split "\n", wrap("", "", join "\n", @msgs);
451    }
[9a023d0]452    for my $body (@msgs) {
453        if ($body =~ /^\/me (.*)/) {
[8ba9313]454            $conn->me($to, Encode::encode('utf-8', $1));
[9a023d0]455            $body = '* '.$conn->nick.' '.$1;
456        } else {
[8ba9313]457            $conn->conn->send_msg('privmsg', $to, Encode::encode('utf-8', $body));
[9a023d0]458        }
459        my $msg = BarnOwl::Message->new(
460            type        => 'IRC',
461            direction   => is_private($to) ? 'out' : 'in',
462            server      => $conn->server,
463            network     => $conn->alias,
464            recipient   => $to,
465            body        => $body,
466            sender      => $conn->nick,
467            is_private($to) ?
468              (isprivate  => 'true') : (channel => $to),
469            replycmd    => BarnOwl::quote('irc-msg',  '-a', $conn->alias, $to),
470            replysendercmd => BarnOwl::quote('irc-msg', '-a', $conn->alias, $to),
471        );
472        BarnOwl::queue_message($msg);
[919535f]473    }
[48f7d12]474    return;
[b38b0b2]475}
476
[e625b5e]477sub cmd_mode {
478    my $cmd = shift;
479    my $conn = shift;
480    my $target = shift;
481    $target ||= shift;
[8ba9313]482    $conn->conn->send_msg(mode => $target, @_);
[48f7d12]483    return;
[e625b5e]484}
485
[2c40dc0]486sub cmd_join {
487    my $cmd = shift;
[330c55a]488    my $conn = shift;
[2c40dc0]489    my $chan = shift or die("Usage: $cmd channel\n");
[8ba9313]490    $conn->conn->send_msg(join => $chan, @_);
[48f7d12]491    return;
[2c40dc0]492}
[b38b0b2]493
[6858d2d]494sub cmd_part {
495    my $cmd = shift;
[330c55a]496    my $conn = shift;
497    my $chan = shift;
[8ba9313]498    $conn->conn->send_msg(part => $chan);
[48f7d12]499    return;
[6858d2d]500}
501
[6286f26]502sub cmd_nick {
503    my $cmd = shift;
[330c55a]504    my $conn = shift;
[b0c8011]505    my $nick = shift or die("Usage: $cmd <new nick>\n");
[8ba9313]506    $conn->conn->send_msg(nick => $nick);
[48f7d12]507    return;
[6286f26]508}
509
[6858d2d]510sub cmd_names {
511    my $cmd = shift;
[330c55a]512    my $conn = shift;
513    my $chan = shift;
[d264c6d]514    $conn->names_tmp([]);
[8ba9313]515    $conn->conn->send_msg(names => $chan);
[48f7d12]516    return;
[6858d2d]517}
518
[b0c8011]519sub cmd_whois {
520    my $cmd = shift;
[330c55a]521    my $conn = shift;
[b0c8011]522    my $who = shift || die("Usage: $cmd <user>\n");
[8ba9313]523    $conn->conn->send_msg(whois => $who);
[48f7d12]524    return;
[b0c8011]525}
526
[56e72d5]527sub cmd_motd {
528    my $cmd = shift;
[330c55a]529    my $conn = shift;
[8ba9313]530    $conn->conn->send_msg('motd');
[48f7d12]531    return;
[56e72d5]532}
533
[f094fc4]534sub cmd_list {
535    my $cmd = shift;
536    my $message = BarnOwl::Style::boldify('Current IRC networks:') . "\n";
537    while (my ($alias, $conn) = each %ircnets) {
538        $message .= '  ' . $alias . ' => ' . $conn->nick . '@' . $conn->server . "\n";
539    }
540    BarnOwl::popless_ztext($message);
[48f7d12]541    return;
[f094fc4]542}
543
544sub cmd_who {
545    my $cmd = shift;
[330c55a]546    my $conn = shift;
[f094fc4]547    my $who = shift || die("Usage: $cmd <user>\n");
[8ba9313]548    $conn->conn->send_msg(who => $who);
[48f7d12]549    return;
[f094fc4]550}
551
552sub cmd_stats {
553    my $cmd = shift;
[330c55a]554    my $conn = shift;
[f094fc4]555    my $type = shift || die("Usage: $cmd <chiklmouy> [server] \n");
[8ba9313]556    $conn->conn->send_msg(stats => $type, @_);
[48f7d12]557    return;
[f094fc4]558}
559
[3ad15ff]560sub cmd_topic {
561    my $cmd = shift;
[330c55a]562    my $conn = shift;
563    my $chan = shift;
[8ba9313]564    $conn->conn->send_msg(topic => $chan, @_ ? join(" ", @_) : undef);
[48f7d12]565    return;
[3ad15ff]566}
567
[af9de56]568sub cmd_quote {
569    my $cmd = shift;
570    my $conn = shift;
[8ba9313]571    $conn->conn->send_msg(@_);
[48f7d12]572    return;
[af9de56]573}
574
[b38b0b2]575################################################################################
576########################### Utilities/Helpers ##################################
577################################################################################
578
[dace02a]579sub find_channel {
580    my $channel = shift;
581    my @found;
582    for my $conn (values %ircnets) {
583        if($conn->conn->{channel_list}{lc $channel}) {
584            push @found, $conn;
585        }
586    }
587    return $found[0] if(scalar @found == 1);
588}
589
[330c55a]590sub mk_irc_command {
591    my $sub = shift;
[54b4a87]592    my $flags = shift || 0;
[330c55a]593    return sub {
594        my $cmd = shift;
595        my $conn;
596        my $alias;
597        my $channel;
598        my $getopt = Getopt::Long::Parser->new;
599        my $m = BarnOwl::getcurmsg();
[b38b0b2]600
[330c55a]601        local @ARGV = @_;
602        $getopt->configure(qw(pass_through permute no_getopt_compat prefix_pattern=-|--));
603        $getopt->getoptions("alias=s" => \$alias);
604
605        if(defined($alias)) {
[416241f]606            $conn = get_connection_by_alias($alias,
607                                            $flags & ALLOW_DISCONNECTED);
[330c55a]608        }
[54b4a87]609        if($flags & CHANNEL_ARG) {
[e625b5e]610            $channel = $ARGV[0];
[330c55a]611            if(defined($channel) && $channel =~ /^#/) {
[dace02a]612                if(my $c = find_channel($channel)) {
[e625b5e]613                    shift @ARGV;
[dace02a]614                    $conn ||= $c;
[330c55a]615                }
[4f7b1f4]616            } elsif (defined($channel) && ($flags & CHANNEL_OR_USER)) {
617                shift @ARGV;
[ecee82f]618            } elsif ($m && $m->type eq 'IRC' && !$m->is_private) {
619                $channel = $m->channel;
620            } else {
621                undef $channel;
[330c55a]622            }
623        }
[ecee82f]624
[731e921]625        if(!defined($channel) &&
[54b4a87]626           ($flags & CHANNEL_ARG) &&
627           !($flags & CHANNEL_OPTIONAL)) {
[330c55a]628            die("Usage: $cmd <channel>\n");
629        }
630        if(!$conn) {
631            if($m && $m->type eq 'IRC') {
[416241f]632                $conn = get_connection_by_alias($m->network,
633                                               $flags & ALLOW_DISCONNECTED);
[330c55a]634            }
635        }
636        if(!$conn && scalar keys %ircnets == 1) {
637            $conn = [values(%ircnets)]->[0];
638        }
639        if(!$conn) {
640            die("You must specify an IRC network using -a.\n");
641        }
[54b4a87]642        if($flags & CHANNEL_ARG) {
[330c55a]643            $sub->($cmd, $conn, $channel, @ARGV);
644        } else {
645            $sub->($cmd, $conn, @ARGV);
646        }
647    };
[6858d2d]648}
649
[b38b0b2]650sub get_connection_by_alias {
[2c40dc0]651    my $key = shift;
[416241f]652    my $allow_disconnected = shift;
653
[3713b86]654    my $conn = $ircnets{$key};
655    die("No such ircnet: $key\n") unless $conn;
656    if ($conn->conn->{registered} || $allow_disconnected) {
657        return $conn;
658    }
659    die("[@{[$conn->alias]}] Not currently connected.");
[b38b0b2]660}
661
6621;
Note: See TracBrowser for help on using the repository browser.