Example #1
0
    def send(self, channel, msg, text):
        # build our payload
        payload = {'message': {'text': text}}

        metadata = msg.metadata if hasattr(msg, 'metadata') else {}
        quick_replies = metadata.get('quick_replies', [])
        formatted_replies = [dict(title=item[:self.quick_reply_text_size], payload=item[:self.quick_reply_text_size],
                                  content_type='text') for item in quick_replies]

        if quick_replies:
            payload['message']['quick_replies'] = formatted_replies

        # this is a ref facebook id, temporary just for this message
        if URN.is_path_fb_ref(msg.urn_path):
            payload['recipient'] = dict(user_ref=URN.fb_ref_from_path(msg.urn_path))
        else:
            payload['recipient'] = dict(id=msg.urn_path)

        url = "https://graph.facebook.com/v2.5/me/messages"
        params = {'access_token': channel.config[Channel.CONFIG_AUTH_TOKEN]}
        headers = {'Content-Type': 'application/json'}
        start = time.time()

        payload = json.dumps(payload)
        event = HttpEvent('POST', url, json.dumps(payload))

        try:
            response = requests.post(url, payload, params=params, headers=headers, timeout=15)
            event.status_code = response.status_code
            event.response_body = response.text
        except Exception as e:
            raise SendException(six.text_type(e), event=event, start=start)

        # for now we only support sending one attachment per message but this could change in future
        attachments = Attachment.parse_all(msg.attachments)
        attachment = attachments[0] if attachments else None

        if attachment:
            category = attachment.content_type.split('/')[0]

            payload = json.loads(payload)
            payload['message'] = {'attachment': {'type': category, 'payload': {'url': attachment.url}}}
            payload = json.dumps(payload)

            event = HttpEvent('POST', url, payload)

            try:
                response = requests.post(url, payload, params=params, headers=headers, timeout=15)
                event.status_code = response.status_code
                event.response_body = response.text
            except Exception as e:
                raise SendException(six.text_type(e), event=event, start=start)

        if response.status_code != 200:  # pragma: no cover
            raise SendException("Got non-200 response [%d] from Facebook" % response.status_code,
                                event=event, start=start)

        # grab our external id out, Facebook response is in format:
        # "{"recipient_id":"997011467086879","message_id":"mid.1459532331848:2534ddacc3993a4b78"}"
        external_id = None
        try:
            external_id = response.json()['message_id']
        except Exception as e:  # pragma: no cover
            # if we can't pull out our message id, that's ok, we still sent
            pass

        # if we sent Facebook a user_ref, look up the real Facebook id for this contact, should be in 'recipient_id'
        if URN.is_path_fb_ref(msg.urn_path):
            contact_obj = Contact.objects.get(id=msg.contact)
            org_obj = Org.objects.get(id=channel.org)
            channel_obj = Channel.objects.get(id=channel.id)

            try:
                real_fb_id = response.json()['recipient_id']

                # associate this contact with our real FB id
                ContactURN.get_or_create(org_obj, contact_obj, URN.from_facebook(real_fb_id), channel=channel_obj)

                # save our ref_id as an external URN on this contact
                ContactURN.get_or_create(org_obj, contact_obj, URN.from_external(URN.fb_ref_from_path(msg.urn_path)))

                # finally, disassociate our temp ref URN with this contact
                ContactURN.objects.filter(id=msg.contact_urn).update(contact=None)

            except Exception as e:  # pragma: no cover
                # if we can't pull out the recipient id, that's ok, msg was sent
                pass

        Channel.success(channel, msg, WIRED, start, event=event, external_id=external_id)
Example #2
0
class USSDSession(ChannelSession):
    USSD_PULL = INCOMING = 'I'
    USSD_PUSH = OUTGOING = 'O'

    objects = USSDQuerySet.as_manager()

    class Meta:
        proxy = True

    @property
    def should_end(self):
        return self.status == self.ENDING

    def mark_ending(self):  # session to be ended
        if self.status != self.ENDING:
            self.status = self.ENDING
            self.save(update_fields=['status'])

    def close(self):  # session has successfully ended
        if self.status == self.ENDING:
            self.status = self.COMPLETED
        else:
            self.status = self.INTERRUPTED

        self.ended_on = timezone.now()
        self.save(update_fields=['status', 'ended_on'])

    def start_async(self, flow, date, message_id):
        from temba.msgs.models import Msg, USSD
        message = Msg.objects.create(channel=self.channel,
                                     contact=self.contact,
                                     contact_urn=self.contact_urn,
                                     sent_on=date,
                                     connection=self,
                                     msg_type=USSD,
                                     external_id=message_id,
                                     created_on=timezone.now(),
                                     modified_on=timezone.now(),
                                     org=self.channel.org,
                                     direction=self.INCOMING)
        flow.start([], [self.contact],
                   start_msg=message,
                   restart_participants=True,
                   connection=self)

    def handle_async(self, urn, content, date, message_id):
        from temba.msgs.models import Msg, USSD
        Msg.create_incoming(channel=self.channel,
                            org=self.org,
                            urn=urn,
                            text=content or '',
                            date=date,
                            connection=self,
                            msg_type=USSD,
                            external_id=message_id)

    def handle_sync(self):  # pragma: needs cover
        # TODO: implement for InfoBip and other sync APIs
        pass

    @classmethod
    def handle_incoming(cls,
                        channel,
                        urn,
                        date,
                        external_id,
                        contact=None,
                        message_id=None,
                        status=None,
                        content=None,
                        starcode=None,
                        org=None,
                        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=ChannelSession.DONE)\
                                                            .get(external_id=external_id)
                created = False
                for k, v in six.iteritems(defaults):
                    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 six.iteritems(defaults):
                setattr(connection, key, value)
            connection.save()
            created = None

        # start session
        if created and async and trigger:
            connection.start_async(trigger.flow, date, message_id)
Example #3
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
Example #4
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)))
Example #5
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=ChannelSession.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