gitinfo-irc.pl 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. #!/usr/bin/perl
  2. package BotIrc;
  3. use common::sense;
  4. use Carp;
  5. use JSON;
  6. use File::Slurp;
  7. use POE;
  8. use POE::Component::IRC::State;
  9. use POE::Component::IRC::Plugin::AutoJoin;
  10. use POE::Component::IRC::Plugin::Connector;
  11. use POE::Component::IRC::Plugin::NickServID;
  12. use POE::Component::Client::DNS;
  13. use POE::Component::Client::HTTP;
  14. use POE::Component::Server::TCP;
  15. use POE::Filter::Line;
  16. use Socket;
  17. use Socket6;
  18. use lib '.';
  19. use control;
  20. use db;
  21. use http;
  22. use plugin;
  23. our $config;
  24. our $config_file = 'config.json';
  25. $config_file = $ARGV[0] if (defined($ARGV[0]) && -f $ARGV[0]);
  26. our $kernel;
  27. our %heap;
  28. my %handlers = ();
  29. my %handler_ctx = ();
  30. sub read_config {
  31. $config = read_file($config_file) or fatal("Config file `$config_file' missing: $!");
  32. $config = decode_json($config);
  33. if (ref($config->{channel}) eq '') {
  34. $config->{channel} = [$config->{channel}];
  35. }
  36. my %chans;
  37. $chans{lc $_} = undef for @{$config->{channel}};
  38. $config->{channel} = \%chans;
  39. }
  40. read_config();
  41. sub msg {
  42. print STDERR "[".localtime."] ".shift."\n";
  43. }
  44. sub info { msg("[INFO] ".shift); }
  45. sub warn { msg("[WARNING] ".shift); }
  46. sub error { msg("[ERROR] ".shift); }
  47. sub fatal { msg("[FATAL] ".shift); exit(1); }
  48. BotDb::init();
  49. BotCtl::init();
  50. BotPlugin::init();
  51. our $irc = POE::Component::IRC::State->spawn(
  52. 'alias' => "IRC",
  53. 'Server' => $config->{server},
  54. 'Port' => ($config->{port} // 6667),
  55. 'Nick' => $config->{nick},
  56. 'Username' => $config->{username},
  57. 'Ircname' => $config->{realname},
  58. 'Password' => $config->{server_password},
  59. 'LocalAddr' => $config->{local_addr},
  60. 'useipv6' => $config->{ipv6},
  61. 'Raw' => 1,
  62. );
  63. POE::Component::Client::HTTP->spawn(
  64. Alias => 'http',
  65. Timeout => 30,
  66. MaxSize => 1_000_000,
  67. FollowRedirects => 2,
  68. );
  69. if ($config->{control_enabled}) {
  70. POE::Component::Server::TCP->new(
  71. Address => $config->{control_addr},
  72. Domain => $config->{control_ipv6} ? AF_INET6 : AF_INET,
  73. Alias => "ControlServer",
  74. Port => $config->{control_port},
  75. Started => sub { info "Control server started."; },
  76. ClientFilter => POE::Filter::Line->new(Literal => "\012"),
  77. ClientConnected => \&BotCtl::on_connected,
  78. ClientDisconnected => \&BotCtl::on_disconnected,
  79. ClientInput => \&BotCtl::on_input,
  80. );
  81. }
  82. our $session = POE::Session->create(
  83. inline_states => {
  84. _start => \&main_start,
  85. },
  86. heap => {
  87. irc => $irc,
  88. },
  89. );
  90. sub main_start {
  91. $kernel = $_[KERNEL];
  92. BotHttp::init();
  93. $irc->plugin_add('AutoJoin', POE::Component::IRC::Plugin::AutoJoin->new(Channels => $config->{channel}));
  94. $irc->plugin_add('Connector', POE::Component::IRC::Plugin::Connector->new());
  95. $irc->plugin_add('NickServID', POE::Component::IRC::Plugin::NickServID->new(Password => $config->{nick_pwd})) if defined $config->{nick_pwd};
  96. add_handler('irc_socketerr', 'core', sub {
  97. error("IRC: socket error while connecting: ". $_[ARG0]);
  98. return 0;
  99. });
  100. add_handler('irc_error', 'core', sub {
  101. warn("IRC: general error: ". $_[ARG0]);
  102. return 0;
  103. });
  104. add_handler('irc_connected', 'core', sub {
  105. info("IRC: connected to ". $_[ARG0]);
  106. return 0;
  107. });
  108. add_handler('irc_disconnected', 'core', sub {
  109. warn("IRC: disconnected from ". $_[ARG0]);
  110. return 0;
  111. });
  112. add_handler('irc_public', 'core', \&on_irc_public);
  113. add_handler('irc_msg', 'core', \&on_irc_msg);
  114. for (@{$config->{autoload_plugins}}) {
  115. BotPlugin::load($_);
  116. }
  117. $irc->yield(register => 'all');
  118. $irc->yield(connect => {});
  119. return;
  120. }
  121. POE::Kernel->run();
  122. sub nickonly {
  123. my $n = shift;
  124. $n =~ s/^([^!]+).*/$1/g;
  125. return $n;
  126. }
  127. sub return_path {
  128. my $source = nickonly(shift);
  129. my $targets = shift;
  130. my @targets = ();
  131. if (ref($targets) eq 'ARRAY') {
  132. @targets = @$targets;
  133. }
  134. return $source if (grep { lc($irc->nick_name()) eq lc($_) } @targets);
  135. my @chan_targets = grep { exists($config->{channel}{lc $_}) } @targets;
  136. return undef unless @chan_targets;
  137. # Just ignore additional channels, if any
  138. return $chan_targets[0];
  139. }
  140. sub msg_or_notice($$) {
  141. my ($target, $msg) = @_;
  142. my $method = ($target =~ /^#/) ? 'privmsg' : 'notice';
  143. $irc->yield($method => $target => $msg);
  144. }
  145. sub noisy_check_priv($$$$) {
  146. my ($rpath, $nick, $priv, $authed) = @_;
  147. return 0 if (!noisy_command_authed($rpath, $nick, $authed));
  148. if (!BotDb::has_priv($nick, $priv)) {
  149. $irc->yield(privmsg => $rpath, "$nick: you are not authorised to perform this action ($priv).");
  150. return 0;
  151. }
  152. return 1;
  153. }
  154. sub noisy_check_antipriv($$$$) {
  155. my ($rpath, $nick, $priv, $authed) = @_;
  156. my $account = $authed ? $nick : '!guest';
  157. if (BotDb::has_priv($account, $priv)) {
  158. $irc->yield(privmsg => $rpath, "$nick: you are not authorised to perform this action (due to $priv).");
  159. return 0;
  160. }
  161. return 1;
  162. }
  163. sub noisy_command_authed($$$) {
  164. my ($rpath, $nick, $authed) = @_;
  165. if (!$authed) {
  166. $irc->yield(privmsg => $rpath, "$nick: you must be logged in to use this command.");
  167. }
  168. return $authed;
  169. }
  170. # Convenience methods for handlers {{{
  171. # Called by core stuff that calls handlers; initializes the handler ctx with
  172. # information about which return paths are available in principle. This will
  173. # later be matched against what kinds of return paths a handler requires and
  174. # generate errors if necessary.
  175. sub prepare_ctx_targets($$$$) {
  176. my ($source, $target, $msg, $authed) = @_;
  177. my $rpath = return_path($source, $target);
  178. $rpath = undef if lc($rpath) eq lc($source);
  179. %handler_ctx = (
  180. user => $source,
  181. channel => $rpath,
  182. line => $msg,
  183. authed => $authed,
  184. no_setup => 1,
  185. );
  186. }
  187. # This is the counterpart to prepare_irc_targets. It's called by handlers to
  188. # perform priv/target checks according to the handler's specs.
  189. # Yay function name overload!
  190. sub check_ctx(%) {
  191. my %cfg = @_;
  192. my $authed = $handler_ctx{authed};
  193. my $source = $handler_ctx{user};
  194. my $channel = $handler_ctx{channel};
  195. my $account = $authed ? $source : '!guest';
  196. if (!%handler_ctx) {
  197. carp("Trying to use uninitialized handler ctx");
  198. return 0;
  199. }
  200. # First, determine where to send replies to...
  201. # ... but reuse established values if this isn't the first invocation
  202. # in the current run of the handler
  203. my ($noise_prefer_channel, $wisdom_prefer_channel);
  204. if ($handler_ctx{no_setup}) {
  205. $noise_prefer_channel = $cfg{noise_public} // $config->{replies_public};
  206. $wisdom_prefer_channel = $cfg{wisdom_public} // 1;
  207. } else {
  208. $noise_prefer_channel = $cfg{noise_public} // ($handler_ctx{noise_target} eq $channel);
  209. $wisdom_prefer_channel = $cfg{wisdom_public} // ($handler_ctx{wisdom_target} eq $channel);
  210. }
  211. if ($noise_prefer_channel && $channel) {
  212. $handler_ctx{noise_type} = 'privmsg';
  213. $handler_ctx{noise_target} = $channel;
  214. } else {
  215. $handler_ctx{noise_type} = 'notice';
  216. $handler_ctx{noise_target} = $source;
  217. }
  218. if ($wisdom_prefer_channel && $channel) {
  219. $handler_ctx{wisdom_type} = 'privmsg';
  220. $handler_ctx{wisdom_target} = $channel;
  221. } else {
  222. $handler_ctx{wisdom_type} = 'notice';
  223. $handler_ctx{wisdom_target} = $source;
  224. }
  225. if (exists $cfg{wisdom_addressee}) {
  226. ctx_set_addressee($cfg{wisdom_addressee});
  227. } else {
  228. # We can do this by default without clashing with commands
  229. # because both only match at the start of the line and a
  230. # command isn't a valid nickname nor vice versa
  231. ctx_set_addressee('!auto');
  232. }
  233. # Don't auto-redirect by default since it might clash with command
  234. # syntax
  235. ctx_auto_redirect($handler_ctx{wisdom_auto_redirect} // 0);
  236. delete $handler_ctx{no_setup};
  237. if (($cfg{authed} || defined($cfg{priv})) && !$handler_ctx{authed}) {
  238. send_noise("You must be logged into NickServ in order to use this command.");
  239. return 0;
  240. }
  241. if (defined $cfg{priv}) {
  242. $cfg{priv} = [$cfg{priv}] if ref($cfg{priv}) eq '';
  243. for (@{$cfg{priv}}) {
  244. if (!BotDb::has_priv($account, $_)) {
  245. send_noise("You are not authorised to perform this action ($_).");
  246. return 0;
  247. }
  248. }
  249. }
  250. if (defined $cfg{antipriv}) {
  251. $cfg{antipriv} = [$cfg{antipriv}] if ref($cfg{antipriv}) eq '';
  252. for (@{$cfg{antipriv}}) {
  253. if (BotDb::has_priv($account, $_)) {
  254. send_noise("You are not authorised to perform this action (due to $_).");
  255. return 0;
  256. }
  257. }
  258. }
  259. return 1;
  260. }
  261. # There are two types of messages sent from handlers: noise and wisdom. Noise
  262. # is stuff like "command successful" or "you lack privileges". Wisdom is stuff
  263. # like "here's the data you requested".
  264. sub send_noise($;$) {
  265. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  266. my $noise = shift;
  267. if (!%$ctx) {
  268. carp("A handler tried to send this noise without valid ctx: $noise");
  269. return;
  270. }
  271. if ($ctx->{no_setup}) {
  272. carp("Handler sending noise without ctx constraints: $noise -> $ctx->{source}");
  273. }
  274. # In channels, address user
  275. $noise = "$ctx->{user}: $noise" if ($ctx->{noise_type} eq 'privmsg');
  276. $irc->yield($ctx->{noise_type} => $ctx->{noise_target} => $noise);
  277. }
  278. sub send_wisdom($;$) {
  279. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  280. my $wisdom = shift;
  281. if (!%$ctx) {
  282. carp("A handler tried to send this wisdom without valid ctx: $wisdom");
  283. return;
  284. }
  285. if ($ctx->{no_setup}) {
  286. carp("Handler sending wisdom without ctx constraints: $wisdom -> $ctx->{source}");
  287. }
  288. my $a = $ctx->{wisdom_addressee};
  289. my $address = "";
  290. $address = "$a: " if (defined $a && ctx_target_has_member($ctx, $a));
  291. $irc->yield($ctx->{wisdom_type} => $ctx->{wisdom_target} => ($address.$wisdom));
  292. }
  293. # Choose who to address in public wisdom. Set to undef to disable or '!auto'
  294. # to enable black magic.
  295. # Note that this is handled during check_ctx, too, and defaults to black magic
  296. # there.
  297. sub ctx_set_addressee($;$) {
  298. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  299. my $a = shift;
  300. if ($a eq '!auto') {
  301. $a = undef;
  302. if ($ctx->{line} && $ctx->{line} =~ /^
  303. ([\w\[\]\{\}\\\|`^{}-]+) # nick (broad match)
  304. (?:[,:]|\s-+) # separator
  305. \s+/ix) {
  306. $a = $1;
  307. }
  308. }
  309. $ctx->{wisdom_addressee} = $a;
  310. }
  311. sub ctx_addressee(;$) {
  312. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  313. return $ctx->{"wisdom_addressee"};
  314. }
  315. sub ctx_target($;$) {
  316. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  317. return $ctx->{shift."_target"};
  318. }
  319. sub ctx_source(;$) {
  320. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  321. return $ctx->{user};
  322. }
  323. # This works for wisdom only! Noise should only go to the actual source of a
  324. # message, really.
  325. sub ctx_target_has_member($;$) {
  326. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  327. my $target = $ctx->{wisdom_target};
  328. return 0 if $target !~ /^#/;
  329. return $irc->is_channel_member($target, shift);
  330. }
  331. sub ctx_redirect_to_channel($;$$) {
  332. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  333. my ($type, $channel) = @_;
  334. if (exists($config->{channel}{lc $channel})) {
  335. $ctx->{"${type}_target"} = $channel;
  336. $ctx->{"${type}_type"} = 'privmsg';
  337. return 1;
  338. }
  339. return 0;
  340. }
  341. sub ctx_redirect_to_addressee {
  342. my $ctx = (ref($_[0]) eq 'HASH') ? shift: \%handler_ctx;
  343. $ctx->{wisdom_target} = $ctx->{wisdom_addressee} // $ctx->{user};
  344. $ctx->{wisdom_type} = 'notice';
  345. }
  346. # Will redirect wisdom caused by private requests into a channel if the
  347. # request contains "to:#thechannel" and #thechannel is known to us.
  348. sub ctx_auto_redirect(;$) {
  349. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  350. if ($ctx->{line} && !ctx_can_target_channel($ctx) && $ctx->{line} =~ /\bto:(#[\S]+)/) {
  351. ctx_redirect_to_channel($ctx, 'wisdom', $1);
  352. }
  353. }
  354. # Was the original message addressed to a known channel?
  355. # This ignores redirections.
  356. sub ctx_can_target_channel(;$) {
  357. my $ctx = (ref($_[0]) eq 'HASH') ? shift : \%handler_ctx;
  358. return defined($ctx->{channel});
  359. }
  360. # Get a copy of the current ctx, for use in async scripts
  361. sub ctx_frozen() {
  362. return {%handler_ctx};
  363. }
  364. # }}}
  365. sub on_irc_public {
  366. my $nick = nickonly($_[ARG0]);
  367. return 1 if ($config->{hardcore_ignore} && BotDb::has_priv($nick, 'no_react'));
  368. return 0 if ($_[ARG2] !~ /^\.([a-z_]+)\s*(.*)$/);
  369. return BotPlugin::maybe_irc_command($nick, $_[ARG1], lc($1), $2, $_[ARG3]);
  370. }
  371. sub on_irc_msg {
  372. my $nick = nickonly($_[ARG0]);
  373. return 1 if ($config->{hardcore_ignore} && BotDb::has_priv($nick, 'no_react'));
  374. return 0 if ($_[ARG2] !~ /^\.([a-z_]+)\s*(.*)$/);
  375. return BotPlugin::maybe_irc_command($nick, $_[ARG1], lc($1), $2, $_[ARG3]);
  376. }
  377. sub add_handler($$$) {
  378. my ($ev, $origin, $code) = @_;
  379. if ($ev eq "irc_anymsg") {
  380. &add_handler("irc_msg", $origin, $code);
  381. &add_handler("irc_public", $origin, $code);
  382. return;
  383. }
  384. if (!exists $handlers{$ev}) {
  385. $handlers{$ev} = [];
  386. $kernel->state($ev, sub {
  387. for (@{$handlers{$ev}}) {
  388. # Prepare handler ctx
  389. my ($authed, $msg) = (0, "");
  390. if ($ev =~ /^irc_(?:msg|public)$/) {
  391. $authed = $_[ARG3];
  392. $msg = $_[ARG2];
  393. }
  394. prepare_ctx_targets(nickonly($_[ARG0]), $_[ARG1], $msg, $authed);
  395. my $res = $_->{code}(@_);
  396. %handler_ctx = ();
  397. last if ($res);
  398. }
  399. });
  400. }
  401. push @{$handlers{$ev}}, { origin => $origin, code => $code };
  402. }
  403. sub remove_handlers($) {
  404. my $origin = shift;
  405. for my $h (keys %handlers) {
  406. @{$handlers{$h}} = grep { $_->{origin} ne $origin } @{$handlers{$h}};
  407. $kernel->state($h) if (!@{$handlers{$h}});
  408. }
  409. }