From 866d802850a7e3e42fc16b37ab31dd873578f2dc Mon Sep 17 00:00:00 2001 From: Pragmatic Software Date: Mon, 3 Feb 2020 09:50:38 -0800 Subject: [PATCH] Replaced admin-levels with user-capabilities [WIP commit 1 of 2] --- PBot/AntiFlood.pm | 6 +- PBot/Capabilities.pm | 163 +++++++++++++++++++++++++++++++++++++++ PBot/ChanOpCommands.pm | 91 +++++++++++++--------- PBot/Commands.pm | 82 +++++++++++--------- PBot/IRCHandlers.pm | 4 +- PBot/PBot.pm | 22 ++++-- PBot/Users.pm | 27 +++---- Plugins/RestrictedMod.pm | 5 ++ 8 files changed, 300 insertions(+), 100 deletions(-) create mode 100644 PBot/Capabilities.pm diff --git a/PBot/AntiFlood.pm b/PBot/AntiFlood.pm index 42a70de2..c69aef52 100644 --- a/PBot/AntiFlood.pm +++ b/PBot/AntiFlood.pm @@ -417,9 +417,9 @@ sub check_flood { $self->{whois_pending}->{$nick} = gettimeofday; } } else { - if ($mode == $self->{pbot}->{messagehistory}->{MSG_JOIN} && exists $self->{pbot}->{capabilities}->{'extended-join'}) { + if ($mode == $self->{pbot}->{messagehistory}->{MSG_JOIN} && exists $self->{pbot}->{irc_capabilities}->{'extended-join'}) { # don't WHOIS joins if extended-join capability is active - } elsif (not exists $self->{pbot}->{capabilities}->{'account-notify'}) { + } elsif (not exists $self->{pbot}->{irc_capabilities}->{'account-notify'}) { if (not exists $self->{whois_pending}->{$nick}) { $self->{pbot}->{messagehistory}->{database}->set_current_nickserv_account($account, ''); $self->{pbot}->{conn}->whois($nick); @@ -820,7 +820,7 @@ sub check_bans { $self->{pbot}->{messagehistory}->{database}->update_channel_data($message_account, $channel, $channel_data); } } else { - if (not exists $self->{pbot}->{capabilities}->{'account-notify'}) { + if (not exists $self->{pbot}->{irc_capabilities}->{'account-notify'}) { # mark this account as needing check-bans when nickserv account is identified my $channel_data = $self->{pbot}->{messagehistory}->{database}->get_channel_data($message_account, $channel, 'validated'); if (not $channel_data->{validated} & $self->{NEEDS_CHECKBAN}) { diff --git a/PBot/Capabilities.pm b/PBot/Capabilities.pm new file mode 100644 index 00000000..fcfffdba --- /dev/null +++ b/PBot/Capabilities.pm @@ -0,0 +1,163 @@ +# 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::Capabilities; + +# purpose: provides interface to set/remove/modify/query user capabilities. +# +# Examples: +# + +use warnings; +use strict; + +use feature 'unicode_strings'; + +use feature 'switch'; +no if $] >= 5.018, warnings => "experimental::smartmatch"; + +use PBot::HashObject; +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__); + my $filename = $conf{filename} // $self->{pbot}->{registry}->get_value('general', 'data_dir') . '/capabilities'; + $self->{caps} = PBot::HashObject->new(name => 'Capabilities', filename => $filename, pbot => $self->{pbot}); + $self->{caps}->load; + # 'cap' command registered in PBot.pm because $self->{pbot}->{commands} is not yet loaded. +} + +sub has { + my ($self, $cap, $subcap, $depth) = @_; + my $cap_data = $self->{caps}->get_data($cap); + return 0 if not defined $cap_data; + + $depth //= 2; + if (--$depth <= 0) { + $self->{pbot}->{logger}->log("Max recursion reached for PBot::Capabilities->has()\n"); + return 0; + } + + foreach my $c (keys %{$cap_data}) { + next if $c eq '_name'; + return 1 if $c eq $subcap; + 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'; + return 1 if $self->has($key, $cap, 10); + } + return 0; +} + +sub exists { + my ($self, $cap) = @_; + $cap = lc $cap; + foreach my $c (keys %{$self->{caps}->{hash}}) { + next if $c eq '_name'; + return 1 if $c eq $cap; + foreach my $sub_cap (keys %{$self->{caps}->{hash}->{$c}}) { + return 1 if $sub_cap eq $cap; + } + } + return 0; +} + +sub add { + my ($self, $cap, $subcap, $dontsave) = @_; + + if (not defined $subcap) { + if (not $self->{caps}->exists($cap)) { + $self->{caps}->add($cap, {}, $dontsave); + } + } else { + if ($self->{caps}->exists($cap)) { + $self->{caps}->set($cap, $subcap, 1, $dontsave); + } else { + $self->{caps}->add($cap, { $subcap => 1 }, $dontsave); + } + } +} + +sub remove { +} + +sub rebuild_botowner_capabilities { + my ($self) = @_; + $self->{caps}->remove('botowner'); + foreach my $cap (keys %{$self->{caps}->{hash}}) { + next if $cap eq '_name'; + $self->add('botowner', $cap, 1); + } +} + +sub capcmd { + my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; + + my $command = $self->{pbot}->{interpreter}->shift_arg($stuff->{arglist}); + my $result; + given ($command) { + when ('list') { + my $cap = $self->{pbot}->{interpreter}->shift_arg($stuff->{arglist}); + if (defined $cap) { + $cap = lc $cap; + return "No such capability $cap." if not exists $self->{caps}->{hash}->{$cap}; + return "Capability $cap has no sub-capabilities." if keys %{$self->{caps}->{hash}->{$cap}} == 1; + + $result = "Sub-capabilities for $cap: "; + $result .= join(', ', grep { $_ ne '_name' } sort keys %{$self->{caps}->{hash}->{$cap}}); + } else { + return "No capabilities defined." if keys(%{$self->{caps}->{hash}}) == 0; + $result = "Capabilities: "; + my @caps; + + # first list all capabilities that have sub-capabilities (i.e. grouped capabilities) + foreach my $cap (sort keys %{$self->{caps}->{hash}}) { + my $count = keys(%{$self->{caps}->{hash}->{$cap}}) - 1; + push @caps, "$cap [$count]" if $count; + } + + # then list stand-alone capabilities + foreach my $cap (sort keys %{$self->{caps}->{hash}}) { + next if keys(%{$self->{caps}->{hash}->{$cap}}) > 1; + push @caps, $cap; + } + + $result .= join ', ', @caps; + } + } + + when ('userhas') { + } + + when ('add') { + } + + when ('remove') { + } + + default { + $result = "Usage: cap list [capability] | cap add [sub-capability] | cap remove [sub-capability] | cap userhas "; + } + } + return $result; +} + +1; diff --git a/PBot/ChanOpCommands.pm b/PBot/ChanOpCommands.pm index f4b8489c..711417dc 100644 --- a/PBot/ChanOpCommands.pm +++ b/PBot/ChanOpCommands.pm @@ -28,22 +28,50 @@ sub new { sub initialize { my ($self, %conf) = @_; - $self->{pbot} = $conf{pbot} // Carp::croak("Missing pbot reference to " . __FILE__); - $self->{pbot}->{commands}->register(sub { return $self->ban_user(@_) }, "ban", 10); - $self->{pbot}->{commands}->register(sub { return $self->unban_user(@_) }, "unban", 10); - $self->{pbot}->{commands}->register(sub { return $self->mute_user(@_) }, "mute", 10); - $self->{pbot}->{commands}->register(sub { return $self->unmute_user(@_) }, "unmute", 10); - $self->{pbot}->{commands}->register(sub { return $self->kick_user(@_) }, "kick", 10); - $self->{pbot}->{commands}->register(sub { return $self->checkban(@_) }, "checkban", 0); - $self->{pbot}->{commands}->register(sub { return $self->checkmute(@_) }, "checkmute", 0); - $self->{pbot}->{commands}->register(sub { return $self->op_user(@_) }, "op", 10); - $self->{pbot}->{commands}->register(sub { return $self->deop_user(@_) }, "deop", 10); - $self->{pbot}->{commands}->register(sub { return $self->voice_user(@_) }, "voice", 10); - $self->{pbot}->{commands}->register(sub { return $self->devoice_user(@_) }, "devoice", 10); - $self->{pbot}->{commands}->register(sub { return $self->mode(@_) }, "mode", 40); - $self->{pbot}->{commands}->register(sub { return $self->invite(@_) }, "invite", 10); + # register commands + $self->{pbot}->{commands}->register(sub { return $self->ban_user(@_) }, "ban", 1); + $self->{pbot}->{commands}->register(sub { return $self->unban_user(@_) }, "unban", 1); + $self->{pbot}->{commands}->register(sub { return $self->mute_user(@_) }, "mute", 1); + $self->{pbot}->{commands}->register(sub { return $self->unmute_user(@_) }, "unmute", 1); + $self->{pbot}->{commands}->register(sub { return $self->kick_user(@_) }, "kick", 1); + $self->{pbot}->{commands}->register(sub { return $self->checkban(@_) }, "checkban", 0); + $self->{pbot}->{commands}->register(sub { return $self->checkmute(@_) }, "checkmute", 0); + $self->{pbot}->{commands}->register(sub { return $self->op_user(@_) }, "op", 1); + $self->{pbot}->{commands}->register(sub { return $self->deop_user(@_) }, "deop", 1); + $self->{pbot}->{commands}->register(sub { return $self->voice_user(@_) }, "voice", 1); + $self->{pbot}->{commands}->register(sub { return $self->devoice_user(@_) }, "devoice", 1); + $self->{pbot}->{commands}->register(sub { return $self->mode(@_) }, "mode", 1); + $self->{pbot}->{commands}->register(sub { return $self->invite(@_) }, "invite", 1); + + # allow commands to set modes + $self->{pbot}->{capabilities}->add('can-ban', 'can-mode-b', 1); + $self->{pbot}->{capabilities}->add('can-unban', 'can-mode-b', 1); + $self->{pbot}->{capabilities}->add('can-mute', 'can-mode-q', 1); + $self->{pbot}->{capabilities}->add('can-unmute', 'can-mode-q', 1); + $self->{pbot}->{capabilities}->add('can-op', 'can-mode-o', 1); + $self->{pbot}->{capabilities}->add('can-deop', 'can-mode-o', 1); + $self->{pbot}->{capabilities}->add('can-voice', 'can-mode-v', 1); + $self->{pbot}->{capabilities}->add('can-devoice', 'can-mode-v', 1); + + # create can-mode-any capabilities group + foreach my $mode ("a" .. "z", "A" .. "Z") { + $self->{pbot}->{capabilities}->add('can-mode-any', "can-mode-$mode", 1); + } + $self->{pbot}->{capabilities}->add('can-mode-any', 'can-mode', 1); + + # create chanop capabilities group + $self->{pbot}->{capabilities}->add('chanop', 'can-ban', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-unban', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-mute', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-unmute', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-kick', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-op', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-deop', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-voice', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-devoice', 1); + $self->{pbot}->{capabilities}->add('chanop', 'can-invite', 1); $self->{invites} = {}; # track who invited who in order to direct invite responses to them @@ -127,7 +155,6 @@ sub generic_mode_user { if ($channel !~ m/^#/) { # from message $channel = $self->{pbot}->{interpreter}->shift_arg($stuff->{arglist}); - $result = 'Done.'; if (not defined $channel) { return "Usage from message: $mode_name [nick]"; } elsif ($channel !~ m/^#/) { @@ -159,10 +186,11 @@ sub generic_mode_user { if ($i >= $max_modes) { my $args = "$channel $mode $list"; $stuff->{arglist} = $self->{pbot}->{interpreter}->make_args($args); - $self->mode($channel, $nick, $stuff->{user}, $stuff->{host}, $args, $stuff); + $result = $self->mode($channel, $nick, $stuff->{user}, $stuff->{host}, $args, $stuff); $mode = $flag; $list = ''; $i = 0; + last if $result ne '' and $result ne 'Done.'; } } } @@ -170,13 +198,12 @@ sub generic_mode_user { if ($i) { my $args = "$channel $mode $list"; $stuff->{arglist} = $self->{pbot}->{interpreter}->make_args($args); - $self->mode($channel, $nick, $stuff->{user}, $stuff->{host}, $args, $stuff); + $result = $self->mode($channel, $nick, $stuff->{user}, $stuff->{host}, $args, $stuff); } return $result; } - sub op_user { my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_; return $self->generic_mode_user('+o', 'op', $from, $nick, $stuff); @@ -222,6 +249,8 @@ sub mode { my ($new_modes, $new_targets) = ("", ""); my $max_modes = $self->{pbot}->{ircd}->{MODES} // 1; + my $u = $self->{pbot}->{users}->loggedin($channel, "$nick!$user\@$host"); + while ($modes =~ m/(.)/g) { my $mode = $1; @@ -231,6 +260,10 @@ sub mode { next; } + if (not $self->{pbot}->{capabilities}->userhas($u, "can-mode-$mode")) { + return "/msg $nick Your user account does not have the can-mode-$mode capability required to set this mode."; + } + my $target = $targets[$arg++] // ""; if (($mode eq 'v' or $mode eq 'o') and $target =~ m/\*/) { @@ -297,6 +330,8 @@ sub mode { if ($from !~ m/^#/) { return "Done."; + } else { + return ""; } } @@ -363,10 +398,6 @@ 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}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { - return "You are not an admin for $channel."; - } - my $result = ''; my $sep = ''; my @targets = split /,/, $target; @@ -430,10 +461,6 @@ sub unban_user { return "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}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { - return "You are not an admin for $channel."; - } - my @targets = split /,/, $target; $immediately = 0 if @targets > 1; @@ -494,10 +521,6 @@ 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}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { - return "You are not an admin for $channel."; - } - my $result = ''; my $sep = ''; my @targets = split /,/, $target; @@ -561,10 +584,6 @@ sub unmute_user { return "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}->{users}->loggedin_admin($channel, "$nick!$user\@$host")) { - return "You are not an admin for $channel."; - } - my @targets = split /,/, $target; $immediately = 0 if @targets > 1; @@ -615,10 +634,6 @@ sub kick_user { $channel = $1; } - 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 "You are not an admin for $channel."; - } - my @insults; if (not length $reason) { if (open my $fh, '<', $self->{pbot}->{registry}->get_value('general', 'module_dir') . '/insults.txt') { diff --git a/PBot/Commands.pm b/PBot/Commands.pm index 02ec5891..97c22741 100644 --- a/PBot/Commands.pm +++ b/PBot/Commands.pm @@ -2,10 +2,8 @@ # # Author: pragma_ # -# Purpose: Derives from Registerable class to provide functionality to -# register subroutines, along with a command name and admin level. -# Registered items will then be executed if their command name matches -# a name provided via input. +# Purpose: Registers commands. Invokes commands with user capability +# validation. # 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 @@ -40,32 +38,35 @@ 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->cmdset(@_) }, "cmdset", 1); + $self->register(sub { $self->cmdunset(@_) }, "cmdunset", 1); $self->register(sub { $self->help(@_) }, "help", 0); $self->register(sub { $self->uptime(@_) }, "uptime", 0); - $self->register(sub { $self->in_channel(@_) }, "in", 10); + $self->register(sub { $self->in_channel(@_) }, "in", 1); } sub register { - my ($self, $subref, $name, $level) = @_; + my ($self, $subref, $name, $requires_cap) = @_; - if (not defined $subref or not defined $name or not defined $level) { + if (not defined $subref or not defined $name) { Carp::croak("Missing parameters to Commands::register"); } my $ref = $self->SUPER::register($subref); $ref->{name} = lc $name; - $ref->{level} = $level; + $ref->{requires_cap} = $requires_cap // 0; if (not $self->{metadata}->exists($name)) { - $self->{metadata}->add($name, { level => $level, help => '' }, 1); + $self->{metadata}->add($name, { requires_cap => $requires_cap, help => '' }, 1); } else { - if (not defined $self->get_meta($name, 'level')) { - $self->{metadata}->set($name, 'level', $level, 1); + if (not defined $self->get_meta($name, 'requires_cap')) { + $self->{metadata}->set($name, 'requires_cap', $requires_cap, 1); } } + # add can-cmd capability + $self->{pbot}->{capabilities}->add("can-$name", undef, 1); + return $ref; } @@ -96,39 +97,44 @@ sub interpreter { $self->{pbot}->{logger}->log(Dumper $stuff); } - 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}->{users}->loggedin_admin($admin_channel, "$stuff->{nick}!$stuff->{user}\@$stuff->{host}"); - my $admin_level = defined $admin ? $admin->{level} : 0; my $keyword = lc $stuff->{keyword}; + my $from = $stuff->{from}; - if (exists $stuff->{'effective-level'}) { - $self->{pbot}->{logger}->log("override level to $stuff->{'effective-level'}\n"); - $admin_level = $stuff->{'effective-level'}; + my ($cmd_channel) = $stuff->{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 + my $user = $self->{pbot}->{users}->loggedin($cmd_channel, "$stuff->{nick}!$stuff->{user}\@$stuff->{host}"); + + my $cap_override; + if (exists $stuff->{'cap-override'}) { + $self->{pbot}->{logger}->log("Override cap to $stuff->{'cap-override'}\n"); + $cap_override = $stuff->{'cap-override'}; } foreach my $ref (@{ $self->{handlers} }) { if ($ref->{name} eq $keyword) { - my $cmd_level = $self->get_meta($keyword, 'level') // $ref->{level}; - if ($admin_level >= $cmd_level) { - $stuff->{no_nickoverride} = 1; - my $result = &{ $ref->{subref} }($stuff->{from}, $stuff->{nick}, $stuff->{user}, $stuff->{host}, $stuff->{arguments}, $stuff); - if ($stuff->{referenced}) { - return undef if $result =~ m/(?:usage:|no results)/i; - } - return $result; - } else { - return undef if $stuff->{referenced}; - if ($admin_level == 0) { - return "/msg $stuff->{nick} You must be an admin to use this command."; + my $requires_cap = $self->get_meta($keyword, 'requires_cap') // $ref->{requires_cap}; + if ($requires_cap) { + if (defined $cap_override) { + if (not $self->{pbot}->{capabilities}->has($cap_override, "can-$keyword")) { + return "/msg $stuff->{nick} The $keyword command requires the can-$keyword capability, which cap-override $cap_override does not have."; + } } else { - return "/msg $stuff->{nick} Your level is too low to use this command."; + if (not defined $user) { + return "/msg $stuff->{nick} You must be logged into your user account to use $keyword."; + } + + if (not $self->{pbot}->{capabilities}->userhas($user, "can-$keyword")) { + return "/msg $stuff->{nick} The $keyword command requires the can-$keyword capability, which your user account does not have."; + } } } + + $stuff->{no_nickoverride} = 1; + my $result = &{ $ref->{subref} }($stuff->{from}, $stuff->{nick}, $stuff->{user}, $stuff->{host}, $stuff->{arguments}, $stuff); + return undef if $stuff->{referenced} and $result =~ m/(?:usage:|no results)/i; + return $result; } } - return undef; } @@ -185,12 +191,12 @@ sub help { if ($self->exists($keyword)) { if (exists $self->{metadata}->{hash}->{$keyword}) { my $name = $self->{metadata}->{hash}->{$keyword}->{_name}; - my $level = $self->{metadata}->{hash}->{$keyword}->{level}; + my $requires_cap = $self->{metadata}->{hash}->{$keyword}->{requires_cap}; my $help = $self->{metadata}->{hash}->{$keyword}->{help}; my $result = "/say $name: "; - if (defined $level and $level > 0) { - $result .= "[Level $level admin command] "; + if ($requires_cap) { + $result .= "[Requires can-$keyword] "; } if (not defined $help or not length $help) { diff --git a/PBot/IRCHandlers.pm b/PBot/IRCHandlers.pm index c75ba149..f12c8635 100644 --- a/PBot/IRCHandlers.pm +++ b/PBot/IRCHandlers.pm @@ -345,7 +345,7 @@ sub on_join { my $msg = 'JOIN'; - if (exists $self->{pbot}->{capabilities}->{'extended-join'}) { + if (exists $self->{pbot}->{irc_capabilities}->{'extended-join'}) { $msg .= " $event->{event}->{args}[0] :$event->{event}->{args}[1]"; $self->{pbot}->{messagehistory}->{database}->update_gecos($message_account, $event->{event}->{args}[1], scalar gettimeofday); @@ -480,7 +480,7 @@ sub on_cap { my @caps = split /\s+/, $event->{event}->{args}->[1]; foreach my $cap (@caps) { - $self->{pbot}->{capabilities}->{$cap} = 1; + $self->{pbot}->{irc_capabilities}->{$cap} = 1; } } else { $self->{pbot}->{logger}->log(Dumper $event->{event}); diff --git a/PBot/PBot.pm b/PBot/PBot.pm index 2c46bf97..d80dac5d 100644 --- a/PBot/PBot.pm +++ b/PBot/PBot.pm @@ -21,6 +21,7 @@ use Carp (); use PBot::Logger; use PBot::VERSION; use PBot::Registry; +use PBot::Capabilities; use PBot::SelectHandler; use PBot::StdinReader; use PBot::IRC; @@ -99,16 +100,22 @@ sub initialize { exit; } + # then capabilities so commands can add new capabilities + $self->{capabilities} = PBot::Capabilities->new(pbot => $self, filename => "$data_dir/capabilities", %conf); + # then commands so the modules can register new commands $self->{commands} = PBot::Commands->new(pbot => $self, filename => "$data_dir/commands", %conf); # 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); + $self->{commands}->register(sub { $self->listcmd(@_) }, "list"); + $self->{commands}->register(sub { $self->ack_die(@_) }, "die", 1); + $self->{commands}->register(sub { $self->export(@_) }, "export", 1); + $self->{commands}->register(sub { $self->reload(@_) }, "reload", 1); + $self->{commands}->register(sub { $self->evalcmd(@_) }, "eval", 1); + $self->{commands}->register(sub { $self->sl(@_) }, "sl", 1); + + # add 'cap' capability command + $self->{commands}->register(sub { $self->{capabilities}->capcmd(@_) }, "cap"); # prepare the version $self->{version} = PBot::VERSION->new(pbot => $self, %conf); @@ -225,6 +232,9 @@ sub initialize { # start timer $self->{timer}->start(); + + # give botowner all capabilities + $self->{capabilities}->rebuild_botowner_capabilities(); } sub random_nick { diff --git a/PBot/Users.pm b/PBot/Users.pm index 0b04ad60..1014c5ee 100644 --- a/PBot/Users.pm +++ b/PBot/Users.pm @@ -32,13 +32,13 @@ sub initialize { $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}->{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", 1); + $self->{pbot}->{commands}->register(sub { return $self->userdel(@_) }, "userdel", 1); + $self->{pbot}->{commands}->register(sub { return $self->userset(@_) }, "userset", 1); + $self->{pbot}->{commands}->register(sub { return $self->userunset(@_) }, "userunset", 1); + $self->{pbot}->{commands}->register(sub { return $self->mycmd(@_) }, "my", 0); $self->{pbot}->{event_dispatcher}->register_handler('irc.join', sub { $self->on_join(@_) }); } @@ -502,21 +502,22 @@ sub mycmd { if (defined $key) { $key = lc $key; - if (defined $value and $u->{level} == 0) { - my @disallowed = qw/name level autoop autovoice/; + if (defined $value and not $self->{pbot}->{capabilities}->userhas($u, 'admin')) { + my @disallowed = qw/name autoop autovoice/; if (grep { $_ eq $key } @disallowed) { - return "You must be an admin to set $key."; + return "The $key metadata requires the admin capability to set, which your user account does not have."; } } - if ($key eq 'level' and defined $value and $u->{level} < 90 and $value > $u->{level}) { - return "You may not increase your level!"; + if (defined $value and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) { + if ($key =~ m/^can-/ or $self->{pbot}->{capabilities}->exists($key)) { + return "The $key metadata requires the can-modify-capabilities capability, which your user account does not have."; + } } } else { $result = "Usage: my [value]; "; } - my ($found_channel, $found_hostmask) = $self->find_user_account($channel, $hostmask); $found_channel = $channel if not defined $found_channel; # let DualIndexHashObject disambiguate $result .= $self->{users}->set($found_channel, $found_hostmask, $key, $value); diff --git a/Plugins/RestrictedMod.pm b/Plugins/RestrictedMod.pm index dde0c8c0..35935162 100644 --- a/Plugins/RestrictedMod.pm +++ b/Plugins/RestrictedMod.pm @@ -32,6 +32,10 @@ sub initialize { $self->{pbot}->{commands}->register(sub { $self->modcmd(@_) }, 'mod', 0); $self->{pbot}->{commands}->set_meta('mod', 'help', 'Provides restricted moderation abilities to voiced users. They can kick/ban/etc only users that are not admins, whitelisted, voiced or opped.'); + $self->{pbot}->{capabilities}->add('chanmod', 'can-mod', 1); + $self->{pbot}->{capabilities}->add('chanmod', 'can-voice', 1); + $self->{pbot}->{capabilities}->add('chanmod', 'can-devoice', 1); + $self->{commands} = { 'help' => { subref => sub { $self->help(@_) }, help => "Provides help about this command. Usage: mod help ; see also: mod help list" }, 'list' => { subref => sub { $self->list(@_) }, help => "Lists available mod commands. Usage: mod list" }, @@ -47,6 +51,7 @@ sub initialize { sub unload { my ($self) = @_; $self->{pbot}->{commands}->unregister('mod'); + $self->{pbot}->{capabilities}->remove('chanmod'); } sub help {