2010-06-20 08:15:10 +02:00
|
|
|
# File: DualIndexHashObject.pm
|
|
|
|
# Author: pragma_
|
|
|
|
#
|
2019-06-26 18:34:19 +02:00
|
|
|
# Purpose: Provides a hash-table object with an abstracted API that includes
|
2020-01-15 03:10:53 +01:00
|
|
|
# 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.
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2017-03-05 22:33:31 +01:00
|
|
|
# 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/.
|
|
|
|
|
2010-06-20 08:15:10 +02:00
|
|
|
package PBot::DualIndexHashObject;
|
2020-02-08 20:04:13 +01:00
|
|
|
use parent 'PBot::Class';
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2020-02-08 20:04:13 +01:00
|
|
|
use warnings; use strict;
|
2019-07-11 03:40:53 +02:00
|
|
|
use feature 'unicode_strings';
|
|
|
|
|
2010-06-20 08:15:10 +02:00
|
|
|
use Text::Levenshtein qw(fastdistance);
|
2019-06-28 09:22:57 +02:00
|
|
|
use JSON;
|
2010-06-20 08:15:10 +02:00
|
|
|
|
|
|
|
sub initialize {
|
|
|
|
my ($self, %conf) = @_;
|
2020-02-06 10:14:08 +01:00
|
|
|
$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} = {};
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub load {
|
2014-05-17 22:08:19 +02:00
|
|
|
my ($self, $filename) = @_;
|
2020-01-15 03:10:53 +01:00
|
|
|
$filename = $self->{filename} if not defined $filename;
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2018-08-09 19:55:53 +02:00
|
|
|
if (not defined $filename) {
|
2010-06-20 08:15:10 +02:00
|
|
|
Carp::carp "No $self->{name} filename specified -- skipping loading from file";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-06-28 09:22:57 +02:00
|
|
|
$self->{pbot}->{logger}->log("Loading $self->{name} from $filename ...\n");
|
|
|
|
|
2018-08-09 19:55:53 +02:00
|
|
|
if (not open(FILE, "< $filename")) {
|
2010-06-20 08:15:10 +02:00
|
|
|
Carp::carp "Skipping loading from file: Couldn't open $filename: $!\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-06-28 09:22:57 +02:00
|
|
|
my $contents = do {
|
|
|
|
local $/;
|
|
|
|
<FILE>;
|
|
|
|
};
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2019-12-22 03:59:17 +01:00
|
|
|
$self->{hash} = decode_json $contents if length $contents;
|
2019-06-28 09:22:57 +02:00
|
|
|
close FILE;
|
2020-01-15 03:10:53 +01:00
|
|
|
|
|
|
|
# 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 (lc $primary_index eq $primary_index) {
|
|
|
|
$self->{hash}->{$primary_index}->{_name} = $primary_index;
|
|
|
|
} else {
|
|
|
|
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 (keys %{ $self->{hash}->{$primary_index} }) {
|
|
|
|
next if $secondary_index eq '_name';
|
|
|
|
if (not exists $self->{hash}->{$primary_index}->{$secondary_index}->{_name}) {
|
|
|
|
if (lc $secondary_index eq $secondary_index) {
|
|
|
|
$self->{hash}->{$primary_index}->{$secondary_index}->{_name} = $secondary_index;
|
|
|
|
} else {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub save {
|
|
|
|
my $self = shift;
|
|
|
|
my $filename;
|
2020-01-15 03:10:53 +01:00
|
|
|
if (@_) { $filename = shift; } else { $filename = $self->{filename}; }
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2018-08-09 19:55:53 +02:00
|
|
|
if (not defined $filename) {
|
2010-06-20 08:15:10 +02:00
|
|
|
Carp::carp "No $self->{name} filename specified -- skipping saving to file.\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-06-28 09:22:57 +02:00
|
|
|
$self->{pbot}->{logger}->log("Saving $self->{name} to $filename\n");
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2019-06-28 09:22:57 +02:00
|
|
|
my $json = JSON->new;
|
2019-07-01 02:00:03 +02:00
|
|
|
my $json_text = $json->pretty->canonical->utf8->encode($self->{hash});
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2019-06-28 09:22:57 +02:00
|
|
|
open(FILE, "> $filename") or die "Couldn't open $filename: $!\n";
|
|
|
|
print FILE "$json_text\n";
|
2010-06-20 08:15:10 +02:00
|
|
|
close FILE;
|
|
|
|
}
|
|
|
|
|
2017-08-06 05:15:15 +02:00
|
|
|
sub clear {
|
|
|
|
my $self = shift;
|
|
|
|
$self->{hash} = {};
|
|
|
|
}
|
|
|
|
|
2010-06-20 08:15:10 +02:00
|
|
|
sub levenshtein_matches {
|
2020-01-15 03:10:53 +01:00
|
|
|
my ($self, $primary_index, $secondary_index, $distance, $strictnamespace) = @_;
|
2010-06-20 08:15:10 +02:00
|
|
|
my $comma = '';
|
|
|
|
my $result = "";
|
|
|
|
|
2011-01-29 02:21:17 +01:00
|
|
|
$distance = 0.60 if not defined $distance;
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
$primary_index = '.*' if not defined $primary_index;
|
2019-06-26 18:34:19 +02:00
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
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;
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2018-08-09 19:55:53 +02:00
|
|
|
if ($distance_result / $length < $distance) {
|
2020-01-15 03:10:53 +01:00
|
|
|
my $name = $self->{hash}->{$index}->{_name};
|
|
|
|
if ($name =~ / /) {
|
|
|
|
$result .= $comma . "\"$name\"";
|
2018-08-09 02:38:57 +02:00
|
|
|
} else {
|
2020-01-15 03:10:53 +01:00
|
|
|
$result .= $comma . $name;
|
2018-08-09 02:38:57 +02:00
|
|
|
}
|
2010-06-20 08:15:10 +02:00
|
|
|
$comma = ", ";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2020-01-15 03:10:53 +01:00
|
|
|
my $lc_primary_index = lc $primary_index;
|
|
|
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
2010-06-20 08:15:10 +02:00
|
|
|
return 'none';
|
|
|
|
}
|
|
|
|
|
2011-01-29 02:21:17 +01:00
|
|
|
my $last_header = "";
|
|
|
|
my $header = "";
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
foreach my $index1 (sort keys %{ $self->{hash} }) {
|
|
|
|
$header = "[$self->{hash}->{$index1}->{_name}] ";
|
|
|
|
$header = '[global] ' if $header eq '[.*] ';
|
2011-01-29 02:21:17 +01:00
|
|
|
|
2018-05-12 11:52:52 +02:00
|
|
|
if ($strictnamespace) {
|
2020-01-15 03:10:53 +01:00
|
|
|
next unless $index1 eq '.*' or $index1 eq $lc_primary_index;
|
|
|
|
$header = "" unless $header eq '[global] ';
|
2018-05-12 11:52:52 +02:00
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
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;
|
2011-01-29 02:21:17 +01:00
|
|
|
|
2018-08-09 19:55:53 +02:00
|
|
|
if ($distance_result / $length < $distance) {
|
2020-01-15 03:10:53 +01:00
|
|
|
my $name = $self->{hash}->{$index1}->{$index2}->{_name};
|
2011-01-29 02:21:17 +01:00
|
|
|
$header = "" if $last_header eq $header;
|
|
|
|
$last_header = $header;
|
2018-05-15 04:21:58 +02:00
|
|
|
$comma = '; ' if $comma ne '' and $header ne '';
|
2020-01-15 03:10:53 +01:00
|
|
|
if ($name =~ / /) {
|
|
|
|
$result .= $comma . $header . "\"$name\"";
|
2018-08-09 02:38:57 +02:00
|
|
|
} else {
|
2020-01-15 03:10:53 +01:00
|
|
|
$result .= $comma . $header . $name;
|
2018-08-09 02:38:57 +02:00
|
|
|
}
|
2011-01-29 02:21:17 +01:00
|
|
|
$comma = ", ";
|
|
|
|
}
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$result =~ s/(.*), /$1 or /;
|
|
|
|
$result = 'none' if $comma eq '';
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub set {
|
2020-01-15 03:10:53 +01:00
|
|
|
my ($self, $primary_index, $secondary_index, $key, $value, $dont_save) = @_;
|
|
|
|
my $lc_primary_index = lc $primary_index;
|
|
|
|
my $lc_secondary_index = lc $secondary_index;
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
|
|
|
my $result = "$self->{name}: $primary_index not found; similiar matches: ";
|
|
|
|
$result .= $self->levenshtein_matches($primary_index);
|
2010-06-20 08:15:10 +02:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
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->{hash}->{$lc_primary_index}->{_name}] $secondary_text not found; similiar matches: ";
|
|
|
|
$result .= $self->levenshtein_matches($primary_index, $secondary_index);
|
2010-06-20 08:15:10 +02:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
my $name1 = $self->{hash}->{$lc_primary_index}->{_name};
|
|
|
|
my $name2 = $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{_name};
|
|
|
|
|
|
|
|
$name1 = 'global' if $name1 eq '.*';
|
|
|
|
$name2 = "\"$name2\"" if $name2 =~ / /;
|
|
|
|
|
2018-08-09 19:55:53 +02:00
|
|
|
if (not defined $key) {
|
2020-01-15 03:10:53 +01:00
|
|
|
my $result = "[$name1] $name2 keys:\n";
|
2010-06-20 08:15:10 +02:00
|
|
|
my $comma = '';
|
2020-01-15 03:10:53 +01:00
|
|
|
foreach my $key (sort keys %{ $self->{hash}->{$lc_primary_index}->{$lc_secondary_index} }) {
|
2020-01-26 05:35:56 +01:00
|
|
|
next if $key eq '_name';
|
2020-01-15 03:10:53 +01:00
|
|
|
$result .= $comma . "$key => " . $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key};
|
2014-05-24 14:01:59 +02:00
|
|
|
$comma = ";\n";
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
2018-08-09 19:55:53 +02:00
|
|
|
$result .= "none" if ($comma eq '');
|
2010-06-20 08:15:10 +02:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2018-08-09 19:55:53 +02:00
|
|
|
if (not defined $value) {
|
2020-01-15 03:10:53 +01:00
|
|
|
$value = $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key};
|
2010-06-20 08:15:10 +02:00
|
|
|
} else {
|
2020-01-15 03:10:53 +01:00
|
|
|
$self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key} = $value;
|
2014-05-17 22:08:19 +02:00
|
|
|
$self->save unless $dont_save;
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
2020-01-19 06:57:54 +01:00
|
|
|
return "[$name1] $name2: $key " . (defined $value ? "set to $value" : "is not set.");
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub unset {
|
2020-01-15 03:10:53 +01:00
|
|
|
my ($self, $primary_index, $secondary_index, $key) = @_;
|
|
|
|
my $lc_primary_index = lc $primary_index;
|
|
|
|
my $lc_secondary_index = lc $secondary_index;
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
|
|
|
my $result = "$self->{name}: $primary_index not found; similiar matches: ";
|
|
|
|
$result .= $self->levenshtein_matches($primary_index);
|
2010-06-20 08:15:10 +02:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
my $name1 = $self->{hash}->{$lc_primary_index}->{_name};
|
|
|
|
$name1 = 'global' if $name1 eq '.*';
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
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);
|
2010-06-20 08:15:10 +02:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
delete $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{$key};
|
2010-06-20 08:15:10 +02:00
|
|
|
$self->save();
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
my $name2 = $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{_name};
|
|
|
|
$name2 = "\"$name2\"" if $name2 =~ / /;
|
|
|
|
|
2020-01-19 06:57:54 +01:00
|
|
|
return "$self->{name}: [$name1] $name2: $key unset.";
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub add {
|
2020-01-15 03:10:53 +01:00
|
|
|
my ($self, $primary_index, $secondary_index, $data, $dont_save) = @_;
|
|
|
|
my $lc_primary_index = lc $primary_index;
|
|
|
|
my $lc_secondary_index = lc $secondary_index;
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
if (exists $self->{hash}->{$lc_primary_index} and exists $self->{$lc_primary_index}->{$lc_secondary_index}) {
|
2020-01-26 00:07:40 +01:00
|
|
|
$self->{pbot}->{logger}->log("Entry $lc_primary_index/$lc_secondary_index already exists.\n");
|
2020-01-15 03:10:53 +01:00
|
|
|
return "Error: entry already exists";
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
|
|
|
$self->{hash}->{$lc_primary_index}->{_name} = $primary_index; # preserve case
|
|
|
|
}
|
|
|
|
|
|
|
|
$data->{_name} = $secondary_index; # preserve case
|
2020-01-26 00:07:40 +01:00
|
|
|
$self->{hash}->{$lc_primary_index}->{$lc_secondary_index} = $data;
|
2020-01-15 03:10:53 +01:00
|
|
|
$self->save() unless $dont_save;
|
|
|
|
|
|
|
|
my $name1 = $self->{hash}->{$lc_primary_index}->{_name};
|
|
|
|
my $name2 = $self->{hash}->{$lc_primary_index}->{$lc_secondary_index}->{_name};
|
|
|
|
$name1 = 'global' if $name1 eq '.*';
|
|
|
|
$name2 = "\"$name2\"" if $name2 =~ / /;
|
2020-01-26 00:07:40 +01:00
|
|
|
$self->{pbot}->{logger}->log("$self->{name}: [$name1]: $name2 added.\n");
|
2020-01-15 03:10:53 +01:00
|
|
|
return "$self->{name}: [$name1]: $name2 added.";
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub remove {
|
2020-01-15 03:10:53 +01:00
|
|
|
my ($self, $primary_index, $secondary_index) = @_;
|
|
|
|
my $lc_primary_index = lc $primary_index;
|
|
|
|
my $lc_secondary_index = lc $secondary_index;
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
if (not exists $self->{hash}->{$lc_primary_index}) {
|
|
|
|
my $result = "$self->{name}: $primary_index not found; similiar matches: ";
|
|
|
|
$result .= $self->levenshtein_matches($primary_index);
|
2010-06-20 08:15:10 +02:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
if (not $secondary_index) {
|
|
|
|
my $data = delete $self->{hash}->{$lc_primary_index};
|
|
|
|
my $name = $data->{_name};
|
|
|
|
$name = 'global' if $name eq '.*';
|
2010-06-20 08:15:10 +02:00
|
|
|
$self->save;
|
2020-01-15 03:10:53 +01:00
|
|
|
return "$self->{name}: $name removed.";
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
my $name1 = $self->{hash}->{$lc_primary_index}->{_name};
|
|
|
|
$name1 = 'global' if $name1 eq '.*';
|
2010-06-20 08:15:10 +02:00
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
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);
|
2010-06-20 08:15:10 +02:00
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
my $data = delete $self->{hash}->{$lc_primary_index}->{$lc_secondary_index};
|
|
|
|
my $name2 = $data->{_name};
|
|
|
|
$name2 = "\"$name2\"" if $name2 =~ / /;
|
2013-07-24 14:35:40 +02:00
|
|
|
|
2020-01-29 23:09:09 +01:00
|
|
|
# remove primary group if no more secondaries (only key left should be the _name key)
|
|
|
|
if (keys %{ $self->{hash}->{$lc_primary_index} } == 1) {
|
2020-01-15 03:10:53 +01:00
|
|
|
delete $self->{hash}->{$lc_primary_index};
|
2013-07-24 14:35:40 +02:00
|
|
|
}
|
|
|
|
|
2010-06-20 08:15:10 +02:00
|
|
|
$self->save();
|
2020-01-15 03:10:53 +01:00
|
|
|
return "$self->{name}: [$name1] $name2 removed.";
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
2020-01-15 03:10:53 +01:00
|
|
|
sub exists {
|
|
|
|
my ($self, $primary_index, $secondary_index) = @_;
|
|
|
|
$primary_index = lc $primary_index;
|
|
|
|
$secondary_index = lc $secondary_index;
|
|
|
|
return (exists $self->{hash}->{$primary_index} and exists $self->{hash}->{$primary_index}->{$secondary_index});
|
2010-06-20 08:15:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
1;
|