mirror of
https://github.com/pragma-/pbot.git
synced 2024-11-25 13:29:29 +01:00
Major refactor of Users
This commit is contained in:
parent
214dccdcdf
commit
8d9e3da249
@ -74,7 +74,9 @@ sub exists {
|
|||||||
$cap = lc $cap;
|
$cap = lc $cap;
|
||||||
foreach my $c ($self->{caps}->get_keys) {
|
foreach my $c ($self->{caps}->get_keys) {
|
||||||
return 1 if $c eq $cap;
|
return 1 if $c eq $cap;
|
||||||
foreach my $sub_cap ($self->{caps}->get_keys($c)) { return 1 if $sub_cap eq $cap; }
|
foreach my $sub_cap ($self->{caps}->get_keys($c)) {
|
||||||
|
return 1 if $sub_cap eq $cap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -82,10 +84,15 @@ sub exists {
|
|||||||
sub add {
|
sub add {
|
||||||
my ($self, $cap, $subcap, $dontsave) = @_;
|
my ($self, $cap, $subcap, $dontsave) = @_;
|
||||||
if (not defined $subcap) {
|
if (not defined $subcap) {
|
||||||
if (not $self->{caps}->exists($cap)) { $self->{caps}->add($cap, {}, $dontsave); }
|
if (not $self->{caps}->exists($cap)) {
|
||||||
|
$self->{caps}->add($cap, {}, $dontsave);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($self->{caps}->exists($cap)) { $self->{caps}->set($cap, $subcap, 1, $dontsave); }
|
if ($self->{caps}->exists($cap)) {
|
||||||
else { $self->{caps}->add($cap, {$subcap => 1}, $dontsave); }
|
$self->{caps}->set($cap, $subcap, 1, $dontsave);
|
||||||
|
} else {
|
||||||
|
$self->{caps}->add($cap, { $subcap => 1 }, $dontsave);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +101,9 @@ sub remove {
|
|||||||
$cap = lc $cap;
|
$cap = lc $cap;
|
||||||
if (not defined $subcap) {
|
if (not defined $subcap) {
|
||||||
foreach my $c ($self->{caps}->get_keys) {
|
foreach my $c ($self->{caps}->get_keys) {
|
||||||
foreach my $sub_cap ($self->{caps}->get_keys($c)) { $self->{caps}->remove($c, $sub_cap, 1) if $sub_cap eq $cap; }
|
foreach my $sub_cap ($self->{caps}->get_keys($c)) {
|
||||||
|
$self->{caps}->remove($c, $sub_cap, 1) if $sub_cap eq $cap;
|
||||||
|
}
|
||||||
$self->{caps}->remove($c, undef, 1) if $c eq $cap;
|
$self->{caps}->remove($c, undef, 1) if $c eq $cap;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -154,43 +163,40 @@ sub capcmd {
|
|||||||
return "Usage: cap whohas <capability>; Lists all users who have <capability>" if not defined $cap;
|
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);
|
return "No such capability $cap." if not $self->exists($cap);
|
||||||
my $result = "Users with capability $cap: ";
|
my $result = "Users with capability $cap: ";
|
||||||
my $matched = 0;
|
|
||||||
my $users = $self->{pbot}->{users}->{users};
|
my $users = $self->{pbot}->{users}->{users};
|
||||||
foreach my $channel (sort $users->get_keys) {
|
my @matches;
|
||||||
my @matches;
|
foreach my $name (sort $users->get_keys) {
|
||||||
foreach my $hostmask (sort $users->get_keys($channel)) {
|
my $u = $users->get_data($name);
|
||||||
my $u = $users->get_data($channel, $hostmask);
|
push @matches, $users->get_key_name($name) if $self->userhas($u, $cap);
|
||||||
push @matches, $u->{name} if $self->userhas($u, $cap);
|
|
||||||
}
|
|
||||||
if (@matches) {
|
|
||||||
$result .= '; ' if $matched;
|
|
||||||
my $global = $matched ? 'global: ' : '';
|
|
||||||
$result .= $users->get_key_name($channel) eq '.*' ? $global : $users->get_key_name($channel) . ': ';
|
|
||||||
$result .= join ', ', @matches;
|
|
||||||
$matched = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$result .= 'nobody' if not $matched;
|
|
||||||
|
if (@matches) {
|
||||||
|
$result .= join(', ', @matches);
|
||||||
|
} else {
|
||||||
|
$result .= 'nobody';
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
when ('userhas') {
|
when ('userhas') {
|
||||||
my ($hostmask, $cap) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 2);
|
my ($name, $cap) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 2);
|
||||||
return "Usage: cap userhas <user> [capability]; Lists capabilities belonging to <user>" if not defined $hostmask;
|
return "Usage: cap userhas <username> [capability]; Lists capabilities belonging to <user>" if not defined $name;
|
||||||
$cap = lc $cap if defined $cap;
|
$cap = lc $cap if defined $cap;
|
||||||
|
|
||||||
my $u = $self->{pbot}->{users}->find_user($from, $hostmask, 1);
|
my $u = $self->{pbot}->{users}->{users}->get_data($name);
|
||||||
if (not defined $u) {
|
if (not defined $u) {
|
||||||
$from = 'global' if $from !~ /^#/;
|
return "No such user $name.";
|
||||||
return "No such user $hostmask in $from.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$name = $self->{pbot}->{users}->{users}->get_key_name($name);
|
||||||
|
|
||||||
if (defined $cap) {
|
if (defined $cap) {
|
||||||
return "Try again. No such capability $cap." if not $self->exists($cap);
|
return "Try again. No such capability $cap." if not $self->exists($cap);
|
||||||
if ($self->userhas($u, $cap)) { return "Yes. User $u->{name} has capability $cap."; }
|
if ($self->userhas($u, $cap)) { return "Yes. User $name has capability $cap."; }
|
||||||
else { return "No. User $u->{name} does not have capability $cap."; }
|
else { return "No. User $name does not have capability $cap."; }
|
||||||
} else {
|
} else {
|
||||||
my $result = "User $u->{name} has capabilities: ";
|
my $result = "User $name has capabilities: ";
|
||||||
my @groups;
|
my @groups;
|
||||||
my @single;
|
my @single;
|
||||||
foreach my $key (sort keys %{$u}) {
|
foreach my $key (sort keys %{$u}) {
|
||||||
@ -201,7 +207,7 @@ sub capcmd {
|
|||||||
else { push @single, $key; }
|
else { push @single, $key; }
|
||||||
}
|
}
|
||||||
if (@groups or @single) { $result .= join ', ', @groups, @single; }
|
if (@groups or @single) { $result .= join ', ', @groups, @single; }
|
||||||
else { $result = "User $u->{name} has no capabilities."; }
|
else { $result = "User $name has no capabilities."; }
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@ sub set {
|
|||||||
my $comma = '';
|
my $comma = '';
|
||||||
foreach my $k (sort grep { $_ ne '_name' } keys %{$self->{hash}->{$lc_index}}) {
|
foreach my $k (sort grep { $_ ne '_name' } keys %{$self->{hash}->{$lc_index}}) {
|
||||||
$result .= $comma . "$k => " . $self->{hash}->{$lc_index}->{$k};
|
$result .= $comma . "$k => " . $self->{hash}->{$lc_index}->{$k};
|
||||||
$comma = "; ";
|
$comma = ";\n";
|
||||||
}
|
}
|
||||||
$result .= "none" if ($comma eq '');
|
$result .= "none" if ($comma eq '');
|
||||||
return $result;
|
return $result;
|
||||||
|
373
PBot/Users.pm
373
PBot/Users.pm
@ -15,7 +15,7 @@ use feature 'unicode_strings';
|
|||||||
|
|
||||||
sub initialize {
|
sub initialize {
|
||||||
my ($self, %conf) = @_;
|
my ($self, %conf) = @_;
|
||||||
$self->{users} = PBot::DualIndexHashObject->new(name => 'Users', filename => $conf{filename}, pbot => $conf{pbot});
|
$self->{users} = PBot::HashObject->new(name => 'Users', filename => $conf{filename}, pbot => $conf{pbot});
|
||||||
$self->load;
|
$self->load;
|
||||||
|
|
||||||
$self->{pbot}->{commands}->register(sub { $self->logincmd(@_) }, "login", 0);
|
$self->{pbot}->{commands}->register(sub { $self->logincmd(@_) }, "login", 0);
|
||||||
@ -34,6 +34,16 @@ sub initialize {
|
|||||||
$self->{pbot}->{capabilities}->add('can-modify-admins', undef, 1);
|
$self->{pbot}->{capabilities}->add('can-modify-admins', undef, 1);
|
||||||
|
|
||||||
$self->{pbot}->{event_dispatcher}->register_handler('irc.join', sub { $self->on_join(@_) });
|
$self->{pbot}->{event_dispatcher}->register_handler('irc.join', sub { $self->on_join(@_) });
|
||||||
|
$self->{pbot}->{event_dispatcher}->register_handler('irc.part', sub { $self->on_departure(@_) });
|
||||||
|
$self->{pbot}->{event_dispatcher}->register_handler('irc.quit', sub { $self->on_departure(@_) });
|
||||||
|
$self->{pbot}->{event_dispatcher}->register_handler('irc.kick', sub { $self->on_kick(@_) });
|
||||||
|
$self->{pbot}->{event_dispatcher}->register_handler('pbot.part', sub { $self->on_self_part(@_) });
|
||||||
|
|
||||||
|
|
||||||
|
$self->{user_index} = {};
|
||||||
|
$self->{user_cache} = {};
|
||||||
|
|
||||||
|
$self->rebuild_user_index;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub on_join {
|
sub on_join {
|
||||||
@ -74,16 +84,36 @@ sub on_join {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub on_departure {
|
||||||
|
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);
|
||||||
|
$self->decache_user($channel, "$nick!$user\@$host");
|
||||||
|
}
|
||||||
|
|
||||||
|
sub on_kick {
|
||||||
|
my ($self, $event_type, $event) = @_;
|
||||||
|
my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->{args}[0]);
|
||||||
|
($nick, $user, $host) = $self->{pbot}->{irchandlers}->normalize_hostmask($nick, $user, $host);
|
||||||
|
$self->decache_user($channel, "$nick!$user\@$host");
|
||||||
|
}
|
||||||
|
|
||||||
|
sub on_self_part {
|
||||||
|
my ($self, $event_type, $event) = @_;
|
||||||
|
delete $self->{user_cache}->{lc $event->{channel}};
|
||||||
|
}
|
||||||
|
|
||||||
sub add_user {
|
sub add_user {
|
||||||
my ($self, $name, $channel, $hostmask, $capabilities, $password, $dont_save) = @_;
|
my ($self, $name, $channels, $hostmasks, $capabilities, $password, $dont_save) = @_;
|
||||||
$channel = '.*' if $channel !~ m/^#/;
|
$channels = 'global' if $channels !~ m/^#/;
|
||||||
|
|
||||||
$capabilities //= 'none';
|
$capabilities //= 'none';
|
||||||
$password //= $self->{pbot}->random_nick(16);
|
$password //= $self->{pbot}->random_nick(16);
|
||||||
|
|
||||||
my $data = {
|
my $data = {
|
||||||
name => $name,
|
channels => $channels,
|
||||||
password => $password
|
hostmasks => $hostmasks,
|
||||||
|
password => $password
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach my $cap (split /\s*,\s*/, lc $capabilities) {
|
foreach my $cap (split /\s*,\s*/, lc $capabilities) {
|
||||||
@ -91,36 +121,32 @@ sub add_user {
|
|||||||
$data->{$cap} = 1;
|
$data->{$cap} = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->{pbot}->{logger}->log("Adding new user (caps: $capabilities): name: $name hostmask: $hostmask channel: $channel\n");
|
$self->{pbot}->{logger}->log("Adding new user (caps: $capabilities): name: $name hostmasks: $hostmasks channels: $channels\n");
|
||||||
$self->{users}->add($channel, $hostmask, $data, $dont_save);
|
$self->{users}->add($name, $data, $dont_save);
|
||||||
|
$self->rebuild_user_index;
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub remove_user {
|
sub remove_user {
|
||||||
my ($self, $channel, $hostmask) = @_;
|
my ($self, $name) = @_;
|
||||||
return $self->{users}->remove($channel, $hostmask);
|
my $result = $self->{users}->remove($name);
|
||||||
|
$self->rebuild_user_index;
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub load {
|
sub load {
|
||||||
my $self = shift;
|
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;
|
$self->{users}->load;
|
||||||
|
|
||||||
my $i = 0;
|
my $i = 0;
|
||||||
foreach my $channel (sort $self->{users}->get_keys) {
|
foreach my $name (sort $self->{users}->get_keys) {
|
||||||
foreach my $hostmask (sort $self->{users}->get_keys($channel)) {
|
$i++;
|
||||||
$i++;
|
my $password = $self->{users}->get_data($name, 'password');
|
||||||
my $name = $self->{users}->get_data($channel, $hostmask, 'name');
|
my $channels = $self->{users}->get_data($name, 'channels');
|
||||||
my $password = $self->{users}->get_data($channel, $hostmask, 'password');
|
my $hostmasks = $self->{users}->get_data($name, 'hostmasks');
|
||||||
if (not defined $name or not defined $password) { Carp::croak "A user in $filename is missing critical data\n"; }
|
if (not defined $channels or not defined $hostmasks or not defined $password) {
|
||||||
|
Carp::croak "User $name is missing critical data\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$self->{pbot}->{logger}->log(" $i users loaded.\n");
|
$self->{pbot}->{logger}->log(" $i users loaded.\n");
|
||||||
@ -131,81 +157,101 @@ sub save {
|
|||||||
$self->{users}->save;
|
$self->{users}->save;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub rebuild_user_index {
|
||||||
|
my ($self) = @_;
|
||||||
|
|
||||||
|
$self->{user_index} = {};
|
||||||
|
|
||||||
|
foreach my $name ($self->{users}->get_keys) {
|
||||||
|
my $channels = $self->{users}->get_data($name, 'channels');
|
||||||
|
my $hostmasks = $self->{users}->get_data($name, 'hostmasks');
|
||||||
|
|
||||||
|
my @c = split /\s*,\s*/, $channels;
|
||||||
|
my @h = split /\s*,\s*/, $hostmasks;
|
||||||
|
|
||||||
|
foreach my $channel (@c) {
|
||||||
|
foreach my $hostmask (@h) {
|
||||||
|
$self->{user_index}->{lc $channel}->{lc $hostmask} = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub cache_user {
|
||||||
|
my ($self, $channel, $hostmask, $username, $account_mask) = @_;
|
||||||
|
$self->{user_cache}->{lc $channel}->{lc $hostmask} = [ $username, $account_mask ];
|
||||||
|
}
|
||||||
|
|
||||||
|
sub decache_user {
|
||||||
|
my ($self, $channel, $hostmask) = @_;
|
||||||
|
my $lc_hostmask = lc $hostmask;
|
||||||
|
delete $self->{user_cache}->{lc $channel}->{$lc_hostmask};
|
||||||
|
delete $self->{user_cache}->{global}->{$lc_hostmask};
|
||||||
|
}
|
||||||
|
|
||||||
sub find_user_account {
|
sub find_user_account {
|
||||||
my ($self, $channel, $hostmask, $any_channel) = @_;
|
my ($self, $channel, $hostmask, $any_channel) = @_;
|
||||||
$channel = lc $channel;
|
$channel = lc $channel;
|
||||||
$hostmask = lc $hostmask;
|
$hostmask = lc $hostmask;
|
||||||
$any_channel //= 0;
|
$any_channel //= 0;
|
||||||
|
|
||||||
my $sort;
|
# first try to find an exact match
|
||||||
if ($channel =~ m/^#/) {
|
|
||||||
$sort = sub { $a cmp $b };
|
if (exists $self->{user_cache}->{$channel} and exists $self->{user_cache}->{$channel}->{$hostmask}) {
|
||||||
} else {
|
my ($username, $account_mask) = @{$self->{user_cache}->{$channel}->{$hostmask}};
|
||||||
$sort = sub { $b cmp $a };
|
return ($channel, $account_mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach my $chan (sort $sort $self->{users}->get_keys) {
|
if (exists $self->{user_cache}->{global} and exists $self->{user_cache}->{global}->{$hostmask}) {
|
||||||
if (($channel !~ m/^#/ and $any_channel) or $channel =~ m/^$chan$/i) {
|
my ($username, $account_mask) = @{$self->{user_cache}->{global}->{$hostmask}};
|
||||||
if (not $self->{users}->exists($chan, $hostmask)) {
|
return ('global', $account_mask);
|
||||||
# find hostmask by account name or wildcard
|
}
|
||||||
foreach my $mask ($self->{users}->get_keys($chan)) {
|
|
||||||
if (lc $self->{users}->get_data($chan, $mask, 'name') eq $hostmask) { return ($chan, $mask); }
|
|
||||||
|
|
||||||
if ($mask =~ /[*?]/) {
|
if (exists $self->{user_index}->{$channel} and exists $self->{user_index}->{$channel}->{$hostmask}) {
|
||||||
# contains * or ? so it's converted to a regex
|
return ($channel, $hostmask);
|
||||||
my $mask_quoted = quotemeta $mask;
|
}
|
||||||
$mask_quoted =~ s/\\\*/.*?/g;
|
|
||||||
$mask_quoted =~ s/\\\?/./g;
|
if (exists $self->{user_index}->{global} and exists $self->{user_index}->{global}->{$hostmask}) {
|
||||||
if ($hostmask =~ m/^$mask_quoted$/i) { return ($chan, $mask); }
|
return ('global', $hostmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
# no exact matches found -- check for wildcard matches
|
||||||
|
|
||||||
|
my @search_channels;
|
||||||
|
|
||||||
|
if ($any_channel) {
|
||||||
|
@search_channels = keys %{$self->{user_index}};
|
||||||
|
} else {
|
||||||
|
@search_channels = ($channel, 'global');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach my $search_channel (@search_channels) {
|
||||||
|
if (exists $self->{user_index}->{$search_channel}) {
|
||||||
|
foreach my $mask (keys %{$self->{user_index}->{$search_channel}}) {
|
||||||
|
if ($mask =~ m/[*?]/) {
|
||||||
|
# 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) {
|
||||||
|
return ($search_channel, $mask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return ($chan, $hostmask);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (undef, $hostmask);
|
return (undef, $hostmask);
|
||||||
}
|
}
|
||||||
|
|
||||||
sub find_user {
|
sub find_user {
|
||||||
my ($self, $channel, $hostmask, $any_channel) = @_;
|
my ($self, $channel, $hostmask, $any_channel) = @_;
|
||||||
$any_channel //= 0;
|
$any_channel //= 0;
|
||||||
($channel, $hostmask) = $self->find_user_account($channel, $hostmask, $any_channel);
|
my ($found_channel, $found_hostmask) = $self->find_user_account($channel, $hostmask, $any_channel);
|
||||||
return undef if not $any_channel and not defined $channel;
|
return undef if not defined $found_channel;
|
||||||
|
my $name = $self->{user_index}->{$found_channel}->{$found_hostmask};
|
||||||
$channel = '.*' if not defined $channel;
|
$self->cache_user($found_channel, $hostmask, $name, $found_hostmask);
|
||||||
$hostmask = '.*' if not defined $hostmask;
|
return wantarray ? ($self->{users}->get_data($name), $name) : $self->{users}->get_data($name);
|
||||||
$hostmask = lc $hostmask;
|
|
||||||
|
|
||||||
my $sort;
|
|
||||||
if ($channel =~ m/^#/) {
|
|
||||||
$sort = sub { $a cmp $b };
|
|
||||||
} else {
|
|
||||||
$sort = sub { $b cmp $a };
|
|
||||||
}
|
|
||||||
|
|
||||||
my $user = eval {
|
|
||||||
foreach my $channel_regex (sort $sort $self->{users}->get_keys) {
|
|
||||||
if (($channel !~ m/^#/ and $any_channel) or $channel =~ m/^$channel_regex$/i) {
|
|
||||||
foreach my $hostmask_regex ($self->{users}->get_keys($channel_regex)) {
|
|
||||||
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) { return $self->{users}->get_data($channel_regex, $hostmask_regex); }
|
|
||||||
} else {
|
|
||||||
# direct comparison
|
|
||||||
if ($hostmask eq lc $hostmask_regex) { return $self->{users}->get_data($channel_regex, $hostmask_regex); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undef;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($@) { $self->{pbot}->{logger}->log("Error in find_user parameters: $@\n"); }
|
|
||||||
return $user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub find_admin {
|
sub find_admin {
|
||||||
@ -233,7 +279,7 @@ sub loggedin_admin {
|
|||||||
sub login {
|
sub login {
|
||||||
my ($self, $channel, $hostmask, $password) = @_;
|
my ($self, $channel, $hostmask, $password) = @_;
|
||||||
my $user = $self->find_user($channel, $hostmask);
|
my $user = $self->find_user($channel, $hostmask);
|
||||||
my $channel_text = $channel eq '.*' ? '' : " for $channel";
|
my $channel_text = $channel eq 'global' ? '' : " for $channel";
|
||||||
|
|
||||||
if (not defined $user) {
|
if (not defined $user) {
|
||||||
$self->{pbot}->{logger}->log("Attempt to login non-existent [$channel][$hostmask] failed\n");
|
$self->{pbot}->{logger}->log("Attempt to login non-existent [$channel][$hostmask] failed\n");
|
||||||
@ -246,8 +292,10 @@ sub login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$user->{loggedin} = 1;
|
$user->{loggedin} = 1;
|
||||||
$self->{pbot}->{logger}->log("$hostmask logged into $user->{name} ($hostmask)$channel_text.\n");
|
my ($user_chan, $user_hostmask) = $self->find_user_account($channel, $hostmask);
|
||||||
return "Logged into $user->{name} ($hostmask)$channel_text.";
|
my $name = $self->{user_index}->{$user_chan}->{$user_hostmask};
|
||||||
|
$self->{pbot}->{logger}->log("$hostmask logged into " . $self->{users}->get_key_name($name) . " ($hostmask)$channel_text.\n");
|
||||||
|
return "Logged into " . $self->{users}->get_key_name($name) . " ($hostmask)$channel_text.";
|
||||||
}
|
}
|
||||||
|
|
||||||
sub logout {
|
sub logout {
|
||||||
@ -283,10 +331,14 @@ sub logincmd {
|
|||||||
my ($user_channel, $user_hostmask) = $self->find_user_account($channel, "$nick!$user\@$host");
|
my ($user_channel, $user_hostmask) = $self->find_user_account($channel, "$nick!$user\@$host");
|
||||||
return "/msg $nick You do not have a user account." if not defined $user_channel;
|
return "/msg $nick You do not have a user account." if not defined $user_channel;
|
||||||
|
|
||||||
my $u = $self->{users}->get_data($user_channel, $user_hostmask);
|
my $name = $self->{user_index}->{$user_channel}->{$user_hostmask};
|
||||||
my $channel_text = $user_channel eq '.*' ? '' : " for $user_channel";
|
|
||||||
|
|
||||||
if ($u->{loggedin}) { return "/msg $nick You are already logged into $u->{name} ($user_hostmask)$channel_text."; }
|
my $u = $self->{users}->get_data($name);
|
||||||
|
my $channel_text = $user_channel eq 'global' ? '' : " for $user_channel";
|
||||||
|
|
||||||
|
if ($u->{loggedin}) {
|
||||||
|
return "/msg $nick You are already logged into " . $self->{users}->get_key_name($name) . " ($user_hostmask)$channel_text.";
|
||||||
|
}
|
||||||
|
|
||||||
my $result = $self->login($user_channel, $user_hostmask, $arguments);
|
my $result = $self->login($user_channel, $user_hostmask, $arguments);
|
||||||
return "/msg $nick $result";
|
return "/msg $nick $result";
|
||||||
@ -298,12 +350,14 @@ sub logoutcmd {
|
|||||||
my ($user_channel, $user_hostmask) = $self->find_user_account($from, "$nick!$user\@$host");
|
my ($user_channel, $user_hostmask) = $self->find_user_account($from, "$nick!$user\@$host");
|
||||||
return "/msg $nick You do not have a user account." if not defined $user_channel;
|
return "/msg $nick You do not have a user account." if not defined $user_channel;
|
||||||
|
|
||||||
my $u = $self->{users}->get_data($user_channel, $user_hostmask);
|
my $name = $self->{user_index}->{$user_channel}->{$user_hostmask};
|
||||||
my $channel_text = $user_channel eq '.*' ? '' : " for $user_channel";
|
|
||||||
return "/msg $nick You are not logged into $u->{name} ($user_hostmask)$channel_text." if not $u->{loggedin};
|
my $u = $self->{users}->get_data($name);
|
||||||
|
my $channel_text = $user_channel eq 'global' ? '' : " for $user_channel";
|
||||||
|
return "/msg $nick You are not logged into " . $self->{users}->get_key_name($name) . " ($user_hostmask)$channel_text." if not $u->{loggedin};
|
||||||
|
|
||||||
$self->logout($user_channel, $user_hostmask);
|
$self->logout($user_channel, $user_hostmask);
|
||||||
return "/msg $nick Logged out of $u->{name} ($user_hostmask)$channel_text.";
|
return "/msg $nick Logged out of " . $self->{users}->get_key_name($name) . " ($user_hostmask)$channel_text.";
|
||||||
}
|
}
|
||||||
|
|
||||||
sub users {
|
sub users {
|
||||||
@ -313,37 +367,38 @@ sub users {
|
|||||||
my $include_global = '';
|
my $include_global = '';
|
||||||
if (not defined $channel) {
|
if (not defined $channel) {
|
||||||
$channel = $from;
|
$channel = $from;
|
||||||
$include_global = '.*';
|
$include_global = 'global';
|
||||||
} else {
|
} else {
|
||||||
$channel = '.*' if $channel !~ /^#/;
|
$channel = 'global' if $channel !~ /^#/;
|
||||||
}
|
}
|
||||||
|
|
||||||
my $text = "Users: ";
|
my $text = "Users: ";
|
||||||
my $last_channel = "";
|
my $last_channel = "";
|
||||||
my $sep = "";
|
my $sep = "";
|
||||||
foreach my $chan (sort $self->{users}->get_keys) {
|
foreach my $chan (sort keys %{$self->{user_index}}) {
|
||||||
next if $from =~ m/^#/ and $chan ne $channel and $chan ne $include_global;
|
next if $from =~ m/^#/ and $chan ne $channel and $chan ne $include_global;
|
||||||
next if $from !~ m/^#/ and $channel =~ m/^#/ and $chan ne $channel;
|
next if $from !~ m/^#/ and $channel =~ m/^#/ and $chan ne $channel;
|
||||||
|
|
||||||
if ($last_channel ne $chan) {
|
if ($last_channel ne $chan) {
|
||||||
$text .= $sep . ($chan eq ".*" ? "global" : $chan) . ": ";
|
$text .= "$sep$chan: ";
|
||||||
$last_channel = $chan;
|
$last_channel = $chan;
|
||||||
$sep = "";
|
$sep = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach my $hostmask (sort { return 0 if $a eq '_name' or $b eq '_name'; $self->{users}->get_data($chan, $a, 'name') cmp $self->{users}->get_data($chan, $b, 'name') }
|
foreach my $hostmask (sort { $self->{user_index}->{$chan}->{$a} cmp $self->{user_index}->{$chan}->{$b} }
|
||||||
$self->{users}->get_keys($chan))
|
keys %{$self->{user_index}->{$chan}})
|
||||||
{
|
{
|
||||||
|
my $name = $self->{user_index}->{$chan}->{$hostmask};
|
||||||
$text .= $sep;
|
$text .= $sep;
|
||||||
my $has_cap = 0;
|
my $has_cap = 0;
|
||||||
foreach my $key ($self->{users}->get_keys($chan, $hostmask)) {
|
foreach my $key ($self->{users}->get_keys($name)) {
|
||||||
if ($self->{pbot}->{capabilities}->exists($key)) {
|
if ($self->{pbot}->{capabilities}->exists($key)) {
|
||||||
$has_cap = 1;
|
$has_cap = 1;
|
||||||
last;
|
last;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$text .= '+' if $has_cap;
|
$text .= '+' if $has_cap;
|
||||||
$text .= $self->{users}->get_data($chan, $hostmask, 'name');
|
$text .= $self->{users}->get_key_name($name);
|
||||||
$sep = " ";
|
$sep = " ";
|
||||||
}
|
}
|
||||||
$sep = "; ";
|
$sep = "; ";
|
||||||
@ -353,18 +408,20 @@ sub users {
|
|||||||
|
|
||||||
sub useradd {
|
sub useradd {
|
||||||
my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_;
|
my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_;
|
||||||
my ($name, $channel, $hostmask, $capabilities, $password) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 5);
|
my ($name, $hostmasks, $channels, $capabilities, $password) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 5);
|
||||||
$capabilities //= 'none';
|
$capabilities //= 'none';
|
||||||
|
|
||||||
if (not defined $name or not defined $channel or not defined $hostmask) { return "Usage: useradd <account name> <channel> <hostmask> [capabilities [password]]"; }
|
if (not defined $name or not defined $hostmasks) { return "Usage: useradd <username> <hostmasks> [channels [capabilities [password]]]"; }
|
||||||
|
|
||||||
$channel = '.*' if $channel !~ /^#/;
|
$channels = 'global' if $channels !~ /^#/;
|
||||||
|
|
||||||
my $u = $self->{pbot}->{users}->find_user($channel, "$nick!$user\@$host");
|
my $u;
|
||||||
|
foreach my $channel (sort split /\s*,\s*/, lc $channels) {
|
||||||
|
$u = $self->{pbot}->{users}->find_user($channel, "$nick!$user\@$host");
|
||||||
|
|
||||||
if (not defined $u) {
|
if (not defined $u) {
|
||||||
$channel = 'global' if $channel eq '.*';
|
return "You do not have a user account for $channel; cannot add users to that channel.\n";
|
||||||
return "You do not have a user account for $channel; cannot add users to that channel.\n";
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($capabilities ne 'none' and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
|
if ($capabilities ne 'none' and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
|
||||||
@ -372,25 +429,28 @@ sub useradd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach my $cap (split /\s*,\s*/, lc $capabilities) {
|
foreach my $cap (split /\s*,\s*/, lc $capabilities) {
|
||||||
next if $cap eq 'none';
|
next if $cap eq 'none';
|
||||||
|
|
||||||
return "There is no such capability $cap." if not $self->{pbot}->{capabilities}->exists($cap);
|
return "There is no such capability $cap." if not $self->{pbot}->{capabilities}->exists($cap);
|
||||||
|
|
||||||
if (not $self->{pbot}->{capabilities}->userhas($u, $cap)) { return "To set the $cap capability your user account must also have it."; }
|
if (not $self->{pbot}->{capabilities}->userhas($u, $cap)) { return "To set the $cap capability your user account must also have it."; }
|
||||||
|
|
||||||
if ($self->{pbot}->{capabilities}->has($cap, 'admin') and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-admins')) {
|
if ($self->{pbot}->{capabilities}->has($cap, 'admin') and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-admins')) {
|
||||||
return "To set the $cap capability your user account must have the can-modify-admins capability.";
|
return "To set the $cap capability your user account must have the can-modify-admins capability.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$self->{pbot}->{users}->add_user($name, $channel, $hostmask, $capabilities, $password);
|
|
||||||
|
$self->{pbot}->{users}->add_user($name, $channels, $hostmasks, $capabilities, $password);
|
||||||
return "User added.";
|
return "User added.";
|
||||||
}
|
}
|
||||||
|
|
||||||
sub userdel {
|
sub userdel {
|
||||||
my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_;
|
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 "Usage: userdel <channel> <hostmask or account name>"; }
|
if (not length $arguments) { return "Usage: userdel <username>"; }
|
||||||
|
|
||||||
my $u = $self->find_user($channel, "$nick!$user\@$host");
|
my $u = $self->find_user($from, "$nick!$user\@$host");
|
||||||
my $t = $self->find_user($channel, $hostmask);
|
my $t = $self->{users}->get_data($arguments);
|
||||||
|
|
||||||
if ($self->{pbot}->{capabilities}->userhas($t, 'botowner') and not $self->{pbot}->{capabilities}->userhas($u, 'botowner')) {
|
if ($self->{pbot}->{capabilities}->userhas($t, 'botowner') and not $self->{pbot}->{capabilities}->userhas($u, 'botowner')) {
|
||||||
return "Only botowners may delete botowner user accounts.";
|
return "Only botowners may delete botowner user accounts.";
|
||||||
@ -400,31 +460,26 @@ sub userdel {
|
|||||||
return "To delete admin user accounts your user account must have the can-modify-admins capability.";
|
return "To delete admin user accounts your user account must have the can-modify-admins capability.";
|
||||||
}
|
}
|
||||||
|
|
||||||
my ($found_channel, $found_hostmask) = $self->find_user_account($channel, $hostmask);
|
return $self->remove_user($arguments);
|
||||||
$found_channel = $channel if not defined $found_channel; # let DualIndexHashObject disambiguate
|
|
||||||
return $self->remove_user($found_channel, $found_hostmask);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub userset {
|
sub userset {
|
||||||
my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_;
|
my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_;
|
||||||
|
|
||||||
if (length $arguments and $stuff->{arglist}[0] !~ m/^(#|\.\*$|global$)/) { $self->{pbot}->{interpreter}->unshift_arg($stuff->{arglist}, $from) }
|
my ($name, $key, $value) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 3);
|
||||||
|
|
||||||
my ($channel, $hostmask, $key, $value) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 4);
|
if (not defined $name) { return "Usage: userset <username> [key [value]]"; }
|
||||||
|
|
||||||
if (not defined $hostmask) { return "Usage: userset [channel] <hostmask or account name> [key [value]]"; }
|
my $u = $self->find_user($from, "$nick!$user\@$host", 1);
|
||||||
|
my $target = $self->{users}->get_data($name);
|
||||||
my $u = $self->find_user($channel, "$nick!$user\@$host");
|
|
||||||
my $target = $self->find_user($channel, $hostmask);
|
|
||||||
|
|
||||||
if (not $u) {
|
if (not $u) {
|
||||||
$channel = 'global' if $channel eq '.*';
|
$from = 'global' if $from !~ /^#/;
|
||||||
return "You do not have a user account for $channel; cannot modify their users.";
|
return "You do not have a user account for $from; cannot modify their users.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (not $target) {
|
if (not $target) {
|
||||||
if ($channel !~ /^#/) { return "There is no user account $hostmask."; }
|
return "There is no user account $name.";
|
||||||
else { return "There is no user account $hostmask for $channel."; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defined $value and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
|
if (defined $value and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
|
||||||
@ -441,52 +496,58 @@ sub userset {
|
|||||||
return "To set the $key capability your user account must also have it." unless $self->{pbot}->{capabilities}->userhas($u, 'botowner');
|
return "To set the $key capability your user account must also have it." unless $self->{pbot}->{capabilities}->userhas($u, 'botowner');
|
||||||
}
|
}
|
||||||
|
|
||||||
my ($found_channel, $found_hostmask) = $self->find_user_account($channel, $hostmask);
|
my $result = $self->{users}->set($name, $key, $value);
|
||||||
$found_channel = $channel if not defined $found_channel; # let DualIndexHashObject disambiguate
|
print "result [$result]\n";
|
||||||
my $result = $self->{users}->set($found_channel, $found_hostmask, $key, $value);
|
|
||||||
$result =~ s/^password => .*;?$/password => <private>;/m;
|
$result =~ s/^password => .*;?$/password => <private>;/m;
|
||||||
|
|
||||||
|
if (defined $key and ($key eq 'channels' or $key eq 'hostmasks') and defined $value) {
|
||||||
|
$self->rebuild_user_index;
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub userunset {
|
sub userunset {
|
||||||
my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_;
|
my ($self, $from, $nick, $user, $host, $arguments, $stuff) = @_;
|
||||||
|
|
||||||
if (length $arguments and $stuff->{arglist}[0] !~ m/^(#|\.\*$|global$)/) { $self->{pbot}->{interpreter}->unshift_arg($stuff->{arglist}, $from) }
|
my ($name, $key) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 2);
|
||||||
|
|
||||||
my ($channel, $hostmask, $key) = $self->{pbot}->{interpreter}->split_args($stuff->{arglist}, 3);
|
if (not defined $name or not defined $key) { return "Usage: userunset <username> <key>"; }
|
||||||
|
|
||||||
if (not defined $hostmask) { return "Usage: userunset [channel] <hostmask or account name> <key>"; }
|
$key = lc $key;
|
||||||
|
|
||||||
my $u = $self->find_user($channel, "$nick!$user\@$host");
|
my @disallowed = qw/channels hostmasks password/;
|
||||||
my $target = $self->find_user($channel, $hostmask);
|
if (grep { $_ eq $key } @disallowed) {
|
||||||
|
return "The $key metadata cannot be unset. Use the `userset` command to modify it.";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $u = $self->find_user($from, "$nick!$user\@$host", 1);
|
||||||
|
my $target = $self->{users}->get_data($name);
|
||||||
|
|
||||||
if (not $u) {
|
if (not $u) {
|
||||||
$channel = 'global' if $channel eq '.*';
|
$from = 'global' if $from !~ /^#/;
|
||||||
return "You do not have a user account for $channel; cannot modify their users.";
|
return "You do not have a user account for $from; cannot modify their users.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (not $target) {
|
if (not $target) {
|
||||||
if ($channel !~ /^#/) { return "There is no user account $hostmask."; }
|
return "There is no user account $name.";
|
||||||
else { return "There is no user account $hostmask for $channel."; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defined $key and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
|
if (not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-capabilities')) {
|
||||||
if ($key =~ m/^can-/i or $self->{pbot}->{capabilities}->exists($key)) {
|
if ($key =~ m/^can-/i or $self->{pbot}->{capabilities}->exists($key)) {
|
||||||
return "The $key metadata requires the can-modify-capabilities capability, which your user account does not have.";
|
return "The $key metadata requires the can-modify-capabilities capability, which your user account does not have.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defined $key and $self->{pbot}->{capabilities}->userhas($target, 'admin') and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-admins')) {
|
if ($self->{pbot}->{capabilities}->userhas($target, 'admin') and not $self->{pbot}->{capabilities}->userhas($u, 'can-modify-admins')) {
|
||||||
return "To modify admin user accounts your user account must have the can-modify-admins capability.";
|
return "To modify admin user accounts your user account must have the can-modify-admins capability.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defined $key and $self->{pbot}->{capabilities}->exists($key) and not $self->{pbot}->{capabilities}->userhas($u, $key)) {
|
if ($self->{pbot}->{capabilities}->exists($key) and not $self->{pbot}->{capabilities}->userhas($u, $key)) {
|
||||||
return "To unset the $key capability your user account must also have it." unless $self->{pbot}->{capabilities}->userhas($u, 'botowner');
|
return "To unset the $key capability your user account must also have it." unless $self->{pbot}->{capabilities}->userhas($u, 'botowner');
|
||||||
}
|
}
|
||||||
|
|
||||||
my ($found_channel, $found_hostmask) = $self->find_user_account($channel, $hostmask);
|
return $self->{users}->unset($name, $key);
|
||||||
$found_channel = $channel if not defined $found_channel; # let DualIndexHashObject disambiguate
|
|
||||||
return $self->{users}->unset($found_channel, $found_hostmask, $key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub mycmd {
|
sub mycmd {
|
||||||
@ -501,24 +562,25 @@ sub mycmd {
|
|||||||
my $channel = $from;
|
my $channel = $from;
|
||||||
my $hostmask = "$nick!$user\@$host";
|
my $hostmask = "$nick!$user\@$host";
|
||||||
|
|
||||||
my $u = $self->find_user($channel, $hostmask, 1);
|
my ($u, $name) = $self->find_user($channel, $hostmask, 1);
|
||||||
|
|
||||||
if (not $u) {
|
if (not $u) {
|
||||||
$channel = '.*';
|
$channel = 'global';
|
||||||
$hostmask = "$nick!$user\@" . $self->{pbot}->{antiflood}->address_to_mask($host);
|
$hostmask = "$nick!$user\@" . $self->{pbot}->{antiflood}->address_to_mask($host);
|
||||||
my $name = $nick;
|
$name = $nick;
|
||||||
|
|
||||||
my ($existing_channel, $existing_hostmask) = $self->find_user_account($channel, $name);
|
$u = $self->{users}->get_data($name);
|
||||||
if ($existing_hostmask ne lc $name) {
|
if ($u) {
|
||||||
# user exists by name
|
$self->{pbot}->{logger}->log("Adding additional hostmask $hostmask to user account $name\n");
|
||||||
return "There is already an user account named $name but its hostmask ($existing_hostmask) does not match your hostmask ($hostmask). Ask an admin for help.";
|
$u->{hostmasks} .= ",$hostmask";
|
||||||
|
$self->rebuild_user_index;
|
||||||
|
} else {
|
||||||
|
$u = $self->add_user($name, $channel, $hostmask, undef, undef, 1);
|
||||||
|
$u->{loggedin} = 1;
|
||||||
|
$u->{stayloggedin} = 1;
|
||||||
|
$u->{autologin} = 1;
|
||||||
|
$self->save;
|
||||||
}
|
}
|
||||||
|
|
||||||
$u = $self->add_user($name, $channel, $hostmask, undef, undef, 1);
|
|
||||||
$u->{loggedin} = 1;
|
|
||||||
$u->{stayloggedin} = 1;
|
|
||||||
$u->{autologin} = 1;
|
|
||||||
$self->save;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
my $result = '';
|
my $result = '';
|
||||||
@ -550,10 +612,7 @@ sub mycmd {
|
|||||||
$result = "Usage: my <key> [value]; ";
|
$result = "Usage: my <key> [value]; ";
|
||||||
}
|
}
|
||||||
|
|
||||||
my ($found_channel, $found_hostmask) = $self->find_user_account($channel, $hostmask, 1);
|
$result .= $self->{users}->set($name, $key, $value);
|
||||||
($found_channel, $found_hostmask) = $self->find_user_account('.*', $hostmask, 1) if not defined $found_channel;
|
|
||||||
return "No user account found in $channel." if not defined $found_channel;
|
|
||||||
$result .= $self->{users}->set($found_channel, $found_hostmask, $key, $value);
|
|
||||||
$result =~ s/^password => .*;?$/password => <private>;/m;
|
$result =~ s/^password => .*;?$/password => <private>;/m;
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ use LWP::UserAgent;
|
|||||||
# These are set automatically by the misc/update_version script
|
# These are set automatically by the misc/update_version script
|
||||||
use constant {
|
use constant {
|
||||||
BUILD_NAME => "PBot",
|
BUILD_NAME => "PBot",
|
||||||
BUILD_REVISION => 3509,
|
BUILD_REVISION => 3512,
|
||||||
BUILD_DATE => "2020-04-22",
|
BUILD_DATE => "2020-04-22",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
2
data/last_update
vendored
2
data/last_update
vendored
@ -1 +1 @@
|
|||||||
3509
|
3512
|
||||||
|
4
data/users
vendored
4
data/users
vendored
@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$metadata$" : {
|
"$metadata$" : {
|
||||||
"$metadata$" : {
|
"update_version" : 3512
|
||||||
"update_version" : 3503
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
doc/Admin.md
27
doc/Admin.md
@ -90,39 +90,40 @@ Usage: `logout`
|
|||||||
### useradd
|
### useradd
|
||||||
Adds a new user to PBot.
|
Adds a new user to PBot.
|
||||||
|
|
||||||
Usage: `useradd <account name> <channel> <hostmask> [capabilities [password]]`
|
Usage: `useradd <username> <hostmasks> [channels [capabilities [password]]]`
|
||||||
|
|
||||||
Parameter | Description
|
Parameter | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
`<account name>` | A unique name to identify this account (usually the `nick` of the user, but it can be any identifier).
|
`username` | A unique name to identify this account (usually the `nick` of the user, but it can be any identifier).
|
||||||
`<channel>` | The channel this user belongs to; use `global` for all channels. This field cannot be changed without removing and re-adding the user.
|
`hostmasks` | The hostmasks from which this user is recognized/allowed to login from (e.g., `somenick!*@*.somedomain.com` or `*!*@unaffiliated/someuser`). Can be a comma-separated list of values.
|
||||||
`<hostmask>` | The hostmask from which this user is recognized/allowed to login from (e.g., `somenick!*@*.somedomain.com` or `*!*@unaffiliated/someuser`). This field cannot be changed without removing and re-adding the user.
|
`channels` | The channels this user belongs to; use `global` for all channels. Can be a comma-separated list of values.
|
||||||
`[capabilities]` | A comma-separated list of [user-capabilities](#user-capabilities) for this user.
|
`capabilities` | A comma-separated list of [user-capabilities](#user-capabilities) for this user.
|
||||||
`[password]` | The password the user will use to login (from `/msg`, obviously). Generates a random password if omitted. Users may view and set their password by using the [`my`](Commands.md#my) command.
|
`password` | The password the user will use to login (from `/msg`, obviously). Generates a random password if omitted. Users may view and set their password by using the [`my`](Commands.md#my) command.
|
||||||
|
|
||||||
### userdel
|
### userdel
|
||||||
Removes a user from PBot. You can use the `account name` field or the `hostmask` field that was set via the [`useradd`](#useradd) command.
|
Removes a user from PBot.
|
||||||
|
|
||||||
Usage: `userdel <channel> <account name or hostmask>`
|
Usage: `userdel <username>`
|
||||||
|
|
||||||
### userset
|
### userset
|
||||||
Sets [metadata](#user-metadata-list) or [user-capabilities](#user-capabilities-list) for a user account. You can use the `account name` field or the `hostmask` field that was set via the [`useradd`](#useradd) command. See also: [user metadata list](#user-metadata-list).
|
Sets [metadata](#user-metadata-list) or [user-capabilities](#user-capabilities-list) for a user account. See also: [user metadata list](#user-metadata-list).
|
||||||
|
|
||||||
If `key` is omitted, it will list all the keys and values that are set. If `value` is omitted, it will show the value for `key`.
|
If `key` is omitted, it will list all the keys and values that are set. If `value` is omitted, it will show the value for `key`.
|
||||||
|
|
||||||
Usage: `userset [channel] <account name or hostmask> [<key> [value]]`
|
Usage: `userset <username> [<key> [value]]`
|
||||||
|
|
||||||
### userunset
|
### userunset
|
||||||
Deletes a [metadata](#user-metadata-list) or [user-capability](#user-capabilities-list) from a user account. You can use the `account name` field or the `hostmask` field that was set via the [`useradd`](#useradd) command.
|
Deletes a [metadata](#user-metadata-list) or [user-capability](#user-capabilities-list) from a user account.
|
||||||
|
|
||||||
Usage: `userunset [channel] <account name or hostmask> <key>`
|
Usage: `userunset <username> <key>`
|
||||||
|
|
||||||
#### User metadata list
|
#### User metadata list
|
||||||
This is a list of recognized metadata keys for user accounts.
|
This is a list of recognized metadata keys for user accounts.
|
||||||
|
|
||||||
Name | Description
|
Name | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
`name` | A unique name identifying the user account.
|
`hostmasks` | A comma-separated list of hostmasks this user is recognized by.
|
||||||
|
`channels` | A comma-separated list of channels this user belongs to.
|
||||||
`password` | The password for the user account.
|
`password` | The password for the user account.
|
||||||
`loggedin` | Whether the user is logged in or not.
|
`loggedin` | Whether the user is logged in or not.
|
||||||
`stayloggedin` | Do not log the user out when they part/quit.
|
`stayloggedin` | Do not log the user out when they part/quit.
|
||||||
|
@ -58,9 +58,9 @@ To whitelist a user, use the [`useradd`](Admin.md#useradd) command with the
|
|||||||
`is-whitelisted` capability argument. To whitelist them in all channels, add
|
`is-whitelisted` capability argument. To whitelist them in all channels, add
|
||||||
the user to the global channel.
|
the user to the global channel.
|
||||||
|
|
||||||
Usage: `useradd <user account name> <channel> <hostmask> is-whitelisted`
|
Usage: `useradd <username> <hostmasks> <channels> is-whitelisted`
|
||||||
|
|
||||||
If the user already exists, use the [`userset`](Admin.md#userset) command to
|
If the user already exists, use the [`userset`](Admin.md#userset) command to
|
||||||
grant them the `is-whitelisted` capability.
|
grant them the `is-whitelisted` capability.
|
||||||
|
|
||||||
Usage: `userset [channel] <user account name> is-whitelisted 1`
|
Usage: `userset <username> is-whitelisted 1`
|
||||||
|
@ -241,7 +241,7 @@ commands in the PBot terminal console.
|
|||||||
|
|
||||||
Suppose your nick is `Bob` and your hostmask is `Bob!~user@some.domain.com`.
|
Suppose your nick is `Bob` and your hostmask is `Bob!~user@some.domain.com`.
|
||||||
|
|
||||||
useradd Bob global Bob!~user@*.domain.com botowner
|
useradd Bob Bob!~user@*.domain.com global botowner
|
||||||
|
|
||||||
This will create a user account named `Bob` with the `botowner` [user-capability](Admin.md#user-capabilities) that can administrate
|
This will create a user account named `Bob` with the `botowner` [user-capability](Admin.md#user-capabilities) that can administrate
|
||||||
all channels. Note the wildcard replacing `some` in `some.domain.com`. Now as long as
|
all channels. Note the wildcard replacing `some` in `some.domain.com`. Now as long as
|
||||||
@ -267,7 +267,9 @@ the terminal console.
|
|||||||
### Adding other users and admins
|
### Adding other users and admins
|
||||||
To add users to PBot, use the [`useradd`](Admin.md#useradd) command.
|
To add users to PBot, use the [`useradd`](Admin.md#useradd) command.
|
||||||
|
|
||||||
useradd <account name> <channel> <hostmask> [capabilities [password]]
|
useradd <username> <hostmasks> [channels [capabilities [password]]]
|
||||||
|
|
||||||
|
The `hostmasks` and `channels` arguments can be a comma-separated list of values.
|
||||||
|
|
||||||
If you omit the `capabilities` argument, the user will be a normal unprivileged user. See [user-capabilities](Admin.md#user-capabilities)
|
If you omit the `capabilities` argument, the user will be a normal unprivileged user. See [user-capabilities](Admin.md#user-capabilities)
|
||||||
for more information about user-capabilities.
|
for more information about user-capabilities.
|
||||||
|
@ -197,7 +197,7 @@ general.send_who_on_join | When joining a channel, send the `WHO` command to get
|
|||||||
general.show_url_titles_channels | A regular-expression or comma-separated list of channels that should display titles for URLs. | `.*`
|
general.show_url_titles_channels | A regular-expression or comma-separated list of channels that should display titles for URLs. | `.*`
|
||||||
general.show_url_titles | If set to a true value, PBot will show titles for URLs. | 1
|
general.show_url_titles | If set to a true value, PBot will show titles for URLs. | 1
|
||||||
general.show_url_titles_ignore_channels | A regular-expression or comma-separated list of channels that will not display titles for URLs. |
|
general.show_url_titles_ignore_channels | A regular-expression or comma-separated list of channels that will not display titles for URLs. |
|
||||||
general.strictnamespace | When enabled, factoids belonging to other channels will not show up in current channels unless specifically invoked.
|
general.strictnamespace | When enabled, factoids belonging to other channels will not show up in current channels unless specifically invoked.| 0
|
||||||
general.trigger | The trigger character(s) or text that will invoke PBot commands. | [!]
|
general.trigger | The trigger character(s) or text that will invoke PBot commands. | [!]
|
||||||
interpreter.max_recursion | The maximum number of recursions allowed before the command interpreter will abort. | 100
|
interpreter.max_recursion | The maximum number of recursions allowed before the command interpreter will abort. | 100
|
||||||
irc.botnick | The IRC nickname of this PBot instance. |
|
irc.botnick | The IRC nickname of this PBot instance. |
|
||||||
|
54
updates/3512_update_users.pl
Executable file
54
updates/3512_update_users.pl
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
# Updates user JSON file to a better format to support multiple hostmasks and
|
||||||
|
# easier channel management
|
||||||
|
|
||||||
|
use warnings; use strict;
|
||||||
|
|
||||||
|
BEGIN {
|
||||||
|
use File::Basename;
|
||||||
|
my $location = -l __FILE__ ? dirname readlink __FILE__ : dirname __FILE__;
|
||||||
|
unshift @INC, $location;
|
||||||
|
}
|
||||||
|
|
||||||
|
use lib3512::HashObject;
|
||||||
|
use lib3512::DualIndexHashObject;
|
||||||
|
use lib3503::PBot;
|
||||||
|
|
||||||
|
my ($data_dir, $version, $last_update) = @ARGV;
|
||||||
|
|
||||||
|
print "Updating users... version: $version, last_update: $last_update, data_dir: $data_dir\n";
|
||||||
|
|
||||||
|
my $pbot = lib3503::PBot->new();
|
||||||
|
|
||||||
|
my $users = lib3512::DualIndexHashObject->new(name => 'old users', filename => "$data_dir/users", pbot => $pbot);
|
||||||
|
$users->load;
|
||||||
|
|
||||||
|
my $users2 = lib3512::HashObject->new(name => 'new users', filename => "$data_dir/users_new", pbot => $pbot);
|
||||||
|
|
||||||
|
foreach my $channel (keys %{$users->{hash}}) {
|
||||||
|
next if $channel eq '$metadata$';
|
||||||
|
foreach my $hostmask (keys %{$users->{hash}->{$channel}}) {
|
||||||
|
next if $hostmask eq '_name';
|
||||||
|
|
||||||
|
my $data = $users->{hash}->{$channel}->{$hostmask};
|
||||||
|
|
||||||
|
my $name = delete $data->{name};
|
||||||
|
delete $data->{_name};
|
||||||
|
my $channels = $channel;
|
||||||
|
$channels = 'global' if $channels eq '.*';
|
||||||
|
$data->{channels} = $channels;
|
||||||
|
$data->{hostmasks} = $hostmask;
|
||||||
|
|
||||||
|
$users2->add($name, $data, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$users2->add('$metadata$', { update_version => 3512 });
|
||||||
|
|
||||||
|
print "Overwriting users with user_new\n";
|
||||||
|
|
||||||
|
use File::Copy;
|
||||||
|
move("$data_dir/users_new", "$data_dir/users") or die "Failed to update users: $!";
|
||||||
|
|
||||||
|
exit 0;
|
426
updates/lib3512/DualIndexHashObject.pm
Normal file
426
updates/lib3512/DualIndexHashObject.pm
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
# File: DualIndexHashObject.pm
|
||||||
|
# Author: pragma_
|
||||||
|
#
|
||||||
|
# Purpose: Provides a hash-table object with an abstracted API that includes
|
||||||
|
# setting and deleting values, saving to and loading from files, etc. This
|
||||||
|
# extends the HashObject with an additional index key. Provides case-insensitive
|
||||||
|
# access to both index keys, while preserving original case when displaying the
|
||||||
|
# keys.
|
||||||
|
#
|
||||||
|
# Data is stored in working memory for lightning fast performance. If you have
|
||||||
|
# a huge amount of data, consider DualIndexSQLiteObject instead.
|
||||||
|
|
||||||
|
# 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 lib3512::DualIndexHashObject;
|
||||||
|
|
||||||
|
use warnings; use strict;
|
||||||
|
use feature 'unicode_strings';
|
||||||
|
|
||||||
|
use Text::Levenshtein qw(fastdistance);
|
||||||
|
use JSON;
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my ($proto, %conf) = @_;
|
||||||
|
my $class = ref($proto) || $proto;
|
||||||
|
my $self = bless {}, $class;
|
||||||
|
Carp::croak("Missing pbot reference to " . __FILE__) unless exists $conf{pbot};
|
||||||
|
$self->{pbot} = $conf{pbot};
|
||||||
|
$self->initialize(%conf);
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub initialize {
|
||||||
|
my ($self, %conf) = @_;
|
||||||
|
$self->{name} = $conf{name} // 'Dual Index hash object';
|
||||||
|
$self->{filename} = $conf{filename} // Carp::carp("Missing filename to DualIndexHashObject, will not be able to save to or load from file.");
|
||||||
|
$self->{hash} = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub load {
|
||||||
|
my ($self, $filename) = @_;
|
||||||
|
$filename = $self->{filename} if not defined $filename;
|
||||||
|
|
||||||
|
if (not defined $filename) {
|
||||||
|
Carp::carp "No $self->{name} filename specified -- skipping loading from file";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $contents = do {
|
||||||
|
local $/;
|
||||||
|
<FILE>;
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->{hash} = decode_json $contents if length $contents;
|
||||||
|
close FILE;
|
||||||
|
|
||||||
|
# update existing entries to use _name to preserve case
|
||||||
|
# and lowercase any non-lowercased entries
|
||||||
|
foreach my $primary_index (keys %{$self->{hash}}) {
|
||||||
|
if (not exists $self->{hash}->{$primary_index}->{_name}) {
|
||||||
|
if ($primary_index ne lc $primary_index) {
|
||||||
|
if (exists $self->{hash}->{lc $primary_index}) {
|
||||||
|
Carp::croak "Cannot update $self->{name} primary index $primary_index; duplicate object found";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $data = delete $self->{hash}->{$primary_index};
|
||||||
|
$data->{_name} = $primary_index;
|
||||||
|
$primary_index = lc $primary_index;
|
||||||
|
$self->{hash}->{$primary_index} = $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach my $secondary_index (grep { $_ ne '_name' } keys %{$self->{hash}->{$primary_index}}) {
|
||||||
|
if (not exists $self->{hash}->{$primary_index}->{$secondary_index}->{_name}) {
|
||||||
|
if ($secondary_index ne lc $secondary_index) {
|
||||||
|
if (exists $self->{hash}->{$primary_index}->{lc $secondary_index}) {
|
||||||
|
Carp::croak "Cannot update $self->{name} $primary_index sub-object $secondary_index; duplicate object found";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $data = delete $self->{hash}->{$primary_index}->{$secondary_index};
|
||||||
|
$data->{_name} = $secondary_index;
|
||||||
|
$secondary_index = lc $secondary_index;
|
||||||
|
$self->{hash}->{$primary_index}->{$secondary_index} = $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub save {
|
||||||
|
my $self = shift;
|
||||||
|
my $filename;
|
||||||
|
if (@_) { $filename = shift; }
|
||||||
|
else { $filename = $self->{filename}; }
|
||||||
|
|
||||||
|
if (not defined $filename) {
|
||||||
|
Carp::carp "No $self->{name} filename specified -- skipping saving to file.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$self->{pbot}->{logger}->log("Saving $self->{name} to $filename\n");
|
||||||
|
|
||||||
|
if (not $self->get_data('$metadata$', '$metadata$', 'update_version')) {
|
||||||
|
$self->add('$metadata$', '$metadata$', { update_version => 3512 });
|
||||||
|
}
|
||||||
|
|
||||||
|
my $json = JSON->new;
|
||||||
|
my $json_text = $json->pretty->canonical->utf8->encode($self->{hash});
|
||||||
|
|
||||||
|
open(FILE, "> $filename") or die "Couldn't open $filename: $!\n";
|
||||||
|
print FILE "$json_text\n";
|
||||||
|
close FILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub clear {
|
||||||
|
my $self = shift;
|
||||||
|
$self->{hash} = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub levenshtein_matches {
|
||||||
|
my ($self, $primary_index, $secondary_index, $distance, $strictnamespace) = @_;
|
||||||
|
my $comma = '';
|
||||||
|
my $result = "";
|
||||||
|
|
||||||
|
$distance = 0.60 if not defined $distance;
|
||||||
|
|
||||||
|
$primary_index = '.*' if not defined $primary_index;
|
||||||
|
|
||||||
|
if (not $secondary_index) {
|
||||||
|
foreach my $index (sort keys %{$self->{hash}}) {
|
||||||
|
my $distance_result = fastdistance($primary_index, $index);
|
||||||
|
my $length = (length $primary_index > length $index) ? length $primary_index : length $index;
|
||||||
|
|
||||||
|
if ($distance_result / $length < $distance) {
|
||||||
|
my $name = $self->get_key_name($index);
|
||||||
|
if ($name =~ / /) { $result .= $comma . "\"$name\""; }
|
||||||
|
else { $result .= $comma . $name; }
|
||||||
|
$comma = ", ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
my $lc_primary_index = lc $primary_index;
|
||||||
|
if (not exists $self->{hash}->{$lc_primary_index}) { return 'none'; }
|
||||||
|
|
||||||
|
my $last_header = "";
|
||||||
|
my $header = "";
|
||||||
|
|
||||||
|
foreach my $index1 (sort keys %{$self->{hash}}) {
|
||||||
|
$header = "[" . $self->get_key_name($index1) . "] ";
|
||||||
|
$header = '[global] ' if $header eq '[.*] ';
|
||||||
|
|
||||||
|
if ($strictnamespace) {
|
||||||
|
next unless $index1 eq '.*' or $index1 eq $lc_primary_index;
|
||||||
|
$header = "" unless $header eq '[global] ';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach my $index2 (sort keys %{$self->{hash}->{$index1}}) {
|
||||||
|
my $distance_result = fastdistance($secondary_index, $index2);
|
||||||
|
my $length = (length $secondary_index > length $index2) ? length $secondary_index : length $index2;
|
||||||
|
|
||||||
|
if ($distance_result / $length < $distance) {
|
||||||
|
my $name = $self->get_key_name($index1, $index2);
|
||||||
|
$header = "" if $last_header eq $header;
|
||||||
|
$last_header = $header;
|
||||||
|
$comma = '; ' if $comma ne '' and $header ne '';
|
||||||
|
if ($name =~ / /) { $result .= $comma . $header . "\"$name\""; }
|
||||||
|
else { $result .= $comma . $header . $name; }
|
||||||
|
$comma = ", ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result =~ s/(.*), /$1 or /;
|
||||||
|
$result = 'none' if $comma eq '';
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub set {
|
||||||
|
my ($self, $primary_index, $secondary_index, $key, $value, $dont_save) = @_;
|
||||||
|
my $lc_primary_index = lc $primary_index;
|
||||||
|
my $lc_secondary_index = lc $secondary_index;
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
||||||
|
my $result = "$self->{name}: $primary_index not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($primary_index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}) {
|
||||||
|
my $secondary_text = $secondary_index =~ / / ? "\"$secondary_index\"" : $secondary_index;
|
||||||
|
my $result = "$self->{name}: [" . $self->get_key_name($lc_primary_index) . "] $secondary_text not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($primary_index, $secondary_index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $name1 = $self->get_key_name($lc_primary_index);
|
||||||
|
my $name2 = $self->get_key_name($lc_primary_index, $lc_secondary_index);
|
||||||
|
|
||||||
|
$name1 = 'global' if $name1 eq '.*';
|
||||||
|
$name2 = "\"$name2\"" if $name2 =~ / /;
|
||||||
|
|
||||||
|
if (not defined $key) {
|
||||||
|
my $result = "[$name1] $name2 keys:\n";
|
||||||
|
my $comma = '';
|
||||||
|
foreach my $key (sort keys %{$self->{hash}->{$lc_primary_index}->{$lc_secondary_index}}) {
|
||||||
|
next if $key eq '_name';
|
||||||
|
$result .= $comma . "$key => " . $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key};
|
||||||
|
$comma = ";\n";
|
||||||
|
}
|
||||||
|
$result .= "none" if ($comma eq '');
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (not defined $value) { $value = $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key}; }
|
||||||
|
else {
|
||||||
|
$self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key} = $value;
|
||||||
|
$self->save unless $dont_save;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "[$name1] $name2: $key " . (defined $value ? "set to $value" : "is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
sub unset {
|
||||||
|
my ($self, $primary_index, $secondary_index, $key) = @_;
|
||||||
|
my $lc_primary_index = lc $primary_index;
|
||||||
|
my $lc_secondary_index = lc $secondary_index;
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
||||||
|
my $result = "$self->{name}: $primary_index not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($primary_index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $name1 = $self->get_key_name($lc_primary_index);
|
||||||
|
$name1 = 'global' if $name1 eq '.*';
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}) {
|
||||||
|
my $result = "$self->{name}: [$name1] $secondary_index not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($primary_index, $secondary_index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $name2 = $self->get_key_name($lc_primary_index, $lc_secondary_index);
|
||||||
|
$name2 = "\"$name2\"" if $name2 =~ / /;
|
||||||
|
|
||||||
|
if (defined delete $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key}) {
|
||||||
|
$self->save;
|
||||||
|
return "$self->{name}: [$name1] $name2: $key unset.";
|
||||||
|
} else {
|
||||||
|
return "$self->{name}: [$name1] $name2: $key does not exist.";
|
||||||
|
}
|
||||||
|
$self->save;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub exists {
|
||||||
|
my ($self, $primary_index, $secondary_index, $data_index) = @_;
|
||||||
|
return 0 if not defined $primary_index;
|
||||||
|
$primary_index = lc $primary_index;
|
||||||
|
return 0 if not exists $self->{hash}->{$primary_index};
|
||||||
|
return 1 if not defined $secondary_index;
|
||||||
|
$secondary_index = lc $secondary_index;
|
||||||
|
return 0 if not exists $self->{hash}->{$primary_index}->{$secondary_index};
|
||||||
|
return 1 if not defined $data_index;
|
||||||
|
return exists $self->{hash}->{$primary_index}->{$secondary_index}->{$data_index};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_key_name {
|
||||||
|
my ($self, $primary_index, $secondary_index) = @_;
|
||||||
|
|
||||||
|
my $lc_primary_index = lc $primary_index;
|
||||||
|
|
||||||
|
return $lc_primary_index if not exists $self->{hash}->{$lc_primary_index};
|
||||||
|
|
||||||
|
if (not defined $secondary_index) {
|
||||||
|
if (exists $self->{hash}->{$lc_primary_index}->{_name}) {
|
||||||
|
return $self->{hash}->{$lc_primary_index}->{_name};
|
||||||
|
} else {
|
||||||
|
return $lc_primary_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $lc_secondary_index = lc $secondary_index;
|
||||||
|
|
||||||
|
return $lc_secondary_index if not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index};
|
||||||
|
|
||||||
|
if (exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{_name}) {
|
||||||
|
return $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{_name};
|
||||||
|
} else {
|
||||||
|
return $lc_secondary_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_keys {
|
||||||
|
my ($self, $primary_index, $secondary_index) = @_;
|
||||||
|
return grep { $_ ne '$metadata$' } keys %{$self->{hash}} if not defined $primary_index;
|
||||||
|
|
||||||
|
my $lc_primary_index = lc $primary_index;
|
||||||
|
|
||||||
|
if (not defined $secondary_index) {
|
||||||
|
return () if not exists $self->{hash}->{$lc_primary_index};
|
||||||
|
return grep { $_ ne '_name' and $_ ne '$metadata$' } keys %{$self->{hash}->{$lc_primary_index}};
|
||||||
|
}
|
||||||
|
|
||||||
|
my $lc_secondary_index = lc $secondary_index;
|
||||||
|
|
||||||
|
return () if not exists $self->{hash}->{$lc_primary_index}
|
||||||
|
or not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index};
|
||||||
|
|
||||||
|
return grep { $_ ne '_name' } keys %{$self->{hash}->{lc $primary_index}->{lc $secondary_index}};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_data {
|
||||||
|
my ($self, $primary_index, $secondary_index, $data_index) = @_;
|
||||||
|
$primary_index = lc $primary_index if defined $primary_index;
|
||||||
|
$secondary_index = lc $secondary_index if defined $secondary_index;
|
||||||
|
return undef if not exists $self->{hash}->{$primary_index};
|
||||||
|
return $self->{hash}->{$primary_index} if not defined $secondary_index;
|
||||||
|
return $self->{hash}->{$primary_index}->{$secondary_index} if not defined $data_index;
|
||||||
|
return $self->{hash}->{$primary_index}->{$secondary_index}->{$data_index};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub add {
|
||||||
|
my ($self, $primary_index, $secondary_index, $data, $dont_save, $quiet) = @_;
|
||||||
|
my $lc_primary_index = lc $primary_index;
|
||||||
|
my $lc_secondary_index = lc $secondary_index;
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
||||||
|
# preserve case
|
||||||
|
if ($primary_index ne $lc_primary_index) {
|
||||||
|
$self->{hash}->{$lc_primary_index}->{_name} = $primary_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($secondary_index ne $lc_secondary_index) {
|
||||||
|
# preserve case
|
||||||
|
$data->{_name} = $secondary_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
$self->{hash}->{$lc_primary_index}->{$lc_secondary_index} = $data;
|
||||||
|
$self->save() unless $dont_save;
|
||||||
|
|
||||||
|
my $name1 = $self->get_key_name($lc_primary_index);
|
||||||
|
my $name2 = $self->get_key_name($lc_primary_index, $lc_secondary_index);
|
||||||
|
$name1 = 'global' if $name1 eq '.*';
|
||||||
|
$name2 = "\"$name2\"" if $name2 =~ / /;
|
||||||
|
$self->{pbot}->{logger}->log("$self->{name}: [$name1]: $name2 added.\n") unless $dont_save or $quiet;
|
||||||
|
return "$self->{name}: [$name1]: $name2 added.";
|
||||||
|
}
|
||||||
|
|
||||||
|
sub remove {
|
||||||
|
my ($self, $primary_index, $secondary_index, $data_index, $dont_save) = @_;
|
||||||
|
my $lc_primary_index = lc $primary_index;
|
||||||
|
my $lc_secondary_index = lc $secondary_index;
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
||||||
|
my $result = "$self->{name}: $primary_index not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($primary_index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (not defined $secondary_index) {
|
||||||
|
my $data = delete $self->{hash}->{$lc_primary_index};
|
||||||
|
if (defined $data) {
|
||||||
|
my $name = exists $data->{_name} ? $data->{_name} : $lc_primary_index;
|
||||||
|
$name = 'global' if $name eq '.*';
|
||||||
|
$self->save unless $dont_save;
|
||||||
|
return "$self->{name}: $name removed.";
|
||||||
|
} else {
|
||||||
|
return "$self->{name}: $primary_index does not exist.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $name1 = $self->get_key_name($lc_primary_index);
|
||||||
|
$name1 = 'global' if $name1 eq '.*';
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}) {
|
||||||
|
my $result = "$self->{name}: [$name1] $secondary_index not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($primary_index, $secondary_index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (not defined $data_index) {
|
||||||
|
my $data = delete $self->{hash}->{$lc_primary_index}->{$lc_secondary_index};
|
||||||
|
if (defined $data) {
|
||||||
|
my $name2 = exists $data->{_name} ? $data->{_name} : $lc_secondary_index;
|
||||||
|
$name2 = "\"$name2\"" if $name2 =~ / /;
|
||||||
|
|
||||||
|
# remove primary group if no more secondaries
|
||||||
|
if (grep { $_ ne '_name' } keys %{$self->{hash}->{$lc_primary_index}} == 0) {
|
||||||
|
delete $self->{hash}->{$lc_primary_index};
|
||||||
|
}
|
||||||
|
|
||||||
|
$self->save unless $dont_save;
|
||||||
|
return "$self->{name}: [$name1] $name2 removed.";
|
||||||
|
} else {
|
||||||
|
return "$self->{name}: [$name1] $secondary_index does not exist.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $name2 = $self->get_key_name($lc_primary_index, $lc_secondary_index);
|
||||||
|
if (defined delete $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$data_index}) {
|
||||||
|
return "$self->{name}: [$name1] $name2.$data_index removed.";
|
||||||
|
} else {
|
||||||
|
return "$self->{name}: [$name1] $name2.$data_index does not exist.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# for compatibility with DualIndexSQLiteObject
|
||||||
|
sub create_metadata { }
|
||||||
|
|
||||||
|
# todo:
|
||||||
|
sub get_each { }
|
||||||
|
sub get_next { }
|
||||||
|
sub get_all { }
|
||||||
|
|
||||||
|
1;
|
251
updates/lib3512/HashObject.pm
Normal file
251
updates/lib3512/HashObject.pm
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
# File: HashObject.pm
|
||||||
|
# Author: pragma_
|
||||||
|
#
|
||||||
|
# Purpose: Provides a hash-table object with an abstracted API that includes
|
||||||
|
# setting and deleting values, saving to and loading from files, etc. Provides
|
||||||
|
# case-insensitive access to the index key while preserving original case when
|
||||||
|
# displaying index key.
|
||||||
|
|
||||||
|
# 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 lib3512::HashObject;
|
||||||
|
|
||||||
|
use warnings; use strict;
|
||||||
|
use feature 'unicode_strings';
|
||||||
|
|
||||||
|
use Text::Levenshtein qw(fastdistance);
|
||||||
|
use JSON;
|
||||||
|
|
||||||
|
sub new {
|
||||||
|
my ($proto, %conf) = @_;
|
||||||
|
my $class = ref($proto) || $proto;
|
||||||
|
my $self = bless {}, $class;
|
||||||
|
Carp::croak("Missing pbot reference to " . __FILE__) unless exists $conf{pbot};
|
||||||
|
$self->{pbot} = $conf{pbot};
|
||||||
|
$self->initialize(%conf);
|
||||||
|
return $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub initialize {
|
||||||
|
my ($self, %conf) = @_;
|
||||||
|
$self->{name} = $conf{name} // 'hash object';
|
||||||
|
$self->{filename} = $conf{filename} // Carp::carp("Missing filename to HashObject, will not be able to save to or load from file.");
|
||||||
|
$self->{hash} = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub load {
|
||||||
|
my $self = shift;
|
||||||
|
my $filename;
|
||||||
|
if (@_) { $filename = shift; }
|
||||||
|
else { $filename = $self->{filename}; }
|
||||||
|
|
||||||
|
$self->clear;
|
||||||
|
|
||||||
|
if (not defined $filename) {
|
||||||
|
Carp::carp "No $self->{name} filename specified -- skipping loading from file";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $contents = do {
|
||||||
|
local $/;
|
||||||
|
<FILE>;
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->{hash} = decode_json $contents;
|
||||||
|
close FILE;
|
||||||
|
|
||||||
|
# update existing entries to use _name to preserve case
|
||||||
|
# and lowercase any non-lowercased entries
|
||||||
|
foreach my $index (keys %{$self->{hash}}) {
|
||||||
|
if (not exists $self->{hash}->{$index}->{_name}) {
|
||||||
|
if ($index ne lc $index) {
|
||||||
|
if (exists $self->{hash}->{lc $index}) {
|
||||||
|
Carp::croak "Cannot update $self->{name} object $index; duplicate object found";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $data = delete $self->{hash}->{$index};
|
||||||
|
$data->{_name} = $index;
|
||||||
|
$self->{hash}->{lc $index} = $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub save {
|
||||||
|
my $self = shift;
|
||||||
|
my $filename;
|
||||||
|
if (@_) { $filename = shift; }
|
||||||
|
else { $filename = $self->{filename}; }
|
||||||
|
|
||||||
|
if (not defined $filename) {
|
||||||
|
Carp::carp "No $self->{name} filename specified -- skipping saving to file.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$self->{pbot}->{logger}->log("Saving $self->{name} to $filename\n");
|
||||||
|
|
||||||
|
if (not $self->get_data('$metadata$', 'update_version')) {
|
||||||
|
$self->add('$metadata$', { update_version => 3512 });
|
||||||
|
}
|
||||||
|
|
||||||
|
my $json = JSON->new;
|
||||||
|
my $json_text = $json->pretty->canonical->utf8->encode($self->{hash});
|
||||||
|
|
||||||
|
open(FILE, "> $filename") or die "Couldn't open $filename: $!\n";
|
||||||
|
print FILE "$json_text\n";
|
||||||
|
close(FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub clear {
|
||||||
|
my $self = shift;
|
||||||
|
$self->{hash} = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub levenshtein_matches {
|
||||||
|
my ($self, $keyword) = @_;
|
||||||
|
my $comma = '';
|
||||||
|
my $result = "";
|
||||||
|
|
||||||
|
foreach my $index (sort keys %{$self->{hash}}) {
|
||||||
|
my $distance = fastdistance($keyword, $index);
|
||||||
|
my $length = (length $keyword > length $index) ? length $keyword : length $index;
|
||||||
|
|
||||||
|
if ($length != 0 && $distance / $length < 0.50) {
|
||||||
|
$result .= $comma . $index;
|
||||||
|
$comma = ", ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result =~ s/(.*), /$1 or /;
|
||||||
|
$result = "none" if $comma eq '';
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub set {
|
||||||
|
my ($self, $index, $key, $value, $dont_save) = @_;
|
||||||
|
my $lc_index = lc $index;
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_index}) {
|
||||||
|
my $result = "$self->{name}: $index not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (not defined $key) {
|
||||||
|
my $result = "[$self->{name}] " . $self->get_key_name($lc_index) . " keys: ";
|
||||||
|
my $comma = '';
|
||||||
|
foreach my $k (sort grep { $_ ne '_name' } keys %{$self->{hash}->{$lc_index}}) {
|
||||||
|
$result .= $comma . "$k => " . $self->{hash}->{$lc_index}->{$k};
|
||||||
|
$comma = "; ";
|
||||||
|
}
|
||||||
|
$result .= "none" if ($comma eq '');
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (not defined $value) { $value = $self->{hash}->{$lc_index}->{$key}; }
|
||||||
|
else {
|
||||||
|
$self->{hash}->{$lc_index}->{$key} = $value;
|
||||||
|
$self->save unless $dont_save;
|
||||||
|
}
|
||||||
|
return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key " . (defined $value ? "set to $value" : "is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
sub unset {
|
||||||
|
my ($self, $index, $key) = @_;
|
||||||
|
my $lc_index = lc $index;
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_index}) {
|
||||||
|
my $result = "$self->{name}: $index not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defined delete $self->{hash}->{$lc_index}->{$key}) {
|
||||||
|
$self->save;
|
||||||
|
return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key unset.";
|
||||||
|
} else {
|
||||||
|
return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key does not exist.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub exists {
|
||||||
|
my ($self, $index, $data_index) = @_;
|
||||||
|
return exists $self->{hash}->{lc $index} if not defined $data_index;
|
||||||
|
return exists $self->{hash}->{lc $index}->{$data_index};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_key_name {
|
||||||
|
my ($self, $index) = @_;
|
||||||
|
my $lc_index = lc $index;
|
||||||
|
return $lc_index if not exists $self->{hash}->{$lc_index};
|
||||||
|
return exists $self->{hash}->{$lc_index}->{_name} ? $self->{hash}->{$lc_index}->{_name} : $lc_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_keys {
|
||||||
|
my ($self, $index) = @_;
|
||||||
|
return grep { $_ ne '$metadata$' } keys %{$self->{hash}} if not defined $index;
|
||||||
|
return grep { $_ ne '_name' } keys %{$self->{hash}->{lc $index}};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_data {
|
||||||
|
my ($self, $index, $data_index) = @_;
|
||||||
|
my $lc_index = lc $index;
|
||||||
|
return undef if not exists $self->{hash}->{$lc_index};
|
||||||
|
return $self->{hash}->{$lc_index} if not defined $data_index;
|
||||||
|
return $self->{hash}->{$lc_index}->{$data_index};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub add {
|
||||||
|
my ($self, $index, $data, $dont_save) = @_;
|
||||||
|
my $lc_index = lc $index;
|
||||||
|
|
||||||
|
# preserve case of index
|
||||||
|
if ($index ne $lc_index) {
|
||||||
|
$data->{_name} = $index;
|
||||||
|
}
|
||||||
|
|
||||||
|
$self->{hash}->{$lc_index} = $data;
|
||||||
|
$self->save unless $dont_save;
|
||||||
|
return "$index added to $self->{name}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
sub remove {
|
||||||
|
my ($self, $index, $data_index, $dont_save) = @_;
|
||||||
|
my $lc_index = lc $index;
|
||||||
|
|
||||||
|
if (not exists $self->{hash}->{$lc_index}) {
|
||||||
|
my $result = "$self->{name}: $index not found; similiar matches: ";
|
||||||
|
$result .= $self->levenshtein_matches($lc_index);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defined $data_index) {
|
||||||
|
if (defined delete $self->{hash}->{$lc_index}->{$data_index}) {
|
||||||
|
delete $self->{hash}->{$lc_index} if keys(%{$self->{hash}->{$lc_index}}) == 1;
|
||||||
|
$self->save unless $dont_save;
|
||||||
|
return $self->get_key_name($lc_index) . ".$data_index removed from $self->{name}";
|
||||||
|
} else {
|
||||||
|
return "$self->{name}: " . $self->get_key_name($lc_index) . ".$data_index does not exist.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $data = delete $self->{hash}->{$lc_index};
|
||||||
|
if (defined $data) {
|
||||||
|
$self->save unless $dont_save;
|
||||||
|
my $name = exists $data->{_name} ? $data->{_name} : $lc_index;
|
||||||
|
return "$name removed from $self->{name}.";
|
||||||
|
} else {
|
||||||
|
return "$self->{name}: $data_index does not exist.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
Loading…
Reference in New Issue
Block a user