From 5dd03f1c0c3fbb6b712bc656f9cab7946316d5c0 Mon Sep 17 00:00:00 2001 From: Pragmatic Software Date: Sat, 25 Jan 2020 12:28:05 -0800 Subject: [PATCH] Massive refactor: added support for generic users! Renamed data/admins to data/users Moved Admins.pm to Users.pm Moved various subroutines in AdminCommands.pm to more appropriate locations Deleted AdminCommands.pm Improvements to Users.pm Added `my` command --- PBot/AdminCommands.pm | 394 ----------------------------------- PBot/Admins.pm | 218 -------------------- PBot/AntiFlood.pm | 2 +- PBot/ChanOpCommands.pm | 14 +- PBot/Channels.pm | 26 +++ PBot/Commands.pm | 25 ++- PBot/FactoidCommands.pm | 78 +------ PBot/Factoids.pm | 4 +- PBot/IRCHandlers.pm | 4 +- PBot/IgnoreList.pm | 12 +- PBot/Interpreter.pm | 2 +- PBot/MessageHistory.pm | 2 +- PBot/NickList.pm | 35 ++-- PBot/PBot.pm | 227 +++++++++++++++++++- PBot/StdinReader.pm | 10 +- PBot/Users.pm | 443 ++++++++++++++++++++++++++++++++++++++++ 16 files changed, 759 insertions(+), 737 deletions(-) delete mode 100644 PBot/AdminCommands.pm delete mode 100644 PBot/Admins.pm create mode 100644 PBot/Users.pm diff --git a/PBot/AdminCommands.pm b/PBot/AdminCommands.pm deleted file mode 100644 index d545b3c5..00000000 --- a/PBot/AdminCommands.pm +++ /dev/null @@ -1,394 +0,0 @@ -# File: AdminCommands.pm -# Author: pragma_ -# -# Purpose: Administrative command subroutines. - -# 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::AdminCommands; - -use warnings; -use strict; - -use feature 'unicode_strings'; - -use feature 'switch'; -no if $] >= 5.018, warnings => "experimental::smartmatch"; - -use Carp (); - -sub new { - Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref($_[1]) eq 'HASH'; - my ($class, %conf) = @_; - my $self = bless {}, $class; - $self->initialize(%conf); - return $self; -} - -sub initialize { - my ($self, %conf) = @_; - - $self->{pbot} = $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); - - $self->{pbot}->{commands}->register(sub { return $self->login(@_) }, "login", 0); - $self->{pbot}->{commands}->register(sub { return $self->logout(@_) }, "logout", 0); - $self->{pbot}->{commands}->register(sub { return $self->in_channel(@_) }, "in", 0); - $self->{pbot}->{commands}->register(sub { return $self->join_channel(@_) }, "join", 40); - $self->{pbot}->{commands}->register(sub { return $self->part_channel(@_) }, "part", 40); - $self->{pbot}->{commands}->register(sub { return $self->ack_die(@_) }, "die", 90); - $self->{pbot}->{commands}->register(sub { return $self->adminadd(@_) }, "adminadd", 60); - $self->{pbot}->{commands}->register(sub { return $self->adminrem(@_) }, "adminrem", 60); - $self->{pbot}->{commands}->register(sub { return $self->adminset(@_) }, "adminset", 60); - $self->{pbot}->{commands}->register(sub { return $self->adminunset(@_) }, "adminunset", 60); - $self->{pbot}->{commands}->register(sub { return $self->sl(@_) }, "sl", 90); - $self->{pbot}->{commands}->register(sub { return $self->export(@_) }, "export", 90); - $self->{pbot}->{commands}->register(sub { return $self->reload(@_) }, "reload", 90); - $self->{pbot}->{commands}->register(sub { return $self->evalcmd(@_) }, "eval", 99); -} - -sub sl { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - - if (not length $arguments) { - return "Usage: sl "; - } - - $self->{pbot}->{conn}->sl($arguments); - return ""; -} - -sub in_channel { - my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; - - my $usage = "Usage: in "; - - if (not $arguments) { - return $usage; - } - - my ($channel, $command) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 2); - return $usage if not defined $channel or not defined $command; - - $stuff->{admin_channel_override} = $channel; - $stuff->{command} = $command; - return $self->{pbot}->{interpreter}->interpret($stuff); -} - -sub login { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - my $channel = $from; - - if (not $arguments) { - return "Usage: login [channel] password"; - } - - if ($arguments =~ m/^([^ ]+)\s+(.+)/) { - $channel = $1; - $arguments = $2; - } - - if ($self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host")) { - return "/msg $nick You are already logged into channel $channel."; - } - - my $result = $self->{pbot}->{admins}->login($channel, "$nick!$user\@$host", $arguments); - return "/msg $nick $result"; -} - -sub logout { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - return "/msg $nick Uh, you aren't logged into channel $from." if (not $self->{pbot}->{admins}->loggedin($from, "$nick!$user\@$host")); - $self->{pbot}->{admins}->logout($from, "$nick!$user\@$host"); - return "/msg $nick Good-bye, $nick."; -} - -sub adminadd { - my $self = shift; - my ($from, $nick, $user, $host, $arguments, $stuff) = @_; - - my ($name, $channel, $hostmask, $level, $password) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 5); - - if (not defined $name or not defined $channel or not defined $hostmask or not defined $level - or not defined $password) { - return "/msg $nick Usage: adminadd "; - } - - $channel = '.*' if lc $channel eq 'global'; - - my $admin = $self->{pbot}->{admins}->find_admin($from, "$nick!$user\@$host"); - - if (not $admin) { - return "You are not an admin in $from.\n"; - } - - if ($admin->{level} < 90 and $level > 60) { - return "You may not set admin level higher than 60.\n"; - } - - $self->{pbot}->{admins}->add_admin($name, $channel, $hostmask, $level, $password); - return "Admin added."; -} - -sub adminrem { - my $self = shift; - my ($from, $nick, $user, $host, $arguments, $stuff) = @_; - - my ($channel, $hostmask) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 2); - - if (not defined $channel or not defined $hostmask) { - return "/msg $nick Usage: adminrem "; - } - - $channel = lc $channel; - $hostmask = lc $hostmask; - - $channel = '.*' if $channel eq 'global'; - - if (exists $self->{pbot}->{admins}->{admins}->{hash}->{$channel}) { - if (not exists $self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$hostmask}) { - foreach my $mask (keys %{ $self->{pbot}->{admins}->{admins}->{hash}->{$channel} }) { - next if $mask eq '_name'; - if ($self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$mask}->{name} eq $hostmask) { - $hostmask = $mask; - last; - } - } - } - } - - return $self->{pbot}->{admins}->remove_admin($channel, $hostmask); -} - -sub adminset { - my $self = shift; - my ($from, $nick, $user, $host, $arguments, $stuff) = @_; - my ($channel, $hostmask, $key, $value) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 4); - - if (not defined $channel or not defined $hostmask) { - return "Usage: adminset [key] [value]"; - } - - $channel = lc $channel; - $hostmask = lc $hostmask; - - $channel = '.*' if $channel eq 'global'; - - if (exists $self->{pbot}->{admins}->{admins}->{hash}->{$channel}) { - if (not exists $self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$hostmask}) { - foreach my $mask (keys %{ $self->{pbot}->{admins}->{admins}->{hash}->{$channel} }) { - next if $mask eq '_name'; - if ($self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$mask}->{name} eq $hostmask) { - $hostmask = $mask; - last; - } - } - } - } - - my $admin = $self->{pbot}->{admins}->find_admin($from, "$nick!$user\@$host"); - my $target = $self->{pbot}->{admins}->find_admin($channel, $hostmask); - - if (not $admin) { - return "You are not an admin in $from."; - } - - if (not $target) { - return "There is no admin $hostmask in channel $channel."; - } - - if ($key eq 'level' && $admin->{level} < 90 and $value > 60) { - return "You may not set admin level higher than 60.\n"; - } - - if ($target->{level} > $admin->{level}) { - return "You may not modify admins higher in level than you."; - } - - my $result = $self->{pbot}->{admins}->{admins}->set($channel, $hostmask, $key, $value); - $result =~ s/^password => .*;$/password => ;/m; - return $result; -} - -sub adminunset { - my $self = shift; - my ($from, $nick, $user, $host, $arguments, $stuff) = @_; - my ($channel, $hostmask, $key) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 3); - - if (not defined $channel or not defined $hostmask) { - return "Usage: adminunset "; - } - - $channel = lc $channel; - $hostmask = lc $hostmask; - - $channel = '.*' if $channel eq 'global'; - - if (exists $self->{pbot}->{admins}->{admins}->{hash}->{$channel}) { - if (not exists $self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$hostmask}) { - foreach my $mask (keys %{ $self->{pbot}->{admins}->{admins}->{hash}->{$channel} }) { - next if $mask eq '_name'; - if ($self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$mask}->{name} eq $hostmask) { - $hostmask = $mask; - last; - } - } - } - } - - return $self->{pbot}->{admins}->{admins}->unset($channel, $hostmask, $key); -} - - -sub join_channel { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - - foreach my $channel (split /[\s+,]/, $arguments) { - $self->{pbot}->{logger}->log("$nick!$user\@$host made me join $channel\n"); - $self->{pbot}->{chanops}->join_channel($channel); - } - - return "/msg $nick Joining $arguments"; -} - -sub part_channel { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - - $arguments = $from if not $arguments; - - foreach my $channel (split /[\s+,]/, $arguments) { - $self->{pbot}->{logger}->log("$nick!$user\@$host made me part $channel\n"); - $self->{pbot}->{chanops}->part_channel($channel); - } - - return "/msg $nick Parting $arguments"; -} - -sub ack_die { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - $self->{pbot}->{logger}->log("$nick!$user\@$host made me exit.\n"); - $self->{pbot}->atexit(); - $self->{pbot}->{conn}->privmsg($from, "Good-bye.") if defined $from; - $self->{pbot}->{conn}->quit("Departure requested."); - exit 0; -} - -sub export { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - - if (not defined $arguments) { - return "/msg $nick Usage: export "; - } - - if ($arguments =~ /^factoids$/i) { - return $self->{pbot}->{factoids}->export_factoids; - } -} - -sub evalcmd { - my ($self, $from, $nick, $user, $host, $arguments) = @_; - - $self->{pbot}->{logger}->log("[$from] $nick!$user\@$host Evaluating [$arguments]\n"); - - my $ret; - my $result = eval $arguments; - if ($@) { - if (length $result) { - $ret .= "[Error: $@] "; - } else { - $ret .= "Error: $@"; - } - $ret =~ s/ at \(eval \d+\) line 1.//; - } - return "/say $ret $result"; -} - -sub reload { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - - my %reloadables = ( - 'commands' => sub { - $self->{pbot}->{commands}->load_metadata; - return "Commands metadata reloaded."; - }, - - 'blacklist' => sub { - $self->{pbot}->{blacklist}->clear_blacklist; - $self->{pbot}->{blacklist}->load_blacklist; - return "Blacklist reloaded."; - }, - - 'whitelist' => sub { - $self->{pbot}->{antiflood}->{whitelist}->clear; - $self->{pbot}->{antiflood}->{whitelist}->load; - return "Whitelist reloaded."; - }, - - 'ignores' => sub { - $self->{pbot}->{ignorelist}->clear_ignores; - $self->{pbot}->{ignorelist}->load_ignores; - return "Ignore list reloaded."; - }, - - 'admins' => sub { - $self->{pbot}->{admins}->{admins}->clear; - $self->{pbot}->{admins}->load_admins; - return "Admins reloaded."; - }, - - 'channels' => sub { - $self->{pbot}->{channels}->{channels}->clear; - $self->{pbot}->{channels}->load_channels; - return "Channels reloaded."; - }, - - 'bantimeouts' => sub { - $self->{pbot}->{chanops}->{unban_timeout}->clear; - $self->{pbot}->{chanops}->{unban_timeout}->load; - return "Ban timeouts reloaded."; - }, - - 'mutetimeouts' => sub { - $self->{pbot}->{chanops}->{unmute_timeout}->clear; - $self->{pbot}->{chanops}->{unmute_timeout}->load; - return "Mute timeouts reloaded."; - }, - - 'registry' => sub { - $self->{pbot}->{registry}->{registry}->clear; - $self->{pbot}->{registry}->load; - return "Registry reloaded."; - }, - - 'factoids' => sub { - $self->{pbot}->{factoids}->{factoids}->clear; - $self->{pbot}->{factoids}->load_factoids; - return "Factoids reloaded."; - }, - - 'funcs' => sub { - $self->{pbot}->{func_cmd}->init_funcs; - return "Funcs reloaded."; - } - ); - - if (not length $arguments or not exists $reloadables{$arguments}) { - my $usage = 'Usage: reload <'; - $usage .= join '|', sort keys %reloadables; - $usage .= '>'; - return $usage; - } - - return $reloadables{$arguments}(); -} - -1; diff --git a/PBot/Admins.pm b/PBot/Admins.pm deleted file mode 100644 index 08c7a5da..00000000 --- a/PBot/Admins.pm +++ /dev/null @@ -1,218 +0,0 @@ -# File: Admins.pm -# Author: pragma_ -# -# Purpose: Manages list of bot admins and whether they are logged in. - -# 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::Admins; - -use warnings; -use strict; - -use feature 'unicode_strings'; - -use PBot::DualIndexHashObject; -use PBot::AdminCommands; - -use Carp (); - -sub new { - Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref($_[1]) eq 'HASH'; - my ($class, %conf) = @_; - my $self = bless {}, $class; - $self->initialize(%conf); - return $self; -} - -sub initialize { - my ($self, %conf) = @_; - $self->{pbot} = $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); - - $self->{admins} = PBot::DualIndexHashObject->new(name => 'Admins', filename => $conf{filename}, pbot => $conf{pbot}); - $self->load_admins; - - $self->{pbot}->{event_dispatcher}->register_handler('irc.join', sub { $self->on_join(@_) }); - - $self->{commands} = PBot::AdminCommands->new(pbot => $conf{pbot}); -} - -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); - ($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host); - - my $admin = $self->find_admin($channel, "$nick!$user\@$host"); - - if (defined $admin) { - if ($self->{pbot}->{chanops}->can_gain_ops($channel)) { - my $modes = '+'; - my $targets = ''; - - if ($admin->{autoop}) { - $self->{pbot}->{logger}->log("[admin] $nick!$user\@$host autoop in $channel\n"); - $modes .= 'o'; - $targets .= "$nick "; - } - - if ($admin->{autovoice}) { - $self->{pbot}->{logger}->log("[admin] $nick!$user\@$host autovoice in $channel\n"); - $modes .= 'v'; - $targets .= "$nick "; - } - - if (length $modes > 1) { - $self->{pbot}->{chanops}->add_op_command($channel, "mode $channel $modes $targets"); - $self->{pbot}->{chanops}->gain_ops($channel); - } - } - - if ($admin->{autologin}) { - $self->{pbot}->{logger}->log("[admin] $nick!$user\@$host autologin to $admin->{name} ($admin->{level}) for $channel\n"); - $admin->{loggedin} = 1; - } - } - return 0; -} - -sub add_admin { - my $self = shift; - my ($name, $channel, $hostmask, $level, $password, $dont_save) = @_; - $channel = '.*' if $channel !~ m/^#/; - - my $data = { - name => $name, - level => $level, - password => $password - }; - - $self->{pbot}->{logger}->log("Adding new level $level admin: [$name] [$hostmask] for channel [$channel]\n"); - $self->{admins}->add($channel, $hostmask, $data, $dont_save); -} - -sub remove_admin { - my $self = shift; - my ($channel, $hostmask) = @_; - return $self->{admins}->remove($channel, $hostmask); -} - -sub load_admins { - my $self = shift; - my $filename; - - if (@_) { $filename = shift; } else { $filename = $self->{admins}->{filename}; } - - if (not defined $filename) { - Carp::carp "No admins path specified -- skipping loading of admins"; - return; - } - - $self->{admins}->load; - - my $i = 0; - foreach my $channel (sort keys %{ $self->{admins}->{hash} } ) { - foreach my $hostmask (sort keys %{ $self->{admins}->{hash}->{$channel} }) { - next if $hostmask eq '_name'; - $i++; - my $name = $self->{admins}->{hash}->{$channel}->{$hostmask}->{name}; - my $level = $self->{admins}->{hash}->{$channel}->{$hostmask}->{level}; - my $password = $self->{admins}->{hash}->{$channel}->{$hostmask}->{password}; - - if (not defined $name or not defined $level or not defined $password) { - Carp::croak "An admin in $filename is missing critical data\n"; - } - - my $chan = $channel eq '.*' ? 'global' : $channel; - $self->{pbot}->{logger}->log("Adding new level $level $chan admin: $name $hostmask\n"); - } - } - - $self->{pbot}->{logger}->log(" $i admins loaded.\n"); -} - -sub save_admins { - my $self = shift; - $self->{admins}->save; -} - -sub find_admin { - my ($self, $from, $hostmask) = @_; - - $from = $self->{pbot}->{registry}->get_value('irc', 'botnick') if not defined $from; - $hostmask = '.*' if not defined $hostmask; - $hostmask = lc $hostmask; - - my $result = eval { - my $admin; - foreach my $channel_regex (keys %{ $self->{admins}->{hash} }) { - if ($from !~ m/^#/ or $from =~ m/^$channel_regex$/i) { - foreach my $hostmask_regex (keys %{ $self->{admins}->{hash}->{$channel_regex} }) { - next if $hostmask_regex eq '_name'; - if ($hostmask_regex =~ m/[*?]/) { - # contains * or ? so it's converted to a regex - my $hostmask_quoted = quotemeta $hostmask_regex; - $hostmask_quoted =~ s/\\\*/.*?/g; - $hostmask_quoted =~ s/\\\?/./g; - if ($hostmask =~ m/^$hostmask_quoted$/i) { - my $temp = $self->{admins}->{hash}->{$channel_regex}->{$hostmask_regex}; - $admin = $temp if not defined $admin or $admin->{level} < $temp->{level}; - } - } else { - # direct comparison - if ($hostmask eq lc $hostmask_regex) { - my $temp = $self->{admins}->{hash}->{$channel_regex}->{$hostmask_regex}; - $admin = $temp if not defined $admin or $admin->{level} < $temp->{level}; - } - } - } - } - } - return $admin; - }; - - if ($@) { - $self->{pbot}->{logger}->log("Error in find_admin parameters: $@\n"); - } - - return $result; -} - -sub loggedin { - my ($self, $channel, $hostmask) = @_; - my $admin = $self->find_admin($channel, $hostmask); - - if (defined $admin && $admin->{loggedin}) { - return $admin; - } else { - return undef; - } -} - -sub login { - my ($self, $channel, $hostmask, $password) = @_; - my $admin = $self->find_admin($channel, $hostmask); - - if (not defined $admin) { - $self->{pbot}->{logger}->log("Attempt to login non-existent [$channel][$hostmask] failed\n"); - return "You do not have an account in $channel."; - } - - if ($admin->{password} ne $password) { - $self->{pbot}->{logger}->log("Bad login password for [$channel][$hostmask]\n"); - return "I don't think so."; - } - - $admin->{loggedin} = 1; - $self->{pbot}->{logger}->log("$hostmask logged into $channel\n"); - return "Logged into $channel."; -} - -sub logout { - my ($self, $channel, $hostmask) = @_; - my $admin = $self->find_admin($channel, $hostmask); - delete $admin->{loggedin} if defined $admin; -} - -1; diff --git a/PBot/AntiFlood.pm b/PBot/AntiFlood.pm index cd3f4460..c63a7c1f 100644 --- a/PBot/AntiFlood.pm +++ b/PBot/AntiFlood.pm @@ -446,7 +446,7 @@ sub check_flood { } # do not do flood enforcement for logged in bot admins - if ($self->{pbot}->{registry}->get_value('antiflood', 'dont_enforce_admins') and $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host")) { + if ($self->{pbot}->{registry}->get_value('antiflood', 'dont_enforce_admins') and $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { $self->{channels}->{$channel}->{last_spoken_nick} = $nick; next; } diff --git a/PBot/ChanOpCommands.pm b/PBot/ChanOpCommands.pm index 9dbdc993..c4668cb7 100644 --- a/PBot/ChanOpCommands.pm +++ b/PBot/ChanOpCommands.pm @@ -251,7 +251,7 @@ sub mode { # removing mode -- check against whitelist, etc next if $nick_data->{nick} eq $self->{pbot}->{registry}->get_value('irc', 'botnick'); next if $self->{pbot}->{antiflood}->whitelisted($channel, $nick_data->{hostmask}); - next if $self->{pbot}->{admins}->loggedin($channel, $nick_data->{hostmask}); + next if $self->{pbot}->{users}->loggedin_admin($channel, $nick_data->{hostmask}); } # skip nick if already has mode set/unset @@ -418,7 +418,7 @@ sub ban_user { my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick'); return "I don't think so." if $target =~ /^\Q$botnick\E!/i; - if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host")) { + if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { return "/msg $nick You are not an admin for $channel."; } @@ -485,7 +485,7 @@ sub unban_user { return "/msg $nick Usage for /msg: unban [false value to use unban queue]" if $channel !~ /^#/; - if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host")) { + if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { return "/msg $nick You are not an admin for $channel."; } @@ -549,7 +549,7 @@ sub mute_user { my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick'); return "I don't think so." if $target =~ /^\Q$botnick\E!/i; - if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host")) { + if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { return "/msg $nick You are not an admin for $channel."; } @@ -616,7 +616,7 @@ sub unmute_user { return "/msg $nick Usage for /msg: unmute [false value to use unban queue]" if $channel !~ /^#/; - if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host")) { + if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { return "/msg $nick You are not an admin for $channel."; } @@ -670,7 +670,7 @@ sub kick_user { $channel = $1; } - if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host")) { + if ($self->{pbot}->{commands}->get_meta($stuff->{keyword}, 'level') and not $stuff->{'effective-level'} and not $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { return "/msg $nick You are not an admin for $channel."; } @@ -704,7 +704,7 @@ sub kick_user { next if $nick_data->{nick} eq $self->{pbot}->{registry}->get_value('irc', 'botnick'); next if $self->{pbot}->{antiflood}->whitelisted($channel, $nick_data->{hostmask}); - next if $self->{pbot}->{admins}->loggedin($channel, $nick_data->{hostmask}); + next if $self->{pbot}->{users}->loggedin_admin($channel, $nick_data->{hostmask}); $self->{pbot}->{chanops}->add_op_command($channel, "kick $channel $nl $reason"); } diff --git a/PBot/Channels.pm b/PBot/Channels.pm index e25d78d9..36cbdb1f 100644 --- a/PBot/Channels.pm +++ b/PBot/Channels.pm @@ -33,6 +33,8 @@ sub initialize { $self->{channels} = PBot::HashObject->new(pbot => $self->{pbot}, name => 'Channels', filename => $conf{filename}); $self->load_channels; + $self->{pbot}->{commands}->register(sub { $self->join(@_) }, "join", 40); + $self->{pbot}->{commands}->register(sub { $self->part(@_) }, "part", 40); $self->{pbot}->{commands}->register(sub { $self->set(@_) }, "chanset", 40); $self->{pbot}->{commands}->register(sub { $self->unset(@_) }, "chanunset", 40); $self->{pbot}->{commands}->register(sub { $self->add(@_) }, "chanadd", 40); @@ -40,6 +42,30 @@ sub initialize { $self->{pbot}->{commands}->register(sub { $self->list(@_) }, "chanlist", 10); } +sub join { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + + foreach my $channel (split /[\s+,]/, $arguments) { + $self->{pbot}->{logger}->log("$nick!$user\@$host made me join $channel\n"); + $self->{pbot}->{chanops}->join_channel($channel); + } + + return "/msg $nick Joining $arguments"; +} + +sub part { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + + $arguments = $from if not $arguments; + + foreach my $channel (split /[\s+,]/, $arguments) { + $self->{pbot}->{logger}->log("$nick!$user\@$host made me part $channel\n"); + $self->{pbot}->{chanops}->part_channel($channel); + } + + return "/msg $nick Parting $arguments"; +} + sub set { my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; my ($channel, $key, $value) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 3); diff --git a/PBot/Commands.pm b/PBot/Commands.pm index d4d60c90..159b8733 100644 --- a/PBot/Commands.pm +++ b/PBot/Commands.pm @@ -39,10 +39,11 @@ sub initialize { $self->{metadata} = PBot::HashObject->new(pbot => $self->{pbot}, name => 'Commands', filename => $conf{filename}); $self->load_metadata; - $self->register(sub { $self->cmdset(@_) }, "cmdset", 90); - $self->register(sub { $self->cmdunset(@_) }, "cmdunset", 90); - $self->register(sub { $self->help(@_) }, "help", 0); - $self->register(sub { $self->uptime(@_) }, "uptime", 0); + $self->register(sub { $self->cmdset(@_) }, "cmdset", 90); + $self->register(sub { $self->cmdunset(@_) }, "cmdunset", 90); + $self->register(sub { $self->help(@_) }, "help", 0); + $self->register(sub { $self->uptime(@_) }, "uptime", 0); + $self->register(sub { $self->in_channel(@_) }, "in", 0); } sub register { @@ -97,7 +98,7 @@ sub interpreter { my $from = exists $stuff->{admin_channel_override} ? $stuff->{admin_channel_override} : $stuff->{from}; my ($admin_channel) = $stuff->{arguments} =~ m/\B(#[^ ]+)/; # assume first channel-like argument $admin_channel = $from if not defined $admin_channel; - my $admin = $self->{pbot}->{admins}->loggedin($admin_channel, "$stuff->{nick}!$stuff->{user}\@$stuff->{host}"); + my $admin = $self->{pbot}->{users}->loggedin_admin($admin_channel, "$stuff->{nick}!$stuff->{user}\@$stuff->{host}"); my $admin_level = defined $admin ? $admin->{level} : 0; my $keyword = lc $stuff->{keyword}; @@ -244,4 +245,18 @@ sub uptime { return localtime ($self->{pbot}->{startup_timestamp}) . " [" . duration (time - $self->{pbot}->{startup_timestamp}) . "]"; } +sub in_channel { + my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; + + my $usage = "Usage: in "; + return $usage if not $arguments; + + my ($channel, $command) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 2); + return $usage if not defined $channel or not defined $command; + + $stuff->{admin_channel_override} = $channel; + $stuff->{command} = $command; + return $self->{pbot}->{interpreter}->interpret($stuff); +} + 1; diff --git a/PBot/FactoidCommands.pm b/PBot/FactoidCommands.pm index 94fc9059..2563c2e6 100644 --- a/PBot/FactoidCommands.pm +++ b/PBot/FactoidCommands.pm @@ -78,7 +78,6 @@ sub initialize { $self->{pbot}->{commands}->register(sub { return $self->factmove(@_) }, "factmove", 0); $self->{pbot}->{commands}->register(sub { return $self->call_factoid(@_) }, "fact", 0); $self->{pbot}->{commands}->register(sub { return $self->factfind(@_) }, "factfind", 0); - $self->{pbot}->{commands}->register(sub { return $self->list(@_) }, "list", 0); $self->{pbot}->{commands}->register(sub { return $self->top20(@_) }, "top20", 0); $self->{pbot}->{commands}->register(sub { return $self->load_module(@_) }, "load", 90); $self->{pbot}->{commands}->register(sub { return $self->unload_module(@_) }, "unload", 90); @@ -408,7 +407,7 @@ sub factundo { } my $factoids = $self->{pbot}->{factoids}->{factoids}->{hash}; - my $admininfo = $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host"); + my $admininfo = $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host"); if ($factoids->{$channel}->{$trigger}->{'locked'}) { return "/say $trigger_name is locked and cannot be reverted." if not defined $admininfo; @@ -501,7 +500,7 @@ sub factredo { } my $factoids = $self->{pbot}->{factoids}->{factoids}->{hash}; - my $admininfo = $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host"); + my $admininfo = $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host"); if ($factoids->{$channel}->{$trigger}->{'locked'}) { return "/say $trigger_name is locked and cannot be reverted." if not defined $admininfo; @@ -563,9 +562,9 @@ sub factset { my $admininfo; if (defined $owner_channel) { - $admininfo = $self->{pbot}->{admins}->loggedin($owner_channel, "$nick!$user\@$host"); + $admininfo = $self->{pbot}->{users}->loggedin_admin($owner_channel, "$nick!$user\@$host"); } else { - $admininfo = $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host"); + $admininfo = $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host"); } my $level = 0; @@ -656,9 +655,9 @@ sub factunset { my $admininfo; if (defined $owner_channel) { - $admininfo = $self->{pbot}->{admins}->loggedin($owner_channel, "$nick!$user\@$host"); + $admininfo = $self->{pbot}->{users}->loggedin_admin($owner_channel, "$nick!$user\@$host"); } else { - $admininfo = $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host"); + $admininfo = $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host"); } my $level = 0; @@ -723,63 +722,6 @@ sub factunset { return $result; } -sub list { - my $self = shift; - my ($from, $nick, $user, $host, $arguments) = @_; - my $text; - - my $usage = "Usage: list "; - - if (not defined $arguments) { - return $usage; - } - - if ($arguments =~ /^modules$/i) { - $text = "Loaded modules: "; - foreach my $channel (sort keys %{ $self->{pbot}->{factoids}->{factoids}->{hash} }) { - foreach my $command (sort keys %{ $self->{pbot}->{factoids}->{factoids}->{hash}->{$channel} }) { - next if $command eq '_name'; - if ($self->{pbot}->{factoids}->{factoids}->{hash}->{$channel}->{$command}->{type} eq 'module') { - $text .= "$self->{pbot}->{factoids}->{factoids}->{hash}->{$channel}->{$command}->{_name} "; - } - } - } - return $text; - } - - if ($arguments =~ /^commands$/i) { - $text = "Registered commands: "; - foreach my $command (sort { $a->{name} cmp $b->{name} } @{ $self->{pbot}->{commands}->{handlers} }) { - $text .= "$command->{name} "; - $text .= "($command->{level}) " if $command->{level} > 0; - } - return $text; - } - - if ($arguments =~ /^admins$/i) { - $text = "Admins: "; - my $last_channel = ""; - my $sep = ""; - foreach my $channel (sort keys %{ $self->{pbot}->{admins}->{admins}->{hash} }) { - next if $from =~ m/^#/ and $channel ne $from and $channel ne '.*'; - if ($last_channel ne $channel) { - $text .= $sep . ($channel eq ".*" ? "global" : $channel) . ": "; - $last_channel = $channel; - $sep = ""; - } - foreach my $hostmask (sort { return 0 if $a eq '_name' or $b eq '_name'; $self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$a}->{name} cmp $self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$b}->{name} } keys %{ $self->{pbot}->{admins}->{admins}->{hash}->{$channel} }) { - next if $hostmask eq '_name'; - $text .= $sep; - $text .= "*" if $self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$hostmask}->{loggedin}; - $text .= $self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$hostmask}->{name} . " (" . $self->{pbot}->{admins}->{admins}->{hash}->{$channel}->{$hostmask}->{level} . ")"; - $sep = "; "; - } - } - return $text; - } - return $usage; -} - sub factmove { my $self = shift; my ($from, $nick, $user, $host, $arguments, $stuff) = @_; @@ -827,7 +769,7 @@ sub factmove { my ($owner) = $factoids->{$found_src_channel}->{$found_source}->{'owner'} =~ m/([^!]+)/; - if ((lc $nick ne lc $owner) and (not $self->{pbot}->{admins}->loggedin($found_src_channel, "$nick!$user\@$host"))) { + if ((lc $nick ne lc $owner) and (not $self->{pbot}->{users}->loggedin_admin($found_src_channel, "$nick!$user\@$host"))) { $self->{pbot}->{logger}->log("$nick!$user\@$host attempted to move [$found_src_channel] $found_source (not owner)\n"); my $chan = ($found_src_channel eq '.*' ? 'the global channel' : $found_src_channel); return "You are not the owner of $source_trigger_name for $source_channel_name."; @@ -1080,7 +1022,7 @@ sub factadd { my ($owner) = $factoids->{$channel}->{$trigger}->{'owner'} =~ m/([^!]+)/; - if ((lc $nick ne lc $owner) and (not $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host"))) { + if ((lc $nick ne lc $owner) and (not $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host"))) { return "You are not the owner of $trigger_name for $channel_name; cannot force overwrite."; } } @@ -1136,7 +1078,7 @@ sub factrem { my ($owner) = $factoids->{$channel}->{$trigger}->{'owner'} =~ m/([^!]+)/; - if ((lc $nick ne lc $owner) and (not $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host"))) { + if ((lc $nick ne lc $owner) and (not $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host"))) { return "You are not the owner of $trigger_name for $channel_name."; } @@ -1682,7 +1624,7 @@ sub factchange { return "/say $trigger_name belongs to $channel_name, but this is $from_chan. Please switch to $channel_name or use /msg to change this factoid."; } - my $admininfo = $self->{pbot}->{admins}->loggedin($channel, "$nick!$user\@$host"); + my $admininfo = $self->{pbot}->{users}->loggedin_admin($channel, "$nick!$user\@$host"); if ($factoids_hash->{$channel}->{$trigger}->{'locked'}) { return "/say $trigger_name is locked and cannot be changed." if not defined $admininfo; diff --git a/PBot/Factoids.pm b/PBot/Factoids.pm index 840e176e..843d84d9 100644 --- a/PBot/Factoids.pm +++ b/PBot/Factoids.pm @@ -876,7 +876,7 @@ sub interpreter { $ratelimit = $self->{factoids}->{hash}->{$channel}->{$keyword}->{rate_limit} if not defined $ratelimit; if (gettimeofday - $self->{factoids}->{hash}->{$channel}->{$keyword}->{last_referenced_on} < $ratelimit) { my $ref_from = $stuff->{ref_from} ? "[$stuff->{ref_from}] " : ""; - return "/msg $stuff->{nick} $ref_from'$trigger_name' is rate-limited; try again in " . duration ($ratelimit - int(gettimeofday - $self->{factoids}->{hash}->{$channel}->{$keyword}->{last_referenced_on})) . "." unless $self->{pbot}->{admins}->loggedin($channel, "$stuff->{nick}!$stuff->{user}\@$stuff->{host}"); + return "/msg $stuff->{nick} $ref_from'$trigger_name' is rate-limited; try again in " . duration ($ratelimit - int(gettimeofday - $self->{factoids}->{hash}->{$channel}->{$keyword}->{last_referenced_on})) . "." unless $self->{pbot}->{users}->loggedin_admin($channel, "$stuff->{nick}!$stuff->{user}\@$stuff->{host}"); } } } @@ -1067,7 +1067,7 @@ sub handle_action { elsif ($self->{factoids}->{hash}->{$channel}->{$keyword}->{type} eq 'text') { # Don't allow user-custom /msg factoids, unless factoid triggered by admin if ($action =~ m/^\/msg/i) { - my $admin = $self->{pbot}->{admins}->loggedin($stuff->{from}, "$stuff->{nick}!$stuff->{user}\@$stuff->{host}"); + my $admin = $self->{pbot}->{users}->loggedin_admin($stuff->{from}, "$stuff->{nick}!$stuff->{user}\@$stuff->{host}"); if (not $admin or $admin->{level} < 60) { $self->{pbot}->{logger}->log("[ABUSE] Bad factoid (contains /msg): $action\n"); return "You are not powerful enough to use /msg in a factoid."; diff --git a/PBot/IRCHandlers.pm b/PBot/IRCHandlers.pm index c605ab5e..c75ba149 100644 --- a/PBot/IRCHandlers.pm +++ b/PBot/IRCHandlers.pm @@ -448,9 +448,9 @@ sub on_departure { $self->{pbot}->{registry}->get_value('antiflood', 'join_flood_time_threshold'), $self->{pbot}->{messagehistory}->{MSG_DEPARTURE}); - my $admin = $self->{pbot}->{admins}->find_admin($channel, "$nick!$user\@$host"); + # auto-logout admins but not users + my $admin = $self->{pbot}->{users}->find_admin($channel, "$nick!$user\@$host"); if (defined $admin and $admin->{loggedin} and not $admin->{stayloggedin}) { - $self->{pbot}->{logger}->log("Whoops, $nick left while still logged in.\n"); $self->{pbot}->{logger}->log("Logged out $nick.\n"); delete $admin->{loggedin}; } diff --git a/PBot/IgnoreList.pm b/PBot/IgnoreList.pm index e3d0366e..b806595a 100644 --- a/PBot/IgnoreList.pm +++ b/PBot/IgnoreList.pm @@ -18,12 +18,8 @@ use PBot::IgnoreListCommands; use Time::HiRes qw(gettimeofday); sub new { - if (ref($_[1]) eq 'HASH') { - Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference"); - } - + Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref($_[1]) eq 'HASH'; my ($class, %conf) = @_; - my $self = bless {}, $class; $self->initialize(%conf); return $self; @@ -32,8 +28,8 @@ sub new { sub initialize { my ($self, %conf) = @_; - $self->{pbot} = delete $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); - $self->{filename} = delete $conf{filename}; + $self->{pbot} = $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); + $self->{filename} = $conf{filename}; $self->{ignore_list} = {}; $self->{ignore_flood_counter} = {}; @@ -41,7 +37,7 @@ sub initialize { $self->{commands} = PBot::IgnoreListCommands->new(pbot => $self->{pbot}); - $self->load_ignores; + $self->load_ignores(); $self->{pbot}->{timer}->register(sub { $self->check_ignore_timeouts }, 10); } diff --git a/PBot/Interpreter.pm b/PBot/Interpreter.pm index 8109e0b5..4a83287a 100644 --- a/PBot/Interpreter.pm +++ b/PBot/Interpreter.pm @@ -173,7 +173,7 @@ sub process_line { foreach $command (@commands) { # check if user is ignored (and command isn't `login`) if ($command !~ /^login / && defined $from && $pbot->{ignorelist}->check_ignore($nick, $user, $host, $from)) { - my $admin = $pbot->{admins}->loggedin($from, "$nick!$user\@$host"); + my $admin = $pbot->{users}->loggedin_admin($from, "$nick!$user\@$host"); if (!defined $admin || $admin->{level} < 10) { # hostmask ignored return 1; diff --git a/PBot/MessageHistory.pm b/PBot/MessageHistory.pm index 1c7f19e9..70525b04 100644 --- a/PBot/MessageHistory.pm +++ b/PBot/MessageHistory.pm @@ -397,7 +397,7 @@ sub recall_message { foreach my $msg (@$messages) { $self->{pbot}->{logger}->log("$nick ($from) recalled <$msg->{nick}/$msg->{channel}> $msg->{msg}\n"); - if ($max_recall_time && gettimeofday - $msg->{timestamp} > $max_recall_time && not $self->{pbot}->{admins}->loggedin($from, "$nick!$user\@$host")) { + if ($max_recall_time && gettimeofday - $msg->{timestamp} > $max_recall_time && not $self->{pbot}->{users}->loggedin_admin($from, "$nick!$user\@$host")) { $max_recall_time = duration($max_recall_time); $recall_text .= "Sorry, you can not recall messages older than $max_recall_time."; return $recall_text; diff --git a/PBot/NickList.pm b/PBot/NickList.pm index b25e9f9b..8a2c34fb 100644 --- a/PBot/NickList.pm +++ b/PBot/NickList.pm @@ -58,29 +58,24 @@ sub show_nicklist { my ($self, $from, $nick, $user, $host, $arguments) = @_; my $nicklist; - my $admin = $self->{pbot}->{admins}->loggedin($from, "$nick!$user\@$host"); - if (not length $arguments) { - if (not $admin) { - return "Usage: nicklist [nick]"; - } - $nicklist = Dumper($self->{nicklist}); - } else { - my @args = split / /, $arguments; + return "Usage: nicklist [nick]"; + } - if (@args == 1) { - if (not exists $self->{nicklist}->{$arguments}) { - return "No nicklist for $arguments."; - } - $nicklist = Dumper($self->{nicklist}->{$arguments}); - } else { - if (not exists $self->{nicklist}->{$args[0]}) { - return "No nicklist for $args[0]."; - } elsif (not exists $self->{nicklist}->{$args[0]}->{$args[1]}) { - return "No such nick $args[1] in channel $args[0]."; - } - $nicklist = Dumper($self->{nicklist}->{$args[0]}->{$args[1]}); + my @args = split / /, $arguments; + + if (@args == 1) { + if (not exists $self->{nicklist}->{lc $arguments}) { + return "No nicklist for $arguments."; } + $nicklist = Dumper($self->{nicklist}->{lc $arguments}); + } else { + if (not exists $self->{nicklist}->{lc $args[0]}) { + return "No nicklist for $args[0]."; + } elsif (not exists $self->{nicklist}->{lc $args[0]}->{lc $args[1]}) { + return "No such nick $args[1] in channel $args[0]."; + } + $nicklist = Dumper($self->{nicklist}->{lc $args[0]}->{lc $args[1]}); } return $nicklist; diff --git a/PBot/PBot.pm b/PBot/PBot.pm index 43c80755..e41a6c9e 100644 --- a/PBot/PBot.pm +++ b/PBot/PBot.pm @@ -37,7 +37,7 @@ use PBot::Interpreter; use PBot::Commands; use PBot::ChanOps; use PBot::Factoids; -use PBot::Admins; +use PBot::Users; use PBot::IgnoreList; use PBot::BlackList; use PBot::Timer; @@ -102,7 +102,15 @@ sub initialize { # then commands so the modules can register new commands $self->{commands} = PBot::Commands->new(pbot => $self, filename => "$data_dir/commands", %conf); - # the version + # add some commands + $self->{commands}->register(sub { $self->listcmd(@_) }, "list", 0); + $self->{commands}->register(sub { $self->ack_die(@_) }, "die", 90); + $self->{commands}->register(sub { $self->export(@_) }, "export", 90); + $self->{commands}->register(sub { $self->reload(@_) }, "reload", 90); + $self->{commands}->register(sub { $self->evalcmd(@_) }, "eval", 99); + $self->{commands}->register(sub { $self->sl(@_) }, "sl", 90); + + # prepare the version $self->{version} = PBot::VERSION->new(pbot => $self, %conf); $self->{logger}->log($self->{version}->version . "\n"); $self->{logger}->log("Args: @ARGV\n") if @ARGV; @@ -187,7 +195,7 @@ sub initialize { $self->{event_dispatcher} = PBot::EventDispatcher->new(pbot => $self, %conf); $self->{irchandlers} = PBot::IRCHandlers->new(pbot => $self, %conf); $self->{select_handler} = PBot::SelectHandler->new(pbot => $self, %conf); - $self->{admins} = PBot::Admins->new(pbot => $self, filename => "$data_dir/admins", %conf); + $self->{users} = PBot::Users->new(pbot => $self, filename => "$data_dir/users", %conf); $self->{stdin_reader} = PBot::StdinReader->new(pbot => $self, %conf); $self->{bantracker} = PBot::BanTracker->new(pbot => $self, %conf); $self->{lagchecker} = PBot::LagChecker->new(pbot => $self, %conf); @@ -219,9 +227,11 @@ sub initialize { } sub random_nick { + my ($self, $length) = @_; + $length //= 9; my @chars = ("A".."Z", "a".."z", "0".."9"); my $nick = $chars[rand @chars - 10]; # nicks cannot start with a digit - $nick .= $chars[rand @chars] for 1..9; + $nick .= $chars[rand @chars] for 1..$length; return $nick; } @@ -238,7 +248,7 @@ sub connect { $self->{logger}->log("Connecting to $server ...\n"); while (not $self->{conn} = $self->{irc}->newconn( - Nick => $self->{registry}->get_value('irc', 'randomize_nick') ? random_nick : $self->{registry}->get_value('irc', 'botnick'), + Nick => $self->{registry}->get_value('irc', 'randomize_nick') ? $self->random_nick : $self->{registry}->get_value('irc', 'botnick'), Username => $self->{registry}->get_value('irc', 'username'), Ircname => $self->{registry}->get_value('irc', 'realname'), Server => $server, @@ -305,4 +315,211 @@ sub change_botnick_trigger { $self->{conn}->nick($newvalue) if $self->{connected}; } +sub listcmd { + my $self = shift; + my ($from, $nick, $user, $host, $arguments) = @_; + my $text; + + my $usage = "Usage: list "; + + if (not defined $arguments) { + return $usage; + } + + if ($arguments =~ /^modules$/i) { + $text = "Loaded modules: "; + foreach my $channel (sort keys %{ $self->{factoids}->{factoids}->{hash} }) { + foreach my $command (sort keys %{ $self->{factoids}->{factoids}->{hash}->{$channel} }) { + next if $command eq '_name'; + if ($self->{factoids}->{factoids}->{hash}->{$channel}->{$command}->{type} eq 'module') { + $text .= "$self->{factoids}->{factoids}->{hash}->{$channel}->{$command}->{_name} "; + } + } + } + return $text; + } + + if ($arguments =~ /^commands$/i) { + $text = "Registered commands: "; + foreach my $command (sort { $a->{name} cmp $b->{name} } @{ $self->{commands}->{handlers} }) { + $text .= "$command->{name} "; + $text .= "($command->{level}) " if $command->{level} > 0; + } + return $text; + } + + if ($arguments =~ /^users$/i) { + $text = "Users: "; + my $last_channel = ""; + my $sep = ""; + foreach my $channel (sort keys %{ $self->{users}->{users}->{hash} }) { + next if $from =~ m/^#/ and $channel ne $from and $channel ne '.*'; + if ($last_channel ne $channel) { + $text .= $sep . ($channel eq ".*" ? "global" : $channel) . ": "; + $last_channel = $channel; + $sep = ""; + } + foreach my $hostmask (sort { return 0 if $a eq '_name' or $b eq '_name'; $self->{users}->{users}->{hash}->{$channel}->{$a}->{name} cmp $self->{users}->{users}->{hash}->{$channel}->{$b}->{name} } keys %{ $self->{users}->{users}->{hash}->{$channel} }) { + next if $hostmask eq '_name'; + $text .= $sep; + $text .= "*" if $self->{users}->{users}->{hash}->{$channel}->{$hostmask}->{loggedin}; + $text .= $self->{users}->{users}->{hash}->{$channel}->{$hostmask}->{name}; + $text .= " (" . $self->{users}->{users}->{hash}->{$channel}->{$hostmask}->{level} . ")" if $self->{users}->{users}->{hash}->{$channel}->{$hostmask}->{level} > 0; + $sep = "; "; + } + } + return $text; + } + + if ($arguments =~ /^admins$/i) { + $text = "Admins: "; + my $last_channel = ""; + my $sep = ""; + foreach my $channel (sort keys %{ $self->{users}->{users}->{hash} }) { + next if $from =~ m/^#/ and $channel ne $from and $channel ne '.*'; + if ($last_channel ne $channel) { + $text .= $sep . ($channel eq ".*" ? "global" : $channel) . ": "; + $last_channel = $channel; + $sep = ""; + } + foreach my $hostmask (sort { return 0 if $a eq '_name' or $b eq '_name'; $self->{users}->{users}->{hash}->{$channel}->{$a}->{name} cmp $self->{users}->{users}->{hash}->{$channel}->{$b}->{name} } keys %{ $self->{users}->{users}->{hash}->{$channel} }) { + next if $hostmask eq '_name'; + next if $self->{users}->{users}->{hash}->{$channel}->{$hostmask}->{level} <= 0; + $text .= $sep; + $text .= "*" if $self->{users}->{users}->{hash}->{$channel}->{$hostmask}->{loggedin}; + $text .= $self->{users}->{users}->{hash}->{$channel}->{$hostmask}->{name} . " (" . $self->{users}->{users}->{hash}->{$channel}->{$hostmask}->{level} . ")"; + $sep = "; "; + } + } + return $text; + } + return $usage; +} + +sub sl { + my $self = shift; + my ($from, $nick, $user, $host, $arguments) = @_; + return "Usage: sl " if not length $arguments; + $self->{conn}->sl($arguments); + return ""; +} + +sub ack_die { + my $self = shift; + my ($from, $nick, $user, $host, $arguments) = @_; + $self->{logger}->log("$nick!$user\@$host made me exit.\n"); + $self->atexit(); + $self->{conn}->privmsg($from, "Good-bye.") if defined $from; + $self->{conn}->quit("Departure requested."); + exit 0; +} + +sub export { + my $self = shift; + my ($from, $nick, $user, $host, $arguments) = @_; + + return "/msg $nick Usage: export " if not defined $arguments; + + if ($arguments =~ /^factoids$/i) { + return $self->{factoids}->export_factoids; + } +} + +sub evalcmd { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + + $self->{logger}->log("[$from] $nick!$user\@$host Evaluating [$arguments]\n"); + + my $ret; + my $result = eval $arguments; + if ($@) { + if (length $result) { + $ret .= "[Error: $@] "; + } else { + $ret .= "Error: $@"; + } + $ret =~ s/ at \(eval \d+\) line 1.//; + } + return "/say $ret $result"; +} + +sub reload { + my $self = shift; + my ($from, $nick, $user, $host, $arguments) = @_; + + my %reloadables = ( + 'commands' => sub { + $self->{commands}->load_metadata; + return "Commands metadata reloaded."; + }, + + 'blacklist' => sub { + $self->{blacklist}->clear_blacklist; + $self->{blacklist}->load_blacklist; + return "Blacklist reloaded."; + }, + + 'whitelist' => sub { + $self->{antiflood}->{whitelist}->clear; + $self->{antiflood}->{whitelist}->load; + return "Whitelist reloaded."; + }, + + 'ignores' => sub { + $self->{ignorelist}->clear_ignores; + $self->{ignorelist}->load_ignores; + return "Ignore list reloaded."; + }, + + 'users' => sub { + $self->{users}->load; + return "Users reloaded."; + }, + + 'channels' => sub { + $self->{channels}->{channels}->clear; + $self->{channels}->load_channels; + return "Channels reloaded."; + }, + + 'bantimeouts' => sub { + $self->{chanops}->{unban_timeout}->clear; + $self->{chanops}->{unban_timeout}->load; + return "Ban timeouts reloaded."; + }, + + 'mutetimeouts' => sub { + $self->{chanops}->{unmute_timeout}->clear; + $self->{chanops}->{unmute_timeout}->load; + return "Mute timeouts reloaded."; + }, + + 'registry' => sub { + $self->{registry}->{registry}->clear; + $self->{registry}->load; + return "Registry reloaded."; + }, + + 'factoids' => sub { + $self->{factoids}->{factoids}->clear; + $self->{factoids}->load_factoids; + return "Factoids reloaded."; + }, + + 'funcs' => sub { + $self->{func_cmd}->init_funcs; + return "Funcs reloaded."; + } + ); + + if (not length $arguments or not exists $reloadables{$arguments}) { + my $usage = 'Usage: reload <'; + $usage .= join '|', sort keys %reloadables; + $usage .= '>'; + return $usage; + } + + return $reloadables{$arguments}(); +} + 1; diff --git a/PBot/StdinReader.pm b/PBot/StdinReader.pm index f5fdc284..7d004d0f 100644 --- a/PBot/StdinReader.pm +++ b/PBot/StdinReader.pm @@ -26,12 +26,12 @@ sub initialize { $self->{pbot} = $conf{pbot} // Carp::croak("Missing pbot reference in StdinReader"); # create implicit bot-admin account for bot - my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick'); - if (not $self->{pbot}->{admins}->find_admin('.*', '*!stdin@pbot')) { + if (not $self->{pbot}->{users}->find_admin('.*', '*!stdin@pbot')) { + my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick'); $self->{pbot}->{logger}->log("Adding stdin admin *!stdin\@pbot...\n"); - $self->{pbot}->{admins}->add_admin($botnick, '.*', '*!stdin@pbot', 100, 'notused', 1); - $self->{pbot}->{admins}->login($botnick, "$botnick!stdin\@pbot", 'notused'); - $self->{pbot}->{admins}->save_admins; + $self->{pbot}->{users}->add_user($botnick, '.*', '*!stdin@pbot', 100, undef, 1); + $self->{pbot}->{users}->login($botnick, "$botnick!stdin\@pbot", undef); + $self->{pbot}->{users}->save; } # used to check whether process is in background or foreground, for stdin reading diff --git a/PBot/Users.pm b/PBot/Users.pm new file mode 100644 index 00000000..49afd166 --- /dev/null +++ b/PBot/Users.pm @@ -0,0 +1,443 @@ +# File: Users.pm +# Author: pragma_ +# +# Purpose: Manages list of bot users/admins and their metadata. + +# 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::Users; + +use warnings; +use strict; + +use feature 'unicode_strings'; + +use PBot::DualIndexHashObject; +use Carp (); + +sub new { + Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref($_[1]) eq 'HASH'; + my ($class, %conf) = @_; + my $self = bless {}, $class; + $self->initialize(%conf); + return $self; +} + +sub initialize { + my ($self, %conf) = @_; + $self->{pbot} = $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); + + $self->{users} = PBot::DualIndexHashObject->new(name => 'Users', filename => $conf{filename}, pbot => $conf{pbot}); + $self->load; + + $self->{pbot}->{commands}->register(sub { return $self->logincmd(@_) }, "login", 0); + $self->{pbot}->{commands}->register(sub { return $self->logoutcmd(@_) }, "logout", 0); + $self->{pbot}->{commands}->register(sub { return $self->useradd(@_) }, "useradd", 60); + $self->{pbot}->{commands}->register(sub { return $self->userdel(@_) }, "userdel", 60); + $self->{pbot}->{commands}->register(sub { return $self->userset(@_) }, "userset", 60); + $self->{pbot}->{commands}->register(sub { return $self->userunset(@_) }, "userunset", 60); + $self->{pbot}->{commands}->register(sub { return $self->mycmd(@_) }, "my", 0); + + $self->{pbot}->{event_dispatcher}->register_handler('irc.join', sub { $self->on_join(@_) }); +} + +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); + ($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host); + + my $u = $self->find_user($channel, "$nick!$user\@$host"); + + if (defined $u) { + if ($self->{pbot}->{chanops}->can_gain_ops($channel)) { + my $modes = '+'; + my $targets = ''; + + if ($u->{autoop}) { + $self->{pbot}->{logger}->log("$nick!$user\@$host autoop in $channel\n"); + $modes .= 'o'; + $targets .= "$nick "; + } + + if ($u->{autovoice}) { + $self->{pbot}->{logger}->log("$nick!$user\@$host autovoice in $channel\n"); + $modes .= 'v'; + $targets .= "$nick "; + } + + if (length $modes > 1) { + $self->{pbot}->{chanops}->add_op_command($channel, "mode $channel $modes $targets"); + $self->{pbot}->{chanops}->gain_ops($channel); + } + } + + if ($u->{autologin}) { + $self->{pbot}->{logger}->log("$nick!$user\@$host autologin to $user->{name} ($user->{level}) for $channel\n"); + $user->{loggedin} = 1; + } + } + return 0; +} + +sub add_user { + my ($self, $name, $channel, $hostmask, $level, $password, $dont_save) = @_; + $channel = '.*' if $channel !~ m/^#/; + + $level //= 0; + $password //= $self->{pbot}->random_nick(16); + + my $data = { + name => $name, + level => $level, + password => $password + }; + + $self->{pbot}->{logger}->log("Adding new user (level $level): name: $name hostmask: $hostmask channel: $channel\n"); + $self->{users}->add($channel, $hostmask, $data, $dont_save); + return $data; +} + +sub remove_user { + my ($self, $channel, $hostmask) = @_; + return $self->{users}->remove($channel, $hostmask); +} + +sub load { + my $self = shift; + my $filename; + + if (@_) { $filename = shift; } else { $filename = $self->{users}->{filename}; } + + if (not defined $filename) { + Carp::carp "No users path specified -- skipping loading of users"; + return; + } + + $self->{users}->load; + + my $i = 0; + foreach my $channel (sort keys %{ $self->{users}->{hash} } ) { + foreach my $hostmask (sort keys %{ $self->{users}->{hash}->{$channel} }) { + next if $hostmask eq '_name'; + $i++; + my $name = $self->{users}->{hash}->{$channel}->{$hostmask}->{name}; + my $level = $self->{users}->{hash}->{$channel}->{$hostmask}->{level}; + my $password = $self->{users}->{hash}->{$channel}->{$hostmask}->{password}; + + if (not defined $name or not defined $level or not defined $password) { + Carp::croak "A user in $filename is missing critical data\n"; + } + } + } + + $self->{pbot}->{logger}->log(" $i users loaded.\n"); +} + +sub save { + my ($self) = @_; + $self->{users}->save; +} + +sub hostmask_or_account_name { + my ($self, $channel, $hostmask) = @_; + + $channel = lc $channel; + $hostmask = lc $hostmask; + $channel = '.*' if $channel !~ /^#/; + + if (exists $self->{users}->{hash}->{$channel}) { + if (not exists $self->{users}->{hash}->{$channel}->{$hostmask}) { + my $last_level = 0; + # find hostmask by account name or wildcard + foreach my $mask (keys %{ $self->{users}->{hash}->{$channel} }) { + next if $mask eq '_name'; + if (lc $self->{users}->{hash}->{$channel}->{$mask}->{name} eq $hostmask) { + if ($last_level < $self->{users}->{hash}->{$channel}->{$mask}->{level}) { + $hostmask = $mask; + $last_level = $self->{users}->{hash}->{$channel}->{$mask}->{level}; + } + } + + if ($mask =~ /[*?]/) { + # contains * or ? so it's converted to a regex + my $mask_quoted = quotemeta $mask; + $mask_quoted =~ s/\\\*/.*?/g; + $mask_quoted =~ s/\\\?/./g; + if ($hostmask =~ m/^$mask_quoted$/i) { + if ($last_level < $self->{users}->{hash}->{$channel}->{$mask}->{level}) { + $hostmask = $mask; + $last_level = $self->{users}->{hash}->{$channel}->{$mask}->{level}; + } + } + } + } + } + } + return $hostmask; +} + +sub find_admin { + my ($self, $channel, $hostmask, $min_level) = @_; + $min_level //= 1; + + $channel = $self->{pbot}->{registry}->get_value('irc', 'botnick') if not defined $channel; + $hostmask = '.*' if not defined $hostmask; + $hostmask = lc $hostmask; + + my $result = eval { + my $admin; + foreach my $channel_regex (keys %{ $self->{users}->{hash} }) { + if ($channel !~ m/^#/ or $channel =~ m/^$channel_regex$/i) { + foreach my $hostmask_regex (keys %{ $self->{users}->{hash}->{$channel_regex} }) { + next if $hostmask_regex eq '_name'; + if ($hostmask_regex =~ m/[*?]/) { + # contains * or ? so it's converted to a regex + my $hostmask_quoted = quotemeta $hostmask_regex; + $hostmask_quoted =~ s/\\\*/.*?/g; + $hostmask_quoted =~ s/\\\?/./g; + if ($hostmask =~ m/^$hostmask_quoted$/i) { + my $temp = $self->{users}->{hash}->{$channel_regex}->{$hostmask_regex}; + $admin = $temp if $temp->{level} >= $min_level and (not defined $admin or $admin->{level} < $temp->{level}); + } + } else { + # direct comparison + if ($hostmask eq lc $hostmask_regex) { + my $temp = $self->{users}->{hash}->{$channel_regex}->{$hostmask_regex}; + $admin = $temp if $temp->{level} >= $min_level and (not defined $admin or $admin->{level} < $temp->{level}); + } + } + } + } + } + return $admin; + }; + + if ($@) { + $self->{pbot}->{logger}->log("Error in find_admin parameters: $@\n"); + } + + return $result; +} + +sub find_user { + my ($self, $from, $hostmask) = @_; + return $self->find_admin($from, $hostmask, 0); +} + +sub loggedin { + my ($self, $channel, $hostmask) = @_; + my $user = $self->find_user($channel, $hostmask); + + if (defined $user and $user->{loggedin}) { + return $user; + } + return undef; +} + +sub loggedin_admin { + my ($self, $channel, $hostmask) = @_; + my $user = $self->loggedin($channel, $hostmask); + + if (defined $user and $user->{level} > 0) { + return $user; + } + return undef; +} + +sub login { + my ($self, $channel, $hostmask, $password) = @_; + my $user = $self->find_user($channel, $hostmask); + + if (not defined $user) { + $self->{pbot}->{logger}->log("Attempt to login non-existent [$channel][$hostmask] failed\n"); + return "You do not have an account in $channel."; + } + + if (defined $password and $user->{password} ne $password) { + $self->{pbot}->{logger}->log("Bad login password for [$channel][$hostmask]\n"); + return "I don't think so."; + } + + $user->{loggedin} = 1; + $self->{pbot}->{logger}->log("$hostmask logged into $channel\n"); + return "Logged into $channel."; +} + +sub logout { + my ($self, $channel, $hostmask) = @_; + my $user = $self->find_user($channel, $hostmask); + delete $user->{loggedin} if defined $user; +} + +sub logincmd { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + my $channel = $from; + + if (not $arguments) { + return "Usage: login [channel] password"; + } + + if ($arguments =~ m/^([^ ]+)\s+(.+)/) { + $channel = $1; + $arguments = $2; + } + + if ($self->loggedin($channel, "$nick!$user\@$host")) { + return "/msg $nick You are already logged into channel $channel."; + } + + my $result = $self->login($channel, "$nick!$user\@$host", $arguments); + return "/msg $nick $result"; +} + +sub logoutcmd { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + return "/msg $nick Uh, you aren't logged into channel $from." if (not $self->loggedin($from, "$nick!$user\@$host")); + $self->logout($from, "$nick!$user\@$host"); + return "/msg $nick Good-bye, $nick."; +} + +sub useradd { + my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; + + my ($name, $channel, $hostmask, $level, $password) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 5); + $level //= 0; + + if (not defined $name or not defined $channel or not defined $hostmask) { + return "/msg $nick Usage: useradd [level] [password]"; + } + + $channel = '.*' if $channel !~ /^#/; + + my $admin = $self->{pbot}->{users}->find_admin($channel, "$nick!$user\@$host"); + + if (not $admin) { + return "You are not an admin for $channel; cannot add users to that channel.\n"; + } + + # don't allow non-bot-owners to add admins that can also add admins + if ($admin->{level} < 90 and $level > 40) { + return "You may not set admin level higher than 40.\n"; + } + + $self->{pbot}->{users}->add_user($name, $channel, $hostmask, $level, $password); + return not $level ? "User added." : "Admin added."; +} + +sub userdel { + my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; + + my ($channel, $hostmask) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 2); + + if (not defined $channel or not defined $hostmask) { + return "/msg $nick Usage: userdel "; + } + + $hostmask = $self->hostmask_or_account_name($channel, $hostmask); + $channel = '.*' if $channel !~ /^#/; + return $self->remove_user($channel, $hostmask); +} + +sub userset { + my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; + my ($channel, $hostmask, $key, $value) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 4); + + if (not defined $channel or not defined $hostmask) { + return "Usage: userset [key] [value]"; + } + + $hostmask = $self->hostmask_or_account_name($channel, $hostmask); + my $admin = $self->find_admin($channel, "$nick!$user\@$host"); + my $target = $self->find_user($channel, $hostmask); + + if (not $admin) { + return "You are not an admin for $channel; cannot modify their users."; + } + + if (not $target) { + return "There is no user $hostmask in channel $channel."; + } + + # don't allow non-bot-owners to add admins that can also add admins + if (defined $key and $key eq 'level' and $admin->{level} < 90 and $value > 40) { + return "You may not set user level higher than 40.\n"; + } + + if (defined $key and $target->{level} > $admin->{level}) { + return "You may not modify users higher in level than you."; + } + + $channel = '.*' if $channel !~ /^#/; + my $result = $self->{users}->set($channel, $hostmask, $key, $value); + $result =~ s/^password => .*;$/password => ;/m; + return $result; +} + +sub userunset { + my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; + my ($channel, $hostmask, $key) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 3); + + if (not defined $channel or not defined $hostmask) { + return "Usage: userunset "; + } + + my $admin = $self->find_admin($channel, "$nick!$user\@$host"); + $hostmask = $self->hostmask_or_account_name($channel, $hostmask); + my $target = $self->find_user($channel, $hostmask); + + if (not $admin) { + return "You are not an admin for $channel; cannot modify their users."; + } + + if (not $target) { + return "There is no user $hostmask in channel $channel."; + } + + if ($target->{level} >= $user->{level}) { + return "You may not modify users equal or higher in level than you."; + } + + $channel = '.*' if $channel !~ /^#/; + return $self->{users}->unset($channel, $hostmask, $key); +} + +sub mycmd { + my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; + my ($key, $value) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 2); + + if (not defined $key) { + return "Usage: my [value]"; + } + + my $channel = $from; + $channel = '.*' if $channel !~ /^#/; + my $hostmask = $self->hostmask_or_account_name($channel, "$nick!$user\@$host"); + my $u = $self->find_user($channel, $hostmask); + + print "hostmask: $hostmask\n"; + use Data::Dumper; + print Dumper \$u; + + if (not $u) { + $channel = '.*'; + $hostmask = "$nick!*\@*"; + $u = $self->add_user("my_$nick", $channel, $hostmask); + $u->{autologin} = 1; + $u->{loggedin} = 1; + } + + if ($u->{level} == 0) { + my @disallowed = qw/level autoop autovoice/; + if (grep { lc $key } @disallowed) { + return "You must be an admin to set $key."; + } + } + + my $result = $self->{users}->set($channel, $hostmask, $key, $value); + $result =~ s/^password => .*;$/password => ;/m; + return $result; +} + +1;