Additional IRC commands and bot tasks for EarwigBot https://en.wikipedia.org/wiki/User:EarwigBot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

159 rivejä
5.6 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com>
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy
  6. # of this software and associated documentation files (the "Software"), to deal
  7. # in the Software without restriction, including without limitation the rights
  8. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. # copies of the Software, and to permit persons to whom the Software is
  10. # furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. # SOFTWARE.
  22. from collections import deque
  23. from datetime import datetime
  24. from threading import Thread
  25. from earwigbot.commands import Command
  26. from earwigbot.irc import RC
  27. class RCMonitor(Command):
  28. """Monitors the recent changes feed for certain edits and reports them to a
  29. dedicated channel."""
  30. name = "rc_monitor"
  31. hooks = ["msg", "rc"]
  32. def setup(self):
  33. try:
  34. self._channel = self.config.commands[self.name]["channel"]
  35. except KeyError:
  36. self._channel = None
  37. log = ('Cannot use without a report channel set as '
  38. 'config.commands["{0}"]["channel"]')
  39. self.logger.warn(log.format(self.name))
  40. self._stats = {
  41. "start": datetime.utcnow(),
  42. "edits": 0,
  43. "hits": 0,
  44. "max_backlog": 0
  45. }
  46. self._levels = {}
  47. self._issues = {}
  48. self._descriptions = {}
  49. self._queue = deque()
  50. self._thread = Thread(target=self._callback, name="rc_monitor")
  51. self._thread.daemon = True
  52. self._thread.running = True
  53. self._prepare_reports()
  54. self._thread.start()
  55. def check(self, data):
  56. if not self._channel:
  57. return
  58. return isinstance(data, RC) or (
  59. data.is_command and data.command == self.name)
  60. def process(self, data):
  61. if isinstance(data, RC):
  62. newlen = len(self._queue) + 1
  63. self._queue.append(data)
  64. if newlen > self._stats["max_backlog"]:
  65. self._stats["max_backlog"] = newlen
  66. return
  67. if not self.config.irc["permissions"].is_admin(data):
  68. self.reply(data, "You must be a bot admin to use this command.")
  69. return
  70. since = self._stats["start"].strftime("%H:%M:%S, %d %B %Y")
  71. seconds = (datetime.utcnow() - self._stats["start"]).total_seconds()
  72. rate = self._stats["edits"] / seconds
  73. msg = ("\x02{edits:,}\x0F edits checked since {since} "
  74. "(\x02{rate:.2f}\x0F edits/sec); \x02{hits:,}\x0F hits; "
  75. "\x02{backlog:,}\x0F-edit backlog (\x02{max_backlog:,}\x0F "
  76. "max).")
  77. self.reply(data, msg.format(
  78. since=since, rate=rate, backlog=len(self._queue), **self._stats))
  79. def unload(self):
  80. self._thread.running = False
  81. self._queue.append(None)
  82. def _prepare_reports(self):
  83. """Set up internal tables for storing report information."""
  84. routine = 1
  85. urgent = 3
  86. self._levels = {
  87. routine: "routine",
  88. # ...
  89. }
  90. self._issues = {
  91. "random": routine,
  92. "random2": urgent,
  93. # ...
  94. }
  95. self._descriptions = {
  96. # ...
  97. }
  98. def _evaluate(self, event):
  99. """Return heuristic information about the given RC event."""
  100. issues = []
  101. # TODO
  102. from random import random
  103. rand = random()
  104. if rand < 0.05:
  105. issues.append("random")
  106. if rand < 0.01:
  107. issues.append("random2")
  108. # END TODO
  109. issues.sort(key=lambda issue: self._issues[issue], reverse=True)
  110. return issues
  111. def _format(self, rc, report):
  112. """Format a RC event for the report channel."""
  113. level = self._levels[max(self._issues[issue] for issue in report)]
  114. descr = ", ".join(self._descriptions[issue] for issue in report)
  115. notify = " ".join("!rcm-" + issue for issue in report)
  116. cmnt = rc.comment if len(rc.comment) <= 50 else rc.comment[:47] + "..."
  117. msg = ("[\x02{level}\x0F] ({descr}) [\x02{notify}\x0F]\x0306 * "
  118. "\x0314[[\x0307{title}\x0314]]\x0306 * \x0303{user}\x0306 * "
  119. "\x0302{url}\x0306 * \x0310{comment}")
  120. return msg.format(
  121. level=level, descr=descr, notify=notify, title=rc.page,
  122. user=rc.user, url=rc.url, comment=cmnt)
  123. def _handle_event(self, event):
  124. """Process a recent change event."""
  125. if not event.is_edit:
  126. return
  127. report = self._evaluate(event)
  128. self._stats["edits"] += 1
  129. if report:
  130. self.say(self._channel, self._format(event, report))
  131. self._stats["hits"] += 1
  132. def _callback(self):
  133. """Internal callback for the RC monitor thread."""
  134. while self._thread.running:
  135. event = self._queue.popleft()
  136. if not self._thread.running:
  137. break
  138. self._handle_event(event)