text_trigger.pm 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. use Encode;
  2. use JSON;
  3. use POE;
  4. my %cache = ();
  5. my $find_trigger = sub {
  6. my $query = shift;
  7. return ($query, $cache{$query}) if (defined $cache{$query});
  8. my @matches = grep(/\Q$query\E/, keys %cache);
  9. return undef if (!@matches);
  10. @matches = sort { length($a) <=> length($b) } @matches;
  11. # We don't want to do partial matches for very short queries
  12. return undef if ($matches[0] ne $query && length($query) < 3);
  13. return ($matches[0], $cache{$matches[0]});
  14. };
  15. my $cache_entry = sub {
  16. if (!defined $_[1]) {
  17. delete $cache{$_[0]};
  18. } else {
  19. $cache{$_[0]} = $_[1];
  20. }
  21. };
  22. my %last;
  23. # Ugly recoding hack to work around double encoding somehow caused by Perl+JSON
  24. my $json_encode = sub {
  25. encode('iso-8859-1', to_json(shift, {canonical => 1}));
  26. };
  27. {
  28. schemata => {
  29. 0 => [
  30. "CREATE TABLE tt_triggers (trigger TEXT NOT NULL,
  31. lock INTEGER NOT NULL DEFAULT 0)",
  32. "CREATE TABLE tt_trigger_contents (tc_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  33. trigger TEXT NOT NULL,
  34. exp TEXT NOT NULL,
  35. approved INT NOT NULL DEFAULT 1,
  36. changed_by TEXT NOT NULL,
  37. changed_at INT NOT NULL DEFAULT CURRENT_TIMESTAMP)",
  38. ],
  39. 1 => [
  40. "ALTER TABLE tt_triggers ADD COLUMN
  41. deleted INTEGER NOT NULL DEFAULT 0"
  42. ],
  43. },
  44. on_load => sub {
  45. my $res = $BotDb::db->selectall_arrayref("SELECT trigger, exp FROM tt_trigger_contents WHERE approved=1 ORDER BY changed_at DESC", {Slice => {}});
  46. for (@$res) {
  47. next if (exists $cache{$_->{trigger}});
  48. $cache_entry->($_->{trigger}, $_->{exp});
  49. }
  50. },
  51. control_commands => {
  52. trigger_list => sub {
  53. my ($client, $data, @args) = @_;
  54. BotCtl::send($client, "ok", $json_encode->(\%cache));
  55. },
  56. trigger_history => sub {
  57. my ($client, $data, @args) = @_;
  58. my $res = $BotDb::db->selectall_arrayref("SELECT tc_id, exp, changed_by, changed_at FROM tt_triggers NATURAL JOIN tt_trigger_contents WHERE approved=1 AND trigger=? ORDER BY changed_at DESC", {Slice => {}}, $args[0]);
  59. BotCtl::send($client, "ok", $json_encode->($res));
  60. },
  61. trigger_recentchanges => sub {
  62. my ($client, $data, @args) = @_;
  63. my $res = $BotDb::db->selectall_arrayref("SELECT trigger, exp, changed_by, changed_at FROM tt_triggers NATURAL JOIN tt_trigger_contents WHERE approved=1 AND deleted=0 ORDER BY changed_at DESC", {Slice => {}});
  64. BotCtl::send($client, "ok", $json_encode->($res));
  65. },
  66. trigger_edit => sub {
  67. my ($client, $data, @args) = @_;
  68. &BotCtl::require_user or return;
  69. if (BotDb::has_priv($data->{level}, 'no_trigger_edit')) {
  70. BotCtl::send($client, "denied");
  71. return;
  72. }
  73. my $res = $BotDb::db->selectrow_hashref("SELECT * FROM tt_triggers WHERE trigger=?", {}, $args[0]);
  74. if (!defined $res) {
  75. if (defined $BotDb::db->err) {
  76. BotCtl::send($client, "error", "db_error", $BotDb::db->errstr);
  77. BotIrc::error("text_trigger: fetching trigger info for $args[0]: $BotDb::db->errstr");
  78. return;
  79. }
  80. BotCtl::send($client, "doesntexist");
  81. return;
  82. }
  83. if ($res->{lock} && !BotDb::has_priv($data->{level}, 'trigger_edit_locked')) {
  84. BotCtl::send($client, "locked");
  85. return;
  86. }
  87. $BotDb::db->do("INSERT INTO tt_trigger_contents (trigger, exp, changed_by) VALUES(?, ?, ?)", {}, $args[0], $args[1], $data->{level});
  88. if ($res->{deleted}) {
  89. $BotDb::db->do("UPDATE tt_triggers SET deleted = 0 WHERE trigger=?", {}, $res->{trigger});
  90. }
  91. $cache_entry->($args[0], $args[1]);
  92. BotCtl::send($client, "ok");
  93. },
  94. trigger_revert => sub {
  95. my ($client, $data, @args) = @_;
  96. &BotCtl::require_user or return;
  97. if (BotDb::has_priv($data->{level}, 'no_trigger_edit')) {
  98. BotCtl::send($client, "denied");
  99. }
  100. my $res = $BotDb::db->selectrow_hashref("SELECT * FROM tt_triggers NATURAL JOIN tt_trigger_contents WHERE tc_id=?", {}, $args[0]);
  101. if (!defined $res) {
  102. if (defined $BotDb::db->err) {
  103. BotCtl::send($client, "error", "db_error", $BotDb::db->errstr);
  104. BotIrc::error("text_trigger: fetching trigger info for $args[0]: $BotDb::db->errstr");
  105. return;
  106. }
  107. BotCtl::send($client, "doesntexist");
  108. return;
  109. }
  110. if ($res->{lock} && !BotDb::has_priv($data->{level}, 'trigger_edit_locked')) {
  111. BotCtl::send($client, "locked");
  112. return;
  113. }
  114. $BotDb::db->do("INSERT INTO tt_trigger_contents (trigger, exp, changed_by) VALUES(?, ?, ?)", {}, $res->{trigger}, $res->{exp}, $data->{level});
  115. if ($res->{deleted}) {
  116. $BotDb::db->do("UPDATE tt_triggers SET deleted = 0 WHERE trigger=?", {}, $res->{trigger});
  117. }
  118. $cache_entry->($res->{trigger}, $res->{exp});
  119. BotCtl::send($client, "ok");
  120. }
  121. },
  122. irc_commands => {
  123. trigger_edit => sub {
  124. my ($source, $targets, $args, $auth) = @_;
  125. BotIrc::check_ctx(authed => 1) or return;
  126. my ($trigger, $exp) = split(/\s+/, $args, 2);
  127. if (!$trigger || !$exp) {
  128. BotIrc::send_noise("Syntax: .trigger_edit <name> <contents>");
  129. return 1;
  130. }
  131. if ($trigger =~ /[^a-z0-9_.-]/i) {
  132. BotIrc::send_noise("Valid trigger names must consist of [a-zA-Z0-9_.-]");
  133. return 1;
  134. }
  135. BotIrc::check_ctx(antipriv => 'no_trigger_edit') or return;
  136. my $res = $BotDb::db->selectrow_hashref("SELECT * FROM tt_triggers WHERE trigger=?", {}, $trigger);
  137. if (!defined $res) {
  138. if (defined $BotDb::db->err) {
  139. BotIrc::send_noise("Uh-oh... something went wrong. Maybe this helps: $BotDb::db->errstr");
  140. BotIrc::error("text_trigger: fetching trigger info for $trigger: $BotDb::db->errstr");
  141. return 1;
  142. }
  143. # New trigger!
  144. BotIrc::check_ctx(priv => 'trigger_add') or return;
  145. $BotDb::db->do("INSERT INTO tt_triggers (trigger) VALUES(?)", {}, $trigger);
  146. }
  147. if ($res->{lock}) {
  148. BotIrc::check_ctx(priv => 'trigger_edit_locked') or return;
  149. }
  150. if ($exp eq '-') {
  151. BotIrc::check_ctx(priv => 'trigger_delete') or return;
  152. $BotDb::db->do("UPDATE tt_triggers SET deleted = 1 WHERE trigger=?", {}, $trigger);
  153. $cache_entry->($trigger, undef);
  154. } else {
  155. $BotDb::db->do("INSERT INTO tt_trigger_contents (trigger, exp, changed_by) VALUES(?, ?, ?)", {}, $trigger, $exp, $source);
  156. if ($res->{deleted}) {
  157. $BotDb::db->do("UPDATE tt_triggers SET deleted = 0 WHERE trigger=?", {}, $res->{trigger});
  158. }
  159. $cache_entry->($trigger, $exp);
  160. }
  161. BotIrc::send_noise("Okay.");
  162. }
  163. },
  164. irc_on_anymsg => sub {
  165. BotIrc::check_ctx(wisdom_auto_redirect => 1) or return 1;
  166. TRIGGERS: while ($_[ARG2] =~ /(?:^|[\s(){}\[\]])!([a-z0-9_.-]+)(\@[p*])?/ig) {
  167. my $query = $1;
  168. my $as_private = $2;
  169. my ($trigger, $exp);
  170. # This construct keeps removing trailing dots until a
  171. # match is found (or no further dots can be removed).
  172. # This is done so that punctuation can run into trigger
  173. # names without causing problems.
  174. while (1) {
  175. ($trigger, $exp) = $find_trigger->($query);
  176. last if (defined $trigger);
  177. next TRIGGERS if (!($query =~ s/\.$//));
  178. }
  179. if ($exp =~ /^\@!([a-z0-9_.-]+)$/i) {
  180. ($trigger, $exp) = $find_trigger->($1);
  181. }
  182. next if $exp =~ m(^\@/dev/null(?:\s+\(.*\)|)$);
  183. next if !defined $trigger;
  184. my $trigger_exp = "";
  185. $trigger_exp = "[!$trigger] " if $trigger ne $query;
  186. BotIrc::ctx_set_addressee(undef) if defined $as_private && $as_private =~ /\*/;
  187. BotIrc::ctx_redirect_to_addressee() if defined $as_private && $as_private =~ /p/;
  188. # Squelch duplicate messages
  189. my $target = BotIrc::ctx_target('wisdom');
  190. my $last = $last{$target};
  191. next if $last && $last->[0] eq $trigger && (time < ($last->[1]+10));
  192. $last{$target} = [$trigger, scalar time];
  193. BotIrc::send_wisdom("$trigger_exp$exp");
  194. }
  195. return 0;
  196. },
  197. };