mata_bot

some cheeky bot for #unix_surrealism
git clone https://git.pastanoggin.com/mata_bot.git
Log | Files | Refs | README | LICENSE

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 }