Example #1
0
def migrate_to_version_11_5(json_flow, flow=None):
    """
    Replaces @flow.foo and @flow.foo.value with @extra.webhook where foo is a webhook or resthook ruleset
    """
    # figure out which rulesets are webhook or resthook calls
    rule_sets = json_flow.get("rule_sets", [])
    webhook_rulesets = set()
    non_webhook_rulesets = set()
    for r in rule_sets:
        slug = Flow.label_to_slug(r["label"])
        if not slug:  # pragma: no cover
            continue
        if r["ruleset_type"] in (RuleSet.TYPE_WEBHOOK, RuleSet.TYPE_RESTHOOK):
            webhook_rulesets.add(slug)
        else:
            non_webhook_rulesets.add(slug)

    # ignore any slugs of webhook rulesets which are also used by non-webhook rulesets
    slugs = webhook_rulesets.difference(non_webhook_rulesets)
    if not slugs:
        return json_flow

    # make a regex that matches a context reference to these (see https://regex101.com/r/65b2ZT/3)
    replace_pattern = r"flow\.(" + "|".join(slugs) + r")(\.value)?(?!\.\w)"
    replace_regex = regex.compile(replace_pattern,
                                  flags=regex.UNICODE | regex.IGNORECASE
                                  | regex.MULTILINE)
    replace_with = r"extra.\1"

    replace_templates(json_flow, lambda t: replace_regex.sub(replace_with, t))

    return json_flow
Example #2
0
def migrate_to_version_11_4(json_flow, flow=None):
    """
    Replaces @flow.foo.text with @step.value for non-waiting rulesets, to bring old world functionality inline with the
    new engine, where @run.results.foo.input is always the router operand.
    """
    # figure out which rulesets aren't waits
    rule_sets = json_flow.get("rule_sets", [])
    non_waiting = {
        Flow.label_to_slug(r["label"])
        for r in rule_sets if r["ruleset_type"] not in RuleSet.TYPE_WAIT
    }

    # make a regex that matches a context reference to the .text on any result from these
    replace_pattern = r"flow\.(" + "|".join(non_waiting) + ")\.text"
    replace_regex = regex.compile(replace_pattern,
                                  flags=regex.UNICODE | regex.IGNORECASE
                                  | regex.MULTILINE)
    replace_with = "step.value"

    # for every action in this flow, replace such references with @step.text
    for actionset in json_flow.get("action_sets", []):
        for action in actionset.get("actions", []):
            if action["type"] in ["reply", "send", "say", "email"]:
                msg = action["msg"]
                if isinstance(msg, str):
                    action["msg"] = replace_regex.sub(replace_with, msg)
                else:
                    for lang, text in msg.items():
                        msg[lang] = replace_regex.sub(replace_with, text)

    return json_flow
Example #3
0
def migrate_export_to_version_11_0(json_export, org, same_site=True):
    """
    Introduces the concept of format_location and format_date. This migration
    wraps all references to rulesets or contact fields which are locations or dates and
    wraps them appropriately
    """
    replacements = [
        [
            r"@date([^0-9a-zA-Z\.]|\.[^0-9a-zA-Z\.]|$|\.$)",
            r"@(format_date(date))\1"
        ],
        [r"@date\.now", r"@(format_date(date.now))"],
    ]

    # get all contact fields that are date or location for this org
    fields = ContactField.user_fields.filter(org=org,
                                             is_active=True,
                                             value_type__in=[
                                                 "D", "S", "I", "W"
                                             ]).only("id", "value_type", "key")

    for cf in fields:
        format_function = "format_date" if cf.value_type == "D" else "format_location"
        replacements.append([
            r"@contact\.%s([^0-9a-zA-Z\.]|\.[^0-9a-zA-Z\.]|$|\.$)" % cf.key,
            r"@(%s(contact.%s))\1" % (format_function, cf.key),
        ])

    for flow in json_export.get("flows", []):

        # figure out which rulesets are date or location
        for rs in flow.get("rule_sets", []):
            rs_type = None
            for rule in rs.get("rules", []):
                test = rule.get("test", {}).get("type")
                if not test:  # pragma: no cover
                    continue
                elif test == "true":
                    continue
                elif not rs_type:
                    rs_type = test
                elif rs_type and test != rs_type:
                    rs_type = "none"

            key = Flow.label_to_slug(rs["label"])

            # any reference to this result value's time property needs wrapped in format_date
            replacements.append([
                r"@flow\.%s\.time" % key,
                r"@(format_date(flow.%s.time))" % key
            ])

            # how we wrap the actual result value depends on its type
            if rs_type in ["date", "date_before", "date_after", "date_equal"]:
                format_function = "format_date"
            elif rs_type in ["state", "district", "ward"]:
                format_function = "format_location"
            else:  # pragma: no cover
                continue

            replacements.append([
                r"@flow\.%s([^0-9a-zA-Z\.]|\.[^0-9a-zA-Z\.]|$|\.$)" % key,
                r"@(%s(flow.%s))\1" % (format_function, key),
            ])

        # for every action in this flow, look for replies, sends or says that use these fields and wrap them
        for actionset in flow.get("action_sets", []):
            for action in actionset.get("actions", []):
                if action["type"] in ["reply", "send", "say"]:
                    msg = action["msg"]
                    for lang, text in msg.items():
                        migrated_text = text
                        for pattern, replacement in replacements:
                            migrated_text = regex.sub(pattern,
                                                      replacement,
                                                      migrated_text,
                                                      flags=regex.UNICODE
                                                      | regex.MULTILINE)

                        msg[lang] = migrated_text

    return json_export
