コード例 #1
0
ファイル: run_audit.py プロジェクト: mxabierto/rapidpro
def has_unparseble_fields(run):
    if run.fields_raw is not None:
        try:
            json.loads(run.fields_raw)
        except ValueError:
            return True
    return False
コード例 #2
0
ファイル: http.py プロジェクト: mxabierto/rapidpro
    def do_POST(self):
        ctype, pdict = parse_header(self.headers["content-type"])
        if ctype == "multipart/form-data":
            data = parse_multipart(self.rfile, pdict)
        elif ctype == "application/x-www-form-urlencoded":
            length = int(self.headers["content-length"])
            data = urlparse.parse_qs(self.rfile.read(length), keep_blank_values=1)
        elif ctype == "application/json":
            length = int(self.headers["content-length"])
            data = json.loads(self.rfile.read(length))
        else:
            data = {}

        return self._handle_request("POST", data)
コード例 #3
0
ファイル: models.py プロジェクト: mxabierto/rapidpro
    def iter_records(self):
        """
        Creates an iterator for the records in this archive, streaming and decompressing on the fly
        """
        s3 = self.s3_client()
        s3_obj = s3.get_object(**self.s3_location())
        stream = gzip.GzipFile(fileobj=s3_obj["Body"])

        while True:
            line = stream.readline()
            if not line:
                break

            yield json.loads(line.decode("utf-8"))
コード例 #4
0
ファイル: tasks.py プロジェクト: teehamaral/rapidpro
def process_message_task(msg_event):
    """
    Given the task JSON from our queue, processes the message, is two implementations to deal with
    backwards compatibility of using contact queues (second branch can be removed later)
    """
    r = get_redis_connection()

    # we have a contact id, we want to get the msg from that queue after acquiring our lock
    if msg_event.get("contact_id"):
        key = "pcm_%d" % msg_event["contact_id"]
        contact_queue = Msg.CONTACT_HANDLING_QUEUE % msg_event["contact_id"]

        # wait for the lock as we want to make sure to process the next message as soon as we are free
        with r.lock(key, timeout=120):

            # pop the next message off our contact queue until we find one that needs handling
            while True:
                with r.pipeline() as pipe:
                    pipe.zrange(contact_queue, 0, 0)
                    pipe.zremrangebyrank(contact_queue, 0, 0)
                    (contact_msg, deleted) = pipe.execute()

                # no more messages in the queue for this contact, we're done
                if not contact_msg:
                    return

                # we have a message in our contact queue, look it up
                msg_event = json.loads(force_text(contact_msg[0]))
                msg = (
                    Msg.objects.filter(id=msg_event["id"])
                    .order_by()
                    .select_related("org", "contact", "contact_urn", "channel")
                    .first()
                )

                # make sure we are still pending
                if msg and msg.status == PENDING:
                    process_message(msg, msg_event.get("new_message", False), msg_event.get("new_contact", False))
                    return

    # backwards compatibility for events without contact ids, we handle the message directly
    else:
        msg = Msg.objects.filter(id=msg_event["id"]).select_related("org", "contact", "contact_urn", "channel").first()
        if msg and msg.status == PENDING:
            # grab our contact lock and handle this message
            key = "pcm_%d" % msg.contact_id
            with r.lock(key, timeout=120):
                process_message(msg, msg_event.get("new_message", False), msg_event.get("new_contact", False))
コード例 #5
0
ファイル: cache.py プロジェクト: teehamaral/rapidpro
def get_cacheable(cache_key, callable, r=None, force_dirty=False):
    """
    Gets the result of a method call, using the given key and TTL as a cache
    """
    if not r:
        r = get_redis_connection()

    if not force_dirty:
        cached = r.get(cache_key)
        if cached is not None:
            return json.loads(force_text(cached))

    (calculated, cache_ttl) = callable()
    r.set(cache_key, json.dumps(calculated), cache_ttl)

    return calculated
コード例 #6
0
ファイル: models.py プロジェクト: teehamaral/rapidpro
    def from_db_value(self, value, *args, **kwargs):
        if self.has_default() and value is None:
            return self.get_default()

        if value is None:
            return value

        if isinstance(value, str):
            data = json.loads(value)

            if type(data) not in (list, dict, OrderedDict):
                raise ValueError("JSONAsTextField should be a dict or a list, got %s => %s" % (type(data), data))
            else:
                return data
        else:
            raise ValueError('Unexpected type "%s" for JSONAsTextField' % (type(value),))  # pragma: no cover
コード例 #7
0
ファイル: views.py プロジェクト: mxabierto/rapidpro
        def post(self, request, *args, **kwargs):
            def update_boundary_aliases(boundary):
                level_boundary = AdminBoundary.objects.filter(osm_id=boundary["osm_id"]).first()
                if level_boundary:
                    boundary_aliases = boundary.get("aliases", "")
                    update_aliases(level_boundary, boundary_aliases)

            def update_aliases(boundary, new_aliases):
                # for now, nuke and recreate all aliases
                BoundaryAlias.objects.filter(boundary=boundary, org=org).delete()
                for new_alias in new_aliases.split("\n"):
                    if new_alias:
                        BoundaryAlias.objects.create(
                            boundary=boundary,
                            org=org,
                            name=new_alias.strip(),
                            created_by=self.request.user,
                            modified_by=self.request.user,
                        )

            # try to parse our body
            json_string = request.body
            org = request.user.get_org()

            try:
                json_list = json.loads(json_string)
            except Exception as e:
                return JsonResponse(dict(status="error", description="Error parsing JSON: %s" % str(e)), status=400)

            # this can definitely be optimized
            for state in json_list:
                state_boundary = AdminBoundary.objects.filter(osm_id=state["osm_id"]).first()
                state_aliases = state.get("aliases", "")
                if state_boundary:
                    update_aliases(state_boundary, state_aliases)
                    if "children" in state:
                        for district in state["children"]:
                            update_boundary_aliases(district)
                            if "children" in district:
                                for ward in district["children"]:
                                    update_boundary_aliases(ward)

            return JsonResponse(json_list, safe=False)
コード例 #8
0
ファイル: tests.py プロジェクト: mxabierto/rapidpro
    def test_schedule_ui(self):

        self.login(self.admin)

        joe = self.create_contact("Joe Blow", "123")

        # test missing recipients
        post_data = dict(text="message content", omnibox="", sender=self.channel.pk, _format="json", schedule=True)
        response = self.client.post(reverse("msgs.broadcast_send"), post_data, follow=True)
        self.assertContains(response, "At least one recipient is required")

        # missing message
        post_data = dict(text="", omnibox="c-%s" % joe.uuid, sender=self.channel.pk, _format="json", schedule=True)
        response = self.client.post(reverse("msgs.broadcast_send"), post_data, follow=True)
        self.assertContains(response, "This field is required")

        # finally create our message
        post_data = dict(
            text="A scheduled message to Joe", omnibox="c-%s" % joe.uuid, sender=self.channel.pk, schedule=True
        )
        response = json.loads(
            self.client.post(reverse("msgs.broadcast_send") + "?_format=json", post_data, follow=True).content
        )
        self.assertIn("/broadcast/schedule_read", response["redirect"])

        # fetch our formax page
        response = self.client.get(response["redirect"])
        self.assertContains(response, "id-schedule")
        broadcast = response.context["object"]

        # update our message
        post_data = dict(message="An updated scheduled message", omnibox="c-%s" % joe.uuid)
        self.client.post(reverse("msgs.broadcast_update", args=[broadcast.pk]), post_data)
        self.assertEqual(Broadcast.objects.get(id=broadcast.id).text, {"base": "An updated scheduled message"})

        # update the schedule
        post_data = dict(repeat_period="W", repeat_days=6, start="later", start_datetime_value=1)
        response = self.client.post(reverse("schedules.schedule_update", args=[broadcast.schedule.pk]), post_data)
コード例 #9
0
    def from_db_value(self, value, *args, **kwargs):
        if self.has_default() and value is None:
            return self.get_default()

        if value is None:
            return value

        if isinstance(value, str):
            data = json.loads(value)

            if type(data) not in (list, dict, OrderedDict):
                raise ValueError(
                    "JSONAsTextField should be a dict or a list, got %s => %s"
                    % (type(data), data))
            else:
                return data
        elif isinstance(
                value, (list, dict)
        ):  # if db column has been converted to JSONB, use value directly
            return value
        else:
            raise ValueError('Unexpected type "%s" for JSONAsTextField' %
                             (type(value), ))
