Esempio n. 1
0
    def parse_contacts(cls, org, json_obj):
        contacts = []
        for contact in json_obj.get(VariableContactAction.CONTACTS):
            name = contact.get(VariableContactAction.NAME, None)
            phone = contact.get(VariableContactAction.PHONE, None)
            contact_uuid = contact.get(VariableContactAction.UUID, None)

            urns = []
            for urn in contact.get(VariableContactAction.URNS, []):
                scheme = urn.get(VariableContactAction.SCHEME)
                path = urn.get(VariableContactAction.PATH)

                if scheme and path:
                    urns.append(URN.from_parts(scheme, path))

            if phone:  # pragma: needs cover
                urns.append(URN.from_tel(phone))

            contact = Contact.objects.filter(uuid=contact_uuid, org=org).first()

            if not contact:
                contact = Contact.get_or_create_by_urns(org, org.created_by, name=None, urns=urns)

                # if they don't have a name use the one in our action
                if name and not contact.name:  # pragma: needs cover
                    contact.name = name
                    contact.save(update_fields=["name"], handle_update=True)

            if contact:
                contacts.append(contact)

        return contacts
Esempio n. 2
0
    def create_contact(self, name=None, number=None, twitter=None, twitterid=None, urn=None, is_test=False, **kwargs):
        """
        Create a contact in the master test org
        """
        urns = []
        if number:
            urns.append(URN.from_tel(number))
        if twitter:
            urns.append(URN.from_twitter(twitter))
        if twitterid:
            urns.append(URN.from_twitterid(twitterid))
        if urn:
            urns.append(urn)

        if not name and not urns:  # pragma: no cover
            raise ValueError("Need a name or URN to create a contact")

        kwargs["name"] = name
        kwargs["urns"] = urns
        kwargs["is_test"] = is_test

        if "org" not in kwargs:
            kwargs["org"] = self.org
        if "user" not in kwargs:
            kwargs["user"] = self.user

        return Contact.get_or_create_by_urns(**kwargs)
Esempio n. 3
0
def serialize_contact(contact):
    from temba.contacts.models import URN

    field_values = {}
    for field in contact.org.cached_contact_fields.values():
        field_values[field.key] = contact.get_field_json(field)

    # augment URN values with preferred channel UUID as a parameter
    urn_values = []
    for u in contact.urns.order_by("-priority", "id"):
        # for each URN we include the preferred channel as a query param if there is one
        if u.channel and u.channel.is_active:
            scheme, path, query, display = URN.to_parts(u.urn)
            urn_str = URN.from_parts(scheme, path, query=urlencode({"channel": str(u.channel.uuid)}), display=display)
        else:
            urn_str = u.urn

        urn_values.append(urn_str)

    return {
        "uuid": contact.uuid,
        "id": contact.id,
        "name": contact.name,
        "language": contact.language,
        "urns": urn_values,
        "groups": [serialize_ref(group) for group in contact.user_groups.filter(is_active=True)],
        "fields": field_values,
    }
Esempio n. 4
0
    def create_contact(self,
                       name=None,
                       number=None,
                       twitter=None,
                       urn=None,
                       fields=None,
                       **kwargs):
        """
        Create a contact in the master test org
        """

        org = kwargs.pop("org", None) or self.org
        user = kwargs.pop("user", None) or self.user

        urns = []
        if number:
            urns.append(URN.from_tel(number))
        if twitter:
            urns.append(URN.from_twitter(twitter))
        if urn:
            urns.append(urn)

        assert name or urns, "contact should have a name or a contact"

        kwargs["name"] = name
        kwargs["urns"] = urns

        contact = Contact.get_or_create_by_urns(org, user, **kwargs)

        if fields:
            update_fields_locally(user, contact, fields)

        return contact
Esempio n. 5
0
    def create_contact(self,
                       name=None,
                       number=None,
                       twitter=None,
                       twitterid=None,
                       urn=None,
                       **kwargs):
        """
        Create a contact in the master test org
        """
        urns = []
        if number:
            urns.append(URN.from_tel(number))
        if twitter:
            urns.append(URN.from_twitter(twitter))
        if twitterid:
            urns.append(URN.from_twitterid(twitterid))
        if urn:
            urns.append(urn)

        if not name and not urns:  # pragma: no cover
            raise ValueError("Need a name or URN to create a contact")

        kwargs["name"] = name
        kwargs["urns"] = urns

        if "org" not in kwargs:
            kwargs["org"] = self.org
        if "user" not in kwargs:
            kwargs["user"] = self.user

        return Contact.get_or_create_by_urns(**kwargs)
Esempio n. 6
0
    def create_contact(self, name=None, number=None, twitter=None, urn=None, is_test=False, **kwargs):
        """
        Create a contact in the master test org
        """
        urns = []
        if number:
            urns.append(URN.from_tel(number))
        if twitter:
            urns.append(URN.from_twitter(twitter))
        if urn:
            urns.append(urn)

        if not name and not urns:  # pragma: no cover
            raise ValueError("Need a name or URN to create a contact")

        kwargs['name'] = name
        kwargs['urns'] = urns
        kwargs['is_test'] = is_test

        if 'org' not in kwargs:
            kwargs['org'] = self.org
        if 'user' not in kwargs:
            kwargs['user'] = self.user

        return Contact.get_or_create(**kwargs)
Esempio n. 7
0
    def create_contact(self,
                       name=None,
                       number=None,
                       twitter=None,
                       urn=None,
                       is_test=False,
                       **kwargs):
        """
        Create a contact in the master test org
        """
        urns = []
        if number:
            urns.append(URN.from_tel(number))
        if twitter:
            urns.append(URN.from_twitter(twitter))
        if urn:
            urns.append(urn)

        if not name and not urns:  # pragma: no cover
            raise ValueError("Need a name or URN to create a contact")

        kwargs['name'] = name
        kwargs['urns'] = urns
        kwargs['is_test'] = is_test

        if 'org' not in kwargs:
            kwargs['org'] = self.org
        if 'user' not in kwargs:
            kwargs['user'] = self.user

        return Contact.get_or_create(**kwargs)
Esempio n. 8
0
    def get_object(self, value):
        # try to normalize as URN but don't blow up if it's a UUID
        try:
            as_urn = URN.identity(URN.normalize(value))
        except ValueError:
            as_urn = value

        return self.get_queryset().filter(Q(uuid=value) | Q(urns__identity=as_urn)).first()
Esempio n. 9
0
    def normalize_urn(self, value):
        if self.request.user.get_org().is_anon:
            raise InvalidQueryError("URN lookups not allowed for anonymous organizations")

        try:
            return URN.identity(URN.normalize(value))
        except ValueError:
            raise InvalidQueryError("Invalid URN: %s" % value)
Esempio n. 10
0
    def test_resolve(self, mock_lookup_user):
        self.joe = self.create_contact("joe", twitter="therealjoe")

        urn = self.joe.get_urns()[0]

        # test no return value, should cause joe to be stopped
        mock_lookup_user.return_value = []
        resolve_twitter_ids()

        self.joe.refresh_from_db()
        urn.refresh_from_db()
        self.assertTrue(self.joe.is_stopped)
        self.assertIsNone(urn.display)
        self.assertEqual("twitter:therealjoe", urn.identity)
        self.assertEqual("therealjoe", urn.path)

        self.joe.unstop(self.admin)

        # test a real return value
        mock_lookup_user.return_value = [dict(screen_name="TheRealJoe", id="123456")]
        resolve_twitter_ids()

        urn.refresh_from_db()
        self.assertIsNone(urn.contact)

        new_urn = self.joe.get_urns()[0]
        self.assertEqual("twitterid:123456", new_urn.identity)
        self.assertEqual("123456", new_urn.path)
        self.assertEqual("therealjoe", new_urn.display)
        self.assertEqual("twitterid:123456#therealjoe", new_urn.urn)

        old_fred = self.create_contact("old fred", urn=URN.from_twitter("fred"))
        new_fred = self.create_contact("new fred", urn=URN.from_twitterid("12345", screen_name="fred"))

        mock_lookup_user.return_value = [dict(screen_name="fred", id="12345")]
        resolve_twitter_ids()

        # new fred shouldn't have any URNs anymore as he really is old_fred
        self.assertEqual(0, len(new_fred.urns.all()))

        # old fred should be unchanged
        self.assertEqual("twitterid:12345", old_fred.urns.all()[0].identity)

        self.jane = self.create_contact("jane", twitter="jane10")
        mock_lookup_user.side_effect = Exception(
            "Twitter API returned a 404 (Not Found), No user matches for specified terms."
        )
        resolve_twitter_ids()

        self.jane.refresh_from_db()
        self.assertTrue(self.jane.is_stopped)

        self.sarah = self.create_contact("sarah", twitter="sarah20")
        mock_lookup_user.side_effect = Exception("Unable to reach API")
        resolve_twitter_ids()

        self.sarah.refresh_from_db()
        self.assertFalse(self.sarah.is_stopped)