Example #4
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
Example #5
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
Example #6
0
def migrate_export_to_version_11_0(json_export, org, same_site=True):
    """
    Introduces the concept of format_location and format_date. This migration
    wraps all references to rulesets or contact fields which are locations or dates and
    wraps them appropriately
    """
    replacements = [[
        r'@date([^0-9a-zA-Z\.]|\.[^0-9a-zA-Z\.]|$|\.$)',
        r'@(format_date(date))\1'
    ], [r'@date\.now', r'@(format_date(date.now))']]

    # get all contact fields that are date or location for this org
    fields = (ContactField.objects.filter(
        org=org, is_active=True,
        value_type__in=['D', 'S', 'I', 'W']).only('id', 'value_type', 'key'))

    for cf in fields:
        format_function = 'format_date' if cf.value_type == 'D' else 'format_location'
        replacements.append([
            r'@contact\.%s([^0-9a-zA-Z\.]|\.[^0-9a-zA-Z\.]|$|\.$)' % cf.key,
            r'@(%s(contact.%s))\1' % (format_function, cf.key)
        ])

    for flow in json_export.get('flows', []):

        # figure out which rulesets are date or location
        for rs in flow.get('rule_sets', []):
            rs_type = None
            for rule in rs.get('rules', []):
                test = rule.get('test', {}).get('type')
                if not test:  # pragma: no cover
                    continue
                elif test == 'true':
                    continue
                elif not rs_type:
                    rs_type = test
                elif rs_type and test != rs_type:
                    rs_type = 'none'

            key = Flow.label_to_slug(rs['label'])

            # any reference to this result value's time property needs wrapped in format_date
            replacements.append([
                r'@flow\.%s\.time' % key,
                r'@(format_date(flow.%s.time))' % key
            ])

            # how we wrap the actual result value depends on its type
            if rs_type in ['date', 'date_before', 'date_after', 'date_equal']:
                format_function = 'format_date'
            elif rs_type in ['state', 'district', 'ward']:
                format_function = 'format_location'
            else:  # pragma: no cover
                continue

            replacements.append([
                r'@flow\.%s([^0-9a-zA-Z\.]|\.[^0-9a-zA-Z\.]|$|\.$)' % key,
                r'@(%s(flow.%s))\1' % (format_function, key)
            ])

        # for every action in this flow, look for replies, sends or says that use these fields and wrap them
        for actionset in flow.get('action_sets', []):
            for action in actionset.get('actions', []):
                if action['type'] in ['reply', 'send', 'say']:
                    msg = action['msg']
                    for lang, text in msg.items():
                        migrated_text = text
                        for pattern, replacement in replacements:
                            migrated_text = regex.sub(pattern,
                                                      replacement,
                                                      migrated_text,
                                                      flags=regex.UNICODE
                                                      | regex.MULTILINE)

                        msg[lang] = migrated_text

    return json_export
Example #7
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