Add basecamp3 webhook integration.

Fixes: #3949.
This commit is contained in:
Tomasz Kolek
2017-03-14 20:04:11 +01:00
committed by Tim Abbott
parent 09f5180da9
commit be0a2cb20b
43 changed files with 2572 additions and 0 deletions

View File

View File

@@ -0,0 +1,46 @@
<p>
Zulip supports integration with Basecamp and can notify you of
events in Basecamp.
</p>
<p>
First, create the stream you'd like to use for Basecamp notifications,
and subscribe all interested parties to this stream. We
recommend the name <code>basecamp</code>.
</p>
<p><code>{{ external_api_uri_subdomain }}/v1/external/basecamp?api_key=abcdefgh&amp;stream=basecamp</code></p>
<p>
where <code>api_key</code> is the API key of your Zulip bot,
and <code>stream</code> is the stream name you want the
notifications sent to.
</p>
<br/>
<p>
Next, go to your project on Basecamp and choose <code>Set up webhooks</code>
from the <code>Settings</code> menu that is located in top right corner.
</p>
<img class="screenshot" src="/static/images/integrations/basecamp/001.png"/>
<br/><br/>
<p>Click on <code>Add a new webhook</code> button</p>
<img class="screenshot" src="/static/images/integrations/basecamp/002.png"/>
<br/><br/>
<p>
Paste the URL you created above and choose which events you want to be notified about.
Make sure that <code>Enable this webhook?</code> is checked.
Click <code>Add this webhook</code>.
</p>
<img class="screenshot" src="/static/images/integrations/basecamp/003.png"/>
<br/><br/>
<p>
<b>Congratulations! You're done!</b><br/>
When you do things in basecamp action, you'll get a notification like this:
</p>
<img class="screenshot" src="/static/images/integrations/basecamp/004.png"/>

View File

@@ -0,0 +1,45 @@
DOC_SUPPORT_EVENTS = [
'document_active',
'document_created',
'document_archived',
'document_unarchived',
'document_publicized',
'document_title_changed',
'document_content_changed',
'document_trashed',
'document_publicized',
]
QUESTION_SUPPORT_EVENTS = [
'question_archived',
'question_created',
'question_trashed',
'question_unarchived',
'question_answer_archived',
'question_answer_content_changed',
'question_answer_created',
'question_answer_trashed',
'question_answer_unarchived',
]
MESSAGE_SUPPORT_EVENTS = [
'message_archived',
'message_content_changed',
'message_created',
'message_subject_changed',
'message_trashed',
'message_unarchived',
'comment_created',
]
TODOS_SUPPORT_EVENTS = [
'todolist_created',
'todolist_description_changed',
'todolist_name_changed',
'todo_assignment_changed',
'todo_completed',
'todo_created',
'todo_due_date_changed',
]
SUPPORT_EVENTS = DOC_SUPPORT_EVENTS + QUESTION_SUPPORT_EVENTS + MESSAGE_SUPPORT_EVENTS + TODOS_SUPPORT_EVENTS

View File

