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