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.

317 lines
11 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 logging import getLogger, NullHandler
  23. from time import gmtime, strptime
  24. from socket import AF_INET, AF_INET6, error as socket_error, inet_pton
  25. from earwigbot.exceptions import UserNotFoundError
  26. from earwigbot.wiki import constants
  27. from earwigbot.wiki.page import Page
  28. __all__ = ["User"]
  29. class User:
  30. """
  31. **EarwigBot: Wiki Toolset: User**
  32. Represents a user on a given :py:class:`~earwigbot.wiki.site.Site`. Has
  33. methods for getting a bunch of information about the user, such as
  34. editcount and user rights, methods for returning the user's userpage and
  35. talkpage, etc.
  36. *Attributes:*
  37. - :py:attr:`site`: the user's corresponding Site object
  38. - :py:attr:`name`: the user's username
  39. - :py:attr:`exists`: ``True`` if the user exists, else ``False``
  40. - :py:attr:`userid`: an integer ID representing the user
  41. - :py:attr:`blockinfo`: information about any current blocks on the user
  42. - :py:attr:`groups`: a list of the user's groups
  43. - :py:attr:`rights`: a list of the user's rights
  44. - :py:attr:`editcount`: the number of edits made by the user
  45. - :py:attr:`registration`: the time the user registered
  46. - :py:attr:`emailable`: ``True`` if you can email the user, or ``False``
  47. - :py:attr:`gender`: the user's gender ("male"/"female"/"unknown")
  48. - :py:attr:`is_ip`: ``True`` if this is an IP address, or ``False``
  49. *Public methods:*
  50. - :py:meth:`reload`: forcibly reloads the user's attributes
  51. - :py:meth:`get_userpage`: returns a Page object representing the user's
  52. userpage
  53. - :py:meth:`get_talkpage`: returns a Page object representing the user's
  54. talkpage
  55. """
  56. def __init__(self, site, name, logger=None):
  57. """Constructor for new User instances.
  58. Takes two arguments, a Site object (necessary for doing API queries),
  59. and the name of the user, preferably without "User:" in front, although
  60. this prefix will be automatically removed by the API if given.
  61. You can also use site.get_user() instead, which returns a User object,
  62. and is preferred.
  63. We won't do any API queries yet for basic information about the user -
  64. save that for when the information is requested.
  65. """
  66. self._site = site
  67. self._name = name
  68. # Set up our internal logger:
  69. if logger:
  70. self._logger = logger
  71. else: # Just set up a null logger to eat up our messages:
  72. self._logger = getLogger("earwigbot.wiki")
  73. self._logger.addHandler(NullHandler())
  74. def __repr__(self):
  75. """Return the canonical string representation of the User."""
  76. return "User(name={0!r}, site={1!r})".format(self._name, self._site)
  77. def __str__(self):
  78. """Return a nice string representation of the User."""
  79. return '<User "{0}" of {1}>'.format(self.name, str(self.site))
  80. def _get_attribute(self, attr):
  81. """Internally used to get an attribute by name.
  82. We'll call _load_attributes() to get this (and all other attributes)
  83. from the API if it is not already defined.
  84. Raises UserNotFoundError if a nonexistant user prevents us from
  85. returning a certain attribute.
  86. """
  87. if not hasattr(self, attr):
  88. self._load_attributes()
  89. if not self._exists:
  90. e = "User '{0}' does not exist.".format(self._name)
  91. raise UserNotFoundError(e)
  92. return getattr(self, attr)
  93. def _load_attributes(self):
  94. """Internally used to load all attributes from the API.
  95. Normally, this is called by _get_attribute() when a requested attribute
  96. is not defined. This defines it.
  97. """
  98. props = "blockinfo|groups|rights|editcount|registration|emailable|gender"
  99. result = self.site.api_query(action="query", list="users",
  100. ususers=self._name, usprop=props)
  101. res = result["query"]["users"][0]
  102. # normalize our username in case it was entered oddly
  103. self._name = res["name"]
  104. try:
  105. self._userid = res["userid"]
  106. except KeyError: # userid is missing, so user does not exist
  107. self._exists = False
  108. return
  109. self._exists = True
  110. try:
  111. self._blockinfo = {
  112. "by": res["blockedby"],
  113. "reason": res["blockreason"],
  114. "expiry": res["blockexpiry"]
  115. }
  116. except KeyError:
  117. self._blockinfo = False
  118. self._groups = res["groups"]
  119. try:
  120. self._rights = list(res["rights"].values())
  121. except AttributeError:
  122. self._rights = res["rights"]
  123. self._editcount = res["editcount"]
  124. reg = res["registration"]
  125. try:
  126. self._registration = strptime(reg, "%Y-%m-%dT%H:%M:%SZ")
  127. except TypeError:
  128. # Sometimes the API doesn't give a date; the user's probably really
  129. # old. There's nothing else we can do!
  130. self._registration = gmtime(0)
  131. try:
  132. res["emailable"]
  133. except KeyError:
  134. self._emailable = False
  135. else:
  136. self._emailable = True
  137. self._gender = res["gender"]
  138. @property
  139. def site(self):
  140. """The user's corresponding Site object."""
  141. return self._site
  142. @property
  143. def name(self):
  144. """The user's username.
  145. This will never make an API query on its own, but if one has already
  146. been made by the time this is retrieved, the username may have been
  147. "normalized" from the original input to the constructor, converted into
  148. a Unicode object, with underscores removed, etc.
  149. """
  150. return self._name
  151. @property
  152. def exists(self):
  153. """``True`` if the user exists, or ``False`` if they do not.
  154. Makes an API query only if we haven't made one already.
  155. """
  156. if not hasattr(self, "_exists"):
  157. self._load_attributes()
  158. return self._exists
  159. @property
  160. def userid(self):
  161. """An integer ID used by MediaWiki to represent the user.
  162. Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
  163. does not exist. Makes an API query only if we haven't made one already.
  164. """
  165. return self._get_attribute("_userid")
  166. @property
  167. def blockinfo(self):
  168. """Information about any current blocks on the user.
  169. If the user is not blocked, returns ``False``. If they are, returns a
  170. dict with three keys: ``"by"`` is the blocker's username, ``"reason"``
  171. is the reason why they were blocked, and ``"expiry"`` is when the block
  172. expires.
  173. Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
  174. does not exist. Makes an API query only if we haven't made one already.
  175. """
  176. return self._get_attribute("_blockinfo")
  177. @property
  178. def groups(self):
  179. """A list of groups this user is in, including ``"*"``.
  180. Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
  181. does not exist. Makes an API query only if we haven't made one already.
  182. """
  183. return self._get_attribute("_groups")
  184. @property
  185. def rights(self):
  186. """A list of this user's rights.
  187. Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
  188. does not exist. Makes an API query only if we haven't made one already.
  189. """
  190. return self._get_attribute("_rights")
  191. @property
  192. def editcount(self):
  193. """Returns the number of edits made by the user.
  194. Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
  195. does not exist. Makes an API query only if we haven't made one already.
  196. """
  197. return self._get_attribute("_editcount")
  198. @property
  199. def registration(self):
  200. """The time the user registered as a :py:class:`time.struct_time`.
  201. Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
  202. does not exist. Makes an API query only if we haven't made one already.
  203. """
  204. return self._get_attribute("_registration")
  205. @property
  206. def emailable(self):
  207. """``True`` if the user can be emailed, or ``False`` if they cannot.
  208. Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
  209. does not exist. Makes an API query only if we haven't made one already.
  210. """
  211. return self._get_attribute("_emailable")
  212. @property
  213. def gender(self):
  214. """The user's gender.
  215. Can return either ``"male"``, ``"female"``, or ``"unknown"``, if they
  216. did not specify it.
  217. Raises :py:exc:`~earwigbot.exceptions.UserNotFoundError` if the user
  218. does not exist. Makes an API query only if we haven't made one already.
  219. """
  220. return self._get_attribute("_gender")
  221. @property
  222. def is_ip(self):
  223. """``True`` if the user is an IP address, or ``False`` otherwise.
  224. This tests for IPv4 and IPv6 using :py:func:`socket.inet_pton` on the
  225. username. No API queries are made.
  226. """
  227. try:
  228. inet_pton(AF_INET, self.name)
  229. except socket_error:
  230. try:
  231. inet_pton(AF_INET6, self.name)
  232. except socket_error:
  233. return False
  234. return True
  235. def reload(self):
  236. """Forcibly reload the user's attributes.
  237. Emphasis on *reload*: this is only necessary if there is reason to
  238. believe they have changed.
  239. """
  240. self._load_attributes()
  241. def get_userpage(self):
  242. """Return a Page object representing the user's userpage.
  243. No checks are made to see if it exists or not. Proper site namespace
  244. conventions are followed.
  245. """
  246. prefix = self.site.namespace_id_to_name(constants.NS_USER)
  247. pagename = ':'.join((prefix, self._name))
  248. return Page(self.site, pagename)
  249. def get_talkpage(self):
  250. """Return a Page object representing the user's talkpage.
  251. No checks are made to see if it exists or not. Proper site namespace
  252. conventions are followed.
  253. """
  254. prefix = self.site.namespace_id_to_name(constants.NS_USER_TALK)
  255. pagename = ':'.join((prefix, self._name))
  256. return Page(self.site, pagename)