mirror of
				https://github.com/pragma-/pbot.git
				synced 2025-10-31 14:47:27 +01:00 
			
		
		
		
	Start refactoring and polishing everything
More to come!
This commit is contained in:
		
							parent
							
								
									990c4f1455
								
							
						
					
					
						commit
						5fc4d8c86a
					
				| @ -7,8 +7,7 @@ use parent 'PBot::Class'; | ||||
| 
 | ||||
| # purpose: provides interface to set/remove/modify/query user capabilities. | ||||
| # | ||||
| # Examples: | ||||
| # | ||||
| # Examples: See doc/Admin.md for examples. | ||||
| 
 | ||||
| use warnings; use strict; | ||||
| use feature 'unicode_strings'; | ||||
| @ -18,16 +17,21 @@ no if $] >= 5.018, warnings => "experimental::smartmatch"; | ||||
| 
 | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
| 
 | ||||
|     # capabilities file | ||||
|     my $filename = $conf{filename} // $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/capabilities'; | ||||
| 
 | ||||
|     # capabilities hash table | ||||
|     $self->{caps} = PBot::HashObject->new(name => 'Capabilities', filename => $filename, pbot => $self->{pbot}); | ||||
| 
 | ||||
|     # load capabilities | ||||
|     $self->{caps}->load; | ||||
| 
 | ||||
|     # 'cap' command registered in PBot.pm because $self->{pbot}->{commands} is not yet loaded. | ||||
|     # 'cap' command registered in PBot.pm because $self->{pbot}->{commands} is not yet loaded at this point. | ||||
| 
 | ||||
|     # add some capabilities used in this file | ||||
|     $self->add('can-modify-capabilities',  undef, 1); | ||||
|     $self->add('can-group-capabilities',   undef, 1); | ||||
|     $self->add('can-ungroup-capabilities', undef, 1); | ||||
| 
 | ||||
|     # add some useful capabilities | ||||
|     $self->add('is-whitelisted', undef, 1); | ||||
| @ -37,7 +41,7 @@ sub cmd_cap { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $command = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); | ||||
|     my $result; | ||||
| 
 | ||||
