A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

314 行
13 KiB

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