@@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
from typing import Text
from zerver.lib.test_classes import WebhookTestCase
class BasecampHookTests(WebhookTestCase):
STREAM_NAME = 'basecamp'
URL_TEMPLATE = u"/api/v1/external/basecamp?stream={stream}&api_key={api_key}"
FIXTURE_DIR_NAME = 'basecamp'
EXPECTED_SUBJECT = "Zulip HQ"
def test_basecamp_makes_doc_active(self):
# type: () -> None
expected_message = u"Tomasz activated the document [New doc](https://3.basecamp.com/3688623/buckets/2957043/documents/432522214)"
self._send_and_test_message('doc_active', expected_message)
def test_basecamp_makes_doc_archived(self):
# type: () -> None
expected_message = u"Tomasz archived the document [new doc](https://3.basecamp.com/3688623/buckets/2957043/documents/434455988)"
self._send_and_test_message('doc_archived', expected_message)
def test_basecamp_makes_doc_changed_content(self):
# type: () -> None
expected_message = u"Tomasz changed content of the document [New doc edit](https://3.basecamp.com/3688623/buckets/2957043/documents/432522214)"
self._send_and_test_message('doc_content_changed', expected_message)
def test_basecamp_makes_doc_changed_title(self):
# type: () -> None
expected_message = u"Tomasz changed title of the document [New doc edit](https://3.basecamp.com/3688623/buckets/2957043/documents/432522214)"
self._send_and_test_message('doc_title_changed', expected_message)
def test_basecamp_makes_doc_publicized(self):
# type: () -> None
expected_message = u"Tomasz publicized the document [new doc](https://3.basecamp.com/3688623/buckets/2957043/documents/434455988)"
self._send_and_test_message('doc_publicized', expected_message)
def test_basecamp_makes_doc_created(self):
# type: () -> None
expected_message = u"Tomasz created the document [new doc](https://3.basecamp.com/3688623/buckets/2957043/documents/434455988)"
self._send_and_test_message('doc_created', expected_message)
def test_basecamp_makes_doc_trashed(self):
# type: () -> None
expected_message = u"Tomasz trashed the document [new doc](https://3.basecamp.com/3688623/buckets/2957043/documents/434455988)"
self._send_and_test_message('doc_trashed', expected_message)
def test_basecamp_makes_doc_unarchived(self):
# type: () -> None
expected_message = u"Tomasz unarchived the document [new doc](https://3.basecamp.com/3688623/buckets/2957043/documents/434455988)"
self._send_and_test_message('doc_unarchive', expected_message)
def test_basecamp_makes_questions_answer_archived(self):
# type: () -> None
expected_message = u"Tomasz archived the [answer](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747/answers/2017-03-16#__recording_432529636) of the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('questions_answer_archived', expected_message)
def test_basecamp_makes_questions_answer_content_changed(self):
# type: () -> None
expected_message = u"Tomasz changed content of the [answer](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747/answers/2017-03-16#__recording_432529636) of the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('questions_answer_content_changed', expected_message)
def test_basecamp_makes_questions_answer_created(self):
# type: () -> None
expected_message = u"Tomasz created the [answer](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747/answers/2017-03-16#__recording_432529636) of the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('questions_answer_created', expected_message)
def test_basecamp_makes_questions_answer_trashed(self):
# type: () -> None
expected_message = u"Tomasz trashed the [answer](https://3.basecamp.com/3688623/buckets/2957043/question_answers/432529636) of the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('questions_answer_trashed', expected_message)
def test_basecamp_makes_questions_answer_unarchived(self):
# type: () -> None
expected_message = u"Tomasz unarchived the [answer](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747/answers/2017-03-16#__recording_432529636) of the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('questions_answer_unarchived', expected_message)
def test_basecamp_makes_question_archived(self):
# type: () -> None
expected_message = u"Tomasz archived the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('question_archived', expected_message)
def test_basecamp_makes_question_created(self):
# type: () -> None
expected_message = u"Tomasz created the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('question_created', expected_message)
def test_basecamp_makes_question_trashed(self):
# type: () -> None
expected_message = u"Tomasz trashed the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('question_trashed', expected_message)
def test_basecamp_makes_question_unarchived(self):
# type: () -> None
expected_message = u"Tomasz unarchived the question [Question](https://3.basecamp.com/3688623/buckets/2957043/questions/432527747)"
self._send_and_test_message('question_unarchived', expected_message)
def test_basecamp_makes_message_archived(self):
# type: () -> None
expected_message = u"Tomasz archived the message [Message Title new](https://3.basecamp.com/3688623/buckets/2957043/messages/430680605)"
self._send_and_test_message('message_archived', expected_message)
def test_basecamp_makes_message_content_change(self):
# type: () -> None
expected_message = u"Tomasz changed content of the message [Message Title new](https://3.basecamp.com/3688623/buckets/2957043/messages/430680605)"
self._send_and_test_message('message_content_changed', expected_message)
def test_basecamp_makes_message_created(self):
# type: () -> None
expected_message = u"Tomasz created the message [Message Title](https://3.basecamp.com/3688623/buckets/2957043/messages/430680605)"
self._send_and_test_message('message_created', expected_message)
def test_basecamp_makes_message_title_change(self):
# type: () -> None
expected_message = u"Tomasz changed subject of the message [Message Title new](https://3.basecamp.com/3688623/buckets/2957043/messages/430680605)"
self._send_and_test_message('message_title_changed', expected_message)
def test_basecamp_makes_message_trashed(self):
# type: () -> None
expected_message = u"Tomasz trashed the message [Message Title new](https://3.basecamp.com/3688623/buckets/2957043/messages/430680605)"
self._send_and_test_message('message_trashed', expected_message)
def test_basecamp_makes_message_unarchived(self):
# type: () -> None
expected_message = u"Tomasz unarchived the message [Message Title new](https://3.basecamp.com/3688623/buckets/2957043/messages/430680605)"
self._send_and_test_message('message_unarchived', expected_message)
def test_basecamp_makes_todo_list_created(self):
# type: () -> None
expected_message = u"Tomasz created the todo list [NEW TO DO LIST](https://3.basecamp.com/3688623/buckets/2957043/todolists/427050190)"
self._send_and_test_message('todo_list_created', expected_message)
def test_basecamp_makes_todo_list_description_changed(self):
# type: () -> None
expected_message = u"Tomasz changed description of the todo list [NEW TO DO LIST](https://3.basecamp.com/3688623/buckets/2957043/todolists/427050190)"
self._send_and_test_message('todo_list_description_changed', expected_message)
def test_basecamp_makes_todo_list_modified(self):
# type: () -> None
expected_message = u"Tomasz changed name of the todo list [NEW Name TO DO LIST](https://3.basecamp.com/3688623/buckets/2957043/todolists/427050190)"
self._send_and_test_message('todo_list_name_changed', expected_message)
def test_basecamp_makes_todo_assignment_changed(self):
# type: () -> None
expected_message = u"Tomasz changed assignment of the todo task [New task](https://3.basecamp.com/3688623/buckets/2957043/todos/427055624)"
self._send_and_test_message('todo_assignment_changed', expected_message)
def test_basecamp_makes_todo_completed(self):
# type: () -> None
expected_message = u"Tomasz completed the todo task [New task](https://3.basecamp.com/3688623/buckets/2957043/todos/427055624)"
self._send_and_test_message('todo_completed', expected_message)
def test_basecamp_makes_todo_created(self):
# type: () -> None
expected_message = u"Tomasz created the todo task [New task](https://3.basecamp.com/3688623/buckets/2957043/todos/427055624)"
self._send_and_test_message('todo_created', expected_message)
def test_basecamp_makes_comment_created(self):
# type: () -> None
expected_message = u"Tomasz created the [comment](https://3.basecamp.com/3688623/buckets/2957043/todos/427055624#__recording_427058780) of the task [New task](https://3.basecamp.com/3688623/buckets/2957043/todos/427055624)"
self._send_and_test_message('comment_created', expected_message)
def _send_and_test_message(self, fixture_name, expected_message):
# type: (Text, Text) -> None
self.send_and_test_stream_message(fixture_name, self.EXPECTED_SUBJECT, expected_message)