Esempio n. 11
0
    def test_resolve(self, mock_lookup_user):
        self.joe = self.create_contact("joe", twitter="therealjoe")

        urn = self.joe.get_urns()[0]

        # test no return value, should cause joe to be stopped
        mock_lookup_user.return_value = []
        resolve_twitter_ids()

        self.joe.refresh_from_db()
        urn.refresh_from_db()
        self.assertTrue(self.joe.is_stopped)
        self.assertIsNone(urn.display)
        self.assertEqual("twitter:therealjoe", urn.identity)
        self.assertEqual("therealjoe", urn.path)

        self.joe.unstop(self.admin)

        # test a real return value
        mock_lookup_user.return_value = [dict(screen_name="TheRealJoe", id="123456")]
        resolve_twitter_ids()

        urn.refresh_from_db()
        self.assertIsNone(urn.contact)

        new_urn = self.joe.get_urns()[0]
        self.assertEqual("twitterid:123456", new_urn.identity)
        self.assertEqual("123456", new_urn.path)
        self.assertEqual("therealjoe", new_urn.display)
        self.assertEqual("twitterid:123456#therealjoe", new_urn.urn)

        old_fred = self.create_contact("old fred", urn=URN.from_twitter("fred"))
        new_fred = self.create_contact("new fred", urn=URN.from_twitterid("12345", screen_name="fred"))

        mock_lookup_user.return_value = [dict(screen_name="fred", id="12345")]
        resolve_twitter_ids()

        # new fred shouldn't have any URNs anymore as he really is old_fred
        self.assertEqual(0, len(new_fred.urns.all()))

        # old fred should be unchanged
        self.assertEqual("twitterid:12345", old_fred.urns.all()[0].identity)

        self.jane = self.create_contact("jane", twitter="jane10")
        mock_lookup_user.side_effect = Exception(
            "Twitter API returned a 404 (Not Found), No user matches for specified terms."
        )
        resolve_twitter_ids()

        self.jane.refresh_from_db()
        self.assertTrue(self.jane.is_stopped)

        self.sarah = self.create_contact("sarah", twitter="sarah20")
        mock_lookup_user.side_effect = Exception("Unable to reach API")
        resolve_twitter_ids()

        self.sarah.refresh_from_db()
        self.assertFalse(self.sarah.is_stopped)
Esempio n. 12
0
    def send(self, channel, msg, text):
        twitter = TembaTwython.from_channel(channel)
        start = time.time()

        try:
            urn = getattr(msg, "urn", URN.from_twitter(msg.urn_path))
            (scheme, path, query, display) = URN.to_parts(urn)

            # this is a legacy URN (no display), the path is our screen name
            if scheme == TWITTER_SCHEME:
                dm = twitter.send_direct_message(screen_name=path, text=text)
                external_id = dm["id"]

            # this is a new twitterid URN, our path is our user id
            else:
                metadata = msg.metadata if hasattr(msg, "metadata") else {}
                quick_replies = metadata.get("quick_replies", [])
                formatted_replies = [dict(label=item[: self.quick_reply_text_size]) for item in quick_replies]

                if quick_replies:
                    params = {
                        "event": {
                            "type": "message_create",
                            "message_create": {
                                "target": {"recipient_id": path},
                                "message_data": {
                                    "text": text,
                                    "quick_reply": {"type": "options", "options": formatted_replies},
                                },
                            },
                        }
                    }
                    dm = twitter.post("direct_messages/events/new", params=params)
                    external_id = dm["event"]["id"]
                else:
                    dm = twitter.send_direct_message(user_id=path, text=text)
                    external_id = dm["id"]

        except Exception as e:
            error_code = getattr(e, "error_code", 400)
            fatal = False

            if error_code == 404:  # handle doesn't exist
                fatal = True
            elif error_code == 403:
                for err in self.FATAL_403S:
                    if str(e).find(err) >= 0:
                        fatal = True
                        break

            # if message can never be sent, stop them contact
            if fatal:
                contact = Contact.objects.get(id=msg.contact)
                contact.stop(contact.modified_by)

            raise SendException(str(e), events=twitter.events, fatal=fatal, start=start)

        Channel.success(channel, msg, WIRED, start, events=twitter.events, external_id=external_id)
Esempio n. 13
0
    def to_internal_value(self, data):
        try:
            normalized = URN.normalize(data)
            if not URN.validate(normalized):
                raise ValueError()
        except ValueError:
            raise serializers.ValidationError("Invalid URN: %s. Ensure phone numbers contain country codes." % data)

        return normalized
Esempio n. 14
0
def validate_urn(value, strict=True):
    try:
        normalized = URN.normalize(value)

        if strict and not URN.validate(normalized):
            raise ValueError()
    except ValueError:
        raise serializers.ValidationError("Invalid URN: %s. Ensure phone numbers contain country codes." % value)
    return normalized
Esempio n. 15
0
    def _create_contact_batch(self, batch):
        """
        Bulk creates a batch of contacts from flat representations
        """
        for c in batch:
            c["object"] = Contact(
                org=c["org"],
                name=c["name"],
                language=c["language"],
                is_stopped=c["is_stopped"],
                is_blocked=c["is_blocked"],
                is_active=c["is_active"],
                created_by=c["user"],
                created_on=c["created_on"],
                modified_by=c["user"],
                modified_on=c["modified_on"],
                fields=c["fields_as_json"],
            )
        Contact.objects.bulk_create([c["object"] for c in batch])

        # now that contacts have pks, bulk create the actual URN, value and group membership objects
        batch_urns = []
        batch_memberships = []

        for c in batch:
            org = c["org"]
            c["urns"] = []

            if c["tel"]:
                c["urns"].append(
                    ContactURN(
                        org=org,
                        contact=c["object"],
                        priority=50,
                        scheme=TEL_SCHEME,
                        path=c["tel"],
                        identity=URN.from_tel(c["tel"]),
                    )
                )
            if c["twitter"]:
                c["urns"].append(
                    ContactURN(
                        org=org,
                        contact=c["object"],
                        priority=50,
                        scheme=TWITTER_SCHEME,
                        path=c["twitter"],
                        identity=URN.from_twitter(c["twitter"]),
                    )
                )
            for g in c["groups"]:
                batch_memberships.append(ContactGroup.contacts.through(contact=c["object"], contactgroup=g))

            batch_urns += c["urns"]

        ContactURN.objects.bulk_create(batch_urns)
        ContactGroup.contacts.through.objects.bulk_create(batch_memberships)
Esempio n. 16
0
        def clean(self):
            # first check that our phone number looks sane
            country = self.cleaned_data["country"]
            normalized = URN.normalize_number(self.cleaned_data["number"], country)
            if not URN.validate(URN.from_parts(URN.TEL_SCHEME, normalized), country):
                raise forms.ValidationError(_("Please enter a valid phone number"))
            self.cleaned_data["number"] = normalized

            return self.cleaned_data
Esempio n. 17
0
def validate_urn(value, strict=True):
    try:
        normalized = URN.normalize(value)

        if strict and not URN.validate(normalized):
            raise ValueError()
    except ValueError:
        raise serializers.ValidationError("Invalid URN: %s. Ensure phone numbers contain country codes." % value)
    return normalized
Esempio n. 18
0
    def _create_contact_batch(self, batch):
        """
        Bulk creates a batch of contacts from flat representations
        """
        for c in batch:
            c["object"] = Contact(
                org=c["org"],
                name=c["name"],
                language=c["language"],
                is_stopped=c["is_stopped"],
                is_blocked=c["is_blocked"],
                is_active=c["is_active"],
                created_by=c["user"],
                created_on=c["created_on"],
                modified_by=c["user"],
                modified_on=c["modified_on"],
                fields=c["fields_as_json"],
            )
        Contact.objects.bulk_create([c["object"] for c in batch])

        # now that contacts have pks, bulk create the actual URN, value and group membership objects
        batch_urns = []
        batch_memberships = []

        for c in batch:
            org = c["org"]
            c["urns"] = []

            if c["tel"]:
                c["urns"].append(
                    ContactURN(
                        org=org,
                        contact=c["object"],
                        priority=50,
                        scheme=TEL_SCHEME,
                        path=c["tel"],
                        identity=URN.from_tel(c["tel"]),
                    )
                )
            if c["twitter"]:
                c["urns"].append(
                    ContactURN(
                        org=org,
                        contact=c["object"],
                        priority=50,
                        scheme=TWITTER_SCHEME,
                        path=c["twitter"],
                        identity=URN.from_twitter(c["twitter"]),
                    )
                )
            for g in c["groups"]:
                batch_memberships.append(ContactGroup.contacts.through(contact=c["object"], contactgroup=g))

            batch_urns += c["urns"]

        ContactURN.objects.bulk_create(batch_urns)
        ContactGroup.contacts.through.objects.bulk_create(batch_memberships)
