Created
August 2, 2020 20:44
-
-
Save davehayes/921f1211ac345f7e590c2f602855fb8b to your computer and use it in GitHub Desktop.
"Simple" matrix bot in Raku
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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