コード例 #10
0
ファイル: queues.py プロジェクト: teehamaral/rapidpro
def start_task(task_name):
    """
    Pops the next 'random' task off our queue, returning the arguments that were saved

    Ex: start_task('start_flow')
    <<< {flow=5, contacts=[1,2,3,4,5,6,7,8,9,10]}
    """
    r = get_redis_connection("default")

    task = None
    active_set = "%s:active" % task_name

    # get what queue we will work against, always the one with the lowest number of workers
    org_queue = r.zrange(active_set, 0, 0)

    while org_queue:
        # this lua script does both a "zpop" (popping the next highest thing off our sorted set) and
        # a clearing of our active set if there is no value in it as an atomic action
        lua = (
            "local val = redis.call('zrange', ARGV[2], 0, 0) \n"
            "if not next(val) then redis.call('zrem', ARGV[1], ARGV[3]) return nil \n"
            "else redis.call('zincrby', ARGV[1], 1, ARGV[3]); redis.call('zremrangebyrank', ARGV[2], 0, 0) return val[1] end\n"
        )

        task = r.eval(
            lua, 3, "active_set", "queue", "org", active_set, "%s:%d" % (task_name, int(org_queue[0])), org_queue[0]
        )

        # found a task? then break out
        if task is not None:
            task = json.loads(force_text(task))
            break

        # if we didn't get a task, then run again against a new queue until there is nothing left in our task queue
        org_queue = r.zrange(active_set, 0, 0)

    return int(org_queue[0]) if org_queue else None, task
コード例 #11
0
ファイル: resumes.py プロジェクト: mxabierto/rapidpro
def reconstruct_session(run):
    """
    Reconstruct session JSON from the given resumable run which is assumed to be WAITING
    """

    # get all the runs that would be in the same session or part of the trigger
    session_runs, trigger_run = get_session_runs(run)
    session_root_run = session_runs[0]

    trigger = {
        "contact": serialize_contact(run.contact),
        "environment": serialize_environment(run.org),
        "flow": {"uuid": str(session_root_run.flow.uuid), "name": session_root_run.flow.name},
        "triggered_on": session_root_run.created_on.isoformat(),
        "params": session_root_run.fields,
    }

    if trigger_run:
        trigger["type"] = "flow_action"
        trigger["run"] = serialize_run_summary(trigger_run)
    else:
        trigger["type"] = "manual"

    runs = [serialize_run(r) for r in session_runs]
    runs[-1]["status"] = "waiting"

    session = {
        "contact": serialize_contact(run.contact),
        "environment": serialize_environment(run.org),
        "runs": runs,
        "status": "waiting",
        "trigger": trigger,
        "wait": {"timeout_on": run.timeout_on.isoformat() if run.timeout_on else None, "type": "msg"},
    }

    # ensure that we are a deep copy - i.e. subsequent changes to the run won't affect this snapshot of session state
    return json.loads(json.dumps(session))
コード例 #12
0
        def post(self, request, *args, **kwargs):
            def update_aliases(boundary, new_aliases):
                boundary_siblings = boundary.parent.children.all()
                # for now, nuke and recreate all aliases
                BoundaryAlias.objects.filter(boundary=boundary, org=org).delete()
                unique_new_aliases = list(set(new_aliases.split("\n")))
                for new_alias in unique_new_aliases:
                    if new_alias:
                        new_alias = new_alias.strip()

                        # aliases are only allowed to exist on one boundary with same parent at a time
                        BoundaryAlias.objects.filter(name=new_alias, boundary__in=boundary_siblings, org=org).delete()

                        BoundaryAlias.objects.create(
                            boundary=boundary,
                            org=org,
                            name=new_alias,
                            created_by=self.request.user,
                            modified_by=self.request.user,
                        )

            # try to parse our body
            json_string = request.body
            org = request.user.get_org()

            try:
                boundary_update = json.loads(json_string)
            except Exception as e:
                return JsonResponse(dict(status="error", description="Error parsing JSON: %s" % str(e)), status=400)

            boundary = AdminBoundary.objects.filter(osm_id=boundary_update["osm_id"]).first()
            aliases = boundary_update.get("aliases", "")
            if boundary:
                update_aliases(boundary, aliases)

            return JsonResponse(boundary_update, safe=False)
コード例 #13
0
ファイル: queues.py プロジェクト: elbic/rapidpro-docker
def start_task(task_name):
    """
    Pops the next 'random' task off our queue, returning the arguments that were saved

    Ex: start_task('start_flow')
    <<< {flow=5, contacts=[1,2,3,4,5,6,7,8,9,10]}
    """
    r = get_redis_connection("default")

    task = None
    active_set = "%s:active" % task_name

    # get what queue we will work against, always the one with the lowest number of workers
    org_queue = r.zrange(active_set, 0, 0)

    while org_queue:
        # this lua script does both a "zpop" (popping the next highest thing off our sorted set) and
        # a clearing of our active set if there is no value in it as an atomic action
        lua = (
            "local val = redis.call('zrange', ARGV[2], 0, 0) \n"
            "if not next(val) then redis.call('zrem', ARGV[1], ARGV[3]) return nil \n"
            "else redis.call('zincrby', ARGV[1], 1, ARGV[3]); redis.call('zremrangebyrank', ARGV[2], 0, 0) return val[1] end\n"
        )

        task = r.eval(lua, 3, "active_set", "queue", "org", active_set,
                      "%s:%d" % (task_name, int(org_queue[0])), org_queue[0])

        # found a task? then break out
        if task is not None:
            task = json.loads(force_text(task))
            break

        # if we didn't get a task, then run again against a new queue until there is nothing left in our task queue
        org_queue = r.zrange(active_set, 0, 0)

    return int(org_queue[0]) if org_queue else None, task
コード例 #14
0
ファイル: base.py プロジェクト: mxabierto/rapidpro
 def json(self):
     return json.loads(self.text)
コード例 #15
0
ファイル: tests.py プロジェクト: teamgeocart/rapidpro
    def test_admin_ui_view(self):
        admin_url = reverse("tickets.types.zendesk.admin_ui")

        ticketer = Ticketer.create(
            self.org,
            self.admin,
            ticketer_type=ZendeskType.slug,
            name="Existing",
            config={"oauth_token": "236272", "secret": "SECRET346", "subdomain": "example"},
        )

        # this view can only be POST'ed to
        response = self.client.get(admin_url)
        self.assertEqual(405, response.status_code)

        # simulate initial POST from Zendesk
        response = self.client.post(
            admin_url,
            {
                "name": "",
                "subdomain": "example",
                "metadata": "",
                "state": "",
                "return_url": "https://example.zendesk.com",
                "locale": "en-US",
                "instance_push_id": "push1234",
                "zendesk_access_token": "sesame",
            },
            HTTP_REFERER="https://example.zendesk.com/channels",
        )

        self.assertEqual(200, response.status_code)
        self.assertContains(response, "This will connect your account to Zendesk")
        self.assertNotContains(response, "This field is required.")

        self.assertEqual(
            ["name", "secret", "return_url", "subdomain", "locale", "instance_push_id", "zendesk_access_token", "loc"],
            list(response.context["form"].fields.keys()),
        )

        # try submitting with blank values for the two visible fields
        response = self.client.post(
            admin_url,
            {
                "name": "",
                "secret": "",
                "return_url": "https://example.zendesk.com",
                "subdomain": "example",
                "locale": "en-US",
                "instance_push_id": "push1234",
                "zendesk_access_token": "sesame",
            },
        )
        self.assertFormError(response, "form", "name", "This field is required.")
        self.assertFormError(response, "form", "secret", "This field is required.")

        # try submitting with incorrect secret
        response = self.client.post(
            admin_url,
            {
                "name": "My Channel",
                "secret": "CHEF",
                "return_url": "https://example.zendesk.com",
                "subdomain": "example",
                "locale": "en-US",
                "instance_push_id": "push1234",
                "zendesk_access_token": "sesame",
            },
        )
        self.assertFormError(response, "form", "secret", "Secret is incorrect.")

        # try submitting with correct secret
        response = self.client.post(
            admin_url,
            {
                "name": "My Channel",
                "secret": "SECRET346",
                "return_url": "https://example.zendesk.com",
                "subdomain": "example",
                "locale": "en-US",
                "instance_push_id": "push1234",
                "zendesk_access_token": "sesame",
            },
        )

        # ticketer config should be updated with push credentials
        ticketer.refresh_from_db()
        self.assertEqual(
            {
                "oauth_token": "236272",
                "secret": "SECRET346",
                "subdomain": "example",
                "push_id": "push1234",
                "push_token": "sesame",
            },
            ticketer.config,
        )

        # should use the special return template to POST back to Zendesk
        self.assertEqual(200, response.status_code)
        self.assertEqual("My Channel", response.context["name"])
        self.assertEqual(
            {"ticketer": str(ticketer.uuid), "secret": "SECRET346"}, json.loads(response.context["metadata"])
        )
コード例 #16
0
 def to_python(self, value):
     if isinstance(value, str):
         value = json.loads(value)
     return value