Esempio n. 19
0
 def clean_number(self):
     # check that our phone number looks sane
     country = self.data["country"]
     number = URN.normalize_number(self.data["number"], country)
     if not URN.validate(URN.from_parts(URN.TEL_SCHEME, number),
                         country):
         raise forms.ValidationError(
             _("Please enter a valid phone number"))
     return number
Esempio n. 20
0
    def to_internal_value(self, data):
        try:
            country_code = self.context['org'].get_country_code()
            normalized = URN.normalize(data, country_code=country_code)
            if not URN.validate(normalized):
                raise ValueError()
        except ValueError:
            raise serializers.ValidationError("Invalid URN: %s" % data)

        return normalized
Esempio n. 21
0
    def get_object(self, value):
        # try to normalize as URN but don't blow up if it's a UUID
        try:
            as_urn = URN.identity(URN.normalize(value))
        except ValueError:
            as_urn = value

        contact_ids_with_urn = list(ContactURN.objects.filter(identity=as_urn).values_list("contact_id", flat=True))

        return self.get_queryset().filter(Q(uuid=value) | Q(id__in=contact_ids_with_urn)).first()
Esempio n. 22
0
    def get_object(self, value):
        # try to normalize as URN but don't blow up if it's a UUID
        try:
            as_urn = URN.identity(URN.normalize(value))
        except ValueError:
            as_urn = value

        contact_ids_with_urn = list(ContactURN.objects.filter(identity=as_urn).values_list('contact_id', flat=True))

        return self.get_queryset().filter(Q(uuid=value) | Q(id__in=contact_ids_with_urn)).first()
Esempio n. 23
0
    def to_internal_value(self, data):
        try:
            normalized = URN.normalize(data)
            if not URN.validate(normalized):
                raise ValueError()
        except ValueError:
            raise serializers.ValidationError(
                "Invalid URN: %s. Ensure phone numbers contain country codes."
                % data)

        return normalized
Esempio n. 24
0
    def _create_contact_batch(self, batch):
        """
        Bulk creates a batch of contacts from flat representations
        """
        for c in batch:
            c['object'] = Contact(org=c['org'], name=c['name'], language=c['language'],
                                  is_stopped=c['is_stopped'], is_blocked=c['is_blocked'],
                                  is_active=c['is_active'],
                                  created_by=c['user'], created_on=c['created_on'],
                                  modified_by=c['user'], modified_on=c['modified_on'])
        Contact.objects.bulk_create([c['object'] for c in batch])

        # now that contacts have pks, bulk create the actual URN, value and group membership objects
        batch_urns = []
        batch_values = []
        batch_memberships = []

        for c in batch:
            org = c['org']
            c['urns'] = []

            if c['tel']:
                c['urns'].append(ContactURN(org=org, contact=c['object'], priority=50, scheme=TEL_SCHEME,
                                            path=c['tel'], urn=URN.from_tel(c['tel'])))
            if c['twitter']:
                c['urns'].append(ContactURN(org=org, contact=c['object'], priority=50, scheme=TWITTER_SCHEME,
                                            path=c['twitter'], urn=URN.from_twitter(c['twitter'])))
            if c['gender']:
                batch_values.append(Value(org=org, contact=c['object'], contact_field=org.cache['fields']['gender'],
                                          string_value=c['gender']))
            if c['age']:
                batch_values.append(Value(org=org, contact=c['object'], contact_field=org.cache['fields']['age'],
                                          string_value=str(c['age']), decimal_value=c['age']))
            if c['joined']:
                batch_values.append(Value(org=org, contact=c['object'], contact_field=org.cache['fields']['joined'],
                                          string_value=datetime_to_str(c['joined']), datetime_value=c['joined']))
            if c['ward']:
                batch_values.append(Value(org=org, contact=c['object'], contact_field=org.cache['fields']['ward'],
                                          string_value=c['ward'].name, location_value=c['ward']))
            if c['district']:
                batch_values.append(Value(org=org, contact=c['object'], contact_field=org.cache['fields']['district'],
                                          string_value=c['district'].name, location_value=c['district']))
            if c['state']:
                batch_values.append(Value(org=org, contact=c['object'], contact_field=org.cache['fields']['state'],
                                          string_value=c['state'].name, location_value=c['state']))
            for g in c['groups']:
                batch_memberships.append(ContactGroup.contacts.through(contact=c['object'], contactgroup=g))

            batch_urns += c['urns']

        ContactURN.objects.bulk_create(batch_urns)
        Value.objects.bulk_create(batch_values)
        ContactGroup.contacts.through.objects.bulk_create(batch_memberships)
Esempio n. 25
0
    def _create_contact_batch(self, batch):
        """
        Bulk creates a batch of contacts from flat representations
        """
        for c in batch:
            c['object'] = Contact(org=c['org'],
                                  name=c['name'],
                                  language=c['language'],
                                  is_stopped=c['is_stopped'],
                                  is_blocked=c['is_blocked'],
                                  is_active=c['is_active'],
                                  created_by=c['user'],
                                  created_on=c['created_on'],
                                  modified_by=c['user'],
                                  modified_on=c['modified_on'],
                                  fields=c['fields_as_json'])
        Contact.objects.bulk_create([c['object'] for c in batch])

        # now that contacts have pks, bulk create the actual URN, value and group membership objects
        batch_urns = []
        batch_memberships = []

        for c in batch:
            org = c['org']
            c['urns'] = []

            if c['tel']:
                c['urns'].append(
                    ContactURN(org=org,
                               contact=c['object'],
                               priority=50,
                               scheme=TEL_SCHEME,
                               path=c['tel'],
                               identity=URN.from_tel(c['tel'])))
            if c['twitter']:
                c['urns'].append(
                    ContactURN(org=org,
                               contact=c['object'],
                               priority=50,
                               scheme=TWITTER_SCHEME,
                               path=c['twitter'],
                               identity=URN.from_twitter(c['twitter'])))
            for g in c['groups']:
                batch_memberships.append(
                    ContactGroup.contacts.through(contact=c['object'],
                                                  contactgroup=g))

            batch_urns += c['urns']

        ContactURN.objects.bulk_create(batch_urns)
        ContactGroup.contacts.through.objects.bulk_create(batch_memberships)
Esempio n. 26
0
        def clean(self):
            # first check that our phone number looks sane
            country = self.cleaned_data["country"]
            normalized = URN.normalize_number(self.cleaned_data["number"],
                                              country)
            if not URN.validate(URN.from_parts(URN.TEL_SCHEME, normalized),
                                country):
                raise forms.ValidationError(
                    _("Please enter a valid phone number"))
            self.cleaned_data["number"] = normalized

            try:
                resp = requests.post(
                    self.cleaned_data["base_url"] + "/v1/users/login",
                    auth=(self.cleaned_data["username"],
                          self.cleaned_data["password"]),
                )

                if resp.status_code != 200:
                    raise Exception("Received non-200 response: %d",
                                    resp.status_code)

                self.cleaned_data["auth_token"] = resp.json(
                )["users"][0]["token"]

            except Exception:
                raise forms.ValidationError(
                    _("Unable to check WhatsApp enterprise account, please check username and password"
                      ))

            # check we can access their facebook templates
            from .type import TEMPLATE_LIST_URL

            if self.cleaned_data[
                    "facebook_template_list_domain"] != "graph.facebook.com":
                response = requests.get(
                    TEMPLATE_LIST_URL %
                    (self.cleaned_data["facebook_template_list_domain"],
                     self.cleaned_data["facebook_business_id"]),
                    params=dict(access_token=self.
                                cleaned_data["facebook_access_token"]),
                )

                if response.status_code != 200:
                    raise forms.ValidationError(
                        _("Unable to access Facebook templates, please check user id and access token and make sure "
                          +
                          "the whatsapp_business_management permission is enabled"
                          ))
            return self.cleaned_data
Esempio n. 27
0
    def validate(self, data):
        if data.get('urns') is not None and data.get('phone') is not None:
            raise serializers.ValidationError(
                "Cannot provide both urns and phone parameters together")

        if data.get('group_uuids') is not None and data.get(
                'groups') is not None:
            raise serializers.ValidationError(
                "Parameter groups is deprecated and can't be used together with group_uuids"
            )

        if self.org.is_anon and self.instance and self.parsed_urns is not None:
            raise serializers.ValidationError(
                "Cannot update contact URNs on anonymous organizations")

        if self.parsed_urns is not None:
            # look up these URNs, keeping track of the contacts that are connected to them
            urn_contacts = set()
            country = self.org.get_country_code()

            for parsed_urn in self.parsed_urns:
                normalized_urn = URN.identity(
                    URN.normalize(parsed_urn, country))
                urn = ContactURN.objects.filter(
                    org=self.org, identity__exact=normalized_urn).first()
                if urn and urn.contact:
                    urn_contacts.add(urn.contact)

            if len(urn_contacts) > 1:
                raise serializers.ValidationError(
                    _("URNs are used by multiple contacts"))

            contact_by_urns = urn_contacts.pop(
            ) if len(urn_contacts) > 0 else None

            if self.instance and contact_by_urns and contact_by_urns != self.instance:  # pragma: no cover
                raise serializers.ValidationError(
                    _("URNs are used by other contacts"))
        else:
            contact_by_urns = None

        contact = self.instance or contact_by_urns

        # if contact is blocked, they can't be added to groups
        if contact and contact.is_blocked and self.group_objs:
            raise serializers.ValidationError(
                "Cannot add blocked contact to groups")

        return data
