Skip to content

Commit

Permalink
HELP-40835: storage updates (2600hz#5385)
Browse files Browse the repository at this point in the history
* remove UI metadata

* start to define http handler schema

* update spec and naming

* log the handler being used

* update the attachments to accept HTTP handler

* handle decoding error response

* make sure the verb is properly created

* create per-test IDs

* remove redundant call

* start to exercise the storage API

* add take_value/{2,3} to kz_json

* add httpd server for recv http requests from kazoo

* start httpd

* verify returned contents match

* add come cleanup help. start httpd with log id

* add logging metadata to help correlate logs

* refactor compose voicemail into smaller functions

* some dialyzing

* separate storing meta from store url creation

* move encoding multipart to kz_http_util

* handle undefined owner_id

* add a blocking wait for requests to come in

* create a vmbox and message to trigger storage

* handle storage put_attachment return

* fix binding key for storage plan cache

* add http storage backend doc

* vmbox API test module

* test that we recv the MP3 on our http server

* make sure vmboxes are started

* fix code complaint

* update schema doc and what not

* dialyzer updates

* add empty response headers on error

* formatting

* spelling updates

* handle multipart response

* build multipart body if configured to do so

* ask for multipart bodies

* use cowlib boundary creator

* validate multipart response

* document multipart

* update schema generated docs

* separate concerns a bit

* set default headers if not supplied by the handler module

* detect the crossbar url/port better

* accept a port argument

* log successful store

* when fetching owner's plan, if missing, try account's

* add default resp_headers

pre-load storage ids for account (2600hz#5375)

add type for storage keys

missing export from kzd_users

missed copyright update
  • Loading branch information
jamesaimonetti committed Jan 3, 2019
1 parent cb03a8c commit 41c9065
Show file tree
Hide file tree
Showing 46 changed files with 1,531 additions and 251 deletions.
2 changes: 2 additions & 0 deletions .aspell.en.prepl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ dynamicly dynamically
applicate applicable
axact exact
sendig sending
func function
attemptting attempting
proprs props
outh oauth
Expand Down Expand Up @@ -138,6 +139,7 @@ broked broken
callflow's callflow
pervent prevent
likey likely
storaging storing
attemp attempt
hairpining Hairpinning
mimetypes MIME types
Expand Down
126 changes: 70 additions & 56 deletions applications/callflow/src/module/cf_voicemail.erl
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ check_mailbox(#mailbox{exists='false'
case find_mailbox(Box, Call, ?DEFAULT_FIND_BOX_PROMPT, Loop) of
{'ok', PossibleBox, NewLoop} -> check_mailbox(PossibleBox, Call, NewLoop);
{'error', 'not_found'} ->
%% can't find mailbox, set Loop to max to play abort prompts in above func clause
%% can't find mailbox, set Loop to max to play abort prompts in above function clause
check_mailbox(Box, Call, MaxLoginAttempts + 1)
end;
check_mailbox(#mailbox{is_setup='false'}=Box, 'true', Call, _) ->
Expand Down Expand Up @@ -369,7 +369,7 @@ find_destination_mailbox(#mailbox{max_login_attempts=MaxLoginAttempts}=Box, Call
find_destination_mailbox(Box, Call, SrcBoxId, NewLoop + 1);
{'ok', DestBox, _NewLoop} -> DestBox;
{'error', 'not_found'} ->
%% can't find mailbox, set Loop to max to play abort prompts in above func clause
%% can't find mailbox, set Loop to max to play abort prompts in above function clause
find_destination_mailbox(Box, Call, SrcBoxId, MaxLoginAttempts + 1)
end.

Expand All @@ -378,30 +378,82 @@ find_destination_mailbox(#mailbox{max_login_attempts=MaxLoginAttempts}=Box, Call
%% @end
%%------------------------------------------------------------------------------

-spec compose_voicemail(mailbox(), kapps_call:call()) ->
'ok' | {'branch', _} |
{'error', 'channel_hungup'}.
-type compose_return() :: 'ok' |
{'branch', _} |
{'error', 'channel_hungup'}.
-spec compose_voicemail(mailbox(), kapps_call:call()) -> compose_return().
compose_voicemail(#mailbox{owner_id=OwnerId}=Box, Call) ->
IsOwner = is_owner(Call, OwnerId),
compose_voicemail(Box, IsOwner, Call).

-spec compose_voicemail(mailbox(), boolean(), kapps_call:call()) ->
'ok' | {'branch', _} |
{'error', 'channel_hungup'}.
-spec compose_voicemail(mailbox(), boolean(), kapps_call:call()) -> compose_return().
compose_voicemail(#mailbox{check_if_owner='true'}=Box, 'true', Call) ->
lager:info("caller is the owner of this mailbox"),
lager:info("overriding action as check (instead of compose)"),
check_mailbox(Box, Call);
compose_voicemail(#mailbox{exists='false'}, _, Call) ->
compose_voicemail(#mailbox{exists='false'}, _IsOwner, Call) ->
lager:info("attempted to compose voicemail for missing mailbox"),
_ = kapps_call_command:b_prompt(<<"vm-not_available_no_voicemail">>, Call),
'ok';
compose_voicemail(#mailbox{max_message_count=MaxCount
,message_count=Count
,mailbox_id=VMBId
,keys=#keys{login=Login}
}=Box, _, Call) when Count >= MaxCount
andalso MaxCount > 0 ->
}=Box
,_IsOwner
,Call
)
when Count >= MaxCount
andalso MaxCount > 0 ->
handle_full_mailbox(Box, Call);
compose_voicemail(Box, _IsOwner, Call) ->
start_composing_voicemail(Box, Call).

-spec start_composing_voicemail(mailbox(), kapps_call:call()) -> compose_return().
start_composing_voicemail(#mailbox{media_extension=Ext}=Box, Call) ->
lager:debug("playing mailbox greeting to caller"),
_ = play_greeting_intro(Box, Call),
_ = play_greeting(Box, Call),
_ = play_instructions(Box, Call),
_NoopId = kapps_call_command:noop(Call),
%% timeout after 5 min for safety, so this process cant hang around forever
case kapps_call_command:wait_for_application_or_dtmf(<<"noop">>, 300000) of
{'ok', _} ->
lager:info("played greeting and instructions to caller, recording new message"),
record_voicemail(tmp_file(Ext), Box, Call);
{'dtmf', Digit} ->
_ = kapps_call_command:b_flush(Call),
handle_compose_dtmf(Box, Call, Digit);
{'error', R} ->
lager:info("error while playing voicemail greeting: ~p", [R])
end.

-spec handle_compose_dtmf(mailbox(), kapps_call:call(), kz_term:ne_binary()) -> compose_return().
handle_compose_dtmf(#mailbox{keys=#keys{login=Login}}=Box, Call, Login) ->
lager:info("caller pressed '~s', redirecting to check voicemail", [Login]),
check_mailbox(Box, Call);
handle_compose_dtmf(#mailbox{media_extension=Ext
,keys=#keys{operator=Operator}
}=Box
,Call
,Operator
) ->
lager:info("caller chose to ring the operator"),
case cf_util:get_operator_callflow(kapps_call:account_id(Call)) of
{'ok', Flow} -> {'branch', Flow};
{'error', _R} -> record_voicemail(tmp_file(Ext), Box, Call)
end;
handle_compose_dtmf(#mailbox{keys=#keys{continue=Continue}}=_Box, _Call, Continue) ->
lager:info("caller chose to continue to the next element in the callflow");
handle_compose_dtmf(#mailbox{media_extension=Ext}=Box, Call, _DTMF) ->
lager:info("caller pressed unbound '~s', skip to recording new message", [_DTMF]),
record_voicemail(tmp_file(Ext), Box, Call).

-spec handle_full_mailbox(mailbox(), kapps_call:call()) ->
'ok' | {'error', 'channel_hungup'}.
handle_full_mailbox(#mailbox{mailbox_id=VMBId
,keys=#keys{login=Login}
,max_message_count=MaxCount
,message_count=Count
}=Box, Call) ->
lager:debug("voicemail box is full, cannot hold more messages, sending notification"),
Props = [{<<"Account-ID">>, kapps_call:account_id(Call)}
,{<<"Voicemail-Box">>, VMBId}
Expand All @@ -423,43 +475,6 @@ compose_voicemail(#mailbox{max_message_count=MaxCount
check_mailbox(Box, Call);
_Else ->
lager:debug("finished with call")
end;
compose_voicemail(#mailbox{keys=#keys{login=Login
,operator=Operator
,continue=Continue
}
,media_extension=Ext
}=Box, _, Call) ->
lager:debug("playing mailbox greeting to caller"),
_ = play_greeting_intro(Box, Call),
_ = play_greeting(Box, Call),
_ = play_instructions(Box, Call),
_NoopId = kapps_call_command:noop(Call),
%% timeout after 5 min for safety, so this process cant hang around forever
case kapps_call_command:wait_for_application_or_dtmf(<<"noop">>, 300000) of
{'ok', _} ->
lager:info("played greeting and instructions to caller, recording new message"),
record_voicemail(tmp_file(Ext), Box, Call);
{'dtmf', Digit} ->
_ = kapps_call_command:b_flush(Call),
case Digit of
Login ->
lager:info("caller pressed '~s', redirecting to check voicemail", [Login]),
check_mailbox(Box, Call);
Operator ->
lager:info("caller chose to ring the operator"),
case cf_util:get_operator_callflow(kapps_call:account_id(Call)) of
{'ok', Flow} -> {'branch', Flow};
{'error', _R} -> record_voicemail(tmp_file(Ext), Box, Call)
end;
Continue ->
lager:info("caller chose to continue to the next element in the callflow");
_Else ->
lager:info("caller pressed unbound '~s', skip to recording new message", [_Else]),
record_voicemail(tmp_file(Ext), Box, Call)
end;
{'error', R} ->
lager:info("error while playing voicemail greeting: ~p", [R])
end.

%%------------------------------------------------------------------------------
Expand Down Expand Up @@ -526,27 +541,26 @@ record_voicemail(AttachmentName, #mailbox{max_message_length=MaxMessageLength
]),
kapps_call_command:tones([Tone], Call),
lager:info("composing new voicemail to ~s", [AttachmentName]),
Routins = [{fun kapps_call:set_message_left/2, 'true'}
],
Routines = [{fun kapps_call:set_message_left/2, 'true'}],
case kapps_call_command:b_record(AttachmentName, ?ANY_DIGIT, kz_term:to_binary(MaxMessageLength), Call) of
{'ok', Msg} ->
Length = kz_json:get_integer_value(<<"Length">>, Msg, 0),
case kz_call_event:hangup_cause(Msg) =:= 'undefined'
andalso review_recording(AttachmentName, 'true', Box, Call)
of
'false' ->
_ = cf_exe:update_call(Call, Routins),
_ = cf_exe:update_call(Call, Routines),
new_message(AttachmentName, Length, Box, Call);
{'ok', 'record'} ->
record_voicemail(tmp_file(Ext), Box, Call);
{'ok', _Selection} ->
_ = cf_exe:update_call(Call, Routins),
_ = cf_exe:update_call(Call, Routines),
cf_util:start_task(fun new_message/4, [AttachmentName, Length, Box], Call),
_ = kapps_call_command:prompt(<<"vm-saved">>, Call),
_ = kapps_call_command:prompt(<<"vm-thank_you">>, Call),
'ok';
{'branch', Flow} ->
_ = cf_exe:update_call(Call, Routins),
_ = cf_exe:update_call(Call, Routines),
_ = new_message(AttachmentName, Length, Box, Call),
_ = kapps_call_command:prompt(<<"vm-saved">>, Call),
{'branch', Flow}
Expand Down Expand Up @@ -1051,7 +1065,7 @@ message_menu(Prompt, #mailbox{keys=#keys{replay=Replay
{'ok', Next} -> {'ok', 'next'};
{'error', _}=E -> E;
_ ->
kapps_call_command:b_prompt(<<"menu-invalid_entry">>, Call),
_ = kapps_call_command:b_prompt(<<"menu-invalid_entry">>, Call),
message_menu(Box, Call)
end.

Expand Down
15 changes: 14 additions & 1 deletion applications/crossbar/doc/ref/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Key | Description | Type | Default | Required | Support Level
`connections` | Describes alternative connections to use (such as alternative CouchDB instances | [#/definitions/storage.connections](#storageconnections) | | `false` |
`id` | ID of the storage document | `string()` | | `false` |
`plan` | Describes how to store documents depending on the database or document type | [#/definitions/storage.plan](#storageplan) | | `false` |
`ui_metadata` | | `object()` | | `false` |

### storage.attachment.aws

Expand Down Expand Up @@ -81,6 +80,20 @@ Key | Description | Type | Default | Required | Support Level
`handler` | What handler module to use | `string('google_storage')` | | `true` |
`settings` | Settings for the Google Storage account | `object()` | | `true` |

### storage.attachment.http

schema for HTTP(s) attachment entry


Key | Description | Type | Default | Required | Support Level
--- | ----------- | ---- | ------- | -------- | -------------
`handler` | The handler interface to use | `string('http')` | | `true` |
`name` | Friendly name for this attachment handler | `string()` | | `false` |
`settings.send_multipart` | Toggle whether to send multipart payload when storing attachment - will include metadata JSON if true | `boolean()` | | `false` |
`settings.url` | The base HTTP(s) URL to use when creating the request | `string()` | | `true` |
`settings.verb` | The HTTP verb to use when sending the data | `string('POST' | 'PUT')` | `POST` | `false` |
`settings` | HTTP server settings | `object()` | | `true` |

### storage.attachment.onedrive

schema for OneDrive attachment entry
Expand Down
89 changes: 89 additions & 0 deletions applications/crossbar/doc/storage.http.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# HTTP Storage Backend

## Using the HTTP storage backend

When you maintain your own web server, you can opt to store attachments like voicemail messages on your system (letting you provide additional services to your customers).

### Create the storage backend

First, create a UUID:

```shell
echo $(tr -dc a-f0-9 < /dev/urandom | dd bs=32 count=1 2> /dev/null)
403f90f67d1b71341f2ea6426eed3d90
```

This UUID will be your reference to your HTTP server in the storage plan.

There are two main pieces of the storage plan to configure now: the `attachments` where you'll define the HTTP server information, and the `plan` where you'll configure KAZOO to use your HTTP server.

For instance, you can store new voicemails with a storage config like the following:
```json
{
"data": {
"attachments": {
"{UUID}": {
"handler": "http",
"name": "My HTTP server",
"settings": {
"url": "http://my.http.server:37635/some_prefix",
"verb": "POST"
}
}
},
"plan": {
"modb": {
"types": {
"mailbox_message": {
"attachments": {
"handler": "{UUID}"
}
}
}
}
}
}
}
```

PUT-ing this to `/v2/accounts/{ACCOUNT_ID}/storage` will result in your web server receiving a PUT/POST to `/some_prefix/{ACCOUNT_ID}/{TEST_ID}/{RANDOM}_test_credentials_file.txt`. The text file will contain something like `some random content: {RANDOM}`. Respond with a 201 to let KAZOO know the reception occurred.

Next, KAZOO will attempt to GET that attachment back. Your web server will see a request for `/some_prefix/{ACCOUNT_ID}/{TEST_ID}/{RANDOM}_test_credentials_file.txt` and expects to see a 200 OK and the contents.

If both the PUT/POST and the GET are successful, the API request to create the storage config will return a 201. You can now safely delete `{RANDOM}_test_credentials_file.txt` from your web server.

### On save

Now, when a voicemail is saved to the account, your web server will receive a PUT/POST request to `PUT req /some_prefix/{ACCOUNT_ID}/{MESSAGE_ID}/uploaded_file_{TIMESTAMP}.mp3` with the binary data as the body. Your web server will then need to respond with a 201 to let KAZOO know storing the data was successful.

!!!note save processing of the file for a later process; return the 201 to KAZOO as soon as your server confirms storing the file was successful locally.

### Multipart requests

If you want to receive both the binary data and the JSON metadata, you can add `"send_multipart":true` to the settings of the handler:

```json
"{UUID}": {
"handler": "http",
"name": "My HTTP server",
"settings": {
"url": "http://my.http.server:37635/some_prefix",
"verb": "POST",
"send_multipart":true
}
}
```

On save, KAZOO will send a `multipart/mixed` request that will something like:

```
{BOUNDARY}
content-type: application/json
{"name":"mailbox 1010 message MM-DD-YYYY HH:MM:SS","description":"voicemail message with media","source_type":"voicemail","source_id":"{SOURCE_ID}","media_source":"recording","streamable":true,"utc_seconds":{TIMESTAMP},"metadata":{"timestamp":{TIMESTAMP},"from":"{SIP_FROM}","to":"{SIP_TO}","caller_id_number":"{CID_NUMBER}","caller_id_name":"{CID_NAME}","call_id":"{CALL_ID}","folder":"new","length":1,"media_id":"{MEDIA_ID}"},"id":"{MEDIA_ID}"}
{BOUNDARY}
content-type: audio/mp3
{BINARY_DATA}
{BOUNDARY}
```
45 changes: 44 additions & 1 deletion applications/crossbar/doc/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ Key | Description | Type | Default | Required | Support Level
`connections` | Describes alternative connections to use (such as alternative CouchDB instances | [#/definitions/storage.connections](#storageconnections) | | `false` |
`id` | ID of the storage document | `string()` | | `false` |
`plan` | Describes how to store documents depending on the database or document type | [#/definitions/storage.plan](#storageplan) | | `false` |
`ui_metadata` | | `object()` | | `false` |

### storage.attachment.aws

Expand Down Expand Up @@ -117,6 +116,20 @@ Key | Description | Type | Default | Required | Support Level
`handler` | What handler module to use | `string('google_storage')` | | `true` |
`settings` | Settings for the Google Storage account | `object()` | | `true` |

### storage.attachment.http

schema for HTTP(s) attachment entry


Key | Description | Type | Default | Required | Support Level
--- | ----------- | ---- | ------- | -------- | -------------
`handler` | The handler interface to use | `string('http')` | | `true` |
`name` | Friendly name for this attachment handler | `string()` | | `false` |
`settings.send_multipart` | Toggle whether to send multipart payload when storing attachment - will include metadata JSON if true | `boolean()` | | `false` |
`settings.url` | The base HTTP(s) URL to use when creating the request | `string()` | | `true` |
`settings.verb` | The HTTP verb to use when sending the data | `string('POST' | 'PUT')` | `POST` | `false` |
`settings` | HTTP server settings | `object()` | | `true` |

### storage.attachment.onedrive

schema for OneDrive attachment entry
Expand Down Expand Up @@ -351,6 +364,36 @@ curl -v -X PUT \
http://{SERVER}:8000/v2/accounts/{ACCOUNT_ID}/storage
```

For instance, setting up your HTTP server to receive new voicemails for the account:

```json
{
"data": {
"attachments": {
"{UUID}": {
"handler": "http",
"name": "My HTTP server",
"settings": {
"url": "http://my.http.server:37635/some_prefix",
"verb": "POST"
}
}
},
"plan": {
"modb": {
"types": {
"mailbox_message": {
"attachments": {
"handler": "{UUID}"
}
}
}
}
}
}
}
```

## Change

> POST /v2/accounts/{ACCOUNT_ID}/storage
Expand Down
Loading

0 comments on commit 41c9065

Please sign in to comment.