コード例 #17
0
    def post(self, request, *args, **kwargs):
        call = IVRCall.objects.filter(pk=kwargs["pk"]).first()

        if not call:
            return HttpResponse("Not found", status=404)

        channel = call.channel

        if not (channel.is_active and channel.org):
            return HttpResponse("No channel found", status=400)

        channel_type = channel.channel_type
        ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol
        client = channel.get_ivr_client()

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

        if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML and request.POST.get("hangup", 0):
            if not request.user.is_anonymous:
                user_org = request.user.get_org()
                if user_org and user_org.pk == call.org.pk:
                    client.hangup(call)
                    return HttpResponse(json.dumps(dict(status="Canceled")), content_type="application/json")
                else:  # pragma: no cover
                    return HttpResponse("Not found", status=404)

        input_redirect = "1" == request.GET.get("input_redirect", "0")
        if client.validate(request):
            status = None
            duration = None
            if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:
                status = request.POST.get("CallStatus", None)
                duration = request.POST.get("CallDuration", None)
            elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                if request_body:
                    body_json = json.loads(request_body)
                    status = body_json.get("status", None)
                    duration = body_json.get("duration", None)

                # force in progress call status for fake (input) redirects
                if input_redirect:
                    status = "answered"

            # nexmo does not set status for some callbacks
            if status is not None:
                call.update_status(status, duration, channel_type)  # update any calls we have spawned with the same
                call.save()

            resume = request.GET.get("resume", 0)
            user_response = request.POST.copy()

            hangup = False
            saved_media_url = None
            text = None
            media_url = None
            has_event = False

            if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:

                # figure out if this is a callback due to an empty gather
                is_empty = "1" == request.GET.get("empty", "0")

                # if the user pressed pound, then record no digits as the input
                if is_empty:
                    user_response["Digits"] = ""

                hangup = "hangup" == user_response.get("Digits", None)

                media_url = user_response.get("RecordingUrl", None)
                # if we've been sent a recording, go grab it
                if media_url:
                    saved_media_url = client.download_media(media_url)

                # parse the user response
                text = user_response.get("Digits", None)

            elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                if request_body:
                    body_json = json.loads(request_body)
                    media_url = body_json.get("recording_url", None)

                    if media_url:
                        cache.set("last_call:media_url:%d" % call.pk, media_url, None)

                    media_url = cache.get("last_call:media_url:%d" % call.pk, None)
                    text = body_json.get("dtmf", None)
                    if input_redirect:
                        text = None

                has_event = "1" == request.GET.get("has_event", "0")
                save_media = "1" == request.GET.get("save_media", "0")
                if media_url:
                    if save_media:
                        saved_media_url = client.download_media(call, media_url)
                        cache.delete("last_call:media_url:%d" % call.pk)
                    else:
                        response_msg = "Saved media url"
                        response = dict(message=response_msg)

                        event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response))
                        ChannelLog.log_ivr_interaction(call, response_msg, event)
                        return JsonResponse(response)

            if not has_event and call.status not in IVRCall.DONE or hangup:
                if call.is_ivr():
                    response = Flow.handle_call(
                        call, text=text, saved_media_url=saved_media_url, hangup=hangup, resume=resume
                    )
                    event = HttpEvent(request_method, request_path, request_body, 200, str(response))
                    if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                        ChannelLog.log_ivr_interaction(call, "Incoming request for call", event)

                        # TODO: what's special here that this needs to be different?
                        return JsonResponse(json.loads(str(response)), safe=False)

                    ChannelLog.log_ivr_interaction(call, "Incoming request for call", event)
                    return HttpResponse(str(response), content_type="text/xml; charset=utf-8")
            else:

                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)

                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)
                return JsonResponse(response)

        else:  # pragma: no cover

            error = "Invalid request signature"
            event = HttpEvent(request_method, request_path, request_body, 200, error)
            ChannelLog.log_ivr_interaction(call, error, event, is_error=True)
            # raise an exception that things weren't properly signed
            raise ValidationError(error)

        return JsonResponse(dict(message="Unhandled"))  # pragma: no cover
コード例 #18
0
ファイル: models.py プロジェクト: tybritten/rapidpro
    def import_campaigns(cls,
                         org,
                         user,
                         campaign_defs,
                         same_site=False) -> List:
        """
        Import campaigns from a list of exported campaigns
        """

        imported = []

        for campaign_def in campaign_defs:
            name = campaign_def[Campaign.EXPORT_NAME]
            campaign = None
            group = None

            # first check if we have the objects by UUID
            if same_site:
                group = ContactGroup.user_groups.filter(
                    uuid=campaign_def[Campaign.EXPORT_GROUP]["uuid"],
                    org=org).first()
                if group:  # pragma: needs cover
                    group.name = campaign_def[Campaign.EXPORT_GROUP]["name"]
                    group.save()

                campaign = Campaign.objects.filter(
                    org=org, uuid=campaign_def[Campaign.EXPORT_UUID]).first()
                if campaign:  # pragma: needs cover
                    campaign.name = Campaign.get_unique_name(org,
                                                             name,
                                                             ignore=campaign)
                    campaign.save()

            # fall back to lookups by name
            if not group:
                group = ContactGroup.get_user_group_by_name(
                    org, campaign_def[Campaign.EXPORT_GROUP]["name"])

            if not campaign:
                campaign = Campaign.objects.filter(org=org, name=name).first()

            # all else fails, create the objects from scratch
            if not group:
                group = ContactGroup.create_static(
                    org, user, campaign_def[Campaign.EXPORT_GROUP]["name"])

            if not campaign:
                campaign_name = Campaign.get_unique_name(org, name)
                campaign = Campaign.create(org, user, campaign_name, group)
            else:
                campaign.group = group
                campaign.save()

            # deactivate all of our events, we'll recreate these
            for event in campaign.events.all():
                event.release()

            # fill our campaign with events
            for event_spec in campaign_def[Campaign.EXPORT_EVENTS]:
                field_key = event_spec["relative_to"]["key"]

                if field_key == "created_on":
                    relative_to = ContactField.system_fields.filter(
                        org=org, key=field_key).first()
                else:
                    relative_to = ContactField.get_or_create(
                        org,
                        user,
                        key=field_key,
                        label=event_spec["relative_to"]["label"],
                        value_type="D")

                start_mode = event_spec.get("start_mode",
                                            CampaignEvent.MODE_INTERRUPT)

                # create our message flow for message events
                if event_spec["event_type"] == CampaignEvent.TYPE_MESSAGE:

                    message = event_spec["message"]
                    base_language = event_spec.get("base_language")

                    if not isinstance(message, dict):
                        try:
                            message = json.loads(message)
                        except ValueError:
                            # if it's not a language dict, turn it into one
                            message = dict(base=message)
                            base_language = "base"

                    event = CampaignEvent.create_message_event(
                        org,
                        user,
                        campaign,
                        relative_to,
                        event_spec["offset"],
                        event_spec["unit"],
                        message,
                        event_spec["delivery_hour"],
                        base_language=base_language,
                        start_mode=start_mode,
                    )
                    event.update_flow_name()
                else:
                    flow = Flow.objects.filter(
                        org=org,
                        is_active=True,
                        is_system=False,
                        uuid=event_spec["flow"]["uuid"]).first()
                    if flow:
                        CampaignEvent.create_flow_event(
                            org,
                            user,
                            campaign,
                            relative_to,
                            event_spec["offset"],
                            event_spec["unit"],
                            flow,
                            event_spec["delivery_hour"],
                            start_mode=start_mode,
                        )

            imported.append(campaign)

        return imported
コード例 #19
0
def call_webhook(run,
                 webhook_url,
                 ruleset,
                 msg,
                 action="POST",
                 resthook=None,
                 headers=None):
    from temba.api.models import WebHookEvent, WebHookResult
    from temba.flows.models import Flow

    flow = run.flow
    contact = run.contact
    org = flow.org
    channel = msg.channel if msg else None
    contact_urn = msg.contact_urn if (
        msg and msg.contact_urn) else contact.get_urn()

    contact_dict = dict(uuid=contact.uuid, name=contact.name)
    if contact_urn:
        contact_dict["urn"] = contact_urn.urn

    post_data = {
        "contact":
        contact_dict,
        "flow":
        dict(name=flow.name,
             uuid=flow.uuid,
             revision=flow.revisions.order_by("revision").last().revision),
        "path":
        run.path,
        "results":
        run.results,
        "run":
        dict(uuid=str(run.uuid), created_on=run.created_on.isoformat()),
    }

    if msg and msg.id > 0:
        post_data["input"] = dict(
            urn=msg.contact_urn.urn if msg.contact_urn else None,
            text=msg.text,
            attachments=(msg.attachments or []))

    if channel:
        post_data["channel"] = dict(name=channel.name, uuid=channel.uuid)

    if not action:  # pragma: needs cover
        action = "POST"

    if resthook:
        WebHookEvent.objects.create(org=org,
                                    data=post_data,
                                    action=action,
                                    resthook=resthook)

    status_code = -1
    message = "None"
    body = None
    request = ""

    start = time.time()

    # webhook events fire immediately since we need the results back
    try:
        # no url, bail!
        if not webhook_url:
            raise ValueError("No webhook_url specified, skipping send")

        # only send webhooks when we are configured to, otherwise fail
        if settings.SEND_WEBHOOKS:
            requests_headers = http_headers(extra=headers)

            s = requests.Session()

            # some hosts deny generic user agents, use Temba as our user agent
            if action == "GET":
                prepped = requests.Request("GET",
                                           webhook_url,
                                           headers=requests_headers).prepare()
            else:
                requests_headers["Content-type"] = "application/json"
                prepped = requests.Request("POST",
                                           webhook_url,
                                           data=json.dumps(post_data),
                                           headers=requests_headers).prepare()

            request = prepped_request_to_str(prepped)
            response = s.send(prepped, timeout=10)
            body = response.text
            if body:
                body = body.strip()
            status_code = response.status_code

        else:
            print("!! Skipping WebHook send, SEND_WEBHOOKS set to False")
            body = "Skipped actual send"
            status_code = 200

        if ruleset:
            run.update_fields({Flow.label_to_slug(ruleset.label): body},
                              do_save=False)
        new_extra = {}

        # process the webhook response
        try:
            response_json = json.loads(body)

            # only update if we got a valid JSON dictionary or list
            if not isinstance(response_json, dict) and not isinstance(
                    response_json, list):
                raise ValueError(
                    "Response must be a JSON dictionary or list, ignoring response."
                )

            new_extra = response_json
            message = "Webhook called successfully."
        except ValueError:
            message = "Response must be a JSON dictionary, ignoring response."

        run.update_fields(new_extra)

        if not (200 <= status_code < 300):
            message = "Got non 200 response (%d) from webhook." % response.status_code
            raise ValueError("Got non 200 response (%d) from webhook." %
                             response.status_code)

    except (requests.ReadTimeout, ValueError) as e:
        message = f"Error calling webhook: {str(e)}"

    except Exception as e:
        logger.error(f"Could not trigger flow webhook: {str(e)}",
                     exc_info=True)

        message = "Error calling webhook: %s" % str(e)

    finally:
        # make sure our message isn't too long
        if message:
            message = message[:255]

        if body is None:
            body = message

        request_time = (time.time() - start) * 1000

        contact = None
        if run:
            contact = run.contact

        result = WebHookResult.objects.create(
            contact=contact,
            url=webhook_url,
            status_code=status_code,
            response=body,
            request=request,
            request_time=request_time,
            org=run.org,
        )

    return result
