2010-06-18 05:19:45 +02:00
|
|
|
# File: HashObject.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. Provides
|
|
|
|
# case-insensitive access to the index key while preserving original case when
|
|
|
|
# displaying index key.
|
2010-06-18 05:19:45 +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-18 05:19:45 +02:00
|
|
|
package PBot::HashObject;
|
|
|
|
|
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-18 05:19:45 +02:00
|
|
|
use Text::Levenshtein qw(fastdistance);
|
2019-06-28 09:22:57 +02:00
|
|
|
use JSON;
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-14 22:32:12 +01:00
|
|
|
sub new {
|
2020-02-15 23:38:32 +01:00
|
|
|
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;
|
2020-02-14 22:32:12 +01:00
|
|
|
}
|
|
|
|
|
2010-06-18 05:19:45 +02:00
|
|
|
sub initialize {
|
2020-02-15 23:38:32 +01:00
|
|
|
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} = {};
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
2017-08-06 05:14:49 +02:00
|
|
|
sub load {
|
2020-02-15 23:38:32 +01:00
|
|
|
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;
|
|
|
|
}
|
2020-01-15 03:10:53 +01:00
|
|
|
|
2021-06-05 22:20:03 +02:00
|
|
|
$self->{pbot}->{logger}->log("Loading $self->{name} from $filename\n");
|
2020-02-15 23:38:32 +01:00
|
|
|
|
|
|
|
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}) {
|
2020-04-23 01:46:09 +02:00
|
|
|
if ($index ne lc $index) {
|
|
|
|
if (exists $self->{hash}->{lc $index}) {
|
|
|
|
Carp::croak "Cannot update $self->{name} object $index; duplicate object found";
|
|
|
|
}
|
2020-02-15 23:38:32 +01:00
|
|
|
|
|
|
|
my $data = delete $self->{hash}->{$index};
|
|
|
|
$data->{_name} = $index;
|
|
|
|
$self->{hash}->{lc $index} = $data;
|
|
|
|
}
|
|
|
|
}
|
2020-01-15 03:10:53 +01:00
|
|
|
}
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
2017-08-06 05:14:49 +02:00
|
|
|
sub save {
|
2020-02-15 23:38:32 +01:00
|
|
|
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;
|
|
|
|
}
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
$self->{pbot}->{logger}->log("Saving $self->{name} to $filename\n");
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-04-22 04:44:14 +02:00
|
|
|
if (not $self->get_data('$metadata$', 'update_version')) {
|
|
|
|
$self->add('$metadata$', { update_version => PBot::VERSION::BUILD_REVISION });
|
|
|
|
}
|
|
|
|
|
2020-04-25 08:04:07 +02:00
|
|
|
$self->set('$metadata$', 'name', $self->{name}, 1);
|
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
my $json = JSON->new;
|
|
|
|
my $json_text = $json->pretty->canonical->utf8->encode($self->{hash});
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
open(FILE, "> $filename") or die "Couldn't open $filename: $!\n";
|
|
|
|
print FILE "$json_text\n";
|
|
|
|
close(FILE);
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
2017-08-06 05:14:49 +02:00
|
|
|
sub clear {
|
2020-02-15 23:38:32 +01:00
|
|
|
my $self = shift;
|
|
|
|
$self->{hash} = {};
|
2017-08-06 05:14:49 +02:00
|
|
|
}
|
|
|
|
|
2010-06-18 05:19:45 +02:00
|
|
|
sub levenshtein_matches {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $keyword) = @_;
|
|
|
|
my $comma = '';
|
|
|
|
my $result = "";
|
2019-06-26 18:34:19 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
foreach my $index (sort keys %{$self->{hash}}) {
|
|
|
|
my $distance = fastdistance($keyword, $index);
|
|
|
|
my $length = (length $keyword > length $index) ? length $keyword : length $index;
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
if ($length != 0 && $distance / $length < 0.50) {
|
|
|
|
$result .= $comma . $index;
|
|
|
|
$comma = ", ";
|
|
|
|
}
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
$result =~ s/(.*), /$1 or /;
|
|
|
|
$result = "none" if $comma eq '';
|
|
|
|
return $result;
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub set {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $index, $key, $value, $dont_save) = @_;
|
|
|
|
my $lc_index = lc $index;
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
if (not exists $self->{hash}->{$lc_index}) {
|
|
|
|
my $result = "$self->{name}: $index not found; similiar matches: ";
|
|
|
|
$result .= $self->levenshtein_matches($index);
|
|
|
|
return $result;
|
2010-06-18 06:40:12 +02:00
|
|
|
}
|
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
if (not defined $key) {
|
2020-04-23 01:46:09 +02:00
|
|
|
my $result = "[$self->{name}] " . $self->get_key_name($lc_index) . " keys: ";
|
2020-02-15 23:38:32 +01:00
|
|
|
my $comma = '';
|
2020-04-23 01:46:09 +02:00
|
|
|
foreach my $k (sort grep { $_ ne '_name' } keys %{$self->{hash}->{$lc_index}}) {
|
2021-02-07 22:43:39 +01:00
|
|
|
$result .= $comma . "$k: " . $self->{hash}->{$lc_index}->{$k};
|
2020-04-24 01:19:36 +02:00
|
|
|
$comma = ";\n";
|
2020-02-15 23:38:32 +01:00
|
|
|
}
|
|
|
|
$result .= "none" if ($comma eq '');
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2020-04-25 08:04:07 +02:00
|
|
|
if (not defined $value) {
|
|
|
|
$value = $self->{hash}->{$lc_index}->{$key};
|
|
|
|
} else {
|
2020-02-15 23:38:32 +01:00
|
|
|
$self->{hash}->{$lc_index}->{$key} = $value;
|
|
|
|
$self->save unless $dont_save;
|
|
|
|
}
|
2020-04-23 01:46:09 +02:00
|
|
|
return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key " . (defined $value ? "set to $value" : "is not set.");
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub unset {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $index, $key) = @_;
|
|
|
|
my $lc_index = lc $index;
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
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;
|
2020-04-23 01:46:09 +02:00
|
|
|
return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key unset.";
|
2020-02-15 23:38:32 +01:00
|
|
|
} else {
|
2020-04-23 01:46:09 +02:00
|
|
|
return "[$self->{name}] " . $self->get_key_name($lc_index) . ": $key does not exist.";
|
2020-02-15 23:38:32 +01:00
|
|
|
}
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
2020-01-19 06:41:47 +01:00
|
|
|
sub exists {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $index, $data_index) = @_;
|
|
|
|
return exists $self->{hash}->{lc $index} if not defined $data_index;
|
|
|
|
return exists $self->{hash}->{lc $index}->{$data_index};
|
2020-02-12 06:49:43 +01:00
|
|
|
}
|
|
|
|
|
2020-04-23 01:46:09 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-02-12 06:49:43 +01:00
|
|
|
sub get_keys {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $index) = @_;
|
2020-04-22 04:44:14 +02:00
|
|
|
return grep { $_ ne '$metadata$' } keys %{$self->{hash}} if not defined $index;
|
2020-02-15 23:38:32 +01:00
|
|
|
return grep { $_ ne '_name' } keys %{$self->{hash}->{lc $index}};
|
2020-01-19 06:41:47 +01:00
|
|
|
}
|
|
|
|
|
2020-02-03 18:47:21 +01:00
|
|
|
sub get_data {
|
2020-02-15 23:38:32 +01:00
|
|
|
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};
|
2020-02-03 18:47:21 +01:00
|
|
|
}
|
|
|
|
|
2010-06-18 05:19:45 +02:00
|
|
|
sub add {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $index, $data, $dont_save) = @_;
|
|
|
|
my $lc_index = lc $index;
|
2020-04-23 01:46:09 +02:00
|
|
|
|
|
|
|
# preserve case of index
|
|
|
|
if ($index ne $lc_index) {
|
|
|
|
$data->{_name} = $index;
|
|
|
|
}
|
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
$self->{hash}->{$lc_index} = $data;
|
|
|
|
$self->save unless $dont_save;
|
|
|
|
return "$index added to $self->{name}.";
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub remove {
|
2020-02-15 23:38:32 +01:00
|
|
|
my ($self, $index, $data_index, $dont_save) = @_;
|
|
|
|
my $lc_index = lc $index;
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
if (not exists $self->{hash}->{$lc_index}) {
|
|
|
|
my $result = "$self->{name}: $index not found; similiar matches: ";
|
|
|
|
$result .= $self->levenshtein_matches($lc_index);
|
|
|
|
return $result;
|
|
|
|
}
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
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;
|
2020-04-23 01:46:09 +02:00
|
|
|
return $self->get_key_name($lc_index) . ".$data_index removed from $self->{name}";
|
2020-02-15 23:38:32 +01:00
|
|
|
} else {
|
2020-04-23 01:46:09 +02:00
|
|
|
return "$self->{name}: " . $self->get_key_name($lc_index) . ".$data_index does not exist.";
|
2020-02-15 23:38:32 +01:00
|
|
|
}
|
2020-02-12 07:56:42 +01:00
|
|
|
}
|
2010-06-18 05:19:45 +02:00
|
|
|
|
2020-02-15 23:38:32 +01:00
|
|
|
my $data = delete $self->{hash}->{$lc_index};
|
|
|
|
if (defined $data) {
|
|
|
|
$self->save unless $dont_save;
|
2020-04-23 01:46:09 +02:00
|
|
|
my $name = exists $data->{_name} ? $data->{_name} : $lc_index;
|
|
|
|
return "$name removed from $self->{name}.";
|
2020-02-15 23:38:32 +01:00
|
|
|
} else {
|
|
|
|
return "$self->{name}: $data_index does not exist.";
|
|
|
|
}
|
2010-06-18 05:19:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
1;
|