Skip to content

Commit fa5c5f9

Browse files
committed
Initial pollbot from previous PR.
1 parent c4876dd commit fa5c5f9

File tree

4 files changed

+553
-0
lines changed

4 files changed

+553
-0
lines changed

zulip_bots/zulip_bots/bots/poll/__init__.py

Whitespace-only changes.
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# See readme.md for instructions on running this code.
2+
3+
from __future__ import absolute_import
4+
from __future__ import print_function
5+
from six.moves import range
6+
7+
from collections import OrderedDict, namedtuple
8+
9+
def input_from_message_content(message_content):
10+
lines = message_content.split('\n')
11+
main_line = lines[0].split(' ')
12+
command = main_line[0]
13+
options = main_line[1:]
14+
title = ""
15+
if len(lines) > 1:
16+
title = lines[1]
17+
vote_options = []
18+
if len(lines) > 2:
19+
vote_options = lines[2:]
20+
vote_options = [v for v in vote_options if len(v) > 0]
21+
Input = namedtuple('Input', ['command', 'options', 'title', 'vote_options'])
22+
return Input(command, options, title, vote_options)
23+
24+
class PollHandler(object):
25+
def usage(self):
26+
return '''
27+
This bot maintains up to one poll per user, per topic, in streams only.
28+
It currently keeps a running count of the votes, as they are made, with one
29+
mesage in the stream being updated to show the current status.
30+
Message the bot privately, appending the stream and topic, or mention it
31+
within a topic (for new, vote and end commands); if the stream or
32+
topic contain spaces use a '+' where the space would be.
33+
'''
34+
35+
def handle_message(self, message, bot_handler, state_handler):
36+
37+
help_msg = OrderedDict([
38+
('about', "gives a simple summary of this bot."),
39+
('help', "produces this help."),
40+
('commands', "a concise form of help, listing the supported commands."),
41+
('new', ("start a new poll: specify a title on the following line "
42+
"and at least two options on subsequent lines.")),
43+
('vote', ("vote in an ongoing poll: specify a poll id given in the poll message "
44+
"followed by the number of the option to vote for.")),
45+
('end', ("end your own ongoing poll.")),
46+
])
47+
stream_topic_notgiven = "\nPlease specify a stream & topic if messaging the bot privately."
48+
space_equivalent = "+"
49+
50+
sender = message["sender_email"]
51+
sender_id = message["sender_id"]
52+
53+
def private_reply(text):
54+
bot_handler.send_message(dict(type='private', to=sender, content=text))
55+
56+
# Break down the text supplied into potential input sets
57+
inp = input_from_message_content(message['content'])
58+
59+
# Updates the poll text, based on the current data in the poll (may modify poll)
60+
# Return True/False, depending if message was successfully posted
61+
def update_poll_text(poll, stream_topic, force_end=False):
62+
# Construct initial/updated poll text
63+
msg = ("Poll by {} (id: {})\n{}\n"
64+
.format(poll['creator'], stream_topic[2], poll['title']))
65+
for i in range(poll['n']):
66+
msg += ("{}. [{}] {}\n"
67+
.format(i+1, len(poll['tallies'][i]), poll[i]))
68+
if force_end:
69+
msg += "**This poll has ended**\n"
70+
# Set/update poll text
71+
if poll['msg_id'] is None:
72+
result = bot_handler.send_message(dict(type='stream',
73+
to=stream_topic[0],
74+
subject=stream_topic[1],
75+
content=msg))
76+
if result['result'] == 'success':
77+
poll['msg_id'] = result['id']
78+
return (msg, True)
79+
else:
80+
return (msg, False)
81+
else:
82+
result = bot_handler.update_message(dict(message_id = poll['msg_id'],
83+
content = msg))
84+
if result['result'] == 'success':
85+
return (msg, True)
86+
else:
87+
return (msg, False) # FIXME Cannot always update a message; should poll end?
88+
# FIXME Should add handling of return value
89+
90+
# Quickly return for simple help response cases
91+
if inp.command == "" or inp.command == "help":
92+
txt = ("{}\n\nIt supports the following commands:\n"
93+
.format(" ".join(self.usage().split())))
94+
for k, v in help_msg.items():
95+
txt += "\n**{}** : {}".format(k, v)
96+
private_reply(txt)
97+
return
98+
elif inp.command == "about":
99+
private_reply(" ".join(self.usage().split()))
100+
return
101+
elif inp.command == "commands":
102+
private_reply("Commands: " + ", ".join((k for k in help_msg)))
103+
return
104+
105+
src_is_private = (message['type'] == 'private')
106+
107+
# Ensure we have some state
108+
active_polls = state_handler.get_state()
109+
if active_polls is None:
110+
active_polls = {}
111+
112+
if inp.command == "new":
113+
# Check where the poll will be -> obtain stream_topic
114+
stream_topic = None
115+
if src_is_private:
116+
if len(inp.options) != 2:
117+
private_reply(stream_topic_notgiven)
118+
return
119+
else:
120+
stream = inp.options[0].replace(space_equivalent, " ")
121+
topic = inp.options[1].replace(space_equivalent, " ")
122+
stream_topic = (stream, topic, sender_id)
123+
else:
124+
stream_topic = (message['display_recipient'], message['subject'], sender_id)
125+
# Check if a poll is already active with this id
126+
if stream_topic in active_polls:
127+
private_reply("Already have a poll with this id; end it explicitly first")
128+
return
129+
# Check we have at least a poll title and 2(+) options
130+
if inp.title == "" or len(inp.vote_options) < 2:
131+
private_reply("To " + help_msg['new'])
132+
return
133+
# Create new poll data
134+
new_poll = {
135+
'title': inp.title, # Poll title
136+
'tallies': [], # List of list of sender_id's who voted
137+
'msg_id': None, # Message id containing poll text
138+
'n': len(inp.vote_options), # How many voting options
139+
'creator': message['sender_full_name'] # Name of poll creator
140+
}
141+
for i, v in enumerate(inp.vote_options): # Set text & tallies for each vote_option
142+
new_poll[i] = v
143+
new_poll['tallies'].append([])
144+
# Try to send initial poll message to stream/topic, and alert user of result
145+
(update_msg, success) = update_poll_text(new_poll, stream_topic)
146+
if success:
147+
# Insert the new poll and update the state
148+
active_polls[stream_topic] = new_poll
149+
state_handler.set_state(active_polls)
150+
# Generate private message to creator, to indicate successful creation
151+
msg = ("Poll created in stream '#{}' with topic '{}':\n{}"
152+
.format(stream_topic[0], stream_topic[1], update_msg))
153+
else:
154+
msg = ("Could not create poll in stream '#{}' with topic '{}'"
155+
.format(stream_topic[0], stream_topic[1]))
156+
private_reply(msg)
157+
elif inp.command == "vote":
158+
# Translate options into requested poll and vote
159+
requested_poll_id = 0
160+
requested_vote_option = ""
161+
stream_topic = None
162+
if src_is_private:
163+
if len(inp.options) != 4:
164+
private_reply("To " + help_msg['vote'] + stream_topic_notgiven)
165+
return
166+
else:
167+
requested_poll_id = inp.options[2]
168+
requested_vote_option = inp.options[3]
169+
stream = inp.options[0].replace(space_equivalent, " ")
170+
topic = inp.options[1].replace(space_equivalent, " ")
171+
stream_topic = (stream, topic, requested_poll_id)
172+
else:
173+
if len(inp.options) != 2:
174+
private_reply("To " + help_msg['vote'])
175+
return
176+
else:
177+
requested_poll_id = inp.options[0]
178+
requested_vote_option = inp.options[1]
179+
stream_topic = (message['display_recipient'],
180+
message['subject'], requested_poll_id)
181+
# Validate requested_poll_id before setting stream_topic & poll
182+
try:
183+
requested_poll_id = int(requested_poll_id)
184+
except ValueError:
185+
private_reply("To " + help_msg['vote'])
186+
return
187+
stream_topic = (stream_topic[0], stream_topic[1], requested_poll_id)
188+
if stream_topic not in active_polls:
189+
private_reply("To " + help_msg['vote'])
190+
return
191+
poll = active_polls[stream_topic]
192+
# Check if this user has voted for any option already
193+
context_txt = (" in the poll on stream '#{}' (topic '{}') titled: '{}'"
194+
.format(stream_topic[0], stream_topic[1], poll['title']))
195+
for i, tally in enumerate(poll['tallies']):
196+
if sender_id in tally:
197+
msg = ("You have already voted{}\n(You voted for {}: {})"
198+
.format(context_txt, i+1, poll[i]))
199+
private_reply(msg)
200+
return
201+
# Check that vote index is within expected bounds
202+
vote_index = 0
203+
try:
204+
vote_index = int(requested_vote_option)
205+
except ValueError:
206+
private_reply("Please select one number to vote for{}".format(context_txt))
207+
return
208+
max_vote_index = poll['n']
209+
if 0 < vote_index <= max_vote_index: # Indexed from 1
210+
# Use the vote
211+
poll['tallies'][vote_index-1].append(sender_id)
212+
(update_msg, success) = update_poll_text(poll, stream_topic)
213+
if success:
214+
state_handler.set_state(active_polls)
215+
msg = ("You just voted{}\n(You voted for {}: {})"
216+
.format(context_txt, vote_index, poll[vote_index-1]))
217+
private_reply(msg)
218+
else:
219+
private_reply("Could not update the poll with your vote.")
220+
# FIXME Should we end the poll automatically here?
221+
else:
222+
private_reply(("Please select a number to vote for, between 1-{},{}"
223+
.format(max_vote_index, context_txt)))
224+
return
225+
elif inp.command == "end":
226+
# Translate options into stream & topic
227+
stream_topic = None
228+
if src_is_private:
229+
if len(inp.options) != 2:
230+
private_reply(stream_topic_notgiven)
231+
return
232+
else:
233+
stream = inp.options[0].replace(space_equivalent, " ")
234+
topic = inp.options[1].replace(space_equivalent, " ")
235+
stream_topic = (stream, topic, sender_id)
236+
else:
237+
stream_topic = (message['display_recipient'], message['subject'], sender_id)
238+
# End the poll if present
239+
if stream_topic in active_polls:
240+
(update_msg, success) = update_poll_text(active_polls[stream_topic],
241+
stream_topic, force_end=True)
242+
private_reply((("Ending your poll in '#{}' and topic '{}', "
243+
"final results were:\n\n{}")
244+
.format(stream, topic, update_msg)))
245+
if not success:
246+
private_reply("NOTE: Your poll ended, but the poll message could not be updated.")
247+
del active_polls[stream_topic]
248+
state_handler.set_state(active_polls)
249+
else:
250+
private_reply(("You do not have a poll in '#{}' and topic '{}'"
251+
.format(stream_topic[0], stream_topic[1])))
252+
else:
253+
msg = "Unsupported command."
254+
private_reply(msg)
255+
256+
257+
handler_class = PollHandler

0 commit comments

Comments
 (0)