3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-11-26 22:09:26 +01:00

Add nick similarity completion

Previously, the bot wouldn't address people by nick if the provided nick
argument doesn't exist in the channel.

Now, the bot's nick list is searched for a nick with a certain similarity
percentage in the order of most recently spoken nicks first. This allows
the bot to address nicks when somebody may have forgotten to add a trailing
underscore/punctuation or may have typoed the nick.

The similarity percentage can be set via the interpreter->nick_similiarty
registry key.  A value of 0 should disable the behavior.
This commit is contained in:
Pragmatic Software 2016-09-25 00:03:37 -07:00
parent 6369a8df99
commit f34854fcec
3 changed files with 58 additions and 3 deletions

View File

@ -615,7 +615,7 @@ sub interpreter {
$arguments = ""; $arguments = "";
} else { } else {
if ($self->{factoids}->hash->{$channel}->{$keyword}->{type} eq 'text') { if ($self->{factoids}->hash->{$channel}->{$keyword}->{type} eq 'text') {
my $target = $self->{pbot}->{nicklist}->is_present($from, $arguments); my $target = $self->{pbot}->{nicklist}->is_present_similar($from, $arguments);
if ($target and $action !~ /\$nick/) { if ($target and $action !~ /\$nick/) {
if ($action !~ m/^(\/[^ ]+) /) { if ($action !~ m/^(\/[^ ]+) /) {
$action =~ s/^/\/say $target: $keyword is / unless defined $tonick; $action =~ s/^/\/say $target: $keyword is / unless defined $tonick;

View File

@ -99,7 +99,7 @@ sub process_line {
if ($cmd_text =~ s/\B$bot_trigger`([^`]+)// || $cmd_text =~ s/\B$bot_trigger\{([^}]+)//) { if ($cmd_text =~ s/\B$bot_trigger`([^`]+)// || $cmd_text =~ s/\B$bot_trigger\{([^}]+)//) {
my $cmd = $1; my $cmd = $1;
my ($nick) = $cmd_text =~ m/^([^ ,:;]+)/; my ($nick) = $cmd_text =~ m/^([^ ,:;]+)/;
$nick = $self->{pbot}->{nicklist}->is_present($from, $nick); $nick = $self->{pbot}->{nicklist}->is_present_similar($from, $nick);
if ($nick) { if ($nick) {
$command = "tell $nick about $cmd"; $command = "tell $nick about $cmd";
} else { } else {
@ -114,6 +114,8 @@ sub process_line {
$nick_override = $1; $nick_override = $1;
$has_code = $2 if length $2 and $nick_override !~ /^(?:enum|struct|union)$/; $has_code = $2 if length $2 and $nick_override !~ /^(?:enum|struct|union)$/;
$preserve_whitespace = 1; $preserve_whitespace = 1;
my $similar = $self->{pbot}->{nicklist}->is_present_similar($from, $nick_override);
$nick_override = $similar if $similar;
$processed += 100; $processed += 100;
} elsif($cmd_text =~ s/^$bot_trigger(.*)$//) { } elsif($cmd_text =~ s/^$bot_trigger(.*)$//) {
$command = $1; $command = $1;
@ -172,8 +174,12 @@ sub interpret {
if($command =~ /^tell\s+(.{1,20})\s+about\s+(.*?)\s+(.*)$/i) if($command =~ /^tell\s+(.{1,20})\s+about\s+(.*?)\s+(.*)$/i)
{ {
($keyword, $arguments, $tonick) = ($2, $3, $1); ($keyword, $arguments, $tonick) = ($2, $3, $1);
my $similar = $self->{pbot}->{nicklist}->is_present_similar($from, $tonick);
$tonick = $similar if $similar;
} elsif($command =~ /^tell\s+(.{1,20})\s+about\s+(.*)$/i) { } elsif($command =~ /^tell\s+(.{1,20})\s+about\s+(.*)$/i) {
($keyword, $tonick) = ($2, $1); ($keyword, $tonick) = ($2, $1);
my $similar = $self->{pbot}->{nicklist}->is_present_similar($from, $tonick);
$tonick = $similar if $similar;
} elsif($command =~ /^(.*?)\s+(.*)$/) { } elsif($command =~ /^(.*?)\s+(.*)$/) {
($keyword, $arguments) = ($1, $2); ($keyword, $arguments) = ($1, $2);
} else { } else {

View File

@ -10,8 +10,10 @@ package PBot::NickList;
use warnings; use warnings;
use strict; use strict;
use Text::Levenshtein qw/fastdistance/;
use Data::Dumper; use Data::Dumper;
use Carp (); use Carp ();
use Time::HiRes qw/gettimeofday/;
sub new { sub new {
Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref $_[1] eq 'HASH'; Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref $_[1] eq 'HASH';
@ -37,6 +39,8 @@ sub initialize {
$self->{pbot}->{event_dispatcher}->register_handler('irc.quit', sub { $self->on_quit(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.quit', sub { $self->on_quit(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.kick', sub { $self->on_kick(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.kick', sub { $self->on_kick(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.nick', sub { $self->on_nickchange(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.nick', sub { $self->on_nickchange(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.public', sub { $self->on_activity(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.caction', sub { $self->on_activity(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.whospcrpl', sub { $self->on_whospcrpl(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.whospcrpl', sub { $self->on_whospcrpl(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.endofwho', sub { $self->on_endofwho(@_) }); $self->{pbot}->{event_dispatcher}->register_handler('irc.endofwho', sub { $self->on_endofwho(@_) });
@ -53,6 +57,16 @@ sub dumpnicks {
return $nicklist; return $nicklist;
} }
sub update_timestamp {
my ($self, $channel, $nick) = @_;
$channel = lc $channel;
$nick = lc $nick;
if (exists $self->{nicklist}->{$channel} and exists $self->{nicklist}->{$channel}->{$nick}) {
$self->{nicklist}->{$channel}->{$nick}->{timestamp} = gettimeofday;
}
}
sub remove_channel { sub remove_channel {
my ($self, $channel) = @_; my ($self, $channel) = @_;
delete $self->{nicklist}->{lc $channel}; delete $self->{nicklist}->{lc $channel};
@ -61,7 +75,7 @@ sub remove_channel {
sub add_nick { sub add_nick {
my ($self, $channel, $nick) = @_; my ($self, $channel, $nick) = @_;
$self->{pbot}->{logger}->log("Adding nick '$nick' to channel '$channel'\n") if $self->{pbot}->{registry}->get_value('nicklist', 'debug'); $self->{pbot}->{logger}->log("Adding nick '$nick' to channel '$channel'\n") if $self->{pbot}->{registry}->get_value('nicklist', 'debug');
$self->{nicklist}->{lc $channel}->{lc $nick} = { nick => $nick }; $self->{nicklist}->{lc $channel}->{lc $nick} = { nick => $nick, timestamp => 0 };
} }
sub remove_nick { sub remove_nick {
@ -98,6 +112,35 @@ sub is_present {
} }
} }
sub is_present_similar {
my ($self, $channel, $nick) = @_;
$channel = lc $channel;
$nick = lc $nick;
return 0 if not exists $self->{nicklist}->{$channel};
return $nick if $self->is_present($channel, $nick);
my $percentage = $self->{pbot}->{registry}->get_value('interpreter', 'nick_similarity');
$percentage = 0.20 if not defined $percentage;
foreach my $person (sort { $self->{nicklist}->{$channel}->{$b}->{timestamp} <=> $self->{nicklist}->{$channel}->{$a}->{timestamp} } keys $self->{nicklist}->{$channel}) {
my $distance = fastdistance($nick, $person);
my $length = length $nick > length $person ? length $nick : length $person;
=cut
my $p = $length != 0 ? $distance / $length : 0;
$self->{pbot}->{logger}->log("[$percentage] $nick <-> $person: $p %\n");
=cut
if ($length != 0 && $distance / $length <= $percentage) {
return $self->{nicklist}->{$channel}->{$person}->{nick};
}
}
return 0;
}
sub random_nick { sub random_nick {
my ($self, $channel) = @_; my ($self, $channel) = @_;
@ -124,6 +167,12 @@ sub on_namreply {
return 0; return 0;
} }
sub on_activity {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->{to}[0]);
$self->update_timestamp($channel, $nick);
}
sub on_join { sub on_join {
my ($self, $event_type, $event) = @_; my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to); my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to);