#! /usr/bin/perl # Copyright © 2023 Wessel Dankers # This work is free. You can redistribute it and/or modify it under the # terms of the Do What The Fuck You Want To Public License, Version 2, # as published by Sam Hocevar. See https://www.wtfpl.net for more details. # homsar:~wsl/mpd-bot , Jan 4 2016 # пет 18 08:36 < joostvb> Fruit: homsar:~wsl/mpd-bot , Jan 4 2016 : is dat # WTFPL licensed? # пет 18 09:11 < Fruit> joostvb: nu wel # # http://banach.uvt.nl:55555/ # MeukNet/#radio-g231 package MpdBot; use base qw(Bot::BasicBot); use warnings FATAL => 'all'; use strict; use utf8; use Net::INET6Glue::INET_is_INET6; use Encode; use IO::File; use IO::Socket::INET6; use Net::IRC; use Time::HiRes qw(setitimer); use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK); use Carp qw(cluck confess); FIXME stuff package main; $SIG{__DIE__} = sub { local $_ = shift; die $_ if ref $_; if(/^(.*) at \S+ line \d+\.?\n\z/a) { local $Carp::CarpLevel = 1; confess($1); } die $_; }; # my $server = 'freitag.oi'; my $server = 'irc.uvt.nl'; # my $chan = '#radio-g236'; my $chan = '#radio-g231'; my $nick = 'mpd-g231'; my $ping = 240; my $next_re = qr/^\@(?:-?e)?(?:next|skip|napster|a+r+g+h?l?|pijn|paniek|nee(?:e*|dank(?:je)?)?|no+(?:e*s)?|nah|barf|bagger|meh|kut|zap|saai|b[o0]ring|suf|live|bekhouwen?|kopdicht|stfu|(?:ge)?(?:zwets|lul|leuter|klets|blaat|gil|schreeuw|zeik|zanik)|grappig(?:stemmetje)?|prng(?:jazz)?|(?:lang)?ha(?:ar|rig(?:tuig)?)|(?:blah?)+|(?:tering)?herrie|vuvuzela(?:'?s)?|kaas|kazig|cheese|cheesy)(?:\s|$)/i; my $np_re = qr/^\@(?:np|wtf)(?:\s|$)/i; binmode(STDIN, ':utf8') or die "binmode(STDIN, :utf8): $!\n"; binmode(STDOUT, ':utf8') or die "binmode(STDIN, :utf8): $!\n"; binmode(STDERR, ':utf8') or die "binmode(STDIN, :utf8): $!\n"; # my $irc = new Net::IRC; my $srv; # until($srv = $irc->newconn(Nick => $nick, Server => $server)) { # 6697 ircs-u Internet Relay Chat via TLS/SSL # until($srv = $irc->newconn(Nick => $nick, Server => $server, Port => 6697)) { until($srv = MpdBot->new(nick => $nick, server => $server, port => 6697)) { sleep 5; } $srv->run(); sub reset_timer { return unless $ping > 0; setitimer(0, $ping, $ping); } $SIG{ALRM} = sub { $srv->disconnect if $srv->connected; $srv->connect }; reset_timer(); $srv->add_default_handler(\&reset_timer, 2); my @common_events = qw(nick quit join part mode topic kick public msg notice ping other invite umode error cping cversion csource ctime cdcc cuserinfo cclientinfo cerrmsg cfinger crping crversion crsource crtime cruserinfo crclientinfo crfinger); foreach my $eventtype (@common_events) { $srv->add_handler($eventtype, \&reset_timer, 2); } $srv->add_handler('disconnect', sub { setitimer(0, 1, 10) }, 2); $srv->add_handler('kill', sub { exit(2) }, 2); $srv->add_handler('leaving', sub { warn "leaving?\n" }, 2); my $joined; sub on_connect { my $self = shift; $self->join($chan); $joined = 1; } $srv->add_handler(endofmotd => \&on_connect, 2); sub utf8_testandset_inplace { foreach my $val (@_) { # upgrade to UTF-8 if possible, without changing any bytes Encode::_utf8_on($val); Encode::_utf8_off($val) unless utf8::valid($val); } return; } sub utf8_testandset { my $str = shift; return undef unless defined $str; utf8_testandset_inplace($str); return $str; } sub msg { reset_timer(); my ($dst, $msg) = @_; utf8_testandset_inplace($msg); utf8::encode($msg); return $srv->privmsg($dst, $msg); } sub topic { reset_timer(); my ($dst, $msg) = @_; utf8_testandset_inplace($msg); utf8::encode($msg); return $srv->topic($dst, $msg); } sub set_non_blocking { my $sock = shift; my $flags = fcntl($sock, F_GETFL, 0) or die "Can't get flags for the socket: $!\n"; fcntl($sock, F_SETFL, $flags | O_NONBLOCK) or die "Can't set flags for the socket: $!\n"; } my $mpd; sub close_mpd { if($mpd) { warn "closing socket\n"; $irc->removefh($mpd); close($mpd); undef $mpd; } } my $mpd_buf = ''; my $current = ''; my $mpd_idle; sub write_mpd_line { my $msg = shift; warn "> $msg\n"; $msg .= "\n"; while($msg ne '') { my $r = syswrite($mpd, $msg); unless($r || $!{EWOULDBLOCK}) { close_mpd(); return; } substr($msg, 0, $r, '') if $r; my $in = ''; vec($in, fileno($mpd), 1) = 1; unless(select(undef, $in, $in, 1)) { close_mpd(); return; } } } sub read_mpd_line { return utf8_testandset($1) if $mpd_buf =~ s/^([^\n]*)\n//; return undef unless $mpd; my $r = sysread $mpd, $mpd_buf, 65536, length($mpd_buf); unless($r) { close_mpd() if defined $r || !$!{EWOULDBLOCK}; return undef; } return utf8_testandset($1) if $mpd_buf =~ s/^([^\n]*)\n//; return undef; } sub read_mpd_line_blocking { for(;;) { my $line = read_mpd_line(); return $line if defined $line; return undef unless $mpd; my $in = ''; vec($in, fileno($mpd), 1) = 1; unless(select($in, undef, $in, 1)) { close_mpd(); return undef; } } } sub mpd_event { my $sock = shift; for(;;) { local $_ = read_mpd_line(); last unless defined; chomp; warn "< $_\n"; if(/^OK/) { if($mpd_idle) { undef $mpd_idle; write_mpd_line("currentsong"); } else { $mpd_idle = 1; write_mpd_line("idle player"); } } elsif(/^file: (.*)$/) { my $next = $1; if($next ne $current) { $current = $next; eval { topic($chan, $current // '') }; warn $@ if $@; } } } } sub on_action { reset_timer(); my (undef, $event) = @_; my ($to) = $event->to; my ($from) = $event->from; my ($msg) = $event->args; $from =~ s/\!.*//; utf8_testandset_inplace($msg); warn "$to * $from $msg\n"; } $srv->add_handler(caction => \&on_action, 2); sub on_public { reset_timer(); my (undef, $event) = @_; my ($to) = $event->to; my ($from) = $event->from; my ($msg) = $event->args; $from =~ s/\!.*//; utf8_testandset_inplace($msg); warn "$to <$from> $msg\n"; if($msg =~ $np_re) { msg($chan, "np: $current"); } elsif($msg =~ /^\@h[ea]lp(?:\s|$)/i) { msg($chan, "om het huidige nummer op te vragen: $np_re"); msg($chan, "om het huidige nummer te skippen: ^@.+"); } elsif($msg =~ /^\@zachter(?:\s|$)/i) { if($mpd_idle) { write_mpd_line("noidle"); return unless read_mpd_line_blocking(); } write_mpd_line('setvol "0"'); return unless read_mpd_line_blocking(); if($mpd_idle) { write_mpd_line("idle player"); } msg($chan, "Volume gereduceerd."); } elsif($msg =~ /^\@./) { if($mpd_idle) { write_mpd_line("noidle"); return unless read_mpd_line_blocking(); } write_mpd_line("next"); return unless read_mpd_line_blocking(); if($mpd_idle) { write_mpd_line("idle player"); } msg($chan, "skipped $current"); } } $srv->add_handler(public => \&on_public, 2); for(;;) { unless($mpd) { $mpd_idle = 1; $mpd_buf = ''; $mpd = new IO::Socket::INET6( PeerAddr => '::1', PeerPort => 6600, ); if($mpd) { if($mpd->connected) { set_non_blocking($mpd); $irc->addfh($mpd, \&mpd_event, 're'); $irc->timeout(undef); } else { close($mpd); undef $mpd; } } } $irc->timeout($mpd ? undef : 1); $irc->do_one_loop; $irc->flush_output_queue; }