View File

@@ -0,0 +1,141 @@
from __future__ import absolute_import
import re
import logging
from typing import Any, Dict, Text
from django.http import HttpRequest, HttpResponse
from zerver.lib.actions import check_send_message
from zerver.lib.response import json_success, json_error
from zerver.decorator import REQ, has_request_variables, api_key_only_webhook_view
from zerver.models import UserProfile, Client
from .support_event import SUPPORT_EVENTS
DOCUMENT_TEMPLATE = "{user_name} {verb} the document [{title}]({url})"
QUESTION_TEMPLATE = "{user_name} {verb} the question [{title}]({url})"
QUESTIONS_ANSWER_TEMPLATE = "{user_name} {verb} the [answer]({answer_url}) of the question [{question_title}]({question_url})"
COMMENT_TEMPLATE = "{user_name} {verb} the [comment]({answer_url}) of the task [{task_title}]({task_url})"
MESSAGE_TEMPLATE = "{user_name} {verb} the message [{title}]({url})"
TODO_LIST_TEMPLATE = "{user_name} {verb} the todo list [{title}]({url})"
TODO_TEMPLATE = "{user_name} {verb} the todo task [{title}]({url})"
@api_key_only_webhook_view('Basecamp')
@has_request_variables
def api_basecamp_webhook(request, user_profile, client, payload=REQ(argument_type='body'),
stream=REQ(default='basecamp')):
# type: (HttpRequest, UserProfile, Client, Dict[str, Any], Text) -> HttpResponse
event = get_event_type(payload)
if event not in SUPPORT_EVENTS:
logging.warning("Basecamp {} event is not supported".format(event))
return json_success()
subject = get_project_name(payload)
if event.startswith('document_'):
body = get_document_body(event, payload)
elif event.startswith('question_answer_'):
body = get_questions_answer_body(event, payload)
elif event.startswith('question_'):
body = get_questions_body(event, payload)
elif event.startswith('message_'):
body = get_message_body(event, payload)
elif event.startswith('todolist_'):
body = get_todo_list_body(event, payload)
elif event.startswith('todo_'):
body = get_todo_body(event, payload)
elif event.startswith('comment_'):
body = get_comment_body(event, payload)
else:
logging.warning("Basecamp handling of {} event is not implemented".format(event))
return json_success()
check_send_message(user_profile, client, 'stream', [stream], subject, body)
return json_success()
def get_project_name(payload):
# type: (Dict[str, Any]) -> Text
return payload['recording']['bucket']['name']
def get_event_type(payload):
# type: (Dict[str, Any]) -> Text
return payload['kind']
def get_event_creator(payload):
# type: (Dict[str, Any]) -> Text
return payload['creator']['name']
def get_subject_url(payload):
# type: (Dict[str, Any]) -> Text
return payload['recording']['app_url']
def get_subject_title(payload):
# type: (Dict[str, Any]) -> Text
return payload['recording']['title']
def get_verb(event, prefix):
# type: (Text, Text) -> Text
verb = event.replace(prefix, '')
if verb == 'active':
return 'activated'
matched = re.match(r"(?P<subject>[A-z]*)_changed", verb)
if matched:
return "changed {} of".format(matched.group('subject'))
return verb
def get_document_body(event, payload):
# type: (Text, Dict[str, Any]) -> Text
return get_generic_body(event, payload, 'document_', DOCUMENT_TEMPLATE)
def get_questions_answer_body(event, payload):
# type: (Text, Dict[str, Any]) -> Text
verb = get_verb(event, 'question_answer_')
question = payload['recording']['parent']
return QUESTIONS_ANSWER_TEMPLATE.format(
user_name=get_event_creator(payload),
verb=verb,
answer_url=get_subject_url(payload),
question_title=question['title'],
question_url=question['app_url']
)
def get_comment_body(event, payload):
# type: (Text, Dict[str, Any]) -> Text
verb = get_verb(event, 'comment_')
task = payload['recording']['parent']
return COMMENT_TEMPLATE.format(
user_name=get_event_creator(payload),
verb=verb,
answer_url=get_subject_url(payload),
task_title=task['title'],
task_url=task['app_url']
)
def get_questions_body(event, payload):
# type: (Text, Dict[str, Any]) -> Text
return get_generic_body(event, payload, 'question_', QUESTION_TEMPLATE)
def get_message_body(event, payload):
# type: (Text, Dict[str, Any]) -> Text
return get_generic_body(event, payload, 'message_', MESSAGE_TEMPLATE)
def get_todo_list_body(event, payload):
# type: (Text, Dict[str, Any]) -> Text
return get_generic_body(event, payload, 'todolist_', TODO_LIST_TEMPLATE)
def get_todo_body(event, payload):
# type: (Text, Dict[str, Any]) -> Text
return get_generic_body(event, payload, 'todo_', TODO_TEMPLATE)
def get_generic_body(event, payload, prefix, template):
# type: (Text, Dict[str, Any], Text, Text) -> Text
verb = get_verb(event, prefix)
return template.format(
user_name=get_event_creator(payload),
verb=verb,
title=get_subject_title(payload),
url=get_subject_url(payload),
)