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)
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)
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
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)))
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