A Python robot that edits Wikipedia and interacts with people over IRC 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.

223 lines
8.9 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 fnmatch import fnmatch
  23. import sqlite3 as sqlite
  24. from threading import Lock
  25. __all__ = ["PermissionsDB"]
  26. class PermissionsDB(object):
  27. """
  28. **EarwigBot: Permissions Database Manager**
  29. Controls the :file:`permissions.db` file, which stores the bot's owners and
  30. admins for the purposes of using certain dangerous IRC commands.
  31. """
  32. ADMIN = 1
  33. OWNER = 2
  34. def __init__(self, dbfile):
  35. self._dbfile = dbfile
  36. self._db_access_lock = Lock()
  37. self._users = {}
  38. self._attributes = {}
  39. def __repr__(self):
  40. """Return the canonical string representation of the PermissionsDB."""
  41. res = "PermissionsDB(dbfile={0!r})"
  42. return res.format(self._dbfile)
  43. def __str__(self):
  44. """Return a nice string representation of the PermissionsDB."""
  45. return "<PermissionsDB at {0}>".format(self._dbfile)
  46. def _create(self, conn):
  47. """Initialize the permissions database with its necessary tables."""
  48. query = """CREATE TABLE users (user_nick, user_ident, user_host,
  49. user_rank);
  50. CREATE TABLE attributes (attr_uid, attr_key, attr_value);"""
  51. conn.executescript(query)
  52. def _is_rank(self, user, rank):
  53. """Return True if the given user has the given rank, else False."""
  54. try:
  55. for rule in self._users[rank]:
  56. if user in rule:
  57. return rule
  58. except KeyError:
  59. pass
  60. return False
  61. def _set_rank(self, user, rank):
  62. """Add a User to the database under a given rank."""
  63. query = "INSERT INTO users VALUES (?, ?, ?, ?)"
  64. with self._db_access_lock:
  65. with sqlite.connect(self._dbfile) as conn:
  66. conn.execute(query, (user.nick, user.ident, user.host, rank))
  67. try:
  68. self._users[rank].append(user)
  69. except KeyError:
  70. self._users[rank] = [user]
  71. return user
  72. def _del_rank(self, user, rank):
  73. """Remove a User from the database."""
  74. query = """DELETE FROM users WHERE user_nick = ? AND user_ident = ? AND
  75. user_host = ? AND user_rank = ?"""
  76. with self._db_access_lock:
  77. try:
  78. for rule in self._users[rank]:
  79. if user in rule:
  80. with sqlite.connect(self._dbfile) as conn:
  81. args = (user.nick, user.ident, user.host, rank)
  82. conn.execute(query, args)
  83. self._users[rank].remove(rule)
  84. return rule
  85. except KeyError:
  86. pass
  87. return None
  88. @property
  89. def users(self):
  90. """A dict of all users in the permissions database."""
  91. return self._users
  92. @property
  93. def attributes(self):
  94. """A dict of all attributes in the permissions database."""
  95. return self._attributes
  96. def load(self):
  97. """Load permissions from an existing database, or create a new one."""
  98. qry1 = "SELECT user_nick, user_ident, user_host, user_rank FROM users"
  99. qry2 = "SELECT attr_uid, attr_key, attr_value FROM attributes"
  100. self._users = {}
  101. with sqlite.connect(self._dbfile) as conn, self._db_access_lock:
  102. try:
  103. for nick, ident, host, rank in conn.execute(qry1):
  104. try:
  105. self._users[rank].append(_User(nick, ident, host))
  106. except KeyError:
  107. self._users[rank] = [_User(nick, ident, host)]
  108. for user, key, value in conn.execute(qry2):
  109. try:
  110. self._attributes[user][key] = value
  111. except KeyError:
  112. self._attributes[user] = {key: value}
  113. except sqlite.OperationalError:
  114. self._create(conn)
  115. def has_exact(self, rank, nick="*", ident="*", host="*"):
  116. """Return ``True`` if there is an exact match for this rule."""
  117. try:
  118. for usr in self._users[rank]:
  119. if nick != usr.nick or ident != usr.ident or host != usr.host:
  120. continue
  121. return usr
  122. except KeyError:
  123. pass
  124. return False
  125. def is_admin(self, data):
  126. """Return ``True`` if the given user is a bot admin, else ``False``."""
  127. user = _User(data.nick, data.ident, data.host)
  128. return self._is_rank(user, rank=self.ADMIN)
  129. def is_owner(self, data):
  130. """Return ``True`` if the given user is a bot owner, else ``False``."""
  131. user = _User(data.nick, data.ident, data.host)
  132. return self._is_rank(user, rank=self.OWNER)
  133. def add_admin(self, nick="*", ident="*", host="*"):
  134. """Add a nick/ident/host combo to the bot admins list."""
  135. return self._set_rank(_User(nick, ident, host), rank=self.ADMIN)
  136. def add_owner(self, nick="*", ident="*", host="*"):
  137. """Add a nick/ident/host combo to the bot owners list."""
  138. return self._set_rank(_User(nick, ident, host), rank=self.OWNER)
  139. def remove_admin(self, nick="*", ident="*", host="*"):
  140. """Remove a nick/ident/host combo to the bot admins list."""
  141. return self._del_rank(_User(nick, ident, host), rank=self.ADMIN)
  142. def remove_owner(self, nick="*", ident="*", host="*"):
  143. """Remove a nick/ident/host combo to the bot owners list."""
  144. return self._del_rank(_User(nick, ident, host), rank=self.OWNER)
  145. def has_attr(self, user, key):
  146. """Return ``True`` if a given user has a certain attribute, *key*."""
  147. return user in self._attributes and key in self._attributes[user]
  148. def get_attr(self, user, key):
  149. """Get the value of the attribute *key* of a given *user*.
  150. Raises :py:exc:`KeyError` if the *key* or *user* is not found.
  151. """
  152. return self._attributes[user][key]
  153. def set_attr(self, user, key, value):
  154. """Set the *value* of the attribute *key* of a given *user*."""
  155. query1 = """SELECT attr_value FROM attributes WHERE attr_uid = ?
  156. AND attr_key = ?"""
  157. query2 = "INSERT INTO attributes VALUES (?, ?, ?)"
  158. query3 = """UPDATE attributes SET attr_value = ? WHERE attr_uid = ?
  159. AND attr_key = ?"""
  160. with self._db_access_lock, sqlite.connect(self._dbfile) as conn:
  161. if conn.execute(query1, (user, key)).fetchone():
  162. conn.execute(query3, (value, user, key))
  163. else:
  164. conn.execute(query2, (user, key, value))
  165. try:
  166. self._attributes[user][key] = value
  167. except KeyError:
  168. self.attributes[user] = {key: value}
  169. def remove_attr(self, user, key):
  170. """Remove the attribute *key* of a given *user*."""
  171. query = "DELETE FROM attributes WHERE attr_uid = ? AND attr_key = ?"
  172. with self._db_access_lock, sqlite.connect(self._dbfile) as conn:
  173. conn.execute(query, (user, key))
  174. class _User(object):
  175. """A class that represents an IRC user for the purpose of testing rules."""
  176. def __init__(self, nick, ident, host):
  177. self.nick = nick
  178. self.ident = ident
  179. self.host = host
  180. def __repr__(self):
  181. """Return the canonical string representation of the User."""
  182. res = "_User(nick={0!r}, ident={1!r}, host={2!r})"
  183. return res.format(self.nick, self.ident, self.host)
  184. def __str__(self):
  185. """Return a nice string representation of the User."""
  186. return "{0}!{1}@{2}".format(self.nick, self.ident, self.host)
  187. def __contains__(self, user):
  188. if fnmatch(user.nick, self.nick):
  189. if fnmatch(user.ident, self.ident):
  190. if fnmatch(user.host, self.host):
  191. return True
  192. return False