コード例 #20
0
ファイル: handlers.py プロジェクト: elbic/rapidpro-docker
    def post(self, request, *args, **kwargs):
        from temba.msgs.models import Msg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                if connection:
                    status = 200
                    response_body = {
                        "status": self.ACK,
                        "session_id": connection.pk
                    }
                    event = HttpEvent(request_method, request_path,
                                      request_body, status,
                                      json.dumps(response_body))
                    log_channel(
                        channel, "Handled USSD message of %s session_event" %
                        (channel_data["session_event"], ), event)
                    return JsonResponse(response_body, status=status)
                else:
                    status = 400
                    response_body = {
                        "status": self.NACK,
                        "reason": "No suitable session found for this message."
                    }
                    event = HttpEvent(request_method, request_path,
                                      request_body, status,
                                      json.dumps(response_body))
                    log_channel(
                        channel,
                        "Failed to handle USSD message of %s session_event" %
                        (channel_data["session_event"], ),
                        event,
                    )
                    return JsonResponse(response_body, status=status)
            else:
                content = data["content"]
                message = Msg.create_incoming(channel,
                                              URN.from_tel(data["from"]),
                                              content)
                status = 200
                response_body = {"status": self.ACK, "message_id": message.pk}
                Msg.objects.filter(pk=message.id).update(
                    external_id=data["message_id"])
                event = HttpEvent(request_method, request_path, request_body,
                                  status, json.dumps(response_body))
                ChannelLog.log_message(message, "Handled inbound message.",
                                       event)
                return JsonResponse(response_body, status=status)
コード例 #21
0
ファイル: base.py プロジェクト: azizur77/rapidpro
 def get_flow_json(self, filename, substitutions=None):
     data = self.get_import_json(filename, substitutions=substitutions)
     return json.loads(data)["flows"][0]
コード例 #22
0
def migrate_export_to_version_9(exported_json, org, same_site=True):
    """
    Migrates remaining ids to uuids. Changes to uuids for Flows, Groups,
    Contacts and Channels inside of Actions, Triggers, Campaigns, Events
    """
    def replace(str, match, replace):
        rexp = regex.compile(match,
                             flags=regex.MULTILINE | regex.UNICODE | regex.V0)

        # replace until no matches found
        matches = 1
        while matches:
            (str, matches) = rexp.subn(replace, str)

        return str

    exported_string = json.dumps(exported_json)

    # any references to @extra.flow are now just @parent
    exported_string = replace(exported_string, r"@(extra\.flow)", "@parent")
    exported_string = replace(exported_string, r"(@\(.*?)extra\.flow(.*?\))",
                              r"\1parent\2")

    # any references to @extra.contact are now @parent.contact
    exported_string = replace(exported_string, r"@(extra\.contact)",
                              "@parent.contact")
    exported_string = replace(exported_string,
                              r"(@\(.*?)extra\.contact(.*?\))",
                              r"\1parent.contact\2")

    exported_json = json.loads(exported_string)

    flow_id_map = {}
    group_id_map = {}
    contact_id_map = {}
    campaign_id_map = {}
    campaign_event_id_map = {}
    label_id_map = {}

    def get_uuid(id_map, obj_id):
        uuid = id_map.get(obj_id, None)
        if not uuid:
            uuid = str(uuid4())
            id_map[obj_id] = uuid
        return uuid

    def replace_with_uuid(ele,
                          manager,
                          id_map,
                          nested_name=None,
                          obj=None,
                          create_dict=False):
        # deal with case of having only a string and no name
        if isinstance(ele, str) and create_dict:
            # variable references should just stay put
            if len(ele) > 0 and ele[0] == "@":
                return ele
            else:
                ele = dict(name=ele)

        obj_id = ele.pop("id", None)
        obj_name = ele.pop("name", None)

        if same_site and not obj and obj_id:
            try:
                obj = manager.filter(pk=obj_id, org=org).first()
            except Exception:
                pass

        # nest it if we were given a nested name
        if nested_name:
            ele[nested_name] = dict()
            ele = ele[nested_name]

        if obj:
            ele["uuid"] = obj.uuid

            if obj.name:
                ele["name"] = obj.name
        else:
            if obj_id:
                ele["uuid"] = get_uuid(id_map, obj_id)

            if obj_name:
                ele["name"] = obj_name

        return ele

    def remap_flow(ele, nested_name=None):
        from temba.flows.models import Flow

        replace_with_uuid(ele, Flow.objects, flow_id_map, nested_name)

    def remap_group(ele):
        from temba.contacts.models import ContactGroup

        return replace_with_uuid(ele,
                                 ContactGroup.user_groups,
                                 group_id_map,
                                 create_dict=True)

    def remap_campaign(ele):
        from temba.campaigns.models import Campaign

        replace_with_uuid(ele, Campaign.objects, campaign_id_map)

    def remap_campaign_event(ele):
        from temba.campaigns.models import CampaignEvent

        event = None
        if same_site:
            event = CampaignEvent.objects.filter(pk=ele["id"],
                                                 campaign__org=org).first()
        replace_with_uuid(ele,
                          CampaignEvent.objects,
                          campaign_event_id_map,
                          obj=event)

    def remap_contact(ele):
        from temba.contacts.models import Contact

        replace_with_uuid(ele, Contact.objects, contact_id_map)

    def remap_channel(ele):
        from temba.channels.models import Channel

        channel_id = ele.get("channel")
        if channel_id:  # pragma: needs cover
            channel = Channel.objects.filter(pk=channel_id).first()
            if channel:
                ele["channel"] = channel.uuid

    def remap_label(ele):
        from temba.msgs.models import Label

        replace_with_uuid(ele, Label.label_objects, label_id_map)

    for flow in exported_json.get("flows", []):
        flow = map_actions(flow, cleanse_group_names)

        for action_set in flow["action_sets"]:
            for action in action_set["actions"]:
                if action["type"] in ("add_group", "del_group", "send",
                                      "trigger-flow"):
                    groups = []
                    for group_json in action.get("groups", []):
                        groups.append(remap_group(group_json))
                    for contact_json in action.get("contacts", []):
                        remap_contact(contact_json)
                    if groups:
                        action["groups"] = groups
                if action["type"] in ("trigger-flow", "flow"):
                    remap_flow(action, "flow")
                if action["type"] == "add_label":
                    for label in action.get("labels", []):
                        remap_label(label)

        metadata = flow["metadata"]
        if "id" in metadata:
            if metadata.get("id", None):
                remap_flow(metadata)
            else:
                del metadata["id"]  # pragma: no cover

    for trigger in exported_json.get("triggers", []):
        if "flow" in trigger:
            remap_flow(trigger["flow"])
        for group in trigger["groups"]:
            remap_group(group)
        remap_channel(trigger)

    for campaign in exported_json.get("campaigns", []):
        remap_campaign(campaign)
        remap_group(campaign["group"])
        for event in campaign.get("events", []):
            remap_campaign_event(event)
            if "id" in event["relative_to"]:
                del event["relative_to"]["id"]
            if "flow" in event:
                remap_flow(event["flow"])
    return exported_json
コード例 #23
0
ファイル: serializers.py プロジェクト: teehamaral/rapidpro
 def get_geometry(self, obj):
     return json.loads(obj.simplified_geometry.geojson) if obj.simplified_geometry else None
