Guest User

Untitled

a guest
Jan 18th, 2018
70
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.82 KB | None | 0 0
  1. @listens_to_regex_command("example", r"(?P<key>.+)=(?P<value>.+)")
  2. def example(self, line, key, value):
  3. return "{} passed in key {} and value {}".format(line.user, key, value)
  4.  
  5. # -*- coding: utf-8 -*-
  6.  
  7. import json
  8. import re
  9. from functools import wraps
  10. from collections import namedtuple
  11.  
  12. from .. import config
  13. from ..base import BasePlugin
  14. from ..decorators import (listens_to_all, listens_to_command,
  15. listens_to_regex_command)
  16.  
  17. DEFAULT_VOTE_OPTION = "__vote__"
  18. ABSTAIN_OPTION = "__abstain__"
  19.  
  20. ERROR_MESSAGES = {
  21. "no_vote_started": u"No vote has been started. Use the “startvote” command to do so.",
  22. "voting_already_running": u"{author}: There’s already a vote going on. Use the “endvote” command to end it before starting a new one.",
  23. "invalid_option": u"“{option}” is not a valid option."
  24. }
  25. INFO_MESSAGES = {
  26. "voting_started": u"Voting has started.",
  27. "voting_started_name": u"Voting has started for proposal “{name}”.",
  28. "voting_ended": u"Voting has ended.",
  29. "voting_ended_name": u"Voting has ended for proposal “{name}”."
  30. }
  31.  
  32.  
  33. def get_section_template(name, for_, against, username_sep):
  34. template = (u"[{}(+{}, -{}): "
  35. .format(name, len(for_), len(against)))
  36.  
  37. if for_:
  38. template += username_sep.join(for_)
  39. if for_ and against:
  40. template += u"; "
  41. if against:
  42. template += username_sep.join(
  43. [u"-" + username for username in against])
  44. template += u"]"
  45. return template
  46.  
  47.  
  48. COUNTVOTE_TEMPLATES = {
  49. "default_for_template": u"[+{number_for}: {users}]",
  50. "default_against_template": u"[-{number_against}: {users}]",
  51. "abstain_template": u"[\{number_abstain}: {users}]",
  52. "section_template": get_section_template
  53. }
  54.  
  55. FOR_NAME = "for"
  56. AGAINST_NAME = "against"
  57. ABSTAIN_NAME = "abstain"
  58. OPPOSING_VOTE = {
  59. FOR_NAME: AGAINST_NAME,
  60. AGAINST_NAME: FOR_NAME
  61. }
  62. VOTE_SHORTCUT_TO_NAME = {
  63. "": FOR_NAME,
  64. "+": FOR_NAME,
  65. "-": AGAINST_NAME
  66. }
  67.  
  68. Vote = namedtuple('Vote', ['voter', 'section', 'vote'])
  69.  
  70. class Config(config.BaseConfig):
  71.  
  72. options_separator = config.Field(help_text="Separator to use when starting a vote with custom options", default=",")
  73. username_separator = config.Field(help_text="Separator between usernames when printing out votes", default=",")
  74. boolean_shortform = config.Field(help_text=u"The short option that voters can use in boolean votes", default="1")
  75.  
  76. class Plugin(BasePlugin):
  77.  
  78. config_class = Config # Defines what lives in self.config
  79.  
  80. @listens_to_regex_command("startvote",
  81. ur"(?P<name>[^[]]+?)?s*([(?P<options>.*)])?s*$")
  82. def startvote(self, line, name, options):
  83. options_sep = self.config["options_separator"]
  84. if self.retrieve("votes"):
  85. return (ERROR_MESSAGES["voting_already_running"].
  86. format(author=line.user))
  87.  
  88. default_options = [DEFAULT_VOTE_OPTION]
  89. options =
  90. filter(bool, [option_name.strip() for option_name
  91. in (options.split(options_sep) if options else [])])
  92. options = options or default_options
  93. # Abstaining should always be an option
  94. options.append(ABSTAIN_OPTION)
  95. options = list(set(options))
  96.  
  97. # NOTE: this will create a for and against in __abstain__,
  98. # but for simplicity's sake we won't try and prevent this.
  99. # All abstainers will be added to __abstain__.for.
  100. initial_votes = {
  101. option_name: {FOR_NAME: [], AGAINST_NAME: []}
  102. for option_name in options
  103. }
  104.  
  105. self.store("name", json.dumps(name))
  106. self.store("options", json.dumps(options))
  107. self.store("votes", json.dumps(initial_votes))
  108. self.store("changes_since_countvote", json.dumps(False))
  109.  
  110. return (INFO_MESSAGES["voting_started_name"].format(name=name)
  111. if name else INFO_MESSAGES["voting_started"])
  112.  
  113. def _depends_on_votestarted(throw_error=True):
  114. def decorator(func):
  115. @wraps(func)
  116. def wrap(*args, **kwargs):
  117. slf = args[0]
  118. if slf.retrieve("votes"):
  119. return func(*args, **kwargs)
  120. elif throw_error:
  121. return ERROR_MESSAGES["no_vote_started"]
  122. return ""
  123. return wrap
  124. return decorator
  125.  
  126. @listens_to_command("endvote")
  127. @_depends_on_votestarted()
  128. def endvote(self, line, args):
  129. name = json.loads(self.retrieve("name"))
  130. changes_since_countvote = json.loads(
  131. self.retrieve("changes_since_countvote"))
  132. reply = (INFO_MESSAGES["voting_ended_name"].format(name=name)
  133. if name else INFO_MESSAGES["voting_ended"])
  134. if changes_since_countvote:
  135. reply += "n" + self._print_votes()
  136.  
  137. self.delete("name")
  138. self.delete("options")
  139. self.delete("votes")
  140. self.delete("changes_since_countvote")
  141.  
  142. return reply
  143.  
  144. @listens_to_command("countvotes")
  145. @_depends_on_votestarted()
  146. def countvotes(self, line, args):
  147. self.store("changes_since_countvote", json.dumps(False))
  148. return self._print_votes()
  149.  
  150. vote_regex_template = (
  151. r"^((?P<vote>[\" +
  152. re.escape("|".join(VOTE_SHORTCUT_TO_NAME.keys())) +
  153. r"]{vote_modifier})(?P<option>.+))$")
  154.  
  155. @listens_to_regex_command("vote", (vote_regex_template
  156. .format(vote_modifier="?")))
  157. @_depends_on_votestarted()
  158. def vote(self, line, vote, option):
  159. try:
  160. changes_made = self._add_vote(self._parse_vote(line.user, (vote, option)))
  161. if changes_made:
  162. self.store("changes_since_countvote", json.dumps(True))
  163. except InvalidOptionError, e:
  164. return unicode(e)
  165.  
  166. # This makes the vote symbol (+, -, ) required, since otherwise we can't
  167. # tell if this is meant to be a vote or not.
  168. @listens_to_all(vote_regex_template.format(vote_modifier=""))
  169. @_depends_on_votestarted(throw_error=False)
  170. def implicit_vote(self, line, vote, option):
  171. try:
  172. parsed_vote = self._parse_vote(line.user, (vote, option))
  173. except InvalidOptionError:
  174. return # Don't complain, will generate too many false positives
  175. changes_made = self._add_vote(parsed_vote)
  176. if changes_made:
  177. self.store("changes_since_countvote", json.dumps(True))
  178.  
  179. @listens_to_command("abstain")
  180. @_depends_on_votestarted()
  181. def abstain(self, line, args):
  182. self.store("changes_since_countvote", json.dumps(True))
  183. self._add_vote(self._get_abstain_vote(line.user))
  184.  
  185. @listens_to_command("cancelvotes")
  186. @_depends_on_votestarted()
  187. def cancelvotes(self, line, args):
  188. self.store("changes_since_countvote", json.dumps(True))
  189. votes = json.loads(self.retrieve("votes"))
  190. votes = self._remove_all_votes(line.user, votes)
  191. self.store("votes", json.dumps(votes))
  192.  
  193. def _parse_vote(self, voter, (vote, option)):
  194. boolean_shortform = self.config["boolean_shortform"]
  195. if vote == "\":
  196. if (option == boolean_shortform or
  197. option == ABSTAIN_NAME):
  198. return self._get_abstain_vote(voter)
  199. else:
  200. raise InvalidOptionError()
  201. if option.strip() == "":
  202. option = boolean_shortform
  203.  
  204. options = self._get_valid_options()
  205. longest_matching_option =
  206. self._find_longest_matching_option(option, options)
  207. if longest_matching_option is None:
  208. raise InvalidOptionError(ERROR_MESSAGES["invalid_option"]
  209. .format(option=option))
  210.  
  211. return Vote(
  212. voter=voter,
  213. section=(longest_matching_option
  214. if longest_matching_option != boolean_shortform
  215. else DEFAULT_VOTE_OPTION),
  216. vote=VOTE_SHORTCUT_TO_NAME[vote]
  217. )
  218.  
  219. def _find_longest_matching_option(self, user_option, valid_options):
  220. user_option = user_option.strip()
  221.  
  222. def matches_user_option(option):
  223. option = option.strip()
  224. # In startswith, adding the space ensures that "oreo"
  225. # is not treated as matching the option "o".
  226. return (user_option == option or
  227. user_option.startswith(option + " "))
  228.  
  229. matching_options = filter(matches_user_option, valid_options)
  230. return (max(matching_options, key=len)
  231. if len(matching_options) != 0 else None)
  232.  
  233. def _add_vote(self, parsed_vote):
  234. votes = json.loads(self.retrieve("votes"))
  235. voter, section, vote = parsed_vote
  236. option = votes[section]
  237. option_vote = option[vote]
  238. opposing_option_vote = option[OPPOSING_VOTE[vote]]
  239.  
  240. if voter in option_vote:
  241. return False
  242. elif voter in opposing_option_vote:
  243. opposing_option_vote.remove(voter)
  244.  
  245. if section == ABSTAIN_OPTION:
  246. votes = self._remove_all_votes(voter, votes)
  247.  
  248. try:
  249. votes[ABSTAIN_OPTION][FOR_NAME].remove(voter)
  250. except ValueError:
  251. pass
  252. option_vote.append(voter.strip())
  253. self.store("votes", json.dumps(votes))
  254. return True
  255.  
  256. def _print_votes(self):
  257. votes = json.loads(self.retrieve("votes"))
  258. section_names = votes.keys()
  259. section_names = sorted(section_names)
  260. # Abstain should always be last in the list
  261. section_names.append(section_names.pop(
  262. section_names.index(ABSTAIN_OPTION)))
  263.  
  264. sections = [self._get_section_repr(section_name, votes[section_name])
  265. for section_name in section_names]
  266. return " ".join(sections)
  267.  
  268. def _get_section_repr(self, section_name, section):
  269. username_sep = self.config["username_separator"] + " "
  270. for_, against = section[FOR_NAME], section[AGAINST_NAME]
  271. users_for, users_against = (username_sep.join(for_),
  272. username_sep.join(against))
  273.  
  274. if section_name == DEFAULT_VOTE_OPTION:
  275. for_portion = (
  276. COUNTVOTE_TEMPLATES["default_for_template"]
  277. .format(
  278. number_for=len(for_),
  279. users=users_for
  280. )
  281. )
  282. against_portion = (
  283. COUNTVOTE_TEMPLATES["default_against_template"]
  284. .format(
  285. number_against=len(against),
  286. users=users_against
  287. )
  288. )
  289. return for_portion + " " + against_portion
  290. elif section_name == ABSTAIN_OPTION:
  291. return (
  292. COUNTVOTE_TEMPLATES["abstain_template"]
  293. .format(
  294. number_abstain=len(for_),
  295. users=users_for
  296. )
  297. )
  298. else:
  299. return (
  300. COUNTVOTE_TEMPLATES["section_template"](
  301. name=section_name,
  302. for_=for_,
  303. against=against,
  304. username_sep=username_sep
  305. )
  306. )
  307.  
  308. def _remove_all_votes(self, voter, votes):
  309. for section in votes.copy():
  310. sec = votes[section]
  311. try:
  312. sec[FOR_NAME].remove(voter)
  313. except ValueError:
  314. pass
  315. try:
  316. sec[AGAINST_NAME].remove(voter)
  317. except ValueError:
  318. pass
  319. return votes
  320.  
  321. def _get_valid_options(self):
  322. options = json.loads(self.retrieve("options"))
  323. if DEFAULT_VOTE_OPTION in options:
  324. options[options.index(DEFAULT_VOTE_OPTION)] =
  325. self.config["boolean_shortform"]
  326. return options
  327.  
  328. def _get_abstain_vote(self, voter):
  329. return Vote(
  330. voter=voter,
  331. section=ABSTAIN_OPTION,
  332. vote=FOR_NAME
  333. )
  334.  
  335.  
  336. class InvalidOptionError(Exception):
  337. pass
Add Comment
Please, Sign In to add comment