|     given ($command) { | ||||
|         when ('list') { | ||||
|             my $cap = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); | ||||
| @ -46,14 +50,25 @@ sub cmd_cap { | ||||
| 
 | ||||
|         when ('whohas') { | ||||
|             my $cap = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); | ||||
|             return "Usage: cap whohas <capability>; Lists all users who have <capability>" if not defined $cap; | ||||
|             return "No such capability $cap."                                              if not $self->exists($cap); | ||||
| 
 | ||||
|             if (not defined $cap) { | ||||
|                 return "Usage: cap whohas <capability>; Lists all users who have <capability>"; | ||||
|             } | ||||
| 
 | ||||
|             if (not $self->exists($cap)) { | ||||
|                 return "No such capability $cap."; | ||||
|             } | ||||
| 
 | ||||
|             my $result  = "Users with capability $cap: "; | ||||
|             my $users   = $self->{pbot}->{users}->{users}; | ||||
|             my @matches; | ||||
| 
 | ||||
|             foreach my $name (sort $users->get_keys) { | ||||
|                 my $u = $users->get_data($name); | ||||
|                 push @matches, $users->get_key_name($name) if $self->userhas($u, $cap); | ||||
| 
 | ||||
|                 if ($self->userhas($u, $cap)) { | ||||
|                     push @matches, $users->get_key_name($name); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (@matches) { | ||||
| @ -67,10 +82,15 @@ sub cmd_cap { | ||||
| 
 | ||||
|         when ('userhas') { | ||||
|             my ($name, $cap) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); | ||||
|             return "Usage: cap userhas <username> [capability]; Lists capabilities belonging to <user>" if not defined $name; | ||||
| 
 | ||||
|             if (not defined $name) { | ||||
|                 return "Usage: cap userhas <username> [capability]; Lists capabilities belonging to <user>"; | ||||
|             } | ||||
| 
 | ||||
|             $cap = lc $cap if defined $cap; | ||||
| 
 | ||||
|             my $u = $self->{pbot}->{users}->{users}->get_data($name); | ||||
| 
 | ||||
|             if (not defined $u) { | ||||
|                 return "No such user $name."; | ||||
|             } | ||||
| @ -78,70 +98,128 @@ sub cmd_cap { | ||||
|             $name = $self->{pbot}->{users}->{users}->get_key_name($name); | ||||
| 
 | ||||
|             if (defined $cap) { | ||||
|                 return "Try again. No such capability $cap." if not $self->exists($cap); | ||||
|                 if   ($self->userhas($u, $cap)) { return "Yes. User $name has capability $cap."; } | ||||
|                 else                            { return "No. User $name  does not have capability $cap."; } | ||||
|                 if (not $self->exists($cap)) { | ||||
|                     return "Try again. No such capability $cap."; | ||||
|                 } | ||||
| 
 | ||||
|                 if ($self->userhas($u, $cap)) { | ||||
|                     return "Yes. User $name has capability $cap."; | ||||
|                 } else { | ||||
|                     return "No. User $name  does not have capability $cap."; | ||||
|                 } | ||||
|             } else { | ||||
|                 my $result = "User $name has capabilities: "; | ||||
|                 my @groups; | ||||
|                 my @single; | ||||
| 
 | ||||
|                 foreach my $key (sort keys %{$u}) { | ||||
|                     next if $key eq '_name'; | ||||
|                     next if not $self->exists($key); | ||||
|                     next if $key eq '_name';          # skip internal cached metadata | ||||
|                     next if not $self->exists($key);  # skip metadata that isn't a capability | ||||
| 
 | ||||
|                     my $count = $self->{caps}->get_keys; | ||||
|                     if   ($count > 0) { push @groups, "$key ($count cap" . ($count == 1 ? '' : 's') . ")"; } | ||||
|                     else              { push @single, $key; } | ||||
| 
 | ||||
|                     if ($count > 0) { | ||||
|                         push @groups, "$key ($count cap" . ($count == 1 ? '' : 's') . ")"; | ||||
|                     } else { | ||||
|                         push @single, $key; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (@groups or @single) { | ||||
|                     # first list all capabilities that have sub-capabilities (i.e. grouped capabilities) | ||||
|                     # then list stand-alone (single) capabilities | ||||
|                     return "User $name has capabilities: " . join ', ', @groups, @single; | ||||
|                 } else { | ||||
|                     return "User $name has no capabilities."; | ||||
|                 } | ||||
|                 if (@groups or @single) { $result .= join ', ', @groups, @single; } | ||||
|                 else                    { $result = "User $name has no capabilities."; } | ||||
|                 return $result; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         when ('group') { | ||||
|             my ($cap, $subcaps) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); | ||||
|             return "Usage: cap group <existing or new capability> <existing capabilities...>" if not defined $cap or not defined $subcaps; | ||||
| 
 | ||||
|             if (not defined $cap or not defined $subcaps) { | ||||
|                 return "Usage: cap group <existing or new capability> <existing capabilities...>"; | ||||
|             } | ||||
| 
 | ||||
|             my $u = $self->{pbot}->{users}->loggedin($context->{from}, $context->{hostmask}); | ||||
|             return "You must be logged into your user account to group capabilities together."           if not defined $u; | ||||
|             return "You must have the can-group-capabilities capability to group capabilities together." if not $self->userhas($u, 'can-group-capabilities'); | ||||
| 
 | ||||
|             my @caps = split /\s+|,/, $subcaps; | ||||
|             if (not defined $u) { | ||||
|                 return "You must be logged into your user account to group capabilities together."; | ||||
|             } | ||||
| 
 | ||||
|             if (not $self->userhas($u, 'can-group-capabilities')) { | ||||
|                 return "You must have the can-group-capabilities capability to group capabilities together."; | ||||
|             } | ||||
| 
 | ||||
|             my @caps = split /\s+|,\s*/, $subcaps; # split by spaces or comma | ||||
| 
 | ||||
|             foreach my $c (@caps) { | ||||
|                 return "No such capability $c."                     if not $self->exists($c); | ||||
|                 return "You cannot group a capability with itself." if lc $cap eq lc $c; | ||||
|                 if (not $self->exists($c)) { | ||||
|                     return "No such capability $c."; | ||||
|                 } | ||||
| 
 | ||||
|                 if (lc $cap eq lc $c) { | ||||
|                     return "You cannot group a capability with itself."; | ||||
|                 } | ||||
| 
 | ||||
|                 $self->add($cap, $c); | ||||
|             } | ||||
|             if (@caps > 1) { return "Capabilities " . join(', ', @caps) . " added to the $cap capability group."; } | ||||
|             else           { return "Capability $subcaps added to the $cap capability group."; } | ||||
| 
 | ||||
|             if (@caps > 1) { | ||||
|                 return "Capabilities " . join(', ', @caps) . " added to the $cap capability group."; | ||||
|             } else { | ||||
|                 return "Capability $subcaps added to the $cap capability group."; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         when ('ungroup') { | ||||
|             my ($cap, $subcaps) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); | ||||
|             return "Usage: cap ungroup <existing capability group> <grouped capabilities...>" if not defined $cap or not defined $subcaps; | ||||
|             return "No such capability $cap."                                                 if not $self->exists($cap); | ||||
| 
 | ||||
|             if (not defined $cap or not defined $subcaps) { | ||||
|                 return "Usage: cap ungroup <existing capability group> <grouped capabilities...>"; | ||||
|             } | ||||
| 
 | ||||
|             if (not $self->exists($cap)) { | ||||
|                 return "No such capability $cap."; | ||||
|             } | ||||
| 
 | ||||
|             my $u = $self->{pbot}->{users}->loggedin($context->{from}, $context->{hostmask}); | ||||
|             return "You must be logged into your user account to remove capabilities from groups."             if not defined $u; | ||||
|             return "You must have the can-ungroup-capabilities capability to remove capabilities from groups." if not $self->userhas($u, 'can-ungroup-capabilities'); | ||||
| 
 | ||||
|             my @caps = split /\s+|,/, $subcaps; | ||||
|             if (not defined $u) { | ||||
|                 return "You must be logged into your user account to remove capabilities from groups."; | ||||
|             } | ||||
| 
 | ||||
|             if (not $self->userhas($u, 'can-group-capabilities')) { | ||||
|                 return "You must have the can-group-capabilities capability to remove capabilities from groups."; | ||||
|             } | ||||
| 
 | ||||
|             my @caps = split /\s+|,\s*/, $subcaps; # split by spaces or comma | ||||
| 
 | ||||
|             foreach my $c (@caps) { | ||||
|                 return "No such capability $c."                                      if not $self->exists($c); | ||||
|                 return "Capability $c does not belong to the $cap capability group." if not $self->has($cap, $c); | ||||
|                 if (not $self->exists($c)) { | ||||
|                     return "No such capability $c."; | ||||
|                 } | ||||
| 
 | ||||
|                 if (not $self->has($cap, $c)) { | ||||
|                     return "Capability $c does not belong to the $cap capability group."; | ||||
|                 } | ||||
| 
 | ||||
|                 $self->remove($cap, $c); | ||||
|             } | ||||
| 
 | ||||
|             if (@caps > 1) { return "Capabilities " . join(', ', @caps) . " removed from the $cap capability group."; } | ||||
|             else           { return "Capability $subcaps removed from the $cap capability group."; } | ||||
|             if (@caps > 1) { | ||||
|                 return "Capabilities " . join(', ', @caps) . " removed from the $cap capability group."; | ||||
|             } else { | ||||
|                 return "Capability $subcaps removed from the $cap capability group."; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         default { | ||||
|             $result = | ||||
|               "Usage: cap list [capability] | cap group <existing or new capability group> <existing capabilities...> | cap ungroup <existing capability group> <grouped capabilities...> | cap userhas <user> [capability] | cap whohas <capability>"; | ||||
|             return "Usage: cap list [capability] | cap group <existing or new capability group> <existing capabilities...> " | ||||
|                 . "| cap ungroup <existing capability group> <grouped capabilities...> | cap userhas <user> [capability] " | ||||
|                 . "| cap whohas <capability>"; | ||||
|         } | ||||
|     } | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| sub has { | ||||
| @ -155,7 +233,8 @@ sub has { | ||||
|         return 1; | ||||
|     } | ||||
| 
 | ||||
|     $depth //= 10; | ||||
|     $depth //= 10;  # set depth to 10 if it's not defined | ||||
| 
 | ||||
|     if (--$depth <= 0) { | ||||
|         $self->{pbot}->{logger}->log("Max recursion reached for PBot::Capabilities->has($cap, $subcap)\n"); | ||||
|         return 0; | ||||
| @ -165,35 +244,46 @@ sub has { | ||||
|         return 1 if $c eq $subcap and $cap_data->{$c}; | ||||
|         return 1 if $self->has($c, $subcap, $depth); | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub userhas { | ||||
|     my ($self, $user, $cap) = @_; | ||||
| 
 | ||||
|     return 0 if not defined $user; | ||||
|     return 1 if $user->{$cap}; | ||||
| 
 | ||||
|     foreach my $key (keys %$user) { | ||||
|         next     if $key eq '_name'; | ||||
|         next     if not $user->{$key}; | ||||
|         return 1 if $self->has($key, $cap); | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub exists { | ||||
|     my ($self, $cap) = @_; | ||||
| 
 | ||||
|     $cap = lc $cap; | ||||
| 
 | ||||
|     foreach my $c ($self->{caps}->get_keys) { | ||||
|         return 1 if $c eq $cap; | ||||
| 
 | ||||
|         foreach my $sub_cap ($self->{caps}->get_keys($c)) { | ||||
|             return 1 if $sub_cap eq $cap; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub add { | ||||
|     my ($self, $cap, $subcap, $dontsave) = @_; | ||||
| 
 | ||||
|     $cap = lc $cap; | ||||
| 
 | ||||
|     if (not defined $subcap) { | ||||
|         if (not $self->{caps}->exists($cap)) { | ||||
|             $self->{caps}->add($cap, {}, $dontsave); | ||||
| @ -209,7 +299,9 @@ sub add { | ||||
| 
 | ||||
| sub remove { | ||||
|     my ($self, $cap, $subcap) = @_; | ||||
| 
 | ||||
|     $cap = lc $cap; | ||||
| 
 | ||||
|     if (not defined $subcap) { | ||||
|         foreach my $c ($self->{caps}->get_keys) { | ||||
|             foreach my $sub_cap ($self->{caps}->get_keys($c)) { | ||||
| @ -220,18 +312,26 @@ sub remove { | ||||
|     } else { | ||||
|         $self->{caps}->remove($cap, $subcap, 1) if $self->{caps}->exists($cap); | ||||
|     } | ||||
| 
 | ||||
|     $self->{caps}->save; | ||||
| } | ||||
| 
 | ||||
| sub rebuild_botowner_capabilities { | ||||
|     my ($self) = @_; | ||||
| 
 | ||||
|     $self->{caps}->remove('botowner', undef, 1); | ||||
|     foreach my $cap ($self->{caps}->get_keys) { $self->add('botowner', $cap, 1); } | ||||
| 
 | ||||
|     foreach my $cap ($self->{caps}->get_keys) { | ||||
|         $self->add('botowner', $cap, 1); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| sub list { | ||||
|     my ($self, $capability) = @_; | ||||
|     return "No such capability $capability." if defined $capability and not $self->{caps}->exists($capability); | ||||
| 
 | ||||
|     if (defined $capability and not $self->{caps}->exists($capability)) { | ||||
|         return "No such capability $capability."; | ||||
|     } | ||||
| 
 | ||||
|     my @caps; | ||||
|     my @groups; | ||||
| @ -243,7 +343,11 @@ sub list { | ||||
|         $result = 'Capabilities: '; | ||||
|     } else { | ||||
|         @caps = sort $self->{caps}->get_keys($capability); | ||||
|         return "Capability $capability has no grouped capabilities." if not @caps; | ||||
| 
 | ||||
|         if (not @caps) { | ||||
|             return "Capability $capability has no grouped capabilities." | ||||
|         } | ||||
| 
 | ||||
|         $result = "Grouped capabilities for $capability: "; | ||||
|     } | ||||
| 
 | ||||
| @ -251,10 +355,16 @@ sub list { | ||||
|     # then list stand-alone capabilities | ||||
|     foreach my $cap (@caps) { | ||||
|         my $count = $self->{caps}->get_keys($cap); | ||||
|         if   ($count > 0) { push @groups,      "$cap ($count cap" . ($count == 1 ? '' : 's') . ")" if $count; } | ||||
|         else              { push @standalones, $cap; } | ||||
| 
 | ||||
|         if ($count > 0) { | ||||
|             push @groups, "$cap ($count cap" . ($count == 1 ? '' : 's') . ")"; | ||||
|         } else { | ||||
|             push @standalones, $cap; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     $result .= join ', ', @groups, @standalones; | ||||
| 
 | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -12,6 +12,9 @@ package PBot::Class; | ||||
| use warnings; | ||||
| use strict; | ||||
| 
 | ||||
| use feature 'unicode_strings'; | ||||
| use utf8; | ||||
| 
 | ||||
| sub new { | ||||
|     my ($proto, %conf) = @_; | ||||
|     my $class = ref($proto) || $proto; | ||||
|  | ||||
| @ -21,23 +21,14 @@ sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
|     $self->PBot::Registerable::initialize(%conf); | ||||
| 
 | ||||
|     $self->{metadata} = PBot::HashObject->new(pbot => $self->{pbot}, name => 'Commands', filename => $conf{filename}); | ||||
|     # command metadata | ||||
|     $self->{metadata} = PBot::HashObject->new(pbot => $self->{pbot}, name => 'Command metadata', filename => $conf{filename}); | ||||
|     $self->{metadata}->load; | ||||
| 
 | ||||
|     $self->register(sub { $self->cmd_set(@_) },        "cmdset",   1); | ||||
|     $self->register(sub { $self->cmd_unset(@_) },      "cmdunset", 1); | ||||
|     $self->register(sub { $self->cmd_help(@_) },       "help",     0); | ||||
|     $self->register(sub { $self->cmd_uptime(@_) },     "uptime",   0); | ||||
|     $self->register(sub { $self->cmd_in_channel(@_) }, "in",       0); | ||||
|     $self->register(sub { $self->cmd_nop(@_) },        "nop",      0); | ||||
| 
 | ||||
|     $self->{pbot}->{capabilities}->add('admin', 'can-in', 1); | ||||
| } | ||||
| 
 | ||||
| sub cmd_nop { | ||||
|     my ($self, $context) = @_; | ||||
|     $self->{pbot}->{logger}->log("Disregarding NOP command.\n"); | ||||
|     return ""; | ||||
|     # register commands to manipulate command metadata and obtain help | ||||
|     $self->register(sub { $self->cmd_set(@_) },   "cmdset",   1); | ||||
|     $self->register(sub { $self->cmd_unset(@_) }, "cmdunset", 1); | ||||
|     $self->register(sub { $self->cmd_help(@_) },  "help",     0); | ||||
| } | ||||
| 
 | ||||
| sub cmd_set { | ||||
| @ -69,13 +60,19 @@ sub cmd_help { | ||||
|             my $name         = $self->{metadata}->get_key_name($keyword); | ||||
|             my $requires_cap = $self->{metadata}->get_data($keyword, 'requires_cap'); | ||||
|             my $help         = $self->{metadata}->get_data($keyword, 'help'); | ||||
|             my $result       = "/say $name: "; | ||||
| 
 | ||||
|             my $result = "/say $name: "; | ||||
|             $result .= "[Requires can-$keyword] " if $requires_cap; | ||||
| 
 | ||||
|             if   (not defined $help or not length $help) { $result .= "I have no help text for this command yet. To add help text, use the command `cmdset $keyword help <text>`."; } | ||||
|             else                                         { $result .= $help; } | ||||
|             if (not defined $help or not length $help) { | ||||
|                 $result .= "I have no help text for this command yet. To add help text, use the command `cmdset $keyword help <text>`."; | ||||
|             } else { | ||||
|                 $result .= $help; | ||||
|             } | ||||
| 
 | ||||
|             return $result; | ||||
|         } | ||||
| 
 | ||||
|         return "$keyword is a built-in command, but I have no help for it yet."; | ||||
|     } | ||||
| 
 | ||||
| @ -119,35 +116,15 @@ sub cmd_help { | ||||
| 
 | ||||
|     my $help = $self->{pbot}->{factoids}->{factoids}->get_data($channel, $trigger, 'help'); | ||||
| 
 | ||||
|     if (not defined $help or not length $help) { return "/say $trigger_name is a factoid for $channel_name, but I have no help text for it yet. To add help text, use the command `factset $trigger_name help <text>`."; } | ||||
|     if (not defined $help or not length $help) { | ||||
|         return "/say $trigger_name is a factoid for $channel_name, but I have no help text for it yet." | ||||
|            . " To add help text, use the command `factset $trigger_name help <text>`."; | ||||
|     } | ||||
| 
 | ||||
|     $result .= $help; | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| sub cmd_uptime { | ||||
|     my ($self, $context) = @_; | ||||
|     return localtime($self->{pbot}->{startup_timestamp}) . " [" . duration(time - $self->{pbot}->{startup_timestamp}) . "]"; | ||||
| } | ||||
| 
 | ||||
| sub cmd_in_channel { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $usage = "Usage: in <channel> <command>"; | ||||
|     return $usage if not length $context->{arguments}; | ||||
| 
 | ||||
|     my ($channel, $command) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2, 0, 1); | ||||
|     return $usage if not defined $channel or not defined $command; | ||||
| 
 | ||||
|     if (not $self->{pbot}->{nicklist}->is_present($channel, $context->{nick})) { | ||||
|         return "You must be present in $channel to do this."; | ||||
|     } | ||||
| 
 | ||||
|     $context->{from}    = $channel; | ||||
|     $context->{command} = $command; | ||||
|     return $self->{pbot}->{interpreter}->interpret($context); | ||||
| } | ||||
| 
 | ||||
| sub register { | ||||
|     my ($self, $subref, $name, $requires_cap) = @_; | ||||
|     Carp::croak("Missing parameters to Commands::register") if not defined $subref or not defined $name; | ||||
| @ -156,12 +133,15 @@ sub register { | ||||
|     $ref->{name}         = lc $name; | ||||
|     $ref->{requires_cap} = $requires_cap // 0; | ||||
| 
 | ||||
|     if (not $self->{metadata}->exists($name)) { $self->{metadata}->add($name, {requires_cap => $requires_cap, help => ''}, 1); } | ||||
|     else { | ||||
|         if (not defined $self->get_meta($name, 'requires_cap')) { $self->{metadata}->set($name, 'requires_cap', $requires_cap, 1); } | ||||
|     if (not $self->{metadata}->exists($name)) { | ||||
|         $self->{metadata}->add($name, {requires_cap => $requires_cap, help => ''}, 1); | ||||
|     } else { | ||||
|         if (not defined $self->get_meta($name, 'requires_cap')) { | ||||
|             $self->{metadata}->set($name, 'requires_cap', $requires_cap, 1); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     # add can-cmd capability | ||||
|     # add can-cmd capability if command requires such | ||||
|     $self->{pbot}->{capabilities}->add("can-$name", undef, 1) if $requires_cap; | ||||
|     return $ref; | ||||
| } | ||||
| @ -207,7 +187,10 @@ sub interpreter { | ||||
|     my $from    = $context->{from}; | ||||
| 
 | ||||
|     my ($cmd_channel) = $context->{arguments} =~ m/\B(#[^ ]+)/;    # assume command is invoked in regards to first channel-like argument | ||||
|     $cmd_channel = $from if not defined $cmd_channel;            # otherwise command is invoked in regards to the channel the user is in | ||||
|     $cmd_channel = $from if not defined $cmd_channel;              # otherwise command is invoked in regards to the channel the user is in | ||||
| 
 | ||||
|     $context->{channel} = $cmd_channel; | ||||
| 
 | ||||
|     my $user = $self->{pbot}->{users}->find_user($cmd_channel, "$context->{nick}!$context->{user}\@$context->{host}"); | ||||
| 
 | ||||
|     my $cap_override; | ||||
|  | ||||
| @ -49,7 +49,7 @@ sub load { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     $self->{pbot}->{logger}->log("Loading $self->{name} from $filename ...\n"); | ||||
|     $self->{pbot}->{logger}->log("Loading $self->{name} from $filename\n"); | ||||
| 
 | ||||
|     if (not open(FILE, "< $filename")) { | ||||
|         $self->{pbot}->{logger}->log("Skipping loading from file: Couldn't open $filename: $!\n"); | ||||
|  | ||||
| @ -27,6 +27,7 @@ use feature 'unicode_strings'; | ||||
| 
 | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
| 
 | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_func(@_) }, 'func', 0); | ||||
| 
 | ||||
|     $self->register( | ||||
| @ -50,15 +51,27 @@ sub initialize { | ||||
| 
 | ||||
| sub cmd_func { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $func = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}); | ||||
|     return "Usage: func <keyword> [arguments]; see also: func help" if not defined $func; | ||||
|     return "[No such func '$func']"                                 if not exists $self->{funcs}->{$func}; | ||||
| 
 | ||||
|     if (not defined $func) { | ||||
|         return "Usage: func <keyword> [arguments]; see also: func help"; | ||||
|     } | ||||
| 
 | ||||
|     if (not exists $self->{funcs}->{$func}) { | ||||
|         return "[No such func '$func']" | ||||
|     } | ||||
| 
 | ||||
|     my @params; | ||||
|     while (defined(my $param = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}))) { push @params, $param; } | ||||
| 
 | ||||
|     while (defined(my $param = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}))) { | ||||
|         push @params, $param; | ||||
|     } | ||||
| 
 | ||||
|     my $result = $self->{funcs}->{$func}->{subref}->(@params); | ||||
|     $result =~ s/\x1/1/g; | ||||
| 
 | ||||
|     $result =~ s/\x1/1/g; # strip CTCP code | ||||
| 
 | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| @ -74,26 +87,43 @@ sub unregister { | ||||
| 
 | ||||
| sub func_help { | ||||
|     my ($self, $func) = @_; | ||||
|     return "func: invoke built-in functions; usage: func <keyword> [arguments]; to list available functions: func list [regex]" if not length $func; | ||||
|     return "No such func '$func'."                                                                                              if not exists $self->{funcs}->{$func}; | ||||
| 
 | ||||
|     if (not length $func) { | ||||
|         return "func: invoke built-in functions; usage: func <keyword> [arguments]; to list available functions: func list [regex]"; | ||||
|     } | ||||
| 
 | ||||
|     if (not exists $self->{funcs}->{$func}) { | ||||
|         return "No such func '$func'."; | ||||
|     } | ||||
| 
 | ||||
|     return "$func: $self->{funcs}->{$func}->{desc}; usage: $self->{funcs}->{$func}->{usage}"; | ||||
| } | ||||
| 
 | ||||
| sub func_list { | ||||
|     my ($self, $regex) = @_; | ||||
|     $regex = '.*' if not defined $regex; | ||||
| 
 | ||||
|     $regex //= '.*'; | ||||
| 
 | ||||
|     my $result = eval { | ||||
|         my $text = ''; | ||||
|         my @funcs; | ||||
| 
 | ||||
|         foreach my $func (sort keys %{$self->{funcs}}) { | ||||
|             if ($func =~ m/$regex/i or $self->{funcs}->{$func}->{desc} =~ m/$regex/i) { $text .= "$func, "; } | ||||
|             if ($func =~ m/$regex/i or $self->{funcs}->{$func}->{desc} =~ m/$regex/i) { | ||||
|                 push @funcs, $func; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $text =~ s/,\s+$//; | ||||
|         if (not length $text) { | ||||
|             if   ($regex eq '.*') { $text = "No funcs yet."; } | ||||
|             else                  { $text = "No matching func."; } | ||||
|         my $result = join ', ', @funcs; | ||||
| 
 | ||||
|         if (not length $result) { | ||||
|             if ($regex eq '.*') { | ||||
|                 $result = "No funcs yet."; | ||||
|             } else { | ||||
|                 $result = "No matching func."; | ||||
|             } | ||||
|         } | ||||
|         return "Available funcs: $text; see also: func help <keyword>"; | ||||
| 
 | ||||
|         return "Available funcs: $result; see also: func help <keyword>"; | ||||
|     }; | ||||
| 
 | ||||
|     if ($@) { | ||||
| @ -101,6 +131,7 @@ sub func_list { | ||||
|         $error =~ s/at PBot.Functions.*$//; | ||||
|         return "Error: $error\n"; | ||||
|     } | ||||
| 
 | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -48,7 +48,7 @@ sub load { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     $self->{pbot}->{logger}->log("Loading $self->{name} from $filename ...\n"); | ||||
|     $self->{pbot}->{logger}->log("Loading $self->{name} from $filename\n"); | ||||
| 
 | ||||
|     if (not open(FILE, "< $filename")) { | ||||
|         $self->{pbot}->{logger}->log("Skipping loading from file: Couldn't open $filename: $!\n"); | ||||
|  | ||||
| @ -47,6 +47,8 @@ sub process_line { | ||||
|     $flood_threshold      = $pbot->{registry}->get_value('antiflood', 'chat_flood_threshold')      if not defined $flood_threshold; | ||||
|     $flood_time_threshold = $pbot->{registry}->get_value('antiflood', 'chat_flood_time_threshold') if not defined $flood_time_threshold; | ||||
| 
 | ||||
| 
 | ||||
| =cut | ||||
|     if (defined $from and $from =~ m/^#/) { | ||||
|         my $chanmodes = $self->{pbot}->{channels}->get_meta($from, 'MODE'); | ||||
|         if (defined $chanmodes and $chanmodes =~ m/z/) { | ||||
| @ -59,6 +61,7 @@ sub process_line { | ||||
|             $context->{banned} = 1 if $self->{pbot}->{banlist}->is_banned($nick, $user, $host, $from); | ||||
|         } | ||||
|     } | ||||
| =cut | ||||
| 
 | ||||
|     $pbot->{antiflood}->check_flood( | ||||
|         $from,                               $nick, $user, $host, $text, | ||||
| @ -66,10 +69,12 @@ sub process_line { | ||||
|         $pbot->{messagehistory}->{MSG_CHAT}, $context | ||||
|     ) if defined $from; | ||||
| 
 | ||||
| =cut | ||||
|     if ($context->{banned} or $context->{unidentified}) { | ||||
|         $self->{pbot}->{logger}->log("Disregarding banned/unidentified user message (channel $from is +z).\n"); | ||||
|         return 1; | ||||
|     } | ||||
| =cut | ||||
| 
 | ||||
|     my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick'); | ||||
| 
 | ||||
|  | ||||
| @ -20,11 +20,21 @@ use Time::Duration; | ||||
| 
 | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
|     $self->{lag_average}    = undef;    # average of entries in lag history, in seconds | ||||
|     $self->{lag_string}     = undef;    # string representation of lag history and lag average | ||||
|     $self->{lag_history}    = [];       # history of previous PING/PONG timings | ||||
|     $self->{pong_received}  = undef;    # tracks pong replies; undef if no ping sent; 0 if ping sent but no pong reply yet; 1 if ping/pong completed | ||||
|     $self->{ping_send_time} = undef;    # when last ping was sent | ||||
| 
 | ||||
|     # average of entries in lag history, in seconds | ||||
|     $self->{lag_average} = undef; | ||||
| 
 | ||||
|     # string representation of lag history and lag average | ||||
|     $self->{lag_string} = undef; | ||||
| 
 | ||||
|     # history of previous PING/PONG timings | ||||
|     $self->{lag_history} = []; | ||||
| 
 | ||||
|     # tracks pong replies; undef if no ping sent; 0 if ping sent but no pong reply yet; 1 if ping/pong completed | ||||
|     $self->{pong_received} = undef; | ||||
| 
 | ||||
|     # when last ping was sent | ||||
|     $self->{ping_send_time} = undef; | ||||
| 
 | ||||
|     # maximum number of lag history entries to retain | ||||
|     $self->{pbot}->{registry}->add_default('text', 'lagchecker', 'lag_history_max', $conf{lag_history_max} // 3); | ||||
| @ -35,85 +45,30 @@ sub initialize { | ||||
|     # how often to send PING, in seconds | ||||
|     $self->{pbot}->{registry}->add_default('text', 'lagchecker', 'lag_history_interval', $conf{lag_history_interval} // 10); | ||||
| 
 | ||||
|     $self->{pbot}->{registry}->add_trigger('lagchecker', 'lag_history_interval', sub { $self->lag_history_interval_trigger(@_) }); | ||||
|     # registry trigger for lag_history_interval changes | ||||
|     $self->{pbot}->{registry}->add_trigger('lagchecker', 'lag_history_interval', sub { $self->trigger_lag_history_interval(@_) }); | ||||
| 
 | ||||
|     # timer to send PINGs | ||||
|     $self->{pbot}->{timer}->register( | ||||
|         sub { $self->send_ping }, | ||||
|         $self->{pbot}->{registry}->get_value('lagchecker', 'lag_history_interval'), | ||||
|         'lag check' | ||||
|     ); | ||||
| 
 | ||||
|     # lagcheck bot command | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_lagcheck(@_) }, "lagcheck", 0); | ||||
| 
 | ||||
|     # PONG IRC handler | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('irc.pong', sub { $self->on_pong(@_) }); | ||||
| } | ||||
| 
 | ||||
| sub lag_history_interval_trigger { | ||||
| # registry trigger fires when value changes | ||||
| sub trigger_lag_history_interval { | ||||
|     my ($self, $section, $item, $newvalue) = @_; | ||||
|     $self->{pbot}->{timer}->update_interval('lag check', $newvalue); | ||||
| } | ||||
| 
 | ||||
| sub send_ping { | ||||
|     my $self = shift; | ||||
|     return unless defined $self->{pbot}->{conn}; | ||||
|     $self->{ping_send_time} = [gettimeofday]; | ||||
|     $self->{pong_received}  = 0; | ||||
|     $self->{pbot}->{conn}->sl("PING :lagcheck"); | ||||
| } | ||||
| 
 | ||||
| sub on_pong { | ||||
|     my $self = shift; | ||||
| 
 | ||||
|     $self->{pong_received} = 1; | ||||
| 
 | ||||
|     my $elapsed = tv_interval($self->{ping_send_time}); | ||||
|     push @{$self->{lag_history}}, [$self->{ping_send_time}[0], $elapsed * 1000]; | ||||
| 
 | ||||
|     my $len = @{$self->{lag_history}}; | ||||
| 
 | ||||
|     my $lag_history_max = $self->{pbot}->{registry}->get_value('lagchecker', 'lag_history_max'); | ||||
| 
 | ||||
|     while ($len > $lag_history_max) { | ||||
|         shift @{$self->{lag_history}}; | ||||
|         $len--; | ||||
|     } | ||||
| 
 | ||||
|     $self->{lag_string} = ""; | ||||
|     my $comma = ""; | ||||
| 
 | ||||
|     my $lag_total = 0; | ||||
|     foreach my $entry (@{$self->{lag_history}}) { | ||||
|         my ($send_time, $lag_result) = @$entry; | ||||
| 
 | ||||
|         $lag_total += $lag_result; | ||||
|         my $ago = concise ago(gettimeofday - $send_time); | ||||
|         $self->{lag_string} .= $comma . "[$ago] " . sprintf "%.1f ms", $lag_result; | ||||
|         $comma = "; "; | ||||
|     } | ||||
| 
 | ||||
|     $self->{lag_average} = $lag_total / $len; | ||||
|     $self->{lag_string} .= "; average: " . sprintf "%.1f ms", $self->{lag_average}; | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub lagging { | ||||
|     my $self = shift; | ||||
| 
 | ||||
|     if (defined $self->{pong_received} and $self->{pong_received} == 0) { | ||||
|         # a ping has been sent (pong_received is not undef) and no pong has been received yet | ||||
|         my $elapsed = tv_interval($self->{ping_send_time}); | ||||
|         return $elapsed >= $self->{pbot}->{registry}->get_value('lagchecker', 'lag_threshold'); | ||||
|     } | ||||
| 
 | ||||
|     return 0 if not defined $self->{lag_average}; | ||||
|     return $self->{lag_average} >= $self->{pbot}->{registry}->get_value('lagchecker', 'lag_threshold'); | ||||
| } | ||||
| 
 | ||||
| sub lagstring { | ||||
|     my $self = shift; | ||||
|     my $lag  = $self->{lag_string} || "initializing"; | ||||
|     return $lag; | ||||
| } | ||||
| 
 | ||||
| # lagcheck bot command | ||||
| sub cmd_lagcheck { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
| @ -144,4 +99,81 @@ sub cmd_lagcheck { | ||||
|     return "My lag: " . $self->lagstring; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| sub send_ping { | ||||
|     my $self = shift; | ||||
| 
 | ||||
|     return unless defined $self->{pbot}->{conn}; | ||||
| 
 | ||||
|     $self->{ping_send_time} = [gettimeofday]; | ||||
|     $self->{pong_received}  = 0; | ||||
| 
 | ||||
|     $self->{pbot}->{conn}->sl("PING :lagcheck"); | ||||
| } | ||||
| 
 | ||||
| sub on_pong { | ||||
|     my $self = shift; | ||||
| 
 | ||||
|     $self->{pong_received} = 1; | ||||
| 
 | ||||
|     my $elapsed = tv_interval($self->{ping_send_time}); | ||||
| 
 | ||||
|     push @{$self->{lag_history}}, [$self->{ping_send_time}[0], $elapsed * 1000]; | ||||
| 
 | ||||
|     my $len = @{$self->{lag_history}}; | ||||
| 
 | ||||
|     my $lag_history_max = $self->{pbot}->{registry}->get_value('lagchecker', 'lag_history_max'); | ||||
| 
 | ||||
|     while ($len > $lag_history_max) { | ||||
|         shift @{$self->{lag_history}}; | ||||
|         $len--; | ||||
|     } | ||||
| 
 | ||||
|     $self->{lag_string} = ''; | ||||
| 
 | ||||
|     my @entries; | ||||
|     my $lag_total = 0; | ||||
| 
 | ||||
|     foreach my $entry (@{$self->{lag_history}}) { | ||||
|         my ($send_time, $lag_result) = @$entry; | ||||
| 
 | ||||
|         $lag_total += $lag_result; | ||||
| 
 | ||||
|         my $ago = concise ago(gettimeofday - $send_time); | ||||
| 
 | ||||
|         push @entries, "[$ago] " . sprintf "%.1f ms", $lag_result; | ||||
|     } | ||||
| 
 | ||||
|     $self->{lag_string} = join '; ', @entries; | ||||
| 
 | ||||
|     $self->{lag_average} = $lag_total / $len; | ||||
| 
 | ||||
|     $self->{lag_string} .= "; average: " . sprintf "%.1f ms", $self->{lag_average}; | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub lagging { | ||||
|     my ($self) = @_; | ||||
| 
 | ||||
|     if (defined $self->{pong_received} and $self->{pong_received} == 0) { | ||||
|         # a ping has been sent (pong_received is not undef) and no pong has been received yet | ||||
|         my $elapsed = tv_interval($self->{ping_send_time}); | ||||
| 
 | ||||
|         return $elapsed >= $self->{pbot}->{registry}->get_value('lagchecker', 'lag_threshold'); | ||||
|     } | ||||
| 
 | ||||
|     return 0 if not defined $self->{lag_average}; | ||||
| 
 | ||||
|     return $self->{lag_average} >= $self->{pbot}->{registry}->get_value('lagchecker', 'lag_threshold'); | ||||
| } | ||||
| 
 | ||||
| sub lagstring { | ||||
|     my ($self) = @_; | ||||
| 
 | ||||
|     my $lag = $self->{lag_string} || "initializing"; | ||||
| 
 | ||||
|     return $lag; | ||||
| } | ||||
| 
 | ||||
| 1; | ||||
|  | ||||
							
								
								
									
										216
									
								
								PBot/MiscCommands.pm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								PBot/MiscCommands.pm
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,216 @@ | ||||
| # File: Commands.pm | ||||
| # | ||||
| # Author: pragma_ | ||||
| # | ||||
| # Purpose: Registers misc PBot commands. | ||||
| 
 | ||||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||
| 
 | ||||
| package PBot::MiscCommands; | ||||
| use parent 'PBot::Class'; | ||||
| 
 | ||||
| use warnings; use strict; | ||||
| use feature 'unicode_strings'; | ||||
| 
 | ||||
| use Time::Duration qw/duration/; | ||||
| 
 | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
| 
 | ||||
|     # misc commands | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_nop(@_) },        'nop',     0); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_uptime(@_) },     'uptime',  0); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_in_channel(@_) }, 'in',      0); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_list(@_) },       'list',    0); | ||||
| 
 | ||||
|     # misc administrative commands | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_sl(@_) },         'sl',      1); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_die(@_) },        'die',     1); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_export(@_) },     'export',  1); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_eval(@_) },       'eval',    1); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_reload(@_) },     'reload',  1); | ||||
| 
 | ||||
|     # misc capabilities | ||||
|     $self->{pbot}->{capabilities}->add('admin', 'can-in', 1); | ||||
| } | ||||
| 
 | ||||
| sub cmd_nop { | ||||
|     my ($self, $context) = @_; | ||||
|     $self->{pbot}->{logger}->log("Disregarding NOP command.\n"); | ||||
|     return ''; | ||||
| } | ||||
| 
 | ||||
| sub cmd_uptime { | ||||
|     my ($self, $context) = @_; | ||||
|     return localtime($self->{pbot}->{startup_timestamp}) . ' [' . duration(time - $self->{pbot}->{startup_timestamp}) . ']'; | ||||
| } | ||||
| 
 | ||||
| sub cmd_in_channel { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $usage = 'Usage: in <channel> <command>'; | ||||
|     return $usage if not length $context->{arguments}; | ||||
| 
 | ||||
|     my ($channel, $command) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2, 0, 1); | ||||
|     return $usage if not defined $channel or not defined $command; | ||||
| 
 | ||||
|     if (not $self->{pbot}->{nicklist}->is_present($channel, $context->{nick})) { | ||||
|         return "You must be present in $channel to do this."; | ||||
|     } | ||||
| 
 | ||||
|     $context->{from}    = $channel; | ||||
|     $context->{command} = $command; | ||||
|     return $self->{pbot}->{interpreter}->interpret($context); | ||||
| } | ||||
| 
 | ||||
| sub cmd_list { | ||||
|     my ($self, $context) = @_; | ||||
|     my $text; | ||||
| 
 | ||||
|     my $usage = 'Usage: list <modules|commands>'; | ||||
| 
 | ||||
|     return $usage if not length $context->{arguments}; | ||||
| 
 | ||||
|     if ($context->{arguments} =~ /^modules$/i) { | ||||
|         $text = 'Loaded modules: '; | ||||
|         foreach my $channel (sort $self->{pbot}->{factoids}->{factoids}->get_keys) { | ||||
|             foreach my $command (sort $self->{pbot}->{factoids}->{factoids}->get_keys($channel)) { | ||||
|                 next if $command eq '_name'; | ||||
|                 if ($self->{pbot}->{factoids}->{factoids}->get_data($channel, $command, 'type') eq 'module') { | ||||
|                     $text .= $self->{pbot}->{factoids}->{factoids}->get_data($channel, $command, '_name') . ' '; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $text; | ||||
|     } | ||||
| 
 | ||||
|     if ($context->{arguments} =~ /^commands$/i) { | ||||
|         $text = 'Registered commands: '; | ||||
|         foreach my $command (sort { $a->{name} cmp $b->{name} } @{$self->{pbot}->{commands}->{handlers}}) { | ||||
|             if ($command->{requires_cap}) { | ||||
|                 $text .= "+$command->{name} "; | ||||
|             } else { | ||||
|                 $text .= "$command->{name} "; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $text; | ||||
|     } | ||||
| 
 | ||||
|     return $usage; | ||||
| } | ||||
| 
 | ||||
| sub cmd_sl { | ||||
|     my ($self, $context) = @_; | ||||
|     return "Usage: sl <ircd command>" if not length $context->{arguments}; | ||||
|     $self->{pbot}->{conn}->sl($context->{arguments}); | ||||
|     return "/msg $context->{nick} sl: command sent. See log for result."; | ||||
| } | ||||
| 
 | ||||
| sub cmd_die { | ||||
|     my ($self, $context) = @_; | ||||
|     $self->{pbot}->{logger}->log("$context->{hostmask} made me exit.\n"); | ||||
|     $self->{pbot}->{conn}->privmsg($context->{from}, "Good-bye.") if $context->{from} ne 'stdin@pbot'; | ||||
|     $self->{pbot}->{conn}->quit("Departure requested.") if defined $self->{pbot}->{conn}; | ||||
|     $self->pbot->atexit(); | ||||
|     exit 0; | ||||
| } | ||||
| 
 | ||||
| sub cmd_export { | ||||
|     my ($self, $context) = @_; | ||||
|     return "Usage: export <factoids>" if not length $context->{arguments}; | ||||
|     if ($context->{arguments} =~ /^factoids$/i) { return $self->{pbot}->{factoids}->export_factoids; } | ||||
| } | ||||
| 
 | ||||
| sub cmd_eval { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     $self->{pbot}->{logger}->log("eval: $context->{from} $context->{hostmask} evaluating `$context->{arguments}`\n"); | ||||
| 
 | ||||
|     my $ret    = ''; | ||||
|     my $result = eval $context->{arguments}; | ||||
|     if ($@) { | ||||
|         if   (length $result) { $ret .= "[Error: $@] "; } | ||||
|         else                  { $ret .= "Error: $@"; } | ||||
|         $ret =~ s/ at \(eval \d+\) line 1.//; | ||||
|     } | ||||
|     $result = 'Undefined.' if not defined $result; | ||||
|     $result = 'No output.' if not length $result; | ||||
|     return "/say $ret $result"; | ||||
| } | ||||
| 
 | ||||
| sub cmd_reload { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my %reloadables = ( | ||||
|         'capabilities' => sub { | ||||
|             $self->{pbot}->{capabilities}->{caps}->load; | ||||
|             return "Capabilities reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'commands' => sub { | ||||
|             $self->{pbot}->{commands}->{metadata}->load; | ||||
|             return "Commands metadata reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'blacklist' => sub { | ||||
|             $self->{pbot}->{blacklist}->clear_blacklist; | ||||
|             $self->{pbot}->{blacklist}->load_blacklist; | ||||
|             return "Blacklist reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'ban-exemptions' => sub { | ||||
|             $self->{pbot}->{antiflood}->{'ban-exemptions'}->load; | ||||
|             return "Ban exemptions reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'ignores' => sub { | ||||
|             $self->{pbot}->{ignorelist}->{ignorelist}->load; | ||||
|             return "Ignore list reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'users' => sub { | ||||
|             $self->{pbot}->{users}->load; | ||||
|             return "Users reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'channels' => sub { | ||||
|             $self->{pbot}->{channels}->{channels}->load; | ||||
|             return "Channels reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'banlist' => sub { | ||||
|             $self->{pbot}->{timer}->dequeue_event('unban #.*'); | ||||
|             $self->{pbot}->{timer}->dequeue_event('unmute #.*'); | ||||
|             $self->{pbot}->{banlist}->{banlist}->load; | ||||
|             $self->{pbot}->{banlist}->{quietlist}->load; | ||||
|             $self->{pbot}->{banlist}->enqueue_timeouts($self->{pbot}->{banlist}->{banlist},   'b'); | ||||
|             $self->{pbot}->{banlist}->enqueue_timeouts($self->{pbot}->{banlist}->{quietlist}, 'q'); | ||||
|             return "Ban list reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'registry' => sub { | ||||
|             $self->{pbot}->{registry}->load; | ||||
|             return "Registry reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'factoids' => sub { | ||||
|             $self->{pbot}->{factoids}->load_factoids; | ||||
|             return "Factoids reloaded."; | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     if (not length $context->{arguments} or not exists $reloadables{$context->{arguments}}) { | ||||
|         my $usage = 'Usage: reload <'; | ||||
|         $usage .= join '|', sort keys %reloadables; | ||||
|         $usage .= '>'; | ||||
|         return $usage; | ||||
|     } | ||||
| 
 | ||||
|     return $reloadables{$context->{arguments}}(); | ||||
| } | ||||
| 
 | ||||
| 1; | ||||
							
								
								
									
										183
									
								
								PBot/NickList.pm
									
									
									
									
									
								
							
							
						
						
									
										183
									
								
								PBot/NickList.pm
									
									
									
									
									
								
							| @ -27,11 +27,18 @@ use Getopt::Long qw/GetOptionsFromArray/; | ||||
| 
 | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
| 
 | ||||
|     # nicklist hash | ||||
|     $self->{nicklist} = {}; | ||||
| 
 | ||||
|     # nicklist debug registry entry | ||||
|     $self->{pbot}->{registry}->add_default('text', 'nicklist', 'debug', '0'); | ||||
| 
 | ||||
|     # nicklist bot command | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_nicklist(@_) }, "nicklist", 1); | ||||
| 
 | ||||
|     # handlers for various IRC events | ||||
|     # TODO: track mode changes to update user flags | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('irc.namreply', sub { $self->on_namreply(@_) }); | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('irc.join',     sub { $self->on_join(@_) }); | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('irc.part',     sub { $self->on_part(@_) }); | ||||
| @ -42,15 +49,14 @@ sub initialize { | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('irc.caction',  sub { $self->on_activity(@_) }); | ||||
| 
 | ||||
|     # handlers for the bot itself joining/leaving channels | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('pbot.join', sub { $self->on_join_channel(@_) }); | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('pbot.part', sub { $self->on_part_channel(@_) }); | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('pbot.join', sub { $self->on_self_join(@_) }); | ||||
|     $self->{pbot}->{event_dispatcher}->register_handler('pbot.part', sub { $self->on_self_part(@_) }); | ||||
| } | ||||
| 
 | ||||
| sub cmd_nicklist { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $nicklist; | ||||
|     my $usage = "Usage: nicklist (<channel [nick]> | <nick>) [-sort <by>] [-hostmask] [-join]; -hostmask to show hostmasks instead of nicks; -join to include join time"; | ||||
|     my $usage = "Usage: nicklist (<channel [nick]> | <nick>) [-sort <by>] [-hostmask] [-join]; -hostmask shows hostmasks instead of nicks; -join includes join time"; | ||||
| 
 | ||||
|     my $getopt_error; | ||||
|     local $SIG{__WARN__} = sub { | ||||
| @ -60,17 +66,19 @@ sub cmd_nicklist { | ||||
| 
 | ||||
|     Getopt::Long::Configure("bundling_override"); | ||||
| 
 | ||||
|     my $sort_method = 'nick'; | ||||
|     my $sort_method   = 'nick'; | ||||
|     my $full_hostmask = 0; | ||||
|     my $include_join = 0; | ||||
|     my $include_join  = 0; | ||||
| 
 | ||||
|     my @args = $self->{pbot}->{interpreter}->split_line($context->{arguments}, strip_quotes => 1); | ||||
| 
 | ||||
|     GetOptionsFromArray( | ||||
|         \@args, | ||||
|         'sort|s=s'     => \$sort_method, | ||||
|         'hostmask|hm'  => \$full_hostmask, | ||||
|         'join|j'       => \$include_join, | ||||
|     ); | ||||
| 
 | ||||
|     return "$getopt_error; $usage" if defined $getopt_error; | ||||
|     return "Too many arguments -- $usage" if @args > 2; | ||||
|     return $usage if @args == 0 or not length $args[0]; | ||||
| @ -110,6 +118,7 @@ sub cmd_nicklist { | ||||
|     ); | ||||
| 
 | ||||
|     my $sort_direction = '+'; | ||||
| 
 | ||||
|     if ($sort_method =~ s/^(\+|\-)//) { | ||||
|         $sort_direction = $1; | ||||
|     } | ||||
| @ -118,87 +127,96 @@ sub cmd_nicklist { | ||||
|         return "Invalid sort method '$sort_method'; valid methods are: " . join(', ', sort keys %sort) . "; prefix with - to invert sort direction."; | ||||
|     } | ||||
| 
 | ||||
|     # insert from channel as first argument if first argument is not a channel | ||||
|     if ($args[0] !~ /^#/) { | ||||
|         unshift @args, $context->{from}; | ||||
|     } | ||||
| 
 | ||||
|     # ensure channel has a nicklist | ||||
|     if (not exists $self->{nicklist}->{lc $args[0]}) { | ||||
|         return "No nicklist for channel $args[0]."; | ||||
|     } | ||||
| 
 | ||||
|     my $result; | ||||
| 
 | ||||
|     if (@args == 1) { | ||||
|         if (not exists $self->{nicklist}->{lc $args[0]}) { | ||||
|             return "No nicklist for channel $args[0]."; | ||||
|         } | ||||
|         # nicklist for a specific channel | ||||
| 
 | ||||
|         my $count = keys %{$self->{nicklist}->{lc $args[0]}}; | ||||
|         $nicklist = "$count nick" . ($count == 1 ? '' : 's') . " in $args[0]:\n"; | ||||
| 
 | ||||
|         $result = "$count nick" . ($count == 1 ? '' : 's') . " in $args[0]:\n"; | ||||
| 
 | ||||
|         foreach my $entry (sort { $sort{$sort_method}->($self->{nicklist}->{lc $args[0]}, $sort_direction) } keys %{$self->{nicklist}->{lc $args[0]}}) { | ||||
|             if ($full_hostmask) { | ||||
|                 $nicklist .= "  $self->{nicklist}->{lc $args[0]}->{$entry}->{hostmask}"; | ||||
|                 $result .= "  $self->{nicklist}->{lc $args[0]}->{$entry}->{hostmask}"; | ||||
|             } else { | ||||
|                 $nicklist .= "  $self->{nicklist}->{lc $args[0]}->{$entry}->{nick}"; | ||||
|                 $result .= "  $self->{nicklist}->{lc $args[0]}->{$entry}->{nick}"; | ||||
|             } | ||||
| 
 | ||||
|             my $sep = ': '; | ||||
| 
 | ||||
|             if ($self->{nicklist}->{lc $args[0]}->{$entry}->{timestamp} > 0) { | ||||
|                 my $duration = concise ago (gettimeofday - $self->{nicklist}->{lc $args[0]}->{$entry}->{timestamp}); | ||||
|                 $nicklist .= "${sep}last spoken $duration"; | ||||
|                 $result .= "${sep}last spoken $duration"; | ||||
|                 $sep = ', '; | ||||
|             } | ||||
| 
 | ||||
|             if ($include_join and $self->{nicklist}->{lc $args[0]}->{$entry}->{join} > 0) { | ||||
|                 my $duration = concise ago (gettimeofday - $self->{nicklist}->{lc $args[0]}->{$entry}->{join}); | ||||
|                 $nicklist .= "${sep}joined $duration"; | ||||
|                 $result .= "${sep}joined $duration"; | ||||
|                 $sep = ', '; | ||||
|             } | ||||
| 
 | ||||
|             foreach my $key (sort keys %{$self->{nicklist}->{lc $args[0]}->{$entry}}) { | ||||
|                 next if grep { $key eq $_ } qw/nick user host join timestamp hostmask/; | ||||
|                 if ($self->{nicklist}->{lc $args[0]}->{$entry}->{$key} == 1) { | ||||
|                     $nicklist .= "$sep$key"; | ||||
|                     $result .= "$sep$key"; | ||||
|                 } else { | ||||
|                     $nicklist .= "$sep$key => $self->{nicklist}->{lc $args[0]}->{$entry}->{$key}"; | ||||
|                     $result .= "$sep$key => $self->{nicklist}->{lc $args[0]}->{$entry}->{$key}"; | ||||
|                 } | ||||
|                 $sep = ', '; | ||||
|             } | ||||
|             $nicklist .= "\n"; | ||||
|             $result .= "\n"; | ||||
|         } | ||||
|     } else { | ||||
|         if (not exists $self->{nicklist}->{lc $args[0]}) { | ||||
|             return "No nicklist for channel $args[0]."; | ||||
|         } elsif (not exists $self->{nicklist}->{lc $args[0]}->{lc $args[1]}) { | ||||
|         # nicklist for a specific user | ||||
| 
 | ||||
|         if (not exists $self->{nicklist}->{lc $args[0]}->{lc $args[1]}) { | ||||
|             return "No such nick $args[1] in channel $args[0]."; | ||||
|         } | ||||
| 
 | ||||
|         $nicklist = "Nicklist information for $self->{nicklist}->{lc $args[0]}->{lc $args[1]}->{hostmask} in $args[0]: "; | ||||
|         $result = "Nicklist information for $self->{nicklist}->{lc $args[0]}->{lc $args[1]}->{hostmask} in $args[0]: "; | ||||
|         my $sep = ''; | ||||
| 
 | ||||
|         if ($self->{nicklist}->{lc $args[0]}->{lc $args[1]}->{timestamp} > 0) { | ||||
|             my $duration = concise ago (gettimeofday - $self->{nicklist}->{lc $args[0]}->{lc $args[1]}->{timestamp}); | ||||
|             $nicklist .= "last spoken $duration"; | ||||
|             $result .= "last spoken $duration"; | ||||
|             $sep = ', '; | ||||
|         } | ||||
| 
 | ||||
|         if ($self->{nicklist}->{lc $args[0]}->{lc $args[1]}->{join} > 0) { | ||||
|             my $duration = concise ago (gettimeofday - $self->{nicklist}->{lc $args[0]}->{lc $args[1]}->{join}); | ||||
|             $nicklist .= "${sep}joined $duration"; | ||||
|             $result .= "${sep}joined $duration"; | ||||
|             $sep = ', '; | ||||
|         } | ||||
| 
 | ||||
|         foreach my $key (sort keys %{$self->{nicklist}->{lc $args[0]}->{lc $args[1]}}) { | ||||
|             next if grep { $key eq $_ } qw/nick user host join timestamp hostmask/; | ||||
|             $nicklist .= "$sep$key => $self->{nicklist}->{lc $args[0]}->{lc $args[1]}->{$key}"; | ||||
|             $result .= "$sep$key => $self->{nicklist}->{lc $args[0]}->{lc $args[1]}->{$key}"; | ||||
|             $sep = ', '; | ||||
|         } | ||||
| 
 | ||||
|         $nicklist .= 'no details' if $sep eq ''; | ||||
|         $result .= 'no details' if $sep eq ''; | ||||
|     } | ||||
| 
 | ||||
|     return $nicklist; | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| sub update_timestamp { | ||||
|     my ($self, $channel, $nick) = @_; | ||||
| 
 | ||||
|     my $orig_nick = $nick; | ||||
| 
 | ||||
|     $channel = lc $channel; | ||||
|     $nick    = lc $nick; | ||||
| 
 | ||||
| @ -216,6 +234,7 @@ sub remove_channel { | ||||
| 
 | ||||
| sub add_nick { | ||||
|     my ($self, $channel, $nick) = @_; | ||||
| 
 | ||||
|     if (not exists $self->{nicklist}->{lc $channel}->{lc $nick}) { | ||||
|         $self->{pbot}->{logger}->log("Adding nick '$nick' to channel '$channel'\n") if $self->{pbot}->{registry}->get_value('nicklist', 'debug'); | ||||
|         $self->{nicklist}->{lc $channel}->{lc $nick} = {nick => $nick, timestamp => 0}; | ||||
| @ -230,12 +249,15 @@ sub remove_nick { | ||||
| 
 | ||||
| sub get_channels { | ||||
|     my ($self, $nick) = @_; | ||||
|     my @channels; | ||||
| 
 | ||||
|     $nick = lc $nick; | ||||
| 
 | ||||
|     my @channels; | ||||
| 
 | ||||
|     foreach my $channel (keys %{$self->{nicklist}}) { | ||||
|         if (exists $self->{nicklist}->{$channel}->{$nick}) { push @channels, $channel; } | ||||
|         if (exists $self->{nicklist}->{$channel}->{$nick}) { | ||||
|             push @channels, $channel; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return \@channels; | ||||
| @ -243,10 +265,17 @@ sub get_channels { | ||||
| 
 | ||||
| sub get_nicks { | ||||
|     my ($self, $channel) = @_; | ||||
| 
 | ||||
|     $channel = lc $channel; | ||||
| 
 | ||||
|     my @nicks; | ||||
| 
 | ||||
|     return @nicks if not exists $self->{nicklist}->{$channel}; | ||||
|     foreach my $nick (keys %{$self->{nicklist}->{$channel}}) { push @nicks, $self->{nicklist}->{$channel}->{$nick}->{nick}; } | ||||
| 
 | ||||
|     foreach my $nick (keys %{$self->{nicklist}->{$channel}}) { | ||||
|         push @nicks, $self->{nicklist}->{$channel}->{$nick}->{nick}; | ||||
|     } | ||||
| 
 | ||||
|     return @nicks; | ||||
| } | ||||
| 
 | ||||
| @ -259,16 +288,19 @@ sub set_meta { | ||||
|     if (not exists $self->{nicklist}->{$channel} or not exists $self->{nicklist}->{$channel}->{$nick}) { | ||||
|         if (exists $self->{nicklist}->{$channel} and $nick =~ m/[*?]/) { | ||||
|             my $regex = quotemeta $nick; | ||||
| 
 | ||||
|             $regex =~ s/\\\*/.*?/g; | ||||
|             $regex =~ s/\\\?/./g; | ||||
| 
 | ||||
|             my $found = 0; | ||||
| 
 | ||||
|             foreach my $n (keys %{$self->{nicklist}->{$channel}}) { | ||||
|                 if (exists $self->{nicklist}->{$channel}->{$n}->{hostmask} and $self->{nicklist}->{$channel}->{$n}->{hostmask} =~ m/$regex/i) { | ||||
|                     $self->{nicklist}->{$channel}->{$n}->{$key} = $value; | ||||
|                     $found++; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return $found; | ||||
|         } else { | ||||
|             $self->{pbot}->{logger}->log("Nicklist: Attempt to set invalid meta ($key => $value) for $nick in $channel.\n"); | ||||
| @ -289,6 +321,7 @@ sub delete_meta { | ||||
|     if (not exists $self->{nicklist}->{$channel} or not exists $self->{nicklist}->{$channel}->{$nick} or not exists $self->{nicklist}->{$channel}->{$nick}->{$key}) { | ||||
|         return undef; | ||||
|     } | ||||
| 
 | ||||
|     return delete $self->{nicklist}->{$channel}->{$nick}->{$key}; | ||||
| } | ||||
| 
 | ||||
| @ -311,8 +344,11 @@ sub is_present_any_channel { | ||||
|     $nick = lc $nick; | ||||
| 
 | ||||
|     foreach my $channel (keys %{$self->{nicklist}}) { | ||||
|         if (exists $self->{nicklist}->{$channel}->{$nick}) { return $self->{nicklist}->{$channel}->{$nick}->{nick}; } | ||||
|         if (exists $self->{nicklist}->{$channel}->{$nick}) { | ||||
|             return $self->{nicklist}->{$channel}->{$nick}->{nick}; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| @ -322,8 +358,11 @@ sub is_present { | ||||
|     $channel = lc $channel; | ||||
|     $nick    = lc $nick; | ||||
| 
 | ||||
|     if   (exists $self->{nicklist}->{$channel} and exists $self->{nicklist}->{$channel}->{$nick}) { return $self->{nicklist}->{$channel}->{$nick}->{nick}; } | ||||
|     else                                                                                          { return 0; } | ||||
|     if (exists $self->{nicklist}->{$channel} and exists $self->{nicklist}->{$channel}->{$nick}) { | ||||
|         return $self->{nicklist}->{$channel}->{$nick}->{nick}; | ||||
|     } else { | ||||
|         return 0; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| sub is_present_similar { | ||||
| @ -332,24 +371,42 @@ sub is_present_similar { | ||||
|     $channel = lc $channel; | ||||
|     $nick    = lc $nick; | ||||
| 
 | ||||
|     return 0                                              if not exists $self->{nicklist}->{$channel}; | ||||
|     return 0 if not exists $self->{nicklist}->{$channel}; | ||||
| 
 | ||||
|     return $self->{nicklist}->{$channel}->{$nick}->{nick} if $self->is_present($channel, $nick); | ||||
|     return 0                                              if $nick =~ m/(?:^\$|\s)/;                     # not nick-like | ||||
| 
 | ||||
|     my $percentage = $self->{pbot}->{registry}->get_value('interpreter', 'nick_similarity'); | ||||
|     $percentage = 0.20 if not defined $percentage; | ||||
|     if ($nick =~ m/(?:^\$|\s)/) { | ||||
|         # not nick-like | ||||
|         # TODO: why do we have this check? added log message to find out when/if it happens | ||||
|         $self->{pbot}->{logger}->log("NickList::is_present_similiar [$channel] [$nick] is not nick-like?\n"); | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     $percentage = $similar if defined $similar; | ||||
|     my $percentage; | ||||
| 
 | ||||
|     if (defined $similar) { | ||||
|         $percentage = $similar; | ||||
|     } else { | ||||
|         $percentage = $self->{pbot}->{registry}->get_value('interpreter', 'nick_similarity') // 0.20; | ||||
|     } | ||||
| 
 | ||||
|     my $now = gettimeofday; | ||||
|     foreach my $person (sort { $self->{nicklist}->{$channel}->{$b}->{timestamp} <=> $self->{nicklist}->{$channel}->{$a}->{timestamp} } keys %{$self->{nicklist}->{$channel}}) | ||||
|     { | ||||
|         return 0 if $now - $self->{nicklist}->{$channel}->{$person}->{timestamp} > 3600;                 # 1 hour | ||||
| 
 | ||||
|     foreach my $person (sort { $self->{nicklist}->{$channel}->{$b}->{timestamp} <=> $self->{nicklist}->{$channel}->{$a}->{timestamp} } keys %{$self->{nicklist}->{$channel}}) { | ||||
|         if ($now - $self->{nicklist}->{$channel}->{$person}->{timestamp} > 3600) { | ||||
|             # if it has been 1 hour since this person has last spoken, the similar nick | ||||
|             # is probably not intended for them. | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         my $distance = fastdistance($nick, $person); | ||||
|         my $length   = length $nick > length $person ? length $nick : length $person; | ||||
| 
 | ||||
|         if ($length != 0 && $distance / $length <= $percentage) { return $self->{nicklist}->{$channel}->{$person}->{nick}; } | ||||
|         if ($length != 0 && $distance / $length <= $percentage) { | ||||
|             return $self->{nicklist}->{$channel}->{$person}->{nick}; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| @ -360,9 +417,14 @@ sub random_nick { | ||||
| 
 | ||||
|     if (exists $self->{nicklist}->{$channel}) { | ||||
|         my $now   = gettimeofday; | ||||
| 
 | ||||
|         # build list of nicks that have spoken within the last 2 hours | ||||
|         my @nicks = grep { $now - $self->{nicklist}->{$channel}->{$_}->{timestamp} < 3600 * 2 } keys %{$self->{nicklist}->{$channel}}; | ||||
| 
 | ||||
|         # pick a random nick from tha list | ||||
|         my $nick = $nicks[rand @nicks]; | ||||
| 
 | ||||
|         # return its canonical name | ||||
|         return $self->{nicklist}->{$channel}->{$nick}->{nick}; | ||||
|     } else { | ||||
|         return undef; | ||||
| @ -375,7 +437,9 @@ sub on_namreply { | ||||
| 
 | ||||
|     foreach my $nick (split ' ', $nicks) { | ||||
|         my $stripped_nick = $nick; | ||||
| 
 | ||||
|         $stripped_nick =~ s/^[@+%]//g;    # remove OP/Voice/etc indicator from nick | ||||
| 
 | ||||
|         $self->add_nick($channel, $stripped_nick); | ||||
| 
 | ||||
|         my ($account_id, $hostmask) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($stripped_nick); | ||||
| @ -393,48 +457,66 @@ sub on_namreply { | ||||
| 
 | ||||
|         if ($nick =~ m/\%/) { $self->set_meta($channel, $stripped_nick, '+h', 1); } | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub on_activity { | ||||
|     my ($self, $event_type, $event) = @_; | ||||
| 
 | ||||
|     my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->{to}[0]); | ||||
| 
 | ||||
|     $self->update_timestamp($channel, $nick); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub on_join { | ||||
|     my ($self, $event_type, $event) = @_; | ||||
| 
 | ||||
|     my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to); | ||||
| 
 | ||||
|     $self->add_nick($channel, $nick); | ||||
| 
 | ||||
|     $self->set_meta($channel, $nick, 'hostmask', "$nick!$user\@$host"); | ||||
|     $self->set_meta($channel, $nick, 'user',     $user); | ||||
|     $self->set_meta($channel, $nick, 'host',     $host); | ||||
|     $self->set_meta($channel, $nick, 'join',     gettimeofday); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub on_part { | ||||
|     my ($self, $event_type, $event) = @_; | ||||
| 
 | ||||
|     my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to); | ||||
| 
 | ||||
|     $self->remove_nick($channel, $nick); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub on_quit { | ||||
|     my ($self, $event_type, $event) = @_; | ||||
|     my ($nick, $user,       $host)  = ($event->{event}->nick, $event->{event}->user, $event->{event}->host); | ||||
| 
 | ||||
|     my ($nick, $user, $host)  = ($event->{event}->nick, $event->{event}->user, $event->{event}->host); | ||||
| 
 | ||||
|     foreach my $channel (keys %{$self->{nicklist}}) { | ||||
|         if ($self->is_present($channel, $nick)) { $self->remove_nick($channel, $nick); } | ||||
|         if ($self->is_present($channel, $nick)) { | ||||
|             $self->remove_nick($channel, $nick); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub on_kick { | ||||
|     my ($self, $event_type, $event) = @_; | ||||
| 
 | ||||
|     my ($nick, $channel) = ($event->{event}->to, $event->{event}->{args}[0]); | ||||
| 
 | ||||
|     $self->remove_nick($channel, $nick); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| @ -445,23 +527,30 @@ sub on_nickchange { | ||||
|     foreach my $channel (keys %{$self->{nicklist}}) { | ||||
|         if ($self->is_present($channel, $nick)) { | ||||
|             my $meta = delete $self->{nicklist}->{$channel}->{lc $nick}; | ||||
|             $meta->{nick}                                = $newnick; | ||||
|             $meta->{timestamp}                           = gettimeofday; | ||||
| 
 | ||||
|             $meta->{nick}      = $newnick; | ||||
|             $meta->{timestamp} = gettimeofday; | ||||
| 
 | ||||
|             $self->{nicklist}->{$channel}->{lc $newnick} = $meta; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub on_join_channel { | ||||
| sub on_self_join { | ||||
|     my ($self, $event_type, $event) = @_; | ||||
| 
 | ||||
|     $self->remove_channel($event->{channel});    # clear nicklist to remove any stale nicks before repopulating with namreplies | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| sub on_part_channel { | ||||
| sub on_self_part { | ||||
|     my ($self, $event_type, $event) = @_; | ||||
| 
 | ||||
|     $self->remove_channel($event->{channel}); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										444
									
								
								PBot/PBot.pm
									
									
									
									
									
								
							
							
						
						
									
										444
									
								
								PBot/PBot.pm
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| # File: PBot.pm | ||||
| # Author: pragma_ | ||||
| # | ||||
| # Purpose: IRC Bot (3rd generation) | ||||
| # Purpose: IRC Bot | ||||
| 
 | ||||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
| @ -11,6 +11,7 @@ package PBot::PBot; | ||||
| 
 | ||||
| use strict; use warnings; | ||||
| use feature 'unicode_strings'; | ||||
| use utf8; | ||||
| 
 | ||||
| # unbuffer stdout | ||||
| STDOUT->autoflush(1); | ||||
| @ -18,39 +19,40 @@ STDOUT->autoflush(1); | ||||
| use Carp (); | ||||
| use PBot::Logger; | ||||
| use PBot::VERSION; | ||||
| use PBot::HashObject; | ||||
| use PBot::DualIndexHashObject; | ||||
| use PBot::DualIndexSQLiteObject; | ||||
| use PBot::Registry; | ||||
| use PBot::Capabilities; | ||||
| use PBot::SelectHandler; | ||||
| use PBot::StdinReader; | ||||
| use PBot::IRC; | ||||
| use PBot::EventDispatcher; | ||||
| use PBot::IRCHandlers; | ||||
| use PBot::Channels; | ||||
| use PBot::BanList; | ||||
| use PBot::NickList; | ||||
| use PBot::LagChecker; | ||||
| use PBot::MessageHistory; | ||||
| use PBot::AntiFlood; | ||||
| use PBot::AntiSpam; | ||||
| use PBot::Interpreter; | ||||
| use PBot::Commands; | ||||
| use PBot::ChanOps; | ||||
| use PBot::Factoids; | ||||
| use PBot::Users; | ||||
| use PBot::IgnoreList; | ||||
| use PBot::BanList; | ||||
| use PBot::BlackList; | ||||
| use PBot::Timer; | ||||
| use PBot::Refresher; | ||||
| use PBot::WebPaste; | ||||
| use PBot::Utils::ParseDate; | ||||
| use PBot::Plugins; | ||||
| use PBot::Capabilities; | ||||
| use PBot::Commands; | ||||
| use PBot::Channels; | ||||
| use PBot::ChanOps; | ||||
| use PBot::DualIndexHashObject; | ||||
| use PBot::DualIndexSQLiteObject; | ||||
| use PBot::EventDispatcher; | ||||
| use PBot::Factoids; | ||||
| use PBot::Functions; | ||||
| use PBot::HashObject; | ||||
| use PBot::IgnoreList; | ||||
| use PBot::Interpreter; | ||||
| use PBot::IRC; | ||||
| use PBot::IRCHandlers; | ||||
| use PBot::LagChecker; | ||||
| use PBot::MessageHistory; | ||||
| use PBot::Modules; | ||||
| use PBot::MiscCommands; | ||||
| use PBot::NickList; | ||||
| use PBot::Plugins; | ||||
| use PBot::ProcessManager; | ||||
| use PBot::Registry; | ||||
| use PBot::Refresher; | ||||
| use PBot::SelectHandler; | ||||
| use PBot::StdinReader; | ||||
| use PBot::Timer; | ||||
| use PBot::Updater; | ||||
| use PBot::Users; | ||||
| use PBot::Utils::ParseDate; | ||||
| use PBot::WebPaste; | ||||
| 
 | ||||
| sub new { | ||||
|     my ($proto, %conf) = @_; | ||||
| @ -62,24 +64,20 @@ sub new { | ||||
| 
 | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
| 
 | ||||
|     $self->{startup_timestamp} = time; | ||||
| 
 | ||||
|     my $data_dir   = $conf{data_dir}; | ||||
|     my $module_dir = $conf{module_dir}; | ||||
|     my $plugin_dir = $conf{plugin_dir}; | ||||
|     my $update_dir = $conf{update_dir}; | ||||
| 
 | ||||
|     # process command-line arguments | ||||
|     # process command-line arguments for path and registry overrides | ||||
|     foreach my $arg (@ARGV) { | ||||
|         if ($arg =~ m/^-?(?:general\.)?((?:data|module|plugin|update)_dir)=(.*)$/) { | ||||
|             # check command-line arguments for directory overrides | ||||
|             my $override = $1; | ||||
|             my $value    = $2; | ||||
|             $value =~ s/[\\\/]$//; # strip trailing directory separator | ||||
|             $data_dir    = $value if $override eq 'data_dir'; | ||||
|             $module_dir  = $value if $override eq 'module_dir'; | ||||
|             $plugin_dir  = $value if $override eq 'plugin_dir'; | ||||
|             $update_dir  = $value if $override eq 'update_dir'; | ||||
|             $conf{data_dir}    = $value if $override eq 'data_dir'; | ||||
|             $conf{module_dir}  = $value if $override eq 'module_dir'; | ||||
|             $conf{plugin_dir}  = $value if $override eq 'plugin_dir'; | ||||
|             $conf{update_dir}  = $value if $override eq 'update_dir'; | ||||
|         } else { | ||||
|             # check command-line arguments for registry overrides | ||||
|             my ($item, $value) = split /=/, $arg, 2; | ||||
| @ -90,6 +88,7 @@ sub initialize { | ||||
|             } | ||||
| 
 | ||||
|             my ($section, $key) = split /\./, $item, 2; | ||||
| 
 | ||||
|             if (not defined $section or not defined $key) { | ||||
|                 print STDERR "Fatal error: bad argument `$arg`; registry entries must be in the form of section.key (e.g.: irc.botnick)\n"; | ||||
|                 exit; | ||||
| @ -100,163 +99,100 @@ sub initialize { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     # make sure the data directory exists | ||||
|     if (not -d $data_dir) { | ||||
|         print STDERR "Data directory ($data_dir) does not exist; aborting...\n"; | ||||
|         exit; | ||||
|     # make sure the paths exist | ||||
|     foreach my $path (qw/data_dir module_dir plugin_dir update_dir/) { | ||||
|         if (not -d $conf{$path}) { | ||||
|             print STDERR "$path path ($conf{$path}) does not exist; aborting.\n"; | ||||
|             exit; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     # let modules register signal handlers | ||||
|     $self->{atexit} = PBot::Registerable->new(%conf, pbot => $self); | ||||
|     # let modules register atexit subroutines | ||||
|     $self->{atexit} = PBot::Registerable->new(pbot => $self, %conf); | ||||
| 
 | ||||
|     # register default signal handlers | ||||
|     $self->register_signal_handlers; | ||||
| 
 | ||||
|     # create logger | ||||
|     $self->{logger} = PBot::Logger->new(pbot => $self, filename => "$data_dir/log/log", %conf); | ||||
|     # prepare and open logger | ||||
|     $self->{logger} = PBot::Logger->new(pbot => $self, filename => "$conf{data_dir}/log/log", %conf); | ||||
| 
 | ||||
|     # make sure the rest of the environment is sane | ||||
|     if (not -d $module_dir) { | ||||
|         $self->{logger}->log("Modules directory ($module_dir) does not exist; aborting...\n"); | ||||
|         exit; | ||||
|     } | ||||
|     # log command-line arguments | ||||
|     $self->{logger}->log("Args: @ARGV\n") if @ARGV; | ||||
| 
 | ||||
|     if (not -d $plugin_dir) { | ||||
|         $self->{logger}->log("Plugins directory ($plugin_dir) does not exist; aborting...\n"); | ||||
|         exit; | ||||
|     } | ||||
|     # log configured paths | ||||
|     $self->{logger}->log("module_dir: $conf{module_dir}\n"); | ||||
|     $self->{logger}->log("plugin_dir: $conf{plugin_dir}\n"); | ||||
|     $self->{logger}->log("  data_dir: $conf{data_dir}\n"); | ||||
|     $self->{logger}->log("update_dir: $conf{update_dir}\n"); | ||||
| 
 | ||||
|     if (not -d $update_dir) { | ||||
|         $self->{logger}->log("Updates directory ($update_dir) does not exist; aborting...\n"); | ||||
|         exit; | ||||
|     } | ||||
| 
 | ||||
|     $self->{updater} = PBot::Updater->new(pbot => $self, data_dir => $data_dir, update_dir => $update_dir); | ||||
|     # prepare the updater | ||||
|     $self->{updater} = PBot::Updater->new(pbot => $self, data_dir => $conf{data_dir}, update_dir => $conf{update_dir}); | ||||
| 
 | ||||
|     # update any data files to new locations/formats | ||||
|     # --- this must happen before any data files are opened! --- | ||||
|     if ($self->{updater}->update) { | ||||
|         $self->{logger}->log("Update failed.\n"); | ||||
|         exit 0; | ||||
|     } | ||||
| 
 | ||||
|     # create capabilities so commands can add new capabilities | ||||
|     $self->{capabilities} = PBot::Capabilities->new(pbot => $self, filename => "$data_dir/capabilities", %conf); | ||||
|     $self->{capabilities} = PBot::Capabilities->new(pbot => $self, filename => "$conf{data_dir}/capabilities", %conf); | ||||
| 
 | ||||
|     # create commands so the modules can register new commands | ||||
|     $self->{commands} = PBot::Commands->new(pbot => $self, filename => "$data_dir/commands", %conf); | ||||
|     $self->{commands} = PBot::Commands->new(pbot => $self, filename => "$conf{data_dir}/commands", %conf); | ||||
| 
 | ||||
|     # add some commands | ||||
|     $self->{commands}->register(sub { $self->cmd_list(@_) },   "list"); | ||||
|     $self->{commands}->register(sub { $self->cmd_die(@_) },    "die", 1); | ||||
|     $self->{commands}->register(sub { $self->cmd_export(@_) }, "export", 1); | ||||
|     $self->{commands}->register(sub { $self->cmd_reload(@_) }, "reload", 1); | ||||
|     $self->{commands}->register(sub { $self->cmd_eval(@_) },   "eval", 1); | ||||
|     $self->{commands}->register(sub { $self->cmd_sl(@_) },     "sl", 1); | ||||
| 
 | ||||
|     # add 'cap' capability command | ||||
|     # add 'cap' capability command here since $self->{commands} is created after $self->{capabilities} | ||||
|     $self->{commands}->register(sub { $self->{capabilities}->cmd_cap(@_) }, "cap"); | ||||
| 
 | ||||
|     # prepare the version | ||||
|     # prepare the version information and `version` command | ||||
|     $self->{version} = PBot::VERSION->new(pbot => $self, %conf); | ||||
|     $self->{logger}->log($self->{version}->version . "\n"); | ||||
|     $self->{logger}->log("Args: @ARGV\n") if @ARGV; | ||||
| 
 | ||||
|     $self->{logger}->log("module_dir: $module_dir\n"); | ||||
|     $self->{logger}->log("plugin_dir: $plugin_dir\n"); | ||||
|     $self->{logger}->log("data_dir: $data_dir\n"); | ||||
|     $self->{logger}->log("update_dir: $update_dir\n"); | ||||
| 
 | ||||
| 
 | ||||
|     $self->{timer}     = PBot::Timer->new(pbot => $self, timeout => 10, name => 'PBot Timer', %conf); | ||||
|     $self->{modules}   = PBot::Modules->new(pbot => $self, %conf); | ||||
|     $self->{functions} = PBot::Functions->new(pbot => $self, %conf); | ||||
|     $self->{refresher} = PBot::Refresher->new(pbot => $self); | ||||
| 
 | ||||
|     # create registry and set some defaults | ||||
|     $self->{registry} = PBot::Registry->new(pbot => $self, filename => "$data_dir/registry", %conf); | ||||
| 
 | ||||
|     $self->{registry}->add_default('text', 'general', 'data_dir',   $data_dir); | ||||
|     $self->{registry}->add_default('text', 'general', 'module_dir', $module_dir); | ||||
|     $self->{registry}->add_default('text', 'general', 'plugin_dir', $plugin_dir); | ||||
|     $self->{registry}->add_default('text', 'general', 'update_dir', $update_dir); | ||||
|     $self->{registry}->add_default('text', 'general', 'trigger',       $conf{trigger} // '!'); | ||||
| 
 | ||||
|     $self->{registry}->add_default('text', 'irc', 'debug',             $conf{irc_debug}         // 0); | ||||
|     $self->{registry}->add_default('text', 'irc', 'show_motd',         $conf{show_motd}         // 1); | ||||
|     $self->{registry}->add_default('text', 'irc', 'max_msg_len',       $conf{max_msg_len}       // 425); | ||||
|     $self->{registry}->add_default('text', 'irc', 'server',            $conf{server}            // "irc.freenode.net"); | ||||
|     $self->{registry}->add_default('text', 'irc', 'port',              $conf{port}              // 6667); | ||||
|     $self->{registry}->add_default('text', 'irc', 'SSL',               $conf{SSL}               // 0); | ||||
|     $self->{registry}->add_default('text', 'irc', 'SSL_ca_file',       $conf{SSL_ca_file}       // 'none'); | ||||
|     $self->{registry}->add_default('text', 'irc', 'SSL_ca_path',       $conf{SSL_ca_path}       // 'none'); | ||||
|     $self->{registry}->add_default('text', 'irc', 'botnick',           $conf{botnick}           // ""); | ||||
|     $self->{registry}->add_default('text', 'irc', 'username',          $conf{username}          // "pbot3"); | ||||
|     $self->{registry}->add_default('text', 'irc', 'realname',          $conf{realname}          // "https://github.com/pragma-/pbot"); | ||||
|     $self->{registry}->add_default('text', 'irc', 'identify_password', $conf{identify_password} // ''); | ||||
|     $self->{registry}->add_default('text', 'irc', 'log_default_handler', 1); | ||||
| 
 | ||||
|     $self->{registry}->set_default('irc', 'SSL_ca_file',       'private', 1); | ||||
|     $self->{registry}->set_default('irc', 'SSL_ca_path',       'private', 1); | ||||
|     $self->{registry}->set_default('irc', 'identify_password', 'private', 1); | ||||
| 
 | ||||
|     # load existing registry entries from file (if exists) to overwrite defaults | ||||
|     if (-e $self->{registry}->{registry}->{filename}) { $self->{registry}->load; } | ||||
| 
 | ||||
|     # update important paths | ||||
|     $self->{registry}->set('general', 'data_dir',   'value', $data_dir,      0, 1); | ||||
|     $self->{registry}->set('general', 'module_dir', 'value', $module_dir,    0, 1); | ||||
|     $self->{registry}->set('general', 'plugin_dir', 'value', $plugin_dir,    0, 1); | ||||
|     $self->{registry}->set('general', 'update_dir', 'value', $update_dir, 0, 1); | ||||
| 
 | ||||
|     # override registry entries with command-line arguments, if any | ||||
|     foreach my $override (keys %{$self->{overrides}}) { | ||||
|         my ($section, $key) = split /\./, $override; | ||||
|         my $value = $self->{overrides}->{$override}; | ||||
|         $self->{logger}->log("Overriding $section.$key to $value\n"); | ||||
|         $self->{registry}->set($section, $key, 'value', $value, 0, 1); | ||||
|     } | ||||
| 
 | ||||
|     # registry triggers fire when value changes | ||||
|     $self->{registry}->add_trigger('irc', 'botnick', sub { $self->change_botnick_trigger(@_) }); | ||||
|     $self->{registry}->add_trigger('irc', 'debug',   sub { $self->irc_debug_trigger(@_) }); | ||||
|     # prepare registry | ||||
|     $self->{registry} = PBot::Registry->new(pbot => $self, filename => "$conf{data_dir}/registry", %conf); | ||||
| 
 | ||||
|     # ensure user has attempted to configure the bot | ||||
|     if (not length $self->{registry}->get_value('irc', 'botnick')) { | ||||
|         $self->{logger}->log("Fatal error: IRC nickname not defined; please set registry key irc.botnick in $data_dir/registry to continue.\n"); | ||||
|         $self->{logger}->log("Fatal error: IRC nickname not defined; please set registry key irc.botnick in $conf{data_dir}/registry to continue.\n"); | ||||
|         exit; | ||||
|     } | ||||
| 
 | ||||
|     # prepare remaining core PBot modules -- do not change this order | ||||
|     $self->{timer} = PBot::Timer->new(pbot => $self, timeout => 10, name => 'PBot Timer', %conf); | ||||
|     $self->{event_dispatcher} = PBot::EventDispatcher->new(pbot => $self, %conf); | ||||
|     $self->{process_manager}  = PBot::ProcessManager->new(pbot => $self, %conf); | ||||
|     $self->{irchandlers}      = PBot::IRCHandlers->new(pbot => $self, %conf); | ||||
|     $self->{select_handler}   = PBot::SelectHandler->new(pbot => $self, %conf); | ||||
|     $self->{users}            = PBot::Users->new(pbot => $self, filename => "$data_dir/users", %conf); | ||||
|     $self->{stdin_reader}     = PBot::StdinReader->new(pbot => $self, %conf); | ||||
|     $self->{lagchecker}       = PBot::LagChecker->new(pbot => $self, %conf); | ||||
|     $self->{messagehistory}   = PBot::MessageHistory->new(pbot => $self, filename => "$data_dir/message_history.sqlite3", %conf); | ||||
|     $self->{users}            = PBot::Users->new(pbot => $self, filename => "$conf{data_dir}/users", %conf); | ||||
|     $self->{antiflood}        = PBot::AntiFlood->new(pbot => $self, %conf); | ||||
|     $self->{antispam}         = PBot::AntiSpam->new(pbot => $self, %conf); | ||||
|     $self->{ignorelist}       = PBot::IgnoreList->new(pbot => $self, filename => "$data_dir/ignorelist", %conf); | ||||
|     $self->{blacklist}        = PBot::BlackList->new(pbot => $self, filename => "$data_dir/blacklist", %conf); | ||||
|     $self->{irc}              = PBot::IRC->new(); | ||||
|     $self->{channels}         = PBot::Channels->new(pbot => $self, filename => "$data_dir/channels", %conf); | ||||
|     $self->{chanops}          = PBot::ChanOps->new(pbot => $self, %conf); | ||||
|     $self->{banlist}          = PBot::BanList->new(pbot => $self, %conf); | ||||
|     $self->{blacklist}        = PBot::BlackList->new(pbot => $self, filename => "$conf{data_dir}/blacklist", %conf); | ||||
|     $self->{channels}         = PBot::Channels->new(pbot => $self, filename => "$conf{data_dir}/channels", %conf); | ||||
|     $self->{chanops}          = PBot::ChanOps->new(pbot => $self, %conf); | ||||
|     $self->{factoids}         = PBot::Factoids->new(pbot => $self, filename => "$conf{data_dir}/factoids.sqlite3", %conf); | ||||
|     $self->{functions}        = PBot::Functions->new(pbot => $self, %conf); | ||||
|     $self->{refresher}        = PBot::Refresher->new(pbot => $self); | ||||
|     $self->{ignorelist}       = PBot::IgnoreList->new(pbot => $self, filename => "$conf{data_dir}/ignorelist", %conf); | ||||
|     $self->{irc}              = PBot::IRC->new(); | ||||
|     $self->{irchandlers}      = PBot::IRCHandlers->new(pbot => $self, %conf); | ||||
|     $self->{interpreter}      = PBot::Interpreter->new(pbot => $self, %conf); | ||||
|     $self->{lagchecker}       = PBot::LagChecker->new(pbot => $self, %conf); | ||||
|     $self->{misc_commands}    = PBot::MiscCommands->new(pbot => $self, %conf); | ||||
|     $self->{messagehistory}   = PBot::MessageHistory->new(pbot => $self, filename => "$conf{data_dir}/message_history.sqlite3", %conf); | ||||
|     $self->{modules}          = PBot::Modules->new(pbot => $self, %conf); | ||||
|     $self->{nicklist}         = PBot::NickList->new(pbot => $self, %conf); | ||||
|     $self->{webpaste}         = PBot::WebPaste->new(pbot => $self, %conf); | ||||
|     $self->{parsedate}        = PBot::Utils::ParseDate->new(pbot => $self, %conf); | ||||
|     $self->{plugins}          = PBot::Plugins->new(pbot => $self, %conf); | ||||
|     $self->{process_manager}  = PBot::ProcessManager->new(pbot => $self, %conf); | ||||
|     $self->{select_handler}   = PBot::SelectHandler->new(pbot => $self, %conf); | ||||
|     $self->{stdin_reader}     = PBot::StdinReader->new(pbot => $self, %conf); | ||||
|     $self->{webpaste}         = PBot::WebPaste->new(pbot => $self, %conf); | ||||
| 
 | ||||
|     $self->{interpreter} = PBot::Interpreter->new(pbot => $self, %conf); | ||||
|     # register command/factoid interpreters | ||||
|     $self->{interpreter}->register(sub { $self->{commands}->interpreter(@_) }); | ||||
|     $self->{interpreter}->register(sub { $self->{factoids}->interpreter(@_) }); | ||||
| 
 | ||||
|     $self->{factoids} = PBot::Factoids->new(pbot => $self, filename => "$data_dir/factoids.sqlite3", %conf); | ||||
| 
 | ||||
|     $self->{plugins} = PBot::Plugins->new(pbot => $self, %conf); | ||||
| 
 | ||||
|     # load available plugins | ||||
|     $self->{plugins}->autoload(%conf); | ||||
| 
 | ||||
|     # give botowner all capabilities | ||||
|     $self->{capabilities}->rebuild_botowner_capabilities(); | ||||
|     # -- this must happen last after all modules have registered their capabilities -- | ||||
|     $self->{capabilities}->rebuild_botowner_capabilities; | ||||
| 
 | ||||
|     # flush all pending save events to disk at exit | ||||
|     $self->{atexit}->register(sub { | ||||
| @ -275,18 +211,20 @@ sub random_nick { | ||||
|     return $nick; | ||||
| } | ||||
| 
 | ||||
| # TODO: add disconnect subroutine | ||||
| # TODO: add disconnect subroutine and connect/disconnect/reconnect commands | ||||
| sub connect { | ||||
|     my ($self, $server) = @_; | ||||
|     my ($self) = @_; | ||||
|     return if $ENV{PBOT_LOCAL}; | ||||
| 
 | ||||
|     if ($self->{connected}) { | ||||
|         # TODO: disconnect, clean-up, etc | ||||
|     } | ||||
| 
 | ||||
|     $server = $self->{registry}->get_value('irc', 'server') if not defined $server; | ||||
|     my $server = $self->{registry}->get_value('irc', 'server'); | ||||
|     my $port   = $self->{registry}->get_value('irc', 'port'); | ||||
|     my $delay  = $self->{registry}->get_value('irc', 'reconnect_delay') // 10; | ||||
| 
 | ||||
|     $self->{logger}->log("Connecting to $server ...\n"); | ||||
|     $self->{logger}->log("Connecting to $server:$port\n"); | ||||
| 
 | ||||
|     while ( | ||||
|         not $self->{conn} = $self->{irc}->newconn( | ||||
| @ -294,17 +232,17 @@ sub connect { | ||||
|             Username => $self->{registry}->get_value('irc', 'username'), | ||||
|             Ircname  => $self->{registry}->get_value('irc', 'realname'), | ||||
|             Server      => $server, | ||||
|             Port        => $port, | ||||
|             Pacing      => 1, | ||||
|             UTF8        => 1, | ||||
|             SSL         => $self->{registry}->get_value('irc', 'SSL'), | ||||
|             SSL_ca_file => $self->{registry}->get_value('irc', 'SSL_ca_file'), | ||||
|             SSL_ca_path => $self->{registry}->get_value('irc', 'SSL_ca_path'), | ||||
|             Port        => $self->{registry}->get_value('irc', 'port') | ||||
|         ) | ||||
|       ) | ||||
|     { | ||||
|         $self->{logger}->log("$0: Can't connect to $server:" . $self->{registry}->get_value('irc', 'port') . ". Retrying in 15 seconds...\n"); | ||||
|         sleep 15; | ||||
|         $self->{logger}->log("$0: Can't connect to $server:$port: $!\nRetrying in $delay seconds...\n"); | ||||
|         sleep $delay; | ||||
|     } | ||||
| 
 | ||||
|     $self->{connected} = 1; | ||||
| @ -331,13 +269,30 @@ sub connect { | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| #main loop | ||||
| sub register_signal_handlers { | ||||
|     my $self = shift; | ||||
| 
 | ||||
|     $SIG{INT} = sub { | ||||
|         $self->{logger}->log("SIGINT received, exiting immediately.\n"); | ||||
|         $self->atexit; exit 0; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| # called when PBot terminates | ||||
| sub atexit { | ||||
|     my $self = shift; | ||||
|     $self->{atexit}->execute_all; | ||||
|     alarm 0; | ||||
| } | ||||
| 
 | ||||
| # main loop | ||||
| sub do_one_loop { | ||||
|     my $self = shift; | ||||
|     $self->{irc}->do_one_loop() if $self->{connected}; | ||||
|     $self->{select_handler}->do_select; | ||||
| } | ||||
| 
 | ||||
| # main entry point | ||||
| sub start { | ||||
|     my $self = shift; | ||||
|     while (1) { | ||||
| @ -346,169 +301,4 @@ sub start { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| sub register_signal_handlers { | ||||
|     my $self = shift; | ||||
|     $SIG{INT} = sub { $self->atexit; exit 0; }; | ||||
| } | ||||
| 
 | ||||
| sub atexit { | ||||
|     my $self = shift; | ||||
|     $self->{atexit}->execute_all; | ||||
|     alarm 0; | ||||
| } | ||||
| 
 | ||||
| sub irc_debug_trigger { | ||||
|     my ($self, $section, $item, $newvalue) = @_; | ||||
|     $self->{irc}->debug($newvalue); | ||||
|     $self->{conn}->debug($newvalue) if $self->{connected}; | ||||
| } | ||||
| 
 | ||||
| sub change_botnick_trigger { | ||||
|     my ($self, $section, $item, $newvalue) = @_; | ||||
|     $self->{conn}->nick($newvalue) if $self->{connected}; | ||||
| } | ||||
| 
 | ||||
| sub cmd_list { | ||||
|     my ($self, $context) = @_; | ||||
|     my $text; | ||||
| 
 | ||||
|     my $usage = "Usage: list <modules|commands>"; | ||||
| 
 | ||||
|     return $usage if not length $context->{arguments}; | ||||
| 
 | ||||
|     if ($context->{arguments} =~ /^modules$/i) { | ||||
|         $text = "Loaded modules: "; | ||||
|         foreach my $channel (sort $self->{factoids}->{factoids}->get_keys) { | ||||
|             foreach my $command (sort $self->{factoids}->{factoids}->get_keys($channel)) { | ||||
|                 next if $command eq '_name'; | ||||
|                 if ($self->{factoids}->{factoids}->get_data($channel, $command, 'type') eq 'module') { | ||||
|                     $text .= $self->{factoids}->{factoids}->get_data($channel, $command, '_name') . ' '; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $text; | ||||
|     } | ||||
| 
 | ||||
|     if ($context->{arguments} =~ /^commands$/i) { | ||||
|         $text = "Registered commands: "; | ||||
|         foreach my $command (sort { $a->{name} cmp $b->{name} } @{$self->{commands}->{handlers}}) { | ||||
|             if   ($command->{requires_cap}) { $text .= "+$command->{name} "; } | ||||
|             else                            { $text .= "$command->{name} "; } | ||||
|         } | ||||
|         return $text; | ||||
|     } | ||||
|     return $usage; | ||||
| } | ||||
| 
 | ||||
| sub cmd_sl { | ||||
|     my ($self, $context) = @_; | ||||
|     return "Usage: sl <ircd command>" if not length $context->{arguments}; | ||||
|     $self->{conn}->sl($context->{arguments}); | ||||
|     return "/msg $context->{nick} sl: command sent. See log for result."; | ||||
| } | ||||
| 
 | ||||
| sub cmd_die { | ||||
|     my ($self, $context) = @_; | ||||
|     $self->{logger}->log("$context->{hostmask} made me exit.\n"); | ||||
|     $self->{conn}->privmsg($context->{from}, "Good-bye.") if $context->{from} ne 'stdin@pbot'; | ||||
|     $self->{conn}->quit("Departure requested.") if defined $self->{conn}; | ||||
|     $self->atexit(); | ||||
|     exit 0; | ||||
| } | ||||
| 
 | ||||
| sub cmd_export { | ||||
|     my ($self, $context) = @_; | ||||
|     return "Usage: export <factoids>" if not length $context->{arguments}; | ||||
|     if ($context->{arguments} =~ /^factoids$/i) { return $self->{factoids}->export_factoids; } | ||||
| } | ||||
| 
 | ||||
| sub cmd_eval { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     $self->{logger}->log("eval: $context->{from} $context->{hostmask} evaluating `$context->{arguments}`\n"); | ||||
| 
 | ||||
|     my $ret    = ''; | ||||
|     my $result = eval $context->{arguments}; | ||||
|     if ($@) { | ||||
|         if   (length $result) { $ret .= "[Error: $@] "; } | ||||
|         else                  { $ret .= "Error: $@"; } | ||||
|         $ret =~ s/ at \(eval \d+\) line 1.//; | ||||
|     } | ||||
|     $result = 'Undefined.' if not defined $result; | ||||
|     $result = 'No output.' if not length $result; | ||||
|     return "/say $ret $result"; | ||||
| } | ||||
| 
 | ||||
| sub cmd_reload { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my %reloadables = ( | ||||
|         'capabilities' => sub { | ||||
|             $self->{capabilities}->{caps}->load; | ||||
|             return "Capabilities reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'commands' => sub { | ||||
|             $self->{commands}->{metadata}->load; | ||||
|             return "Commands metadata reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'blacklist' => sub { | ||||
|             $self->{blacklist}->clear_blacklist; | ||||
|             $self->{blacklist}->load_blacklist; | ||||
|             return "Blacklist reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'ban-exemptions' => sub { | ||||
|             $self->{antiflood}->{'ban-exemptions'}->load; | ||||
|             return "Ban exemptions reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'ignores' => sub { | ||||
|             $self->{ignorelist}->{ignorelist}->load; | ||||
|             return "Ignore list reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'users' => sub { | ||||
|             $self->{users}->load; | ||||
|             return "Users reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'channels' => sub { | ||||
|             $self->{channels}->{channels}->load; | ||||
|             return "Channels reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'banlist' => sub { | ||||
|             $self->{timer}->dequeue_event('unban #.*'); | ||||
|             $self->{timer}->dequeue_event('unmute #.*'); | ||||
|             $self->{banlist}->{banlist}->load; | ||||
|             $self->{banlist}->{quietlist}->load; | ||||
|             $self->{banlist}->enqueue_timeouts($self->{banlist}->{banlist},   'b'); | ||||
|             $self->{banlist}->enqueue_timeouts($self->{banlist}->{quietlist}, 'q'); | ||||
|             return "Ban list reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'registry' => sub { | ||||
|             $self->{registry}->load; | ||||
|             return "Registry reloaded."; | ||||
|         }, | ||||
| 
 | ||||
|         'factoids' => sub { | ||||
|             $self->{factoids}->load_factoids; | ||||
|             return "Factoids reloaded."; | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     if (not length $context->{arguments} or not exists $reloadables{$context->{arguments}}) { | ||||
|         my $usage = 'Usage: reload <'; | ||||
|         $usage .= join '|', sort keys %reloadables; | ||||
|         $usage .= '>'; | ||||
|         return $usage; | ||||
|     } | ||||
| 
 | ||||
|     return $reloadables{$context->{arguments}}(); | ||||
| } | ||||
| 
 | ||||
| 1; | ||||
|  | ||||
| @ -22,6 +22,9 @@ sub initialize { | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_unplug(@_) },   "unplug",   1); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_replug(@_) },   "replug",   1); | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_pluglist(@_) }, "pluglist", 0); | ||||
| 
 | ||||
|     # load configured plugins | ||||
|     $self->autoload(%conf); | ||||
| } | ||||
| 
 | ||||
| sub cmd_plug { | ||||
|  | ||||
							
								
								
									
										172
									
								
								PBot/Registry.pm
									
									
									
									
									
								
							
							
						
						
									
										172
									
								
								PBot/Registry.pm
									
									
									
									
									
								
							| @ -2,7 +2,7 @@ | ||||
| # Author: pragma_ | ||||
| # | ||||
| # Purpose: Provides a centralized registry of configuration settings that can | ||||
| # easily be examined and updated via set/unset commands without restarting. | ||||
| # easily be examined and updated via getters and setters. | ||||
| 
 | ||||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
| @ -20,28 +20,121 @@ use PBot::RegistryCommands; | ||||
| 
 | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
|     my $filename = $conf{filename} // Carp::croak("Missing filename reference in " . __FILE__); | ||||
| 
 | ||||
|     # ensure we have a registry filename | ||||
|     my $filename = $conf{filename} // Carp::croak("Missing filename configuration item in " . __FILE__); | ||||
| 
 | ||||
|     # registry is stored as a dual-index hash object | ||||
|     $self->{registry} = PBot::DualIndexHashObject->new(name => 'Registry', filename => $filename, pbot => $self->{pbot}); | ||||
| 
 | ||||
|     # registry triggers are processed when a registry entry is modified | ||||
|     $self->{triggers} = {}; | ||||
| 
 | ||||
|     # save registry data at bot exit | ||||
|     $self->{pbot}->{atexit}->register(sub { $self->save; return; }); | ||||
| 
 | ||||
|     # prepare registry-specific bot commands | ||||
|     PBot::RegistryCommands->new(pbot => $self->{pbot}); | ||||
| 
 | ||||
|     # add default registry items | ||||
|     $self->add_default('text', 'general', 'data_dir',      $conf{data_dir}); | ||||
|     $self->add_default('text', 'general', 'module_dir',    $conf{module_dir}); | ||||
|     $self->add_default('text', 'general', 'plugin_dir',    $conf{plugin_dir}); | ||||
|     $self->add_default('text', 'general', 'update_dir',    $conf{update_dir}); | ||||
| 
 | ||||
|     $self->add_default('text', 'general', 'trigger',       $conf{trigger}           // '!'); | ||||
| 
 | ||||
|     $self->add_default('text', 'irc', 'debug',             $conf{irc_debug}         // 0); | ||||
|     $self->add_default('text', 'irc', 'show_motd',         $conf{show_motd}         // 1); | ||||
|     $self->add_default('text', 'irc', 'max_msg_len',       $conf{max_msg_len}       // 425); | ||||
|     $self->add_default('text', 'irc', 'server',            $conf{server}            // "irc.libera.chat"); | ||||
|     $self->add_default('text', 'irc', 'port',              $conf{port}              // 6667); | ||||
|     $self->add_default('text', 'irc', 'SSL',               $conf{SSL}               // 0); | ||||
|     $self->add_default('text', 'irc', 'SSL_ca_file',       $conf{SSL_ca_file}       // 'none'); | ||||
|     $self->add_default('text', 'irc', 'SSL_ca_path',       $conf{SSL_ca_path}       // 'none'); | ||||
|     $self->add_default('text', 'irc', 'botnick',           $conf{botnick}           // ""); | ||||
|     $self->add_default('text', 'irc', 'username',          $conf{username}          // "pbot3"); | ||||
|     $self->add_default('text', 'irc', 'realname',          $conf{realname}          // "https://github.com/pragma-/pbot"); | ||||
|     $self->add_default('text', 'irc', 'identify_password', $conf{identify_password} // ''); | ||||
|     $self->add_default('text', 'irc', 'log_default_handler', 1); | ||||
| 
 | ||||
|     $self->set_default('irc', 'SSL_ca_file',       'private', 1); | ||||
|     $self->set_default('irc', 'SSL_ca_path',       'private', 1); | ||||
|     $self->set_default('irc', 'identify_password', 'private', 1); | ||||
| 
 | ||||
|     # load existing registry entries from file (if exists) to overwrite defaults | ||||
|     if (-e $filename) { | ||||
|         $self->load; | ||||
|     } else { | ||||
|         $self->{pbot}->{logger}->log("No registry found at $filename, using defaults.\n"); | ||||
|     } | ||||
| 
 | ||||
|     # update important paths | ||||
|     $self->set('general', 'data_dir',   'value', $conf{data_dir},   0, 1); | ||||
|     $self->set('general', 'module_dir', 'value', $conf{module_dir}, 0, 1); | ||||
|     $self->set('general', 'plugin_dir', 'value', $conf{plugin_dir}, 0, 1); | ||||
|     $self->set('general', 'update_dir', 'value', $conf{update_dir}, 0, 1); | ||||
| 
 | ||||
|     # override registry entries with command-line arguments, if any | ||||
|     foreach my $override (keys %{$self->{pbot}->{overrides}}) { | ||||
|         my $value = $self->{pbot}->{overrides}->{$override}; | ||||
|         my ($section, $key) = split /\./, $override; | ||||
| 
 | ||||
|         $self->{pbot}->{logger}->log("Overriding $section.$key to $value\n"); | ||||
| 
 | ||||
|         $self->set($section, $key, 'value', $value, 0, 1); | ||||
|     } | ||||
| 
 | ||||
|     # add triggers | ||||
|     $self->add_trigger('irc', 'debug',   sub { $self->trigger_irc_debug(@_) }); | ||||
|     $self->add_trigger('irc', 'botnick', sub { $self->trigger_change_botnick(@_) }); | ||||
| } | ||||
| 
 | ||||
| # registry triggers fire when value changes | ||||
| 
 | ||||
| sub trigger_irc_debug { | ||||
|     my ($self, $section, $item, $newvalue) = @_; | ||||
| 
 | ||||
|     $self->{pbot}->{irc}->debug($newvalue); | ||||
| 
 | ||||
|     if ($self->{pbot}->{connected}) { | ||||
|         $self->{pbot}->{conn}->debug($newvalue); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| sub trigger_change_botnick { | ||||
|     my ($self, $section, $item, $newvalue) = @_; | ||||
| 
 | ||||
|     if ($self->{pbot}->{connected}) { | ||||
|         $self->{pbot}->{conn}->nick($newvalue) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| # registry api | ||||
| 
 | ||||
| sub load { | ||||
|     my $self = shift; | ||||
| 
 | ||||
|     # load registry from file | ||||
|     $self->{registry}->load; | ||||
| 
 | ||||
|     # fire off all registered triggers | ||||
|     foreach my $section ($self->{registry}->get_keys) { | ||||
|         foreach my $item ($self->{registry}->get_keys($section)) { $self->process_trigger($section, $item, $self->{registry}->get_data($section, $item, 'value')); } | ||||
|         foreach my $item ($self->{registry}->get_keys($section)) { | ||||
|             $self->process_trigger($section, $item, $self->{registry}->get_data($section, $item, 'value')); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| sub save { | ||||
|     my $self = shift; | ||||
| 
 | ||||
|     $self->{registry}->save; | ||||
| } | ||||
| 
 | ||||
| sub add_default { | ||||
|     my ($self, $type, $section, $item, $value) = @_; | ||||
| 
 | ||||
|     $self->add($type, $section, $item, $value, 1); | ||||
| } | ||||
| 
 | ||||
| @ -50,7 +143,10 @@ sub add { | ||||
|     my ($type, $section, $item, $value, $is_default) = @_; | ||||
|     $type = lc $type; | ||||
| 
 | ||||
|     if ($is_default) { return if $self->{registry}->exists($section, $item); } | ||||
|     if ($is_default) { | ||||
|         # don't replace existing registry values if we're just adding a default value | ||||
|         return if $self->{registry}->exists($section, $item); | ||||
|     } | ||||
| 
 | ||||
|     if (not $self->{registry}->exists($section, $item)) { | ||||
|         my $data = { | ||||
| @ -62,73 +158,102 @@ sub add { | ||||
|         $self->{registry}->set($section, $item, 'value', $value, 1); | ||||
|         $self->{registry}->set($section, $item, 'type',  $type,  1) unless $self->{registry}->exists($section, $item, 'type'); | ||||
|     } | ||||
|     $self->process_trigger($section, $item, $value) unless $is_default; | ||||
|     $self->save unless $is_default; | ||||
| 
 | ||||
|     unless ($is_default) { | ||||
|         $self->process_trigger($section, $item, $value); | ||||
|         $self->save; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| sub remove { | ||||
|     my $self = shift; | ||||
|     my ($section, $item) = @_; | ||||
|     my ($self, $section, $item) = @_; | ||||
| 
 | ||||
|     $self->{registry}->remove($section, $item); | ||||
| } | ||||
| 
 | ||||
| sub set_default { | ||||
|     my ($self, $section, $item, $key, $value) = @_; | ||||
| 
 | ||||
|     $self->set($section, $item, $key, $value, 1); | ||||
| } | ||||
| 
 | ||||
| sub set { | ||||
|     my ($self, $section, $item, $key, $value, $is_default, $dont_save) = @_; | ||||
| 
 | ||||
|     $key = lc $key if defined $key; | ||||
| 
 | ||||
|     if ($is_default) { return if $self->{registry}->exists($section, $item, $key); } | ||||
|     if ($is_default && $self->{registry}->exists($section, $item, $key)) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     my $oldvalue = $self->get_value($section, $item, 1) if defined $value; | ||||
|     $oldvalue = '' if not defined $oldvalue; | ||||
|     my $oldvalue; | ||||
| 
 | ||||
|     if (defined $value) { | ||||
|         $oldvalue = $self->get_value($section, $item, 1); | ||||
|     } | ||||
| 
 | ||||
|     $oldvalue //= ''; | ||||
| 
 | ||||
|     my $result = $self->{registry}->set($section, $item, $key, $value, 1); | ||||
| 
 | ||||
|     if (defined $key and $key eq 'value' and defined $value and $oldvalue ne $value) { $self->process_trigger($section, $item, $value); } | ||||
|     if (defined $key and $key eq 'value' and defined $value and $oldvalue ne $value) { | ||||
|         $self->process_trigger($section, $item, $value); | ||||
|     } | ||||
| 
 | ||||
|     $self->save if !$dont_save && $result =~ m/set to/ && not $is_default; | ||||
| 
 | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| sub unset { | ||||
|     my ($self, $section, $item, $key) = @_; | ||||
|     $key = lc $key; | ||||
| 
 | ||||
|     $key = lc $key if defined $key; | ||||
| 
 | ||||
|     return $self->{registry}->unset($section, $item, $key); | ||||
| } | ||||
| 
 | ||||
| sub get_value { | ||||
|     my ($self, $section, $item, $as_text, $context) = @_; | ||||
| 
 | ||||
|     $section = lc $section; | ||||
|     $item    = lc $item; | ||||
| 
 | ||||
|     my $key = $item; | ||||
| 
 | ||||
|     # TODO: use user-metadata for this | ||||
|     if (defined $context and exists $context->{nick}) { | ||||
|         my $context_nick = lc $context->{nick}; | ||||
|         if ($self->{registry}->exists($section, "$item.nick.$context_nick")) { $key = "$item.nick.$context_nick"; } | ||||
|         if ($self->{registry}->exists($section, "$item.nick.$context_nick")) { | ||||
|             $key = "$item.nick.$context_nick"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if ($self->{registry}->exists($section, $key)) { | ||||
|         if (not $as_text and $self->{registry}->get_data($section, $key, 'type') eq 'array') { return split /\s*,\s*/, $self->{registry}->get_data($section, $key, 'value'); } | ||||
|         else                                                                                 { return $self->{registry}->get_data($section, $key, 'value'); } | ||||
|         if (not $as_text and $self->{registry}->get_data($section, $key, 'type') eq 'array') { | ||||
|             return split /\s*,\s*/, $self->{registry}->get_data($section, $key, 'value'); | ||||
|         } else { | ||||
|             return $self->{registry}->get_data($section, $key, 'value'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return undef; | ||||
| } | ||||
| 
 | ||||
| sub get_array_value { | ||||
|     my ($self, $section, $item, $index, $context) = @_; | ||||
| 
 | ||||
|     $section = lc $section; | ||||
|     $item    = lc $item; | ||||
| 
 | ||||
|     my $key = $item; | ||||
| 
 | ||||
|     # TODO: use user-metadata for this | ||||
|     if (defined $context and exists $context->{nick}) { | ||||
|         my $context_nick = lc $context->{nick}; | ||||
|         if ($self->{registry}->exists($section, "$item.nick.$context_nick")) { $key = "$item.nick.$context_nick"; } | ||||
|         if ($self->{registry}->exists($section, "$item.nick.$context_nick")) { | ||||
|             $key = "$item.nick.$context_nick"; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if ($self->{registry}->exists($section, $key)) { | ||||
| @ -139,20 +264,27 @@ sub get_array_value { | ||||
|             return $self->{registry}->get_data($section, $key, 'value'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return undef; | ||||
| } | ||||
| 
 | ||||
| sub add_trigger { | ||||
|     my ($self, $section, $item, $subref) = @_; | ||||
| 
 | ||||
|     $self->{triggers}->{lc $section}->{lc $item} = $subref; | ||||
| } | ||||
| 
 | ||||
| sub process_trigger { | ||||
|     my $self = shift; | ||||
|     my ($section, $item) = @_; | ||||
|     my $self = shift;           # shift $self off of the top of @_ | ||||
|     my ($section, $item) = @_;  # but leave $section and $item in @_ | ||||
| 
 | ||||
|     $section = lc $section; | ||||
|     $item    = lc $item; | ||||
|     if (exists $self->{triggers}->{$section} and exists $self->{triggers}->{$section}->{$item}) { return &{$self->{triggers}->{$section}->{$item}}(@_); } | ||||
| 
 | ||||
|     if (exists $self->{triggers}->{$section} and exists $self->{triggers}->{$section}->{$item}) { | ||||
|         return &{$self->{triggers}->{$section}->{$item}}(@_);  # $section and $item still in @_ | ||||
|     } | ||||
| 
 | ||||
|     return undef; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| # File: RegistryCommands.pm | ||||
| # Author: pragma_ | ||||
| # | ||||
| # Purpose: Commands to introspect and update Registry | ||||
| # Purpose: Bot commands to manipulate Registry entries. | ||||
| 
 | ||||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
| @ -26,11 +26,13 @@ sub initialize { | ||||
| 
 | ||||
| sub cmd_regset { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $usage = "Usage: regset <section>.<item> [value]"; | ||||
| 
 | ||||
|     # support "<section>.<key>" syntax in addition to "<section> <key>" | ||||
|     my $section = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}) // return $usage; | ||||
|     my ($item, $value); | ||||
| 
 | ||||
|     if ($section =~ m/^(.+?)\.(.+)$/) { | ||||
|         ($section, $item) = ($1, $2); | ||||
|         ($value) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 1); | ||||
| @ -38,64 +40,90 @@ sub cmd_regset { | ||||
|         ($item, $value) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); | ||||
|     } | ||||
| 
 | ||||
|     if (not defined $section or not defined $item) { return $usage; } | ||||
|     if (not defined $section or not defined $item) { | ||||
|         return $usage; | ||||
|     } | ||||
| 
 | ||||
|     if (defined $value) { $self->{pbot}->{registry}->add('text', $section, $item, $value); } | ||||
|     else                { return $self->{pbot}->{registry}->set($section, $item, 'value'); } | ||||
|     if (defined $value) { | ||||
|         $self->{pbot}->{registry}->add('text', $section, $item, $value); | ||||
|     } else { | ||||
|         return $self->{pbot}->{registry}->set($section, $item, 'value'); | ||||
|     } | ||||
| 
 | ||||
|     $self->{pbot}->{logger}->log("$context->{hostmask} set registry entry $section.$item => $value\n"); | ||||
| 
 | ||||
|     $self->{pbot}->{logger}->log("$context->{hostmask} set registry entry [$section] $item => $value\n"); | ||||
|     return "$section.$item set to $value"; | ||||
| } | ||||
| 
 | ||||
| sub cmd_regunset { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $usage = "Usage: regunset <section>.<item>"; | ||||
| 
 | ||||
|     # support "<section>.<key>" syntax in addition to "<section> <key>" | ||||
|     my $section = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}) // return $usage; | ||||
|     my $item; | ||||
|     if ($section =~ m/^(.+?)\.(.+)$/) { ($section, $item) = ($1, $2); } | ||||
|     else                              { ($item) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 1); } | ||||
| 
 | ||||
|     if (not defined $section or not defined $item) { return $usage; } | ||||
|     if ($section =~ m/^(.+?)\.(.+)$/) { | ||||
|         ($section, $item) = ($1, $2); | ||||
|     } else { | ||||
|         ($item) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 1); | ||||
|     } | ||||
| 
 | ||||
|     if (not $self->{pbot}->{registry}->{registry}->exists($section)) { return "No such registry section $section."; } | ||||
|     if (not defined $section or not defined $item) { | ||||
|         return $usage; | ||||
|     } | ||||
| 
 | ||||
|     if (not $self->{pbot}->{registry}->{registry}->exists($section, $item)) { return "No such item $item in section $section."; } | ||||
|     if (not $self->{pbot}->{registry}->{registry}->exists($section)) { | ||||
|         return "No such registry section $section."; | ||||
|     } | ||||
| 
 | ||||
|     if (not $self->{pbot}->{registry}->{registry}->exists($section, $item)) { | ||||
|         return "No such item $item in section $section."; | ||||
|     } | ||||
| 
 | ||||
|     $self->{pbot}->{logger}->log("$context->{hostmask} removed registry entry $section.$item\n"); | ||||
| 
 | ||||
|     $self->{pbot}->{registry}->remove($section, $item); | ||||
| 
 | ||||
|     return "$section.$item deleted from registry"; | ||||
| } | ||||
| 
 | ||||
| sub cmd_regsetmeta { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $usage = "Usage: regsetmeta <section>.<item> [key [value]]"; | ||||
| 
 | ||||
|     # support "<section>.<key>" syntax in addition to "<section> <key>" | ||||
|     my $section = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}) // return $usage; | ||||
|     my ($item, $key, $value); | ||||
| 
 | ||||
|     if ($section =~ m/^(.+?)\.(.+)$/) { | ||||
|         ($section, $item)  = ($1, $2); | ||||
|         ($key,     $value) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); | ||||
|         ($key, $value) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); | ||||
|     } else { | ||||
|         ($item, $key, $value) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 3); | ||||
|     } | ||||
| 
 | ||||
|     if (not defined $section or not defined $item) { return $usage; } | ||||
|     if (not defined $section or not defined $item) { | ||||
|         return $usage; | ||||
|     } | ||||
| 
 | ||||
|     $key   = undef if not length $key; | ||||
|     $value = undef if not length $value; | ||||
| 
 | ||||
|     return $self->{pbot}->{registry}->set($section, $item, $key, $value); | ||||
| } | ||||
| 
 | ||||
| sub cmd_regunsetmeta { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my $usage = "Usage: regunsetmeta <section>.<item> <key>"; | ||||
| 
 | ||||
|     # support "<section>.<key>" syntax in addition to "<section> <key>" | ||||
|     my $section = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}) // return $usage; | ||||
|     my ($item, $key); | ||||
| 
 | ||||
|     if ($section =~ m/^(.+?)\.(.+)$/) { | ||||
|         ($section, $item) = ($1, $2); | ||||
|         ($key) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 1); | ||||
| @ -103,86 +131,114 @@ sub cmd_regunsetmeta { | ||||
|         ($item, $key) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); | ||||
|     } | ||||
| 
 | ||||
|     if (not defined $section or not defined $item or not defined $key) { return $usage; } | ||||
|     if (not defined $section or not defined $item or not defined $key) { | ||||
|         return $usage; | ||||
|     } | ||||
| 
 | ||||
|     return $self->{pbot}->{registry}->unset($section, $item, $key); | ||||
| } | ||||
| 
 | ||||
| sub cmd_regshow { | ||||
|     my ($self, $context) = @_; | ||||
|     my $registry = $self->{pbot}->{registry}->{registry}; | ||||
| 
 | ||||
|     my $usage    = "Usage: regshow <section>.<item>"; | ||||
|     my $registry = $self->{pbot}->{registry}->{registry}; | ||||
| 
 | ||||
|     # support "<section>.<key>" syntax in addition to "<section> <key>" | ||||
|     my $section = $self->{pbot}->{interpreter}->shift_arg($context->{arglist}) // return $usage; | ||||
|     my $item; | ||||
|     if ($section =~ m/^(.+?)\.(.+)$/) { ($section, $item) = ($1, $2); } | ||||
|     else                              { ($item) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 1); } | ||||
| 
 | ||||
|     if (not defined $section or not defined $item) { return $usage; } | ||||
|     if ($section =~ m/^(.+?)\.(.+)$/) { | ||||
|         ($section, $item) = ($1, $2); | ||||
|     } else { | ||||
|         ($item) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 1); | ||||
|     } | ||||
| 
 | ||||
|     if (not $registry->exists($section)) { return "No such registry section $section."; } | ||||
|     if (not defined $section or not defined $item) { | ||||
|         return $usage; | ||||
|     } | ||||
| 
 | ||||
|     if (not $registry->exists($section, $item)) { return "No such registry item $item in section $section."; } | ||||
|     if (not $registry->exists($section)) { | ||||
|         return "No such registry section $section."; | ||||
|     } | ||||
| 
 | ||||
|     if ($registry->get_data($section, $item, 'private')) { return "$section.$item: <private>"; } | ||||
|     if (not $registry->exists($section, $item)) { | ||||
|         return "No such registry item $item in section $section."; | ||||
|     } | ||||
| 
 | ||||
|     if ($registry->get_data($section, $item, 'private')) { | ||||
|         return "$section.$item: <private>"; | ||||
|     } | ||||
| 
 | ||||
|     my $result = "$section.$item: " . $registry->get_data($section, $item, 'value'); | ||||
| 
 | ||||
|     if ($registry->get_data($section, $item, 'type') eq 'array') { $result .= ' [array]'; } | ||||
|     if ($registry->get_data($section, $item, 'type') eq 'array') { | ||||
|         $result .= ' [array]'; | ||||
|     } | ||||
| 
 | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| sub cmd_regfind { | ||||
|     my ($self, $context) = @_; | ||||
|     my $registry = $self->{pbot}->{registry}->{registry}; | ||||
|     my $usage    = "Usage: regfind [-showvalues] [-section section] <regex>"; | ||||
| 
 | ||||
|     my $usage     = "Usage: regfind [-showvalues] [-section section] <regex>"; | ||||
|     my $registry  = $self->{pbot}->{registry}->{registry}; | ||||
| 
 | ||||
|     my $arguments = $context->{arguments}; | ||||
| 
 | ||||
|     return $usage if not defined $arguments; | ||||
| 
 | ||||
|     # TODO maybe use GetOptionsFromArray here | ||||
|     my ($section, $showvalues); | ||||
|     $section    = $1 if $arguments =~ s/-section\s+([^\b\s]+)//i; | ||||
|     $showvalues = 1  if $arguments =~ s/-showvalues?//i; | ||||
| 
 | ||||
|     $arguments =~ s/^\s+//; | ||||
|     $arguments =~ s/\s+$//; | ||||
|     $arguments =~ s/^\s+|\s+$//g; | ||||
|     $arguments =~ s/\s+/ /g; | ||||
| 
 | ||||
|     return $usage if $arguments eq ""; | ||||
|     return $usage if not length $arguments; | ||||
| 
 | ||||
|     $section = lc $section if defined $section; | ||||
| 
 | ||||
|     my ($text, $last_item, $last_section, $i); | ||||
|     $last_section = ""; | ||||
|     $i            = 0; | ||||
|     my ($text, $last_item, $last_section, $count); | ||||
|     $last_section = ''; | ||||
|     $count        = 0; | ||||
| 
 | ||||
|     eval { | ||||
|         use re::engine::RE2 -strict => 1; | ||||
|         use re::engine::RE2 -strict => 1; # prevent user-defined regex from exploding | ||||
| 
 | ||||
|         foreach my $section_key (sort $registry->get_keys) { | ||||
|             next if defined $section and $section_key ne $section; | ||||
|             foreach my $item_key (sort $registry->get_keys($section_key)) { | ||||
|                 next if $item_key eq '_name'; | ||||
|                 next if $item_key eq '_name'; # skip internal cached value | ||||
| 
 | ||||
|                 if ($registry->get_data($section_key, $item_key, 'private')) { | ||||
|                     # do not match on value if private | ||||
|                     # if private, match on key only -- do not match on value | ||||
|                     next if $item_key !~ /$arguments/i; | ||||
|                 } else { | ||||
|                     next if $registry->get_data($section_key, $item_key, 'value') !~ /$arguments/i and $item_key !~ /$arguments/i; | ||||
|                     # otherwise check for match on key and value | ||||
|                     next if $item_key !~ /$arguments/i and $registry->get_data($section_key, $item_key, 'value') !~ /$arguments/i; | ||||
|                 } | ||||
| 
 | ||||
|                 $i++; | ||||
|                 $count++; | ||||
| 
 | ||||
|                 if ($section_key ne $last_section) { | ||||
|                     $text .= "[$section_key]\n"; | ||||
|                     $last_section = $section_key; | ||||
|                 } | ||||
| 
 | ||||
|                 if ($showvalues) { | ||||
|                     if ($registry->get_data($section_key, $item_key, 'private')) { $text .= "  $item_key = <private>\n"; } | ||||
|                     else { | ||||
|                         $text .= | ||||
|                           "  $item_key = " . $registry->get_data($section_key, $item_key, 'value') . ($registry->get_data($section_key, $item_key, 'type') eq 'array' ? " [array]\n" : "\n"); | ||||
|                     if ($registry->get_data($section_key, $item_key, 'private')) { | ||||
|                         $text .= "  $item_key = <private>\n"; | ||||
|                     } else { | ||||
|                         $text .= "  $item_key = " . $registry->get_data($section_key, $item_key, 'value') | ||||
|                           . ($registry->get_data($section_key, $item_key, 'type') eq 'array' ? " [array]\n" : "\n"); | ||||
|                     } | ||||
|                 } else { | ||||
|                     $text .= "  $item_key\n"; | ||||
|                 } | ||||
| 
 | ||||
|                 $last_item = $item_key; | ||||
|             } | ||||
|         } | ||||
| @ -190,18 +246,20 @@ sub cmd_regfind { | ||||
| 
 | ||||
|     return "/msg $context->{nick} $context->{arguments}: $@" if $@; | ||||
| 
 | ||||
|     if ($i == 1) { | ||||
|         chop $text; | ||||
|         if ($registry->get_data($last_section, $last_item, 'private')) { return "Found one registry entry: [$last_section] $last_item: <private>"; } | ||||
|         else { | ||||
|     if ($count == 1) { | ||||
|         chomp $text; | ||||
| 
 | ||||
|         if ($registry->get_data($last_section, $last_item, 'private')) { | ||||
|             return "Found one registry entry: [$last_section] $last_item: <private>"; | ||||
|         } else { | ||||
|             return | ||||
|                 "Found one registry entry: [$last_section] $last_item: " | ||||
|               . $registry->get_data($last_section, $last_item, 'value') | ||||
|               . ($registry->get_data($last_section, $last_item, 'type') eq 'array' ? ' [array]' : ''); | ||||
|         } | ||||
|     } elsif ($count > 1) { | ||||
|         return "Found $count registry entries:\n$text"; | ||||
|     } else { | ||||
|         return "Found $i registry entries:\n$text" unless $i == 0; | ||||
| 
 | ||||
|         my $sections = (defined $section ? "section $section" : 'any sections'); | ||||
|         return "No matching registry entries found in $sections."; | ||||
|     } | ||||
| @ -209,7 +267,9 @@ sub cmd_regfind { | ||||
| 
 | ||||
| sub cmd_regchange { | ||||
|     my ($self, $context) = @_; | ||||
| 
 | ||||
|     my ($section, $item, $delim, $tochange, $changeto, $modifier); | ||||
| 
 | ||||
|     my $arguments = $context->{arguments}; | ||||
| 
 | ||||
|     if (length $arguments) { | ||||
| @ -226,31 +286,44 @@ sub cmd_regchange { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (not defined $section or not defined $item or not defined $changeto) { return "Usage: regchange <section>.<item> s/<pattern>/<replacement>/"; } | ||||
|     if (not defined $section or not defined $item or not defined $changeto) { | ||||
|         return "Usage: regchange <section>.<item> s/<pattern>/<replacement>/"; | ||||
|     } | ||||
| 
 | ||||
|     $section = lc $section; | ||||
|     $item    = lc $item; | ||||
| 
 | ||||
|     my $registry = $self->{pbot}->{registry}->{registry}; | ||||
| 
 | ||||
|     if (not $registry->exists($section)) { return "No such registry section $section."; } | ||||
|     if (not $registry->exists($section)) { | ||||
|         return "No such registry section $section."; | ||||
|     } | ||||
| 
 | ||||
|     if (not $registry->exists($section, $item)) { return "No such registry item $item in section $section."; } | ||||
|     if (not $registry->exists($section, $item)) { | ||||
|         return "No such registry item $item in section $section."; | ||||
|     } | ||||
| 
 | ||||
|     my $ret = eval { | ||||
|         use re::engine::RE2 -strict => 1; | ||||
|         if (not $registry->get_data($section, $item, 'value') =~ s|$tochange|$changeto|) { | ||||
|     my $result = eval { | ||||
|         use re::engine::RE2 -strict => 1; # prevent user-defined regex from exploding | ||||
| 
 | ||||
|         my $value = $registry->get_data($section, $item, 'value'); | ||||
| 
 | ||||
|         if (not $value =~ s|$tochange|$changeto|) { | ||||
|             $self->{pbot}->{logger}->log("($context->{from}) $context->{hostmask}: failed to change $section.$item 's$delim$tochange$delim$changeto$delim$modifier\n"); | ||||
|             return "/msg $context->{nick} Change $section.$item failed."; | ||||
|         } else { | ||||
|             $registry->set($section, $item, 'value', $value, 1); | ||||
| 
 | ||||
|             $self->{pbot}->{logger}->log("($context->{from}) $context->{hostmask}: changed $section.$item 's/$tochange/$changeto/\n"); | ||||
|             $self->{pbot}->{registry}->process_trigger($section, $item, 'value', $registry->get_data($section, $item, 'value')); | ||||
|             $self->{pbot}->{registry}->process_trigger($section, $item, 'value', $value); | ||||
|             $self->{pbot}->{registry}->save; | ||||
|             return "$section.$item set to " . $registry->get_data($section, $item, 'value'); | ||||
|             return "$section.$item set to $value"; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return "/msg $context->{nick} Failed to change $section.$item: $@" if $@; | ||||
|     return $ret; | ||||
| 
 | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| 1; | ||||
|  | ||||
| @ -25,7 +25,11 @@ use constant { | ||||
| 
 | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
| 
 | ||||
|     # register `version` command | ||||
|     $self->{pbot}->{commands}->register(sub { $self->cmd_version(@_) }, "version", 0); | ||||
| 
 | ||||
|     # initialize last_check version data | ||||
|     $self->{last_check} = {timestamp => 0, version => BUILD_REVISION, date => BUILD_DATE}; | ||||
| } | ||||
| 
 | ||||
| @ -38,31 +42,44 @@ sub cmd_version { | ||||
|         $self->{last_check}->{timestamp} = time; | ||||
| 
 | ||||
|         my $url = $self->{pbot}->{registry}->get_value('version', 'check_url') // 'https://raw.githubusercontent.com/pragma-/pbot/master/PBot/VERSION.pm'; | ||||
| 
 | ||||
|         $self->{pbot}->{logger}->log("Checking $url for new version...\n"); | ||||
| 
 | ||||
|         my $ua       = LWP::UserAgent->new(timeout => 10); | ||||
|         my $response = $ua->get($url); | ||||
| 
 | ||||
|         return "Unable to get version information: " . $response->status_line if not $response->is_success; | ||||
|         if (not $response->is_success) { | ||||
|             return "Unable to get version information: " . $response->status_line; | ||||
|         } | ||||
| 
 | ||||
|         my $text = $response->decoded_content; | ||||
|         my ($version, $date) = $text =~ m/^\s+BUILD_REVISION => (\d+).*^\s+BUILD_DATE\s+=> "([^"]+)"/ms; | ||||
| 
 | ||||
|         if (not defined $version or not defined $date) { return "Unable to get version information: data did not match expected format"; } | ||||
|         if (not defined $version or not defined $date) { | ||||
|             return "Unable to get version information: data did not match expected format"; | ||||
|         } | ||||
| 
 | ||||
|         $self->{last_check} = {timestamp => time, version => $version, date => $date}; | ||||
|     } | ||||
| 
 | ||||
|     my $target_nick; | ||||
|     $target_nick = $self->{pbot}->{nicklist}->is_present_similar($context->{from}, $context->{arguments}) if length $context->{arguments}; | ||||
|     if (length $context->{arguments}) { | ||||
|         $target_nick = $self->{pbot}->{nicklist}->is_present_similar($context->{from}, $context->{arguments}); | ||||
|     } | ||||
| 
 | ||||
|     my $result = '/say '; | ||||
|     $result .= "$target_nick: " if $target_nick; | ||||
|     $result .= $self->version; | ||||
| 
 | ||||
|     if ($self->{last_check}->{version} > BUILD_REVISION) { $result .= "; new version available: $self->{last_check}->{version} $self->{last_check}->{date}!"; } | ||||
|     if ($self->{last_check}->{version} > BUILD_REVISION) { | ||||
|         $result .= "; new version available: $self->{last_check}->{version} $self->{last_check}->{date}!"; | ||||
|     } | ||||
| 
 | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| sub version { return BUILD_NAME . " version " . BUILD_REVISION . " " . BUILD_DATE; } | ||||
| sub version { | ||||
|     return BUILD_NAME . " version " . BUILD_REVISION . " " . BUILD_DATE; | ||||
| } | ||||
| 
 | ||||
| 1; | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| # File: WebPaste.pm | ||||
| # Author: pragma_ | ||||
| # | ||||
| # Purpose: Pastes text to web paste sites. | ||||
| # Purpose: Pastes text to a cycling list of web paste sites. | ||||
| 
 | ||||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
| @ -22,6 +22,9 @@ use Encode; | ||||
| sub initialize { | ||||
|     my ($self, %conf) = @_; | ||||
| 
 | ||||
|     # There used to be many more paste sites in this list but one by one | ||||
|     # many have died off. :-( | ||||
| 
 | ||||
|     $self->{paste_sites} = [ | ||||
|         sub { $self->paste_0x0st(@_) }, | ||||
|         # sub { $self->paste_ixio(@_) }, # removed due to being too slow (temporarily hopefully) | ||||
| @ -32,53 +35,74 @@ sub initialize { | ||||
| 
 | ||||
| sub get_paste_site { | ||||
|     my ($self) = @_; | ||||
| 
 | ||||
|     my $subref = $self->{paste_sites}->[$self->{current_site}]; | ||||
|     if (++$self->{current_site} >= @{$self->{paste_sites}}) { $self->{current_site} = 0; } | ||||
| 
 | ||||
|     if (++$self->{current_site} >= @{$self->{paste_sites}}) { | ||||
|         $self->{current_site} = 0; | ||||
|     } | ||||
| 
 | ||||
|     return $subref; | ||||
| } | ||||
| 
 | ||||
| sub paste { | ||||
|     my ($self, $text, %opts) = @_; | ||||
| 
 | ||||
|     my %default_opts = ( | ||||
|         no_split => 0, | ||||
|     ); | ||||
| 
 | ||||
|     %opts = (%default_opts, %opts); | ||||
| 
 | ||||
|     $text =~ s/(.{120})\s/$1\n/g unless $opts{no_split}; | ||||
| 
 | ||||
|     my $result; | ||||
|     my $response; | ||||
| 
 | ||||
|     for (my $tries = 3; $tries > 0; $tries--) { | ||||
|         my $paste_site = $self->get_paste_site; | ||||
|         $result = $paste_site->($text); | ||||
|         last if $result !~ m/error pasting/; | ||||
| 
 | ||||
|         $response = $paste_site->($text); | ||||
| 
 | ||||
|         last if $response->is_success; | ||||
|     } | ||||
| 
 | ||||
|     if (not $response->is_success) { | ||||
|         return "error pasting: " . $response->status_line; | ||||
|     } | ||||
| 
 | ||||
|     my $result = $response->decoded_content; | ||||
| 
 | ||||
|     $result =~ s/^\s+|\s+$//g; | ||||
| 
 | ||||
|     alarm 1; # LWP::UserAgent::Paranoid kills alarm | ||||
| 
 | ||||
|     return $result; | ||||
| } | ||||
| 
 | ||||
| sub paste_0x0st { | ||||
|     my ($self, $text) = @_; | ||||
| 
 | ||||
|     my $ua = LWP::UserAgent::Paranoid->new(request_timeout => 10); | ||||
| 
 | ||||
|     push @{$ua->requests_redirectable}, 'POST'; | ||||
|     my $response = $ua->post( | ||||
| 
 | ||||
|     return $ua->post( | ||||
|         "https://0x0.st", | ||||
|         [ file => [ undef, "file", Content => $text ] ], | ||||
|         Content_Type => 'form-data' | ||||
|     ); | ||||
|     alarm 1;    # LWP::UserAgent::Paranoid kills alarm | ||||
|     return "error pasting: " . $response->status_line if not $response->is_success; | ||||
|     return $response->content; | ||||
| } | ||||
| 
 | ||||
| sub paste_ixio { | ||||
|     my ($self, $text) = @_; | ||||
| 
 | ||||
|     my $ua = LWP::UserAgent::Paranoid->new(request_timeout => 10); | ||||
| 
 | ||||
|     push @{$ua->requests_redirectable}, 'POST'; | ||||
|     my %post     = ('f:1' => $text); | ||||
|     my $response = $ua->post("http://ix.io", \%post); | ||||
|     alarm 1;    # LWP::UserAgent::Paranoid kills alarm | ||||
|     return "error pasting: " . $response->status_line if not $response->is_success; | ||||
|     return $response->content; | ||||
| 
 | ||||
|     my %post = ('f:1' => $text); | ||||
| 
 | ||||
|     return $ua->post("http://ix.io", \%post); | ||||
| } | ||||
| 
 | ||||
| 1; | ||||
|  | ||||
							
								
								
									
										2
									
								
								data/last_update
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								data/last_update
									
									
									
									
										vendored
									
									
								
							| @ -1 +1 @@ | ||||
| 3917 | ||||
| 3998 | ||||
|  | ||||
| @ -333,8 +333,7 @@ Name | Description | Belongs to group | ||||
| `can-mode-any` | Allows the [`mode`](#mode) command to set any mode flag. | botowner | ||||
| `can-modify-admins` | Allows the user to modify user accounts that have the `admin` capability | botowner | ||||
| `can-modify-capabilities` | Allows the user to use the [`useradd`](#useradd) or [`userset`](#userset) commands to add or remove capabilities from users. | botowner | ||||
| `can-group-capabilities` | Allows the user to use the [`cap group`](#cap) command to modify capability groups. | botowner | ||||
| `can-ungroup-capabilities` | Allows the user to use the [`cap ungroup`](#cap) command to modify capability groups. | botowner | ||||
| `can-group-capabilities` | Allows the user to use the [`cap group`](#cap) and [`cap ungroup`](#cap) commands to modify capability groups. | botowner | ||||
| `can-clear-bans` | Allows the user to use [`unban *`](#unbanunmute) to clear a channel's bans. | botowner, admin | ||||
| `can-clear-mutes` | Allows the user to use [`unmute *`](#unbanunmute) to clear a channel's mutes. | botowner, admin | ||||
| `can-kick-wildcard` | Allows the user to use wildcards with the [`kick`](#kick) command. | botowner, admin | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Pragmatic Software
						Pragmatic Software