Esempio n. 28
0
    def validate_urns(self, value):
        if value is not None:
            self.parsed_urns = []
            for urn in value:
                try:
                    normalized = URN.normalize(urn)
                    scheme, path, query, display = URN.to_parts(normalized)
                    # for backwards compatibility we don't validate phone numbers here
                    if scheme != TEL_SCHEME and not URN.validate(normalized):  # pragma: needs cover
                        raise ValueError()
                except ValueError:
                    raise serializers.ValidationError("Invalid URN: '%s'" % urn)

                self.parsed_urns.append(normalized)

        return value
Esempio n. 29
0
        def clean(self):
            # first check that our phone number looks sane
            number, valid = URN.normalize_number(self.cleaned_data["number"],
                                                 self.cleaned_data["country"])
            if not valid:
                raise forms.ValidationError(
                    _("Please enter a valid phone number"))
            self.cleaned_data["number"] = number

            try:
                resp = requests.post(
                    self.cleaned_data["base_url"] + "/v1/users/login",
                    auth=(self.cleaned_data["username"],
                          self.cleaned_data["password"]),
                )

                if resp.status_code != 200:
                    raise Exception("Received non-200 response: %d",
                                    resp.status_code)

                self.cleaned_data["auth_token"] = resp.json(
                )["users"][0]["token"]

            except Exception:
                raise forms.ValidationError(
                    _("Unable to check WhatsApp enterprise account, please check username and password"
                      ))

            return self.cleaned_data
Esempio n. 30
0
    def validate(self, data):
        urns = data.get("urn", [])
        phones = data.get("phone", [])
        contacts = data.get("contact", [])
        channel = data.get("channel")

        if (not urns and not phones and not contacts) or (urns and phones):  # pragma: needs cover
            raise serializers.ValidationError("Must provide either urns or phone or contact and not both")

        if not channel:
            channel = Channel.objects.filter(is_active=True, org=self.org).order_by("-last_seen").first()
            if not channel:  # pragma: no cover
                raise serializers.ValidationError("There are no channels for this organization.")
            data["channel"] = channel

        if phones:
            if self.org.is_anon:  # pragma: needs cover
                raise serializers.ValidationError("Cannot create messages for anonymous organizations")

            # check our numbers for validity
            country = channel.country
            for urn in phones:
                try:
                    tel, phone, query, display = URN.to_parts(urn)
                    normalized = phonenumbers.parse(phone, country.code)
                    if not phonenumbers.is_possible_number(normalized):  # pragma: needs cover
                        raise serializers.ValidationError("Invalid phone number: '%s'" % phone)
                except Exception:
                    raise serializers.ValidationError("Invalid phone number: '%s'" % phone)

        return data
Esempio n. 31
0
    def test_claim(self, mocked):
        url = reverse("channels.types.discord.claim")

        self.login(self.admin)

        # check that claim page URL appears on claim list page
        response = self.client.get(reverse("channels.channel_claim"))
        self.assertContains(response, url)

        # can fetch the claim page
        response = self.client.get(url)
        self.assertContains(response, "Connect Discord")

        # claim with an invalid token
        response = self.client.post(url, {"auth_token": "invalid", "proxy_url": "http://foo.bar"})
        self.assertEqual(200, response.status_code)
        self.assertEqual(
            "Couldn't log in using that bot token. Please check and try again",
            response.context["form"].errors["auth_token"][0],
        )

        # Test what happens if discord is unreachable
        response = self.client.post(url, {"auth_token": "fake-network-error", "proxy_url": "http://foo.bar"})
        self.assertEqual(200, response.status_code)
        self.assertEqual(
            "An error occurred accessing the Discord API. Please try again",
            response.context["form"].errors["auth_token"][0],
        )

        # Claim with a (fake) valid token
        response = self.client.post(url, {"auth_token": "fake-valid-token", "proxy_url": "http://foo.bar"})
        channel = Channel.objects.get(name="Rapidpro-Test-Bot-Do-Not-Use")
        self.assertEqual(channel.channel_type, "DS")
        self.assertEqual(
            channel.config,
            {
                "auth_token": "fake-valid-token",
                "send_url": "http://foo.bar/discord/rp/send",
                "callback_domain": channel.callback_domain,
            },
        )

        self.assertRedirect(response, reverse("channels.channel_read", args=[channel.uuid]))
        self.assertEqual(302, response.status_code)

        response = self.client.post(url, {"auth_token": "fake-valid-token", "proxy_url": "http://foo.bar"})
        self.assertEqual(
            "A Discord channel for this bot already exists on your account.",
            response.context["form"].errors["auth_token"][0],
        )

        contact = self.create_contact("Discord User", urns=[URN.from_discord("750841288886321253")])

        # make sure we our discord channel satisfies as a send channel
        response = self.client.get(reverse("contacts.contact_read", args=[contact.uuid]))
        send_channel = response.context["send_channel"]
        self.assertIsNotNone(send_channel)
        self.assertEqual(send_channel.channel_type, "DS")
        # Release the channel. We don't test it separately, so this gives us full coverage
        channel.release()
Esempio n. 32
0
        def clean(self):
            # first check that our phone number looks sane
            number, valid = URN.normalize_number(self.cleaned_data['number'],
                                                 self.cleaned_data['country'])
            if not valid:
                raise forms.ValidationError(
                    _("Please enter a valid phone number"))
            self.cleaned_data['number'] = number

            try:
                resp = requests.post(self.cleaned_data['base_url'] +
                                     '/v1/users/login',
                                     auth=(self.cleaned_data['username'],
                                           self.cleaned_data['password']))

                if resp.status_code != 200:
                    raise Exception("Received non-200 response: %d",
                                    resp.status_code)

                self.cleaned_data['auth_token'] = resp.json(
                )['users'][0]['token']

            except Exception:
                raise forms.ValidationError(
                    _("Unable to check WhatsApp enterprise account, please check username and password"
                      ))

            return self.cleaned_data
    def validate_urns(self, value):
        if value is not None:
            self.parsed_urns = []
            for urn in value:
                try:
                    normalized = URN.normalize(urn)
                    scheme, path, display = URN.to_parts(normalized)
                    # for backwards compatibility we don't validate phone numbers here
                    if scheme != TEL_SCHEME and not URN.validate(normalized):  # pragma: needs cover
                        raise ValueError()
                except ValueError:
                    raise serializers.ValidationError("Invalid URN: '%s'" % urn)

                self.parsed_urns.append(normalized)

        return value
    def validate(self, data):
        urns = data.get('urn', [])
        phones = data.get('phone', [])
        contacts = data.get('contact', [])
        channel = data.get('channel')

        if (not urns and not phones and not contacts) or (urns and phones):  # pragma: needs cover
            raise serializers.ValidationError("Must provide either urns or phone or contact and not both")

        if not channel:
            channel = Channel.objects.filter(is_active=True, org=self.org).order_by('-last_seen').first()
            if not channel:  # pragma: no cover
                raise serializers.ValidationError("There are no channels for this organization.")
            data['channel'] = channel

        if phones:
            if self.org.is_anon:  # pragma: needs cover
                raise serializers.ValidationError("Cannot create messages for anonymous organizations")

            # check our numbers for validity
            country = channel.country
            for urn in phones:
                try:
                    tel, phone, display = URN.to_parts(urn)
                    normalized = phonenumbers.parse(phone, country.code)
                    if not phonenumbers.is_possible_number(normalized):  # pragma: needs cover
                        raise serializers.ValidationError("Invalid phone number: '%s'" % phone)
                except Exception:
                    raise serializers.ValidationError("Invalid phone number: '%s'" % phone)

        return data
Esempio n. 35
0
    def start_call(self, call, to, from_, status_callback):
        if not settings.SEND_CALLS:
            raise IVRException("SEND_CALLS set to False, skipping call start")

        channel = call.channel
        Contact.get_or_create(channel.org,
                              channel.created_by,
                              urns=[URN.from_tel(to)])

        # Verboice differs from Twilio in that they expect the first block of twiml up front
        payload = six.text_type(Flow.handle_call(call))

        # now we can post that to verboice
        url = "%s?%s" % (self.endpoint,
                         urlencode(
                             dict(channel=self.verboice_channel, address=to)))
        response = requests.post(url, data=payload, auth=self.auth).json()

        if 'call_id' not in response:
            raise IVRException(_('Verboice connection failed.'))

        # store the verboice call id in our IVRCall
        call.external_id = response['call_id']
        call.status = IVRCall.IN_PROGRESS
        call.save()
