Skip to content

Commit 4d03ab0

Browse files
Steve Howellshowell
authored andcommitted
Add incident bot.
This is a pretty alpha proof-of-concept.
1 parent 09deda9 commit 4d03ab0

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

zulip_bots/zulip_bots/bots/incident/__init__.py

Whitespace-only changes.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import html
2+
import json
3+
import random
4+
import re
5+
from zulip_bots.lib import Any, StateHandlerError
6+
7+
from typing import Optional, Any, Dict, Tuple
8+
9+
QUESTION = 'How should we handle this?'
10+
11+
ANSWERS = {
12+
'1': 'known issue',
13+
'2': 'ignore',
14+
'3': 'in process',
15+
'4': 'escalate',
16+
}
17+
18+
class InvalidAnswerException(Exception):
19+
pass
20+
21+
class IncidentHandler:
22+
def usage(self) -> str:
23+
return '''
24+
This plugin lets folks reports incidents and
25+
triage them. It is intended to be sample code.
26+
In the real world you'd modify this code to talk
27+
to some kind of issue tracking system. But the
28+
glue code here should be pretty portable.
29+
'''
30+
31+
def handle_message(self, message: Dict[str, Any], bot_handler: Any) -> None:
32+
query = message['content']
33+
if query.startswith('new '):
34+
start_new_incident(query, message, bot_handler)
35+
elif query.startswith('answer '):
36+
try:
37+
(ticket_id, answer) = parse_answer(query)
38+
except InvalidAnswerException:
39+
bot_response = 'Invalid answer format'
40+
bot_handler.send_reply(message, bot_response)
41+
return
42+
bot_response = 'Incident %s\n status = %s' % (ticket_id, answer)
43+
bot_handler.send_reply(message, bot_response)
44+
else:
45+
bot_response = 'type "new <description>" for a new incident'
46+
bot_handler.send_reply(message, bot_response)
47+
48+
def start_new_incident(query: str, message: Dict[str, Any], bot_handler: Any) -> None:
49+
# Here is where we would enter the incident in some sort of backend
50+
# system. We just simulate everything by having an incident id that
51+
# we generate here.
52+
53+
incident = query[len('new '):]
54+
55+
ticket_id = generate_ticket_id(bot_handler.storage)
56+
bot_response = format_incident_for_markdown(ticket_id, incident)
57+
widget_content = format_incident_for_widget(ticket_id, incident)
58+
59+
bot_handler.send_reply(message, bot_response, widget_content)
60+
61+
def parse_answer(query: str) -> Tuple[str, str]:
62+
m = re.match('answer\s+(TICKET....)\s+(.)', query)
63+
if not m:
64+
raise InvalidAnswerException()
65+
66+
ticket_id = m.group(1)
67+
68+
# In a real world system, we'd validate the ticket_id against
69+
# a backend system. (You could use Zulip itself to store incident
70+
# data, if you want something really lite, but there are plenty
71+
# of systems that specialize in incident management.)
72+
73+
answer = m.group(2).upper()
74+
if answer not in '1234':
75+
raise InvalidAnswerException()
76+
77+
return (ticket_id, ANSWERS[answer])
78+
79+
def generate_ticket_id(storage: Any) -> str:
80+
try:
81+
incident_num = storage.get('ticket_id')
82+
except (StateHandlerError, KeyError):
83+
incident_num = 0
84+
incident_num += 1
85+
incident_num = incident_num % (1000)
86+
storage.put('ticket_id', incident_num)
87+
ticket_id = 'TICKET%04d' % (incident_num,)
88+
return ticket_id
89+
90+
def format_incident_for_widget(ticket_id: str, incident: Dict[str, Any]) -> str:
91+
widget_type = 'zform'
92+
93+
heading = ticket_id + ': ' + incident
94+
95+
def get_choice(code: str) -> Dict[str, str]:
96+
answer = ANSWERS[code]
97+
reply = 'answer ' + ticket_id + ' ' + code
98+
99+
return dict(
100+
type='multiple_choice',
101+
short_name=code,
102+
long_name=answer,
103+
reply=reply,
104+
)
105+
106+
choices = [get_choice(code) for code in '1234']
107+
108+
extra_data = dict(
109+
type='choices',
110+
heading=heading,
111+
choices=choices,
112+
)
113+
114+
widget_content = dict(
115+
widget_type=widget_type,
116+
extra_data=extra_data,
117+
)
118+
payload = json.dumps(widget_content)
119+
return payload
120+
121+
def format_incident_for_markdown(ticket_id: str, incident: Dict[str, Any]) -> str:
122+
answer_list = '\n'.join([
123+
'* **{code}** {answer}'.format(
124+
code=code,
125+
answer=ANSWERS[code],
126+
)
127+
for code in '1234'
128+
])
129+
how_to_respond = '''**reply**: answer {ticket_id} <code>'''.format(ticket_id=ticket_id)
130+
131+
content = '''
132+
Incident: {incident}
133+
Q: {question}
134+
135+
{answer_list}
136+
{how_to_respond}'''.format(
137+
question=QUESTION,
138+
answer_list=answer_list,
139+
how_to_respond=how_to_respond,
140+
incident=incident,
141+
)
142+
return content
143+
144+
handler_class = IncidentHandler

0 commit comments

Comments
 (0)