コード例 #24
0
ファイル: models.py プロジェクト: elbic/rapidpro-docker
    def import_campaigns(cls, exported_json, org, user, same_site=False):
        """
        Import campaigns from our export file
        """
        from temba.orgs.models import EARLIEST_IMPORT_VERSION

        if Flow.is_before_version(
                exported_json.get("version", "0"),
                EARLIEST_IMPORT_VERSION):  # pragma: needs cover
            raise ValueError(
                _("Unknown version (%s)" % exported_json.get("version", 0)))

        if "campaigns" in exported_json:
            for campaign_spec in exported_json["campaigns"]:
                name = campaign_spec["name"]
                campaign = None
                group = None

                # first check if we have the objects by id
                if same_site:
                    group = ContactGroup.user_groups.filter(
                        uuid=campaign_spec["group"]["uuid"], org=org).first()
                    if group:  # pragma: needs cover
                        group.name = campaign_spec["group"]["name"]
                        group.save()

                    campaign = Campaign.objects.filter(
                        org=org, uuid=campaign_spec["uuid"]).first()
                    if campaign:  # pragma: needs cover
                        campaign.name = Campaign.get_unique_name(
                            org, name, ignore=campaign)
                        campaign.save()

                # fall back to lookups by name
                if not group:
                    group = ContactGroup.get_user_group(
                        org, campaign_spec["group"]["name"])

                if not campaign:
                    campaign = Campaign.objects.filter(org=org,
                                                       name=name).first()

                # all else fails, create the objects from scratch
                if not group:
                    group = ContactGroup.create_static(
                        org, user, campaign_spec["group"]["name"])

                if not campaign:
                    campaign_name = Campaign.get_unique_name(org, name)
                    campaign = Campaign.create(org, user, campaign_name, group)
                else:
                    campaign.group = group
                    campaign.save()

                # deactivate all of our events, we'll recreate these
                for event in campaign.events.all():
                    event.release()

                # fill our campaign with events
                for event_spec in campaign_spec["events"]:
                    field_key = event_spec["relative_to"]["key"]

                    if field_key == "created_on":
                        relative_to = ContactField.system_fields.filter(
                            org=org, key=field_key).first()
                    else:
                        relative_to = ContactField.get_or_create(
                            org,
                            user,
                            key=field_key,
                            label=event_spec["relative_to"]["label"],
                            value_type="D")

                    start_mode = event_spec.get("start_mode",
                                                CampaignEvent.MODE_INTERRUPT)

                    # create our message flow for message events
                    if event_spec["event_type"] == CampaignEvent.TYPE_MESSAGE:

                        message = event_spec["message"]
                        base_language = event_spec.get("base_language")

                        if not isinstance(message, dict):
                            try:
                                message = json.loads(message)
                            except ValueError:
                                # if it's not a language dict, turn it into one
                                message = dict(base=message)
                                base_language = "base"

                        event = CampaignEvent.create_message_event(
                            org,
                            user,
                            campaign,
                            relative_to,
                            event_spec["offset"],
                            event_spec["unit"],
                            message,
                            event_spec["delivery_hour"],
                            base_language=base_language,
                            start_mode=start_mode,
                        )
                        event.update_flow_name()
                    else:
                        flow = Flow.objects.filter(
                            org=org,
                            is_active=True,
                            is_system=False,
                            uuid=event_spec["flow"]["uuid"]).first()
                        if flow:
                            CampaignEvent.create_flow_event(
                                org,
                                user,
                                campaign,
                                relative_to,
                                event_spec["offset"],
                                event_spec["unit"],
                                flow,
                                event_spec["delivery_hour"],
                                start_mode=start_mode,
                            )

                # update our scheduled events for this campaign
                EventFire.update_campaign_events(campaign)
コード例 #25
0
ファイル: views.py プロジェクト: mxabierto/rapidpro
    def post(self, request, *args, **kwargs):
        call = IVRCall.objects.filter(pk=kwargs["pk"]).first()

        if not call:
            return HttpResponse("Not found", status=404)

        channel = call.channel

        if not (channel.is_active and channel.org):
            return HttpResponse("No channel found", status=400)

        channel_type = channel.channel_type
        ivr_protocol = Channel.get_type_from_code(channel_type).ivr_protocol
        client = channel.get_ivr_client()

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

        if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML and request.POST.get("hangup", 0):
            if not request.user.is_anonymous:
                user_org = request.user.get_org()
                if user_org and user_org.pk == call.org.pk:
                    client.hangup(call)
                    return HttpResponse(json.dumps(dict(status="Canceled")), content_type="application/json")
                else:  # pragma: no cover
                    return HttpResponse("Not found", status=404)

        input_redirect = "1" == request.GET.get("input_redirect", "0")
        if client.validate(request):
            status = None
            duration = None
            if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:
                status = request.POST.get("CallStatus", None)
                duration = request.POST.get("CallDuration", None)
            elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                if request_body:
                    body_json = json.loads(request_body)
                    status = body_json.get("status", None)
                    duration = body_json.get("duration", None)

                # force in progress call status for fake (input) redirects
                if input_redirect:
                    status = "answered"

            # nexmo does not set status for some callbacks
            if status is not None:
                call.update_status(status, duration, channel_type)  # update any calls we have spawned with the same
                call.save()

            resume = request.GET.get("resume", 0)
            user_response = request.POST.copy()

            hangup = False
            saved_media_url = None
            text = None
            media_url = None
            has_event = False

            if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_TWIML:

                # figure out if this is a callback due to an empty gather
                is_empty = "1" == request.GET.get("empty", "0")

                # if the user pressed pound, then record no digits as the input
                if is_empty:
                    user_response["Digits"] = ""

                hangup = "hangup" == user_response.get("Digits", None)

                media_url = user_response.get("RecordingUrl", None)
                # if we've been sent a recording, go grab it
                if media_url:
                    saved_media_url = client.download_media(media_url)

                # parse the user response
                text = user_response.get("Digits", None)

            elif ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                if request_body:
                    body_json = json.loads(request_body)
                    media_url = body_json.get("recording_url", None)

                    if media_url:
                        cache.set("last_call:media_url:%d" % call.pk, media_url, None)

                    media_url = cache.get("last_call:media_url:%d" % call.pk, None)
                    text = body_json.get("dtmf", None)
                    if input_redirect:
                        text = None

                has_event = "1" == request.GET.get("has_event", "0")
                save_media = "1" == request.GET.get("save_media", "0")
                if media_url:
                    if save_media:
                        saved_media_url = client.download_media(call, media_url)
                        cache.delete("last_call:media_url:%d" % call.pk)
                    else:
                        response_msg = "Saved media url"
                        response = dict(message=response_msg)

                        event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response))
                        ChannelLog.log_ivr_interaction(call, response_msg, event)
                        return JsonResponse(response)

            if not has_event and call.status not in IVRCall.DONE or hangup:
                if call.is_ivr():
                    response = Flow.handle_call(
                        call, text=text, saved_media_url=saved_media_url, hangup=hangup, resume=resume
                    )
                    event = HttpEvent(request_method, request_path, request_body, 200, str(response))
                    if ivr_protocol == ChannelType.IVRProtocol.IVR_PROTOCOL_NCCO:
                        ChannelLog.log_ivr_interaction(call, "Incoming request for call", event)

                        # TODO: what's special here that this needs to be different?
                        return JsonResponse(json.loads(str(response)), safe=False)

                    ChannelLog.log_ivr_interaction(call, "Incoming request for call", event)
                    return HttpResponse(str(response), content_type="text/xml; charset=utf-8")
            else:

                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)

                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)
                return JsonResponse(response)

        else:  # pragma: no cover

            error = "Invalid request signature"
            event = HttpEvent(request_method, request_path, request_body, 200, error)
            ChannelLog.log_ivr_interaction(call, error, event, is_error=True)
            # raise an exception that things weren't properly signed
            raise ValidationError(error)

        return JsonResponse(dict(message="Unhandled"))  # pragma: no cover
コード例 #26
0
ファイル: serializers.py プロジェクト: teehamaral/rapidpro
 def get_geometry(self, obj):
     if self.context["include_geometry"] and obj.simplified_geometry:
         return json.loads(obj.simplified_geometry.geojson)
     else:
         return None