Esempio n. 36
0
    def handle_direct_inbound(self, request, uuid, data):
        from warapidpro.types import WhatsAppDirectType
        channel = self.lookup_channel(WhatsAppDirectType.code, uuid)
        if not channel:
            error_msg = "Channel not found for id: %s" % (uuid, )
            logger.error(error_msg)
            return HttpResponse(error_msg, status=400)

        from_addr = data['from_addr']
        content = self.get_content(data)
        attachments = self.get_attachments(data)

        message = Msg.create_incoming(channel,
                                      URN.from_tel(from_addr),
                                      content,
                                      external_id=data['uuid'],
                                      attachments=attachments)
        response_body = {
            'message_id': message.pk,
        }

        request_body = request.body
        request_method = request.method
        request_path = request.get_full_path()

        event = HttpEvent(request_method, request_path, request_body, 201,
                          json.dumps(response_body))
        ChannelLog.log_message(message, 'Handled inbound message.', event)
        return JsonResponse(response_body, status=201)
Esempio n. 37
0
    def create_contact(self,
                       name=None,
                       *,
                       language=None,
                       phone=None,
                       urns=None,
                       fields=None,
                       org=None,
                       user=None,
                       last_seen_on=None):
        """
        Create a new contact
        """

        org = org or self.org
        user = user or self.user
        urns = [URN.from_tel(phone)] if phone else urns

        return create_contact_locally(org,
                                      user,
                                      name,
                                      language,
                                      urns or [],
                                      fields or {},
                                      group_uuids=[],
                                      last_seen_on=last_seen_on)
Esempio n. 38
0
    def start_call(self, call, to, from_, status_callback):
        if not settings.SEND_CALLS:
            raise ValueError("SEND_CALLS set to False, skipping call start")

        channel = call.channel
        Contact.get_or_create(channel.org, URN.from_tel(to), channel)

        # Verboice differs from Twilio in that they expect the first block of twiml up front
        payload = str(Flow.handle_call(call))

        # now we can post that to verboice
        url = "%s?%s" % (self.endpoint, urlencode(dict(channel=self.verboice_channel, address=to)))
        response = requests.post(url, data=payload, auth=self.auth).json()

        if "call_id" not in response:
            call.status = IVRCall.FAILED
            call.save()

            raise IVRException(_("Verboice connection failed."))

        # store the verboice call id in our IVRCall
        call.external_id = response["call_id"]

        # the call was successfully sent to the IVR provider
        call.status = IVRCall.WIRED
        call.save()
Esempio n. 39
0
        def clean(self):
            # first check that our phone number looks sane
            number, valid = URN.normalize_number(self.cleaned_data['number'],
                                                 self.cleaned_data['country'])
            if not valid:
                raise forms.ValidationError(
                    _("Please enter a valid phone number"))
            self.cleaned_data['number'] = number

            try:
                resp = requests.post(self.cleaned_data['base_url'] +
                                     '/api/check_health.php',
                                     json=dict(payload=['gateway_status']),
                                     auth=(self.cleaned_data['username'],
                                           self.cleaned_data['password']))

                if resp.status_code != 200:
                    raise Exception("Received non-200 response: %d",
                                    resp.status_code)

            except Exception:
                raise forms.ValidationError(
                    _("Unable to check WhatsApp enterprise account, please check username and password"
                      ))

            return self.cleaned_data
Esempio n. 40
0
    def test_claim(self, mock_set_webhook, mock_get_me):
        url = reverse('channels.types.telegram.claim')

        self.login(self.admin)

        # check that claim page URL appears on claim list page
        response = self.client.get(reverse('channels.channel_claim'))
        self.assertContains(response, url)

        # can fetch the claim page
        response = self.client.get(url)
        self.assertContains(response, "Connect Telegram")

        # claim with an invalid token
        mock_get_me.side_effect = telegram.TelegramError('Boom')
        response = self.client.post(url, {'auth_token': 'invalid'})
        self.assertEqual(200, response.status_code)
        self.assertEqual(
            'Your authentication token is invalid, please check and try again',
            response.context['form'].errors['auth_token'][0])

        user = telegram.User(123, 'Rapid', True)
        user.last_name = 'Bot'
        user.username = '******'

        mock_get_me.side_effect = None
        mock_get_me.return_value = user
        mock_set_webhook.return_value = ''

        response = self.client.post(
            url,
            {'auth_token': '184875172:BAEKbsOKAL23CXufXG4ksNV7Dq7e_1qi3j8'})
        channel = Channel.objects.get(address="rapidbot")
        self.assertEqual(channel.channel_type, 'TG')
        self.assertEqual(
            channel.config, {
                'auth_token': '184875172:BAEKbsOKAL23CXufXG4ksNV7Dq7e_1qi3j8',
                'callback_domain': channel.callback_domain
            })

        self.assertRedirect(
            response, reverse('channels.channel_read', args=[channel.uuid]))
        self.assertEqual(302, response.status_code)

        response = self.client.post(
            url,
            {'auth_token': '184875172:BAEKbsOKAL23CXufXG4ksNV7Dq7e_1qi3j8'})
        self.assertEqual(
            'A telegram channel for this bot already exists on your account.',
            response.context['form'].errors['auth_token'][0])

        contact = self.create_contact('Telegram User',
                                      urn=URN.from_telegram('1234'))

        # make sure we our telegram channel satisfies as a send channel
        response = self.client.get(
            reverse('contacts.contact_read', args=[contact.uuid]))
        send_channel = response.context['send_channel']
        self.assertIsNotNone(send_channel)
        self.assertEqual(send_channel.channel_type, 'TG')
Esempio n. 41
0
    def validate_urn(self, value):
        urns = []
        if value:
            # if we have tel URNs, we may need a country to normalize by
            country = self.org.get_country_code()

            for urn in value:
                try:
                    normalized = URN.normalize(urn, country)
                except ValueError as e:  # pragma: needs cover
                    raise serializers.ValidationError(str(e))

                if not URN.validate(normalized, country):  # pragma: needs cover
                    raise serializers.ValidationError("Invalid URN: '%s'" % urn)
                urns.append(normalized)

        return urns
Esempio n. 42
0
    def to_internal_value(self, data):
        if isinstance(data, str):
            return [URN.from_tel(data)]

        elif isinstance(data, list):
            if len(data) > 100:
                raise serializers.ValidationError("You can only specify up to 100 numbers at a time.")

            urns = []
            for phone in data:
                if not isinstance(phone, str):  # pragma: no cover
                    raise serializers.ValidationError("Invalid phone: %s" % str(phone))
                urns.append(URN.from_tel(phone))

            return urns
        else:
            raise serializers.ValidationError("Invalid phone: %s" % data)
Esempio n. 43
0
    def get_object(self, value):
        # try to normalize as URN but don't blow up if it's a UUID
        try:
            as_urn = URN.normalize(value)
        except ValueError:
            as_urn = value

        return self.get_queryset().filter(Q(uuid=value) | Q(urns__urn=as_urn)).first()
Esempio n. 44
0
    def test_claim(self, mock_set_webhook, mock_get_me):
        url = reverse("channels.types.telegram.claim")

        self.login(self.admin)

        # check that claim page URL appears on claim list page
        response = self.client.get(reverse("channels.channel_claim"))
        self.assertContains(response, url)

        # can fetch the claim page
        response = self.client.get(url)
        self.assertContains(response, "Connect Telegram")

        # claim with an invalid token
        mock_get_me.side_effect = telegram.TelegramError("Boom")
        response = self.client.post(url, {"auth_token": "invalid"})
        self.assertEqual(200, response.status_code)
        self.assertEqual(
            "Your authentication token is invalid, please check and try again",
            response.context["form"].errors["auth_token"][0],
        )

        user = telegram.User(123, "Rapid", True)
        user.last_name = "Bot"
        user.username = "******"

        mock_get_me.side_effect = None
        mock_get_me.return_value = user
        mock_set_webhook.return_value = ""

        response = self.client.post(url, {"auth_token": "184875172:BAEKbsOKAL23CXufXG4ksNV7Dq7e_1qi3j8"})
        channel = Channel.objects.get(address="rapidbot")
        self.assertEqual(channel.channel_type, "TG")
        self.assertEqual(
            channel.config,
            {
                "auth_token": "184875172:BAEKbsOKAL23CXufXG4ksNV7Dq7e_1qi3j8",
                "callback_domain": channel.callback_domain,
            },
        )

        self.assertRedirect(response, reverse("channels.channel_read", args=[channel.uuid]))
        self.assertEqual(302, response.status_code)

        response = self.client.post(url, {"auth_token": "184875172:BAEKbsOKAL23CXufXG4ksNV7Dq7e_1qi3j8"})
        self.assertEqual(
            "A telegram channel for this bot already exists on your account.",
            response.context["form"].errors["auth_token"][0],
        )

        contact = self.create_contact("Telegram User", urn=URN.from_telegram("1234"))

        # make sure we our telegram channel satisfies as a send channel
        response = self.client.get(reverse("contacts.contact_read", args=[contact.uuid]))
        send_channel = response.context["send_channel"]
        self.assertIsNotNone(send_channel)
        self.assertEqual(send_channel.channel_type, "TG")
