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.

197 lines
8.8 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2016 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 time import sleep, time
  23. from urllib.request import build_opener
  24. from earwigbot import exceptions
  25. from earwigbot.wiki.copyvios.markov import MarkovChain
  26. from earwigbot.wiki.copyvios.parsers import ArticleTextParser
  27. from earwigbot.wiki.copyvios.search import SEARCH_ENGINES
  28. from earwigbot.wiki.copyvios.workers import (
  29. globalize, localize, CopyvioWorkspace)
  30. __all__ = ["CopyvioMixIn", "globalize", "localize"]
  31. class CopyvioMixIn:
  32. """
  33. **EarwigBot: Wiki Toolset: Copyright Violation MixIn**
  34. This is a mixin that provides two public methods, :py:meth:`copyvio_check`
  35. and :py:meth:`copyvio_compare`. The former checks the page for copyright
  36. violations using a search engine API, and the latter compares the page
  37. against a given URL. Credentials for the search engine API are stored in
  38. the :py:class:`~earwigbot.wiki.site.Site`'s config.
  39. """
  40. def __init__(self, site):
  41. self._search_config = site._search_config
  42. self._exclusions_db = self._search_config.get("exclusions_db")
  43. self._addheaders = [("User-Agent", site.user_agent),
  44. ("Accept-Encoding", "gzip")]
  45. def _get_search_engine(self):
  46. """Return a function that can be called to do web searches.
  47. The function takes one argument, a search query, and returns a list of
  48. URLs, ranked by importance. The underlying logic depends on the
  49. *engine* argument within our config; for example, if *engine* is
  50. "Yahoo! BOSS", we'll use YahooBOSSSearchEngine for querying.
  51. Raises UnknownSearchEngineError if the 'engine' listed in our config is
  52. unknown to us, and UnsupportedSearchEngineError if we are missing a
  53. required package or module, like oauth2 for "Yahoo! BOSS".
  54. """
  55. engine = self._search_config["engine"]
  56. if engine not in SEARCH_ENGINES:
  57. raise exceptions.UnknownSearchEngineError(engine)
  58. klass = SEARCH_ENGINES[engine]
  59. credentials = self._search_config["credentials"]
  60. opener = build_opener()
  61. opener.addheaders = self._addheaders
  62. for dep in klass.requirements():
  63. try:
  64. __import__(dep).__name__
  65. except (ImportError, AttributeError):
  66. e = "Missing a required dependency ({}) for the {} engine"
  67. e = e.format(dep, engine)
  68. raise exceptions.UnsupportedSearchEngineError(e)
  69. return klass(credentials, opener)
  70. def copyvio_check(self, min_confidence=0.75, max_queries=15, max_time=-1,
  71. no_searches=False, no_links=False, short_circuit=True):
  72. """Check the page for copyright violations.
  73. Returns a :class:`.CopyvioCheckResult` object with information on the
  74. results of the check.
  75. *min_confidence* is the minimum amount of confidence we must have in
  76. the similarity between a source text and the article in order for us to
  77. consider it a suspected violation. This is a number between 0 and 1.
  78. *max_queries* is self-explanatory; we will never make more than this
  79. number of queries in a given check.
  80. *max_time* can be set to prevent copyvio checks from taking longer than
  81. a set amount of time (generally around a minute), which can be useful
  82. if checks are called through a web server with timeouts. We will stop
  83. checking new URLs as soon as this limit is reached.
  84. Setting *no_searches* to ``True`` will cause only URLs in the wikitext
  85. of the page to be checked; no search engine queries will be made.
  86. Setting *no_links* to ``True`` will cause the opposite to happen: URLs
  87. in the wikitext will be ignored; search engine queries will be made
  88. only. Setting both of these to ``True`` is pointless.
  89. Normally, the checker will short-circuit if it finds a URL that meets
  90. *min_confidence*. This behavior normally causes it to skip any
  91. remaining URLs and web queries, but setting *short_circuit* to
  92. ``False`` will prevent this.
  93. Raises :exc:`.CopyvioCheckError` or subclasses
  94. (:exc:`.UnknownSearchEngineError`, :exc:`.SearchQueryError`, ...) on
  95. errors.
  96. """
  97. log = "Starting copyvio check for [[{0}]]"
  98. self._logger.info(log.format(self.title))
  99. searcher = self._get_search_engine()
  100. parser = ArticleTextParser(self.get(), args={
  101. "nltk_dir": self._search_config["nltk_dir"],
  102. "lang": self._site.lang
  103. })
  104. article = MarkovChain(parser.strip())
  105. parser_args = {}
  106. if self._exclusions_db:
  107. self._exclusions_db.sync(self.site.name)
  108. exclude = lambda u: self._exclusions_db.check(self.site.name, u)
  109. parser_args["mirror_hints"] = \
  110. self._exclusions_db.get_mirror_hints(self)
  111. else:
  112. exclude = None
  113. workspace = CopyvioWorkspace(
  114. article, min_confidence, max_time, self._logger, self._addheaders,
  115. short_circuit=short_circuit, parser_args=parser_args, exclude_check=exclude,
  116. config=self._search_config)
  117. if article.size < 20: # Auto-fail very small articles
  118. result = workspace.get_result()
  119. self._logger.info(result.get_log_message(self.title))
  120. return result
  121. if not no_links:
  122. workspace.enqueue(parser.get_links())
  123. num_queries = 0
  124. if not no_searches:
  125. chunks = parser.chunk(max_queries)
  126. for chunk in chunks:
  127. if short_circuit and workspace.finished:
  128. workspace.possible_miss = True
  129. break
  130. log = "[[{0}]] -> querying {1} for {2!r}"
  131. self._logger.debug(log.format(self.title, searcher.name, chunk))
  132. workspace.enqueue(searcher.search(chunk))
  133. num_queries += 1
  134. sleep(1)
  135. workspace.wait()
  136. result = workspace.get_result(num_queries)
  137. self._logger.info(result.get_log_message(self.title))
  138. return result
  139. def copyvio_compare(self, url, min_confidence=0.75, max_time=30):
  140. """Check the page like :py:meth:`copyvio_check` against a specific URL.
  141. This is essentially a reduced version of :meth:`copyvio_check` - a
  142. copyivo comparison is made using Markov chains and the result is
  143. returned in a :class:`.CopyvioCheckResult` object - but without using a
  144. search engine, since the suspected "violated" URL is supplied from the
  145. start.
  146. Its primary use is to generate a result when the URL is retrieved from
  147. a cache, like the one used in EarwigBot's Tool Labs site. After a
  148. search is done, the resulting URL is stored in a cache for 72 hours so
  149. future checks against that page will not require another set of
  150. time-and-money-consuming search engine queries. However, the comparison
  151. itself (which includes the article's and the source's content) cannot
  152. be stored for data retention reasons, so a fresh comparison is made
  153. using this function.
  154. Since no searching is done, neither :exc:`.UnknownSearchEngineError`
  155. nor :exc:`.SearchQueryError` will be raised.
  156. """
  157. log = "Starting copyvio compare for [[{0}]] against {1}"
  158. self._logger.info(log.format(self.title, url))
  159. article = MarkovChain(ArticleTextParser(self.get()).strip())
  160. workspace = CopyvioWorkspace(
  161. article, min_confidence, max_time, self._logger, self._addheaders,
  162. max_time, num_workers=1, config=self._search_config)
  163. workspace.enqueue([url])
  164. workspace.wait()
  165. result = workspace.get_result()
  166. self._logger.info(result.get_log_message(self.title))
  167. return result