コード例 #27
0
def backfill_flow_deps(Flow, Channel, Label):
    FlowChannelDeps = Flow.channel_dependencies.through
    FlowLabelDeps = Flow.label_dependencies.through

    flows_qs = Flow.objects.filter(is_active=True)

    total_count = flows_qs.count()
    print(f"Found {total_count} flows with missing channel and label deps...")

    num_updated = 0
    total_added_channels = 0
    total_added_labels = 0

    invalid_flow_ids = set()

    with transaction.atomic():
        for flow_id, org_id, revision, flow_definition in latest_flow_revisions(
        ):
            # validate flow
            try:
                validated = mailroom.get_client().flow_validate(
                    None, json.loads(flow_definition))
            except MailroomException:
                invalid_flow_ids.add(flow_id)
                continue  # skip error

            dependencies = validated["_dependencies"]

            channel_uuids = [
                g["uuid"] for g in dependencies.get("channels", [])
            ]
            label_uuids = [g["uuid"] for g in dependencies.get("labels", [])]

            channel_ids = Channel.objects.filter(org_id=org_id,
                                                 uuid__in=channel_uuids,
                                                 is_active=True).values_list(
                                                     "id", flat=True)
            label_ids = Label.all_objects.filter(label_type="L",
                                                 org_id=org_id,
                                                 uuid__in=label_uuids,
                                                 is_active=True).values_list(
                                                     "id", flat=True)

            # channels
            FlowChannelDeps.objects.filter(flow_id=flow_id).delete()

            bulk_chan_deps_to_add = [
                FlowChannelDeps(flow_id=flow_id, channel_id=chan_id)
                for chan_id in channel_ids
            ]
            FlowChannelDeps.objects.bulk_create(bulk_chan_deps_to_add)
            total_added_channels += len(bulk_chan_deps_to_add)

            # labels
            FlowLabelDeps.objects.filter(flow_id=flow_id).delete()

            bulk_label_deps_to_add = [
                FlowLabelDeps(flow_id=flow_id, label_id=label_id)
                for label_id in label_ids
            ]
            FlowLabelDeps.objects.bulk_create(bulk_label_deps_to_add)
            total_added_labels += len(bulk_label_deps_to_add)

            num_updated += 1

            if num_updated % 1000 == 0:
                print(
                    f" > Updated {num_updated} of {total_count} flows, invalid flows {len(invalid_flow_ids)}, c:{total_added_channels} l:{total_added_labels}"
                )

        if num_updated:
            print(
                f" > Updated {num_updated} of {total_count} flows, invalid flows {len(invalid_flow_ids)}, c:{total_added_channels} l:{total_added_labels}"
            )
コード例 #28
0
ファイル: base.py プロジェクト: mxabierto/rapidpro
 def import_file(self, filename, site="http://rapidpro.io", substitutions=None):
     data = self.get_import_json(filename, substitutions=substitutions)
     self.org.import_app(json.loads(data), self.admin, site=site)
コード例 #29
0
ファイル: handlers.py プロジェクト: elbic/rapidpro-docker
    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)
コード例 #30
0
ファイル: models.py プロジェクト: Ilhasoft/rapidpro
 def generator():
     for line in in_stream:
         record = json.loads(line.decode("utf-8"))
         yield record
コード例 #31
0
ファイル: base.py プロジェクト: azizur77/rapidpro
 def import_file(self,
                 filename,
                 site="http://rapidpro.io",
                 substitutions=None):
     data = self.get_import_json(filename, substitutions=substitutions)
     self.org.import_app(json.loads(data), self.admin, site=site)
コード例 #32
0
ファイル: models.py プロジェクト: teehamaral/rapidpro
    def import_campaigns(cls, exported_json, org, user, same_site=False):
        """
        Import campaigns from our export file
        """
        from temba.orgs.models import EARLIEST_IMPORT_VERSION

        if Flow.is_before_version(exported_json.get("version", "0"), EARLIEST_IMPORT_VERSION):  # pragma: needs cover
            raise ValueError(_("Unknown version (%s)" % exported_json.get("version", 0)))

        if "campaigns" in exported_json:
            for campaign_spec in exported_json["campaigns"]:
                name = campaign_spec["name"]
                campaign = None
                group = None

                # first check if we have the objects by id
                if same_site:
                    group = ContactGroup.user_groups.filter(uuid=campaign_spec["group"]["uuid"], org=org).first()
                    if group:  # pragma: needs cover
                        group.name = campaign_spec["group"]["name"]
                        group.save()

                    campaign = Campaign.objects.filter(org=org, uuid=campaign_spec["uuid"]).first()
                    if campaign:  # pragma: needs cover
                        campaign.name = Campaign.get_unique_name(org, name, ignore=campaign)
                        campaign.save()

                # fall back to lookups by name
                if not group:
                    group = ContactGroup.get_user_group(org, campaign_spec["group"]["name"])

                if not campaign:
                    campaign = Campaign.objects.filter(org=org, name=name).first()

                # all else fails, create the objects from scratch
                if not group:
                    group = ContactGroup.create_static(org, user, campaign_spec["group"]["name"])

                if not campaign:
                    campaign_name = Campaign.get_unique_name(org, name)
                    campaign = Campaign.create(org, user, campaign_name, group)
                else:
                    campaign.group = group
                    campaign.save()

                # deactivate all of our events, we'll recreate these
                for event in campaign.events.all():
                    event.release()

                # fill our campaign with events
                for event_spec in campaign_spec["events"]:
                    field_key = event_spec["relative_to"]["key"]

                    if field_key == "created_on":
                        relative_to = ContactField.system_fields.filter(org=org, key=field_key).first()
                    else:
                        relative_to = ContactField.get_or_create(
                            org, user, key=field_key, label=event_spec["relative_to"]["label"], value_type="D"
                        )

                    start_mode = event_spec.get("start_mode", CampaignEvent.MODE_INTERRUPT)

                    # create our message flow for message events
                    if event_spec["event_type"] == CampaignEvent.TYPE_MESSAGE:

                        message = event_spec["message"]
                        base_language = event_spec.get("base_language")

                        if not isinstance(message, dict):
                            try:
                                message = json.loads(message)
                            except ValueError:
                                # if it's not a language dict, turn it into one
                                message = dict(base=message)
                                base_language = "base"

                        event = CampaignEvent.create_message_event(
                            org,
                            user,
                            campaign,
                            relative_to,
                            event_spec["offset"],
                            event_spec["unit"],
                            message,
                            event_spec["delivery_hour"],
                            base_language=base_language,
                            start_mode=start_mode,
                        )
                        event.update_flow_name()
                    else:
                        flow = Flow.objects.filter(
                            org=org, is_active=True, is_system=False, uuid=event_spec["flow"]["uuid"]
                        ).first()
                        if flow:
                            CampaignEvent.create_flow_event(
                                org,
                                user,
                                campaign,
                                relative_to,
                                event_spec["offset"],
                                event_spec["unit"],
                                flow,
                                event_spec["delivery_hour"],
                                start_mode=start_mode,
                            )

                # update our scheduled events for this campaign
                EventFire.update_campaign_events(campaign)
コード例 #33
0
ファイル: tests.py プロジェクト: UNICEFIndia/RapidPro
    def test_schedule_ui(self):
        self.login(self.admin)

        joe = self.create_contact("Joe Blow", "123")

        # test missing recipients
        post_data = dict(text="message content",
                         omnibox="",
                         sender=self.channel.pk,
                         _format="json",
                         schedule=True)
        response = self.client.post(reverse("msgs.broadcast_send"),
                                    post_data,
                                    follow=True)
        self.assertContains(response, "At least one recipient is required")

        # missing message
        post_data = dict(text="",
                         omnibox="c-%s" % joe.uuid,
                         sender=self.channel.pk,
                         _format="json",
                         schedule=True)
        response = self.client.post(reverse("msgs.broadcast_send"),
                                    post_data,
                                    follow=True)
        self.assertContains(response, "This field is required")

        # finally create our message
        post_data = dict(text="A scheduled message to Joe",
                         omnibox="c-%s" % joe.uuid,
                         sender=self.channel.pk,
                         schedule=True)
        response = json.loads(
            self.client.post(reverse("msgs.broadcast_send") + "?_format=json",
                             post_data,
                             follow=True).content)
        self.assertIn("/broadcast/schedule_read", response["redirect"])

        # should have a schedule with no next fire
        bcast = Broadcast.objects.get()
        schedule = bcast.schedule

        self.assertIsNone(schedule.next_fire)
        self.assertEqual(Schedule.REPEAT_NEVER, schedule.repeat_period)

        # fetch our formax page
        response = self.client.get(response["redirect"])
        self.assertContains(response, "id-schedule")
        broadcast = response.context["object"]

        # update our message
        post_data = dict(message="An updated scheduled message",
                         omnibox="c-%s" % joe.uuid)
        self.client.post(reverse("msgs.broadcast_update", args=[broadcast.pk]),
                         post_data)
        self.assertEqual(
            Broadcast.objects.get(id=broadcast.id).text,
            {"base": "An updated scheduled message"})

        start = datetime(2045,
                         9,
                         19,
                         hour=10,
                         minute=15,
                         second=0,
                         microsecond=0)
        start = pytz.utc.normalize(self.org.timezone.localize(start))
        start_stamp = time.mktime(start.timetuple())

        # update the schedule
        post_data = dict(
            repeat_period=Schedule.REPEAT_WEEKLY,
            repeat_days_of_week="W",
            start="later",
            start_datetime_value=start_stamp,
        )
        response = self.client.post(
            reverse("schedules.schedule_update", args=[broadcast.schedule.pk]),
            post_data)

        # assert out next fire was updated properly
        schedule.refresh_from_db()
        self.assertEqual(Schedule.REPEAT_WEEKLY, schedule.repeat_period)
        self.assertEqual("W", schedule.repeat_days_of_week)
        self.assertEqual(10, schedule.repeat_hour_of_day)
        self.assertEqual(15, schedule.repeat_minute_of_hour)
        self.assertEqual(start, schedule.next_fire)

        # manually set our fire in the past
        schedule.next_fire = timezone.now() - timedelta(days=1)
        schedule.save(update_fields=["next_fire"])

        self.assertIsNotNone(str(schedule))
コード例 #34
0
ファイル: handlers.py プロジェクト: teehamaral/rapidpro
    def post(self, request, *args, **kwargs):
        from temba.msgs.models import Msg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                if connection:
                    status = 200
                    response_body = {"status": self.ACK, "session_id": connection.pk}
                    event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                    log_channel(
                        channel, "Handled USSD message of %s session_event" % (channel_data["session_event"],), event
                    )
                    return JsonResponse(response_body, status=status)
                else:
                    status = 400
                    response_body = {"status": self.NACK, "reason": "No suitable session found for this message."}
                    event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                    log_channel(
                        channel,
                        "Failed to handle USSD message of %s session_event" % (channel_data["session_event"],),
                        event,
                    )
                    return JsonResponse(response_body, status=status)
            else:
                content = data["content"]
                message = Msg.create_incoming(channel, URN.from_tel(data["from"]), content)
                status = 200
                response_body = {"status": self.ACK, "message_id": message.pk}
                Msg.objects.filter(pk=message.id).update(external_id=data["message_id"])
                event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body))
                ChannelLog.log_message(message, "Handled inbound message.", event)
                return JsonResponse(response_body, status=status)
コード例 #35
0
ファイル: models.py プロジェクト: teehamaral/rapidpro
    def trigger_flow_webhook(cls, run, webhook_url, ruleset, msg, action="POST", resthook=None, headers=None):

        flow = run.flow
        contact = run.contact
        org = flow.org
        channel = msg.channel if msg else None
        contact_urn = msg.contact_urn if (msg and msg.contact_urn) else contact.get_urn()

        contact_dict = dict(uuid=contact.uuid, name=contact.name)
        if contact_urn:
            contact_dict["urn"] = contact_urn.urn

        post_data = {
            "contact": contact_dict,
            "flow": dict(name=flow.name, uuid=flow.uuid, revision=flow.revisions.order_by("revision").last().revision),
            "path": run.path,
            "results": run.results,
            "run": dict(uuid=str(run.uuid), created_on=run.created_on.isoformat()),
        }

        if msg and msg.id > 0:
            post_data["input"] = dict(
                urn=msg.contact_urn.urn if msg.contact_urn else None,
                text=msg.text,
                attachments=(msg.attachments or []),
            )

        if channel:
            post_data["channel"] = dict(name=channel.name, uuid=channel.uuid)

        api_user = get_api_user()
        if not action:  # pragma: needs cover
            action = "POST"

        webhook_event = cls.objects.create(
            org=org,
            event=cls.TYPE_FLOW,
            channel=channel,
            data=post_data,
            run=run,
            try_count=1,
            action=action,
            resthook=resthook,
            created_by=api_user,
            modified_by=api_user,
        )

        status_code = -1
        message = "None"
        body = None

        start = time.time()

        # webhook events fire immediately since we need the results back
        try:
            # no url, bail!
            if not webhook_url:
                raise ValueError("No webhook_url specified, skipping send")

            # only send webhooks when we are configured to, otherwise fail
            if settings.SEND_WEBHOOKS:
                requests_headers = http_headers(extra=headers)

                # some hosts deny generic user agents, use Temba as our user agent
                if action == "GET":
                    response = requests.get(webhook_url, headers=requests_headers, timeout=10)
                else:
                    requests_headers["Content-type"] = "application/json"
                    response = requests.post(
                        webhook_url, data=json.dumps(post_data), headers=requests_headers, timeout=10
                    )

                body = response.text
                if body:
                    body = body.strip()
                status_code = response.status_code
            else:
                print("!! Skipping WebHook send, SEND_WEBHOOKS set to False")
                body = "Skipped actual send"
                status_code = 200

            if ruleset:
                run.update_fields({Flow.label_to_slug(ruleset.label): body}, do_save=False)
            new_extra = {}

            # process the webhook response
            try:
                response_json = json.loads(body)

                # only update if we got a valid JSON dictionary or list
                if not isinstance(response_json, dict) and not isinstance(response_json, list):
                    raise ValueError("Response must be a JSON dictionary or list, ignoring response.")

                new_extra = response_json
                message = "Webhook called successfully."
            except ValueError:
                message = "Response must be a JSON dictionary, ignoring response."

            run.update_fields(new_extra)

            if 200 <= status_code < 300:
                webhook_event.status = cls.STATUS_COMPLETE
            else:
                webhook_event.status = cls.STATUS_FAILED
                message = "Got non 200 response (%d) from webhook." % response.status_code
                raise Exception("Got non 200 response (%d) from webhook." % response.status_code)

        except Exception as e:
            import traceback

            traceback.print_exc()

            webhook_event.status = cls.STATUS_FAILED
            message = "Error calling webhook: %s" % str(e)

        finally:
            webhook_event.save(update_fields=("status",))

            # make sure our message isn't too long
            if message:
                message = message[:255]

            request_time = (time.time() - start) * 1000

            contact = None
            if webhook_event.run:
                contact = webhook_event.run.contact

            result = WebHookResult.objects.create(
                event=webhook_event,
                contact=contact,
                url=webhook_url,
                status_code=status_code,
                body=body,
                message=message,
                data=post_data,
                request_time=request_time,
                created_by=api_user,
                modified_by=api_user,
            )

            # if this is a test contact, add an entry to our action log
            if run.contact.is_test:  # pragma: no cover
                log_txt = "Triggered <a href='%s' target='_log'>webhook event</a> - %d" % (
                    reverse("api.log_read", args=[webhook_event.pk]),
                    status_code,
                )
                ActionLog.create(run, log_txt, safe=True)

        return result
コード例 #36
0
ファイル: serializers.py プロジェクト: elbic/rapidpro-docker
 def get_geometry(self, obj):
     return json.loads(obj.simplified_geometry.geojson
                       ) if obj.simplified_geometry else None
コード例 #37
0
ファイル: serializers.py プロジェクト: resistbot/rapidpro
 def get_geometry(self, obj):
     if self.context["include_geometry"] and obj.simplified_geometry:
         return json.loads(obj.simplified_geometry.geojson)
     else:
         return None
コード例 #38
0
 def _serialize_deserialize(self, action):
     action_json = json.dumps(action.as_json())
     return Action.from_json(self.org, json.loads(action_json))
コード例 #39
0
    def test_schedule_ui(self):

        self.login(self.admin)

        joe = self.create_contact("Joe Blow", "123")

        # test missing recipients
        post_data = dict(text="message content",
                         omnibox="",
                         sender=self.channel.pk,
                         _format="json",
                         schedule=True)
        response = self.client.post(reverse("msgs.broadcast_send"),
                                    post_data,
                                    follow=True)
        self.assertContains(response, "At least one recipient is required")

        # missing message
        post_data = dict(text="",
                         omnibox="c-%s" % joe.uuid,
                         sender=self.channel.pk,
                         _format="json",
                         schedule=True)
        response = self.client.post(reverse("msgs.broadcast_send"),
                                    post_data,
                                    follow=True)
        self.assertContains(response, "This field is required")

        # finally create our message
        post_data = dict(text="A scheduled message to Joe",
                         omnibox="c-%s" % joe.uuid,
                         sender=self.channel.pk,
                         schedule=True)
        response = json.loads(
            self.client.post(reverse("msgs.broadcast_send") + "?_format=json",
                             post_data,
                             follow=True).content)
        self.assertIn("/broadcast/schedule_read", response["redirect"])

        # fetch our formax page
        response = self.client.get(response["redirect"])
        self.assertContains(response, "id-schedule")
        broadcast = response.context["object"]

        # update our message
        post_data = dict(message="An updated scheduled message",
                         omnibox="c-%s" % joe.uuid)
        self.client.post(reverse("msgs.broadcast_update", args=[broadcast.pk]),
                         post_data)
        self.assertEqual(
            Broadcast.objects.get(id=broadcast.id).text,
            {"base": "An updated scheduled message"})

        # update the schedule
        post_data = dict(repeat_period="W",
                         repeat_days=6,
                         start="later",
                         start_datetime_value=1)
        response = self.client.post(
            reverse("schedules.schedule_update", args=[broadcast.schedule.pk]),
            post_data)