Esempio n. 45
0
    def validate_phone(self, value):
        if value:
            try:
                normalized = phonenumbers.parse(value, None)
                if not phonenumbers.is_possible_number(normalized):
                    raise serializers.ValidationError("Invalid phone number: '%s'" % value)
            except Exception:
                raise serializers.ValidationError("Invalid phone number: '%s'" % value)

            e164_number = phonenumbers.format_number(normalized, phonenumbers.PhoneNumberFormat.E164)
            self.parsed_urns = [URN.from_tel(e164_number)]
        return value
Esempio n. 46
0
    def validate(self, data):
        if data.get("urns") is not None and data.get("phone") is not None:
            raise serializers.ValidationError("Cannot provide both urns and phone parameters together")

        if data.get("group_uuids") is not None and data.get("groups") is not None:
            raise serializers.ValidationError(
                "Parameter groups is deprecated and can't be used together with group_uuids"
            )

        if self.org.is_anon and self.instance and self.parsed_urns is not None:
            raise serializers.ValidationError("Cannot update contact URNs on anonymous organizations")

        if self.parsed_urns is not None:
            # look up these URNs, keeping track of the contacts that are connected to them
            urn_contacts = set()
            country = self.org.get_country_code()

            for parsed_urn in self.parsed_urns:
                normalized_urn = URN.identity(URN.normalize(parsed_urn, country))
                urn = ContactURN.objects.filter(org=self.org, identity__exact=normalized_urn).first()
                if urn and urn.contact:
                    urn_contacts.add(urn.contact)

            if len(urn_contacts) > 1:
                raise serializers.ValidationError(_("URNs are used by multiple contacts"))

            contact_by_urns = urn_contacts.pop() if len(urn_contacts) > 0 else None

            if self.instance and contact_by_urns and contact_by_urns != self.instance:  # pragma: no cover
                raise serializers.ValidationError(_("URNs are used by other contacts"))
        else:
            contact_by_urns = None

        contact = self.instance or contact_by_urns

        # if contact is blocked, they can't be added to groups
        if contact and contact.is_blocked and self.group_objs:
            raise serializers.ValidationError("Cannot add blocked contact to groups")

        return data
Esempio n. 47
0
def reduce_event(event):
    new_event = copy_keys(event, {"type", "msg"})

    if "msg" in new_event:
        new_event["msg"] = copy_keys(event["msg"], {"text", "urn", "channel", "attachments"})
        new_msg = new_event["msg"]

        # legacy events are re-constructed from real messages which have their text stripped
        if "text" in new_msg:
            new_msg["text"] = new_msg["text"].strip()

        # legacy events have absolute paths for attachments, new have relative
        if "attachments" in new_msg:
            abs_prefix = f"https://{settings.AWS_BUCKET_DOMAIN}/"
            new_msg["attachments"] = [a.replace(abs_prefix, "") for a in new_msg["attachments"]]

        # new engine events might have params on URNs that we're not interested in
        if "urn" in new_msg:
            scheme, path, query, fragment = URN.to_parts(new_msg["urn"])
            new_msg["urn"] = URN.from_parts(scheme, path, None, fragment)

    return new_event
Esempio n. 48
0
    def default(self, line):
        """
        Sends a message as the current contact's highest priority URN
        """
        urn = self.contact.get_urn()

        incoming = Msg.create_incoming(None, URN.from_parts(urn.scheme, urn.path),
                                       line, date=timezone.now(), org=self.org)

        self.echo((Fore.GREEN + "[%s] " + Fore.YELLOW + ">>" + Fore.MAGENTA + " %s" + Fore.WHITE) % (urn.urn, incoming.text))

        # look up any message responses
        outgoing = Msg.all_messages.filter(org=self.org, pk__gt=incoming.pk, direction=OUTGOING).order_by('sent_on')
        for response in outgoing:
            self.echo((Fore.GREEN + "[%s] " + Fore.YELLOW + "<<" + Fore.MAGENTA + " %s" + Fore.WHITE) % (urn.urn, response.text))
Esempio n. 49
0
    def test_claim(self):
        url = reverse("channels.types.wechat.claim")

        self.login(self.admin)

        # check that claim page URL appears on claim list page
        response = self.client.get(reverse("channels.channel_claim"))
        self.assertNotContains(response, url)

        # try to claim a channel
        response = self.client.get(url)
        post_data = response.context["form"].initial
        post_data["app_id"] = "foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo"
        post_data["app_secret"] = "barbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbarbar"

        response = self.client.post(url, post_data)

        channel = Channel.objects.get(channel_type="WC")

        self.assertEqual(
            channel.config,
            {
                "wechat_app_id": post_data["app_id"],
                "wechat_app_secret": post_data["app_secret"],
                "secret": channel.config["secret"],
            },
        )

        config_url = reverse("channels.channel_configuration", args=[channel.uuid])
        self.assertRedirect(response, config_url)

        response = self.client.get(config_url)
        self.assertEqual(200, response.status_code)

        self.assertContains(response, reverse("courier.wc", args=[channel.uuid]))
        self.assertContains(response, channel.config[Channel.CONFIG_SECRET])

        # check we show the IP to whitelist
        self.assertContains(response, "10.10.10.10")
        self.assertContains(response, "172.16.20.30")

        contact = self.create_contact("WeChat User", urn=URN.from_wechat("1234"))

        # make sure we our jiochat channel satisfies as a send channel
        response = self.client.get(reverse("contacts.contact_read", args=[contact.uuid]))
        send_channel = response.context["send_channel"]
        self.assertIsNotNone(send_channel)
        self.assertEqual(send_channel.channel_type, "WC")
Esempio n. 50
0
        def form_valid(self, *args, **kwargs):
            data = self.form.cleaned_data
            handled = Msg.create_incoming(data['channel'], URN.from_tel(data['urn']), data['text'],
                                          user=self.request.user)

            kwargs = self.get_form_kwargs()
            kwargs['initial'] = data
            next_form = TestMessageForm(**kwargs)

            context = self.get_context_data()
            context['handled'] = handled
            context['form'] = next_form
            context['responses'] = handled.responses.all()

            # passing a minimal base template and a simple Context (instead of RequestContext) helps us
            # minimize number of other queries, allowing us to more easily measure queries per request
            context['base_template'] = 'msgs/msg_test_frame.html'
            return self.render_to_response(Context(context))
Esempio n. 51
0
    def start_call(self, call, to, from_, status_callback):

        channel = call.channel
        Contact.get_or_create(channel.org, channel.created_by, urns=[URN.from_tel(to)])

        # Verboice differs from Twilio in that they expect the first block of twiml up front
        payload = unicode(Flow.handle_call(call, {}))

        # now we can post that to verboice
        url = "%s?%s" % (self.endpoint, urlencode(dict(channel=self.verboice_channel, address=to)))
        response = requests.post(url, data=payload, auth=self.auth).json()

        if 'call_id' not in response:
            raise IVRException(_('Verboice connection failed.'))

        # store the verboice call id in our IVRCall
        call.external_id = response['call_id']
        call.status = IN_PROGRESS
        call.save()
Esempio n. 52
0
        def form_valid(self, *args, **kwargs):  # pragma: no cover
            data = self.form.cleaned_data
            handled = Msg.create_incoming(
                data["channel"], URN.from_tel(data["urn"]), data["text"], user=self.request.user
            )

            kwargs = self.get_form_kwargs()
            kwargs["initial"] = data
            next_form = TestMessageForm(**kwargs)

            context = self.get_context_data()
            context["handled"] = handled
            context["form"] = next_form
            context["responses"] = handled.responses.all()

            # passing a minimal base template and a simple Context (instead of RequestContext) helps us
            # minimize number of other queries, allowing us to more easily measure queries per request
            context["base_template"] = "msgs/msg_test_frame.html"
            return self.render_to_response(context)
