Example #1
0
    def start_call(self, call, to, from_, status_callback):

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

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

        # our config should have our http basic auth parameters and verboice channel
        config = channel.config_json()

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

        # store the verboice call id in our IVRCall
        call.external_id = response['call_id']
        call.status = IN_PROGRESS
        call.save()
Example #2
0
    def start_call(self, call, to, from_, status_callback):
        if not settings.SEND_CALLS:
            raise ValueError("SEND_CALLS set to False, skipping call start")

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

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

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

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

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

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

        # the call was successfully sent to the IVR provider
        call.status = IVRCall.WIRED
        call.save()
Example #3
0
    def start_call(self, call, to, from_, status_callback):
        if not settings.SEND_CALLS:
            raise IVRException("SEND_CALLS set to False, skipping call start")

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

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

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

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

        # store the verboice call id in our IVRCall
        call.external_id = response['call_id']
        call.status = IVRCall.IN_PROGRESS
        call.save()
Example #4
0
        def save(self, obj):
            urns = []
            for field_key, value in self.form.cleaned_data.iteritems():
                if field_key.startswith('__urn__') and value:
                    scheme = field_key[7:]
                    urns.append((scheme, value))

            Contact.get_or_create(obj.org, self.request.user, obj.name, urns)
Example #5
0
        def save(self, obj):
            urns = []
            for field_key, value in self.form.cleaned_data.iteritems():
                if field_key.startswith('__urn__') and value:
                    scheme = field_key[7:]
                    urns.append((scheme, value))

            Contact.get_or_create(obj.org, self.request.user, obj.name, urns)
