diff --git a/static/images/integrations/facebook/001.png b/static/images/integrations/facebook/001.png
new file mode 100644
index 0000000000..186635ea60
Binary files /dev/null and b/static/images/integrations/facebook/001.png differ
diff --git a/static/images/integrations/facebook/002.png b/static/images/integrations/facebook/002.png
new file mode 100644
index 0000000000..4a3c9cee2a
Binary files /dev/null and b/static/images/integrations/facebook/002.png differ
diff --git a/static/images/integrations/facebook/003.png b/static/images/integrations/facebook/003.png
new file mode 100644
index 0000000000..501360f8bd
Binary files /dev/null and b/static/images/integrations/facebook/003.png differ
diff --git a/static/images/integrations/facebook/004.png b/static/images/integrations/facebook/004.png
new file mode 100644
index 0000000000..5062cd5d9d
Binary files /dev/null and b/static/images/integrations/facebook/004.png differ
diff --git a/static/images/integrations/facebook/005.png b/static/images/integrations/facebook/005.png
new file mode 100644
index 0000000000..e549a2ecf3
Binary files /dev/null and b/static/images/integrations/facebook/005.png differ
diff --git a/static/images/integrations/facebook/006.png b/static/images/integrations/facebook/006.png
new file mode 100644
index 0000000000..8c9ba52ccf
Binary files /dev/null and b/static/images/integrations/facebook/006.png differ
diff --git a/static/images/integrations/facebook/007.png b/static/images/integrations/facebook/007.png
new file mode 100644
index 0000000000..8c140ab1b7
Binary files /dev/null and b/static/images/integrations/facebook/007.png differ
diff --git a/static/images/integrations/facebook/008.png b/static/images/integrations/facebook/008.png
new file mode 100644
index 0000000000..672b6e4bc1
Binary files /dev/null and b/static/images/integrations/facebook/008.png differ
diff --git a/static/images/integrations/logos/facebook.svg b/static/images/integrations/logos/facebook.svg
new file mode 100644
index 0000000000..3dd9c97d7c
--- /dev/null
+++ b/static/images/integrations/logos/facebook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py
index 7db8f9fbb4..6313ca854c 100644
--- a/zerver/lib/integrations.py
+++ b/zerver/lib/integrations.py
@@ -368,7 +368,8 @@ WEBHOOK_INTEGRATIONS = [
WebhookIntegration('zapier', ['meta-integration']),
WebhookIntegration('zendesk', ['customer-support']),
WebhookIntegration('gci', ['misc'], display_name='Google Code-in',
- stream_name='gci')
+ stream_name='gci'),
+ WebhookIntegration('facebook', ['communication'], display_name='Facebook')
] # type: List[WebhookIntegration]
INTEGRATIONS = {
diff --git a/zerver/webhooks/facebook/__init__.py b/zerver/webhooks/facebook/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/zerver/webhooks/facebook/doc.md b/zerver/webhooks/facebook/doc.md
new file mode 100644
index 0000000000..7c24d6fd52
--- /dev/null
+++ b/zerver/webhooks/facebook/doc.md
@@ -0,0 +1,75 @@
+{!create-stream.md!}
+
+Next, on your {{ settings_html|safe }},
+[create a bot](/help/add-a-bot-or-integration) for
+{{ integration_display_name }}. Make sure that you select
+**Incoming webhook** as the **Bot type**:
+
+
+
+The API key for an incoming webhook bot cannot be used to read messages out
+of Zulip. Thus, using an incoming webhook bot lowers the security risk of
+exposing the bot's API key to a third-party service.
+
+Construct the URL for the {{ integration_display_name }}
+bot using the bot's API key and the desired stream name:
+
+`{{ api_url }}{{ integration_url }}?api_key=abcdefgh&stream={{ recommended_stream_name }}&token=sampletoken`
+
+Modify the parameters of the URL above, where `api_key` is the API key
+of your Zulip bot, and `stream` is the stream name you want the
+notifications sent to.
+
+`token` is an arbitrary string of your choosing that can be used to confirm to your
+ server that the request is valid. This string will be included in Facebook's
+ incoming payloads each time they send your server a verification request.
+
+{!append-stream-name.md!}
+
+### Configuring the webhook
+
+Sign In to the following URL:
+
+Next, click on **+ Add a New App** button.
+
+
+
+Then, fill in the following form to create a new Facebook app:
+
+
+
+Next, under **Webhooks**, click on **Set up**:
+
+
+
+Choose a category for the webhook:
+
+
+
+This guide explains how to subscribe to a "feed" in the **User** category.
+
+Select the **User** category, and click on **Subscribe to this topic**.
+Fill in the **Edit User Subscription** form as follows:
+
+1. **Callback URL**: enter the webhook URL created above.
+2. **Verify Token**: enter the token you chose above. For instance, in this example you may enter **sampletoken**
+3. Activate the **Include Values** option.
+4. Click on **Verify and Save**.
+
+The resulting form would look like:
+
+
+
+Finally, click **Subscribe** and **Test** in the **feed** row, like so:
+
+
+
+Click on **Send to My Server** and a test message will be sent to your Zulip server.
+
+
+
+{!congrats.md!}
+
+
+
+**This integration is not created by, affiliated with, or supported by Facebook, Inc.**
diff --git a/zerver/webhooks/facebook/fixtures/page_conversation.json b/zerver/webhooks/facebook/fixtures/page_conversations.json
similarity index 100%
rename from zerver/webhooks/facebook/fixtures/page_conversation.json
rename to zerver/webhooks/facebook/fixtures/page_conversations.json
diff --git a/zerver/webhooks/facebook/fixtures/permissions_ads_management.json b/zerver/webhooks/facebook/fixtures/permissions_ads_management.json
index 4fcedc3158..350128ef56 100644
--- a/zerver/webhooks/facebook/fixtures/permissions_ads_management.json
+++ b/zerver/webhooks/facebook/fixtures/permissions_ads_management.json
@@ -1,13 +1,22 @@
{
- "entry":[
- {
- "time":1514912190,
- "id":"0",
- "changed_fields":[
- "ads_management"
- ],
- "uid":"0"
- }
- ],
- "object":"permissions"
+ "entry": [
+ {
+ "changes": [
+ {
+ "field": "ads_management",
+ "value": {
+ "target_ids": [
+ "123123123123123",
+ "321321321321321"
+ ],
+ "verb": "granted"
+ }
+ }
+ ],
+ "id": "0",
+ "time": 1515252883,
+ "uid": "0"
+ }
+ ],
+ "object": "permissions"
}
diff --git a/zerver/webhooks/facebook/fixtures/permissions_manage_pages.json b/zerver/webhooks/facebook/fixtures/permissions_manage_pages.json
index 7f33be3d3b..307ebd24f2 100644
--- a/zerver/webhooks/facebook/fixtures/permissions_manage_pages.json
+++ b/zerver/webhooks/facebook/fixtures/permissions_manage_pages.json
@@ -1,13 +1,22 @@
{
- "entry":[
- {
- "time":1514912232,
- "id":"0",
- "changed_fields":[
- "manage_pages"
- ],
- "uid":"0"
- }
- ],
- "object":"permissions"
+ "entry": [
+ {
+ "changes": [
+ {
+ "field": "manage_pages",
+ "value": {
+ "target_ids": [
+ "123123123123123",
+ "321321321321321"
+ ],
+ "verb": "granted"
+ }
+ }
+ ],
+ "id": "0",
+ "time": 1515254831,
+ "uid": "0"
+ }
+ ],
+ "object": "permissions"
}
diff --git a/zerver/webhooks/facebook/fixtures/users_email.json b/zerver/webhooks/facebook/fixtures/user_email.json
similarity index 100%
rename from zerver/webhooks/facebook/fixtures/users_email.json
rename to zerver/webhooks/facebook/fixtures/user_email.json
diff --git a/zerver/webhooks/facebook/fixtures/users_feed.json b/zerver/webhooks/facebook/fixtures/user_feed.json
similarity index 100%
rename from zerver/webhooks/facebook/fixtures/users_feed.json
rename to zerver/webhooks/facebook/fixtures/user_feed.json
diff --git a/zerver/webhooks/facebook/tests.py b/zerver/webhooks/facebook/tests.py
new file mode 100644
index 0000000000..eb30db4ceb
--- /dev/null
+++ b/zerver/webhooks/facebook/tests.py
@@ -0,0 +1,110 @@
+from typing import Optional, Text
+
+from zerver.lib.test_classes import WebhookTestCase
+
+class FacebookTests(WebhookTestCase):
+ STREAM_NAME = 'Facebook'
+ URL_TEMPLATE = "/api/v1/external/facebook?api_key={api_key}&stream={stream}&token=aaaa"
+ FIXTURE_DIR_NAME = 'facebook'
+
+ def test_application_plugin_comment(self) -> None:
+ expected_subject = u'application notification'
+ expected_message = u'**plugin_comment** received'\
+ u'\n**Test User:**'\
+ u'\n```quote'\
+ u'\nTest Comment'\
+ u'\n```'
+ self.send_and_test_stream_message('application_plugin_comment',
+ expected_subject, expected_message)
+
+ def test_application_plugin_comment_reply(self) -> None:
+ expected_subject = u'application notification'
+ expected_message = u'**plugin_comment_reply** received'\
+ u'\n**Test User 1:** (Parent)'\
+ u'\n```quote'\
+ u'\nTest Parent Comment'\
+ u'\n```'\
+ u'\n**Test User:**'\
+ u'\n```quote'\
+ u'\n```quote'\
+ u'\nTest Comment'\
+ u'\n```'\
+ u'\n```'
+ self.send_and_test_stream_message('application_plugin_comment_reply',
+ expected_subject, expected_message)
+
+ def test_page_conversations(self) -> None:
+ expected_subject = u'page notification'
+ expected_message = u'Updated **conversations**'\
+ u'\n[Open conversations...](https://www.facebook.com/'\
+ u'4444444/t_mid.14833205540:9182a4e489)'
+ self.send_and_test_stream_message('page_conversations',
+ expected_subject, expected_message)
+
+ def test_page_website_test(self) -> None:
+ expected_subject = u'page notification'
+ expected_message = u'Changed **website**'
+ self.send_and_test_stream_message('page_website',
+ expected_subject, expected_message)
+
+ def test_permissions_ads_management(self) -> None:
+ expected_subject = u'permissions notification'
+ expected_message = u'**ads_management permission** changed'\
+ u'\n* granted'\
+ u'\n * 123123123123123'\
+ u'\n * 321321321321321'
+ self.send_and_test_stream_message('permissions_ads_management',
+ expected_subject, expected_message)
+
+ def test_permissions_manage_pages(self) -> None:
+ expected_subject = u'permissions notification'
+ expected_message = u'**manage_pages permission** changed'\
+ u'\n* granted'\
+ u'\n * 123123123123123'\
+ u'\n * 321321321321321'
+ self.send_and_test_stream_message('permissions_manage_pages',
+ expected_subject, expected_message)
+
+ def test_user_email(self) -> None:
+ expected_subject = u'user notification'
+ expected_message = u'Changed **email**'\
+ u'\nTo: *example_email@facebook.com*'
+ self.send_and_test_stream_message('user_email',
+ expected_subject, expected_message)
+
+ def test_user_feed(self) -> None:
+ expected_subject = u'user notification'
+ expected_message = u'Changed **feed**'
+ self.send_and_test_stream_message('user_feed',
+ expected_subject, expected_message)
+
+ def test_webhook_verify_request(self) -> None:
+ self.subscribe(self.test_user, self.STREAM_NAME)
+ get_params = {'stream_name': self.STREAM_NAME,
+ 'hub.challenge': '9B2SVL4orbt5DxLMqJHI6pOTipTqingt2YFMIO0g06E',
+ 'api_key': self.test_user.api_key,
+ 'hub.mode': 'subscribe',
+ 'hub.verify_token': 'aaaa',
+ 'token': 'aaaa'}
+ result = self.client_get(self.url, get_params)
+ self.assert_in_response('9B2SVL4orbt5DxLMqJHI6pOTipTqingt2YFMIO0g06E', result)
+
+ def test_error_webhook_verify_request_wrong_token(self) -> None:
+ self.subscribe(self.test_user, self.STREAM_NAME)
+ get_params = {'stream_name': self.STREAM_NAME,
+ 'hub.challenge': '9B2SVL4orbt5DxLMqJHI6pOTipTqingt2YFMIO0g06E',
+ 'api_key': self.test_user.api_key,
+ 'hub.mode': 'subscribe',
+ 'hub.verify_token': 'aaaa',
+ 'token': 'wrong_token'}
+ result = self.client_get(self.url, get_params)
+ self.assert_in_response('Error: Token is wrong', result)
+
+ def test_error_webhook_verify_request_unsupported_method(self) -> None:
+ self.subscribe(self.test_user, self.STREAM_NAME)
+ get_params = {'stream_name': self.STREAM_NAME,
+ 'api_key': self.test_user.api_key,
+ 'hub.mode': 'unsupported_method',
+ 'token': 'aaaa'}
+ result = self.client_get(self.url, get_params)
+ self.assert_in_response('Error: Unsupported method', result)
diff --git a/zerver/webhooks/facebook/view.py b/zerver/webhooks/facebook/view.py
new file mode 100644
index 0000000000..486bf71eee
--- /dev/null
+++ b/zerver/webhooks/facebook/view.py
@@ -0,0 +1,116 @@
+from typing import Any, Dict, Optional, Text
+
+from django.http import HttpRequest, HttpResponse, QueryDict
+from django.utils.translation import ugettext as _
+
+from zerver.decorator import api_key_only_webhook_view
+from zerver.lib.actions import check_send_stream_message, create_stream_if_needed
+from zerver.lib.request import REQ, has_request_variables
+from zerver.lib.response import json_success, json_error
+from zerver.models import UserProfile
+import json
+
+class UnknownEventType(Exception):
+ pass
+
+def user_event(payload: Dict[Text, Any]) -> Text:
+ field = payload['entry'][0]['changes'][0]['field']
+ message = "Changed **{field}**".format(field=field)
+ if field == "email":
+ if payload['entry'][0]['changes'][0]['value'] is not None:
+ message = message + '\nTo: *{email}*'.format(
+ email=payload['entry'][0]['changes'][0]['value']
+ )
+ return message
+
+def page_event(payload: Dict[Text, Any]) -> Text:
+ field = payload['entry'][0]['changes'][0]['field']
+ message = ''
+ if field == 'conversations':
+ message = message + 'Updated **conversations**'
+ message = message + '\n[Open conversations...](https://www.facebook.com/'\
+ '{page_id}/{thread_id})'.format(
+ page_id=payload['entry'][0]['changes'][0]['value']['page_id'],
+ thread_id=payload['entry'][0]['changes'][0]['value']['thread_id']
+ )
+ elif field == 'website':
+ message = message + 'Changed **website**'
+ return message
+
+def permissions_event(payload: Dict[Text, Any]) -> Text:
+ field = payload['entry'][0]['changes'][0]['field']
+ message = '**{field} permission** changed'.format(field=field)
+ if field == 'ads_management':
+ message = message + '\n* {verb}'.format(
+ verb=payload['entry'][0]['changes'][0]['value']['verb']
+ )
+ for id in payload['entry'][0]['changes'][0]['value']['target_ids']:
+ message = message + '\n * {id}'.format(id=id)
+ elif field == 'manage_pages':
+ message = message + '\n* {verb}'.format(
+ verb=payload['entry'][0]['changes'][0]['value']['verb']
+ )
+ for id in payload['entry'][0]['changes'][0]['value']['target_ids']:
+ message = message + '\n * {id}'.format(id=id)
+ return message
+
+def application_event(payload: Dict[Text, Any]) -> Text:
+ field = payload['entry'][0]['changes'][0]['field']
+ message = '**{field}** received'.format(field=field)
+ if field == 'plugin_comment':
+ message = message + '\n**{msg_user}:**\n```quote\n{message}\n```'.format(
+ msg_user=payload['entry'][0]['changes'][0]['value']['from']['name'],
+ message=payload['entry'][0]['changes'][0]['value']['message']
+ )
+ if field == 'plugin_comment_reply':
+ message = message + '\n**{prt_msg_user}:** (Parent)\n'\
+ '```quote\n{prt_message}\n```'.format(
+ prt_msg_user=payload['entry'][0]['changes'][0]['value']['parent']['from']['name'],
+ prt_message=payload['entry'][0]['changes'][0]['value']['parent']['message']
+ )
+ message = message + '\n**{cld_msg_user}:**\n```quote\n'\
+ '```quote\n{cld_message}\n```\n```'.format(
+ cld_msg_user=payload['entry'][0]['changes'][0]['value']['from']['name'],
+ cld_message=payload['entry'][0]['changes'][0]['value']['message']
+ )
+ return message
+
+@api_key_only_webhook_view("Facebook")
+@has_request_variables
+def api_facebook_webhook(request: HttpRequest, user_profile: UserProfile,
+ stream: Text=REQ(default='Facebook'), token: Text=REQ()) -> HttpResponse:
+
+ if request.method == 'GET': # facebook webhook verify
+ if request.GET.get("hub.mode") == 'subscribe':
+ if request.GET.get('hub.verify_token') == token:
+ return HttpResponse(request.GET.get('hub.challenge'))
+ else:
+ return json_error(_('Error: Token is wrong'))
+ return json_error(_('Error: Unsupported method'))
+
+ payload = json.loads(request.body.decode("UTF-8"))
+ event = get_event(payload)
+ if event is not None:
+ body = get_body_based_on_event(event)(payload)
+ subject = event + " notification"
+ check_send_stream_message(user_profile, request.client,
+ stream, subject, body)
+ return json_success()
+
+# This integration doesn't support instant_workflow, instagram
+# and certificate_transparency event.
+EVENTS_FUNCTION_MAPPER = {
+ 'user': user_event,
+ 'page': page_event,
+ 'permissions': permissions_event,
+ 'application': application_event
+}
+
+def get_event(payload: Dict[Text, Any]) -> Optional[Text]:
+ event = payload['object']
+ if event in EVENTS_FUNCTION_MAPPER:
+ return event
+ raise UnknownEventType(u"OEvent '{}' is unknown and cannot be handled".format(event)) # nocoverage
+
+def get_body_based_on_event(event: Text) -> Any:
+ return EVENTS_FUNCTION_MAPPER[event]