Skip to content

Instantly share code, notes, and snippets.

@davehayes
Created August 2, 2020 20:44
Show Gist options
  • Select an option

  • Save davehayes/921f1211ac345f7e590c2f602855fb8b to your computer and use it in GitHub Desktop.

Select an option

Save davehayes/921f1211ac345f7e590c2f602855fb8b to your computer and use it in GitHub Desktop.
"Simple" matrix bot in Raku
#!/rw/home/dave/.rakubrew/versions/moar-2020.05.1/install/bin/raku
use Matrix::Bot;
use Matrix::Bot::Plugin;
use Net::DNS;
my $VERSION = "1.0";
use LogP6 :configure;
filter(name => "", level => $trace, :update);
## Configure here
my $admin = "\@somebot:matrix.org";
## Meta programming, go go raku! :D
role ListableThing {
has Str $.name;
has Str $.desc;
method describe(-->Str) { return $!desc; }
}
class ListOfThings {
has $.classname;
has %.insttable;
method addThing(Str :name($myname), *%rest) {
my $obj = $.classname.new( name => $myname, |%rest );
%!insttable{$myname} = $obj;
say "### ADDED this: $myname";
dd $obj;
}
method describeThing(Str $name) {
return $.insttable{$name}.describe();
}
method getThing(Str $name) {
return $.insttable{$name}
}
method iterThings($cb, Bool $sorted = False) {
if ($sorted) {
for %!insttable.keys.sort -> $key { $cb($key, %.insttable{$key}); }
} else {
for %!insttable.keys -> $key { $cb($key,%.insttable{$key}); }
}
}
method listThings() { return $.insttable.keys.sort }
}
## Room Options
class RoomOption does ListableThing {
has $.default;
has $.error;
has Bool $.multi;
has @.possible;
has Set $!pset;
has %.values;
method TWEAK() {
$!pset = set @.possible;
}
method addRoom(Str $room) {
%!values{ $room } = $.default;
}
method setRoomValue (Str $room, Str $valuestr) {
if ($!multi) {
my @newvals = split(',', $valuestr);
if (@newvals <E2><8A><82> $!pset) {
%!values{$room} = @newvals;
return True;
} else {
$!error = "Illegal elements in set, must be one of " ~ @.possible.join(',');
return False;
}
} else {
if ($valuestr ~~ /\,/) {
$!error = "You may not provide a comma in single valued options";
return False;
}
%!values{$room} = $valuestr;
return True;
}
}
method getRoomValue (Str $room) {
return %.values{$room};
}
}
my $RoomOpts = ListOfThings.new( classname => RoomOption );
my %RoomFwd;
my %RoomRev;
sub setRoomInfo(Str $id, Str $name) {
%RoomFwd{$id} = $name;
%RoomRev{$name} = $id;
}
## Commands
class C3POCmd does ListableThing {
has Str $.usage;
has $.runcmd;
method pusage { return $.usage; }
}
my $Commands = ListOfThings.new( classname => C3POCmd );
## Magic Matrix Quoting
sub matrixQuote(Str $text) { return '```' ~ "\n" ~ $text ~ "\n```\n"; }
## DNS SECTION
class C3POResolvers does ListableThing {
has Str $.host;
has $.res;
method TWEAK { $!res = Net::DNS.new($.host); }
}
my $Resolvers = ListOfThings.new( classname => C3POResolvers );
$Resolvers.addThing( name => 'GOOGLE', host => '8.8.8.8',
desc => 'Google\'s own public DNS' );
$RoomOpts.addThing(
name => 'useDnsServers',
desc => "Which DNS servers should we use for DNS lookups?",
default => <GOOGLE>,
multi => True,
possible => $Resolvers.listThings(),
);
## Safe URL parsing? Found this on the web. Big ticket item for Raku!
grammar URL
{
regex TOP { <Scheme> <Hostinfo> <Path>? }
regex Scheme { <[a..z]><[a..z 0..9 + . : \-]>* }
regex SchemeW { <Scheme> <SchemeS> }
regex SchemeS { ':' }
regex Host { <[\w \. \-]>* }
regex Hostinfo { '//' <Host> <PortW>? }
regex PortW { <PortS> <Port> }
regex PortS { ':' }
regex Port { \d+ }
regex Path { '/'? <[\w \d -] - [#?]>* <Query>? }
regex Query { '?' <[\w \d = -] - [#?]>* }
}
## All the commands this bot supports
$Commands.addThing(
name => 'curl',
usage => 'curl <url>',
desc => 'Does a curl request for <url>. Just prints headers and debug info.',
runcmd => sub ($plugin, $cmd, $e, @args) {
my $url = @args[0];
say $e.user ~ " requested a curl to " ~ $url;
my $result = URL.parse($url);
if $result {
my $proc = run "curl", "-q", "-s", "-v", "$url", :out :err;
my $err = $proc.err.slurp: :close;
return matrixQuote($err);
} else {
return matrixQuote("Malformed URL and I am not printing that to the screen");
}
}
);
$Commands.addThing(
name => 'smile',
usage => 'smile',
desc => 'Smile at the room',
runcmd => sub ($plugin, $cmd, $e, @args) { return ':D'; }
);
$Commands.addThing(
name => 'frown',
usage => 'frown',
desc => 'Frown at the room',
runcmd => sub ($plugin, $cmd, $e, @args) { return 'But why should I frown?'; }
);
$Commands.addThing(
name => 'version',
usage => 'version',
desc => 'Show versions of everything',
runcmd => sub ($plugin, $cmd, $e, @args) {
return "**C3PO** ```$VERSION``` -- **OS** ```" ~ $*DISTRO.gist ~ "``` -- **LANG** ```" ~ $*RAKU.gist ~ "```";
}
);
$Commands.addThing(
name => 'opts',
usage => 'opts [<option>] [<value>]',
desc => 'Get or set options for this room',
runcmd => sub ($plugin, $cmd, $e, @args) {
if (!defined(@args[0])) {
# List all options
my $output = '### Room options for room ' ~ $e.room ~ "\n";
$RoomOpts.iterThings(
sub ($option, $optob) {
my $value = $optob.getRoomValue(%RoomFwd{$e.room});
my $desc = $optob.describe();
$output ~= "[$option]:($value) -- $desc\n";
}, True);
return matrixQuote($output);
} elsif (!defined(@args[1])) {
my $optob = $RoomOpts.getThing(@args[0]);
return "No such option: " ~ @args[0] unless (defined($optob));
my $value = $optob.getRoomValue(%RoomFwd{$e.room});
my $desc = $optob.describe();
return sprintf("**%10s** - currently: %s\n\n%s\n", @args[0], $value, matrixQuote($desc));
} else {
my $optob = $RoomOpts.getThing(@args[0]);
return "No such option: " ~ @args[0] unless (defined($optob));
my $ok = $optob.setRoomValue(%RoomFwd{$e.room}, @args[1]);
return $optob.error unless ($ok);
return sprintf("[%10s] Option set to '%s'\n", @args[0], @args[1]);
}
},
);
$Commands.addThing(
name => 'help',
usage => 'help [<command>]',
desc => 'List available commands',
runcmd => sub ($plugin, $cmd, $e, @args) {
if (defined(@args[0])) {
my $cmd = $Commands.getThing(@args[0]);
return "No such command " ~ @args[0] unless (defined($cmd));
return @args[0] ~ ' -- ' ~ $cmd.describe();
} else {
my $output = "## mybot Command Help:\n";
$Commands.iterThings(
sub ($cmd, $cmdob) {
say "(($cmd))";
my $u = $cmdob.pusage();
my $desc = $cmdob.describe();
$output ~= sprintf("%-10s\n %s\n", $u, $desc);
}, True);
return matrixQuote($output);
}
},
);
sub formatDNS(Str $prefix, Str $type, Seq $response --> Str) {
my $output = "";
for $response<> -> $line {
dd $line;
my $extra = "IN";
try {
if ($line.priority > 0) {
$extra ~= " " ~ $line.priority
}
}
my $result = sprintf("%s -- $extra $type %s\n", $prefix, $line);
$output ~= $result;
}
$output ~= "\n";
return $output
}
$Commands.addThing(
name => 'dns',
usage => 'dns <hostname> [<type>]',
desc => 'Do a dns lookup from configured servers and print that here.
<type> defaults to \'A\' and can be any valid DNS record type.',
runcmd => sub ($plugin, $cmd, $e, @args) {
say "DNS command invoked in " ~ %RoomFwd{$e.room};
my $host = @args[0];
return matrixQuote("Usage: " ~ $cmd.usage) unless (defined($host));
my $type = @args[1] // 'A';
my $out = '';
my @validres = @( $RoomOpts.getThing('useDnsServers').getRoomValue(%RoomFwd{$e.room}) );
dd @validres;
say "Performing DNS on those sites for '$host' and '$type'";
$Resolvers.iterThings(
sub ($name, $res) {
say "iteration $name";
return unless ($name <E2><88><88> @validres);
say "Looking up via $name";
my $lookup = $res.res.lookup($type, $host);
if ($lookup.elems == 0) {
$out ~= "DNS Lookup of ```$host``` fails from **$name**\n\n";
} else {
$out ~= "DNS Lookup of ```$host``` using **$name:**\n\n```\n";
$out ~= formatDNS(uc($name), uc($type), $res.res.lookup($type, $host));
$out ~= "\n```\n\n";
}
}, True);
return $out;
},
);
### The bot itself
my $bot;
class Local::Matrix::Plugin is Matrix::Bot::Plugin {
has $.myclient;
multi method handle-room-text ($e) {
say "### Room text:";
dd $e;
if ($e.message ~~ /^mybot\s+(.*)/) {
my @tokens = $0.split(/\s+/);
my $cmd = @tokens.shift;
say "Dispatch to '$cmd'";
my $cmdob = $Commands.getThing($cmd);
return "Unknown command: '$cmd'" unless (defined($cmdob));
return $cmdob.runcmd.(self, $cmdob, $e, @tokens);
} else {
return False;
}
}
multi method handle-invite ($e --> Bool) {
if ($e.user eq $admin) {
self.log.info("Joining room {$e.room}, invited by {$e.user}");
return True;
} else {
self.log.error("Not Joining room {$e.room}, invited by {$e.user} who is not an admin");
return False;
}
}
multi method handle-connect ($e) {
say "### Connecting:";
my $c = $bot.client;
$!myclient = $c;
for $c.joined-rooms -> $room {
say "I am apparently in room id (" ~ $room.id ~ "): [" ~ $room.name ~ "]";
$bot.send-markdown($room.id, "**mybot** has reconnected to this room");
say "...sent a message to " ~ $room.name;
setRoomInfo($room.id, $room.name);
$RoomOpts.iterThings(
sub ($opt, $optob) {
say "...adding default config '$opt' for room " ~ $room.id ~ '[' ~ $room.name ~ ']';
$optob.addRoom($room.name);
});
}
say "### Connect finished";
return True;
}
}
sub cleanly_exit($whatTheFrigIsThisArgSupposedToBe) {
say "Trying to cleanly exit";
my $c = $bot.client;
for $c.joined-rooms -> $room {
say "...disconnect from room: " ~ $room.id ~ ' [' ~ $room.name ~ ']';
$bot.send-markdown($room.id, "**mybot** is disconnecting");
}
exit 0;
}
signal(SIGINT).tap( &cleanly_exit );
signal(SIGTERM).tap( &cleanly_exit );
say "C3P0 starting up";
# using pantaliamon here, that's why the localhost connect
my $local = Local::Matrix::Plugin.new;
$bot = Matrix::Bot.new(
home-server => "http://127.0.0.1:8342",
username => "MyBot",
password => "thisisntreal",
plugins => [ $local ],
);
$bot.run;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment