From 499274e7a1c38582fdced4a2fbade241b786a168 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Tue, 26 Nov 2024 15:39:56 +0200 Subject: [PATCH] Support updating existing channels for Facebook and Instagram, remove old refresh token views --- temba/channels/models.py | 1 + temba/channels/types/facebookapp/tests.py | 93 ++-------- temba/channels/types/facebookapp/type.py | 16 +- temba/channels/types/facebookapp/views.py | 138 ++------------- temba/channels/types/instagram/tests.py | 108 ++---------- temba/channels/types/instagram/type.py | 18 +- temba/channels/types/instagram/views.py | 159 +++--------------- temba/channels/types/whatsapp/type.py | 4 +- temba/channels/views.py | 11 +- .../types/facebookapp/refresh_token.html | 78 --------- .../types/instagram/refresh_token.html | 76 --------- 11 files changed, 78 insertions(+), 624 deletions(-) delete mode 100644 templates/channels/types/facebookapp/refresh_token.html delete mode 100644 templates/channels/types/instagram/refresh_token.html diff --git a/temba/channels/models.py b/temba/channels/models.py index 7e78f5627e2..bc0651ae093 100644 --- a/temba/channels/models.py +++ b/temba/channels/models.py @@ -94,6 +94,7 @@ class Category(Enum): beta_only = False unique_addresses = False + matching_addresses_updates = False # the courier handling URL, will be wired automatically for use in templates, but wired to a null handler courier_url = None diff --git a/temba/channels/types/facebookapp/tests.py b/temba/channels/types/facebookapp/tests.py index dc8a71d2875..3c73c4eba22 100644 --- a/temba/channels/types/facebookapp/tests.py +++ b/temba/channels/types/facebookapp/tests.py @@ -25,7 +25,7 @@ def setUp(self): address="12345", role="SR", schemes=["facebook"], - config={"auth_token": "09876543"}, + config={"auth_token": "09876543", "page_name": "FirstName"}, ) @override_settings(FACEBOOK_APPLICATION_ID="FB_APP_ID", FACEBOOK_APPLICATION_SECRET="FB_APP_SECRET") @@ -172,6 +172,11 @@ def test_claim_long_name(self, mock_get, mock_post): @patch("requests.post") @patch("requests.get") def test_claim_already_connected(self, mock_get, mock_post): + channel = Channel.objects.get(address="12345", channel_type="FBA") + self.assertEqual(channel.config[Channel.CONFIG_AUTH_TOKEN], "09876543") + self.assertEqual(channel.config[Channel.CONFIG_PAGE_NAME], "FirstName") + self.assertEqual(channel.name, "Facebook") + token = "x" * 200 name = "Temba" @@ -180,7 +185,7 @@ def test_claim_already_connected(self, mock_get, mock_post): MockResponse(200, json.dumps({"access_token": f"long-life-user-{token}"})), MockResponse( 200, - json.dumps({"data": [{"name": name, "id": "12345", "access_token": f"page-long-life-{token}"}]}), + json.dumps({"data": [{"name": name, "id": "12345", "access_token": f"page-long-life-update-{token}"}]}), ), ] @@ -206,7 +211,10 @@ def test_claim_already_connected(self, mock_get, mock_post): post_data["page_name"] = name response = self.client.post(url, post_data, follow=True) - self.assertContains(response, "This channel is already connected in this workspace.") + channel = Channel.objects.get(address="12345", channel_type="FBA") + self.assertEqual(channel.config[Channel.CONFIG_AUTH_TOKEN], f"page-long-life-update-{token}") + self.assertEqual(channel.config[Channel.CONFIG_PAGE_NAME], "Temba") + self.assertEqual(channel.name, "Facebook") mock_get.side_effect = [ MockResponse(200, json.dumps({"data": {"user_id": "098765", "expired_at": 100}})), @@ -246,85 +254,6 @@ def test_release(self, mock_delete): "https://graph.facebook.com/v18.0/12345/subscribed_apps", params={"access_token": "09876543"} ) - @override_settings(FACEBOOK_APPLICATION_ID="FB_APP_ID", FACEBOOK_APPLICATION_SECRET="FB_APP_SECRET") - @patch("requests.post") - @patch("requests.get") - def test_refresh_token(self, mock_get, mock_post): - token = "x" * 200 - - url = reverse("channels.types.facebookapp.refresh_token", args=(self.channel.uuid,)) - - self.login(self.admin) - - mock_post.return_value = MockResponse(200, json.dumps({"success": True})) - - mock_get.side_effect = [ - MockResponse(400, json.dumps({"error": "token invalid"})), - ] - - response = self.client.get(url) - self.assertContains(response, "Reconnect Facebook Page") - self.assertEqual(response.context["facebook_app_id"], "FB_APP_ID") - self.assertEqual(response.context["refresh_url"], url) - self.assertTrue(response.context["error_connect"]) - - mock_get.side_effect = [MockResponse(200, json.dumps({"data": {"is_valid": False}}))] - response = self.client.get(url) - self.assertContains(response, "Reconnect Facebook Page") - self.assertEqual(response.context["facebook_app_id"], "FB_APP_ID") - self.assertEqual(response.context["refresh_url"], url) - self.assertTrue(response.context["error_connect"]) - - mock_get.side_effect = [ - MockResponse(200, json.dumps({"data": {"is_valid": True}})), - MockResponse(200, json.dumps({"access_token": f"long-life-user-{token}"})), - MockResponse( - 200, - json.dumps({"data": [{"name": "Temba", "id": "12345", "access_token": f"page-long-life-{token}"}]}), - ), - ] - - response = self.client.get(url) - self.assertContains(response, "Reconnect Facebook Page") - self.assertEqual(response.context["facebook_app_id"], "FB_APP_ID") - self.assertEqual(response.context["refresh_url"], url) - self.assertFalse(response.context["error_connect"]) - - post_data = response.context["form"].initial - post_data["fb_user_id"] = "098765" - post_data["user_access_token"] = token - - response = self.client.post(url, post_data, follow=True) - - # assert our channel got created - channel = Channel.objects.get(address="12345", channel_type="FBA") - self.assertEqual(channel.config[Channel.CONFIG_AUTH_TOKEN], f"page-long-life-{token}") - self.assertEqual(channel.config[Channel.CONFIG_PAGE_NAME], "Temba") - self.assertEqual(channel.address, "12345") - - self.assertEqual(response.request["PATH_INFO"], reverse("channels.channel_read", args=[channel.uuid])) - - mock_get.assert_any_call( - "https://graph.facebook.com/oauth/access_token", - params={ - "grant_type": "fb_exchange_token", - "client_id": "FB_APP_ID", - "client_secret": "FB_APP_SECRET", - "fb_exchange_token": token, - }, - ) - mock_get.assert_any_call( - "https://graph.facebook.com/v18.0/098765/accounts", params={"access_token": f"long-life-user-{token}"} - ) - - mock_post.assert_any_call( - "https://graph.facebook.com/v18.0/12345/subscribed_apps", - data={ - "subscribed_fields": "messages,message_deliveries,messaging_optins,messaging_optouts,messaging_postbacks,message_reads,messaging_referrals,messaging_handovers" - }, - params={"access_token": f"page-long-life-{token}"}, - ) - def test_new_conversation_triggers(self): flow = self.create_flow("Test") diff --git a/temba/channels/types/facebookapp/type.py b/temba/channels/types/facebookapp/type.py index 2a27aba83f0..18945cc6d4a 100644 --- a/temba/channels/types/facebookapp/type.py +++ b/temba/channels/types/facebookapp/type.py @@ -1,14 +1,13 @@ import requests from django.conf import settings -from django.urls import re_path from django.utils.translation import gettext_lazy as _ from temba.contacts.models import URN from temba.triggers.models import Trigger from ...models import Channel, ChannelType -from .views import ClaimView, RefreshToken +from .views import ClaimView class FacebookAppType(ChannelType): @@ -21,6 +20,7 @@ class FacebookAppType(ChannelType): category = ChannelType.Category.SOCIAL_MEDIA unique_addresses = True + matching_addresses_updates = True courier_url = r"^fba/receive" schemes = [URN.FACEBOOK_SCHEME] @@ -32,15 +32,9 @@ class FacebookAppType(ChannelType): ) % {"link": 'Facebook'} claim_view = ClaimView - menu_items = [dict(label=_("Reconnect Facebook Page"), view_name="channels.types.facebookapp.refresh_token")] - - def get_urls(self): - return [ - self.get_claim_url(), - re_path( - r"^(?P[a-z0-9\-]+)/refresh_token/$", RefreshToken.as_view(channel_type=self), name="refresh_token" - ), - ] + menu_items = [ + dict(label=_("Reconnect Facebook Page"), view_name="channels.types.facebookapp.claim", obj_view=False) + ] def deactivate(self, channel): config = channel.config diff --git a/temba/channels/types/facebookapp/views.py b/temba/channels/types/facebookapp/views.py index 30628074bcb..f0afac43f87 100644 --- a/temba/channels/types/facebookapp/views.py +++ b/temba/channels/types/facebookapp/views.py @@ -1,16 +1,15 @@ import requests -from smartmin.views import SmartFormView, SmartModelActionView +from smartmin.views import SmartFormView from django import forms from django.conf import settings from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from temba.orgs.views.mixins import OrgObjPermsMixin from temba.utils.text import truncate from ...models import Channel -from ...views import ChannelTypeMixin, ClaimViewMixin +from ...views import ClaimViewMixin class ClaimView(ClaimViewMixin, SmartFormView): @@ -129,127 +128,16 @@ def form_valid(self, form): Channel.CONFIG_PAGE_NAME: name, } - self.object = Channel.create( - self.request.org, self.request.user, None, self.channel_type, name=name, address=page_id, config=config - ) - - return super().form_valid(form) - - -class RefreshToken(ChannelTypeMixin, OrgObjPermsMixin, SmartModelActionView, SmartFormView): - class Form(forms.Form): - user_access_token = forms.CharField(min_length=32, required=True, help_text=_("The User Access Token")) - fb_user_id = forms.CharField( - required=True, help_text=_("The Facebook User ID of the admin that connected the channel") - ) - - slug_url_kwarg = "uuid" - success_url = "uuid@channels.channel_read" - form_class = Form - permission = "channels.channel_claim" - fields = () - template_name = "channels/types/facebookapp/refresh_token.html" - title = _("Reconnect Facebook Page") - menu_path = "/settings/workspace" - - def derive_menu_path(self): - return f"/settings/channels/{self.get_object().uuid}" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["refresh_url"] = reverse("channels.types.facebookapp.refresh_token", args=(self.object.uuid,)) - - app_id = settings.FACEBOOK_APPLICATION_ID - app_secret = settings.FACEBOOK_APPLICATION_SECRET - - context["facebook_app_id"] = app_id - - context["facebook_login_messenger_config_id"] = settings.FACEBOOK_LOGIN_MESSENGER_CONFIG_ID - - url = "https://graph.facebook.com/v18.0/debug_token" - params = { - "access_token": f"{app_id}|{app_secret}", - "input_token": self.object.config[Channel.CONFIG_AUTH_TOKEN], - } - resp = requests.get(url, params=params) - - error_connect = False - if resp.status_code != 200: - error_connect = True + existing_channel = Channel.objects.filter( + org=self.request.org, address=page_id, channel_type=self.channel_type.code + ).first() + if existing_channel: + existing_channel.config = config + existing_channel.save() + self.object = existing_channel else: - valid_token = resp.json().get("data", dict()).get("is_valid", False) - if not valid_token: - error_connect = True - - context["error_connect"] = error_connect - - return context - - def get_queryset(self): - return self.request.org.channels.filter(is_active=True, channel_type=self.channel_type.code) + self.object = Channel.create( + self.request.org, self.request.user, None, self.channel_type, name=name, address=page_id, config=config + ) - def execute_action(self): - form = self.form - channel = self.object - - auth_token = form.data["user_access_token"] - fb_user_id = form.data["fb_user_id"] - - page_id = channel.address - - app_id = settings.FACEBOOK_APPLICATION_ID - app_secret = settings.FACEBOOK_APPLICATION_SECRET - - # get user long lived access token - url = "https://graph.facebook.com/oauth/access_token" - params = { - "grant_type": "fb_exchange_token", - "client_id": app_id, - "client_secret": app_secret, - "fb_exchange_token": auth_token, - } - - response = requests.get(url, params=params) - - if response.status_code != 200: # pragma: no cover - raise Exception("Failed to get a user long lived token") - - long_lived_auth_token = response.json().get("access_token", "") - - if long_lived_auth_token == "": # pragma: no cover - raise Exception("Empty user access token!") - - url = f"https://graph.facebook.com/v18.0/{fb_user_id}/accounts" - params = {"access_token": long_lived_auth_token} - - response = requests.get(url, params=params) - - if response.status_code != 200: # pragma: no cover - raise Exception("Failed to get a page long lived token") - - response_json = response.json() - - page_access_token = "" - for elt in response_json["data"]: - if elt["id"] == str(page_id): - page_access_token = elt["access_token"] - name = elt["name"] - break - - if page_access_token == "": # pragma: no cover - raise Exception("Empty page access token!") - - url = f"https://graph.facebook.com/v18.0/{page_id}/subscribed_apps" - params = {"access_token": page_access_token} - data = { - "subscribed_fields": "messages,message_deliveries,messaging_optins,messaging_optouts,messaging_postbacks,message_reads,messaging_referrals,messaging_handovers" - } - - response = requests.post(url, data=data, params=params) - - if response.status_code != 200: # pragma: no cover - raise Exception("Failed to subscribe to app for webhook events") - - channel.config[Channel.CONFIG_AUTH_TOKEN] = page_access_token - channel.config[Channel.CONFIG_PAGE_NAME] = name - channel.save(update_fields=["config"]) + return super().form_valid(form) diff --git a/temba/channels/types/instagram/tests.py b/temba/channels/types/instagram/tests.py index 6d5f332ff2b..3b6df34453e 100644 --- a/temba/channels/types/instagram/tests.py +++ b/temba/channels/types/instagram/tests.py @@ -26,7 +26,7 @@ def setUp(self): address="019283", role="SR", schemes=["instagram"], - config={"auth_token": "09876543", "page_id": "123456"}, + config={"auth_token": "09876543", "page_name": "FirstName", "page_id": "123456"}, ) @override_settings(FACEBOOK_APPLICATION_ID="FB_APP_ID", FACEBOOK_APPLICATION_SECRET="FB_APP_SECRET") @@ -206,6 +206,12 @@ def test_claim_long_name(self, mock_get, mock_post): @patch("requests.post") @patch("requests.get") def test_claim_already_connected(self, mock_get, mock_post): + channel = Channel.objects.get(address="019283", channel_type="IG") + self.assertEqual(channel.config[Channel.CONFIG_AUTH_TOKEN], "09876543") + self.assertEqual(channel.config[Channel.CONFIG_PAGE_NAME], "FirstName") + self.assertEqual(channel.config["page_id"], "123456") + self.assertEqual(channel.name, "Instagram") + name = "Temba" mock_get.side_effect = [ @@ -219,7 +225,7 @@ def test_claim_already_connected(self, mock_get, mock_post): { "name": name, "id": "123456", - "access_token": self.long_life_page_token, + "access_token": self.long_life_page_token + "-updated", } ] } @@ -253,7 +259,11 @@ def test_claim_already_connected(self, mock_get, mock_post): post_data["page_name"] = name response = self.client.post(url, post_data, follow=True) - self.assertContains(response, "This channel is already connected in this workspace.") + channel = Channel.objects.get(address="019283", channel_type="IG") + self.assertEqual(channel.config[Channel.CONFIG_AUTH_TOKEN], self.long_life_page_token + "-updated") + self.assertEqual(channel.config[Channel.CONFIG_PAGE_NAME], "Temba") + self.assertEqual(channel.config["page_id"], 123456) + self.assertEqual(channel.name, "Instagram") mock_get.side_effect = [ MockResponse(200, json.dumps({"data": {"user_id": "098765", "expired_at": 100}})), @@ -341,98 +351,6 @@ def test_release(self, mock_delete): params={"access_token": "09876543"}, ) - @override_settings(FACEBOOK_APPLICATION_ID="FB_APP_ID", FACEBOOK_APPLICATION_SECRET="FB_APP_SECRET") - @patch("requests.post") - @patch("requests.get") - def test_refresh_token(self, mock_get, mock_post): - token = "x" * 200 - - url = reverse("channels.types.instagram.refresh_token", args=(self.channel.uuid,)) - - self.login(self.admin) - - mock_post.return_value = MockResponse(200, json.dumps({"success": True})) - - mock_get.side_effect = [ - MockResponse(400, json.dumps({"error": "token invalid"})), - ] - - response = self.client.get(url) - self.assertContains(response, "Reconnect Instagram Business Account") - self.assertEqual(response.context["facebook_app_id"], "FB_APP_ID") - self.assertEqual(response.context["refresh_url"], url) - self.assertTrue(response.context["error_connect"]) - - mock_get.side_effect = [MockResponse(200, json.dumps({"data": {"is_valid": False}}))] - response = self.client.get(url) - self.assertContains(response, "Reconnect Instagram Business Account") - self.assertEqual(response.context["facebook_app_id"], "FB_APP_ID") - self.assertEqual(response.context["refresh_url"], url) - self.assertTrue(response.context["error_connect"]) - - mock_get.side_effect = [ - MockResponse(200, json.dumps({"data": {"is_valid": True}})), - MockResponse(200, json.dumps({"access_token": self.long_life_page_token})), - MockResponse( - 200, - json.dumps( - { - "data": [ - { - "name": "Temba", - "id": "123456", - "access_token": self.long_life_page_token, - } - ] - } - ), - ), - ] - - response = self.client.get(url) - self.assertContains(response, "Reconnect Instagram Business Account") - self.assertEqual(response.context["facebook_app_id"], "FB_APP_ID") - self.assertEqual(response.context["refresh_url"], url) - self.assertFalse(response.context["error_connect"]) - - post_data = response.context["form"].initial - post_data["fb_user_id"] = "098765" - post_data["user_access_token"] = token - - response = self.client.post(url, post_data, follow=True) - - # assert our channel got created - channel = Channel.objects.get(address="019283", channel_type="IG") - self.assertEqual(channel.config[Channel.CONFIG_AUTH_TOKEN], self.long_life_page_token) - self.assertEqual(channel.config[Channel.CONFIG_PAGE_NAME], "Temba") - self.assertEqual(channel.address, "019283") - - self.assertEqual( - response.request["PATH_INFO"], - reverse("channels.channel_read", args=[channel.uuid]), - ) - - mock_get.assert_any_call( - "https://graph.facebook.com/oauth/access_token", - params={ - "grant_type": "fb_exchange_token", - "client_id": "FB_APP_ID", - "client_secret": "FB_APP_SECRET", - "fb_exchange_token": self.token, - }, - ) - - mock_get.assert_any_call( - "https://graph.facebook.com/v18.0/098765/accounts", - params={"access_token": self.long_life_page_token}, - ) - - mock_post.assert_any_call( - "https://graph.facebook.com/v18.0/123456/subscribed_apps", - data={"subscribed_fields": "messages,messaging_postbacks"}, - params={"access_token": self.long_life_page_token}, - ) - def test_get_error_ref_url(self): self.assertEqual( "https://developers.facebook.com/docs/instagram-api/reference/error-codes", diff --git a/temba/channels/types/instagram/type.py b/temba/channels/types/instagram/type.py index a36a2e73110..1803c146b68 100644 --- a/temba/channels/types/instagram/type.py +++ b/temba/channels/types/instagram/type.py @@ -1,13 +1,12 @@ import requests from django.conf import settings -from django.urls import re_path from django.utils.translation import gettext_lazy as _ from temba.contacts.models import URN from ...models import Channel, ChannelType -from .views import ClaimView, RefreshToken +from .views import ClaimView class InstagramType(ChannelType): @@ -20,6 +19,7 @@ class InstagramType(ChannelType): category = ChannelType.Category.SOCIAL_MEDIA unique_addresses = True + matching_addresses_updates = True courier_url = r"^ig/receive" schemes = [URN.INSTAGRAM_SCHEME] @@ -29,17 +29,9 @@ class InstagramType(ChannelType): } claim_view = ClaimView - menu_items = [dict(label=_("Reconnect Business Account"), view_name="channels.types.instagram.refresh_token")] - - def get_urls(self): - return [ - self.get_claim_url(), - re_path( - r"^(?P[a-z0-9\-]+)/refresh_token/$", - RefreshToken.as_view(channel_type=self), - name="refresh_token", - ), - ] + menu_items = [ + dict(label=_("Reconnect Business Account"), view_name="channels.types.instagram.claim", obj_view=False) + ] def deactivate(self, channel): config = channel.config diff --git a/temba/channels/types/instagram/views.py b/temba/channels/types/instagram/views.py index 0d943692cc2..2d16477921e 100644 --- a/temba/channels/types/instagram/views.py +++ b/temba/channels/types/instagram/views.py @@ -1,18 +1,17 @@ import logging import requests -from smartmin.views import SmartFormView, SmartModelActionView +from smartmin.views import SmartFormView from django import forms from django.conf import settings from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from temba.orgs.views.mixins import OrgObjPermsMixin from temba.utils.text import truncate from ...models import Channel -from ...views import ChannelTypeMixin, ClaimViewMixin +from ...views import ClaimViewMixin logger = logging.getLogger(__name__) @@ -154,142 +153,22 @@ def form_valid(self, form): "page_id": page_id, } - self.object = Channel.create( - self.request.org, - self.request.user, - None, - self.channel_type, - name=name, - address=ig_user_id, - config=config, - ) - - return super().form_valid(form) - - -class RefreshToken(ChannelTypeMixin, OrgObjPermsMixin, SmartModelActionView, SmartFormView): - class Form(forms.Form): - user_access_token = forms.CharField(min_length=32, required=True, help_text=_("The User Access Token")) - fb_user_id = forms.CharField( - required=True, - help_text=_("The Facebook User ID of the admin that connected the channel"), - ) - - slug_url_kwarg = "uuid" - success_url = "uuid@channels.channel_read" - form_class = Form - permission = "channels.channel_claim" - fields = () - template_name = "channels/types/instagram/refresh_token.html" - title = _("Reconnect Instagram Business Account") - - def derive_menu_path(self): - return f"/settings/channels/{self.get_object().uuid}" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["refresh_url"] = reverse("channels.types.instagram.refresh_token", args=(self.object.uuid,)) - - app_id = settings.FACEBOOK_APPLICATION_ID - app_secret = settings.FACEBOOK_APPLICATION_SECRET - - context["facebook_app_id"] = app_id - - context["facebook_login_instagram_config_id"] = settings.FACEBOOK_LOGIN_INSTAGRAM_CONFIG_ID - - url = "https://graph.facebook.com/v18.0/debug_token" - params = { - "access_token": f"{app_id}|{app_secret}", - "input_token": self.object.config[Channel.CONFIG_AUTH_TOKEN], - } - resp = requests.get(url, params=params) - - error_connect = False - if resp.status_code != 200: - error_connect = True + existing_channel = Channel.objects.filter( + org=self.request.org, address=ig_user_id, channel_type=self.channel_type.code + ).first() + if existing_channel: + existing_channel.config = config + existing_channel.save(update_fields=("name", "config")) + self.object = existing_channel else: - valid_token = resp.json().get("data", dict()).get("is_valid", False) - if not valid_token: - error_connect = True - - context["error_connect"] = error_connect - - return context - - def get_queryset(self): - return self.request.org.channels.filter(is_active=True, channel_type=self.channel_type.code) - - def execute_action(self): - form = self.form - channel = self.object + self.object = Channel.create( + self.request.org, + self.request.user, + None, + self.channel_type, + name=name, + address=ig_user_id, + config=config, + ) - auth_token = form.data["user_access_token"] - fb_user_id = form.data["fb_user_id"] - - page_id = channel.config.get("page_id") - - if page_id is None: - raise Exception("Failed to get channel page ID") # pragma: needs cover - - app_id = settings.FACEBOOK_APPLICATION_ID - app_secret = settings.FACEBOOK_APPLICATION_SECRET - - # get user long lived access token - url = "https://graph.facebook.com/oauth/access_token" - params = { - "grant_type": "fb_exchange_token", - "client_id": app_id, - "client_secret": app_secret, - "fb_exchange_token": auth_token, - } - - response = requests.get(url, params=params) - - if response.status_code != 200: # pragma: no cover - raise Exception("Failed to get a user long lived token") - - long_lived_auth_token = response.json().get("access_token", "") - - if long_lived_auth_token == "": # pragma: no cover - raise Exception("Empty user access token!") - - url = f"https://graph.facebook.com/v18.0/{fb_user_id}/accounts" - params = {"access_token": long_lived_auth_token} - - page_access_token = "" - - while True: - response = requests.get(url, params=params) - response_json = response.json() - - if response.status_code != 200: # pragma: no cover - raise Exception("Failed to get a page long lived token") - - for page in response_json.get("data", []): - if page["id"] == str(page_id): - page_access_token = page["access_token"] - name = page["name"] - break - - if page_access_token != "": - break - - next_ = response_json["paging"].get("next", None) # pragma: needs cover - if next_: # pragma: needs cover - url = next_ - - else: # pragma: needs cover - break - - url = f"https://graph.facebook.com/v18.0/{page_id}/subscribed_apps" - params = {"access_token": page_access_token} - data = {"subscribed_fields": "messages,messaging_postbacks"} - - response = requests.post(url, data=data, params=params) - - if response.status_code != 200: # pragma: no cover - raise Exception("Failed to subscribe to app for webhook events") - - channel.config[Channel.CONFIG_AUTH_TOKEN] = page_access_token - channel.config[Channel.CONFIG_PAGE_NAME] = name - channel.save(update_fields=["config"]) + return super().form_valid(form) diff --git a/temba/channels/types/whatsapp/type.py b/temba/channels/types/whatsapp/type.py index 01b8b379169..e94211672a8 100644 --- a/temba/channels/types/whatsapp/type.py +++ b/temba/channels/types/whatsapp/type.py @@ -35,7 +35,9 @@ class WhatsAppType(ChannelType): claim_blurb = _("If you have an enterprise WhatsApp account, you can connect it to communicate with your contacts") claim_view = ClaimView - menu_items = [dict(label=_("Verify Number"), view_name="channels.types.whatsapp.request_code")] + menu_items = [ + dict(label=_("Verify Number"), view_name="channels.types.whatsapp.request_code", obj_view=True), + ] def get_urls(self): return [ diff --git a/temba/channels/views.py b/temba/channels/views.py index 900d79567c7..2b11b977122 100644 --- a/temba/channels/views.py +++ b/temba/channels/views.py @@ -98,9 +98,11 @@ def clean(self): schemes__overlap=list(self.channel_type.schemes), ).first() if existing: - if existing.org == self.request.org: + if existing.org != self.request.org: + raise forms.ValidationError(_("This channel is already connected in another workspace.")) + + if not self.channel_type.matching_addresses_updates: raise forms.ValidationError(_("This channel is already connected in this workspace.")) - raise forms.ValidationError(_("This channel is already connected in another workspace.")) return super().clean() @@ -487,7 +489,10 @@ def build_context_menu(self, menu): obj = self.get_object() for item in obj.type.menu_items: - menu.add_link(item["label"], reverse(item["view_name"], args=[obj.uuid])) + menu.add_link( + item["label"], + reverse(item["view_name"], args=[obj.uuid]) if item["obj_view"] else reverse(item["view_name"]), + ) if obj.type.config_ui: menu.add_link(_("Configuration"), reverse("channels.channel_configuration", args=[obj.uuid])) diff --git a/templates/channels/types/facebookapp/refresh_token.html b/templates/channels/types/facebookapp/refresh_token.html deleted file mode 100644 index e63e31fa1eb..00000000000 --- a/templates/channels/types/facebookapp/refresh_token.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "channels/channel_claim_form.html" %} -{% load i18n compress temba %} - -{% block pre-form %} -

