A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

320 linhas
12 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2015 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 ast import literal_eval
  23. from earwigbot.commands import Command
  24. from earwigbot.irc import RC
  25. class Stalk(Command):
  26. """Stalk a particular user (!stalk/!unstalk) or page (!watch/!unwatch) for
  27. edits. Applies to the current bot session only."""
  28. name = "stalk"
  29. commands = ["stalk", "watch", "unstalk", "unwatch", "stalks", "watches",
  30. "allstalks", "allwatches", "unstalkall", "unwatchall"]
  31. hooks = ["msg", "rc"]
  32. MAX_STALKS_PER_USER = 10
  33. def setup(self):
  34. self._users = {}
  35. self._pages = {}
  36. self._load_stalks()
  37. def check(self, data):
  38. if isinstance(data, RC):
  39. return True
  40. if data.is_command and data.command in self.commands:
  41. return True
  42. return False
  43. def process(self, data):
  44. if isinstance(data, RC):
  45. return self._process_rc(data)
  46. data.is_admin = self.config.irc["permissions"].is_admin(data)
  47. if data.command.startswith("all"):
  48. if data.is_admin:
  49. self.reply(data, self._all_stalks())
  50. else:
  51. self.reply(data, "You must be a bot admin to view all stalked "
  52. "users or watched pages. View your own with "
  53. "\x0306!stalks\x0F.")
  54. return
  55. if data.command.endswith("all"):
  56. if not data.is_admin:
  57. self.reply(data, "You must be a bot admin to unstalk a user "
  58. "or unwatch a page for all users.")
  59. return
  60. if not data.args:
  61. self.reply(data, "You must give a user to unstalk or a page "
  62. "to unwatch. View all active with "
  63. "\x0306!allstalks\x0F.")
  64. return
  65. if not data.args or data.command in ["stalks", "watches"]:
  66. self.reply(data, self._current_stalks(data.nick))
  67. return
  68. target = " ".join(data.args).replace("_", " ")
  69. if target.startswith("[[") and target.endswith("]]"):
  70. target = target[2:-2]
  71. if target.startswith("User:") and "stalk" in data.command:
  72. target = target[5:]
  73. target = target[0].upper() + target[1:]
  74. if data.command in ["stalk", "watch"]:
  75. if data.is_private:
  76. stalkinfo = (data.nick, None)
  77. elif not data.is_admin:
  78. self.reply(data, "You must be a bot admin to stalk users or "
  79. "watch pages publicly. Retry this command in "
  80. "a private message.")
  81. return
  82. else:
  83. stalkinfo = (data.nick, data.chan)
  84. if data.command == "stalk":
  85. self._add_stalk("user", data, target, stalkinfo)
  86. elif data.command == "watch":
  87. self._add_stalk("page", data, target, stalkinfo)
  88. elif data.command == "unstalk":
  89. self._remove_stalk("user", data, target)
  90. elif data.command == "unwatch":
  91. self._remove_stalk("page", data, target)
  92. elif data.command == "unstalkall":
  93. self._remove_all_stalks("user", data, target)
  94. elif data.command == "unwatchall":
  95. self._remove_all_stalks("page", data, target)
  96. def _process_rc(self, rc):
  97. """Process a watcher event."""
  98. def _update_chans(items):
  99. for item in items:
  100. if item[1]:
  101. if item[1] in chans:
  102. chans[item[1]].add(item[0])
  103. else:
  104. chans[item[1]] = {item[0]}
  105. else:
  106. chans[item[0]] = None
  107. def _wildcard_match(target, tag):
  108. return target[-1] == "*" and tag.startswith(target[:-1])
  109. def _process(table, tag):
  110. for target, stalks in table.iteritems():
  111. if target == tag or _wildcard_match(target, tag):
  112. _update_chans(stalks)
  113. chans = {}
  114. _process(self._users, rc.user)
  115. if rc.is_edit:
  116. _process(self._pages, rc.page)
  117. if not chans:
  118. return
  119. with self.bot.component_lock:
  120. frontend = self.bot.frontend
  121. if frontend and not frontend.is_stopped():
  122. pretty = rc.prettify()
  123. for chan in chans:
  124. if chans[chan]:
  125. nicks = ", ".join(sorted(chans[chan]))
  126. msg = "\x02{0}\x0F: {1}".format(nicks, pretty)
  127. else:
  128. msg = pretty
  129. if len(msg) > 400:
  130. msg = msg[:397] + "..."
  131. frontend.say(chan, msg)
  132. @staticmethod
  133. def _get_stalks_by_nick(nick, table):
  134. """Return a dictionary of stalklist entries by the given nick."""
  135. entries = {}
  136. for target, stalks in table.iteritems():
  137. for info in stalks:
  138. if info[0] == nick:
  139. if target in entries:
  140. entries[target].append(info[1])
  141. else:
  142. entries[target] = [info[1]]
  143. return entries
  144. def _add_stalk(self, stalktype, data, target, stalkinfo):
  145. """Add a stalk entry to the given table."""
  146. if stalktype == "user":
  147. table = self._users
  148. verb = "stalk"
  149. else:
  150. table = self._pages
  151. verb = "watch"
  152. if not data.is_admin:
  153. nstalks = len(self._get_stalks_by_nick(data.nick, table))
  154. if nstalks >= self.MAX_STALKS_PER_USER:
  155. msg = ("Already {0}ing {1} {2}s for you, which is the limit "
  156. "for non-bot admins.")
  157. self.reply(data, msg.format(verb, nstalks, stalktype))
  158. return
  159. if target in table:
  160. if stalkinfo in table[target]:
  161. msg = "Already {0}ing that {1} in here for you."
  162. self.reply(data, msg.format(verb, stalktype))
  163. return
  164. else:
  165. table[target].append(stalkinfo)
  166. else:
  167. table[target] = [stalkinfo]
  168. msg = "Now {0}ing {1} \x0302{2}\x0F. Remove with \x0306!un{0} {2}\x0F."
  169. self.reply(data, msg.format(verb, stalktype, target))
  170. self._save_stalks()
  171. def _remove_stalk(self, stalktype, data, target):
  172. """Remove a stalk entry from the given table."""
  173. if stalktype == "user":
  174. table = self._users
  175. verb = "stalk"
  176. plural = "stalks"
  177. else:
  178. table = self._pages
  179. verb = "watch"
  180. plural = "watches"
  181. to_remove = []
  182. if target in table:
  183. for info in table[target]:
  184. if info[0] == data.nick:
  185. to_remove.append(info)
  186. if not to_remove:
  187. msg = ("I haven't been {0}ing that {1} for you in the first "
  188. "place. View your active {2} with \x0306!{2}\x0F.")
  189. if data.is_admin:
  190. msg += (" As a bot admin, you can clear all active {2} on "
  191. "that {1} with \x0306!un{0}all {3}\x0F.")
  192. self.reply(data, msg.format(verb, stalktype, plural, target))
  193. return
  194. for info in to_remove:
  195. table[target].remove(info)
  196. if not table[target]:
  197. del table[target]
  198. msg = "No longer {0}ing {1} \x0302{2}\x0F for you."
  199. self.reply(data, msg.format(verb, stalktype, target))
  200. self._save_stalks()
  201. def _remove_all_stalks(self, stalktype, data, target):
  202. """Remove all entries for a particular target from the given table."""
  203. if stalktype == "user":
  204. table = self._users
  205. verb = "stalk"
  206. plural = "stalks"
  207. else:
  208. table = self._pages
  209. verb = "watch"
  210. plural = "watches"
  211. try:
  212. del table[target]
  213. except KeyError:
  214. msg = ("I haven't been {0}ing that {1} for anyone in the first "
  215. "place. View all active {2} with \x0306!all{2}\x0F.")
  216. self.reply(data, msg.format(verb, stalktype, plural))
  217. else:
  218. msg = "No longer {0}ing {1} \x0302{2}\x0F for anyone."
  219. self.reply(data, msg.format(verb, stalktype, target))
  220. self._save_stalks()
  221. def _current_stalks(self, nick):
  222. """Return the given user's current stalks."""
  223. def _format_chans(chans):
  224. if None in chans:
  225. chans.remove(None)
  226. if not chans:
  227. return "privately"
  228. if len(chans) == 1:
  229. return "in {0} and privately".format(chans[0])
  230. return "in " + ", ".join(chans) + ", and privately"
  231. return "in " + ", ".join(chans)
  232. def _format_stalks(stalks):
  233. return ", ".join(
  234. "\x0302{0}\x0F ({1})".format(target, _format_chans(chans))
  235. for target, chans in stalks.iteritems())
  236. users = self._get_stalks_by_nick(nick, self._users)
  237. pages = self._get_stalks_by_nick(nick, self._pages)
  238. if users:
  239. uinfo = " Users: {0}.".format(_format_stalks(users))
  240. if pages:
  241. pinfo = " Pages: {0}.".format(_format_stalks(pages))
  242. msg = "Currently stalking {0} user{1} and watching {2} page{3} for you.{4}{5}"
  243. return msg.format(len(users), "s" if len(users) != 1 else "",
  244. len(pages), "s" if len(pages) != 1 else "",
  245. uinfo if users else "", pinfo if pages else "")
  246. def _all_stalks(self):
  247. """Return all existing stalks, for bot admins."""
  248. def _format_info(info):
  249. if info[1]:
  250. return "for {0} in {1}".format(info[0], info[1])
  251. return "for {0} privately".format(info[0])
  252. def _format_data(data):
  253. return ", ".join(_format_info(info) for info in data)
  254. def _format_stalks(stalks):
  255. return ", ".join(
  256. "\x0302{0}\x0F ({1})".format(target, _format_data(data))
  257. for target, data in stalks.iteritems())
  258. users, pages = self._users, self._pages
  259. if users:
  260. uinfo = " Users: {0}.".format(_format_stalks(users))
  261. if pages:
  262. pinfo = " Pages: {0}.".format(_format_stalks(pages))
  263. msg = "Currently stalking {0} user{1} and watching {2} page{3}.{4}{5}"
  264. return msg.format(len(users), "s" if len(users) != 1 else "",
  265. len(pages), "s" if len(pages) != 1 else "",
  266. uinfo if users else "", pinfo if pages else "")
  267. def _load_stalks(self):
  268. """Load saved stalks from the database."""
  269. permdb = self.config.irc["permissions"]
  270. try:
  271. data = permdb.get_attr("command:stalk", "data")
  272. except KeyError:
  273. return
  274. self._users, self._pages = literal_eval(data)
  275. def _save_stalks(self):
  276. """Save stalks to the database."""
  277. permdb = self.config.irc["permissions"]
  278. data = str((self._users, self._pages))
  279. permdb.set_attr("command:stalk", "data", data)