コード例 #40
0
ファイル: tests.py プロジェクト: mxabierto/rapidpro
    def test_flow_event(self, mock_send):
        self.setupChannel()

        org = self.channel.org
        org.save()

        flow = self.get_flow("color")

        # replace our uuid of 4 with the right thing
        actionset = ActionSet.objects.get(x=4)
        actionset.actions = [WebhookAction(str(uuid4()), org.get_webhook_url()).as_json()]
        actionset.save()

        # run a user through this flow
        flow.start([], [self.joe])

        # have joe reply with mauve, which will put him in the other category that triggers the API Action
        sms = self.create_msg(
            contact=self.joe,
            direction="I",
            status="H",
            text="Mauve",
            attachments=["image/jpeg:http://s3.com/text.jpg", "audio/mp4:http://s3.com/text.mp4"],
        )

        mock_send.return_value = MockResponse(200, "{}")
        Flow.find_and_handle(sms)

        # should have one event created
        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("successfully", result.message)
        self.assertEqual(200, result.status_code)
        self.assertEqual(self.joe, result.contact)

        self.assertTrue(mock_send.called)

        args = mock_send.call_args_list[0][0]
        prepared_request = args[0]
        self.assertIn(self.channel.org.get_webhook_url(), prepared_request.url)

        data = json.loads(prepared_request.body)

        self.assertEqual(data["channel"], {"uuid": str(self.channel.uuid), "name": self.channel.name})
        self.assertEqual(
            data["contact"], {"uuid": str(self.joe.uuid), "name": self.joe.name, "urn": str(self.joe.get_urn("tel"))}
        )
        self.assertEqual(data["flow"], {"uuid": str(flow.uuid), "name": flow.name, "revision": 1})
        self.assertEqual(
            data["input"],
            {
                "urn": "tel:+250788123123",
                "text": "Mauve",
                "attachments": ["image/jpeg:http://s3.com/text.jpg", "audio/mp4:http://s3.com/text.mp4"],
            },
        )
        self.assertEqual(
            data["results"],
            {
                "color": {
                    "category": "Other",
                    "node_uuid": matchers.UUID4String(),
                    "name": "color",
                    "value": "Mauve\nhttp://s3.com/text.jpg\nhttp://s3.com/text.mp4",
                    "created_on": matchers.ISODate(),
                    "input": "Mauve\nhttp://s3.com/text.jpg\nhttp://s3.com/text.mp4",
                }
            },
        )
コード例 #41
0
    def _request(self, url, method="GET", params=None, api_call=None):
        """Internal request method"""
        method = method.lower()
        params = params or {}

        func = getattr(self.client, method)
        params, files = (params, None) if "event" in params else _transparent_params(params)

        requests_args = {}
        for k, v in self.client_args.items():
            # Maybe this should be set as a class variable and only done once?
            if k in ("timeout", "allow_redirects", "stream", "verify"):
                requests_args[k] = v

        if method == "get":
            requests_args["params"] = params
        else:
            requests_args.update({"data": json.dumps(params) if "event" in params else params, "files": files})
        try:
            if method == "get":
                event = HttpEvent(method, url + "?" + urlencode(params))
            else:
                event = HttpEvent(method, url, urlencode(params))
            self.events.append(event)

            response = func(url, **requests_args)
            event.status_code = response.status_code
            event.response_body = response.text

        except requests.RequestException as e:
            raise TwythonError(str(e))
        content = response.content.decode("utf-8")

        # create stash for last function intel
        self._last_call = {
            "api_call": api_call,
            "api_error": None,
            "cookies": response.cookies,
            "headers": response.headers,
            "status_code": response.status_code,
            "url": response.url,
            "content": content,
        }

        #  Wrap the json loads in a try, and defer an error
        #  Twitter will return invalid json with an error code in the headers
        json_error = False
        if content:
            try:
                try:
                    # try to get json
                    content = content.json()
                except AttributeError:
                    # if unicode detected
                    content = json.loads(content)
            except ValueError:
                json_error = True
                content = {}

        if response.status_code > 304:
            # If there is no error message, use a default.
            errors = content.get("errors", [{"message": "An error occurred processing your request."}])
            if errors and isinstance(errors, list):
                error_message = errors[0]["message"]
            else:
                error_message = errors  # pragma: no cover
            self._last_call["api_error"] = error_message

            ExceptionType = TwythonError
            if response.status_code == 429:
                # Twitter API 1.1, always return 429 when rate limit is exceeded
                ExceptionType = TwythonRateLimitError  # pragma: no cover
            elif response.status_code == 401 or "Bad Authentication data" in error_message:
                # Twitter API 1.1, returns a 401 Unauthorized or
                # a 400 "Bad Authentication data" for invalid/expired app keys/user tokens
                ExceptionType = TwythonAuthError

            raise ExceptionType(
                error_message, error_code=response.status_code, retry_after=response.headers.get("retry-after")
            )

        # if we have a json error here, then it's not an official Twitter API error
        if json_error and response.status_code not in (200, 201, 202):  # pragma: no cover
            raise TwythonError("Response was not valid JSON, unable to decode.")

        return content
コード例 #42
0
ファイル: base.py プロジェクト: mxabierto/rapidpro
 def get_flow_json(self, filename, substitutions=None):
     data = self.get_import_json(filename, substitutions=substitutions)
     return json.loads(data)["flows"][0]
コード例 #43
0
ファイル: handlers.py プロジェクト: teehamaral/rapidpro
    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)
コード例 #44
0
ファイル: models.py プロジェクト: teehamaral/rapidpro
 def to_python(self, value):
     if isinstance(value, str):
         value = json.loads(value)
     return value
コード例 #45
0
ファイル: base.py プロジェクト: clinton-chikwata/rapidpro
 def json(self):
     return json.loads(self.text)
コード例 #46
0
    def trigger_flow_webhook(cls,
                             run,
                             webhook_url,
                             ruleset,
                             msg,
                             action="POST",
                             resthook=None,
                             headers=None):

        flow = run.flow
        contact = run.contact
        org = flow.org
        channel = msg.channel if msg else None
        contact_urn = msg.contact_urn if (
            msg and msg.contact_urn) else contact.get_urn()

        contact_dict = dict(uuid=contact.uuid, name=contact.name)
        if contact_urn:
            contact_dict["urn"] = contact_urn.urn

        post_data = {
            "contact":
            contact_dict,
            "flow":
            dict(name=flow.name,
                 uuid=flow.uuid,
                 revision=flow.revisions.order_by("revision").last().revision),
            "path":
            run.path,
            "results":
            run.results,
            "run":
            dict(uuid=str(run.uuid), created_on=run.created_on.isoformat()),
        }

        if msg and msg.id > 0:
            post_data["input"] = dict(
                urn=msg.contact_urn.urn if msg.contact_urn else None,
                text=msg.text,
                attachments=(msg.attachments or []),
            )

        if channel:
            post_data["channel"] = dict(name=channel.name, uuid=channel.uuid)

        api_user = get_api_user()
        if not action:  # pragma: needs cover
            action = "POST"

        webhook_event = cls.objects.create(
            org=org,
            event=cls.TYPE_FLOW,
            channel=channel,
            data=post_data,
            run=run,
            try_count=1,
            action=action,
            resthook=resthook,
            created_by=api_user,
            modified_by=api_user,
        )

        status_code = -1
        message = "None"
        body = None

        start = time.time()

        # webhook events fire immediately since we need the results back
        try:
            # no url, bail!
            if not webhook_url:
                raise ValueError("No webhook_url specified, skipping send")

            # only send webhooks when we are configured to, otherwise fail
            if settings.SEND_WEBHOOKS:
                requests_headers = http_headers(extra=headers)

                # some hosts deny generic user agents, use Temba as our user agent
                if action == "GET":
                    response = requests.get(webhook_url,
                                            headers=requests_headers,
                                            timeout=10)
                else:
                    requests_headers["Content-type"] = "application/json"
                    response = requests.post(webhook_url,
                                             data=json.dumps(post_data),
                                             headers=requests_headers,
                                             timeout=10)

                body = response.text
                if body:
                    body = body.strip()
                status_code = response.status_code
            else:
                print("!! Skipping WebHook send, SEND_WEBHOOKS set to False")
                body = "Skipped actual send"
                status_code = 200

            if ruleset:
                run.update_fields({Flow.label_to_slug(ruleset.label): body},
                                  do_save=False)
            new_extra = {}

            # process the webhook response
            try:
                response_json = json.loads(body)

                # only update if we got a valid JSON dictionary or list
                if not isinstance(response_json, dict) and not isinstance(
                        response_json, list):
                    raise ValueError(
                        "Response must be a JSON dictionary or list, ignoring response."
                    )

                new_extra = response_json
                message = "Webhook called successfully."
            except ValueError:
                message = "Response must be a JSON dictionary, ignoring response."

            run.update_fields(new_extra)

            if 200 <= status_code < 300:
                webhook_event.status = cls.STATUS_COMPLETE
            else:
                webhook_event.status = cls.STATUS_FAILED
                message = "Got non 200 response (%d) from webhook." % response.status_code
                raise Exception("Got non 200 response (%d) from webhook." %
                                response.status_code)

        except Exception as e:
            import traceback

            traceback.print_exc()

            webhook_event.status = cls.STATUS_FAILED
            message = "Error calling webhook: %s" % str(e)

        finally:
            webhook_event.save(update_fields=("status", ))

            # make sure our message isn't too long
            if message:
                message = message[:255]

            request_time = (time.time() - start) * 1000

            contact = None
            if webhook_event.run:
                contact = webhook_event.run.contact

            result = WebHookResult.objects.create(
                event=webhook_event,
                contact=contact,
                url=webhook_url,
                status_code=status_code,
                body=body,
                message=message,
                data=post_data,
                request_time=request_time,
                created_by=api_user,
                modified_by=api_user,
            )

            # if this is a test contact, add an entry to our action log
            if run.contact.is_test:  # pragma: no cover
                log_txt = "Triggered <a href='%s' target='_log'>webhook event</a> - %d" % (
                    reverse("api.log_read", args=[webhook_event.pk]),
                    status_code,
                )
                ActionLog.create(run, log_txt, safe=True)

        return result