diff --git a/README.md b/README.md index 197e139..f6fdf14 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ IRC Commands [GitPython](http://packages.python.org/GitPython), which can be installed with `pip install GitPython`. +- **lta_monitor**: monitors for LTAs. No further information is available. + - **praise**: adds a simple way for the bot respond to ad-hoc commands based on entries in `praise`'s config (in the `"praises"` dictionary). Its original intention was to implement silly "easter eggs" praising certain users; for @@ -37,6 +39,11 @@ IRC Commands `"praises"` with the key `"earwig"` and the value `"Earwig is the bestest Python programmer ever!"`. +- **rc_monitor**: monitors the recent changes feed for certain edits and + reports them to a dedicated channel. The channel is stored under the + `"channel"` key in the command's dictionary. Reportable edits match various + heuristics, like urgent speedy taggings. + - **stars**: gets the number of stargazers for (i.e., people starring) a given [GitHub](https://github.com/) repository. diff --git a/commands/lta_monitor.py b/commands/lta_monitor.py new file mode 100644 index 0000000..62bbceb --- /dev/null +++ b/commands/lta_monitor.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2015 Ben Kurtovic +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import re + +from earwigbot.commands import Command +from earwigbot.exceptions import APIError + +class LTAMonitor(Command): + """Monitors for LTAs. No further information is available.""" + name = "lta_monitor" + hooks = ["join", "part"] + + def setup(self): + try: + config = self.config.commands[self.name] + self._monitor_chan = config["monitorChannel"] + self._report_chan = config["reportChannel"] + except KeyError: + self._monitor_chan = self._report_chan = None + self.logger.warn("Cannot use without being properly configured") + self._recent = [] + self._recent_max = 10 + + def check(self, data): + return (self._monitor_chan and self._report_chan and + data.chan == self._monitor_chan) + + def process(self, data): + if not data.host.startswith("gateway/web/"): + return + match = re.search(r"/ip\.(.*?)$", data.host) + if not match: + return + ip = match.group(1) + if ip in self._recent: + return + self._recent.append(ip) + if len(self._recent) > self._recent_max: + self._recent.pop(0) + + site = self.bot.wiki.get_site() + try: + result = site.api_query(action="query", list="blocks", bkip=ip, bklimit=1) + except APIError: + return + blocks = result["query"]["blocks"] + if not blocks: + return + + msg = ("\x02[Alert]\x0F Joined user \x02{nick}\x0F is IP-blocked " + "on-wiki ([[User:{user}]] by {by}) because: {reason}") + self.say(self._report_chan, msg.format(nick=data.nick, **blocks[0])) diff --git a/commands/rc_monitor.py b/commands/rc_monitor.py new file mode 100644 index 0000000..82c464e --- /dev/null +++ b/commands/rc_monitor.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2016 Ben Kurtovic +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from collections import deque +from datetime import datetime +from threading import Thread + +from earwigbot.commands import Command +from earwigbot.irc import RC + +class RCMonitor(Command): + """Monitors the recent changes feed for certain edits and reports them to a + dedicated channel.""" + name = "rc_monitor" + hooks = ["msg", "rc"] + + def setup(self): + try: + self._channel = self.config.commands[self.name]["channel"] + except KeyError: + self._channel = None + log = ('Cannot use without a report channel set as ' + 'config.commands["{0}"]["channel"]') + self.logger.warn(log.format(self.name)) + + self._stats = { + "start": datetime.utcnow(), + "edits": 0, + "hits": 0, + "max_backlog": 0 + } + self._levels = {} + self._issues = {} + self._descriptions = {} + self._queue = deque() + + self._thread = Thread(target=self._callback, name="rc_monitor") + self._thread.daemon = True + self._thread.running = True + self._prepare_reports() + self._thread.start() + + def check(self, data): + if not self._channel: + return + return isinstance(data, RC) or ( + data.is_command and data.command == self.name) + + def process(self, data): + if isinstance(data, RC): + newlen = len(self._queue) + 1 + self._queue.append(data) + if newlen > self._stats["max_backlog"]: + self._stats["max_backlog"] = newlen + return + + if not self.config.irc["permissions"].is_admin(data): + self.reply(data, "You must be a bot admin to use this command.") + return + + since = self._stats["start"].strftime("%H:%M:%S, %d %B %Y") + seconds = (datetime.utcnow() - self._stats["start"]).total_seconds() + rate = self._stats["edits"] / seconds + msg = ("\x02{edits:,}\x0F edits checked since {since} " + "(\x02{rate:.2f}\x0F edits/sec); \x02{hits:,}\x0F hits; " + "\x02{backlog:,}\x0F-edit backlog (\x02{max_backlog:,}\x0F " + "max).") + self.reply(data, msg.format( + since=since, rate=rate, backlog=len(self._queue), **self._stats)) + + def unload(self): + self._thread.running = False + self._queue.append(None) + + def _prepare_reports(self): + """Set up internal tables for storing report information.""" + routine = 1 + urgent = 3 + + self._levels = { + routine: "routine", + # ... + } + self._issues = { + "random": routine, + "random2": urgent, + # ... + } + self._descriptions = { + # ... + } + + def _evaluate(self, event): + """Return heuristic information about the given RC event.""" + issues = [] + + # TODO + from random import random + rand = random() + if rand < 0.05: + issues.append("random") + if rand < 0.01: + issues.append("random2") + # END TODO + + issues.sort(key=lambda issue: self._issues[issue], reverse=True) + return issues + + def _format(self, rc, report): + """Format a RC event for the report channel.""" + level = self._levels[max(self._issues[issue] for issue in report)] + descr = ", ".join(self._descriptions[issue] for issue in report) + notify = " ".join("!rcm-" + issue for issue in report) + cmnt = rc.comment if len(rc.comment) <= 50 else rc.comment[:47] + "..." + + msg = ("[\x02{level}\x0F] ({descr}) [\x02{notify}\x0F]\x0306 * " + "\x0314[[\x0307{title}\x0314]]\x0306 * \x0303{user}\x0306 * " + "\x0302{url}\x0306 * \x0310{comment}") + return msg.format( + level=level, descr=descr, notify=notify, title=rc.page, + user=rc.user, url=rc.url, comment=cmnt) + + def _handle_event(self, event): + """Process a recent change event.""" + if not event.is_edit: + return + report = self._evaluate(event) + self._stats["edits"] += 1 + if report: + self.say(self._channel, self._format(event, report)) + self._stats["hits"] += 1 + + def _callback(self): + """Internal callback for the RC monitor thread.""" + while self._thread.running: + event = self._queue.popleft() + if not self._thread.running: + break + self._handle_event(event)