Esempio n. 53
0
        def clean(self):
            # first check that our phone number looks sane
            number, valid = URN.normalize_number(self.cleaned_data["number"], self.cleaned_data["country"])
            if not valid:
                raise forms.ValidationError(_("Please enter a valid phone number"))
            self.cleaned_data["number"] = number

            try:
                resp = requests.post(
                    self.cleaned_data["base_url"] + "/v1/users/login",
                    auth=(self.cleaned_data["username"], self.cleaned_data["password"]),
                )

                if resp.status_code != 200:
                    raise Exception("Received non-200 response: %d", resp.status_code)

                self.cleaned_data["auth_token"] = resp.json()["users"][0]["token"]

            except Exception:
                raise forms.ValidationError(
                    _("Unable to check WhatsApp enterprise account, please check username and password")
                )

            return self.cleaned_data
Esempio n. 54
0
    def post(self, request, *args, **kwargs):
        from twilio.request_validator import RequestValidator
        from temba.flows.models import FlowSession

        signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "")
        url = "https://" + request.get_host() + "%s" % request.get_full_path()

        channel_uuid = kwargs.get("uuid")
        call_sid = self.get_param("CallSid")
        direction = self.get_param("Direction")
        status = self.get_param("CallStatus")
        to_number = self.get_param("To")
        to_country = self.get_param("ToCountry")
        from_number = self.get_param("From")

        # Twilio sometimes sends un-normalized numbers
        if to_number and not to_number.startswith("+") and to_country:  # pragma: no cover
            to_number, valid = URN.normalize_number(to_number, to_country)

        # see if it's a twilio call being initiated
        if to_number and call_sid and direction == "inbound" and status == "ringing":

            # find a channel that knows how to answer twilio calls
            channel = self.get_ringing_channel(uuid=channel_uuid)
            if not channel:
                response = VoiceResponse()
                response.say("Sorry, there is no channel configured to take this call. Goodbye.")
                response.hangup()
                return HttpResponse(str(response))

            org = channel.org

            if self.get_channel_type() == "T" and not org.is_connected_to_twilio():
                return HttpResponse("No Twilio account is connected", status=400)

            client = self.get_client(channel=channel)
            validator = RequestValidator(client.auth[1])
            signature = request.META.get("HTTP_X_TWILIO_SIGNATURE", "")

            url = "https://%s%s" % (request.get_host(), request.get_full_path())

            if validator.validate(url, request.POST, signature):
                from temba.ivr.models import IVRCall

                # find a contact for the one initiating us
                urn = URN.from_tel(from_number)
                contact, urn_obj = Contact.get_or_create(channel.org, urn, channel)

                flow = Trigger.find_flow_for_inbound_call(contact)

                if flow:
                    call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, call_sid)
                    session = FlowSession.create(contact, connection=call)

                    call.update_status(
                        request.POST.get("CallStatus", None), request.POST.get("CallDuration", None), "T"
                    )
                    call.save()

                    FlowRun.create(flow, contact, session=session, connection=call)
                    response = Flow.handle_call(call)
                    return HttpResponse(str(response))

                else:

                    # we don't have an inbound trigger to deal with this call.
                    response = channel.generate_ivr_response()

                    # say nothing and hangup, this is a little rude, but if we reject the call, then
                    # they'll get a non-working number error. We send 'busy' when our server is down
                    # so we don't want to use that here either.
                    response.say("")
                    response.hangup()

                    # if they have a missed call trigger, fire that off
                    Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel)

                    # either way, we need to hangup now
                    return HttpResponse(str(response))

        # check for call progress events, these include post-call hangup notifications
        if request.POST.get("CallbackSource", None) == "call-progress-events":
            if call_sid:
                from temba.ivr.models import IVRCall

                call = IVRCall.objects.filter(external_id=call_sid).first()
                if call:
                    call.update_status(
                        request.POST.get("CallStatus", None), request.POST.get("CallDuration", None), "TW"
                    )
                    call.save()
                    return HttpResponse("Call status updated")
            return HttpResponse("No call found")

        return HttpResponse("Not Handled, unknown action", status=400)  # pragma: no cover
Esempio n. 55
0
    def post(self, request, *args, **kwargs):
        from temba.msgs.models import Msg

        request_body = request.body
        request_method = request.method
        request_path = request.get_full_path()

        def log_channel(channel, description, event, is_error=False):
            return ChannelLog.objects.create(
                channel_id=channel.pk,
                is_error=is_error,
                request=event.request_body,
                response=event.response_body,
                url=event.url,
                method=event.method,
                response_status=event.status_code,
                description=description,
            )

        action = kwargs["action"].lower()
        request_uuid = kwargs["uuid"]

        data = json.loads(force_text(request_body))
        is_ussd = self.is_ussd_message(data)
        channel_data = data.get("channel_data", {})
        channel_types = ("JNU", "JN")

        # look up the channel
        channel = Channel.objects.filter(uuid=request_uuid, is_active=True, channel_type__in=channel_types).first()

        if not channel:
            return HttpResponse("Channel not found for id: %s" % request_uuid, status=400)

        auth = request.META.get("HTTP_AUTHORIZATION", "").split(" ")
        secret = channel.config.get(Channel.CONFIG_SECRET)
        if secret is not None and (len(auth) != 2 or auth[0] != "Token" or auth[1] != secret):
            return JsonResponse(dict(error="Incorrect authentication token"), status=401)

        # Junebug is sending an event
        if action == "event":
            expected_keys = ["event_type", "message_id", "timestamp"]
            if not set(expected_keys).issubset(data.keys()):
                status = 400
                response_body = "Missing one of %s in request parameters." % (", ".join(expected_keys))
                event = HttpEvent(request_method, request_path, request_body, status, response_body)
                log_channel(channel, "Failed to handle event.", event, is_error=True)
                return HttpResponse(response_body, status=status)

            message_id = data["message_id"]
            event_type = data["event_type"]

            # look up the message
            message = Msg.objects.filter(channel=channel, external_id=message_id).select_related("channel")
            if not message:
                status = 400
                response_body = "Message with external id of '%s' not found" % (message_id,)
                event = HttpEvent(request_method, request_path, request_body, status, response_body)
                log_channel(channel, "Failed to handle %s event_type." % (event_type), event)
                return HttpResponse(response_body, status=status)

            if event_type == "submitted":
                for message_obj in message:
                    message_obj.status_sent()
            if event_type == "delivery_succeeded":
                for message_obj in message:
                    message_obj.status_delivered()
            elif event_type in ["delivery_failed", "rejected"]:
                for message_obj in message:
                    message_obj.status_fail()

            response_body = {"status": self.ACK, "message_ids": [message_obj.pk for message_obj in message]}
            event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response_body))
            log_channel(channel, "Handled %s event_type." % (event_type), event)
            # Let Junebug know we're happy
            return JsonResponse(response_body)

        # Handle an inbound message
        elif action == "inbound":
            expected_keys = [
                "channel_data",
                "from",
                "channel_id",
                "timestamp",
                "content",
                "to",
                "reply_to",
                "message_id",
            ]
            if not set(expected_keys).issubset(data.keys()):
                status = 400
                response_body = "Missing one of %s in request parameters." % (", ".join(expected_keys))
                event = HttpEvent(request_method, request_path, request_body, status, response_body)
                log_channel(channel, "Failed to handle message.", event, is_error=True)
                return HttpResponse(response_body, status=status)

            if is_ussd:
                status = {"close": USSDSession.INTERRUPTED, "new": USSDSession.TRIGGERED}.get(
                    channel_data.get("session_event"), USSDSession.IN_PROGRESS
                )

                message_date = datetime.strptime(data["timestamp"], "%Y-%m-%d %H:%M:%S.%f")
                gmt_date = pytz.timezone("GMT").localize(message_date)
                # Use a session id if provided, otherwise fall back to using the `from` address as the identifier
                session_id = channel_data.get("session_id") or data["from"]

                connection = USSDSession.handle_incoming(
                    channel=channel,
                    urn=data["from"],
                    content=data["content"],
                    status=status,
                    date=gmt_date,
                    external_id=session_id,
                    message_id=data["message_id"],
                    starcode=data["to"],
                )

                if connection:
                    status = 200
                    response_body = {"status": self.ACK, "session_id": connection.pk}
                    event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                    log_channel(
                        channel, "Handled USSD message of %s session_event" % (channel_data["session_event"],), event
                    )
                    return JsonResponse(response_body, status=status)
                else:
                    status = 400
                    response_body = {"status": self.NACK, "reason": "No suitable session found for this message."}
                    event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                    log_channel(
                        channel,
                        "Failed to handle USSD message of %s session_event" % (channel_data["session_event"],),
                        event,
                    )
                    return JsonResponse(response_body, status=status)
            else:
                content = data["content"]
                message = Msg.create_incoming(channel, URN.from_tel(data["from"]), content)
                status = 200
                response_body = {"status": self.ACK, "message_id": message.pk}
                Msg.objects.filter(pk=message.id).update(external_id=data["message_id"])
                event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                ChannelLog.log_message(message, "Handled inbound message.", event)
                return JsonResponse(response_body, status=status)
