revbank/revbank
2019-07-02 04:40:08 +02:00

269 lines
7.2 KiB
Perl
Executable file

#!/usr/bin/perl -w
use strict;
use attributes;
use IO::Select;
use List::Util ();
use Term::ReadLine;
require Term::ReadLine::Gnu; # The other one sucks.
use FindBin qw($RealBin);
use lib "$RealBin/lib";
use RevBank::Plugins;
use RevBank::Global;
use RevBank::Messages;
use RevBank::Cart;
our $VERSION = "2";
our %HELP = (
"abort" => "Abort the current transaction",
);
my @words;
my $one_off = 0;
if (@ARGV) {
# Non-interactive like in sh: -c command_string
@ARGV >= 1 and $ARGV[0] eq '-c' or die "$0 has no switches, except -c.";
$one_off = 1;
@words = split " ", $ARGV[1];
push @words, @ARGV[3 .. $#ARGV] if @ARGV > 3;
push @words, "help" if not @words;
}
$| = 1;
my $readline = Term::ReadLine->new($0);
$readline->ornaments('me,md,,');
my $select = IO::Select->new;
$select->add(\*STDIN);
my $cart = RevBank::Cart->new;
sub prompt {
my ($prompt, $plugins, @completions) = @_;
$prompt =~ s/$/: /;
$prompt =~ s/\?: $/? /;
my @matches;
$readline->Attribs->{completion_entry_function} = sub {
my ($word, $state) = @_;
@matches = grep /^\Q$word\E/i, @completions if $state == 0;
return shift @matches;
};
my $done;
my $input;
$readline->callback_handler_install($prompt, sub {
$done = 1;
$input = shift;
$readline->callback_handler_remove;
});
my $begin = my $time = time;
while (not $done) {
if ($::ABORT_HACK) {
# Global variable that a signal handling plugin can set.
# Do not use, but "return ABORT" instead.
my $reason = $::ABORT_HACK;
$::ABORT_HACK = 0;
abort($reason);
}
if ($select->can_read(.05)) {
$readline->callback_read_char;
$begin = $time;
}
if (time > $time) {
$time = time;
call_hooks(
"prompt_idle",
$cart,
(@$plugins > 1 ? undef : $plugins->[0]), # >1 plugin = main loop
$time - $begin,
$readline,
);
}
}
print "\e[0m";
defined $input or return;
$readline->addhistory($input);
$input =~ s/^\s+//; # trim leading whitespace
$input =~ s/\s+$//; # trim trailing whitespace
return $input;
}
RevBank::Plugins->load;
call_hooks("startup");
OUTER: for (;;) {
print "\n" if not @words;
if (not @words and $cart->changed) {
call_hooks("cart_changed", $cart);
}
my $split_input = 1;
my $prompt = "Product ID, amount or command";
my @plugins = RevBank::Plugins->new;
my $method = "command";
sub abort {
print @_, " " if @_;
@words = ();
call_hooks "abort", $cart;
$cart->empty;
{ no warnings; redo OUTER; }
}
PROMPT: {
if (not @words) {
if ($one_off) {
exit if $one_off++ > 1;
abort "Incomplete command." if $cart->size;
exit;
}
call_hooks "prompt", $cart, $prompt;
my %completions = qw(abort 1);
for my $plugin (@plugins) {
my $attr = attributes::get(
ref $method ? $method : $plugin->can($method)
) or next;
my ($tab) = $attr =~ /Tab \( (.*?) \)/x;
for my $keyword (split /\s*,\s*/, $tab) {
if ($keyword =~ /^&(.*)/) {
my $method = $1;
@completions{ $plugin->$method } = ();
} else {
$completions{ $keyword }++;
}
}
}
if (delete $completions{USERS}) {
$completions{$_}++ for RevBank::Users::names;
}
if (delete $completions{NOABORT}) {
delete $completions{abort};
}
my $input = prompt $prompt, \@plugins, keys %completions;
call_hooks "input", $cart, $input, $split_input;
length $input or redo PROMPT;
@words = ($split_input ? split(" ", $input) : $input);
$split_input = 0; # Only split 'outer' input.
}
WORD: for (;;) {
redo PROMPT if not @words;
abort if grep $_ eq 'abort', @words;
my $word = shift @words;
PLUGIN: for my $plugin (@plugins) {
my ($rv, @rvargs) = eval { $plugin->$method($cart, $word) };
if ($@) {
call_hooks "plugin_fail", $plugin->id, $@;
abort;
}
if (not defined $rv) {
call_hooks "plugin_fail", $plugin->id, "No return code";
abort;
}
if (not ref $rv) {
$prompt = $rv;
@plugins = $plugin;
($method) = @rvargs;
call_hooks "plugin_fail", $plugin->id, "No method supplied"
if not ref $method;
abort "Incomplete command." if $one_off and not @words;
next WORD;
}
if ($rv == ABORT) {
abort(@rvargs);
}
if ($rv == REJECT) {
my ($reason) = @rvargs;
call_hooks "reject", $plugin->id, $reason, @words ? 1 : 0;
abort if @words;
redo PROMPT;
}
if ($rv == ACCEPT) {
next OUTER;
}
if ($rv == NEXT) {
next PLUGIN if $method eq 'command';
call_hooks "plugin_fail", $plugin->id, "Only 'command' "
. "should ever return NEXT.";
abort;
}
call_hooks "plugin_fail", $plugin->id, "Invalid return value";
abort;
}
call_hooks "invalid_input", $cart, $word;
abort if @words;
redo OUTER;
}
}
}
=head1 NAME
revbank - Banking for hackerspace visitors
=head1 ANNOUNCEMENTS
The following will disappear in a future version:
=head2 Deprecated: C<nyan>, C<game>
These non-serious, non-banking plugins will be removed. Please remove them
from C<revbank.plugins>.
=head2 Deprecated: creating new accounts with C<deposit>
For a while now, there has been a dedicated plugin, C<adduser> to create new
accounts. The old way of creating new accounts (unknown input after a
C<deposit> command was assumed to be the name of the a account) did not allow
for any input validation and would cause trouble if a user name already
existed.
Please add C<adduser> to C<revbank.plugins>.
=head1 DESCRIPTION
Maybe I'll write some documentation, but not now.
Shell-like invocation with C<-c> is supported, sort of, but it has to be a
complete command. Currently, multiple commands are supported on the command
line (space separated), but that's an unintended feature...
=head1 PLUGINS
Refer to L<RevBank::Plugins> for documentation about writing plugins.
Plugins themselves may have some documentation in the respective plugin files.
=head1 AUTHOR
Juerd Waalboer <#####@juerd.nl>
=head1 LICENSE
Pick your favorite OSI license.