Example #6
0
    def create_contact(self, name=None, number=None, twitter=None, urn=None, is_test=False, **kwargs):
        """
        Create a contact in the master test org
        """
        urns = []
        if number:
            urns.append((TEL_SCHEME, number))
        if twitter:
            urns.append((TWITTER_SCHEME, 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)
Example #7
0
    def create_contact(self,
                       name=None,
                       number=None,
                       twitter=None,
                       urn=None,
                       is_test=False,
                       **kwargs):
        """
        Create a contact in the master test org
        """
        urns = []
        if number:
            urns.append(URN.from_tel(number))
        if twitter:
            urns.append(URN.from_twitter(twitter))
        if urn:
            urns.append(urn)

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

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

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

        return Contact.get_or_create(**kwargs)
Example #8
0
    def restore_object(self, attrs, instance=None):
        """
        Actually start our flows for each contact
        """
        if instance:  # pragma: no cover
            raise ValidationError("Invalid operation")

        flow = attrs['flow']
        groups = attrs.get('groups', [])
        contacts = attrs.get('contacts', [])
        extra = attrs.get('extra', None)

        # include contacts created/matched via deprecated phone field
        phone_urns = attrs.get('phone', [])
        if phone_urns:
            channel = self.org.get_send_channel(TEL_SCHEME)
            for urn in phone_urns:
                # treat each URN as separate contact
                contact = Contact.get_or_create(self.user,
                                                channel.org,
                                                urns=[urn],
                                                channel=channel)
                contacts.append(contact)

        if contacts or groups:
            return flow.start(groups,
                              contacts,
                              restart_participants=True,
                              extra=extra)
        else:
            return []
Example #9
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        if 'urn' in self.validated_data and self.validated_data['urn']:
            urns = self.validated_data.get('urn')
        else:
            urns = self.validated_data.get('phone', [])

        channel = self.validated_data.get('channel')
        contacts = list()
        for urn in urns:
            # treat each urn as a separate contact
            contacts.append(
                Contact.get_or_create(channel.org, self.user, urns=[urn]))

        # add any contacts specified by uuids
        uuid_contacts = self.validated_data.get('contact', [])
        for contact in uuid_contacts:
            contacts.append(contact)

        # create the broadcast
        broadcast = Broadcast.create(self.org,
                                     self.user,
                                     self.validated_data['text'],
                                     recipients=contacts,
                                     channel=channel)

        # send it
        broadcast.send(expressions_context={})
        return broadcast
Example #10
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        if "urn" in self.validated_data and self.validated_data["urn"]:
            urns = self.validated_data.get("urn")
        else:
            urns = self.validated_data.get("phone", [])

        channel = self.validated_data.get("channel")
        contacts = list()
        for urn in urns:
            # treat each urn as a separate contact
            contact, urn_obj = Contact.get_or_create(channel.org, urn, user=self.user)
            contacts.append(contact)

        # add any contacts specified by uuids
        uuid_contacts = self.validated_data.get("contact", [])
        for contact in uuid_contacts:
            contacts.append(contact)

        # create the broadcast
        broadcast = Broadcast.create(
            self.org, self.user, self.validated_data["text"], contacts=contacts, channel=channel
        )

        # send it
        broadcast.send(expressions_context={})
        return broadcast
Example #11
0
    def restore_object(self, attrs, instance=None):
        """
        Create a new broadcast to send out
        """
        if instance: # pragma: no cover
            raise ValidationError("Invalid operation")

        user = self.user
        org = self.org

        if 'urn' in attrs and attrs['urn']:
            urns = attrs.get('urn', [])
        else:
            urns = attrs.get('phone', [])
        
        # create the broadcast
        broadcast = Broadcast.create(user, attrs['text'], org=org)

        channel = attrs['channel']
        contacts = list()
        for urn in urns:
            # treat each urn as a separate contact
            contacts.append(Contact.get_or_create(user, channel.org, urns=[urn], channel=channel))

        # add any contacts specified by uuids
        uuid_contacts = attrs.get('contact', [])
        for contact in uuid_contacts:
            contacts.append(contact)

        broadcast.set_recipients(*contacts)

        # send it
        broadcast.send()
        return broadcast
Example #12
0
    def validate_urns(self, value):
        urn_contacts = []
        for urn in value:
            contact = Contact.get_or_create(self.context['org'], self.context['user'], urns=[urn])
            urn_contacts.append(contact)

        return urn_contacts
Example #13
0
    def restore_object(self, attrs, instance=None):
        """
        Create a new broadcast to send out
        """
        if instance: # pragma: no cover
            raise ValidationError("Invalid operation")

        user = self.user
        org = self.org

        if 'urn' in attrs and attrs['urn']:
            urns = attrs.get('urn', [])
        else:
            urns = attrs.get('phone', [])

        channel = attrs['channel']
        contacts = list()
        for urn in urns:
            # treat each urn as a separate contact
            contacts.append(Contact.get_or_create(user, channel.org, urns=[urn], channel=channel))

        # add any contacts specified by uuids
        uuid_contacts = attrs.get('contact', [])
        for contact in uuid_contacts:
            contacts.append(contact)

        # create the broadcast
        broadcast = Broadcast.create(org, user, attrs['text'], recipients=contacts)

        # send it
        broadcast.send()
        return broadcast
Example #14
0
    def start_call(self, call, to, from_, status_callback):

        channel = call.channel
        Contact.get_or_create(channel.org, channel.created_by, urns=[(TEL_SCHEME, 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()

        # store the verboice call id in our IVRCall
        call.external_id = response['call_id']
        call.status = IN_PROGRESS
        call.save()
Example #15
0
    def save(self):
        urns = self.validated_data.get("urns", [])
        contacts = self.validated_data.get("contacts", [])
        groups = self.validated_data.get("groups", [])
        restart_participants = self.validated_data.get("restart_participants",
                                                       True)
        extra = self.validated_data.get("extra")

        params = self.validated_data.get("params")
        if params:
            extra = params

        # convert URNs to contacts
        for urn in urns:
            contact, urn_obj = Contact.get_or_create(self.context["org"],
                                                     urn,
                                                     user=self.context["user"])
            contacts.append(contact)

        # ok, let's go create our flow start, the actual starting will happen in our view
        return FlowStart.create(
            self.validated_data["flow"],
            self.context["user"],
            restart_participants=restart_participants,
            contacts=contacts,
            groups=groups,
            extra=extra,
        )
Example #16
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        contact_urns = []
        for urn in self.validated_data.get("urns", []):
            # create contacts for URNs if necessary
            __, contact_urn = Contact.get_or_create(self.context["org"],
                                                    urn,
                                                    user=self.context["user"])
            contact_urns.append(contact_urn)

        text, base_language = self.validated_data["text"]

        if not self.validated_data["new_expressions"]:
            text = migrate_translations(text)

        # create the broadcast
        broadcast = Broadcast.create(
            self.context["org"],
            self.context["user"],
            text=text,
            base_language=base_language,
            groups=self.validated_data.get("groups", []),
            contacts=self.validated_data.get("contacts", []),
            urns=contact_urns,
            channel=self.validated_data.get("channel"),
            template_state=Broadcast.TEMPLATE_STATE_UNEVALUATED,
        )

        # send it
        on_transaction_commit(lambda: broadcast.send())

        return broadcast
Example #17
0
    def restore_object(self, attrs, instance=None):
        """
        Actually start our flows for each contact
        """
        if instance:  # pragma: no cover
            raise ValidationError("Invalid operation")

        flow = attrs['flow']
        groups = attrs.get('groups', [])
        contacts = attrs.get('contacts', [])
        extra = attrs.get('extra', None)

        # include contacts created/matched via deprecated phone field
        phone_urns = attrs.get('phone', [])
        if phone_urns:
            channel = self.org.get_send_channel(TEL_SCHEME)
            for urn in phone_urns:
                # treat each URN as separate contact
                contact = Contact.get_or_create(self.user, channel.org, urns=[urn], channel=channel)
                contacts.append(contact)

        if contacts or groups:
            return flow.start(groups, contacts, restart_participants=True, extra=extra)
        else:
            return []
Example #18
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        if "urn" in self.validated_data and self.validated_data["urn"]:
            urns = self.validated_data.get("urn")
        else:
            urns = self.validated_data.get("phone", [])

        channel = self.validated_data.get("channel")
        contacts = list()
        for urn in urns:
            # treat each urn as a separate contact
            contact, urn_obj = Contact.get_or_create(channel.org,
                                                     urn,
                                                     user=self.user)
            contacts.append(contact)

        # add any contacts specified by uuids
        uuid_contacts = self.validated_data.get("contact", [])
        for contact in uuid_contacts:
            contacts.append(contact)

        # create the broadcast
        broadcast = Broadcast.create(self.org,
                                     self.user,
                                     self.validated_data["text"],
                                     contacts=contacts,
                                     channel=channel)

        # send it
        broadcast.send(expressions_context={})
        return broadcast
Example #19
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        recipients = self.validated_data.get(
            'contacts', []) + self.validated_data.get('groups', [])

        for urn in self.validated_data.get('urns', []):
            # create contacts for URNs if necessary
            contact = Contact.get_or_create(self.context['org'],
                                            self.context['user'],
                                            urns=[urn])
            contact_urn = contact.urn_objects[urn]
            recipients.append(contact_urn)

        text, base_language = self.validated_data['text']

        # create the broadcast
        broadcast = Broadcast.create(
            self.context['org'],
            self.context['user'],
            text=text,
            base_language=base_language,
            recipients=recipients,
            channel=self.validated_data.get('channel'))

        # send in task
        on_transaction_commit(lambda: send_broadcast_task.delay(broadcast.id))

        return broadcast
Example #20
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        contact_urns = []
        for urn in self.validated_data.get("urns", []):
            # create contacts for URNs if necessary
            __, contact_urn = Contact.get_or_create(self.context["org"], urn, user=self.context["user"])
            contact_urns.append(contact_urn)

        text, base_language = self.validated_data["text"]

        # create the broadcast
        broadcast = Broadcast.create(
            self.context["org"],
            self.context["user"],
            text=text,
            base_language=base_language,
            groups=self.validated_data.get("groups", []),
            contacts=self.validated_data.get("contacts", []),
            urns=contact_urns,
            channel=self.validated_data.get("channel"),
        )

        # send in task
        on_transaction_commit(lambda: send_broadcast_task.delay(broadcast.id))

        return broadcast
Example #21
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        contact_urns = []
        for urn in self.validated_data.get("urns", []):
            # create contacts for URNs if necessary
            __, contact_urn = Contact.get_or_create(self.context["org"],
                                                    urn,
                                                    user=self.context["user"])
            contact_urns.append(contact_urn)

        text, base_language = self.validated_data["text"]

        # create the broadcast
        broadcast = Broadcast.create(
            self.context["org"],
            self.context["user"],
            text=text,
            base_language=base_language,
            groups=self.validated_data.get("groups", []),
            contacts=self.validated_data.get("contacts", []),
            urns=contact_urns,
            channel=self.validated_data.get("channel"),
        )

        # send in task
        on_transaction_commit(lambda: send_broadcast_task.delay(broadcast.id))

        return broadcast
Example #22
0
    def restore_object(self, attrs, instance=None):
        """
        Actually start our flows for each contact
        """
        if instance:  # pragma: no cover
            raise ValidationError("Invalid operation")

        flow = attrs['flow']
        channel = self.org.get_send_channel(TEL_SCHEME)

        if 'urn' in attrs and attrs['urn']:
            urns = attrs.get('urn', [])
        else:
            urns = attrs.get('phone', [])

        contacts = []
        for urn in urns:
            # treat each URN as separate contact
            contact = Contact.get_or_create(self.user, channel.org, urns=[urn], channel=channel)
            contacts.append(contact)

        # also add in any contacts specified by uuid
        uuid_contacts = attrs.get('contact', [])
        contacts += uuid_contacts

        if contacts:
            runs = flow.start([], contacts, restart_participants=True, extra=attrs.get('extra', None))
        else:
            runs = []

        return runs
Example #23
0
    def get_or_create_contact(self, urn):
        if ':' in urn:
            parsed = ContactURN.parse_urn(urn)
            urn = (parsed.scheme, parsed.path)
        else:
            urn = (TEL_SCHEME, urn)  # assume phone number

        return Contact.get_or_create(self.org, self.user, name=None, urns=[urn])
Example #24
0
    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])
Example #25
0
    def validate_urns(self, value):
        urn_contacts = []
        for urn in value:
            contact = Contact.get_or_create(self.context['org'],
                                            self.context['user'],
                                            urns=[urn])
            urn_contacts.append(contact)

        return urn_contacts
Example #26
0
    def start_call(self, call, to, from_, status_callback):

        channel = call.channel
        Contact.get_or_create(channel.org, channel.created_by, urns=[(TEL_SCHEME, 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()
Example #27
0
    def start_call(self, call, to, from_, status_callback):

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

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

        # our config should have our http basic auth parameters and verboice channel
        config = channel.config_json()

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

        # store the verboice call id in our IVRCall
        call.external_id = response["call_id"]
        call.status = IN_PROGRESS
        call.save()
Example #28
0
    def save(self):
        """
        Update our contact
        """
        name = self.validated_data.get('name')
        fields = self.validated_data.get('fields')
        language = self.validated_data.get('language')

        changed = []

        if self.instance:
            if self.parsed_urns is not None:
                self.instance.update_urns(self.user, self.parsed_urns)

            # update our name and language
            if name != self.instance.name:
                self.instance.name = name
                changed.append('name')
        else:
            self.instance = Contact.get_or_create(self.org,
                                                  self.user,
                                                  name,
                                                  urns=self.parsed_urns,
                                                  language=language)

        # Contact.get_or_create doesn't nullify language so do that here
        if 'language' in self.validated_data and language is None:
            self.instance.language = language.lower() if language else None
            self.instance.save()

        # save our contact if it changed
        if changed:
            self.instance.save(update_fields=changed)

        # update our fields
        if fields is not None:
            for key, value in fields.items():
                existing_by_key = ContactField.objects.filter(
                    org=self.org, key__iexact=key, is_active=True).first()
                if existing_by_key:
                    self.instance.set_field(self.user, existing_by_key.key,
                                            value)
                    continue

                # TODO as above, need to get users to stop updating via label
                existing_by_label = ContactField.get_by_label(self.org, key)
                if existing_by_label:
                    self.instance.set_field(self.user, existing_by_label.key,
                                            value)

        # update our contact's groups
        if self.group_objs is not None:
            self.instance.update_static_groups(self.user, self.group_objs)

        return self.instance
Example #29
0
    def _create_contacts(self, count, base_names):
        """
        Creates the given number of contacts with URNs of each type, and fields value for dob and nickname
        """
        contacts = []

        for c in range(0, count):
            name = '%s %d' % (base_names[c % len(base_names)], c + 1)
            scheme, path, channel = self.urn_generators[c % len(self.urn_generators)](c)
            contacts.append(Contact.get_or_create(self.org, self.user, name, urns=[':'.join([scheme, path])]))
        return contacts
Example #30
0
    def _create_contacts(self, count, base_names):
        """
        Creates the given number of contacts with URNs of each type, and fields value for dob and nickname
        """
        contacts = []

        for c in range(0, count):
            name = '%s %d' % (base_names[c % len(base_names)], c + 1)
            scheme, path, channel = self.urn_generators[c % len(self.urn_generators)](c)
            contacts.append(Contact.get_or_create(self.user, self.org, name, urns=[(scheme, path)]))
        return contacts
Example #31
0
    def get_or_create_contact(self, urn):
        if ':' in urn:
            parsed = ContactURN.parse_urn(urn)
            urn = (parsed.scheme, parsed.path)
        else:
            urn = (TEL_SCHEME, urn)  # assume phone number

        return Contact.get_or_create(self.org,
                                     self.user,
                                     name=None,
                                     urns=[urn])
Example #32
0
    def create_contact(self, name=None, number=None, twitter=None):
        """
        Create a contact in the master test org
        """
        urns = []
        if number:
            urns.append((TEL_SCHEME, number))
        if twitter:
            urns.append((TWITTER_SCHEME, twitter))

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

        return Contact.get_or_create(self.user, org=self.org, name=name, urns=urns)
Example #33
0
    def create_contact(self, name=None, number=None, twitter=None, is_test=False):
        """
        Create a contact in the master test org
        """
        urns = []
        if number:
            urns.append((TEL_SCHEME, number))
        if twitter:
            urns.append((TWITTER_SCHEME, twitter))

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

        return Contact.get_or_create(self.org, self.user, name, urns=urns, is_test=is_test)
Example #34
0
def omnibox_deserialize(org, omnibox, user=None):
    group_ids = [item["id"] for item in omnibox if item["type"] == "group"]
    contact_ids = [item["id"] for item in omnibox if item["type"] == "contact"]
    urn_specs = [item["id"] for item in omnibox if item["type"] == "urn"]

    urns = []
    if not org.is_anon:
        for urn_spec in urn_specs:
            contact, urn = Contact.get_or_create(org, urn_spec, user)
            urns.append(urn)

    return {
        "groups": ContactGroup.all_groups.filter(uuid__in=group_ids, org=org, is_active=True),
        "contacts": Contact.objects.filter(uuid__in=contact_ids, org=org, is_active=True),
        "urns": urns,
    }
Example #35
0
    def save(self):
        urns = self.validated_data.get('urns', [])
        contacts = self.validated_data.get('contacts', [])
        groups = self.validated_data.get('groups', [])
        restart_participants = self.validated_data.get('restart_participants', True)
        extra = self.validated_data.get('extra')

        # convert URNs to contacts
        for urn in urns:
            contact = Contact.get_or_create(self.context['org'], self.context['user'], urns=[urn])
            contacts.append(contact)

        # ok, let's go create our flow start, the actual starting will happen in our view
        return FlowStart.create(self.validated_data['flow'], self.context['user'],
                                restart_participants=restart_participants,
                                contacts=contacts, groups=groups, extra=extra)
Example #36
0
    def save(self):
        urns = self.validated_data.get('urns', [])
        contacts = self.validated_data.get('contacts', [])
        groups = self.validated_data.get('groups', [])
        restart_participants = self.validated_data.get('restart_participants', True)
        extra = self.validated_data.get('extra')

        # convert URNs to contacts
        for urn in urns:
            contact = Contact.get_or_create(self.context['org'], self.context['user'], urns=[urn])
            contacts.append(contact)

        # ok, let's go create our flow start, the actual starting will happen in our view
        return FlowStart.create(self.validated_data['flow'], self.context['user'],
                                restart_participants=restart_participants,
                                contacts=contacts, groups=groups, extra=extra)
Example #37
0
    def save(self):
        """
        Update our contact
        """
        name = self.validated_data.get('name')
        language = self.validated_data.get('language')
        urns = self.validated_data.get('urns')
        groups = self.validated_data.get('groups')
        fields = self.validated_data.get('fields')

        changed = []

        if self.instance:
            # update our name and language
            if 'name' in self.validated_data and name != self.instance.name:
                self.instance.name = name
                changed.append('name')
            if 'language' in self.validated_data and language != self.instance.language:
                self.instance.language = language
                changed.append('language')

            if 'urns' in self.validated_data and urns is not None:
                self.instance.update_urns(self.context['user'], urns)

            if changed:
                self.instance.save(update_fields=changed)
        else:
            if urns is None:
                # if user is using URN as identifier, ok to create contact from it if they don't already exist
                urn_as_id = self.validated_data.get('urn')
                if urn_as_id:
                    urns = [urn_as_id]
                else:
                    urns = []

            self.instance = Contact.get_or_create(self.context['org'], self.context['user'], name, urns=urns, language=language)

        # update our fields
        if fields is not None:
            for key, value in fields.items():
                self.instance.set_field(self.context['user'], key, value)

        # update our groups
        if groups is not None:
            self.instance.update_static_groups(self.context['user'], groups)

        return self.instance
Example #38
0
    def save(self):
        """
        Update our contact
        """
        name = self.validated_data.get('name')
        language = self.validated_data.get('language')
        urns = self.validated_data.get('urns')
        groups = self.validated_data.get('groups')
        fields = self.validated_data.get('fields')

        changed = []

        if self.instance:
            # update our name and language
            if 'name' in self.validated_data and name != self.instance.name:
                self.instance.name = name
                changed.append('name')
            if 'language' in self.validated_data and language != self.instance.language:
                self.instance.language = language
                changed.append('language')

            if 'urns' in self.validated_data and urns is not None:
                self.instance.update_urns(self.context['user'], urns)

            if changed:
                self.instance.save(update_fields=changed)
        else:
            if urns is None:
                # if user is using URN as identifier, ok to create contact from it if they don't already exist
                urn_as_id = self.validated_data.get('urn')
                if urn_as_id:
                    urns = [urn_as_id]
                else:
                    urns = []

            self.instance = Contact.get_or_create(self.context['org'], self.context['user'], name, urns=urns, language=language)

        # update our fields
        if fields is not None:
            for key, value in fields.items():
                self.instance.set_field(self.context['user'], key, value)

        # update our groups
        if groups is not None:
            self.instance.update_static_groups(self.context['user'], groups)

        return self.instance
Example #39
0
    def create_group_contacts(self, spec, org, user):
        self._log(f"Generating group contacts...")

        for g in spec["groups"]:
            size = int(g.get("size", 0))
            if size > 0:
                group = ContactGroup.user_groups.get(org=org, name=g["name"])

                contacts = []
                for i in range(size):
                    urn = "tel:+250788%06d" % i
                    contact, _ = Contact.get_or_create(org, urn, user=user)
                    contacts.append(contact)

                group.update_contacts(user, contacts, True)

        self._log(self.style.SUCCESS("OK") + "\n")
Example #40
0
    def save(self):
        """
        Update our contact
        """
        name = self.validated_data.get('name')
        language = self.validated_data.get('language')
        urns = self.validated_data.get('urns')
        groups = self.validated_data.get('groups')
        custom_fields = self.validated_data.get('fields')

        changed = []

        if self.instance:
            # update our name and language
            if 'name' in self.validated_data and name != self.instance.name:
                self.instance.name = name
                changed.append('name')
            if 'language' in self.validated_data and language != self.instance.language:
                self.instance.language = language
                changed.append('language')

            if 'urns' in self.validated_data and urns is not None:
                self.instance.update_urns(self.context['user'], urns)

            if changed:
                self.instance.save(update_fields=changed)
        else:
            self.instance = Contact.get_or_create(self.context['org'],
                                                  self.context['user'],
                                                  name,
                                                  urns=urns,
                                                  language=language)

        # update our fields
        if custom_fields is not None:
            for key, value in six.iteritems(custom_fields):
                self.instance.set_field(self.context['user'], key, value)

        # update our groups
        if groups is not None:
            self.instance.update_static_groups(self.context['user'], groups)

        return self.instance
Example #41
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        from temba.msgs.tasks import send_broadcast_task

        recipients = self.validated_data.get('contacts', []) + self.validated_data.get('groups', [])

        for urn in self.validated_data.get('urns', []):
            # create contacts for URNs if necessary
            contact = Contact.get_or_create(self.context['org'], self.context['user'], urns=[urn])
            contact_urn = contact.urn_objects[urn]
            recipients.append(contact_urn)

        # create the broadcast
        broadcast = Broadcast.create(self.context['org'], self.context['user'], self.validated_data['text'],
                                     recipients=recipients, channel=self.validated_data.get('channel'))

        # send in task
        send_broadcast_task.delay(broadcast.id)
        return broadcast
Example #42
0
    def save(self):
        urns = self.validated_data.get("urns", [])
        contacts = self.validated_data.get("contacts", [])
        groups = self.validated_data.get("groups", [])
        restart_participants = self.validated_data.get("restart_participants", True)
        extra = self.validated_data.get("extra")

        # convert URNs to contacts
        for urn in urns:
            contact, urn_obj = Contact.get_or_create(self.context["org"], urn, user=self.context["user"])
            contacts.append(contact)

        # ok, let's go create our flow start, the actual starting will happen in our view
        return FlowStart.create(
            self.validated_data["flow"],
            self.context["user"],
            restart_participants=restart_participants,
            contacts=contacts,
            groups=groups,
            extra=extra,
        )
Example #43
0
    def save(self):
        """
        Create a new broadcast to send out
        """
        from temba.msgs.tasks import send_broadcast_task

        recipients = self.validated_data.get('contacts', []) + self.validated_data.get('groups', [])

        for urn in self.validated_data.get('urns', []):
            # create contacts for URNs if necessary
            contact = Contact.get_or_create(self.context['org'], self.context['user'], urns=[urn])
            contact_urn = contact.urn_objects[urn]
            recipients.append(contact_urn)

        # create the broadcast
        broadcast = Broadcast.create(self.context['org'], self.context['user'], self.validated_data['text'],
                                     recipients=recipients, channel=self.validated_data.get('channel'))

        # send in task
        send_broadcast_task.delay(broadcast.id)
        return broadcast
Example #44
0
    def save(self):
        """
        Update our contact
        """
        name = self.validated_data.get('name')
        language = self.validated_data.get('language')
        urns = self.validated_data.get('urns')
        groups = self.validated_data.get('groups')
        custom_fields = self.validated_data.get('fields')

        changed = []

        if self.instance:
            # update our name and language
            if 'name' in self.validated_data and name != self.instance.name:
                self.instance.name = name
                changed.append('name')
            if 'language' in self.validated_data and language != self.instance.language:
                self.instance.language = language
                changed.append('language')

            if 'urns' in self.validated_data and urns is not None:
                self.instance.update_urns(self.context['user'], urns)

            if changed:
                self.instance.save(update_fields=changed)
        else:
            self.instance = Contact.get_or_create(self.context['org'], self.context['user'], name,
                                                  urns=urns, language=language)

        # update our fields
        if custom_fields is not None:
            for key, value in six.iteritems(custom_fields):
                self.instance.set_field(self.context['user'], key, value)

        # update our groups
        if groups is not None:
            self.instance.update_static_groups(self.context['user'], groups)

        return self.instance
Example #45
0
    def restore_object(self, attrs, instance=None):
        """
        Create a new broadcast to send out
        """
        from temba.msgs.tasks import send_broadcast_task

        if instance:  # pragma: no cover
            raise ValidationError("Invalid operation")

        recipients = attrs.get('contacts') + attrs.get('groups')

        for urn in attrs.get('urns'):
            # create contacts for URNs if necessary
            contact = Contact.get_or_create(self.org, self.user, urns=[urn])
            contact_urn = contact.urn_objects[urn]
            recipients.append(contact_urn)

        # create the broadcast
        broadcast = Broadcast.create(self.org, self.user, attrs['text'],
                                     recipients=recipients, channel=attrs['channel'])

        # send in task
        send_broadcast_task.delay(broadcast.id)
        return broadcast
Example #46
0
    def post(self, request, *args, **kwargs):
        from twilio.request_validator import RequestValidator
        from temba.flows.models import FlowSession

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

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

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

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

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

            org = channel.org

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

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

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

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

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

                flow = Trigger.find_flow_for_inbound_call(contact)

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

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

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

                else:

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

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

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

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

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

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

        return HttpResponse("Not Handled, unknown action", status=400)  # pragma: no cover
Example #47
0
    def get(self, request, *args, **kwargs):
        from temba.flows.models import FlowSession
        from temba.ivr.models import IVRCall

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

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

        request_uuid = kwargs["uuid"]

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

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

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

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

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

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

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

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

            return JsonResponse(response)

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

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

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

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

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

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

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

            flow = Trigger.find_flow_for_inbound_call(contact)

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

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

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

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

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

                # either way, we need to hangup now
                return JsonResponse(json.loads(str(response)), safe=False)
Example #48
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 #49
0
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)
Example #50
0
    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])
Example #51
0
    def test_event_deliveries(self):
        sms = self.create_msg(contact=self.joe, direction="I", status="H", text="I'm gonna pop some tags")

        with patch("requests.Session.send") as mock:
            now = timezone.now()
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event, shouldnn't fire as we don't have a webhook
            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            self.assertFalse(WebHookEvent.objects.all())

        self.setupChannel()

        with patch("requests.Session.send") as mock:
            # clear out which events we listen for, we still shouldnt be notified though we have a webhook
            self.channel.org.webhook_events = 0
            self.channel.org.save()

            now = timezone.now()
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event, shouldnn't fire as we don't have a webhook
            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            self.assertFalse(WebHookEvent.objects.all())

        self.setupChannel()

        with patch("requests.Session.send") as mock:
            # remove all the org users
            self.org.administrators.clear()
            self.org.editors.clear()
            self.org.viewers.clear()

            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event
            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEqual("F", event.status)
            self.assertEqual(0, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertIn("No active user", result.message)
            self.assertEqual(0, result.status_code)

            self.assertFalse(mock.called)

            # what if they send weird json back?
            self.release(WebHookEvent.objects.all())

        # add ad manager back in
        self.org.administrators.add(self.admin)
        self.admin.set_org(self.org)

        with patch("requests.Session.send") as mock:
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event
            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEqual("C", event.status)
            self.assertEqual(1, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertIn("Event delivered successfully", result.message)
            self.assertIn("not JSON", result.message)
            self.assertEqual(200, result.status_code)

            self.assertTrue(mock.called)

            self.release(WebHookEvent.objects.all())

        with patch("requests.Session.send") as mock:
            mock.side_effect = [MockResponse(500, "I am error")]

            # trigger an event
            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.all().first()

            self.assertEqual("E", event.status)
            self.assertEqual(1, event.try_count)
            self.assertTrue(event.next_attempt)

            mock.return_value = MockResponse(200, "Hello World")
            # simulate missing channel
            event.channel = None
            event.save()

            # no exception should raised
            event.deliver()

            self.assertTrue(mock.called)
            self.assertEqual(mock.call_count, 2)

            self.release(WebHookEvent.objects.all())

        with patch("requests.Session.send") as mock:
            # valid json, but not our format
            bad_json = '{ "thrift_shops": ["Goodwill", "Value Village"] }'
            mock.return_value = MockResponse(200, bad_json)

            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEqual("C", event.status)
            self.assertEqual(1, event.try_count)
            self.assertFalse(event.next_attempt)

            self.assertTrue(mock.called)

            result = WebHookResult.objects.get()
            self.assertIn("Event delivered successfully", result.message)
            self.assertIn("ignoring", result.message)
            self.assertEqual(200, result.status_code)
            self.assertEqual(bad_json, result.body)

            self.release(WebHookEvent.objects.all())

        with patch("requests.Session.send") as mock:
            mock.return_value = MockResponse(200, '{ "phone": "+250788123123", "text": "I am success" }')

            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEqual("C", event.status)
            self.assertEqual(1, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertEqual(200, result.status_code)

            self.assertTrue(mock.called)

            broadcast = Broadcast.objects.get()
            contact, urn_obj = Contact.get_or_create(self.org, "tel:+250788123123", self.channel, user=self.admin)
            self.assertTrue(broadcast.text, {"base": "I am success"})
            self.assertTrue(contact, broadcast.contacts.all())

            self.assertTrue(mock.called)
            args = mock.call_args_list[0][0]
            prepared_request = args[0]
            self.assertEqual(self.org.get_webhook_url(), prepared_request.url)

            data = parse_qs(prepared_request.body)
            self.assertEqual(self.joe.get_urn(TEL_SCHEME).path, data["phone"][0])
            self.assertEqual(str(self.joe.get_urn(TEL_SCHEME)), data["urn"][0])
            self.assertEqual(self.joe.uuid, data["contact"][0])
            self.assertEqual(self.joe.name, data["contact_name"][0])
            self.assertEqual(sms.pk, int(data["sms"][0]))
            self.assertEqual(self.channel.pk, int(data["channel"][0]))
            self.assertEqual(WebHookEvent.TYPE_SMS_RECEIVED, data["event"][0])
            self.assertEqual("I'm gonna pop some tags", data["text"][0])
            self.assertIn("time", data)

            self.release(WebHookEvent.objects.all())

        with patch("requests.Session.send") as mock:
            mock.return_value = MockResponse(500, "I am error")

            next_attempt_earliest = timezone.now() + timedelta(minutes=4)
            next_attempt_latest = timezone.now() + timedelta(minutes=6)

            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEqual("E", event.status)
            self.assertEqual(1, event.try_count)
            self.assertTrue(event.next_attempt)
            self.assertTrue(next_attempt_earliest < event.next_attempt and next_attempt_latest > event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertIn("Error", result.message)
            self.assertEqual(500, result.status_code)
            self.assertEqual("I am error", result.body)

            # make sure things become failures after three retries
            event.try_count = 2
            event.deliver()
            event.save()

            self.assertTrue(mock.called)

            self.assertEqual("F", event.status)
            self.assertEqual(3, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertIn("Error", result.message)
            self.assertEqual(500, result.status_code)
            self.assertEqual("I am error", result.body)
            self.assertEqual("http://fake.com/webhook.php", result.url)
            self.assertTrue(result.data.find("pop+some+tags") > 0)

            # check out our api log
            response = self.client.get(reverse("api.log"))
            self.assertRedirect(response, reverse("users.user_login"))

            response = self.client.get(reverse("api.log_read", args=[event.pk]))
            self.assertRedirect(response, reverse("users.user_login"))

            self.release(WebHookEvent.objects.all())

        # add a webhook header to the org
        self.channel.org.webhook = {
            "url": "http://fake.com/webhook.php",
            "headers": {"X-My-Header": "foobar", "Authorization": "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="},
            "method": "POST",
        }
        self.channel.org.save()

        # check that our webhook settings have saved
        self.assertEqual("http://fake.com/webhook.php", self.channel.org.get_webhook_url())
        self.assertDictEqual(
            {"X-My-Header": "foobar", "Authorization": "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="},
            self.channel.org.get_webhook_headers(),
        )

        with patch("requests.Session.send") as mock:
            mock.return_value = MockResponse(200, "Boom")
            WebHookEvent.trigger_sms_event(WebHookEvent.TYPE_SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            result = WebHookResult.objects.get()
            # both headers should be in the json-encoded url string
            self.assertIn("X-My-Header: foobar", result.request)
            self.assertIn("Authorization: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", result.request)
Example #52
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 #53
0
    def test_event_deliveries(self):
        sms = self.create_msg(contact=self.joe, direction='I', status='H', text="I'm gonna pop some tags")

        with patch('requests.Session.send') as mock:
            now = timezone.now()
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event, shouldnn't fire as we don't have a webhook
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            self.assertFalse(WebHookEvent.objects.all())

        self.setupChannel()

        with patch('requests.Session.send') as mock:
            # clear out which events we listen for, we still shouldnt be notified though we have a webhook
            self.channel.org.webhook_events = 0
            self.channel.org.save()

            now = timezone.now()
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event, shouldnn't fire as we don't have a webhook
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            self.assertFalse(WebHookEvent.objects.all())

        self.setupChannel()

        with patch('requests.Session.send') as mock:
            # remove all the org users
            self.org.administrators.clear()
            self.org.editors.clear()
            self.org.viewers.clear()

            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('F', event.status)
            self.assertEquals(0, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("No active user", result.message)
            self.assertEquals(0, result.status_code)

            self.assertFalse(mock.called)

            # what if they send weird json back?
            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        # add ad manager back in
        self.org.administrators.add(self.admin)
        self.admin.set_org(self.org)

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Event delivered successfully", result.message)
            self.assertStringContains("not JSON", result.message)
            self.assertEquals(200, result.status_code)

            self.assertTrue(mock.called)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            # valid json, but not our format
            bad_json = '{ "thrift_shops": ["Goodwill", "Value Village"] }'
            mock.return_value = MockResponse(200, bad_json)

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            self.assertTrue(mock.called)

            result = WebHookResult.objects.get()
            self.assertStringContains("Event delivered successfully", result.message)
            self.assertStringContains("ignoring", result.message)
            self.assertEquals(200, result.status_code)
            self.assertEquals(bad_json, result.body)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, '{ "phone": "+250788123123", "text": "I am success" }')

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertEquals(200, result.status_code)

            self.assertTrue(mock.called)

            broadcast = Broadcast.objects.get()
            contact = Contact.get_or_create(self.org, self.admin, name=None, urns=["tel:+250788123123"], channel=self.channel)
            self.assertTrue("I am success", broadcast.text)
            self.assertTrue(contact, broadcast.contacts.all())

            self.assertTrue(mock.called)
            args = mock.call_args_list[0][0]
            prepared_request = args[0]
            self.assertEquals(self.org.get_webhook_url(), prepared_request.url)

            data = parse_qs(prepared_request.body)
            self.assertEquals(self.joe.get_urn(TEL_SCHEME).path, data['phone'][0])
            self.assertEquals(unicode(self.joe.get_urn(TEL_SCHEME)), data['urn'][0])
            self.assertEquals(self.joe.uuid, data['contact'][0])
            self.assertEquals(self.joe.name, data['contact_name'][0])
            self.assertEquals(sms.pk, int(data['sms'][0]))
            self.assertEquals(self.channel.pk, int(data['channel'][0]))
            self.assertEquals(SMS_RECEIVED, data['event'][0])
            self.assertEquals("I'm gonna pop some tags", data['text'][0])
            self.assertTrue('time' in data)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(500, "I am error")

            next_attempt_earliest = timezone.now() + timedelta(minutes=4)
            next_attempt_latest = timezone.now() + timedelta(minutes=6)

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('E', event.status)
            self.assertEquals(1, event.try_count)
            self.assertTrue(event.next_attempt)
            self.assertTrue(next_attempt_earliest < event.next_attempt and next_attempt_latest > event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Error", result.message)
            self.assertEquals(500, result.status_code)
            self.assertEquals("I am error", result.body)

            # make sure things become failures after three retries
            event.try_count = 2
            event.deliver()
            event.save()

            self.assertTrue(mock.called)

            self.assertEquals('F', event.status)
            self.assertEquals(3, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Error", result.message)
            self.assertEquals(500, result.status_code)
            self.assertEquals("I am error", result.body)
            self.assertEquals("http://fake.com/webhook.php", result.url)
            self.assertTrue(result.data.find("pop+some+tags") > 0)

            # check out our api log
            response = self.client.get(reverse('api.log'))
            self.assertRedirect(response, reverse('users.user_login'))

            response = self.client.get(reverse('api.log_read', args=[event.pk]))
            self.assertRedirect(response, reverse('users.user_login'))

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        # add a webhook header to the org
        self.channel.org.webhook = u'{"url": "http://fake.com/webhook.php", "headers": {"X-My-Header": "foobar", "Authorization": "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, "method": "POST"}'
        self.channel.org.save()

        # check that our webhook settings have saved
        self.assertEquals('http://fake.com/webhook.php', self.channel.org.get_webhook_url())
        self.assertDictEqual({'X-My-Header': 'foobar', 'Authorization': 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}, self.channel.org.get_webhook_headers())

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, "Boom")
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            result = WebHookResult.objects.get()
            # both headers should be in the json-encoded url string
            self.assertStringContains('X-My-Header: foobar', result.request)
            self.assertStringContains('Authorization: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', result.request)
Example #54
0
    def test_event_deliveries(self):
        sms = self.create_msg(contact=self.joe, direction='I', status='H', text="I'm gonna pop some tags")

        with patch('requests.Session.send') as mock:
            now = timezone.now()
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event, shouldnn't fire as we don't have a webhook
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            self.assertFalse(WebHookEvent.objects.all())

        self.setupChannel()

        with patch('requests.Session.send') as mock:
            # clear out which events we listen for, we still shouldnt be notified though we have a webhook
            self.channel.org.webhook_events = 0
            self.channel.org.save()

            now = timezone.now()
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event, shouldnn't fire as we don't have a webhook
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            self.assertFalse(WebHookEvent.objects.all())

        self.setupChannel()

        with patch('requests.Session.send') as mock:
            # remove all the org users
            self.org.administrators.clear()
            self.org.editors.clear()
            self.org.viewers.clear()

            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('F', event.status)
            self.assertEquals(0, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("No active user", result.message)
            self.assertEquals(0, result.status_code)

            self.assertFalse(mock.called)

            # what if they send weird json back?
            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        # add ad manager back in
        self.org.administrators.add(self.admin)
        self.admin.set_org(self.org)

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Event delivered successfully", result.message)
            self.assertStringContains("not JSON", result.message)
            self.assertEquals(200, result.status_code)

            self.assertTrue(mock.called)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            # valid json, but not our format
            bad_json = '{ "thrift_shops": ["Goodwill", "Value Village"] }'
            mock.return_value = MockResponse(200, bad_json)

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            self.assertTrue(mock.called)

            result = WebHookResult.objects.get()
            self.assertStringContains("Event delivered successfully", result.message)
            self.assertStringContains("ignoring", result.message)
            self.assertEquals(200, result.status_code)
            self.assertEquals(bad_json, result.body)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, '{ "phone": "+250788123123", "text": "I am success" }')

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertEquals(200, result.status_code)

            self.assertTrue(mock.called)

            broadcast = Broadcast.objects.get()
            contact = Contact.get_or_create(self.org, self.admin, name=None, urns=[(TEL_SCHEME, "+250788123123")],
                                            incoming_channel=self.channel)
            self.assertTrue("I am success", broadcast.text)
            self.assertTrue(contact, broadcast.contacts.all())

            self.assertTrue(mock.called)
            args = mock.call_args_list[0][0]
            prepared_request = args[0]
            self.assertEquals(self.org.get_webhook_url(), prepared_request.url)

            data = parse_qs(prepared_request.body)
            self.assertEquals(self.joe.get_urn(TEL_SCHEME).path, data['phone'][0])
            self.assertEquals(sms.pk, int(data['sms'][0]))
            self.assertEquals(self.channel.pk, int(data['channel'][0]))
            self.assertEquals(SMS_RECEIVED, data['event'][0])
            self.assertEquals("I'm gonna pop some tags", data['text'][0])
            self.assertTrue('time' in data)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(500, "I am error")

            next_attempt_earliest = timezone.now() + timedelta(minutes=4)
            next_attempt_latest = timezone.now() + timedelta(minutes=6)

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('E', event.status)
            self.assertEquals(1, event.try_count)
            self.assertTrue(event.next_attempt)
            self.assertTrue(next_attempt_earliest < event.next_attempt and next_attempt_latest > event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Error", result.message)
            self.assertEquals(500, result.status_code)
            self.assertEquals("I am error", result.body)

            # make sure things become failures after three retries
            event.try_count = 2
            event.deliver()
            event.save()

            self.assertTrue(mock.called)

            self.assertEquals('F', event.status)
            self.assertEquals(3, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Error", result.message)
            self.assertEquals(500, result.status_code)
            self.assertEquals("I am error", result.body)
            self.assertEquals("http://fake.com/webhook.php", result.url)
            self.assertTrue(result.data.find("pop+some+tags") > 0)

            # check out our api log
            response = self.client.get(reverse('api.log'))
            self.assertRedirect(response, reverse('users.user_login'))

            response = self.client.get(reverse('api.log_read', args=[event.pk]))
            self.assertRedirect(response, reverse('users.user_login'))

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        # add a webhook header to the org
        self.channel.org.webhook = u'{"url": "http://fake.com/webhook.php", "headers": {"X-My-Header": "foobar", "Authorization": "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, "method": "POST"}'
        self.channel.org.save()

        # check that our webhook settings have saved
        self.assertEquals('http://fake.com/webhook.php', self.channel.org.get_webhook_url())
        self.assertDictEqual({'X-My-Header': 'foobar', 'Authorization': 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}, self.channel.org.get_webhook_headers())

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, "Boom")
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            result = WebHookResult.objects.get()
            # both headers should be in the json-encoded url string
            self.assertStringContains('X-My-Header: foobar', result.request)
            self.assertStringContains('Authorization: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', result.request)
Example #55
0
 def get_contact(self, number):
     return Contact.get_or_create(self.org, self.user, name=None, urns=[(TEL_SCHEME, number)])
Example #56
0
    def restore_object(self, attrs, instance=None):
        """
        Update our contact
        """
        if instance:  # pragma: no cover
            raise ValidationError("Invalid operation")

        org = self.user.get_org()
        if org.is_anon:
            raise ValidationError("Cannot update contacts on anonymous organizations")

        uuid = attrs.get('uuid', None)
        if uuid:
            contact = Contact.objects.get(uuid=uuid, org=org, is_active=True)

        urns = attrs.get('urns', None)
        phone = attrs.get('phone', None)

        # user didn't specify either urns or phone, stick to what already exists
        if urns is None and phone is None:
            urns = [(u.scheme, u.path) for u in contact.urns.all()]

        # user only specified phone, build our urns from it
        if phone:
            urns = [(TEL_SCHEME, attrs['phone'])]

        if uuid:
            contact.update_urns(urns)
        else:
            contact = Contact.get_or_create(self.user, org, urns=urns, uuid=uuid)

        changed = []

        # update our name and language
        if attrs.get('name', None):
            contact.name = attrs['name']
            changed.append('name')

        if 'language' in attrs:
            contact.language = attrs['language']
            changed.append('language')

        # save our contact if it changed
        if changed:
            contact.save(update_fields=changed)

        # update our fields
        fields = attrs.get('fields', None)
        if not fields is None:
            for key, value in fields.items():
                existing_by_key = ContactField.objects.filter(org=self.user.get_org(), key__iexact=key, is_active=True).first()
                if existing_by_key:
                    contact.set_field(existing_by_key.key, value)
                    continue

                # TODO as above, need to get users to stop updating via label
                existing_by_label = ContactField.objects.filter(org=self.user.get_org(), label__iexact=key, is_active=True).first()
                if existing_by_label:
                    contact.set_field(existing_by_label.key, value)

        # update our groups by UUID or name (deprecated)
        group_uuids = attrs.get('group_uuids', None)
        group_names = attrs.get('groups', None)

        if not group_uuids is None:
            contact.update_groups(group_uuids)

        elif not group_names is None:
            # by name creates groups if necessary
            groups = [ContactGroup.get_or_create(self.user.get_org(), self.user, name) for name in group_names]
            contact.update_groups(groups)

        return contact