Esempio n. 56
0
    def get(self, request, *args, **kwargs):
        from temba.flows.models import FlowSession
        from temba.ivr.models import IVRCall

        action = kwargs["action"].lower()

        request_body = force_text(request.body)
        request_path = request.get_full_path()
        request_method = request.method

        request_uuid = kwargs["uuid"]

        if action == "event":
            if not request_body:
                return HttpResponse("")

            body_json = json.loads(request_body)
            status = body_json.get("status", None)
            duration = body_json.get("duration", None)
            call_uuid = body_json.get("uuid", None)
            conversation_uuid = body_json.get("conversation_uuid", None)

            if call_uuid is None:
                return HttpResponse("Missing uuid parameter, ignoring")

            call = IVRCall.objects.filter(external_id=call_uuid).first()
            if not call:
                # try looking up by the conversation uuid (inbound calls start with that)
                call = IVRCall.objects.filter(external_id=conversation_uuid).first()
                if call:
                    call.external_id = call_uuid
                    call.save()
                else:
                    response = dict(message="Call not found for %s" % call_uuid)
                    return JsonResponse(response)

            channel = call.channel
            channel_type = channel.channel_type
            call.update_status(status, duration, channel_type)
            call.save()

            response = dict(
                description="Updated call status", call=dict(status=call.get_status_display(), duration=call.duration)
            )

            event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response))
            ChannelLog.log_ivr_interaction(call, "Updated call status", event)

            if call.status == IVRCall.COMPLETED:
                # if our call is completed, hangup
                runs = FlowRun.objects.filter(connection=call)
                for run in runs:
                    if not run.is_completed():
                        run.set_completed(exit_uuid=None)

            return JsonResponse(response)

        if action == "answer":
            if not request_body:
                return HttpResponse("")

            body_json = json.loads(request_body)
            from_number = body_json.get("from", None)
            channel_number = body_json.get("to", None)
            external_id = body_json.get("conversation_uuid", None)

            if not from_number or not channel_number or not external_id:
                return HttpResponse("Missing parameters, Ignoring")

            # look up the channel
            address_q = Q(address=channel_number) | Q(address=("+" + channel_number))
            channel = Channel.objects.filter(address_q).filter(is_active=True, channel_type="NX").first()

            # make sure we got one, and that it matches the key for our org
            org_uuid = None
            if channel:
                org_uuid = channel.org.config.get(NEXMO_UUID, None)

            if not channel or org_uuid != request_uuid:
                return HttpResponse("Channel not found for number: %s" % channel_number, status=404)

            urn = URN.from_tel(from_number)
            contact, urn_obj = Contact.get_or_create(channel.org, urn, channel)

            flow = Trigger.find_flow_for_inbound_call(contact)

            if flow:
                call = IVRCall.create_incoming(channel, contact, urn_obj, channel.created_by, external_id)
                session = FlowSession.create(contact, connection=call)

                FlowRun.create(flow, contact, session=session, connection=call)
                response = Flow.handle_call(call)
                channel_type = channel.channel_type
                call.update_status("answered", None, channel_type)

                event = HttpEvent(request_method, request_path, request_body, 200, str(response))
                ChannelLog.log_ivr_interaction(call, "Incoming request for call", event)
                return JsonResponse(json.loads(str(response)), safe=False)
            else:
                # we don't have an inbound trigger to deal with this call.
                response = channel.generate_ivr_response()

                # say nothing and hangup, this is a little rude, but if we reject the call, then
                # they'll get a non-working number error. We send 'busy' when our server is down
                # so we don't want to use that here either.
                response.say("")
                response.hangup()

                # if they have a missed call trigger, fire that off
                Trigger.catch_triggers(contact, Trigger.TYPE_MISSED_CALL, channel)

                # either way, we need to hangup now
                return JsonResponse(json.loads(str(response)), safe=False)
Esempio n. 57
0
def resolve_twitter_ids():
    r = get_redis_connection()
    # TODO: we can't use our non-overlapping task decorator as it creates a loop in the celery resolver when registering
    if r.get("resolve_twitter_ids_task"):  # pragma: no cover
        return

    with r.lock("resolve_twitter_ids_task", 1800):
        # look up all 'twitter' URNs, limiting to 30k since that's the most our API would allow anyways
        twitter_urns = ContactURN.objects.filter(
            scheme=TWITTER_SCHEME, contact__is_stopped=False, contact__is_blocked=False
        ).exclude(contact=None)
        twitter_urns = twitter_urns[:30000].only("id", "org", "contact", "path")
        api_key = settings.TWITTER_API_KEY
        api_secret = settings.TWITTER_API_SECRET
        client = Twython(api_key, api_secret)

        updated = 0
        print("found %d twitter urns to resolve" % len(twitter_urns))

        # contacts we will stop
        stop_contacts = []

        # we try to look these up 100 at a time
        for urn_batch in chunk_list(twitter_urns, 100):
            screen_names = [u.path for u in urn_batch]
            screen_map = {u.path: u for u in urn_batch}

            # try to fetch our users by screen name
            try:
                resp = client.lookup_user(screen_name=",".join(screen_names))

                for twitter_user in resp:
                    screen_name = twitter_user["screen_name"].lower()
                    twitter_id = twitter_user["id"]

                    if screen_name in screen_map and twitter_user["id"]:
                        twitterid_urn = URN.normalize(URN.from_twitterid(twitter_id, screen_name))
                        old_urn = screen_map[screen_name]

                        # create our new contact URN
                        new_urn = ContactURN.get_or_create(old_urn.org, old_urn.contact, twitterid_urn)

                        # if our new URN already existed for another contact and it is newer
                        # than our old contact, reassign it to the old contact
                        if (
                            new_urn.contact != old_urn.contact
                            and new_urn.contact.created_on > old_urn.contact.created_on
                        ):
                            new_urn.contact = old_urn.contact
                            new_urn.save(update_fields=["contact"])

                        # get rid of our old URN
                        ContactURN.objects.filter(id=old_urn.id).update(contact=None)
                        del screen_map[screen_name]
                        updated += 1

            except Exception as e:
                # if this wasn't an exception caused by not finding any of the users, then break
                if str(e).find("No user matches") < 0:
                    # exit, we'll try again later
                    print("exiting resolve_twitter_ids due to exception: %s" % e)
                    break

            # add all remaining contacts to the contacts we will stop
            for contact in screen_map.values():
                stop_contacts.append(contact)

        # stop all the contacts we couldn't resolve that have only a twitter URN
        stopped = 0
        for contact_urn in stop_contacts:
            contact = contact_urn.contact
            if len(contact.urns.all()) == 1:
                contact.stop(contact.created_by)
                stopped += 1

        if len(twitter_urns) > 0:
            print("updated %d twitter urns, %d stopped" % (updated, len(stop_contacts)))
Esempio n. 58
0
    def handle_incoming(
        cls,
        channel,
        urn,
        date,
        external_id,
        contact=None,
        message_id=None,
        status=None,
        content=None,
        starcode=None,
        org=None,
        do_async=True,
    ):

        trigger = None
        contact_urn = None

        # handle contact with channel
        urn = URN.from_tel(urn)

        if not contact:
            contact, contact_urn = Contact.get_or_create(channel.org, urn, channel)
        elif urn:
            contact_urn = ContactURN.get_or_create(org, contact, urn, channel=channel)

        contact.set_preferred_channel(channel)

        if contact_urn:
            contact_urn.update_affinity(channel)

        # setup session
        defaults = dict(
            channel=channel, contact=contact, contact_urn=contact_urn, org=channel.org if channel else contact.org
        )

        if status == cls.TRIGGERED:
            trigger = Trigger.find_trigger_for_ussd_session(contact, starcode)
            if not trigger:
                return False
            defaults.update(dict(started_on=date, direction=cls.USSD_PULL, status=status))

        elif status == cls.INTERRUPTED:
            defaults.update(dict(ended_on=date, status=status))

        else:
            defaults.update(dict(status=cls.IN_PROGRESS))

        # check if there's an initiated PUSH connection
        connection = cls.objects.get_initiated_push(contact)

        created = False
        if not connection:
            try:
                connection = (
                    cls.objects.select_for_update()
                    .exclude(status__in=ChannelConnection.DONE)
                    .get(external_id=external_id)
                )
                created = False
                for k, v in defaults.items():
                    setattr(connection, k, v() if callable(v) else v)
                connection.save()
            except cls.DoesNotExist:
                defaults["external_id"] = external_id
                connection = cls.objects.create(**defaults)
                FlowSession.create(contact, connection=connection)
                created = True
        else:
            defaults.update(dict(external_id=external_id))
            for key, value in defaults.items():
                setattr(connection, key, value)
            connection.save()
            created = None

        # start session
        if created and do_async and trigger:
            connection.start_async(trigger.flow, date, message_id)

        # resume session, deal with incoming content and all the other states
        else:
            connection.handle_async(urn, content, date, message_id)

        return connection