- {% if error_connect %} - {% trans "Error with token, you need to reconnect the Facebook page by clicking the button below" %} - {% else %} - {% trans "Everything looks good. No need to reconnect" %} - {% endif %} -

-{% endblock pre-form %} -{% block form %} -
- -
-
{% trans "Reconnect Facebook page" %}
-
- -
-{% endblock form %} -{% block extra-script %} - {{ block.super }} - - - -{% endblock extra-script %} diff --git a/templates/channels/types/instagram/refresh_token.html b/templates/channels/types/instagram/refresh_token.html deleted file mode 100644 index 135ed93ce18..00000000000 --- a/templates/channels/types/instagram/refresh_token.html +++ /dev/null @@ -1,76 +0,0 @@ -{% extends "channels/channel_claim_form.html" %} -{% load i18n compress temba %} - -{% block pre-form %} -

- {% if error_connect %} - {% trans "Error with token, you need to reconnect the Instagram Business Account by clicking the button below" %} - {% else %} - {% trans "Everything looks good. No need to reconnect" %} - {% endif %} -

-{% endblock pre-form %} -{% block form %} -
- -
-
{% trans "Reconnect Instagram Business Account" %}
-
- -
-{% endblock form %} -{% block extra-script %} - {{ block.super }} - - - -{% endblock extra-script %}