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()
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)
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)
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
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)
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
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()
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)
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)
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)
def get_or_create_contact(self, urn): if ':' not in urn: urn = URN.from_tel(urn) # assume phone number return Contact.get_or_create(self.org, self.user, name=None, urns=[urn])
def to_internal_value(self, data): if isinstance(data, six.string_types): 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, six.string_types): # 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)
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)
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
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)
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)
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))
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()
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)
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)
def handle_group_inbound(self, request, uuid, data): from warapidpro.types import WhatsAppGroupType channel = self.lookup_channel(WhatsAppGroupType.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) group_uuid = data.get('group', {}).get('uuid') # The group webhook receives messages for all groups, # only grab the message if it's a group we're a channel for. if channel.config_json()['group_uuid'] != group_uuid: logger.info('Received message for a different group.') return JsonResponse({}, status=200) 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)
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)
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)
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)
class USSDSession(ChannelSession): USSD_PULL = INCOMING = 'I' USSD_PUSH = OUTGOING = 'O' objects = USSDQuerySet.as_manager() class Meta: proxy = True def start_session_async(self, flow, urn, content, 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, session=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, session=self) def handle_session_async(self, urn, content, date, message_id): from temba.msgs.models import Msg, USSD Msg.create_incoming(channel=self.channel, urn=urn, text=content or '', date=date, session=self, msg_type=USSD, external_id=message_id) def handle_ussd_session_sync(self): # pragma: needs cover # TODO: implement for InfoBip and other sync APIs pass @classmethod def handle_incoming(cls, channel, urn, date, external_id, message_id=None, status=None, flow=None, content=None, starcode=None, org=None, async=True): trigger = None # handle contact with channel urn = URN.from_tel(urn) contact = Contact.get_or_create(channel.org, channel.created_by, urns=[urn], channel=channel) contact_urn = contact.urn_objects[urn] contact.set_preferred_channel(channel) contact_urn.update_affinity(channel) # setup session defaults = dict(channel=channel, contact=contact, contact_urn=contact_urn, org=channel.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=USSDSession.IN_PROGRESS)) # check if there's an initiated PUSH session session = cls.objects.get_initiated_push_session(contact) if not session: session, created = cls.objects.update_or_create( external_id=external_id, defaults=defaults) else: defaults.update(dict(external_id=external_id)) for key, value in six.iteritems(defaults): setattr(session, key, value) session.save() created = None # start session if created and async and trigger: session.start_session_async(trigger.flow, urn, content, date, message_id)
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)
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)
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
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
def get_or_create_contact(self, urn): if ":" not in urn: urn = URN.from_tel(urn) # assume phone number contact, urn_obj = Contact.get_or_create(self.org, urn, user=self.user) return contact
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 create_contacts(self, orgs, locations, num_total): batch_size = 5000 num_test_contacts = len(orgs) * len(USERS) group_membership_model = ContactGroup.contacts.through group_counts = defaultdict(int) self._log("Creating %d test contacts...\n" % num_test_contacts) for org in orgs: for user in org.cache['users']: Contact.get_test_contact(user) self._log("Creating %d regular contacts...\n" % (num_total - num_test_contacts)) base_contact_id = self.get_current_id(Contact) + 1 # Disable table triggers to speed up insertion and in the case of contact group m2m, avoid having an unsquashed # count row for every contact with DisableTriggersOn(Contact, ContactURN, Value, group_membership_model): names = [('%s %s' % (c1, c2)).strip() for c2 in CONTACT_NAMES[1] for c1 in CONTACT_NAMES[0]] names = [n if n else None for n in names] batch = 1 for index_batch in chunk_list(range(num_total - num_test_contacts), batch_size): contacts = [] urns = [] values = [] memberships = [] def add_to_group(g): group_counts[g] += 1 memberships.append(group_membership_model(contact_id=c['id'], contactgroup=g)) for c_index in index_batch: # pragma: no cover org = orgs[c_index] if c_index < len(orgs) else self.random_org(orgs) # at least 1 contact per org name = self.random_choice(names) location = self.random_choice(locations) if self.probability(CONTACT_HAS_FIELD_PROB) else None created_on = self.timeline_date(float(num_test_contacts + c_index) / num_total) c = { 'id': base_contact_id + c_index, # database id this contact will have when created 'org': org, 'user': org.cache['users'][0], 'name': name, 'tel': '+2507%08d' % c_index if self.probability(CONTACT_HAS_TEL_PROB) else None, 'twitter': '%s%d' % (name.replace(' ', '_').lower() if name else 'tweep', c_index) if self.probability(CONTACT_HAS_TWITTER_PROB) else None, 'gender': self.random_choice(('M', 'F')) if self.probability(CONTACT_HAS_FIELD_PROB) else None, 'age': self.random.randint(16, 80) if self.probability(CONTACT_HAS_FIELD_PROB) else None, 'joined': self.random_date() if self.probability(CONTACT_HAS_FIELD_PROB) else None, 'ward': location[0] if location else None, 'district': location[1] if location else None, 'state': location[2] if location else None, 'language': self.random_choice(CONTACT_LANGS), 'is_stopped': self.probability(CONTACT_IS_STOPPED_PROB), 'is_blocked': self.probability(CONTACT_IS_BLOCKED_PROB), 'is_active': self.probability(1 - CONTACT_IS_DELETED_PROB), 'created_on': created_on, 'modified_on': self.random_date(created_on, self.db_ends_on), } if c['is_active']: if not c['is_blocked'] and not c['is_stopped']: add_to_group(org.cache['system_groups'][ContactGroup.TYPE_ALL]) if c['is_blocked']: add_to_group(org.cache['system_groups'][ContactGroup.TYPE_BLOCKED]) if c['is_stopped']: add_to_group(org.cache['system_groups'][ContactGroup.TYPE_STOPPED]) contacts.append(Contact(org=org, name=c['name'], language=c['language'], is_stopped=c['is_stopped'], is_blocked=c['is_blocked'], is_active=c['is_active'], created_by=user, created_on=c['created_on'], modified_by=user, modified_on=c['modified_on'])) if c['tel']: urns.append(ContactURN(org=org, contact_id=c['id'], priority=50, scheme=TEL_SCHEME, path=c['tel'], urn=URN.from_tel(c['tel']))) if c['twitter']: urns.append(ContactURN(org=org, contact_id=c['id'], priority=50, scheme=TWITTER_SCHEME, path=c['twitter'], urn=URN.from_twitter(c['twitter']))) if c['gender']: values.append(Value(org=org, contact_id=c['id'], contact_field=org.cache['fields']['gender'], string_value=c['gender'])) if c['age']: values.append(Value(org=org, contact_id=c['id'], contact_field=org.cache['fields']['age'], string_value=str(c['age']), decimal_value=c['age'])) if c['joined']: values.append(Value(org=org, contact_id=c['id'], contact_field=org.cache['fields']['joined'], string_value=datetime_to_str(c['joined']), datetime_value=c['joined'])) if location: values.append(Value(org=org, contact_id=c['id'], contact_field=org.cache['fields']['ward'], string_value=c['ward'].name, location_value=c['ward'])) values.append(Value(org=org, contact_id=c['id'], contact_field=org.cache['fields']['district'], string_value=c['district'].name, location_value=c['district'])) values.append(Value(org=org, contact_id=c['id'], contact_field=org.cache['fields']['state'], string_value=c['state'].name, location_value=c['state'])) # let each group decide if it is taking this contact for g in org.cache['groups']: if g.member(c) if callable(g.member) else self.probability(g.member): add_to_group(g) Contact.objects.bulk_create(contacts) ContactURN.objects.bulk_create(urns) Value.objects.bulk_create(values) group_membership_model.objects.bulk_create(memberships) self._log(" > Created batch %d of %d\n" % (batch, max(num_total // batch_size, 1))) batch += 1 # create group count records manually counts = [] for group, count in group_counts.items(): counts.append(ContactGroupCount(group=group, count=count, is_squashed=True)) ContactGroupCount.objects.bulk_create(counts) # for sanity check that our presumed last contact id matches the last actual contact id assert c['id'] == Contact.objects.order_by('-id').first().id
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
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)