diff --git a/GatewayClient.pm b/GatewayClient.pm new file mode 100644 index 0000000..d1da189 --- /dev/null +++ b/GatewayClient.pm @@ -0,0 +1,312 @@ +package GatewayClient; + +# HTTP client for the odch-gateway Bot API. +# All bot data operations go through this module. + +use strict; +use warnings; +use LWP::UserAgent; +use JSON; +use Log::Log4perl qw(:levels); + +my $logger = Log::Log4perl->get_logger('GatewayClient'); + +sub new { + my ($class, %opts) = @_; + my $self = bless { + base_url => $opts{url} || 'http://127.0.0.1:3000', + api_key => $opts{api_key} || '', + ua => LWP::UserAgent->new(timeout => 10), + }, $class; + + # Strip trailing slash + $self->{base_url} =~ s/\/+$//; + + return $self; +} + +# ----------------------------------------------------------------------- +# Internal HTTP helpers +# ----------------------------------------------------------------------- + +sub _get { + my ($self, $path) = @_; + my $url = "$self->{base_url}/api/v1/bot$path"; + my $req = HTTP::Request->new(GET => $url); + $req->header('X-API-Key' => $self->{api_key}); + my $res = $self->{ua}->request($req); + return _parse_response($res, $url); +} + +sub _post { + my ($self, $path, $body) = @_; + my $url = "$self->{base_url}/api/v1/bot$path"; + my $req = HTTP::Request->new(POST => $url); + $req->header('Content-Type' => 'application/json'); + $req->header('X-API-Key' => $self->{api_key}); + $req->content(encode_json($body || {})); + my $res = $self->{ua}->request($req); + return _parse_response($res, $url); +} + +sub _put { + my ($self, $path, $body) = @_; + my $url = "$self->{base_url}/api/v1/bot$path"; + my $req = HTTP::Request->new(PUT => $url); + $req->header('Content-Type' => 'application/json'); + $req->header('X-API-Key' => $self->{api_key}); + $req->content(encode_json($body || {})); + my $res = $self->{ua}->request($req); + return _parse_response($res, $url); +} + +sub _delete { + my ($self, $path) = @_; + my $url = "$self->{base_url}/api/v1/bot$path"; + my $req = HTTP::Request->new(DELETE => $url); + $req->header('X-API-Key' => $self->{api_key}); + my $res = $self->{ua}->request($req); + return _parse_response($res, $url); +} + +sub _parse_response { + my ($res, $url) = @_; + unless ($res->is_success) { + $logger->warn("API call failed: $url -> " . $res->status_line); + return undef; + } + my $data = eval { decode_json($res->decoded_content) }; + if ($@) { + $logger->warn("JSON parse error from $url: $@"); + return undef; + } + return $data; +} + +# ----------------------------------------------------------------------- +# Tells +# ----------------------------------------------------------------------- + +sub create_tell { + my ($self, $from, $to, $message) = @_; + return $self->_post('/tells', { + from_nick => $from, + to_nick => $to, + message => $message, + }); +} + +sub get_pending_tells { + my ($self, $nick) = @_; + my $data = $self->_get("/tells/$nick"); + return $data ? ($data->{tells} || []) : []; +} + +sub mark_tell_delivered { + my ($self, $id) = @_; + return $self->_delete("/tells/$id/deliver"); +} + +# ----------------------------------------------------------------------- +# Bans +# ----------------------------------------------------------------------- + +sub create_ban { + my ($self, %opts) = @_; + return $self->_post('/bans', { + nick => $opts{nick}, + ip => $opts{ip}, + reason => $opts{reason} || '', + banned_by => $opts{banned_by}, + expires_at => $opts{expires_at}, + }); +} + +sub check_ban { + my ($self, $nick) = @_; + my $data = $self->_get("/bans/check/$nick"); + return $data ? $data->{banned} : 0; +} + +sub delete_ban { + my ($self, $id) = @_; + return $self->_delete("/bans/$id"); +} + +# ----------------------------------------------------------------------- +# Users +# ----------------------------------------------------------------------- + +sub get_user { + my ($self, $nick) = @_; + return $self->_get("/users/$nick"); +} + +sub user_connect { + my ($self, $nick, $ip, $tls) = @_; + return $self->_post("/users/$nick/connect", { + ip => $ip || '', + tls => $tls ? JSON::true : JSON::false, + }); +} + +sub user_disconnect { + my ($self, $nick) = @_; + return $self->_post("/users/$nick/disconnect", {}); +} + +# ----------------------------------------------------------------------- +# Quotes +# ----------------------------------------------------------------------- + +sub create_quote { + my ($self, $nick, $text, $added_by) = @_; + return $self->_post('/quotes', { + nick => $nick, + quote_text => $text, + added_by => $added_by, + }); +} + +sub random_quote { + my ($self, $nick) = @_; + my $path = '/quotes/random'; + $path .= "?nick=$nick" if $nick; + my $data = $self->_get($path); + return $data ? $data->{quote} : undef; +} + +# ----------------------------------------------------------------------- +# Watches +# ----------------------------------------------------------------------- + +sub create_watch { + my ($self, $watcher, $watched) = @_; + return $self->_post('/watches', { + watcher_nick => $watcher, + watched_nick => $watched, + }); +} + +sub get_watchers { + my ($self, $nick) = @_; + my $data = $self->_get("/watches/$nick"); + return $data ? ($data->{watchers} || []) : []; +} + +sub delete_watch { + my ($self, $watcher, $watched) = @_; + return $self->_delete("/watches/$watcher/$watched"); +} + +# ----------------------------------------------------------------------- +# Stats +# ----------------------------------------------------------------------- + +sub get_current_stats { + my ($self) = @_; + return $self->_get('/stats/current'); +} + +sub create_snapshot { + my ($self, $user_count, $total_share) = @_; + return $self->_post('/stats/snapshot', { + user_count => $user_count, + total_share => $total_share, + }); +} + +# ----------------------------------------------------------------------- +# Chat search +# ----------------------------------------------------------------------- + +sub search_chat { + my ($self, $query, $nick, $limit) = @_; + my $path = "/chat/search?q=$query"; + $path .= "&nick=$nick" if $nick; + $path .= "&limit=$limit" if $limit; + my $data = $self->_get($path); + return $data ? ($data->{results} || []) : []; +} + +sub first_message { + my ($self, $nick) = @_; + my $data = $self->_get("/chat/first/$nick"); + return $data ? $data->{message} : undef; +} + +sub last_message { + my ($self, $nick) = @_; + my $data = $self->_get("/chat/last/$nick"); + return $data ? $data->{message} : undef; +} + +# ----------------------------------------------------------------------- +# Gags +# ----------------------------------------------------------------------- + +sub create_gag { + my ($self, %opts) = @_; + return $self->_post('/gags', { + nick => $opts{nick}, + reason => $opts{reason} || '', + gagged_by => $opts{gagged_by}, + expires_at => $opts{expires_at}, + }); +} + +sub check_gag { + my ($self, $nick) = @_; + my $data = $self->_get("/gags/check/$nick"); + return $data ? $data->{gagged} : 0; +} + +sub delete_gag { + my ($self, $id) = @_; + return $self->_delete("/gags/$id"); +} + +# ----------------------------------------------------------------------- +# Key-value storage +# ----------------------------------------------------------------------- + +sub get_data { + my ($self, $namespace, $key) = @_; + my $data = $self->_get("/data/$namespace/$key"); + return $data ? $data->{value} : undef; +} + +sub set_data { + my ($self, $namespace, $key, $value) = @_; + return $self->_put("/data/$namespace/$key", { value => $value }); +} + +sub delete_data { + my ($self, $namespace, $key) = @_; + return $self->_delete("/data/$namespace/$key"); +} + +sub list_data { + my ($self, $namespace) = @_; + my $data = $self->_get("/data/$namespace"); + return $data ? ($data->{entries} || []) : []; +} + +# ----------------------------------------------------------------------- +# Bot registration +# ----------------------------------------------------------------------- + +sub register { + my ($self, $nick, @commands) = @_; + return $self->_post('/register', { + nick => $nick, + commands => \@commands, + }); +} + +sub unregister { + my ($self) = @_; + return $self->_delete('/register'); +} + +1; diff --git a/NMDCClient.pm b/NMDCClient.pm new file mode 100644 index 0000000..79f417d --- /dev/null +++ b/NMDCClient.pm @@ -0,0 +1,317 @@ +package NMDCClient; + +# NMDC protocol client for connecting to a DC hub as a regular user. +# Handles $Lock/$Key handshake, login, chat send/receive. + +use strict; +use warnings; +use IO::Socket::INET; +use IO::Select; +use Log::Log4perl qw(:levels); + +my $logger = Log::Log4perl->get_logger('NMDCClient'); + +sub new { + my ($class, %opts) = @_; + my $self = bless { + host => $opts{host} || '127.0.0.1', + port => $opts{port} || 4012, + nick => $opts{nick} || 'Dragon', + password => $opts{password} || '', + description => $opts{description} || 'Hub Bot', + email => $opts{email} || '', + share => $opts{share} || 0, + tag => $opts{tag} || '', + speed => $opts{speed} || 'LAN(T1)', + socket => undef, + select => undef, + buffer => '', + connected => 0, + on_chat => $opts{on_chat}, # callback: sub($nick, $message) + on_pm => $opts{on_pm}, # callback: sub($from, $message) + on_join => $opts{on_join}, # callback: sub($nick) + on_quit => $opts{on_quit}, # callback: sub($nick) + on_myinfo => $opts{on_myinfo}, # callback: sub($nick, $info) + }, $class; + return $self; +} + +sub connect { + my ($self) = @_; + + $logger->info("Connecting to $self->{host}:$self->{port}..."); + + $self->{socket} = IO::Socket::INET->new( + PeerAddr => $self->{host}, + PeerPort => $self->{port}, + Proto => 'tcp', + Timeout => 10, + ) or do { + $logger->error("Connection failed: $!"); + return 0; + }; + + $self->{socket}->autoflush(1); + $self->{select} = IO::Select->new($self->{socket}); + $self->{connected} = 1; + + $logger->info("Connected to hub"); + + # Read the $Lock message + my $lock_msg = $self->_read_message(5); + unless ($lock_msg && $lock_msg =~ /^\$Lock\s+(\S+)/) { + $logger->error("Expected \$Lock, got: " . ($lock_msg // 'nothing')); + $self->disconnect(); + return 0; + } + + my $lock = $1; + my $key = lock_to_key($lock); + + # Send key + nick + $self->_send("\$Supports|"); + $self->_send("\$Key $key|"); + $self->_send("\$ValidateNick $self->{nick}|"); + + # Read response — might be $Hello, $GetPass, or $ValidateDenide + my $response = $self->_read_message(5); + if (!$response) { + $logger->error("No response after ValidateNick"); + $self->disconnect(); + return 0; + } + + # Handle password request + if ($response =~ /\$GetPass/) { + if ($self->{password}) { + $self->_send("\$MyPass $self->{password}|"); + $response = $self->_read_message(5); + } else { + $logger->error("Hub requires password but none configured"); + $self->disconnect(); + return 0; + } + } + + if ($response =~ /\$ValidateDenide/) { + $logger->error("Nick validation denied by hub"); + $self->disconnect(); + return 0; + } + + # Should have $Hello at this point (possibly in the response) + unless ($response =~ /\$Hello\s+\Q$self->{nick}\E/) { + $logger->warn("Expected \$Hello, got: $response (continuing anyway)"); + } + + # Send our info + $self->_send("\$Version 1,0091|"); + $self->_send("\$GetNickList|"); + $self->_send_myinfo(); + + $logger->info("Logged in as $self->{nick}"); + return 1; +} + +sub disconnect { + my ($self) = @_; + if ($self->{socket}) { + eval { $self->{socket}->close(); }; + $self->{socket} = undef; + } + $self->{connected} = 0; + $self->{select} = undef; + $self->{buffer} = ''; + $logger->info("Disconnected from hub"); +} + +sub is_connected { + my ($self) = @_; + return $self->{connected} && defined $self->{socket}; +} + +# Send a public chat message +sub send_chat { + my ($self, $message) = @_; + $self->_send("<$self->{nick}> $message|"); +} + +# Send a private message +sub send_pm { + my ($self, $to_nick, $message) = @_; + $self->_send("\$To: $to_nick From: $self->{nick} \$<$self->{nick}> $message|"); +} + +# Poll for incoming messages. Returns after timeout_ms or when a message is processed. +# Call this in a loop. +sub poll { + my ($self, $timeout_ms) = @_; + $timeout_ms //= 100; + + return unless $self->{select}; + + my @ready = $self->{select}->can_read($timeout_ms / 1000.0); + return unless @ready; + + my $buf; + my $bytes = $self->{socket}->sysread($buf, 65536); + + if (!defined $bytes || $bytes == 0) { + $logger->warn("Hub disconnected"); + $self->{connected} = 0; + return; + } + + $self->{buffer} .= $buf; + + # Process complete messages (pipe-delimited) + while ($self->{buffer} =~ s/^([^|]*)\|//) { + my $msg = $1; + $self->_handle_message($msg); + } +} + +# ----------------------------------------------------------------------- +# Internal methods +# ----------------------------------------------------------------------- + +sub _send { + my ($self, $data) = @_; + return unless $self->{socket}; + eval { + $self->{socket}->syswrite($data); + }; + if ($@) { + $logger->error("Send failed: $@"); + $self->{connected} = 0; + } +} + +sub _send_myinfo { + my ($self) = @_; + # $MyINFO $ALL nick description$ $speed\x01$email$share$| + my $info = sprintf( + "\$MyINFO \$ALL %s %s%s\$ \$%s\x01\$%s\$%d\$|", + $self->{nick}, + $self->{description}, + $self->{tag}, + $self->{speed}, + $self->{email}, + $self->{share}, + ); + $self->_send($info); +} + +sub _read_message { + my ($self, $timeout) = @_; + $timeout //= 5; + + my $end = time() + $timeout; + while (time() < $end) { + # Check if we already have a complete message in buffer + if ($self->{buffer} =~ s/^([^|]*)\|//) { + return $1; + } + + my @ready = $self->{select}->can_read(0.5); + next unless @ready; + + my $buf; + my $bytes = $self->{socket}->sysread($buf, 65536); + return undef unless defined $bytes && $bytes > 0; + + $self->{buffer} .= $buf; + } + + # Try one more time + if ($self->{buffer} =~ s/^([^|]*)\|//) { + return $1; + } + return undef; +} + +sub _handle_message { + my ($self, $msg) = @_; + + # Public chat: message + if ($msg =~ /^<([^>]+)>\s*(.*)$/) { + my ($nick, $text) = ($1, $2); + if ($self->{on_chat} && $nick ne $self->{nick}) { + $self->{on_chat}->($nick, $text); + } + } + # Private message: $To: me From: nick $ message + elsif ($msg =~ /^\$To:\s+\Q$self->{nick}\E\s+From:\s+(\S+)\s+\$<[^>]+>\s*(.*)$/) { + my ($from, $text) = ($1, $2); + if ($self->{on_pm}) { + $self->{on_pm}->($from, $text); + } + } + # User join: $Hello nick + elsif ($msg =~ /^\$Hello\s+(.+)$/) { + my $nick = $1; + if ($self->{on_join} && $nick ne $self->{nick}) { + $self->{on_join}->($nick); + } + } + # User quit: $Quit nick + elsif ($msg =~ /^\$Quit\s+(.+)$/) { + my $nick = $1; + if ($self->{on_quit}) { + $self->{on_quit}->($nick); + } + } + # MyINFO: $MyINFO $ALL nick ... + elsif ($msg =~ /^\$MyINFO\s+\$ALL\s+(\S+)\s+(.*)$/) { + my ($nick, $info) = ($1, $2); + if ($self->{on_myinfo}) { + $self->{on_myinfo}->($nick, $info); + } + } + # Hub name + elsif ($msg =~ /^\$HubName\s+(.+)$/) { + $logger->debug("Hub name: $1"); + } + # Ignore other protocol messages silently +} + +# ----------------------------------------------------------------------- +# Lock-to-Key algorithm (ported from Rust odch-gateway/src/nmdc/lock_to_key.rs) +# ----------------------------------------------------------------------- + +sub lock_to_key { + my ($lock_str) = @_; + + my @lock = map { ord($_) } split //, $lock_str; + my $len = scalar @lock; + return '' if $len < 3; + + my @key; + + # Step 1-2: XOR + $key[0] = $lock[0] ^ $lock[$len - 1] ^ $lock[$len - 2] ^ 5; + for my $i (1 .. $len - 1) { + $key[$i] = $lock[$i] ^ $lock[$i - 1]; + } + + # Step 3: Nibble swap + for my $i (0 .. $len - 1) { + $key[$i] = (($key[$i] << 4) | ($key[$i] >> 4)) & 0xFF; + } + + # Step 4: Encode special characters + my $result = ''; + for my $b (@key) { + if ($b == 0) { $result .= '/%DCN000%/'; } + elsif ($b == 5) { $result .= '/%DCN005%/'; } + elsif ($b == 36) { $result .= '/%DCN036%/'; } + elsif ($b == 96) { $result .= '/%DCN096%/'; } + elsif ($b == 124) { $result .= '/%DCN124%/'; } + elsif ($b == 126) { $result .= '/%DCN126%/'; } + else { $result .= chr($b); } + } + + return $result; +} + +1; diff --git a/dragon.pl b/dragon.pl new file mode 100644 index 0000000..61c02f4 --- /dev/null +++ b/dragon.pl @@ -0,0 +1,268 @@ +#!/usr/bin/perl + +#-------------------------- +# Dragon (ODCHBot v4) — Standalone NMDC Client +# +# Connects to the hub as a regular DC client. +# Appears in the user list. Calls Gateway API for data. +# Optional add-on for fun commands, external integrations, and plugins. +#-------------------------- + +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin"; +use Log::Log4perl qw(:levels); +use YAML::AppConfig; +use POSIX qw(strftime); + +use NMDCClient; +use GatewayClient; + +our $VERSION = '4.0.0'; + +# ----------------------------------------------------------------------- +# Initialize logging +# ----------------------------------------------------------------------- +{ + my $conf_file = "$FindBin::Bin/odchbot.log4perl.conf"; + if (-f $conf_file) { + open my $fh, '<', $conf_file or die "Cannot open $conf_file: $!"; + my $conf = do { local $/; <$fh> }; + close $fh; + $conf =~ s{^(log4perl\.appender\.\w+\.filename=)(?!/)(.+)$}{$1$FindBin::Bin/$2}mg; + Log::Log4perl->init(\$conf); + } else { + Log::Log4perl->easy_init($INFO); + } +} +my $logger = Log::Log4perl->get_logger('Dragon'); +$logger->info("Dragon v$VERSION starting"); + +# ----------------------------------------------------------------------- +# Load config +# ----------------------------------------------------------------------- +my $config_file = "$FindBin::Bin/odchbot.yml"; +die "Config not found: $config_file\n" unless -f $config_file; +my $app_config = YAML::AppConfig->new(file => $config_file); +my $config = $app_config->config(); + +my $hub_config = $config->{hub} || {}; +my $gw_config = $config->{gateway} || {}; + +# ----------------------------------------------------------------------- +# Initialize Gateway client +# ----------------------------------------------------------------------- +my $gateway = GatewayClient->new( + url => $gw_config->{url} || 'http://127.0.0.1:3000', + api_key => $gw_config->{bot_api_key} || '', +); + +# Register with gateway — only declare commands Dragon actually implements. +# Gateway's built-in commands (tell, ban, history, stats, etc.) stay active. +my $nick = $hub_config->{nick} || 'Dragon'; +$gateway->register($nick, 'coin', 'roll', 'time', 'magic_8ball', '8ball', + 'russianroulette', 'rr', 'lasercats'); + +$logger->info("Registered with gateway as $nick"); + +# ----------------------------------------------------------------------- +# Load command modules +# ----------------------------------------------------------------------- +my %commands; +my $cmd_path = $config->{config}{commandPath} || 'commands'; +my $cmd_dir = "$FindBin::Bin/$cmd_path"; + +if (-d $cmd_dir) { + opendir(my $dh, $cmd_dir) or $logger->warn("Cannot open $cmd_dir: $!"); + if ($dh) { + for my $file (readdir $dh) { + next unless $file =~ /^(\w+)\.pm$/; + my $name = $1; + eval { + require "$cmd_dir/$file"; + $commands{$name} = 1; + $logger->debug("Loaded command: $name"); + }; + if ($@) { + $logger->warn("Failed to load command $name: $@"); + } + } + closedir $dh; + } +} +$logger->info("Loaded " . scalar(keys %commands) . " command modules"); + +# ----------------------------------------------------------------------- +# Connect to hub +# ----------------------------------------------------------------------- +my $client = NMDCClient->new( + host => $hub_config->{host} || '127.0.0.1', + port => $hub_config->{port} || 4012, + nick => $nick, + password => $hub_config->{password} || '', + description => $hub_config->{description} || 'Hub Bot', + email => $hub_config->{email} || '', + tag => "", + speed => 'LAN(T1)', + + on_chat => sub { + my ($from_nick, $message) = @_; + handle_chat($from_nick, $message); + }, + + on_pm => sub { + my ($from_nick, $message) = @_; + handle_pm($from_nick, $message); + }, + + on_join => sub { + my ($joined_nick) = @_; + handle_join($joined_nick); + }, + + on_quit => sub { + my ($quit_nick) = @_; + handle_quit($quit_nick); + }, +); + +# ----------------------------------------------------------------------- +# Main loop with reconnect +# ----------------------------------------------------------------------- +my $reconnect_delay = 5; + +while (1) { + if ($client->connect()) { + $reconnect_delay = 5; + $logger->info("Connected and logged in as $nick"); + + # Main event loop + while ($client->is_connected()) { + $client->poll(500); + } + + $logger->warn("Disconnected from hub"); + } else { + $logger->error("Failed to connect to hub"); + } + + $logger->info("Reconnecting in ${reconnect_delay}s..."); + sleep($reconnect_delay); + $reconnect_delay = ($reconnect_delay * 2 > 300) ? 300 : $reconnect_delay * 2; +} + +# ----------------------------------------------------------------------- +# Event handlers +# ----------------------------------------------------------------------- + +sub handle_chat { + my ($from_nick, $message) = @_; + + # Check for command prefix + if ($message =~ /^!(\w+)\s*(.*)$/) { + my ($cmd, $args) = ($1, $2 // ''); + handle_command($from_nick, lc($cmd), $args); + } +} + +sub handle_pm { + my ($from_nick, $message) = @_; + # Dragon could handle PM commands here + $logger->debug("PM from $from_nick: $message"); +} + +sub handle_join { + my ($joined_nick) = @_; + $logger->debug("User joined: $joined_nick"); + + # Deliver pending tells + eval { + my $tells = $gateway->get_pending_tells($joined_nick); + for my $tell (@$tells) { + my $from = $tell->{from_nick}; + my $msg = $tell->{message}; + my $when = $tell->{created_at} || 'sometime'; + $client->send_pm($joined_nick, + "Tell from $from ($when): $msg"); + $gateway->mark_tell_delivered($tell->{id}); + } + }; + $logger->warn("Tell delivery error: $@") if $@; + + # Notify watchers + eval { + my $watchers = $gateway->get_watchers($joined_nick); + for my $w (@$watchers) { + $client->send_pm($w->{watcher_nick}, + "$joined_nick has logged in."); + } + }; +} + +sub handle_quit { + my ($quit_nick) = @_; + $logger->debug("User quit: $quit_nick"); + + # Notify watchers + eval { + my $watchers = $gateway->get_watchers($quit_nick); + for my $w (@$watchers) { + $client->send_pm($w->{watcher_nick}, + "$quit_nick has logged out."); + } + }; +} + +sub handle_command { + my ($from_nick, $cmd, $args) = @_; + + # Built-in Dragon commands (fun, external, personality) + if ($cmd eq 'coin') { + my @options = ('Heads!', 'Tails!'); + $client->send_chat($options[int(rand(2))]); + } + elsif ($cmd eq 'roll') { + my ($count, $sides) = ($args =~ /^(\d+)?d(\d+)$/i); + $count ||= 1; $sides ||= 6; + $count = 10 if $count > 10; + $sides = 1000 if $sides > 1000; + my @rolls = map { int(rand($sides)) + 1 } 1..$count; + my $total = 0; $total += $_ for @rolls; + $client->send_chat(sprintf("%s rolled %dd%d: %s (total: %d)", + $from_nick, $count, $sides, join(', ', @rolls), $total)); + } + elsif ($cmd eq 'time') { + $client->send_chat(strftime("Current time: %Y-%m-%d %H:%M:%S UTC", gmtime)); + } + elsif ($cmd eq 'magic_8ball' || $cmd eq '8ball') { + my @answers = ( + "It is certain.", "It is decidedly so.", "Without a doubt.", + "Yes definitely.", "You may rely on it.", "As I see it, yes.", + "Most likely.", "Outlook good.", "Yes.", "Signs point to yes.", + "Reply hazy, try again.", "Ask again later.", + "Better not tell you now.", "Cannot predict now.", + "Concentrate and ask again.", "Don't count on it.", + "My reply is no.", "My sources say no.", + "Outlook not so good.", "Very doubtful.", + ); + $client->send_chat($answers[int(rand(scalar @answers))]); + } + elsif ($cmd eq 'russianroulette' || $cmd eq 'rr') { + if (int(rand(6)) == 0) { + $client->send_chat("*BANG* $from_nick is dead!"); + } else { + $client->send_chat("*click* $from_nick survives!"); + } + } + elsif ($cmd eq 'lasercats') { + $client->send_chat(' /\_/\ PEW PEW PEW'); + $client->send_chat(' ( o.o ) ----======='); + $client->send_chat(' > ^ <'); + } + else { + # Unknown command — Dragon doesn't handle it + # (Gateway built-in commands will catch it instead) + $logger->debug("Unknown Dragon command: $cmd"); + } +} diff --git a/odchbot.yml.example b/odchbot.yml.example index 1870039..422dc26 100644 --- a/odchbot.yml.example +++ b/odchbot.yml.example @@ -1,31 +1,23 @@ --- +# Dragon v4.0.0 — Standalone NMDC client bot +# Connects to hub as a regular DC user, calls Gateway API for data. + +# Hub connection (NMDC client) +hub: + host: "127.0.0.1" + port: 4012 + nick: Dragon + password: "" + description: "I am Dragon, hear me RAWR" + email: "dragon@localhost" + +# Gateway API connection +gateway: + url: "http://127.0.0.1:3000" + bot_api_key: "CHANGE_ME_your_bot_api_key" + +# Bot settings config: - allow_anon: 1 - allow_external: 1 - allow_passive: 1 - botdescription: I am Dragon, hear me RAWR - botemail: dragon@localhost - botname: Dragon - botshare: 136571 - botspeed: LAN(T1) - bottag: RAWRDC++ commandPath: commands - cp: "-" - db: - database: odchbot.db - driver: SQLite - host: '' - password: '' - path: logs - port: '' - username: '' debug: 0 - hubname: OpenDCHub ChatHub - hubname_short: ODCH - maintainer_email: odchbot@gmail.com - min_username: 3 - minshare: '5368709120' - no_perms: You do not have adequate permissions to use this function! timezone: Australia/Canberra - username_anonymous: Anonymous - username_max_length: 35