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.

157 lines
5.4 KiB

  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime, timedelta
  3. from os.path import expanduser
  4. from threading import Lock
  5. from time import sleep
  6. import oursql
  7. from classes import BaseTask
  8. import config
  9. import wiki
  10. # Valid submission statuses:
  11. STATUS_NONE = 0
  12. STATUS_PEND = 1
  13. STATUS_DECLINE = 2
  14. STATUS_ACCEPT = 3
  15. class Task(BaseTask):
  16. """A task to generate charts about AfC submissions over time.
  17. The main function of the task is to work through the "AfC submissions by
  18. date" categories (e.g. [[Category:AfC submissions by date/12 July 2011]])
  19. and determine the number of declined, accepted, and currently pending
  20. submissions every day.
  21. This information is saved to a MySQL database ("u_earwig_afc_history") and
  22. used to generate attractive graphs showing the number of AfC submissions
  23. over time.
  24. """
  25. name = "afc_history"
  26. def __init__(self):
  27. cfg = config.tasks.get(self.name, {})
  28. self.destination = cfg.get("destination", "afc_history.png")
  29. self.categories = cfg.get("categories", {})
  30. # Connection data for our SQL database:
  31. kwargs = cfg.get("sql", {})
  32. kwargs["read_default_file"] = expanduser("~/.my.cnf")
  33. self.conn_data = kwargs
  34. self.db_access_lock = Lock()
  35. def run(self, **kwargs):
  36. self.site = wiki.get_site()
  37. with self.db_access_lock:
  38. self.conn = oursql.connect(**self.conn_data)
  39. action = kwargs.get("action")
  40. try:
  41. if action == "update":
  42. self.update(kwargs.get("days", 90))
  43. elif action == "generate":
  44. self.generate(kwargs.get("days", 90))
  45. finally:
  46. self.conn.close()
  47. def update(self, num_days):
  48. self.logger.info("Updating past {0} days".format(num_days))
  49. generator = self.backwards_cat_iterator()
  50. for d in xrange(num_days):
  51. category = generator.next()
  52. date = category.title().split("/")[-1]
  53. self.update_date(date, category)
  54. sleep(15)
  55. self.logger.info("Update complete")
  56. def generate(self, data):
  57. self.logger.info("Generating chart for past {0} days".format(num_days))
  58. data = {}
  59. generator = self.backwards_cat_iterator()
  60. for d in xrange(num_days):
  61. category = generator.next()
  62. date = category.title().split("/")[-1]
  63. data[date] = self.get_date_counts(date)
  64. dest = expanduser(self.destination)
  65. with open(dest, "wb") as fp:
  66. fp.write(data)
  67. self.logger.info("Chart saved to {0}".format(dest))
  68. def backwards_cat_iterator(self):
  69. date_base = self.categories["dateBase"]
  70. current = datetime.utcnow()
  71. while 1:
  72. subcat = current.strftime("%d %B %Y")
  73. title = "/".join((date_base, subcat))
  74. yield self.site.get_category(title)
  75. current -= timedelta(1) # Subtract one day from date
  76. def update_date(self, date, category):
  77. msg = "Updating {0} ([[{1}]])".format(date, category.title())
  78. self.logger.debug(msg)
  79. q_select = "SELECT page_id, page_status FROM page WHERE page_date = ?"
  80. q_delete = "DELETE FROM page WHERE page_id = ?"
  81. q_update = "UPDATE page SET page_status = ? WHERE page_id = ?"
  82. q_insert = "INSERT INTO page VALUES (?, ?, ?)"
  83. members = category.members(use_sql=True)
  84. tracked = []
  85. statuses = {}
  86. with self.conn.cursor() as cursor:
  87. cursor.execute(q_select, (date,))
  88. for pageid, status in cursor:
  89. tracked.append(pageid)
  90. statuses[pageid] = status
  91. for title, pageid in members:
  92. status = self.get_status(title, pageid)
  93. if status == STATUS_NONE:
  94. if pageid in tracked:
  95. cursor.execute(q_delete, (pageid,))
  96. continue
  97. if pageid in tracked:
  98. if status != statuses[pageid]:
  99. cursor.execute(q_update, (status, pageid))
  100. else:
  101. cursor.execute(q_insert, (pageid, date, status))
  102. def get_status(self, title, pageid):
  103. page = self.site.get_page(title)
  104. ns = page.namespace()
  105. if ns == wiki.NS_FILE_TALK: # Ignore accepted FFU requests
  106. return STATUS_NONE
  107. if ns == wiki.NS_TALK:
  108. new_page = page.toggle_talk()
  109. if new_page.is_redirect():
  110. return STATUS_NONE # Ignore accepted AFC/R requests
  111. return STATUS_ACCEPT
  112. cats = self.categories
  113. query = "SELECT 1 FROM categorylinks WHERE cl_from = ? AND cl_to = ?"
  114. match = lambda cat: list(self.site.sql_query(query, (cat, pageid)))
  115. if match(cats["pending"]):
  116. return STATUS_PEND
  117. elif match(cats["unsubmitted"]):
  118. return STATUS_NONE
  119. elif match(cats["declined"]):
  120. return STATUS_DECLINE
  121. return STATUS_NONE
  122. def get_date_counts(self, date):
  123. query = "SELECT COUNT(*) FROM page WHERE page_date = ? AND page_status = ?"
  124. statuses = [STATUS_PEND, STATUS_DECLINE, STATUS_ACCEPT]
  125. counts = {}
  126. with self.conn.cursor() as cursor:
  127. for status in statuses:
  128. cursor.execute(query, (date, status))
  129. count = cursor.fetchall()[0][0]
  130. counts[status] = count
  131. return counts