A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

325 Zeilen
13 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 datetime import datetime
  23. from os import path
  24. import re
  25. import sqlite3 as sqlite
  26. from threading import Lock
  27. from earwigbot.commands import Command
  28. class Notes(Command):
  29. """A mini IRC-based wiki for storing notes, tips, and reminders."""
  30. name = "notes"
  31. commands = ["notes", "note", "about"]
  32. version = 2
  33. def setup(self):
  34. self._dbfile = path.join(self.config.root_dir, "notes.db")
  35. self._db_access_lock = Lock()
  36. def process(self, data):
  37. commands = {
  38. "help": self.do_help,
  39. "list": self.do_list,
  40. "read": self.do_read,
  41. "edit": self.do_edit,
  42. "info": self.do_info,
  43. "rename": self.do_rename,
  44. "delete": self.do_delete,
  45. }
  46. if not data.args:
  47. msg = ("\x0302The Earwig Mini-Wiki\x0F: running v{0}. Subcommands "
  48. "are: {1}. You can get help on any with '!{2} help subcommand'.")
  49. cmnds = ", ".join((commands))
  50. self.reply(data, msg.format(self.version, cmnds, data.command))
  51. return
  52. command = data.args[0].lower()
  53. if command in commands:
  54. commands[command](data)
  55. else:
  56. msg = "Unknown subcommand: \x0303{0}\x0F.".format(command)
  57. self.reply(data, msg)
  58. def do_help(self, data):
  59. """Get help on a subcommand."""
  60. info = {
  61. "help": "Get help on other subcommands.",
  62. "list": "List existing entries.",
  63. "read": "Read an existing entry ('!notes read [name]').",
  64. "edit": """Modify or create a new entry ('!notes edit name
  65. [entry content]...'). If modifying, you must be the
  66. entry author or a bot admin.""",
  67. "info": """Get information on an existing entry ('!notes info
  68. [name]').""",
  69. "rename": """Rename an existing entry ('!notes rename [old_name]
  70. [new_name]'). You must be the entry author or a bot
  71. admin.""",
  72. "delete": """Delete an existing entry ('!notes delete [name]'). You
  73. must be the entry author or a bot admin.""",
  74. }
  75. try:
  76. command = data.args[1]
  77. except IndexError:
  78. self.reply(data, "Please specify a subcommand to get help on.")
  79. return
  80. try:
  81. help_ = re.sub(r"\s\s+", " ", info[command].replace("\n", ""))
  82. self.reply(data, "\x0303{0}\x0F: ".format(command) + help_)
  83. except KeyError:
  84. msg = "Unknown subcommand: \x0303{0}\x0F.".format(command)
  85. self.reply(data, msg)
  86. def do_list(self, data):
  87. """Show a list of entries in the notes database."""
  88. query = "SELECT entry_title FROM entries"
  89. with sqlite.connect(self._dbfile) as conn, self._db_access_lock:
  90. try:
  91. entries = conn.execute(query).fetchall()
  92. except sqlite.OperationalError:
  93. entries = []
  94. if entries:
  95. entries = [entry[0].encode("utf8") for entry in entries]
  96. self.reply(data, "Entries: {0}".format(", ".join(entries)))
  97. else:
  98. self.reply(data, "No entries in the database.")
  99. def do_read(self, data):
  100. """Read an entry from the notes database."""
  101. query = """SELECT entry_title, rev_content FROM entries
  102. INNER JOIN revisions ON entry_revision = rev_id
  103. WHERE entry_slug = ?"""
  104. try:
  105. slug = self.slugify(data.args[1])
  106. except IndexError:
  107. self.reply(data, "Please specify an entry to read from.")
  108. return
  109. with sqlite.connect(self._dbfile) as conn, self._db_access_lock:
  110. try:
  111. title, content = conn.execute(query, (slug,)).fetchone()
  112. except (sqlite.OperationalError, TypeError):
  113. title, content = slug, None
  114. title = title.encode("utf8")
  115. if content:
  116. msg = "\x0302{0}\x0F: {1}"
  117. self.reply(data, msg.format(title, content.encode("utf8")))
  118. else:
  119. self.reply(data, "Entry \x0302{0}\x0F not found.".format(title))
  120. def do_edit(self, data):
  121. """Edit an entry in the notes database."""
  122. query1 = """SELECT entry_id, entry_title, user_host FROM entries
  123. INNER JOIN revisions ON entry_revision = rev_id
  124. INNER JOIN users ON rev_user = user_id
  125. WHERE entry_slug = ?"""
  126. query2 = "INSERT INTO revisions VALUES (?, ?, ?, ?, ?)"
  127. query3 = "INSERT INTO entries VALUES (?, ?, ?, ?)"
  128. query4 = "UPDATE entries SET entry_revision = ? WHERE entry_id = ?"
  129. try:
  130. slug = self.slugify(data.args[1])
  131. except IndexError:
  132. self.reply(data, "Please specify an entry to edit.")
  133. return
  134. content = " ".join(data.args[2:]).strip().decode("utf8")
  135. if not content:
  136. self.reply(data, "Please give some content to put in the entry.")
  137. return
  138. with sqlite.connect(self._dbfile) as conn, self._db_access_lock:
  139. create = True
  140. try:
  141. id_, title, author = conn.execute(query1, (slug,)).fetchone()
  142. create = False
  143. except sqlite.OperationalError:
  144. id_, title, author = 1, data.args[1].decode("utf8"), data.host
  145. self.create_db(conn)
  146. except TypeError:
  147. id_ = self.get_next_entry(conn)
  148. title, author = data.args[1].decode("utf8"), data.host
  149. permdb = self.config.irc["permissions"]
  150. if author != data.host and not permdb.is_admin(data):
  151. msg = "You must be an author or a bot admin to edit this entry."
  152. self.reply(data, msg)
  153. return
  154. revid = self.get_next_revision(conn)
  155. userid = self.get_user(conn, data.host)
  156. now = datetime.utcnow().strftime("%b %d, %Y %H:%M:%S")
  157. conn.execute(query2, (revid, id_, userid, now, content))
  158. if create:
  159. conn.execute(query3, (id_, slug, title, revid))
  160. else:
  161. conn.execute(query4, (revid, id_))
  162. msg = "Entry \x0302{0}\x0F updated."
  163. self.reply(data, msg.format(title.encode("utf8")))
  164. def do_info(self, data):
  165. """Get info on an entry in the notes database."""
  166. query = """SELECT entry_title, rev_timestamp, user_host FROM entries
  167. INNER JOIN revisions ON entry_id = rev_entry
  168. INNER JOIN users ON rev_user = user_id
  169. WHERE entry_slug = ?"""
  170. try:
  171. slug = self.slugify(data.args[1])
  172. except IndexError:
  173. self.reply(data, "Please specify an entry to get info on.")
  174. return
  175. with sqlite.connect(self._dbfile) as conn, self._db_access_lock:
  176. try:
  177. info = conn.execute(query, (slug,)).fetchall()
  178. except sqlite.OperationalError:
  179. info = []
  180. if info:
  181. title = info[0][0]
  182. times = [datum[1] for datum in info]
  183. earliest = min(times)
  184. msg = "\x0302{0}\x0F: {1} edits since {2}"
  185. msg = msg.format(title.encode("utf8"), len(info), earliest)
  186. if len(times) > 1:
  187. latest = max(times)
  188. msg += "; last edit on {0}".format(latest)
  189. names = [datum[2] for datum in info]
  190. msg += "; authors: {0}.".format(", ".join(list(set(names))))
  191. self.reply(data, msg)
  192. else:
  193. title = data.args[1]
  194. self.reply(data, "Entry \x0302{0}\x0F not found.".format(title))
  195. def do_rename(self, data):
  196. """Rename an entry in the notes database."""
  197. query1 = """SELECT entry_id, user_host FROM entries
  198. INNER JOIN revisions ON entry_revision = rev_id
  199. INNER JOIN users ON rev_user = user_id
  200. WHERE entry_slug = ?"""
  201. query2 = """UPDATE entries SET entry_slug = ?, entry_title = ?
  202. WHERE entry_id = ?"""
  203. try:
  204. slug = self.slugify(data.args[1])
  205. except IndexError:
  206. self.reply(data, "Please specify an entry to rename.")
  207. return
  208. try:
  209. newtitle = data.args[2]
  210. except IndexError:
  211. self.reply(data, "Please specify a new name for the entry.")
  212. return
  213. if newtitle == data.args[1]:
  214. self.reply(data, "The old and new names are identical.")
  215. return
  216. with sqlite.connect(self._dbfile) as conn, self._db_access_lock:
  217. try:
  218. id_, author = conn.execute(query1, (slug,)).fetchone()
  219. except (sqlite.OperationalError, TypeError):
  220. msg = "Entry \x0302{0}\x0F not found.".format(data.args[1])
  221. self.reply(data, msg)
  222. return
  223. permdb = self.config.irc["permissions"]
  224. if author != data.host and not permdb.is_admin(data):
  225. msg = "You must be an author or a bot admin to rename this entry."
  226. self.reply(data, msg)
  227. return
  228. args = (self.slugify(newtitle), newtitle.decode("utf8"), id_)
  229. conn.execute(query2, args)
  230. msg = "Entry \x0302{0}\x0F renamed to \x0302{1}\x0F."
  231. self.reply(data, msg.format(data.args[1], newtitle))
  232. def do_delete(self, data):
  233. """Delete an entry from the notes database."""
  234. query1 = """SELECT entry_id, user_host FROM entries
  235. INNER JOIN revisions ON entry_revision = rev_id
  236. INNER JOIN users ON rev_user = user_id
  237. WHERE entry_slug = ?"""
  238. query2 = "DELETE FROM entries WHERE entry_id = ?"
  239. query3 = "DELETE FROM revisions WHERE rev_entry = ?"
  240. try:
  241. slug = self.slugify(data.args[1])
  242. except IndexError:
  243. self.reply(data, "Please specify an entry to delete.")
  244. return
  245. with sqlite.connect(self._dbfile) as conn, self._db_access_lock:
  246. try:
  247. id_, author = conn.execute(query1, (slug,)).fetchone()
  248. except (sqlite.OperationalError, TypeError):
  249. msg = "Entry \x0302{0}\x0F not found.".format(data.args[1])
  250. self.reply(data, msg)
  251. return
  252. permdb = self.config.irc["permissions"]
  253. if author != data.host and not permdb.is_admin(data):
  254. msg = "You must be an author or a bot admin to delete this entry."
  255. self.reply(data, msg)
  256. return
  257. conn.execute(query2, (id_,))
  258. conn.execute(query3, (id_,))
  259. self.reply(data, "Entry \x0302{0}\x0F deleted.".format(data.args[1]))
  260. def slugify(self, name):
  261. """Convert *name* into an identifier for storing in the database."""
  262. return name.lower().replace("_", "").replace("-", "").decode("utf8")
  263. def create_db(self, conn):
  264. """Initialize the notes database with its necessary tables."""
  265. script = """
  266. CREATE TABLE entries (entry_id, entry_slug, entry_title,
  267. entry_revision);
  268. CREATE TABLE users (user_id, user_host);
  269. CREATE TABLE revisions (rev_id, rev_entry, rev_user, rev_timestamp,
  270. rev_content);
  271. """
  272. conn.executescript(script)
  273. def get_next_entry(self, conn):
  274. """Get the next entry ID."""
  275. query = "SELECT MAX(entry_id) FROM entries"
  276. later = conn.execute(query).fetchone()[0]
  277. return later + 1 if later else 1
  278. def get_next_revision(self, conn):
  279. """Get the next revision ID."""
  280. query = "SELECT MAX(rev_id) FROM revisions"
  281. later = conn.execute(query).fetchone()[0]
  282. return later + 1 if later else 1
  283. def get_user(self, conn, host):
  284. """Get the user ID corresponding to a hostname, or make one."""
  285. query1 = "SELECT user_id FROM users WHERE user_host = ?"
  286. query2 = "SELECT MAX(user_id) FROM users"
  287. query3 = "INSERT INTO users VALUES (?, ?)"
  288. user = conn.execute(query1, (host,)).fetchone()
  289. if user:
  290. return user[0]
  291. last = conn.execute(query2).fetchone()[0]
  292. later = last + 1 if last else 1
  293. conn.execute(query3, (later, host))
  294. return later