mata_bot.pl (7655B)
1 #!/usr/bin/perl 2 use v5.40; 3 4 use Getopt::Std; 5 use HTTP::Tiny; 6 use IO::Socket qw(AF_INET SOCK_STREAM); 7 use IO::Socket::SSL; 8 use POSIX qw(strftime); 9 10 my $CHUNK_LENGTH = 1024; 11 my $NICK = 'mata_bot'; 12 my $USER = 'mata_bot_beta4'; 13 my $REAL = 'death to technomage!!'; 14 my $MOTHER = 'anelli'; 15 my $MATA_NORM = '[._.]'; 16 my $MATA_HAPPY = '[^_^]'; 17 my $MATA_DEAD = '[x~x]'; 18 my $MATA_CUTE = '[>.<]'; 19 20 my $chan = '#unix_surrealism'; 21 my $host = 'irc.libera.chat'; 22 my $logging = 0; 23 my $port = '6697'; 24 my $tls = 1; 25 my %subbuffer; 26 27 # read in the quotes file; 28 open (my $quotes_file, "<", "quotes") or die "couldn't open quotes: $!"; 29 chomp(my @quotes = <$quotes_file>); 30 my $quotes_num = $.; 31 close $quotes_file or die "$quotes_file: $!"; 32 33 # read in the 8ball file 34 open (my $ball_file, "<", "ball") or die "couldn't open ball: $!"; 35 chomp(my @ball = <$ball_file>); 36 my $ball_num = $.; 37 close $ball_file or die "$ball_file: $!"; 38 39 sub randint { 40 my($min, $max) = @_; 41 return $min if $min == $max; 42 ($min, $max) = ($max, $min) if $min > $max; 43 return $min + int rand(1 + $max - $min); 44 } 45 46 sub logger { 47 my $logmessage = shift; 48 open(my $logfile, ">>", "bot.log") or die "Can't open bot.log: $!"; 49 print $logfile strftime('%Y-%m-%dT%H:%M:%SZ ', gmtime()), $logmessage, "\n"; 50 } 51 52 sub out { 53 my ($sock, $message) = @_; 54 logger($message) if ($logging); 55 print $sock "$message\r\n"; 56 } 57 58 sub msg { 59 my ($sock, $message) = @_; 60 out($sock, "PRIVMSG $chan :$message"); 61 } 62 63 sub respond_command { 64 my ($sock, $sender_nick, $content) = @_; 65 if ($content =~ /\b{wb}([1-9]\d*)?d([1-9]\d*)\b{wb}/) { 66 # we got dice 67 my $ndice = $1 // 1; 68 my $nface = $2; 69 my $min = $ndice; 70 my $max = $ndice * $nface; 71 my $result = $min + int rand(1 + $max - $min); 72 my $roll = "d${nface}"; 73 $roll = $ndice . $roll if $ndice > 1; 74 msg($sock, "${sender_nick} rolled a ${roll} and got ${result}! ${MATA_HAPPY}"); 75 } elsif ($content =~ /\b{wb}good\b{wb}[^\000\r\n]*\b{wb}(bot|boy|girl)/) { 76 # we got a compliment 77 if ($sender_nick eq $MOTHER) { 78 msg($sock, "Thank you, mother! ${MATA_HAPPY}"); 79 } else { 80 msg($sock, "Thanks! ${MATA_HAPPY}"); 81 } 82 } elsif ($content =~ /\?$/) { 83 # we got a question 84 msg($sock, "$ball[int rand($ball_num)] ${MATA_CUTE}"); 85 } else { 86 # we got anything else 87 if ($sender_nick eq $MOTHER) { 88 msg($sock, "Done, mother! ${MATA_HAPPY}"); 89 } else { 90 msg($sock, "\1ACTION leans over and places it's hand near it's antenna. \"HUUH?\" ${MATA_NORM}\1"); 91 } 92 } 93 } 94 95 sub respond_mention { 96 my ($sock, $sender_nick) = @_; 97 if ($sender_nick eq $MOTHER) { 98 msg($sock, "Yes, mother? ${MATA_HAPPY}"); 99 } else { 100 msg($sock, "$quotes[int rand($quotes_num)] ${MATA_NORM}"); 101 } 102 } 103 104 # respond to channel 105 # returns 1 if bot shouldn't remember last message for s///, 0 otherwise 106 sub respond { 107 my ($sock, $sender_nick, $message) = @_; 108 if ($subbuffer{$sender_nick} && $message =~ m,\b{wb}s/([^\000\r\n/]*)/([^\000\r\n/]*)/?,) { 109 # chat s/// 110 my $toreplace = $1; 111 my $replacement = $2; 112 if ($subbuffer{$sender_nick} =~ s/$toreplace/$replacement/) { 113 msg($sock, "${sender_nick} meant to say: $subbuffer{$sender_nick}"); 114 return 1; 115 } 116 } elsif ($message =~ m,watch\?v=([a-zA-Z0-9_-]+),) { 117 # post youtube video titles from video ID 118 my $video_id = $1; 119 my $response = HTTP::Tiny->new->get("https://fuyt.lab8.cz/?s=${video_id}&o=relevance"); 120 unless ($response->{success}) { 121 msg($sock, "failed to get title of video ${video_id} ${MATA_DEAD} ($response->{status} $response->{reason}!)"); 122 return 0; 123 } 124 unless (length $response->{content}) { 125 msg($sock, "failed to get title of youtube video ${video_id} ${MATA_DEAD} (HTTP response empty!)"); 126 return 0; 127 } 128 my $content = $response->{content}; 129 if ($content =~ m,<span class="title"><a href="https://www\.youtube\.com/watch\?v=$video_id" accesskey="0">([^<]+)</a>,) { 130 my $title = $1; 131 $title =~ tr/[\000\r\n]//d; 132 $title = trim($title); 133 msg($sock, "YouTube: $title"); 134 } else { 135 msg($sock, "failed to get title of video ${video_id} ${MATA_DEAD} (no videos matching ID $video_id!)"); 136 } 137 } elsif ($message =~ m,(https?://[^\000\r\n ]+)$,) { 138 my $url = $1; 139 # get in it's HEAD to check if it's text/html 140 my $response = HTTP::Tiny->new->head($url); 141 unless ($response->{success}) { 142 msg($sock, "failed to get info about link: ${url} ${MATA_DEAD} ($response->{status} $response->{reason}!)"); 143 return 0; 144 } 145 unless ($response->{headers}->{'content-type'}) { 146 msg($sock, "failed to get info about link: ${url} ${MATA_DEAD} (no ``content-type'' header found in HTTP response!)"); 147 return 0; 148 } 149 unless ($response->{headers}->{'content-type'} =~ m,text/html,) { 150 # we got a non text/html content type 151 msg($sock, "File: $response->{headers}->{'content-type'}"); 152 return 0; 153 } 154 155 # if it's text/html, GET it's title 156 $response = HTTP::Tiny->new->get($url); 157 unless ($response->{success}) { 158 msg($sock, "failed to get title of link ${url} ${MATA_DEAD} ($response->{status} $response->{reason}!)"); 159 return 0; 160 } 161 unless (length $response->{content}) { 162 msg($sock, "failed to get title of link ${url} ${MATA_DEAD} (HTTP response empty!)"); 163 return 0; 164 } 165 my $content = $response->{content}; 166 if ($content =~ m,<title[^>]*>([^<]+)</title[^>]*>,) { 167 my $title = $1; 168 $title =~ tr/[\000\r\n]//d; 169 $title = trim($title); 170 msg($sock, "Title: $title"); 171 } else { 172 msg($sock, "failed to get title of link ${url} ${MATA_DEAD} (no title found!)"); 173 } 174 } elsif ($message =~ /\b${NICK}\b/) { 175 if ($message =~ /^ *${NICK}[:, ] *([^\000\r\n]+)$/) { 176 respond_command($sock, $sender_nick, $1); 177 } else { 178 respond_mention($sock, $sender_nick); 179 } 180 } 181 return 0; 182 } 183 184 # process args 185 getopts('h:j:lp:t', \my %opts); 186 $chan = $opts{'j'} if ($opts{'j'}); 187 $host = $opts{'h'} if ($opts{'h'}); 188 $logging = 1 if ($opts{'l'}); 189 $port = $opts{'p'} if ($opts{'p'}); 190 $tls = 0 if ($opts{'t'}); 191 192 # start the connection 193 my $sock; 194 if ($tls) { 195 $sock = IO::Socket::SSL->new( 196 Domain => AF_INET, 197 Type => SOCK_STREAM, 198 PeerHost => $host, 199 PeerPort => $port, 200 ) || die "Can't open socket: $IO::Socket::errstr"; 201 } else { 202 $sock = IO::Socket->new( 203 Domain => AF_INET, 204 Type => SOCK_STREAM, 205 proto => 'tcp', 206 PeerHost => $host, 207 PeerPort => $port, 208 ) || die "Can't open socket: $IO::Socket::errstr"; 209 } 210 211 # set user, real, and nick, then join 212 out($sock, "USER $USER * * :$REAL"); 213 out($sock, "NICK $NICK"); 214 out($sock, "JOIN $chan"); 215 216 # evasdrop 217 my $buffer = ''; 218 my $chunk = ''; 219 my $message = ''; 220 while (1) { 221 # buffer up 222 if ($tls) { 223 $chunk = <$sock>; 224 } else { 225 $sock->recv($chunk, $CHUNK_LENGTH); 226 } 227 $chunk =~ /^([^\r\n]+)(\r\n)?([^\r\n]+)?$/; 228 # keep reading if chunk is empty 229 next if (not $1); 230 # if chunks isn't empty, check for framing point 231 if ($2) { 232 # if we found a framing point, flush buffer and text till framing point 233 $message = $buffer . $1; 234 if ($3) { 235 # if we have text after framing, make it the new content of buffer 236 $buffer = $3; 237 } else { 238 # if we have no text after framing, clear buffer 239 $buffer = ''; 240 } 241 } else { 242 # if there's no framing. append chunk to end of buffer and keep reading 243 $buffer .= $chunk; 244 next; 245 } 246 247 # log message 248 logger($message) if ($logging); 249 250 # respond to message 251 if ($message =~ /^PING :([^\000\r\n\ ]+)$/) { 252 # if we got a ping, pong back 253 out($sock, "PONG :$1"); 254 } elsif ($message =~ /^:([^\000\r\n\#\&\ ][^\000\r\n\ ]*)![^\000\r\n\ ]+@[^\000\r\n\ ]+ PRIVMSG ${chan} :([^\000\r\n]*)$/) { 255 # if we got a message to our chan. read and act accordingly 256 my $sender_nick = $1; 257 my $sender_message = $2; 258 unless (respond($sock, $sender_nick, $sender_message)) { 259 $subbuffer{$sender_nick} = $sender_message; 260 } 261 } 262 }