def has_unparseble_fields(run): if run.fields_raw is not None: try: json.loads(run.fields_raw) except ValueError: return True return False
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)
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"))
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))
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
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
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)
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)
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), ))
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
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))
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)
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
def json(self): return json.loads(self.text)
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"]) )
def to_python(self, value): if isinstance(value, str): value = json.loads(value) return value
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
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
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 post(self, request, *args, **kwargs): from temba.msgs.models import Msg request_body = request.body request_method = request.method request_path = request.get_full_path() def log_channel(channel, description, event, is_error=False): return ChannelLog.objects.create( channel_id=channel.pk, is_error=is_error, request=event.request_body, response=event.response_body, url=event.url, method=event.method, response_status=event.status_code, description=description, ) action = kwargs["action"].lower() request_uuid = kwargs["uuid"] data = json.loads(force_text(request_body)) is_ussd = self.is_ussd_message(data) channel_data = data.get("channel_data", {}) channel_types = ("JNU", "JN") # look up the channel channel = Channel.objects.filter( uuid=request_uuid, is_active=True, channel_type__in=channel_types).first() if not channel: return HttpResponse("Channel not found for id: %s" % request_uuid, status=400) auth = request.META.get("HTTP_AUTHORIZATION", "").split(" ") secret = channel.config.get(Channel.CONFIG_SECRET) if secret is not None and (len(auth) != 2 or auth[0] != "Token" or auth[1] != secret): return JsonResponse(dict(error="Incorrect authentication token"), status=401) # Junebug is sending an event if action == "event": expected_keys = ["event_type", "message_id", "timestamp"] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % ( ", ".join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle event.", event, is_error=True) return HttpResponse(response_body, status=status) message_id = data["message_id"] event_type = data["event_type"] # look up the message message = Msg.objects.filter( channel=channel, external_id=message_id).select_related("channel") if not message: status = 400 response_body = "Message with external id of '%s' not found" % ( message_id, ) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle %s event_type." % (event_type), event) return HttpResponse(response_body, status=status) if event_type == "submitted": for message_obj in message: message_obj.status_sent() if event_type == "delivery_succeeded": for message_obj in message: message_obj.status_delivered() elif event_type in ["delivery_failed", "rejected"]: for message_obj in message: message_obj.status_fail() response_body = { "status": self.ACK, "message_ids": [message_obj.pk for message_obj in message] } event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response_body)) log_channel(channel, "Handled %s event_type." % (event_type), event) # Let Junebug know we're happy return JsonResponse(response_body) # Handle an inbound message elif action == "inbound": expected_keys = [ "channel_data", "from", "channel_id", "timestamp", "content", "to", "reply_to", "message_id", ] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % ( ", ".join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle message.", event, is_error=True) return HttpResponse(response_body, status=status) if is_ussd: status = { "close": USSDSession.INTERRUPTED, "new": USSDSession.TRIGGERED }.get(channel_data.get("session_event"), USSDSession.IN_PROGRESS) message_date = datetime.strptime(data["timestamp"], "%Y-%m-%d %H:%M:%S.%f") gmt_date = pytz.timezone("GMT").localize(message_date) # Use a session id if provided, otherwise fall back to using the `from` address as the identifier session_id = channel_data.get("session_id") or data["from"] connection = USSDSession.handle_incoming( channel=channel, urn=data["from"], content=data["content"], status=status, date=gmt_date, external_id=session_id, message_id=data["message_id"], starcode=data["to"], ) if connection: status = 200 response_body = { "status": self.ACK, "session_id": connection.pk } event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, "Handled USSD message of %s session_event" % (channel_data["session_event"], ), event) return JsonResponse(response_body, status=status) else: status = 400 response_body = { "status": self.NACK, "reason": "No suitable session found for this message." } event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, "Failed to handle USSD message of %s session_event" % (channel_data["session_event"], ), event, ) return JsonResponse(response_body, status=status) else: content = data["content"] message = Msg.create_incoming(channel, URN.from_tel(data["from"]), content) status = 200 response_body = {"status": self.ACK, "message_id": message.pk} Msg.objects.filter(pk=message.id).update( external_id=data["message_id"]) event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) ChannelLog.log_message(message, "Handled inbound message.", event) return JsonResponse(response_body, status=status)
def get_flow_json(self, filename, substitutions=None): data = self.get_import_json(filename, substitutions=substitutions) return json.loads(data)["flows"][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
def get_geometry(self, obj): return json.loads(obj.simplified_geometry.geojson) if obj.simplified_geometry else None
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)
def get_geometry(self, obj): if self.context["include_geometry"] and obj.simplified_geometry: return json.loads(obj.simplified_geometry.geojson) else: return None
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}" )
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)
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)
def generator(): for line in in_stream: record = json.loads(line.decode("utf-8")) yield record
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)
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))
def post(self, request, *args, **kwargs): from temba.msgs.models import Msg request_body = request.body request_method = request.method request_path = request.get_full_path() def log_channel(channel, description, event, is_error=False): return ChannelLog.objects.create( channel_id=channel.pk, is_error=is_error, request=event.request_body, response=event.response_body, url=event.url, method=event.method, response_status=event.status_code, description=description, ) action = kwargs["action"].lower() request_uuid = kwargs["uuid"] data = json.loads(force_text(request_body)) is_ussd = self.is_ussd_message(data) channel_data = data.get("channel_data", {}) channel_types = ("JNU", "JN") # look up the channel channel = Channel.objects.filter(uuid=request_uuid, is_active=True, channel_type__in=channel_types).first() if not channel: return HttpResponse("Channel not found for id: %s" % request_uuid, status=400) auth = request.META.get("HTTP_AUTHORIZATION", "").split(" ") secret = channel.config.get(Channel.CONFIG_SECRET) if secret is not None and (len(auth) != 2 or auth[0] != "Token" or auth[1] != secret): return JsonResponse(dict(error="Incorrect authentication token"), status=401) # Junebug is sending an event if action == "event": expected_keys = ["event_type", "message_id", "timestamp"] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % (", ".join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle event.", event, is_error=True) return HttpResponse(response_body, status=status) message_id = data["message_id"] event_type = data["event_type"] # look up the message message = Msg.objects.filter(channel=channel, external_id=message_id).select_related("channel") if not message: status = 400 response_body = "Message with external id of '%s' not found" % (message_id,) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle %s event_type." % (event_type), event) return HttpResponse(response_body, status=status) if event_type == "submitted": for message_obj in message: message_obj.status_sent() if event_type == "delivery_succeeded": for message_obj in message: message_obj.status_delivered() elif event_type in ["delivery_failed", "rejected"]: for message_obj in message: message_obj.status_fail() response_body = {"status": self.ACK, "message_ids": [message_obj.pk for message_obj in message]} event = HttpEvent(request_method, request_path, request_body, 200, json.dumps(response_body)) log_channel(channel, "Handled %s event_type." % (event_type), event) # Let Junebug know we're happy return JsonResponse(response_body) # Handle an inbound message elif action == "inbound": expected_keys = [ "channel_data", "from", "channel_id", "timestamp", "content", "to", "reply_to", "message_id", ] if not set(expected_keys).issubset(data.keys()): status = 400 response_body = "Missing one of %s in request parameters." % (", ".join(expected_keys)) event = HttpEvent(request_method, request_path, request_body, status, response_body) log_channel(channel, "Failed to handle message.", event, is_error=True) return HttpResponse(response_body, status=status) if is_ussd: status = {"close": USSDSession.INTERRUPTED, "new": USSDSession.TRIGGERED}.get( channel_data.get("session_event"), USSDSession.IN_PROGRESS ) message_date = datetime.strptime(data["timestamp"], "%Y-%m-%d %H:%M:%S.%f") gmt_date = pytz.timezone("GMT").localize(message_date) # Use a session id if provided, otherwise fall back to using the `from` address as the identifier session_id = channel_data.get("session_id") or data["from"] connection = USSDSession.handle_incoming( channel=channel, urn=data["from"], content=data["content"], status=status, date=gmt_date, external_id=session_id, message_id=data["message_id"], starcode=data["to"], ) if connection: status = 200 response_body = {"status": self.ACK, "session_id": connection.pk} event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, "Handled USSD message of %s session_event" % (channel_data["session_event"],), event ) return JsonResponse(response_body, status=status) else: status = 400 response_body = {"status": self.NACK, "reason": "No suitable session found for this message."} event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) log_channel( channel, "Failed to handle USSD message of %s session_event" % (channel_data["session_event"],), event, ) return JsonResponse(response_body, status=status) else: content = data["content"] message = Msg.create_incoming(channel, URN.from_tel(data["from"]), content) status = 200 response_body = {"status": self.ACK, "message_id": message.pk} Msg.objects.filter(pk=message.id).update(external_id=data["message_id"]) event = HttpEvent(request_method, request_path, request_body, status, json.dumps(response_body)) ChannelLog.log_message(message, "Handled inbound message.", event) return JsonResponse(response_body, status=status)
def 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 get_geometry(self, obj): return json.loads(obj.simplified_geometry.geojson ) if obj.simplified_geometry else None
def _serialize_deserialize(self, action): action_json = json.dumps(action.as_json()) return Action.from_json(self.org, json.loads(action_json))
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)
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", } }, )
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
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)
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