Add incident bot.

This is a pretty alpha proof-of-concept.
This commit is contained in:
Steve Howell 2018-09-18 21:21:51 +00:00 committed by showell
parent 09deda9466
commit 4d03ab0558
2 changed files with 144 additions and 0 deletions

View file

@ -0,0 +1,144 @@
import html
import json
import random
import re
from zulip_bots.lib import Any, StateHandlerError
from typing import Optional, Any, Dict, Tuple
QUESTION = 'How should we handle this?'
ANSWERS = {
'1': 'known issue',
'2': 'ignore',
'3': 'in process',
'4': 'escalate',
}
class InvalidAnswerException(Exception):
pass
class IncidentHandler:
def usage(self) -> str:
return '''
This plugin lets folks reports incidents and
triage them. It is intended to be sample code.
In the real world you'd modify this code to talk
to some kind of issue tracking system. But the
glue code here should be pretty portable.
'''
def handle_message(self, message: Dict[str, Any], bot_handler: Any) -> None:
query = message['content']
if query.startswith('new '):
start_new_incident(query, message, bot_handler)
elif query.startswith('answer '):
try:
(ticket_id, answer) = parse_answer(query)
except InvalidAnswerException:
bot_response = 'Invalid answer format'
bot_handler.send_reply(message, bot_response)
return
bot_response = 'Incident %s\n status = %s' % (ticket_id, answer)
bot_handler.send_reply(message, bot_response)
else:
bot_response = 'type "new <description>" for a new incident'
bot_handler.send_reply(message, bot_response)
def start_new_incident(query: str, message: Dict[str, Any], bot_handler: Any) -> None:
# Here is where we would enter the incident in some sort of backend
# system. We just simulate everything by having an incident id that
# we generate here.
incident = query[len('new '):]
ticket_id = generate_ticket_id(bot_handler.storage)
bot_response = format_incident_for_markdown(ticket_id, incident)
widget_content = format_incident_for_widget(ticket_id, incident)
bot_handler.send_reply(message, bot_response, widget_content)
def parse_answer(query: str) -> Tuple[str, str]:
m = re.match('answer\s+(TICKET....)\s+(.)', query)
if not m:
raise InvalidAnswerException()
ticket_id = m.group(1)
# In a real world system, we'd validate the ticket_id against
# a backend system. (You could use Zulip itself to store incident
# data, if you want something really lite, but there are plenty
# of systems that specialize in incident management.)
answer = m.group(2).upper()
if answer not in '1234':
raise InvalidAnswerException()
return (ticket_id, ANSWERS[answer])
def generate_ticket_id(storage: Any) -> str:
try:
incident_num = storage.get('ticket_id')
except (StateHandlerError, KeyError):
incident_num = 0
incident_num += 1
incident_num = incident_num % (1000)
storage.put('ticket_id', incident_num)
ticket_id = 'TICKET%04d' % (incident_num,)
return ticket_id
def format_incident_for_widget(ticket_id: str, incident: Dict[str, Any]) -> str:
widget_type = 'zform'
heading = ticket_id + ': ' + incident
def get_choice(code: str) -> Dict[str, str]:
answer = ANSWERS[code]
reply = 'answer ' + ticket_id + ' ' + code
return dict(
type='multiple_choice',
short_name=code,
long_name=answer,
reply=reply,
)
choices = [get_choice(code) for code in '1234']
extra_data = dict(
type='choices',
heading=heading,
choices=choices,
)
widget_content = dict(
widget_type=widget_type,
extra_data=extra_data,
)
payload = json.dumps(widget_content)
return payload
def format_incident_for_markdown(ticket_id: str, incident: Dict[str, Any]) -> str:
answer_list = '\n'.join([
'* **{code}** {answer}'.format(
code=code,
answer=ANSWERS[code],
)
for code in '1234'
])
how_to_respond = '''**reply**: answer {ticket_id} <code>'''.format(ticket_id=ticket_id)
content = '''
Incident: {incident}
Q: {question}
{answer_list}
{how_to_respond}'''.format(
question=QUESTION,
answer_list=answer_list,
how_to_respond=how_to_respond,
incident=incident,
)
return content
handler_class = IncidentHandler