├── .gitignore ├── images ├── jester-logo.png ├── lua-freeswitch-logo.png └── lua-freeswitch-logo.xcf ├── profiles ├── voicemail │ ├── sequences │ │ ├── none.lua │ │ ├── repeat_message.lua │ │ ├── mailbox_login_failed.lua │ │ ├── mailbox_login_incorrect_have_mailbox.lua │ │ ├── set_current_folder.lua │ │ ├── play_first_message.lua │ │ ├── transfer_to_operator.lua │ │ ├── invalid_extension.lua │ │ ├── copy_new_old_messages.lua │ │ ├── post_command.lua │ │ ├── send_reply_save_message.lua │ │ ├── auto_delete_messages.lua │ │ ├── mailbox_setup.lua │ │ ├── get_message_count.lua │ │ ├── login.lua │ │ ├── erase_temp_greeting.lua │ │ ├── forward_message_prepend_menu.lua │ │ ├── record_greeting_thank_you.lua │ │ ├── exit.lua │ │ ├── cleanup_temp_recording.lua │ │ ├── update_message_deleted.lua │ │ ├── load_message_group.lua │ │ ├── main_menu_advanced_options.lua │ │ ├── prepare_messages.lua │ │ ├── login_without_password.lua │ │ ├── collect_outdial_number.lua │ │ ├── messages_checked.lua │ │ ├── no_more_messages.lua │ │ ├── forward_message_menu.lua │ │ ├── callback_set_number.lua │ │ ├── prev_message.lua │ │ ├── login_missing_mailbox.lua │ │ ├── play_messages.lua │ │ ├── mailbox_login_incorrect_missing_mailbox.lua │ │ ├── next_message.lua │ │ ├── listen_to_greeting.lua │ │ ├── record_greeting_confirm.lua │ │ ├── callback.lua │ │ ├── load_new_old_messages.lua │ │ ├── message_deleted_undeleted.lua │ │ ├── delete_undelete_message.lua │ │ ├── provision_mailbox.lua │ │ ├── message_saved.lua │ │ ├── accept_greeting.lua │ │ ├── login_have_mailbox.lua │ │ ├── mailbox_login_incorrect.lua │ │ ├── update_message_folder.lua │ │ ├── operator_transfer_prepare.lua │ │ ├── main_greeting_prepare_message.lua │ │ ├── new_user_walkthrough.lua │ │ ├── caller_rerecord_message.lua │ │ ├── get_messages.lua │ │ ├── record_message.lua │ │ ├── caller_save_recorded_message.lua │ │ ├── caller_playback_recorded_message.lua │ │ ├── forward_message_prepend.lua │ │ ├── send_reply_prepare_message.lua │ │ ├── send_reply.lua │ │ ├── save_message.lua │ │ ├── remove_mailbox_deleted_message_files.lua │ │ ├── advanced_options.lua │ │ ├── load_mailbox_settings.lua │ │ ├── collect_extension.lua │ │ ├── change_folders.lua │ │ ├── manage_temp_greeting.lua │ │ ├── send_reply_check.lua │ │ ├── outdial.lua │ │ ├── update_password.lua │ │ ├── play_message_number.lua │ │ ├── play_message.lua │ │ ├── mailbox_options.lua │ │ ├── remove_mailbox_deleted_messages.lua │ │ ├── record_greeting.lua │ │ ├── check_for_recorded_message.lua │ │ ├── review_message.lua │ │ ├── change_password.lua │ │ ├── email_message.lua │ │ ├── play_cid_envelope.lua │ │ ├── help.lua │ │ ├── save_individual_message.lua │ │ ├── validate_mailbox_login.lua │ │ ├── save_recorded_message.lua │ │ ├── message_options.lua │ │ ├── main_greeting.lua │ │ ├── forward_message.lua │ │ ├── save_group_message.lua │ │ └── main_menu.lua │ ├── voicemail.sql │ ├── INSTALL.md │ └── example_dialplan.xml ├── socket │ ├── sequences │ │ ├── exit.lua │ │ ├── clean_deleted_messages.lua │ │ └── remove_deleted_message_files.lua │ ├── INSTALL.txt │ └── conf.lua └── demos │ ├── sequences │ └── speech_to_text.lua │ └── conf.lua ├── TODO.md ├── support ├── variable.lua ├── shell.lua ├── function.lua ├── file.lua ├── string.lua └── table.lua ├── .busted ├── examples ├── log_test.lua ├── speech_to_text.lua └── phone_to_post_test.lua ├── BUGS.md ├── scripts ├── compile.sh ├── compile.txt └── extract_actions.lua ├── spec ├── http_mock.lua ├── mock_spec.lua └── unit │ └── core_spec.lua ├── LICENSE.txt ├── ldoc.custom.css ├── config.ld ├── modules ├── speech_to_text │ ├── google.lua │ ├── support.lua │ ├── att.lua │ └── init.lua ├── event │ └── init.lua ├── hangup │ └── init.lua ├── log │ └── init.lua ├── system │ └── init.lua ├── tracker │ └── init.lua └── format │ └── init.lua ├── CHANGELOG.md ├── doc ├── 04-Scripts.md ├── 05-Developer.md ├── 02-Profiles.md └── 01-Intro.md ├── README.md ├── conf.lua ├── INSTALL.md ├── socket.lua └── utilities └── startup_script.lua /.gitignore: -------------------------------------------------------------------------------- 1 | documentation/ 2 | examples/private.lua 3 | -------------------------------------------------------------------------------- /images/jester-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehunmonkgroup/jester/HEAD/images/jester-logo.png -------------------------------------------------------------------------------- /images/lua-freeswitch-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehunmonkgroup/jester/HEAD/images/lua-freeswitch-logo.png -------------------------------------------------------------------------------- /images/lua-freeswitch-logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thehunmonkgroup/jester/HEAD/images/lua-freeswitch-logo.xcf -------------------------------------------------------------------------------- /profiles/voicemail/sequences/none.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Dummy action, used for taking no action. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "none", 9 | }, 10 | } 11 | 12 | -------------------------------------------------------------------------------- /profiles/socket/sequences/exit.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Dummy action, used for exiting the remove files loop. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "none", 9 | }, 10 | } 11 | 12 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | * Refactor speech_to_text module to support retry base module 3 | * figure out how to work the record_trim script into Jester -- perhaps as an 4 | action in the record module? 5 | -------------------------------------------------------------------------------- /support/variable.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Support functions for variables. 3 | ]] 4 | 5 | --[[ 6 | Checks for empty variable. 7 | ]] 8 | function empty(obj) 9 | return not obj or obj == '' 10 | end 11 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/repeat_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play a message again. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "call_sequence", 9 | sequence = "play_messages", 10 | }, 11 | } 12 | 13 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/mailbox_login_failed.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Login failed, notify user and hang up. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "play_phrase", 9 | phrase = "login_incorrect", 10 | }, 11 | { 12 | action = "call_sequence", 13 | sequence = "exit", 14 | }, 15 | } 16 | 17 | -------------------------------------------------------------------------------- /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | coverage = false, 4 | verbose = true, 5 | lpath = "../?/init.lua;../?.lua", 6 | }, 7 | default = { 8 | }, 9 | unit = { 10 | ROOT = {"spec/unit"}, 11 | }, 12 | integration = { 13 | ROOT = {"spec/integration"}, 14 | }, 15 | } 16 | 17 | -- vi: ft=lua 18 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/mailbox_login_incorrect_have_mailbox.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Incorrect login workflow when the mailbox was provided. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "play_phrase", 9 | phrase = "login_incorrect", 10 | }, 11 | -- Top of the stack will permit another login attempt. 12 | { 13 | action = "navigation_top", 14 | }, 15 | } 16 | 17 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/set_current_folder.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Set the current folder. 3 | ]] 4 | 5 | -- The folder to make the current folder. 6 | folder = args(1) 7 | 8 | return 9 | { 10 | { 11 | action = "call_sequence", 12 | sequence = "sub:prepare_messages " .. folder, 13 | }, 14 | { 15 | action = "call_sequence", 16 | sequence = "help" 17 | }, 18 | } 19 | 20 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/play_first_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Set to the first message in the folder, and redirect to play messages. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "counter", 9 | storage_key = "message_number", 10 | reset = true, 11 | increment = 1, 12 | }, 13 | { 14 | action = "call_sequence", 15 | sequence = "play_messages", 16 | }, 17 | } 18 | 19 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/transfer_to_operator.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Transfer to the operator extension. 3 | ]] 4 | 5 | operator_extension = storage("mailbox_settings", "operator_extension") 6 | 7 | return 8 | { 9 | { 10 | action = "play_phrase", 11 | phrase = "transfer_announcement", 12 | }, 13 | { 14 | action = "transfer", 15 | extension = operator_extension, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/invalid_extension.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Announce an invalid extension, and send to the next appropriate sequence. 3 | ]] 4 | 5 | -- Next sequence to call. 6 | next_sequence = args(1) 7 | 8 | return 9 | { 10 | { 11 | action = "play_phrase", 12 | phrase = "invalid_extension", 13 | }, 14 | { 15 | action = "call_sequence", 16 | sequence = next_sequence, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/copy_new_old_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copy loaded new or old message data to the message storage area. 3 | We already have the message data loaded, and it saves another hit to the 4 | expensive data_load action. 5 | ]] 6 | 7 | -- Which set of message data to copy. 8 | storage_area = args(1) 9 | 10 | return 11 | { 12 | { 13 | action = "copy_storage", 14 | storage_area = storage_area, 15 | copy_to = "message", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/post_command.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Determines where to send the call after a mailbox command. 3 | ]] 4 | 5 | -- Setting for where to go after a command. 6 | next_after_command = storage("mailbox_settings", "next_after_command") 7 | 8 | return 9 | { 10 | { 11 | action = "conditional", 12 | value = next_after_command, 13 | compare_to = "yes", 14 | if_true = "next_message", 15 | if_false = "message_options", 16 | }, 17 | } 18 | 19 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/send_reply_save_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Clean up and redirect after saving a message reply. 3 | ]] 4 | 5 | return 6 | { 7 | -- Explicitly clear this so the manual message saving sequence can always 8 | -- accurately know if a reply is being saved or not. 9 | { 10 | action = "clear_storage", 11 | storage_area = "send_reply_info", 12 | }, 13 | { 14 | action = "call_sequence", 15 | sequence = "message_options", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /examples/log_test.lua: -------------------------------------------------------------------------------- 1 | return 2 | { 3 | { 4 | action = "log", 5 | message = "Hello world", 6 | }, 7 | { 8 | action = "log", 9 | message = "Jester sequences directory: " .. global.sequence_path, 10 | }, 11 | { 12 | action = "log", 13 | message = "Jester channel variable : " .. variable("caller_id_number"), 14 | }, 15 | { 16 | action = "log", 17 | message = "Jester storage test: " .. storage("data", "voicemail_settings_mailbox"), 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /profiles/socket/INSTALL.txt: -------------------------------------------------------------------------------- 1 | This is an experimental profile to provide some basic non-realtime operations 2 | on voicemail data via Jester's socket listener. 3 | 4 | Currently the only sequence in the profile is 'clean_deleted_messages', which 5 | removes all database entries and recordings for messages that have been marked 6 | as deleted. It takes no arguments. 7 | 8 | See the code notes at the top of jester/socket.lua for more information on 9 | using this profile via the socket listener. 10 | 11 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/auto_delete_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Checks to see if deleted messages should be removed automatically, and calls 3 | the deletion sequence if necessary. 4 | ]] 5 | 6 | mailbox = storage("login_settings", "mailbox_number") 7 | 8 | return 9 | { 10 | { 11 | action = "conditional", 12 | value = profile.auto_delete_messages, 13 | compare_to = true, 14 | comparison = "equal", 15 | if_true = "remove_mailbox_deleted_messages " .. mailbox .. "," .. profile.domain, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/mailbox_setup.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Mark a mailbox as set up in the database. 3 | ]] 4 | 5 | -- Mailbox info. 6 | mailbox = args(1) 7 | domain = args(2) 8 | 9 | return 10 | { 11 | { 12 | action = "data_update", 13 | handler = "odbc", 14 | config = profile.db_config_mailbox, 15 | fields = { 16 | mailbox_setup_complete = "yes", 17 | }, 18 | filters = { 19 | mailbox = mailbox, 20 | domain = domain, 21 | }, 22 | update_type = "update", 23 | }, 24 | } 25 | 26 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/get_message_count.lua: -------------------------------------------------------------------------------- 1 | domain = args(1) 2 | mailbox = args(2) 3 | folder = args(3) 4 | message_type = args(4) 5 | 6 | return 7 | { 8 | { 9 | action = "data_load_count", 10 | handler = "odbc", 11 | config = profile.db_config_message, 12 | filters = { 13 | mailbox = mailbox, 14 | domain = domain, 15 | __folder = folder, 16 | __deleted = 0, 17 | }, 18 | count_field = "id", 19 | storage_key = "message_" .. message_type .. "_count", 20 | }, 21 | } 22 | 23 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/login.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Main login sequence. 3 | ]] 4 | 5 | -- Mailbox to log in for -- may or may not be provided. 6 | mailbox = args(1) 7 | 8 | return 9 | { 10 | -- Direct to the proper login workflow depending on if the mailbox was 11 | -- provided or not. 12 | { 13 | action = "conditional", 14 | value = mailbox, 15 | compare_to = "", 16 | comparison = "equal", 17 | if_true = "login_missing_mailbox", 18 | if_false = "login_have_mailbox " .. mailbox, 19 | }, 20 | } 21 | 22 | -------------------------------------------------------------------------------- /BUGS.md: -------------------------------------------------------------------------------- 1 | TODO: Update for 2.x 2 | ## Known bugs 3 | * Navigation stack probably doesn't work properly in inside any subsequences 4 | * DTMF in the global key handler is not queued -- keys that are pressed 5 | during actions that have no keys param are simply discarded. 6 | * Modules currently can't implement handlers for other modules -- need a way 7 | to tell the core how to load the module file providing the handler, but 8 | call the action from the file it's implemented in. 9 | * Path separators are all assumed to be Lunix-style, which breaks on Windows. 10 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/erase_temp_greeting.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Erase the user's temporary greeting. 3 | ]] 4 | 5 | mailbox = storage("login_settings", "mailbox_number") 6 | mailbox_directory = profile.mailboxes_dir .. "/" .. mailbox 7 | temp_greeting = mailbox_directory .. "/temp.wav" 8 | 9 | return 10 | { 11 | { 12 | action = "delete_file", 13 | file = temp_greeting, 14 | }, 15 | { 16 | action = "play_phrase", 17 | phrase = "temp_greeting_removed", 18 | }, 19 | { 20 | action = "call_sequence", 21 | sequence = "mailbox_options", 22 | }, 23 | } 24 | 25 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/forward_message_prepend_menu.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play a menu of options for prepending a message to a forward message. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "play_phrase", 9 | phrase = "forward_options", 10 | keys = { 11 | ["1"] = "forward_message prepend", 12 | ["2"] = "forward_message", 13 | ["*"] = "message_options", 14 | }, 15 | repetitions = profile.menu_repetitions, 16 | wait = profile.menu_replay_wait, 17 | }, 18 | { 19 | action = "call_sequence", 20 | sequence = "exit" 21 | }, 22 | } 23 | 24 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/record_greeting_thank_you.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Thanks the user for recording a greeting. 3 | ]] 4 | 5 | -- The name of the recorded greeting. 6 | greeting = args(1) 7 | 8 | return 9 | { 10 | { 11 | action = "play_phrase", 12 | phrase = "thank_you", 13 | keys = { 14 | ["1"] = "accept_greeting " .. greeting, 15 | ["2"] = "listen_to_greeting " .. greeting, 16 | ["3"] = "record_greeting " .. greeting, 17 | }, 18 | }, 19 | { 20 | action = "call_sequence", 21 | sequence = "record_greeting_confirm " .. greeting, 22 | }, 23 | } 24 | 25 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/exit.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Say goodbye and exit voicemail. 3 | ]] 4 | 5 | -- How are we exiting? 6 | exit_type = args(1) 7 | exit_extension = storage("mailbox_settings", "exit_extension") 8 | 9 | -- Build the exit action based on how we should exit. 10 | if exit_type == "exit_extension" and exit_extension ~= "" then 11 | exit = { 12 | action = "transfer", 13 | extension = exit_extension, 14 | } 15 | else 16 | exit = { 17 | action = "hangup", 18 | } 19 | end 20 | 21 | return 22 | { 23 | { 24 | action = "play_phrase", 25 | phrase = "goodbye" 26 | }, 27 | exit, 28 | } 29 | 30 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/cleanup_temp_recording.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Clean up a temporary recording. 3 | ]] 4 | 5 | recording = storage("record", "last_recording_name") 6 | 7 | -- If no recording exists, we don't want to call the delete function, because 8 | -- it could erroneously try to delete the containing directory, so build the 9 | -- final action based on this. 10 | if recording ~= "" then 11 | action = { 12 | action = "delete_file", 13 | file = profile.temp_recording_dir .. "/" .. recording, 14 | } 15 | else 16 | action = { 17 | action = "none", 18 | } 19 | end 20 | 21 | return 22 | { 23 | action, 24 | } 25 | 26 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/update_message_deleted.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Update the deletion state of a message. 3 | ]] 4 | 5 | -- The deleted state to update to. 6 | deleted = args(1) 7 | 8 | -- Message data. 9 | message_number = storage("counter", "message_number") 10 | message_id = storage("message", "id_" .. message_number) 11 | 12 | return 13 | { 14 | { 15 | action = "data_update", 16 | handler = "odbc", 17 | config = profile.db_config_message, 18 | fields = { 19 | __deleted = deleted, 20 | }, 21 | filters = { 22 | __id = message_id, 23 | }, 24 | update_type = "update", 25 | }, 26 | } 27 | 28 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/load_message_group.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Load mailbox information for a message group, then direct to the group save 3 | sequence. 4 | ]] 5 | 6 | message_group = args(1) 7 | 8 | return 9 | { 10 | { 11 | action = "data_load", 12 | handler = "odbc", 13 | config = profile.db_config_message_group, 14 | filters = { 15 | group_name = message_group, 16 | }, 17 | fields = { 18 | "mailbox", 19 | "domain", 20 | }, 21 | storage_area = "message_group", 22 | multiple = true, 23 | }, 24 | { 25 | action = "call_sequence", 26 | sequence = "save_group_message", 27 | }, 28 | } 29 | 30 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/main_menu_advanced_options.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play advanced options menu for the main menu. 3 | ]] 4 | 5 | outdial_extension = storage("mailbox_settings", "outdial_extension") 6 | 7 | return 8 | { 9 | { 10 | action = "play_phrase", 11 | phrase = "advanced_options_list", 12 | phrase_arguments = "N:N:N:Y:N", 13 | keys = { 14 | -- Outdial. 15 | ["4"] = "outdial " .. outdial_extension .. ",help,collect", 16 | ["*"] = "help", 17 | }, 18 | repetitions = profile.menu_repetitions, 19 | wait = profile.menu_replay_wait, 20 | }, 21 | { 22 | action = "call_sequence", 23 | sequence = "exit" 24 | }, 25 | } 26 | 27 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/prepare_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Prepare messages in a folder for playing. 3 | ]] 4 | 5 | -- The folder to prepare. 6 | folder = args(1) 7 | 8 | return 9 | { 10 | -- Load the messages. 11 | { 12 | action = "call_sequence", 13 | sequence = "sub:get_messages " .. folder, 14 | }, 15 | -- Set the active folder. 16 | { 17 | action = "set_storage", 18 | storage_area = "message_settings", 19 | data = { 20 | current_folder = folder, 21 | }, 22 | }, 23 | -- Reset the message counter. 24 | { 25 | action = "counter", 26 | storage_key = "message_number", 27 | increment = 1, 28 | reset = true, 29 | }, 30 | } 31 | 32 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/login_without_password.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Login workflow when a mailbox is provided, and no password is required. 3 | ]] 4 | 5 | mailbox = storage("login_settings", "mailbox_number") 6 | -- Result of the attempt to load the mailbox. 7 | loaded_mailbox = storage("mailbox_settings", "mailbox") 8 | 9 | return 10 | { 11 | { 12 | action = "call_sequence", 13 | sequence = "sub:load_mailbox_settings " .. mailbox .. "," .. profile.domain .. ",mailbox_settings", 14 | }, 15 | { 16 | action = "conditional", 17 | value = loaded_mailbox, 18 | compare_to = "", 19 | comparison = "equal", 20 | if_true = "invalid_extension exit", 21 | if_false = "load_new_old_messages", 22 | }, 23 | } 24 | 25 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/collect_outdial_number.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Collect an outdial number to dial from the user. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "get_digits", 9 | min_digits = profile.mailbox_min_digits, 10 | max_digits = 20, 11 | audio_files = "phrase:collect_outdial_number", 12 | bad_input = "", 13 | -- * is for cancelling the collection. Unfortunately, there's no way in 14 | -- Lua to escape the timeout and collect the escape key, so if a person 15 | -- presses *, they'll have to wait for the timeout before the cancel 16 | -- sequence is called. 17 | digits_regex = "\\d+|\\*", 18 | storage_key = "outdial_number", 19 | timeout = profile.user_input_timeout, 20 | }, 21 | } 22 | 23 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/messages_checked.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Fire an event that messages have been checked. 3 | ]] 4 | 5 | -- Mailbox that was checked. 6 | mailbox = storage("login_settings", "mailbox_number") 7 | 8 | -- Get message count, supplied to the fired event. 9 | message_count = storage("data", "message_new_count") 10 | 11 | return 12 | { 13 | { 14 | action = "call_sequence", 15 | sequence = "sub:get_message_count " .. profile.domain .. "," .. mailbox .. ",0,new", 16 | }, 17 | { 18 | action = "fire_event", 19 | event_type = "messages_checked", 20 | headers = { 21 | Mailbox = mailbox, 22 | Domain = profile.domain, 23 | ["New-Message-Count"] = message_count, 24 | }, 25 | }, 26 | } 27 | 28 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/no_more_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play 'no more messages', and direct to the appropriate next sequence. 3 | ]] 4 | 5 | -- The next sequence to call. 6 | next_sequence = args(1) 7 | 8 | return 9 | { 10 | { 11 | action = "play_phrase", 12 | phrase = "no_more_messages", 13 | keys = { 14 | ["3"] = "advanced_options", 15 | ["4"] = "prev_message", 16 | ["5"] = "repeat_message", 17 | ["6"] = "next_message", 18 | ["7"] = "delete_undelete_message", 19 | ["8"] = "forward_message_menu", 20 | ["9"] = "save_message", 21 | ["*"] = "help", 22 | ["#"] = "exit", 23 | }, 24 | }, 25 | { 26 | action = "call_sequence", 27 | sequence = next_sequence, 28 | }, 29 | } 30 | 31 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/forward_message_menu.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play a menu of options for collecting the forwarding number. 3 | ]] 4 | 5 | return 6 | { 7 | { 8 | action = "play_phrase", 9 | phrase = "forward_message_choose_method", 10 | keys = { 11 | ["1"] = "collect_extension forward_message_prepend_menu,forward_message_menu", 12 | -- TODO: Option 2 is for choosing the number from the voicemail 13 | -- directory, which still needs to be built. 14 | ["2"] = "invalid_extension message_options", 15 | ["*"] = "message_options", 16 | }, 17 | repetitions = profile.menu_repetitions, 18 | wait = profile.menu_replay_wait, 19 | }, 20 | { 21 | action = "call_sequence", 22 | sequence = "exit" 23 | }, 24 | } 25 | 26 | -------------------------------------------------------------------------------- /support/shell.lua: -------------------------------------------------------------------------------- 1 | require "jester.support.string" 2 | 3 | function standardize_output(output) 4 | local lines = output:split("\n") 5 | if lines[#lines] == "" then 6 | table.remove(lines) 7 | end 8 | return lines 9 | end 10 | 11 | function run_shell_command(command, collect_stderr) 12 | if collect_stderr then 13 | command = string.format([[%s 2>&1]], command) 14 | end 15 | -- This will open the file 16 | local file = io.popen(command) 17 | -- This will read all of the output 18 | local output = file:read('*all') 19 | -- This will get a table with some return stuff 20 | -- rc[1] will be true, false or nil 21 | -- rc[3] will be the signal 22 | local rc = {file:close()} 23 | local success = rc[1] 24 | return success, output 25 | end 26 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/callback_set_number.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Set the number to call to the extension that left the message. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | caller_id_number = storage("message", "caller_id_number_" .. message_number) 8 | 9 | callback_extension = storage("mailbox_settings", "callback_extension") 10 | 11 | return 12 | { 13 | -- The outdial sequence looks here for the number, so set it explicitly. 14 | { 15 | action = "set_storage", 16 | storage_area = "get_digits", 17 | data = { 18 | outdial_number = caller_id_number, 19 | }, 20 | }, 21 | { 22 | action = "call_sequence", 23 | sequence = "outdial " .. callback_extension .. ",message_options", 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/prev_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Checks for previous message, and sets up playback for it if necessary. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | 8 | return 9 | { 10 | -- If we're already on the first message, send to the 'no more messages' 11 | -- sequence. 12 | { 13 | action = "conditional", 14 | value = message_number, 15 | compare_to = 1, 16 | if_true = "no_more_messages message_options", 17 | }, 18 | -- Otherwise, decrement the message counter and play the message. 19 | { 20 | action = "counter", 21 | storage_key = "message_number", 22 | increment = -1, 23 | }, 24 | { 25 | action = "call_sequence", 26 | sequence = "play_messages", 27 | }, 28 | } 29 | 30 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/login_missing_mailbox.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Login workflow when a mailbox is not provided. 3 | ]] 4 | 5 | -- The user entered mailbox. 6 | mailbox = storage("get_digits", "digits") 7 | 8 | return 9 | { 10 | { 11 | action = "get_digits", 12 | min_digits = profile.mailbox_min_digits, 13 | max_digits = profile.mailbox_max_digits, 14 | audio_files = "phrase:get_mailbox_number", 15 | bad_input = "", 16 | timeout = profile.user_input_timeout, 17 | }, 18 | { 19 | action = "set_storage", 20 | storage_area = "login_settings", 21 | data = { 22 | mailbox_number = mailbox, 23 | login_type = "missing_mailbox", 24 | }, 25 | }, 26 | { 27 | action = "call_sequence", 28 | sequence = "validate_mailbox_login", 29 | }, 30 | } 31 | 32 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/play_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play message and all message-related annoucements for a message. 3 | ]] 4 | 5 | -- Message data. 6 | message_count = storage("message", "__count") 7 | 8 | return 9 | { 10 | { 11 | action = "call_sequence", 12 | sequence = "sub:play_message_number", 13 | }, 14 | { 15 | action = "call_sequence", 16 | sequence = "sub:play_cid_envelope", 17 | }, 18 | { 19 | action = "call_sequence", 20 | sequence = "sub:play_message", 21 | }, 22 | --[[ 23 | --This section may be used later for auto-advancing messages. 24 | { 25 | action = "counter", 26 | storage_key = "message_number", 27 | increment = 1, 28 | }, 29 | { 30 | action = "call_sequence", 31 | sequence = "play_messages", 32 | }, 33 | ]] 34 | } 35 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/mailbox_login_incorrect_missing_mailbox.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Incorrect login workflow when the mailbox is not provided. 3 | ]] 4 | 5 | -- User's mailbox number input. 6 | mailbox = storage("get_digits", "digits") 7 | 8 | return 9 | { 10 | -- Ask for the mailbox again. 11 | { 12 | action = "get_digits", 13 | min_digits = profile.mailbox_min_digits, 14 | max_digits = profile.mailbox_max_digits, 15 | audio_files = "phrase:login_incorrect_mailbox", 16 | bad_input = "", 17 | timeout = profile.user_input_timeout, 18 | }, 19 | { 20 | action = "set_storage", 21 | storage_area = "login_settings", 22 | data = { 23 | mailbox_number = mailbox, 24 | }, 25 | }, 26 | { 27 | action = "call_sequence", 28 | sequence = "validate_mailbox_login", 29 | }, 30 | } 31 | 32 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/next_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Workflow for going to the next message in a folder. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | message_count = storage("message", "__count") 8 | 9 | return 10 | { 11 | -- If we're on the last message, run no more messages sequence. 12 | { 13 | action = "conditional", 14 | value = message_number, 15 | compare_to = message_count, 16 | if_true = "no_more_messages message_options", 17 | }, 18 | -- Otherwise, increment the message counter and send back to the message 19 | -- playing sequence. 20 | { 21 | action = "counter", 22 | storage_key = "message_number", 23 | increment = 1, 24 | }, 25 | { 26 | action = "call_sequence", 27 | sequence = "play_messages", 28 | }, 29 | } 30 | 31 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/listen_to_greeting.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Listen to a recorded greeting, with options for accepting and re-recording. 3 | ]] 4 | 5 | mailbox = storage("login_settings", "mailbox_number") 6 | mailbox_directory = profile.mailboxes_dir .. "/" .. mailbox 7 | 8 | -- The temporary greeting to listen to. 9 | greeting = args(1) 10 | greeting_filename = mailbox_directory .. "/" .. greeting .. ".tmp.wav" 11 | 12 | return 13 | { 14 | { 15 | action = "play", 16 | file = greeting_filename, 17 | keys = { 18 | ["1"] = "accept_greeting " .. greeting, 19 | ["2"] = "listen_to_greeting " .. greeting, 20 | ["3"] = "record_greeting " .. greeting, 21 | ["#"] = ":break", 22 | }, 23 | }, 24 | { 25 | action = "call_sequence", 26 | sequence = "record_greeting_confirm " .. greeting, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/record_greeting_confirm.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play a menu allowing the caller to accept, listen to, or re-record a 3 | greeting. 4 | ]] 5 | 6 | -- The name of the recorded greeting. 7 | greeting = args(1) 8 | 9 | -- Mailbox info. 10 | mailbox = storage("login_settings", "mailbox_number") 11 | mailbox_directory = profile.mailboxes_dir .. "/" .. mailbox 12 | 13 | return 14 | { 15 | { 16 | action = "play_phrase", 17 | phrase = "greeting_options", 18 | repetitions = profile.menu_repetitions, 19 | wait = profile.menu_replay_wait, 20 | keys = { 21 | ["1"] = "accept_greeting " .. greeting, 22 | ["2"] = "listen_to_greeting " .. greeting, 23 | ["3"] = "record_greeting " .. greeting, 24 | }, 25 | }, 26 | { 27 | action = "call_sequence", 28 | sequence = "exit", 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/callback.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Menu for getting the method of collecting the number to call. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | caller_id_number = storage("message", "caller_id_number_" .. message_number) 8 | 9 | outdial_extension = storage("mailbox_settings", "outdial_extension") 10 | 11 | return 12 | { 13 | { 14 | action = "play_phrase", 15 | phrase = "callback", 16 | phrase_arguments = caller_id_number, 17 | keys = { 18 | ["1"] = "callback_set_number", 19 | ["2"] = "outdial " .. outdial_extension .. ",message_options,collect", 20 | ["*"] = "message_options", 21 | }, 22 | repetitions = profile.menu_repetitions, 23 | wait = profile.menu_replay_wait, 24 | }, 25 | { 26 | action = "call_sequence", 27 | sequence = "exit" 28 | }, 29 | } 30 | 31 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/load_new_old_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Initial load of new and old messages, then send to main menu. 3 | ]] 4 | 5 | timezone = storage("mailbox_settings", "default_timezone") 6 | default_language = storage("mailbox_settings", "default_language") 7 | 8 | return 9 | { 10 | -- Set the timezone channel variable to the mailbox's timezone, so the say 11 | -- applications will say date/time correctly. 12 | { 13 | action = "set_variable", 14 | data = { 15 | timezone = timezone, 16 | default_language = default_language, 17 | }, 18 | }, 19 | { 20 | action = "call_sequence", 21 | sequence = "sub:get_messages 0,new", 22 | }, 23 | { 24 | action = "call_sequence", 25 | sequence = "sub:get_messages 1,old", 26 | }, 27 | { 28 | action = "call_sequence", 29 | sequence = "main_menu", 30 | }, 31 | } 32 | 33 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/message_deleted_undeleted.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play a message deleted/undeleted announcement. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | deleted = storage("message", "deleted_" .. message_number) 8 | 9 | return 10 | { 11 | { 12 | action = "play_phrase", 13 | phrase = "message_deleted_undeleted", 14 | phrase_arguments = deleted, 15 | keys = { 16 | ["2"] = "change_folders", 17 | ["3"] = "advanced_options", 18 | ["4"] = "prev_message", 19 | ["5"] = "repeat_message", 20 | ["6"] = "next_message", 21 | ["7"] = "delete_undelete_message", 22 | ["8"] = "forward_message_menu", 23 | ["9"] = "save_message", 24 | ["*"] = "help", 25 | ["#"] = "exit", 26 | }, 27 | }, 28 | { 29 | action = "call_sequence", 30 | sequence = "post_command", 31 | }, 32 | } 33 | 34 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/delete_undelete_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Handle deleting and undeleting messages. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | deleted_key = "deleted_" .. message_number 8 | deleted = storage("message", deleted_key) 9 | 10 | -- Flip the state. 11 | if deleted == "0" then 12 | deleted = "1" 13 | else 14 | deleted = "0" 15 | end 16 | 17 | return 18 | { 19 | { 20 | action = "call_sequence", 21 | sequence = "sub:update_message_deleted " .. deleted, 22 | }, 23 | -- Update the local reference so it can be checked again for reversal in the 24 | -- same session. 25 | { 26 | action = "set_storage", 27 | storage_area = "message", 28 | data = { 29 | [deleted_key] = deleted, 30 | }, 31 | }, 32 | { 33 | action = "call_sequence", 34 | sequence = "message_deleted_undeleted", 35 | }, 36 | } 37 | 38 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/provision_mailbox.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Provision a mailbox. 3 | ]] 4 | 5 | -- Mailbox info. 6 | mailbox = args(1) 7 | domain = args(2) 8 | 9 | return 10 | { 11 | -- Try to create the domain directory since it may not be created yet. 12 | { 13 | action = "create_directory", 14 | directory = profile.voicemail_dir .. "/" .. domain, 15 | }, 16 | -- Create the mailbox directory. 17 | { 18 | action = "create_directory", 19 | directory = profile.voicemail_dir .. "/" .. domain .. "/" .. mailbox, 20 | }, 21 | -- Mark the mailbox as provisioned. 22 | { 23 | action = "data_update", 24 | handler = "odbc", 25 | config = profile.db_config_mailbox, 26 | fields = { 27 | mailbox_provisioned = "yes", 28 | }, 29 | filters = { 30 | mailbox = mailbox, 31 | domain = domain, 32 | }, 33 | update_type = "update", 34 | }, 35 | } 36 | 37 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/message_saved.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play a message saved announcement. 3 | ]] 4 | 5 | -- The folder the new message was saved to. 6 | folder = args(1) 7 | -- The message number that's being saved. 8 | message_number = storage("counter", "message_number") 9 | 10 | return 11 | { 12 | { 13 | action = "play_phrase", 14 | phrase = "message_saved", 15 | phrase_arguments = message_number .. ":" .. folder, 16 | keys = { 17 | ["2"] = "change_folders", 18 | ["3"] = "advanced_options", 19 | ["4"] = "prev_message", 20 | ["5"] = "repeat_message", 21 | ["6"] = "next_message", 22 | ["7"] = "delete_undelete_message", 23 | ["8"] = "forward_message_menu", 24 | ["9"] = "save_message", 25 | ["*"] = "help", 26 | ["#"] = "exit", 27 | }, 28 | }, 29 | { 30 | action = "call_sequence", 31 | sequence = "post_command", 32 | }, 33 | } 34 | 35 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/accept_greeting.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Accept a recorded greeting. 3 | ]] 4 | 5 | mailbox = storage("login_settings", "mailbox_number") 6 | mailbox_directory = profile.mailboxes_dir .. "/" .. mailbox 7 | 8 | -- Have we set up this mailbox yet? 9 | mailbox_setup_complete = storage("mailbox_settings", "mailbox_setup_complete") 10 | 11 | greeting = args(1) 12 | greeting_tmp = mailbox_directory .. "/" .. greeting .. ".tmp.wav" 13 | greeting_new = mailbox_directory .. "/" .. greeting .. ".wav" 14 | 15 | return 16 | { 17 | { 18 | action = "move_file", 19 | source = greeting_tmp, 20 | destination = greeting_new, 21 | }, 22 | { 23 | action = "play_phrase", 24 | phrase = "greeting_saved", 25 | }, 26 | { 27 | action = "conditional", 28 | value = mailbox_setup_complete, 29 | compare_to = "no", 30 | comparison = "equal", 31 | if_false = "mailbox_options", 32 | }, 33 | } 34 | 35 | -------------------------------------------------------------------------------- /profiles/socket/sequences/clean_deleted_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Cleans messages marked as deleted. 3 | ]] 4 | 5 | return 6 | { 7 | -- Load file information for all message files that are to be removed. 8 | { 9 | action = "data_load", 10 | handler = "odbc", 11 | config = profile.db_config_message, 12 | fields = { 13 | "mailbox", 14 | "domain", 15 | "recording", 16 | }, 17 | filters = { 18 | __deleted = 1, 19 | }, 20 | multiple = true, 21 | storage_area = "message_files_to_remove", 22 | }, 23 | -- Delete the database rows for deleted messages. 24 | { 25 | action = "data_delete", 26 | handler = "odbc", 27 | config = profile.db_config_message, 28 | filters = { 29 | __deleted = 1, 30 | }, 31 | }, 32 | -- Clean up the message files. 33 | { 34 | action = "call_sequence", 35 | sequence = "remove_deleted_message_files", 36 | }, 37 | } 38 | 39 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/login_have_mailbox.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Login workflow when mailbox is provided. 3 | ]] 4 | 5 | mailbox = args(1) 6 | login_without_password = variable("voicemail_login_without_password") 7 | 8 | return 9 | { 10 | -- Create a new navigation stack so we can easily return here if login 11 | -- validation fails. 12 | { 13 | action = "add_to_stack", 14 | }, 15 | { 16 | action = "set_storage", 17 | storage_area = "login_settings", 18 | data = { 19 | mailbox_number = mailbox, 20 | login_type = "have_mailbox", 21 | }, 22 | }, 23 | -- If the special 'voicemail_login_without_password' channel variable is set, 24 | -- then skip password validation. 25 | { 26 | action = "conditional", 27 | value = login_without_password, 28 | compare_to = "", 29 | comparison = "equal", 30 | if_true = "validate_mailbox_login", 31 | if_false = "login_without_password", 32 | }, 33 | } 34 | 35 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/mailbox_login_incorrect.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Bookkeeping/redirection for failed login attempts. 3 | ]] 4 | 5 | -- The type of login that failed (mailbox/no mailbox). 6 | login_type = storage("login_settings", "login_type") 7 | 8 | return 9 | { 10 | -- Keep track of how many login attempts have happened, and disconnect the 11 | -- user after 3 failures. 12 | { 13 | action = "counter", 14 | storage_key = "failed_login_counter", 15 | increment = 1, 16 | compare_to = profile.max_login_attempts, 17 | if_equal = "mailbox_login_failed", 18 | }, 19 | -- Redirect to the appropriate incorrect login workflow based on the login 20 | -- type. 21 | { 22 | action = "conditional", 23 | value = login_type, 24 | compare_to = "have_mailbox", 25 | comparison = "equal", 26 | if_true = "mailbox_login_incorrect_have_mailbox", 27 | if_false = "mailbox_login_incorrect_missing_mailbox", 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/update_message_folder.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Update the folder a message lives in. 3 | ]] 4 | 5 | -- The folder value to update the message to. 6 | folder = args(1) 7 | -- Is this an explicit save or an auto-save? 8 | operation = args(2) 9 | 10 | -- Message data. 11 | message_number = storage("counter", "message_number") 12 | message_id = storage("message", "id_" .. message_number) 13 | 14 | 15 | return 16 | { 17 | { 18 | action = "data_update", 19 | handler = "odbc", 20 | config = profile.db_config_message, 21 | fields = { 22 | __folder = folder, 23 | }, 24 | filters = { 25 | __id = message_id, 26 | }, 27 | update_type = "update", 28 | }, 29 | -- If it's an explicit save, call the message saved sequence. 30 | { 31 | action = "conditional", 32 | value = operation, 33 | compare_to = "save", 34 | comparison = "equal", 35 | if_true = "message_saved " .. folder, 36 | }, 37 | } 38 | 39 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/operator_transfer_prepare.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Ask the caller to save their message or hold for an operator. 3 | ]] 4 | 5 | -- Key map for transferring. 6 | transfer_keys = { 7 | ["1"] = "caller_save_recorded_message transfer_to_operator", 8 | } 9 | 10 | return 11 | { 12 | -- Ask to accept message. 13 | { 14 | action = "play_phrase", 15 | phrase = "accept_recording_or_hold", 16 | keys = transfer_keys, 17 | }, 18 | -- Wait for response. 19 | { 20 | action = "wait", 21 | milliseconds = profile.menu_replay_wait, 22 | keys = transfer_keys, 23 | }, 24 | -- No response, so delete the message and inform the user. 25 | { 26 | action = "call_sequence", 27 | sequence = "sub:cleanup_temp_recording", 28 | }, 29 | { 30 | action = "play_phrase", 31 | phrase = "message_deleted_undeleted", 32 | phrase_arguments = "1", 33 | }, 34 | { 35 | action = "call_sequence", 36 | sequence = "transfer_to_operator", 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /profiles/demos/sequences/speech_to_text.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Records a sound file, then converts the speech to text -- with console 3 | debugging on the result will be printed to the console. 4 | 5 | The recording is limited to 10 seconds. 6 | ]] 7 | 8 | file_to_transcribe = "speech_to_text_demo.wav" 9 | 10 | -- Set up initial recording keys. 11 | record_keys = { 12 | ["#"] = ":break", 13 | } 14 | 15 | return 16 | { 17 | { 18 | action = "wait", 19 | milliseconds = 500, 20 | }, 21 | { 22 | action = "record", 23 | location = profile.temp_recording_dir, 24 | filename = file_to_transcribe, 25 | pre_record_sound = "phrase:beep", 26 | max_length = 10, 27 | silence_secs = 2, 28 | keys = record_keys, 29 | }, 30 | { 31 | action = "play_phrase", 32 | phrase = "thank_you", 33 | keys = record_keys, 34 | }, 35 | { 36 | action = "speech_to_text_from_file", 37 | filepath = profile.temp_recording_dir .. "/" .. file_to_transcribe, 38 | }, 39 | } 40 | 41 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/main_greeting_prepare_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Prepares a recorded message from the main greeting for saving. 3 | ]] 4 | 5 | -- Message data. 6 | caller_id_number = variable("caller_id_number") 7 | caller_id_name = variable("caller_id_name") 8 | timestamp = storage("record", "last_recording_timestamp") 9 | duration = storage("record", "last_recording_duration") 10 | recording_name = storage("record", "last_recording_name") 11 | 12 | return 13 | { 14 | { 15 | action = "set_storage", 16 | storage_area = "message_info", 17 | data = { 18 | mailbox = profile.mailbox, 19 | domain = profile.domain, 20 | caller_id_number = caller_id_number, 21 | caller_id_name = caller_id_name, 22 | caller_domain = profile.caller_domain, 23 | timestamp = timestamp, 24 | duration = duration, 25 | recording_name = recording_name, 26 | }, 27 | }, 28 | { 29 | action = "call_sequence", 30 | sequence = "check_for_recorded_message", 31 | }, 32 | } 33 | 34 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/new_user_walkthrough.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Walkthrough for new users. 3 | Prompt them to change their password and record their greet file. 4 | ]] 5 | 6 | mailbox = storage("login_settings", "mailbox_number") 7 | 8 | return 9 | { 10 | { 11 | action = "play_phrase", 12 | phrase = "mailbox_setup", 13 | }, 14 | { 15 | action = "call_sequence", 16 | sequence = "sub:change_password", 17 | }, 18 | { 19 | action = "call_sequence", 20 | sequence = "sub:record_greeting greet", 21 | }, 22 | -- Update the user's mailbox_setup_complete flag. 23 | { 24 | action = "call_sequence", 25 | sequence = "sub:mailbox_setup " .. mailbox .. "," .. profile.domain, 26 | }, 27 | -- Reload the mailbox settings. 28 | { 29 | action = "call_sequence", 30 | sequence = "sub:load_mailbox_settings " .. mailbox .. "," .. profile.domain .. ",mailbox_settings", 31 | }, 32 | { 33 | action = "call_sequence", 34 | sequence = "load_new_old_messages", 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/caller_rerecord_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Set up for re-recording a message. 3 | ]] 4 | 5 | operator_extension = storage("mailbox_settings", "operator_extension") 6 | 7 | greeting_keys = { 8 | ["#"] = ":break", 9 | } 10 | 11 | -- If an operator extension is allowed on record, then add that to the menu 12 | -- options, and pass that data along to the record sequence. 13 | operator_on_record = "" 14 | if operator_extension ~= "" then 15 | greeting_keys["0"] = "transfer_to_operator" 16 | operator_on_record = "operator" 17 | end 18 | 19 | return 20 | { 21 | -- Get rid of the old recording first so it's not saved if the caller hangs 22 | -- up here. 23 | { 24 | action = "call_sequence", 25 | sequence = "sub:cleanup_temp_recording", 26 | }, 27 | { 28 | action = "play_phrase", 29 | phrase = "default_greeting", 30 | keys = greeting_keys, 31 | }, 32 | { 33 | action = "call_sequence", 34 | sequence = "record_message " .. operator_on_record, 35 | }, 36 | } 37 | 38 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/get_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Load all messages from the specified folder into the specified storage area. 3 | ]] 4 | 5 | -- The numeric folder identifier found in the database. 6 | folder = args(1) 7 | -- Special type suffix for initial load of new/old messages, empty otherwise. 8 | message_type = args(2) 9 | 10 | mailbox = storage("login_settings", "mailbox_number") 11 | 12 | return 13 | { 14 | { 15 | action = "data_load", 16 | handler = "odbc", 17 | config = profile.db_config_message, 18 | filters = { 19 | mailbox = mailbox, 20 | domain = profile.domain, 21 | __folder = folder, 22 | __deleted = 0, 23 | }, 24 | fields = { 25 | "__id", 26 | "caller_id_number", 27 | "caller_id_name", 28 | "caller_domain", 29 | "__timestamp", 30 | "__duration", 31 | "__deleted", 32 | "recording", 33 | }, 34 | storage_area = "message" .. message_type, 35 | multiple = true, 36 | sort = "timestamp", 37 | }, 38 | } 39 | 40 | -------------------------------------------------------------------------------- /scripts/compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script will byte-compile all code files listed in 'jester/compile.txt'. 4 | # By default this would be all non-config and non-sequence code. 5 | # 6 | # The script takes one argument, the destination folder to write the files to. 7 | # It first makes a complete copy of the entire jester folder to that new 8 | # folder, then individually byte-compiles files that are listed in 9 | # 'jester/compile.txt', writing them to the appropriate location in the 10 | # specified directory. 11 | # 12 | # You must be in the main jester directory when you execute the script. 13 | # 14 | # So, if you wanted the byte-compiled version of the code at 15 | # '/tmp/jester.compiled', you would cd into the main jester directory, and run: 16 | # 17 | # ./scripts/compile.sh "/tmp/jester.compiled" 18 | 19 | dest_dir=$1 20 | 21 | mkdir -p $dest_dir 22 | cp -a . $dest_dir 23 | 24 | for file in `cat scripts/compile.txt` 25 | do 26 | echo "byte-compiling $file to $dest_dir/$file" 27 | luac -o $dest_dir/$file $file 28 | done 29 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/record_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Records a message. 3 | ]] 4 | 5 | -- Is the operator option enabled? 6 | operator = args(1) 7 | 8 | -- Set up initial recording keys. 9 | record_keys = { 10 | ["#"] = ":break", 11 | } 12 | 13 | -- Add in the operator key action if it's enabled. 14 | if operator == "operator" then 15 | record_keys["0"] = "operator_transfer_prepare" 16 | end 17 | 18 | return 19 | { 20 | { 21 | action = "wait", 22 | milliseconds = 500, 23 | }, 24 | { 25 | action = "record", 26 | location = profile.temp_recording_dir, 27 | pre_record_sound = "phrase:beep", 28 | max_length = profile.max_message_length, 29 | silence_secs = profile.recording_silence_end, 30 | silence_threshold = profile.recording_silence_threshold, 31 | keys = record_keys, 32 | }, 33 | { 34 | action = "play_phrase", 35 | phrase = "thank_you", 36 | keys = record_keys, 37 | }, 38 | { 39 | action = "call_sequence", 40 | sequence = "review_message " .. operator, 41 | }, 42 | } 43 | 44 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/caller_save_recorded_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Explicitely save a recorded message, and send the caller to the next 3 | appropriate sequence. 4 | ]] 5 | 6 | next_sequence = args(1) 7 | -- This will always be empty unless a person is replying to a message. 8 | is_reply = storage("send_reply_info", "mailbox") 9 | 10 | -- Message replies have a different workflow, so set that up here if necessary. 11 | if is_reply ~= "" then 12 | prepare_message = "send_reply_prepare_message" 13 | next_sequence = "send_reply_save_message" 14 | else 15 | prepare_message = "main_greeting_prepare_message" 16 | end 17 | 18 | return 19 | { 20 | -- The prepare sequence calls the save sequence. 21 | { 22 | action = "call_sequence", 23 | sequence = "sub:" .. prepare_message, 24 | }, 25 | { 26 | action = "play_phrase", 27 | phrase = "greeting_saved", 28 | }, 29 | { 30 | action = "wait", 31 | milliseconds = 500, 32 | }, 33 | { 34 | action = "call_sequence", 35 | sequence = next_sequence, 36 | }, 37 | } 38 | 39 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/caller_playback_recorded_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play back the recorded message, and offer a menu of message actions. 3 | ]] 4 | 5 | operator_extension = storage("mailbox_settings", "operator_extension") 6 | 7 | recording_name = storage("record", "last_recording_name") 8 | 9 | playback_keys = { 10 | ["1"] = "caller_save_recorded_message exit", 11 | ["2"] = "caller_playback_recorded_message", 12 | ["3"] = "caller_rerecord_message", 13 | } 14 | 15 | -- If there's an available operator extension, then include it in the options 16 | -- and pass that data along to the review sequence. 17 | operator_on_review = "" 18 | if operator_extension ~= "" then 19 | playback_keys["0"] = "transfer_to_operator" 20 | operator_on_review = "operator" 21 | end 22 | 23 | return 24 | { 25 | { 26 | action = "play", 27 | file = profile.temp_recording_dir .. "/" .. recording_name, 28 | keys = playback_keys, 29 | }, 30 | { 31 | action = "call_sequence", 32 | sequence = "review_message " .. operator_on_review, 33 | }, 34 | } 35 | 36 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/forward_message_prepend.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Record and prepend a custom message to a forward message. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | recording_name = storage("message", "recording_" .. message_number) 8 | prepend_recording_name = storage("record", "last_recording_name") 9 | 10 | return 11 | { 12 | { 13 | action = "wait", 14 | milliseconds = 500, 15 | }, 16 | { 17 | action = "record", 18 | location = profile.temp_recording_dir, 19 | pre_record_sound = "phrase:beep", 20 | max_length = profile.max_message_length, 21 | silence_secs = profile.recording_silence_end, 22 | silence_threshold = profile.recording_silence_threshold, 23 | keys = { 24 | ["#"] = ":break", 25 | }, 26 | }, 27 | 28 | { 29 | action = "record_merge", 30 | base_file = profile.temp_recording_dir .. "/" .. recording_name, 31 | merge_file = profile.temp_recording_dir .. "/" .. prepend_recording_name, 32 | merge_type = "prepend", 33 | }, 34 | } 35 | 36 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/send_reply_prepare_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Prepare a message reply for saving. 3 | ]] 4 | 5 | -- Message data. 6 | mailbox = storage("send_reply_info", "mailbox") 7 | domain = storage("send_reply_info", "domain") 8 | timestamp = storage("record", "last_recording_timestamp") 9 | duration = storage("record", "last_recording_duration") 10 | recording_name = storage("record", "last_recording_name") 11 | 12 | return 13 | { 14 | { 15 | action = "set_storage", 16 | storage_area = "message_info", 17 | data = { 18 | mailbox = mailbox, 19 | domain = domain, 20 | caller_id_number = profile.mailbox, 21 | -- TODO: Comedian mail doesn't CID name for replies, but perhaps 22 | -- there's a way it can be done on FreeSWITCH? 23 | caller_id_name = "", 24 | caller_domain = profile.domain, 25 | timestamp = timestamp, 26 | duration = duration, 27 | recording_name = recording_name, 28 | }, 29 | }, 30 | { 31 | action = "call_sequence", 32 | sequence = "check_for_recorded_message", 33 | }, 34 | } 35 | 36 | -------------------------------------------------------------------------------- /spec/http_mock.lua: -------------------------------------------------------------------------------- 1 | local ltn12 = require "ltn12" 2 | local cjson = require "cjson" 3 | 4 | local _M = {} 5 | 6 | function _M.new(self, responses) 7 | local mock = {} 8 | mock.count = 0 9 | mock.responses = responses[1] and responses or {responses} 10 | setmetatable(mock, self) 11 | self.__index = self 12 | return mock 13 | end 14 | 15 | function _M:get_handler() 16 | return { 17 | request = function(data) 18 | if self.count < #self.responses then 19 | self.count = self.count + 1 20 | end 21 | local response = self.responses[self.count] 22 | local response_string = type(response.data) == "table" and cjson.encode(response.data) or response.data 23 | ltn12.pump.all(ltn12.source.string(response_string), data.sink) 24 | local body = response.body or "" 25 | local status_code = response.status_code or 200 26 | local headers = response.headers or {} 27 | local status_description = response.status_description or "OK" 28 | return body, status_code, headers, status_description 29 | end, 30 | } 31 | end 32 | 33 | return _M 34 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/send_reply.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Set up sending a reply to a message. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | -- The mailbox to save to is the caller ID of the message. 8 | mailbox = storage("message", "caller_id_number_" .. message_number) 9 | -- The domain to save to is stored with the original message. 10 | domain = storage("message", "caller_domain_" .. message_number) 11 | 12 | return 13 | { 14 | -- Set up the reply information for other sequences to use. 15 | { 16 | action = "set_storage", 17 | storage_area = "send_reply_info", 18 | data = { 19 | mailbox = mailbox, 20 | domain = domain, 21 | }, 22 | }, 23 | { 24 | action = "play_phrase", 25 | phrase = "default_greeting", 26 | keys = { 27 | ["#"] = ":break", 28 | }, 29 | }, 30 | -- Register sequence in the exit loop to save the message in case the caller 31 | -- doesn't explicitly save it. 32 | { 33 | action = "exit_sequence", 34 | sequence = "send_reply_prepare_message", 35 | }, 36 | { 37 | action = "call_sequence", 38 | sequence = "record_message", 39 | }, 40 | } 41 | 42 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/save_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play options to the user for saving a message to a folder. 3 | ]] 4 | 5 | return 6 | { 7 | -- Place key map in the main sequence since it's used for all actions. 8 | keys = { 9 | ["0"] = "update_message_folder 0,save", 10 | ["1"] = "update_message_folder 1,save", 11 | ["2"] = "update_message_folder 2,save", 12 | ["3"] = "update_message_folder 3,save", 13 | ["4"] = "update_message_folder 4,save", 14 | ["#"] = "help" 15 | }, 16 | { 17 | action = "play_phrase", 18 | phrase = "save_to_folder", 19 | }, 20 | { 21 | action = "play_keys", 22 | key_announcements = { 23 | ["0"] = "new_messages", 24 | ["1"] = "old_messages", 25 | ["2"] = "work_messages", 26 | ["3"] = "family_messages", 27 | ["4"] = "friends_messages", 28 | ["#"] = "pound_cancel", 29 | }, 30 | order = { 31 | "0", 32 | "1", 33 | "2", 34 | "3", 35 | "4", 36 | "#", 37 | }, 38 | repetitions = profile.menu_repetitions, 39 | wait = profile.menu_replay_wait, 40 | }, 41 | { 42 | action = "call_sequence", 43 | sequence = "exit", 44 | }, 45 | } 46 | 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021, Chad Phillips 2 | 3 | Original copyright and sponsorship: 2010, Star2Star Communications, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/remove_mailbox_deleted_message_files.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Removes message files from a mailbox that have been deleted from the 3 | database. 4 | ]] 5 | 6 | mailbox = args(1) 7 | domain = args(2) 8 | 9 | -- Total number of files to remove. 10 | total_files = storage("message_files_to_remove", "__count") 11 | -- Which file we're currently on. 12 | file_count = storage("counter", "file_row") 13 | -- File info for the current file. 14 | recording = storage("message_files_to_remove", "recording_" .. file_count) 15 | file_to_remove = profile.voicemail_dir .. "/" .. domain .. "/" .. mailbox .. "/" .. recording 16 | 17 | return 18 | { 19 | -- Increment the group counter by one. If we're past the total files, 20 | -- then exit. 21 | { 22 | action = "counter", 23 | increment = 1, 24 | storage_key = "file_row", 25 | compare_to = total_files, 26 | if_greater = "none", 27 | }, 28 | { 29 | action = "delete_file", 30 | file = file_to_remove, 31 | }, 32 | -- Call the sequence again to trigger the next file deletion. 33 | { 34 | action = "call_sequence", 35 | sequence = "remove_mailbox_deleted_message_files " .. mailbox .. "," .. domain, 36 | }, 37 | } 38 | 39 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/advanced_options.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play advanced message options to the caller. 3 | ]] 4 | 5 | callback_extension = storage("mailbox_settings", "callback_extension") 6 | outdial_extension = storage("mailbox_settings", "outdial_extension") 7 | 8 | option_keys = { 9 | ["1"] = "send_reply_check", 10 | ["3"] = "play_cid_envelope advanced_options", 11 | ["*"] = "message_options", 12 | } 13 | 14 | callback = "N" 15 | outdial = "N" 16 | 17 | -- Add in the option to call back the calling party. 18 | if callback_extension ~= "" then 19 | option_keys["2"] = "callback" 20 | callback = "Y" 21 | end 22 | 23 | -- Add in the option to place calls to outside numbers. 24 | if outdial_extension ~= "" then 25 | option_keys["4"] = "outdial " .. outdial_extension .. ",message_options,collect" 26 | outdial = "Y" 27 | end 28 | 29 | return 30 | { 31 | { 32 | action = "play_phrase", 33 | phrase = "advanced_options_list", 34 | phrase_arguments = "Y:" .. callback .. ":Y:" .. outdial .. ":N", 35 | keys = option_keys, 36 | repetitions = profile.menu_repetitions, 37 | wait = profile.menu_replay_wait, 38 | }, 39 | { 40 | action = "call_sequence", 41 | sequence = "exit" 42 | }, 43 | } 44 | 45 | -------------------------------------------------------------------------------- /profiles/socket/sequences/remove_deleted_message_files.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Removes message files that have been deleted from the database. 3 | ]] 4 | 5 | -- Total number of files to remove. 6 | total_files = storage("message_files_to_remove", "__count") 7 | -- Which file we're currently on. 8 | file_count = storage("counter", "file_row") 9 | -- File info for the current file. 10 | mailbox = storage("message_files_to_remove", "mailbox_" .. file_count) 11 | domain = storage("message_files_to_remove", "domain_" .. file_count) 12 | recording = storage("message_files_to_remove", "recording_" .. file_count) 13 | file_to_remove = profile.voicemail_dir .. "/" .. domain .. "/" .. mailbox .. "/" .. recording 14 | 15 | return 16 | { 17 | -- Increment the group counter by one. If we're past the total files, 18 | -- then exit. 19 | { 20 | action = "counter", 21 | increment = 1, 22 | storage_key = "file_row", 23 | compare_to = total_files, 24 | if_greater = "exit", 25 | }, 26 | { 27 | action = "delete_file", 28 | file = file_to_remove, 29 | }, 30 | -- Call the sequence again to trigger the next file deletion. 31 | { 32 | action = "call_sequence", 33 | sequence = "remove_deleted_message_files", 34 | }, 35 | } 36 | 37 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/load_mailbox_settings.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Load mailbox settings for the specified mailbox. 3 | ]] 4 | 5 | mailbox = args(1) 6 | domain = args(2) 7 | -- The storage area to save the settings to. 8 | storage_area = args(3) 9 | 10 | return 11 | { 12 | { 13 | action = "data_load", 14 | handler = "odbc", 15 | config = profile.db_config_mailbox, 16 | filters = { 17 | mailbox = mailbox, 18 | domain = domain, 19 | }, 20 | -- Note that there are more columns in the mailbox table, but these 21 | -- columns are the only ones with support in the profile now. 22 | fields = { 23 | "mailbox", 24 | "domain", 25 | "password", 26 | "mailbox_setup_complete", 27 | "mailbox_provisioned", 28 | "default_language", 29 | "default_timezone", 30 | "email", 31 | "email_template", 32 | "email_messages", 33 | "play_caller_id", 34 | "play_envelope", 35 | "review_messages", 36 | "next_after_command", 37 | "temp_greeting_warn", 38 | "operator_extension", 39 | "callback_extension", 40 | "outdial_extension", 41 | "exit_extension", 42 | }, 43 | storage_area = storage_area, 44 | }, 45 | } 46 | 47 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/collect_extension.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Collect and extension from the user, validate that it's a valid extension, 3 | then pass to the appropriate next sequence. 4 | ]] 5 | 6 | -- Options for next sequence to call. 7 | valid_extension_sequence = args(1) 8 | invalid_extension_sequence = args(2) 9 | -- Collected extension. 10 | extension = storage("get_digits", "extension") 11 | -- Result of the mailbox load. 12 | loaded_mailbox = storage("mailbox_settings_message", "mailbox") 13 | 14 | return 15 | { 16 | { 17 | action = "get_digits", 18 | min_digits = profile.mailbox_min_digits, 19 | max_digits = profile.mailbox_max_digits, 20 | audio_files = "phrase:extension", 21 | bad_input = "", 22 | storage_key = "extension", 23 | timeout = profile.user_input_timeout, 24 | }, 25 | { 26 | action = "call_sequence", 27 | sequence = "sub:load_mailbox_settings " .. extension .. "," .. profile.domain .. ",mailbox_settings_message", 28 | }, 29 | { 30 | action = "conditional", 31 | value = loaded_mailbox, 32 | compare_to = "", 33 | comparison = "equal", 34 | if_false = valid_extension_sequence, 35 | if_true = "invalid_extension " .. invalid_extension_sequence, 36 | }, 37 | } 38 | 39 | -------------------------------------------------------------------------------- /scripts/compile.txt: -------------------------------------------------------------------------------- 1 | core.lua 2 | jester.lua 3 | socket.lua 4 | modules/core_actions/conf.lua 5 | modules/core_actions/core_actions.lua 6 | modules/couchdb/conf.lua 7 | modules/couchdb/couchdb.lua 8 | modules/data/conf.lua 9 | modules/data/data.lua 10 | modules/data/odbc.lua 11 | modules/dialplan_tools/conf.lua 12 | modules/dialplan_tools/dialplan_tools.lua 13 | modules/email/conf.lua 14 | modules/email/email.lua 15 | modules/event/conf.lua 16 | modules/event/event.lua 17 | modules/file/conf.lua 18 | modules/file/file.lua 19 | modules/format/conf.lua 20 | modules/format/format.lua 21 | modules/get_digits/conf.lua 22 | modules/get_digits/get_digits.lua 23 | modules/hangup/conf.lua 24 | modules/hangup/hangup.lua 25 | modules/log/conf.lua 26 | modules/log/log.lua 27 | modules/navigation/conf.lua 28 | modules/navigation/navigation.lua 29 | modules/play/conf.lua 30 | modules/play/play.lua 31 | modules/record/conf.lua 32 | modules/record/record.lua 33 | modules/service/conf.lua 34 | modules/service/service.lua 35 | modules/speech_to_text/conf.lua 36 | modules/speech_to_text/speech_to_text.lua 37 | modules/system/conf.lua 38 | modules/system/system.lua 39 | modules/tracker/conf.lua 40 | modules/tracker/tracker.lua 41 | support/file.lua 42 | support/string.lua 43 | support/table.lua 44 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/change_folders.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Menu for changing to another folder. 3 | ]] 4 | 5 | -- Folder data. 6 | current_folder = storage("message_settings", "current_folder") 7 | 8 | return 9 | { 10 | -- Place key map in the sequence space, since all actions use the same map. 11 | keys = { 12 | ["0"] = "set_current_folder 0", 13 | ["1"] = "set_current_folder 1", 14 | ["2"] = "set_current_folder 2", 15 | ["3"] = "set_current_folder 3", 16 | ["4"] = "set_current_folder 4", 17 | ["#"] = "set_current_folder " .. current_folder, 18 | }, 19 | { 20 | action = "play_phrase", 21 | phrase = "change_to_folder", 22 | }, 23 | { 24 | action = "play_keys", 25 | key_announcements = { 26 | ["0"] = "new_messages", 27 | ["1"] = "old_messages", 28 | ["2"] = "work_messages", 29 | ["3"] = "family_messages", 30 | ["4"] = "friends_messages", 31 | ["#"] = "pound_cancel", 32 | }, 33 | order = { 34 | "0", 35 | "1", 36 | "2", 37 | "3", 38 | "4", 39 | "#", 40 | }, 41 | repetitions = profile.menu_repetitions, 42 | wait = profile.menu_replay_wait, 43 | }, 44 | { 45 | action = "call_sequence", 46 | sequence = "exit", 47 | }, 48 | } 49 | 50 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/manage_temp_greeting.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Check for a temporary greeting, redirect to record it if not found, otherwise 3 | give the user options for recording/erasing it. 4 | ]] 5 | 6 | -- Mailbox info. 7 | mailbox = storage("login_settings", "mailbox_number") 8 | mailbox_directory = profile.mailboxes_dir .. "/" .. mailbox 9 | -- Result of the check for the temporary greeting. 10 | file_exists = storage("file", "file_exists") 11 | 12 | return 13 | { 14 | { 15 | action = "file_exists", 16 | file = mailbox_directory .. "/temp.wav", 17 | }, 18 | -- If no temporary greeting exists, redirect the user to record one. 19 | { 20 | action = "conditional", 21 | value = file_exists, 22 | compare_to = "false", 23 | comparison = "equal", 24 | if_true = "record_greeting temp", 25 | }, 26 | -- Otherwise, give them a menu to record/erase it. 27 | { 28 | action = "play_phrase", 29 | phrase = "temp_greeting_options", 30 | repetitions = profile.menu_repetitions, 31 | wait = profile.menu_replay_wait, 32 | keys = { 33 | ["1"] = "record_greeting temp", 34 | ["2"] = "erase_temp_greeting", 35 | }, 36 | }, 37 | { 38 | action = "call_sequence", 39 | sequence = "exit", 40 | }, 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/send_reply_check.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Verify that a message can be replied to. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | -- The mailbox to save to is the caller ID of the message. 8 | mailbox = storage("message", "caller_id_number_" .. message_number) 9 | -- The domain to save to is stored with the original message. 10 | domain = storage("message", "caller_domain_" .. message_number) 11 | -- Result of the attempt to load the reply to mailbox. 12 | loaded_mailbox = storage("mailbox_settings_message", "mailbox") 13 | 14 | return 15 | { 16 | -- Try to load the mailbox. 17 | { 18 | action = "call_sequence", 19 | sequence = "sub:load_mailbox_settings " .. mailbox .. "," .. domain .. ",mailbox_settings_message", 20 | }, 21 | -- If it's found, trigger the reply sequence. 22 | { 23 | action = "conditional", 24 | value = loaded_mailbox, 25 | compare_to = "", 26 | comparison = "equal", 27 | if_false = "send_reply", 28 | }, 29 | -- Otherwise, inform the user that they can't reply to this message. 30 | { 31 | action = "play_phrase", 32 | phrase = "no_mailbox", 33 | }, 34 | { 35 | action = "call_sequence", 36 | sequence = "message_options", 37 | }, 38 | } 39 | 40 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/outdial.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Optionally get an number to call, then transfer to the specified extension. 3 | ]] 4 | 5 | -- The extension to transfer to for the outdial. 6 | extension = args(1) 7 | -- The sequence to call if the operation is cancelled. 8 | cancel_sequence = args(2) 9 | -- Whether to collect the number or not. 10 | collect_outdial_number = args(3) 11 | outdial_number = storage("get_digits", "outdial_number") 12 | 13 | return 14 | { 15 | { 16 | action = "conditional", 17 | value = collect_outdial_number, 18 | compare_to = "collect", 19 | comparison = "equal", 20 | if_true = "sub:collect_outdial_number", 21 | }, 22 | { 23 | action = "conditional", 24 | value = outdial_number, 25 | compare_to = "*", 26 | comparison = "equal", 27 | if_true = cancel_sequence, 28 | }, 29 | { 30 | action = "play_phrase", 31 | phrase = "please_wait_while_connecting", 32 | }, 33 | -- Set the 'voicemail_outdial_number' channel variable so the receiving 34 | -- extension knows where to dial. 35 | { 36 | action = "set_variable", 37 | data = { 38 | voicemail_outdial_number = outdial_number, 39 | }, 40 | }, 41 | { 42 | action = "transfer", 43 | extension = extension, 44 | }, 45 | } 46 | 47 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/update_password.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Update the password for a mailbox. 3 | ]] 4 | 5 | -- Mailbox info. 6 | mailbox = storage("login_settings", "mailbox_number") 7 | -- The updated password. 8 | password = storage("get_digits", "new_password_1") 9 | 10 | -- Have we set up this mailbox yet? 11 | mailbox_setup_complete = storage("mailbox_settings", "mailbox_setup_complete") 12 | 13 | return 14 | { 15 | { 16 | action = "data_update", 17 | handler = "odbc", 18 | config = profile.db_config_mailbox, 19 | fields = { 20 | password = password, 21 | }, 22 | filters = { 23 | domain = profile.domain, 24 | mailbox = mailbox, 25 | }, 26 | update_type = "update", 27 | }, 28 | -- Fire a 'mailbox_updated' event, passing the new password. 29 | { 30 | action = "fire_event", 31 | event_type = "mailbox_updated", 32 | headers = { 33 | Mailbox = mailbox, 34 | Domain = domain, 35 | }, 36 | body = "password: " .. password, 37 | }, 38 | { 39 | action = "play_phrase", 40 | phrase = "password_updated", 41 | }, 42 | { 43 | action = "conditional", 44 | value = mailbox_setup_complete, 45 | compare_to = "no", 46 | comparison = "equal", 47 | if_false = "mailbox_options", 48 | }, 49 | } 50 | 51 | -------------------------------------------------------------------------------- /ldoc.custom.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | padding: 0; 5 | margin: 0; 6 | /* 7 | font-family: "Comic Sans MS", cursive, sans-serif; 8 | */ 9 | font-family: arial, helvetica, geneva, sans-serif; 10 | } 11 | 12 | body, 13 | #container, 14 | #main, 15 | #navigation, 16 | #content { 17 | background: #f5f5f5; 18 | } 19 | 20 | #content { 21 | background: no-repeat; 22 | background-image: url('../images/lua-freeswitch-logo.png'); 23 | background-position: right top; 24 | background-origin: content-box; 25 | } 26 | 27 | #content h1 { 28 | margin-top: 20px; 29 | } 30 | 31 | #navigation h1 { 32 | font-size: 2.9em; 33 | min-height: 50px; 34 | margin-right: 10px; 35 | background: no-repeat; 36 | background-image: url('../images/jester-logo.png'); 37 | background-position: right top; 38 | background-origin: content-box; 39 | } 40 | 41 | #navigation h2 { 42 | background: #cccccc; 43 | margin-right: 10px; 44 | border: 1px solid #cccccc; 45 | border-radius: 5px; 46 | } 47 | #content { 48 | height: 100%; 49 | } 50 | #about { 51 | display: none; 52 | } 53 | table.module_list td.name, 54 | table.module_list td.summary, 55 | table.function_list td.name, 56 | table.function_list td.summary { 57 | background: #dedede; 58 | border-color: gray; 59 | } 60 | a:link, a:visited { 61 | color: #133B6B; 62 | } 63 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/play_message_number.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play the message number. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | message_count = storage("message", "__count") 8 | 9 | -- Use the profile setting if there's only one total message. 10 | if message_count == 1 then 11 | location = profile.single_message_announcement 12 | else 13 | -- Check if we're on the first or last message in the folder, and use first 14 | -- and last announcements if necessary, otherwise use the message number. 15 | if message_number == 1 then 16 | location = "first" 17 | elseif message_number == message_count then 18 | location = "last" 19 | else 20 | location = message_number 21 | end 22 | end 23 | 24 | return 25 | { 26 | { 27 | action = "play_phrase", 28 | phrase = "message_number", 29 | phrase_arguments = location, 30 | keys = { 31 | ["1"] = "top:play_message", 32 | ["2"] = "top:change_folders", 33 | ["3"] = "top:advanced_options", 34 | ["4"] = "top:prev_message", 35 | ["5"] = "top:repeat_message", 36 | ["6"] = "top:next_message", 37 | ["7"] = "top:delete_undelete_message", 38 | ["8"] = "top:forward_message_menu", 39 | ["9"] = "top:save_message", 40 | ["*"] = "help", 41 | ["#"] = "exit", 42 | }, 43 | }, 44 | } 45 | 46 | -------------------------------------------------------------------------------- /config.ld: -------------------------------------------------------------------------------- 1 | project = "Jester" 2 | description = "Scripting toolkit for FreeSWITCH" 3 | --package = "jester" 4 | title = "Jester documentation" 5 | file = { 6 | -- Core. 7 | "./core.lua", 8 | "./conf.lua", 9 | -- Modules. 10 | "./modules/core_actions/init.lua", 11 | "./modules/couchdb/init.lua", 12 | "./modules/data/init.lua", 13 | "./modules/dialplan_tools/init.lua", 14 | "./modules/email/init.lua", 15 | "./modules/event/init.lua", 16 | "./modules/file/init.lua", 17 | "./modules/format/init.lua", 18 | "./modules/get_digits/init.lua", 19 | "./modules/hangup/init.lua", 20 | "./modules/log/init.lua", 21 | "./modules/navigation/init.lua", 22 | "./modules/play/init.lua", 23 | "./modules/record/init.lua", 24 | "./modules/service/init.lua", 25 | "./modules/speech_to_text/init.lua", 26 | "./modules/speech_to_text/watson.lua", 27 | "./modules/system/init.lua", 28 | "./modules/tracker/init.lua", 29 | -- Scripts. 30 | "./socket.lua", 31 | "./scripts/extract_actions.lua", 32 | "./action_map.lua", 33 | } 34 | dir = "./documentation" 35 | topics = { 36 | "./doc" 37 | } 38 | format = "markdown" 39 | all = false 40 | examples = "./examples" 41 | wrap = true 42 | no_space_before_args = true 43 | style = "!fixed" 44 | new_type("action", "Actions") 45 | new_type("handler", "Handlers") 46 | custom_css = "ldoc.custom.css" 47 | 48 | -- vi: ft=lua 49 | -------------------------------------------------------------------------------- /profiles/demos/conf.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Profile configuration file. All variables put in here will be processed once 3 | during the jester bootstrap. 4 | 5 | If you have a variable foo that you want to have a value of "bar", do: 6 | foo = "bar" 7 | 8 | Array/record syntax is like this: 9 | foo = { bar = "baz", bing = "bong" } 10 | 11 | Variables from the main configuration may be used in values, by accessing 12 | them through the global. namespace. 13 | 14 | Channel variables may be used in values, by accessing them through the 15 | variable("") function. 16 | 17 | Storage variables may be used in values, by accessing them through the 18 | storage("") function. 19 | 20 | Initial arguments may be used in values, by accessing them through the 21 | args() function. 22 | ]] 23 | 24 | --[[ 25 | Everything in this section should not be edited unless you know what you are 26 | doing! 27 | ]] 28 | 29 | -- Overrides the global debug configuration for this profile only. 30 | debug = true 31 | 32 | -- Overrides the global sequence path for this profile only. 33 | sequence_path = global.profile_path .. "/demos/sequences" 34 | 35 | --[[ 36 | The sections below can be customized safely. 37 | ]] 38 | 39 | --[[ 40 | Directory paths. 41 | ]] 42 | 43 | -- The directory where recordings are stored temporarily while recording. 44 | temp_recording_dir = "/tmp" 45 | 46 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/play_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play a message. 3 | ]] 4 | 5 | -- Mailbox info. 6 | mailbox = storage("login_settings", "mailbox_number") 7 | mailbox_directory = profile.mailboxes_dir .. "/" .. mailbox 8 | 9 | -- Message data. 10 | message_number = storage("counter", "message_number") 11 | recording_name = storage("message", "recording_" .. message_number) 12 | 13 | -- Folder data. 14 | current_folder = storage("message_settings", "current_folder") 15 | 16 | return 17 | { 18 | -- New messages get automatically moved to old messages. 19 | { 20 | action = "conditional", 21 | value = current_folder, 22 | compare_to = "0", 23 | if_true = "sub:update_message_folder 1" 24 | }, 25 | { 26 | action = "play", 27 | file = mailbox_directory .. "/" .. recording_name, 28 | keys = { 29 | ["1"] = "top:play_first_message", 30 | ["2"] = ":seek:0", 31 | ["3"] = "top:advanced_options", 32 | ["4"] = "top:prev_message", 33 | ["5"] = "top:repeat_message", 34 | ["6"] = "top:next_message", 35 | ["7"] = "top:delete_undelete_message", 36 | ["8"] = "top:forward_message_menu", 37 | ["9"] = "top:save_message", 38 | ["0"] = ":pause", 39 | ["*"] = ":seek:-5000", 40 | ["#"] = ":seek:+1500", 41 | }, 42 | }, 43 | { 44 | action = "call_sequence", 45 | sequence = "sub:message_options", 46 | }, 47 | } 48 | 49 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/mailbox_options.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play mailbox options menu to the user. 3 | ]] 4 | 5 | -- Mailbox info. 6 | mailbox = storage("login_settings", "mailbox_number") 7 | mailbox_directory = profile.mailboxes_dir .. "/" .. mailbox 8 | 9 | -- Result of the check for the temporary greeting. 10 | file_exists = storage("file", "file_exists") 11 | -- Are we supposed to warn about a temporary greeting? 12 | temp_greeting_warn = storage("mailbox_settings", "temp_greeting_warn") 13 | warn = "" 14 | if temp_greeting_warn == "yes" and file_exists == "true" then 15 | warn = "true" 16 | end 17 | 18 | return 19 | { 20 | -- Check for the existence of a temporary greeting -- this result is passed 21 | -- into the phrase for announcing mailbox options. 22 | { 23 | action = "file_exists", 24 | file = mailbox_directory .. "/temp.wav", 25 | }, 26 | { 27 | action = "play_phrase", 28 | phrase = "mailbox_options", 29 | phrase_arguments = warn, 30 | repetitions = profile.menu_repetitions, 31 | wait = profile.menu_replay_wait, 32 | keys = { 33 | ["1"] = "record_greeting unavail", 34 | ["2"] = "record_greeting busy", 35 | ["3"] = "record_greeting greet", 36 | ["4"] = "manage_temp_greeting", 37 | ["5"] = "change_password", 38 | ["*"] = "main_menu", 39 | }, 40 | }, 41 | { 42 | action = "call_sequence", 43 | sequence = "exit", 44 | }, 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/remove_mailbox_deleted_messages.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Cleans messages from a mailbox marked as deleted. 3 | ]] 4 | 5 | mailbox = args(1) 6 | domain = args(2) 7 | 8 | -- Are there any messages to remove? 9 | messages_to_remove = storage("message_files_to_remove", "__count") 10 | 11 | return 12 | { 13 | -- Load file information for all message files that are to be removed. 14 | { 15 | action = "data_load", 16 | handler = "odbc", 17 | config = profile.db_config_message, 18 | fields = { 19 | "recording", 20 | }, 21 | filters = { 22 | mailbox = mailbox, 23 | domain = domain, 24 | __deleted = 1, 25 | }, 26 | multiple = true, 27 | storage_area = "message_files_to_remove", 28 | }, 29 | -- See if there are any messages to remove. 30 | { 31 | action = "conditional", 32 | value = messages_to_remove, 33 | compare_to = 0, 34 | comparison = "equal", 35 | if_true = "none", 36 | }, 37 | -- Delete the database rows for deleted messages. 38 | { 39 | action = "data_delete", 40 | handler = "odbc", 41 | config = profile.db_config_message, 42 | filters = { 43 | mailbox = mailbox, 44 | domain = domain, 45 | __deleted = 1, 46 | }, 47 | }, 48 | -- Clean up the message files. 49 | { 50 | action = "call_sequence", 51 | sequence = "remove_mailbox_deleted_message_files " .. mailbox .. "," .. domain, 52 | }, 53 | } 54 | 55 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/record_greeting.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Record a greeting. 3 | ]] 4 | 5 | -- The greeting name to record. 6 | greeting = args(1) 7 | 8 | -- Mailbox info. 9 | mailbox = storage("login_settings", "mailbox_number") 10 | mailbox_directory = profile.mailboxes_dir .. "/" .. mailbox 11 | mailbox_provisioned = storage("mailbox_settings", "mailbox_provisioned") 12 | 13 | return 14 | { 15 | -- Provision the mailbox if it's not provisioned yet. 16 | { 17 | action = "conditional", 18 | value = mailbox_provisioned, 19 | compare_to = "no", 20 | comparison = "equal", 21 | if_true = "sub:provision_mailbox " .. mailbox .. "," .. profile.domain, 22 | }, 23 | { 24 | action = "play_phrase", 25 | phrase = "record_greeting", 26 | phrase_arguments = greeting, 27 | keys = { 28 | ["#"] = ":break", 29 | }, 30 | }, 31 | { 32 | action = "wait", 33 | milliseconds = 500, 34 | }, 35 | -- Record to a temporary file. 36 | { 37 | action = "record", 38 | location = mailbox_directory, 39 | filename = greeting .. ".tmp.wav", 40 | pre_record_sound = "phrase:beep", 41 | max_length = profile.max_greeting_length, 42 | silence_secs = profile.recording_silence_end, 43 | silence_threshold = profile.recording_silence_threshold, 44 | keys = { 45 | ["#"] = ":break", 46 | }, 47 | }, 48 | { 49 | action = "call_sequence", 50 | sequence = "record_greeting_thank_you " .. greeting, 51 | }, 52 | } 53 | 54 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/check_for_recorded_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Check for a valid recorded message, and call the appropriate save sequence 3 | if one is found. 4 | ]] 5 | 6 | recording_name = storage("record", "last_recording_name") 7 | file_size = storage("file", "size") 8 | 9 | return 10 | { 11 | -- Make sure we have a recording name set, otherwise we're just checking to see 12 | -- if our temp_recording_dir exists (which it probably does). 13 | { 14 | action = "conditional", 15 | value = recording_name, 16 | compare_to = "", 17 | comparison = "equal", 18 | if_true = "none", 19 | }, 20 | { 21 | action = "file_exists", 22 | file = profile.temp_recording_dir .. "/" .. recording_name, 23 | if_false = "none", 24 | }, 25 | { 26 | action = "file_size", 27 | file = profile.temp_recording_dir .. "/" .. recording_name, 28 | }, 29 | -- Check our file size, if we're too small, bail out. 30 | { 31 | action = "conditional", 32 | value = file_size, 33 | compare_to = profile.minimum_recorded_file_size, 34 | comparison = "less_than", 35 | if_true = "none", 36 | }, 37 | -- Individual messages and message groups have different save workflows, so 38 | -- detect which one it is here. 39 | { 40 | action = "conditional", 41 | value = profile.message_group, 42 | compare_to = "", 43 | comparison = "equal", 44 | if_true = "save_individual_message", 45 | if_false = "load_message_group " .. profile.message_group, 46 | }, 47 | } 48 | 49 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/review_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play options to the user to save, playback, or re-record their message. 3 | ]] 4 | 5 | -- Is the operator extension enabled? 6 | operator = args(1) 7 | no_review_next_sequence = "exit" 8 | 9 | -- Message review option for the mailbox. 10 | review_messages = storage("mailbox_settings", "review_messages") 11 | 12 | -- This will always be empty unless a person is replying to a message. 13 | is_reply = storage("send_reply_info", "mailbox") 14 | -- If it's a reply, then send user back to the message options instead of 15 | -- hanging up on them. 16 | if is_reply ~= "" then 17 | no_review_next_sequence = "message_options" 18 | end 19 | 20 | -- Set up the initial review keys. 21 | review_keys = { 22 | ["1"] = "caller_save_recorded_message exit", 23 | ["2"] = "caller_playback_recorded_message", 24 | ["3"] = "caller_rerecord_message", 25 | } 26 | 27 | -- Add in the operator extension if it's enabled. 28 | if operator == "operator" then 29 | review_keys["0"] = "operator_transfer_prepare" 30 | end 31 | 32 | return 33 | { 34 | -- If message review isn't enabled, then exit the call. 35 | { 36 | action = "conditional", 37 | value = review_messages, 38 | compare_to = "no", 39 | comparison = "equal", 40 | if_true = no_review_next_sequence, 41 | }, 42 | { 43 | action = "play_phrase", 44 | phrase = "greeting_options", 45 | keys = review_keys, 46 | repetitions = profile.menu_repetitions, 47 | wait = profile.menu_replay_wait, 48 | }, 49 | { 50 | action = "call_sequence", 51 | sequence = "exit", 52 | }, 53 | } 54 | 55 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/change_password.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Collect a new password twice, validate they match, and call the update 3 | sequence as appropriate. 4 | ]] 5 | 6 | password_1 = storage("get_digits", "new_password_1") 7 | password_2 = storage("get_digits", "new_password_2") 8 | 9 | -- Have we set up this mailbox yet? 10 | mailbox_setup_complete = storage("mailbox_settings", "mailbox_setup_complete") 11 | 12 | return 13 | { 14 | { 15 | action = "get_digits", 16 | min_digits = profile.password_min_digits, 17 | max_digits = profile.password_max_digits, 18 | audio_files = "phrase:enter_new_password", 19 | bad_input = "", 20 | storage_key = "new_password_1", 21 | timeout = profile.user_input_timeout, 22 | }, 23 | { 24 | action = "get_digits", 25 | min_digits = profile.password_min_digits, 26 | max_digits = profile.password_max_digits, 27 | audio_files = "phrase:reenter_new_password", 28 | bad_input = "", 29 | storage_key = "new_password_2", 30 | timeout = profile.user_input_timeout, 31 | }, 32 | { 33 | action = "conditional", 34 | value = password_1, 35 | compare_to = password_2, 36 | comparison = "equal", 37 | if_true = "update_password", 38 | }, 39 | { 40 | action = "play_phrase", 41 | phrase = "password_mismatch", 42 | }, 43 | -- If we're still in mailbox setup and we've made it this far, then re-try. 44 | { 45 | action = "conditional", 46 | value = mailbox_setup_complete, 47 | compare_to = "yes", 48 | comparison = "equal", 49 | if_true = "mailbox_options", 50 | if_false = "change_password", 51 | }, 52 | } 53 | 54 | -------------------------------------------------------------------------------- /modules/speech_to_text/google.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | NOTE: This handler is left for historical purposes, it's doubtful that it 3 | still works. 4 | ]] 5 | 6 | local core = require "jester.core" 7 | 8 | local _M = {} 9 | 10 | local http = require("socket.http") 11 | local ltn12 = require("ltn12") 12 | local cjson = require("cjson") 13 | 14 | --[[ 15 | Speech to text using Google's API. 16 | ]] 17 | function _M.speech_to_text_from_file_google(action, attributes) 18 | local status = 1 19 | local translations = {} 20 | 21 | local response = {} 22 | 23 | local body, status_code, headers, status_description = http.request({ 24 | method = "POST", 25 | headers = { 26 | ["content-length"] = attributes.filesize, 27 | ["content-type"] = "audio/x-flac; rate=8000", 28 | }, 29 | url = "http://www.google.com/speech-api/v2/recognize?xjerr=1&client=chromium&lang=en-US", 30 | sink = ltn12.sink.table(response), 31 | source = ltn12.source.file(attributes.file), 32 | }) 33 | 34 | if status_code == 200 then 35 | local response_string = table.concat(response) 36 | core.log.debug("Google API server response: %s", response_string) 37 | local data = cjson.decode(response_string) 38 | status = data.status 39 | if status == 0 and data.hypotheses then 40 | for k, chunk in ipairs(data.hypotheses) do 41 | translations[k] = {} 42 | translations[k].text = chunk.utterance 43 | translations[k].confidence = chunk.confidence 44 | end 45 | end 46 | else 47 | core.log.debug("ERROR: Request to Google API server failed: %s", status_description) 48 | end 49 | 50 | return status, translations 51 | end 52 | 53 | return _M 54 | -------------------------------------------------------------------------------- /support/function.lua: -------------------------------------------------------------------------------- 1 | local socket = require "socket" 2 | local core = require "jester.core" 3 | core.bootstrap() 4 | 5 | local LOG_PREFIX = "JESTER::SUPPORT::FUNCTION" 6 | 7 | local log = core.logger({prefix = LOG_PREFIX}) 8 | 9 | --[[ 10 | Issues a query against the database, retrying if failed. 11 | 12 | Function to call must return a non-nil value as the first return value for 13 | the call to be considered successful. 14 | 15 | If the function crashes, and error log is generated, if it returns nil, 16 | a debug log is generated, and the retry continues. 17 | ]] 18 | function call_function_with_retry(tries, retry_interval_seconds, func, ...) 19 | local name = debug.getinfo(func).name or "unamed" 20 | log.debug("Calling function '%s', %d tries left", name, tries) 21 | local data = { pcall(func, ...) } 22 | local success = table.remove(data, 1) 23 | local first_return_value = data[1] 24 | if success and first_return_value ~= nil then 25 | return table.unpack(data) 26 | else 27 | if success then 28 | log.debug("Function '%s' returned nil, will retry", name) 29 | else 30 | log.err("Failed call of function '%s', error: %s", name, first_return_value) 31 | end 32 | tries = tries - 1 33 | if tries > 0 then 34 | log.debug("Trying function '%s' call again in %d seconds", name, retry_interval_seconds) 35 | socket.sleep(retry_interval_seconds) 36 | return call_function_with_retry(tries, retry_interval_seconds, func, ...) 37 | else 38 | log.err("All tries on calling function '%s' exhausted, giving up, error: %s, %s", name, first_return_value, debug.traceback()) 39 | end 40 | return false 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/email_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Email a message. 3 | ]] 4 | 5 | -- Message data. 6 | mailbox = storage("message_info", "mailbox") 7 | recording_name = storage("message_info", "recording_name") 8 | timestamp = storage("message_info", "timestamp") 9 | caller_id_number = storage("message_info", "caller_id_number") 10 | caller_id_name = storage("message_info", "caller_id_name") 11 | 12 | -- Mailbox settings. 13 | email = storage("mailbox_settings_message", "email") 14 | email_template = storage("mailbox_settings_message", "email_template") 15 | timezone = storage("mailbox_settings_message", "default_timezone") 16 | 17 | -- Formatted date. 18 | formatted_date = storage("format", "formatted_date") 19 | 20 | return 21 | { 22 | { 23 | action = "format_date", 24 | storage_key = "formatted_date", 25 | timestamp = timestamp, 26 | timezone = timezone, 27 | format = profile.email_date_format, 28 | }, 29 | { 30 | action = "email", 31 | -- Attachments may or may not be allowed by the template used for the 32 | -- message, but always include it so it's available. 33 | attachments = { 34 | { 35 | filetype = "audio/x-wav", 36 | filename = "message.wav", 37 | filepath = profile.temp_recording_dir .. "/" .. recording_name, 38 | } 39 | }, 40 | from = profile.email_from_address, 41 | to = email, 42 | template = email_template, 43 | server = profile.email_server, 44 | port = profile.email_port, 45 | tokens = { 46 | mailbox = mailbox, 47 | datetime = formatted_date, 48 | caller_id_number = caller_id_number, 49 | caller_id_name = caller_id_name, 50 | }, 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/play_cid_envelope.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play caller ID/envelope information. 3 | ]] 4 | 5 | -- Coming from where? Could be either prior to playing the message, or from 6 | -- the advanced menu options. 7 | from = args(1) 8 | 9 | -- Always play both if we're coming from the advanced options. 10 | if from == "advanced_options" then 11 | play_envelope = "yes" 12 | play_caller_id = "yes" 13 | -- Otherwise, only play the information according to the mailbox settings. 14 | else 15 | play_envelope = storage("mailbox_settings", "play_envelope") 16 | play_caller_id = storage("mailbox_settings", "play_caller_id") 17 | end 18 | 19 | -- Message data. 20 | message_number = storage("counter", "message_number") 21 | timestamp = storage("message", "timestamp_" .. message_number) 22 | caller_id_number = storage("message", "caller_id_number_" .. message_number) 23 | 24 | 25 | return 26 | { 27 | { 28 | action = "play_phrase", 29 | phrase = "cid_envelope", 30 | phrase_arguments = play_envelope .. ":" .. timestamp .. ":" .. play_caller_id .. ":" .. caller_id_number, 31 | keys = { 32 | ["1"] = "top:play_message", 33 | ["2"] = "top:change_folders", 34 | ["3"] = "top:advanced_options", 35 | ["4"] = "top:prev_message", 36 | ["5"] = "top:repeat_message", 37 | ["6"] = "top:next_message", 38 | ["7"] = "top:delete_undelete_message", 39 | ["8"] = "top:forward_message_menu", 40 | ["9"] = "top:save_message", 41 | ["*"] = "help", 42 | ["#"] = "exit", 43 | }, 44 | }, 45 | { 46 | action = "conditional", 47 | value = from, 48 | compare_to = "advanced_options", 49 | comparison = "equal", 50 | if_true = "top:message_options", 51 | }, 52 | } 53 | 54 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/help.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play main help options to the user. 3 | ]] 4 | 5 | -- Option to skip the folder announcement, eg. "new messages". 6 | skip_folder_announcement = args(1) 7 | 8 | -- Message data. 9 | total_messages = storage("message", "__count") 10 | 11 | -- Folder data. 12 | current_folder = storage("message_settings", "current_folder") 13 | 14 | help_keys = { 15 | ["2"] = "change_folders", 16 | ["0"] = "mailbox_options", 17 | ["*"] = "help " .. skip_folder_announcement, 18 | ["#"] = "exit exit_extension", 19 | } 20 | 21 | -- total_messages may still be empty here, and Lua will complain about 22 | -- comparing a string to a number, so guard against it. 23 | if total_messages ~= "" and total_messages > 0 then 24 | help_keys["1"] = "play_messages" 25 | help_keys["3"] = "advanced_options" 26 | help_keys["4"] = "prev_message" 27 | help_keys["5"] = "repeat_message" 28 | help_keys["6"] = "next_message" 29 | end 30 | 31 | -- Overide option for not playing the folder announcement -- used when first 32 | -- logging in to smooth the workflow. 33 | if total_messages == "" or skip_folder_announcement == "skip_folder_announcement" then 34 | announcement = "skip" 35 | else 36 | announcement = current_folder 37 | end 38 | 39 | return 40 | { 41 | keys = help_keys, 42 | { 43 | action = "play_phrase", 44 | phrase = "announce_folder", 45 | phrase_arguments = announcement, 46 | }, 47 | { 48 | action = "play_phrase", 49 | phrase = "help", 50 | phrase_arguments = total_messages .. ":" .. current_folder, 51 | repetitions = profile.menu_repetitions, 52 | wait = profile.menu_replay_wait, 53 | }, 54 | { 55 | action = "call_sequence", 56 | sequence = "exit", 57 | }, 58 | } 59 | 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | TODO: Update for 2.x 2 | New in 2.0: 3 | * Sequence environment now has full access to most Lua globals: previously, 4 | sequences were loaded in a highly restricted environment, without access 5 | to almost all of Lua's functional libraries. This restriction has been 6 | removed, and almost all modules/functions available in the normal global 7 | Lua namespace are now accessible from within sequences. 8 | * jhelp and all command line documentation are replaced with ldoc generated 9 | HTML pages: Jester documentation was too inaccessible in its previously 10 | structured form. It can now be generated from ldoc, or accessed online at 11 | http://thehunmonkgroup.github.io/jester/doc/ 12 | * format module action 'format_number' deprecated: use equivalent 13 | 'format_string' action instead. 14 | * navigation module action 'add_to_stack' deprecated: use equivalent 15 | 'navigation_add' action instead. 16 | * navigation module action 'navigation_up' deprecated: use equivalent 17 | 'navigation_previous' action instead. 18 | * navigation module action 'navigation_top' deprecated: use equivalent 19 | 'navigation_beginning' action instead. 20 | * tracker module, counter action, increment default behavior change: 21 | Previously, the default behavior of the tracker module's counter action 22 | when its 'increment' parameter was not set was to do no incrementing of 23 | the counter. The new default behavior is to increment by 1. The old default 24 | behavior can be acheived by setting 'increment' to 0. 25 | * Main module files now named init.lua: Previously, module naming convention 26 | for the main module file was 'modules/[name]/[name].lua'. This has been 27 | changed to the more standardized 'modules/[name]/init.lua'. 28 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/save_individual_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Save a individual message. 3 | ]] 4 | 5 | -- Mailbox info. 6 | mailbox = storage("message_info", "mailbox") 7 | domain = storage("message_info", "domain") 8 | 9 | -- Result of the attempt to load the mailbox. 10 | loaded_mailbox = storage("mailbox_settings_message", "mailbox") 11 | 12 | -- Mailbox settings. 13 | email_messages = storage("mailbox_settings_message", "email_messages") 14 | mailbox_provisioned = storage("mailbox_settings_message", "mailbox_provisioned") 15 | 16 | return 17 | { 18 | -- Try to load the mailbox. 19 | { 20 | action = "call_sequence", 21 | sequence = "sub:load_mailbox_settings " .. mailbox .. "," .. domain .. ",mailbox_settings_message", 22 | }, 23 | -- Mailbox load failed, clean up. 24 | { 25 | action = "conditional", 26 | value = loaded_mailbox, 27 | compare_to = "", 28 | comparison = "equal", 29 | if_true = "cleanup_temp_recording", 30 | }, 31 | -- Provision the mailbox if it's not provisioned. 32 | { 33 | action = "conditional", 34 | value = mailbox_provisioned, 35 | compare_to = "no", 36 | comparison = "equal", 37 | if_true = "sub:provision_mailbox " .. mailbox .. "," .. domain, 38 | }, 39 | -- Email the message if necessary. 40 | { 41 | action = "conditional", 42 | value = email_messages, 43 | compare_to = "no", 44 | comparison = "equal", 45 | if_false = "sub:email_message", 46 | }, 47 | -- Save the message to the mailbox if necessary, otherwise clean up. 48 | { 49 | action = "conditional", 50 | value = email_messages, 51 | compare_to = "email_only", 52 | comparison = "equal", 53 | if_true = "sub:cleanup_temp_recording", 54 | if_false = "sub:save_recorded_message", 55 | }, 56 | } 57 | 58 | -------------------------------------------------------------------------------- /doc/04-Scripts.md: -------------------------------------------------------------------------------- 1 | # Various helper scripts 2 | 3 | Developing in any code system can be a complicated process, and developers inevitably come up with various helper scripts to help smooth the process. 4 | 5 | Here you will find some scripts that ease working with Jester. 6 | 7 | ## jsequence 8 | 9 | Easily generate properly formatted sequence templates or action fragments with one command. 10 | 11 | jsequence removes the headache of getting initial sequence templates laid out. Probably the most frustrating part of building sequences is dealing with syntax errors due to improper formatting, and this script eliminates many of those issues. 12 | 13 | When called with no arguments, it brings up a dialog asking a few simple questions which it uses to generate the template. Alternatively, it can be called with a variable number of arguments, each the name of an action, and it will output just the structure for the passed actions, which can then be copied or piped into an existing sequence. 14 | 15 | You can also call it with the following special arguments: 16 | 17 | * keys: outputs the template for the rather complex keys parameter. 18 | * actions: outputs a list of all known actions. 19 | 20 | #### Installation 21 | 22 | 27 | 28 | ###### Manually 29 | 30 | 1. Follow the instructions in the [main Jester installation instructions](https://github.com/thehunmonkgroup/jester/blob/master/INSTALL.md) for properly setting your LUA_PATH environment variable. 31 | 2. Move (or better yet, symlink) this script somewhere into your $PATH (~/bin, /usr/local/bin, etc.) 32 | 3. If you use bash completion, you can place scripts/jsequence.bash\_completion in your bash completion directory to enable completion for the command. 33 | -------------------------------------------------------------------------------- /doc/05-Developer.md: -------------------------------------------------------------------------------- 1 | # Developer documentation 2 | 3 | In the future this section will be more detailed. For now it's just a holding place for the outline of the future help, and a quick reference to the Jester core functions that are most commonly used in modules. 4 | 5 | The core code and modules are fairly well documented, and much can be learned from reviewed them directly. The most important quick tips are: 6 | 7 | * You must have this line at the top of your main module file: 8 | local core = require "jester.core" 9 | This allows you to access Jester core via the core variable. 10 | * The main module file must named init.lua. The foo module would live at 11 | jester/modules/foo/init.lua 12 | * A 'conf.lua' is required: 13 | jester/modules/foo/conf.lua 14 | The file contains a mapping of module functions to action names, and can declare multiple handlers for actions. 15 | 16 | ## Quick reference 17 | 18 | * workflow: 19 | core.run_action(action) 20 | core.queue_sequence(sequence) 21 | * data: 22 | core.get_storage(area, key, [default]) 23 | core.set_storage(area, key, value) 24 | core.clear_storage(area, [key]) 25 | * key_presses: 26 | core.actionable_key() 27 | core.keys 28 | * loops: 29 | -- Different than session:ready()! 30 | core.ready() 31 | * logging: 32 | -- Recommended, use extensively so that problems can be easily spotted 33 | -- when debugging is turned on. 34 | core.log.debug(msg) 35 | * misc: 36 | core.wait(milliseconds) 37 | core.trim(string) 38 | 39 | Other core functions are available, check the code directly. 40 | 41 | ## Core stacks 42 | 43 | Jester keeps track of a lot of things in stacks, at the following namespaces: 44 | 45 | * core.channel.stack.sequence 46 | * core.channel.stack.sequence\_name 47 | * core.channel.stack.navigation 48 | * core.channel.stack.active 49 | * core.channel.stack.exit 50 | * core.channel.stack.hangup 51 | 52 | -------------------------------------------------------------------------------- /scripts/extract_actions.lua: -------------------------------------------------------------------------------- 1 | --- Extracts Jester module actions using ldoc. 2 | -- 3 | -- This script extracts a simple map of all Jester actions into a small module 4 | -- that can be used by other modules/scripts needing a global map which 5 | -- includes all parameters and their value types. 6 | -- 7 | -- To re-generate the map, run the following from the root Jester directory: 8 | -- 9 | -- 10 | -- ldoc --filter scripts.extract_actions.filter . 11 | -- 12 | -- 13 | -- @script extract_actions.lua 14 | -- @author Chad Phillips 15 | -- @copyright 2011-2015 Chad Phillips 16 | 17 | local filename = "action_map.lua" 18 | 19 | local output = "--- Table of Jester actions as extracted from Jester using ldoc.\n" 20 | output = output .. "--\n" 21 | output = output .. "-- It is a simple map of all actions, their parameters (minus the action\n" 22 | output = output .. "-- itself), and what value type the parameter accepts.\n" 23 | output = output .. "--\n" 24 | output = output .. "-- @script action_map.lua\n" 25 | output = output .. "-- @author Chad Phillips\n" 26 | output = output .. "-- @copyright 2011-2015 Chad Phillips\n" 27 | output = output .. [[ 28 | 29 | return { 30 | ]] 31 | 32 | return { 33 | filter = function (t) 34 | local data = {} 35 | for _, mod in ipairs(t) do 36 | for _, item in ipairs(mod.items) do 37 | if item.type == 'action' then 38 | local action = item.name 39 | output = output .. " " .. action .. " = {\n" 40 | for _, param in ipairs(item.params) do 41 | if param ~= "action" and param ~= "handler" then 42 | output = output .. " " .. param .. [[ = "]] .. item.modifiers.param[param].type .. [[",]] .. "\n" 43 | end 44 | end 45 | output = output .. " },\n" 46 | end 47 | end 48 | end 49 | output = output .. "}" 50 | local file, err = io.open(filename, "wb") 51 | if err then print(err) end 52 | file:write(output) 53 | file:close() 54 | print ("actions extracted to " .. filename); 55 | end 56 | } 57 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/validate_mailbox_login.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Validate a login attempt to a mailbox. 3 | ]] 4 | 5 | uuid = storage("channel", "uuid") 6 | 7 | -- Mailbox info. 8 | mailbox = storage("login_settings", "mailbox_number") 9 | password = storage("mailbox_settings", "password") 10 | -- The user entered password. 11 | entered_password = storage("get_digits", "password") 12 | 13 | -- Have we set up this mailbox yet? 14 | mailbox_setup_complete = storage("mailbox_settings", "mailbox_setup_complete") 15 | 16 | return 17 | { 18 | -- Load the mailbox for the user. 19 | { 20 | action = "call_sequence", 21 | sequence = "sub:load_mailbox_settings " .. mailbox .. "," .. profile.domain .. ",mailbox_settings", 22 | }, 23 | -- Clear the DTMF queue before collecting the password, in case a stray 24 | -- terminator key was pressed when entering the mailbox number. 25 | { 26 | action = "api_command", 27 | command = "uuid_flush_dtmf " .. uuid, 28 | }, 29 | -- Get a password from the user. Use the correct password to set the 30 | -- max_digit parameter -- smoothes user experience. 31 | { 32 | action = "get_digits", 33 | audio_files = "phrase:get_password", 34 | bad_input = "", 35 | max_digits = ":" .. password, 36 | storage_key = "password", 37 | timeout = profile.user_input_timeout, 38 | }, 39 | -- No entered password is a fail. 40 | { 41 | action = "conditional", 42 | value = entered_password, 43 | compare_to = "", 44 | comparison = "equal", 45 | if_true = "exit", 46 | }, 47 | -- Check for password match, fail if incorrect. 48 | { 49 | action = "conditional", 50 | value = password, 51 | compare_to = entered_password, 52 | comparison = "equal", 53 | if_false = "mailbox_login_incorrect", 54 | }, 55 | -- Check for new user condition and redirect as appropriate. 56 | { 57 | action = "conditional", 58 | value = mailbox_setup_complete, 59 | compare_to = "yes", 60 | comparison = "equal", 61 | if_true = "load_new_old_messages", 62 | if_false = "new_user_walkthrough", 63 | }, 64 | } 65 | 66 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/save_recorded_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Saves a recorded message. 3 | ]] 4 | 5 | -- Copy or move the temporary recording? 6 | operation = args(1) 7 | 8 | -- Message data. 9 | mailbox = storage("message_info", "mailbox") 10 | domain = storage("message_info", "domain") 11 | recording_name = storage("message_info", "recording_name") 12 | timestamp = storage("message_info", "timestamp") 13 | duration = storage("message_info", "duration") 14 | caller_id_number = storage("message_info", "caller_id_number") 15 | caller_id_name = storage("message_info", "caller_id_name") 16 | caller_domain = storage("message_info", "caller_domain") 17 | 18 | -- Get our message count for use in new_message event. 19 | message_count = storage("data", "message_new_count") 20 | 21 | -- Set up the file move action, specifying copying if necessary. 22 | file_operation = { 23 | action = "move_file", 24 | source = profile.temp_recording_dir .. "/" .. recording_name, 25 | destination = profile.voicemail_dir .. "/" .. domain .. "/" .. mailbox .. "/" .. recording_name, 26 | } 27 | if operation == "copy" then 28 | file_operation.copy = true 29 | end 30 | 31 | return 32 | { 33 | file_operation, 34 | { 35 | action = "data_update", 36 | handler = "odbc", 37 | config = profile.db_config_message, 38 | fields = { 39 | domain = domain, 40 | mailbox = mailbox, 41 | -- Inbox folder is 0, all new messages go there. 42 | __folder = 0, 43 | caller_id_number = caller_id_number, 44 | caller_id_name = caller_id_name, 45 | caller_domain = caller_domain, 46 | __timestamp = timestamp, 47 | __duration = duration, 48 | recording = recording_name, 49 | }, 50 | update_type = "insert", 51 | }, 52 | { 53 | action = "call_sequence", 54 | sequence = "sub:get_message_count " .. domain .. "," .. mailbox .. ",0,new", 55 | }, 56 | -- Fire an event indicating a new message was received. 57 | { 58 | action = "fire_event", 59 | event_type = "new_message", 60 | headers = { 61 | Mailbox = mailbox, 62 | Domain = domain, 63 | ["New-Message-Count"] = message_count, 64 | }, 65 | }, 66 | } 67 | 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jester 2 | 3 | **IMPORTANT: The master/2.x branch of Jester is under heavy development, and not guaranteed to work, or show accurate documentation. You may use the [1.x branch](https://github.com/thehunmonkgroup/jester/tree/v1.x) if you need stable functionality, just know that it is unsupported.** 4 | 5 | ## Introduction 6 | Jester is a scripting toolkit for [FreeSWITCH](https://freeswitch.org) written in the [Lua](http://www.lua.org) programming language. 7 | 8 | It is a collection of libraries and convenience functions built and tested by a developer experienced in both FreeSWITCH and Lua. 9 | 10 | The goal of Jester is to ease development of voice workflows by providing a simple, unified way to implement more complex features that normally require complex custom scripting. 11 | 12 | ## Installation 13 | See [INSTALL.md](INSTALL.md) for installation instructions. 14 | 15 | ## Architecture 16 | Jester is written to be small, simple, and extensible. 17 | 18 | Most functionality is carried out by pluggable modules, and people familiar with Lua scripting will find it easy to add new modules to extend functionality further. 19 | 20 | ## Documentation 21 | 22 | Jester comes with extensive documentation available [online](http://thehunmonkgroup.github.io/jester/doc/), which should make it easy for new users and developers to get up to speed. 23 | 24 | Once you've installed Jester, the next best step is to read the help. If you'd like to install it locally, install [LDoc](https://github.com/stevedonovan/LDoc), then run the following from the root directory: 25 | 26 | ```sh 27 | ldoc . 28 | ``` 29 | 30 | ## Support 31 | 32 | The issue tracker for this project is provided to file bug reports, feature requests, and project tasks -- support requests are not accepted via the issue tracker. For all support-related issues, including configuration, usage, and training, consider hiring a competent consultant. 33 | 34 | ## Other stuff 35 | See [LICENSE.txt](LICENSE.txt) to view the license for this software. 36 | 37 | See [BUGS.md](BUGS.md) for a list of known issues. 38 | 39 | See [TODO.md](TODO.md) for a list of things we're working on. 40 | 41 | See [CHANGELOG.md](CHANGELOG.md) for a running list of important changes. 42 | -------------------------------------------------------------------------------- /spec/mock_spec.lua: -------------------------------------------------------------------------------- 1 | require "jester.support.file" 2 | local cjson = require "cjson" 3 | local ltn12 = require "ltn12" 4 | local http_mock = require "jester.spec.http_mock" 5 | 6 | local function request(url, params, attributes) 7 | local request_handler = params.request_handler or https 8 | local response = {} 9 | local body, status_code, headers, status_description = request_handler.request({ 10 | method = "POST", 11 | headers = { 12 | ["content-length"] = attributes.content_length, 13 | ["content-type"] = attributes.file_type, 14 | ["accept"] = "application/json", 15 | }, 16 | url = url, 17 | sink = ltn12.sink.table(response), 18 | source = ltn12.source.file(attributes.file), 19 | }) 20 | return response, status_code, status_description 21 | end 22 | 23 | describe("HTTP mock", function() 24 | it("test mock", function() 25 | local mock1_data = { 26 | data = {hello = "world"} 27 | } 28 | local mock2_data = { 29 | data = {foo = "bar"} 30 | } 31 | local mock = http_mock:new({ 32 | mock1_data, 33 | mock2_data, 34 | }) 35 | local file, filedata = load_file("/vagrant/hello.wav") 36 | local url = "/" 37 | local params = { 38 | request_handler = mock:get_handler(), 39 | } 40 | local attributes = { 41 | file = file, 42 | file_type = "audio/wav", 43 | content_length = filedata.filesize, 44 | } 45 | local response, status_code, status_description = request(url, params, attributes) 46 | assert.is.equal(status_code, 200) 47 | assert.is.equal(status_description, "OK") 48 | assert.is.equal(cjson.encode(mock1_data.data), table.concat(response)) 49 | local response, status_code, status_description = request(url, params, attributes) 50 | assert.is.equal(status_code, 200) 51 | assert.is.equal(status_description, "OK") 52 | assert.is.equal(cjson.encode(mock2_data.data), table.concat(response)) 53 | local response, status_code, status_description = request(url, params, attributes) 54 | assert.is.equal(status_code, 200) 55 | assert.is.equal(status_description, "OK") 56 | assert.is.equal(cjson.encode(mock2_data.data), table.concat(response)) 57 | end) 58 | end) 59 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/message_options.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play message options to the user. 3 | ]] 4 | 5 | -- Message data. 6 | message_number = storage("counter", "message_number") 7 | deleted = storage("message", "deleted_" .. message_number) 8 | total_messages = storage("message", "__count") 9 | prev_message = "" 10 | next_message = "" 11 | delete_undelete_message = "delete" 12 | 13 | -- Build the initial options announcements. 14 | announcements = { 15 | ["3"] = "advanced_options", 16 | ["5"] = "repeat_message", 17 | ["7"] = "delete_message", 18 | ["8"] = "forward_message", 19 | ["9"] = "save_message", 20 | ["*"] = "help_exit", 21 | -- The # key is not included here because the helpexit audio file contains 22 | -- both announcements -- so we just announce the * key. 23 | } 24 | 25 | -- More than one message exists, so we might need prev/next announcements. 26 | if total_messages > 1 then 27 | -- Not on the first message, so we need a prev announcement. 28 | if message_number > 1 then 29 | announcements["4"] = "prev_message" 30 | end 31 | -- Not on the last message, so we need a next announcement. 32 | if message_number < total_messages then 33 | announcements["6"] = "next_message" 34 | end 35 | end 36 | 37 | -- If the message is already marked deleted, then change the annoucement option 38 | -- to undelete. 39 | if deleted == "1" then 40 | announcements["7"] = "undelete_message" 41 | end 42 | 43 | return 44 | { 45 | { 46 | action = "play_keys", 47 | key_announcements = announcements, 48 | keys = { 49 | ["2"] = "top:change_folders", 50 | ["3"] = "top:advanced_options", 51 | ["4"] = "top:prev_message", 52 | ["5"] = "top:repeat_message", 53 | ["6"] = "top:next_message", 54 | ["7"] = "top:delete_undelete_message", 55 | ["8"] = "top:forward_message_menu", 56 | ["9"] = "top:save_message", 57 | ["0"] = "mailbox_options", 58 | ["*"] = "top:help", 59 | ["#"] = "top:exit exit_extension", 60 | }, 61 | order = { 62 | "4", 63 | "3", 64 | "5", 65 | "6", 66 | "7", 67 | "8", 68 | "9", 69 | "*", 70 | }, 71 | repetitions = profile.menu_repetitions, 72 | wait = profile.menu_replay_wait, 73 | }, 74 | { 75 | action = "call_sequence", 76 | sequence = "top:exit", 77 | }, 78 | } 79 | 80 | -------------------------------------------------------------------------------- /support/file.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Support functions for files. 3 | ]] 4 | 5 | local io = require("io") 6 | 7 | local core = require "jester.core" 8 | core.bootstrap() 9 | 10 | local LOG_PREFIX = "JESTER::SUPPORT::FILE" 11 | 12 | local log = core.logger({prefix = LOG_PREFIX}) 13 | 14 | --[[ 15 | Returns the file size in bytes. 16 | ]] 17 | function filesize(file) 18 | local current = file:seek() 19 | local size = file:seek("end") 20 | file:seek("set", current) 21 | return size 22 | end 23 | 24 | function filepath_elements(filepath) 25 | return string.match(filepath, "(.-)([^\\/]-%.?([^%.\\/]*))$") 26 | end 27 | 28 | --[[ 29 | Checks for the existence of a file. 30 | ]] 31 | function file_exists(n) 32 | local f = io.open(n) 33 | if f == nil then 34 | return false 35 | else 36 | io.close(f) 37 | return true 38 | end 39 | end 40 | 41 | function directify_path(path) 42 | if path:sub(-1) ~= "/" then 43 | path = path .. "/" 44 | end 45 | return path 46 | end 47 | 48 | function strip_trailing_slash(path) 49 | if path:sub(-1) == "/" then 50 | path = path:sub(1, -2) 51 | end 52 | return path 53 | end 54 | 55 | function load_file(filepath, mode) 56 | mode = mode and mode or "rb" 57 | local file, err = io.open(filepath, mode) 58 | local data 59 | if file then 60 | data = { 61 | filesize = filesize(file), 62 | } 63 | else 64 | data = err 65 | end 66 | return file, data 67 | end 68 | 69 | --[[ 70 | Write file. 71 | ]] 72 | function write_file(filepath, data, mode) 73 | mode = mode and mode or "w" 74 | local file 75 | local success, err = false, nil 76 | file, err = io.open(filepath, mode) 77 | if file then 78 | success, err = file:write(data) 79 | if success then 80 | log.debug("Wrote file: %s", filepath) 81 | success = true 82 | else 83 | log.err("Could not write file %s: %s", filepath, err) 84 | end 85 | file:close() 86 | end 87 | return success, err 88 | end 89 | 90 | --[[ 91 | Removes file. 92 | ]] 93 | function remove_file(filepath) 94 | local ok, err = os.remove(filepath) 95 | if ok then 96 | log.debug("Removed filepath: %s", filepath) 97 | else 98 | log.err("Could not remove filepath %s: %s", filepath, err) 99 | end 100 | return ok, err 101 | end 102 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/main_greeting.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Main greeting for a user's mailbox. 3 | ]] 4 | 5 | operator_extension = storage("mailbox_settings", "operator_extension") 6 | -- Result of the check for the name greeting. 7 | greet_exists = storage("file", "file_exists") 8 | greet = profile.mailbox_dir .. "/greet.wav" 9 | 10 | -- Build the default name greeting based on if the name greeting exists or 11 | -- not. 12 | default_greet = "phrase:default_greeting_name:" .. greet 13 | if greet_exists == "false" then 14 | default_greet = "phrase:default_greeting:" .. profile.mailbox 15 | end 16 | 17 | -- Set up the available key presses for the caller based on the profile 18 | -- configuration. 19 | greeting_keys = { 20 | ["#"] = ":break", 21 | } 22 | if profile.check_messages then 23 | greeting_keys["*"] = "login " .. profile.mailbox .. "," .. profile.domain 24 | end 25 | -- If there's an available operator extension, then include it in the options 26 | -- and pass that data along to the record sequence. 27 | operator_on_record = "" 28 | if operator_extension ~= "" then 29 | greeting_keys["0"] = "transfer_to_operator" 30 | operator_on_record = "operator" 31 | end 32 | 33 | return 34 | { 35 | -- Load the mailbox settings, we'll need these for some of the message 36 | -- options. 37 | { 38 | action = "call_sequence", 39 | sequence = "sub:load_mailbox_settings " .. profile.mailbox .. "," .. profile.domain .. ",mailbox_settings", 40 | }, 41 | -- Check for existence of the name greeting, which might be used in the 42 | -- default greeting below. 43 | { 44 | action = "file_exists", 45 | file = greet, 46 | }, 47 | -- This action will play the first valid file it finds. It checks, in order: 48 | -- temporary greeting, unavailable greeting, default greeting. 49 | { 50 | action = "play_valid_file", 51 | files = { 52 | profile.mailbox_dir .. "/temp.wav", 53 | profile.mailbox_dir .. "/unavail.wav", 54 | default_greet, 55 | }, 56 | keys = greeting_keys, 57 | }, 58 | -- Register saving the message in the exit loop, in case the caller hangs up 59 | -- instead of explicitly saving the message. 60 | { 61 | action = "exit_sequence", 62 | sequence = "main_greeting_prepare_message", 63 | }, 64 | { 65 | action = "call_sequence", 66 | sequence = "record_message " .. operator_on_record, 67 | }, 68 | } 69 | 70 | -------------------------------------------------------------------------------- /examples/speech_to_text.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Add examples/private.lua in same directory as this file, with these 3 | global variables set: 4 | 5 | Rev.ai 6 | REV_AI_API_KEY 7 | 8 | Watson 9 | WATSON_API_KEY 10 | WATSON_SERVICE_URI 11 | ]] 12 | require("jester.examples.private") 13 | 14 | local inspect = require("inspect") 15 | local core = require "jester.core" 16 | local stt = require "jester.modules.speech_to_text" 17 | local rev_ai = require "jester.modules.speech_to_text.rev_ai" 18 | local watson = require "jester.modules.speech_to_text.watson" 19 | 20 | local DEFAULT_HANDLER = "watson" 21 | 22 | local rev_ai_handler = rev_ai:new({ 23 | api_key = REV_AI_API_KEY, 24 | options = { 25 | speaker_channels_count = 1, 26 | remove_disfluencies = true, 27 | delete_after_seconds = 120, 28 | skip_diarization = true, 29 | filter_profanity = true, 30 | }, 31 | }) 32 | local watson_handler = watson:new({ 33 | api_key = WATSON_API_KEY, 34 | service_uri = WATSON_SERVICE_URI, 35 | query_parameters = { 36 | model = "en-US_NarrowbandModel", 37 | smart_formatting = true, 38 | split_transcript_at_phrase_end = true, 39 | speaker_labels = true, 40 | word_confidence = true, 41 | profanity_filter = true, 42 | }, 43 | }) 44 | 45 | function speech_to_text_from_file(filepath, handler) 46 | local params = { 47 | retries = 10, 48 | retry_wait_seconds = 30, 49 | timeout_seconds = 300, 50 | } 51 | local file_params = { 52 | path = filepath, 53 | } 54 | local confidence, text 55 | local stt_obj = stt:new(handler, params) 56 | local success, data = stt_obj:speech_to_text_from_file(file_params) 57 | if success then 58 | confidence, text = handler:transcriptions_to_text(data) 59 | else 60 | core.log.err(data) 61 | end 62 | return success, data, confidence, text 63 | end 64 | 65 | local filepath = arg[1] 66 | local handler_string = arg[2] or DEFAULT_HANDLER 67 | local handler 68 | if handler_string == "watson" then 69 | handler = watson_handler 70 | elseif handler_string == "revai" then 71 | handler = rev_ai_handler 72 | end 73 | core.bootstrap() 74 | local success, data, confidence, text = speech_to_text_from_file(filepath, handler) 75 | if success then 76 | core.log.info("RAW DATA: \n\n%s", inspect(data)) 77 | core.log.info("Confidence in transcription: %s\n", confidence) 78 | core.log.info("TEXT: \n\n%s", text) 79 | end 80 | -------------------------------------------------------------------------------- /examples/phone_to_post_test.lua: -------------------------------------------------------------------------------- 1 | --- Converts phone call audio into web page data. 2 | -- 3 | -- Records a sound file, converts the speech to text, and stores it on a 4 | -- public web server. 5 | -- 6 | -- The @{speech_to_text} module must be properly configured, and the default 7 | -- FreeSWITCH voicemail phrase macros must be available for this to work. 8 | -- 9 | -- The recording is limited to 10 seconds, you must wait for the "saved" 10 | -- prompt to allow the webservice POST to complete. 11 | -- 12 | -- The location of the web page containing the message will be output to the 13 | -- FreeSWITCH console -- on that web page the message will be listed in the 14 | -- QUERY_STRING header. 15 | -- 16 | -- This is a very simple workflow -- more robust workflows would deal with 17 | -- the user hanging up before the service requests are made, validating that 18 | -- the translation was successful, etc. 19 | 20 | temp_dir = "/tmp" 21 | filename = "post_test.wav" 22 | message = storage("speech_to_text", "translation_1") 23 | post_response = storage("service", "raw") 24 | 25 | return 26 | { 27 | { 28 | action = "wait", 29 | milliseconds = 500, 30 | }, 31 | { 32 | action = "play_phrase", 33 | phrase = "voicemail_record_message", 34 | }, 35 | { 36 | action = "record", 37 | location = temp_dir, 38 | filename = filename, 39 | pre_record_sound = "tone", 40 | max_length = 10, 41 | silence_secs = 2, 42 | keys = { 43 | ["0"] = ":break", 44 | ["1"] = ":break", 45 | ["2"] = ":break", 46 | ["3"] = ":break", 47 | ["4"] = ":break", 48 | ["5"] = ":break", 49 | ["6"] = ":break", 50 | ["7"] = ":break", 51 | ["8"] = ":break", 52 | ["9"] = ":break", 53 | ["*"] = ":break", 54 | ["#"] = ":break", 55 | }, 56 | }, 57 | { 58 | action = "speech_to_text_from_file", 59 | filepath = temp_dir .. "/" .. filename, 60 | app_key = profile.att_app_key, 61 | app_secret = profile.att_app_secret, 62 | }, 63 | { 64 | action = "http_request", 65 | path = "post.php", 66 | query = { 67 | dir = "jester", 68 | message = message, 69 | }, 70 | server = "posttestserver.com", 71 | }, 72 | { 73 | action = "log", 74 | message = "POST MESSAGE RESPONSE: " .. post_response, 75 | }, 76 | { 77 | action = "play_phrase", 78 | phrase = "voicemail_ack", 79 | phrase_arguments = "saved", 80 | }, 81 | { 82 | action = "hangup", 83 | }, 84 | } 85 | -------------------------------------------------------------------------------- /modules/event/init.lua: -------------------------------------------------------------------------------- 1 | --- Interact with the FreeSWITCH event system. 2 | -- 3 | -- This module provides actions for interacting with the FreeSWITCH event 4 | -- system. 5 | -- 6 | -- @module event 7 | -- @author Chad Phillips 8 | -- @copyright 2011-2015 Chad Phillips 9 | 10 | 11 | --- Fires a custom event. 12 | -- 13 | -- Event-Name will be 'CUSTOM', and Event-Subclass will 14 | -- be '[subclass]::[event_type]'. 15 | -- 16 | -- The body will automatically have two newline characters appended to it. 17 | -- 18 | -- @action fire_event 19 | -- @string action 20 | -- fire_event 21 | -- @string body 22 | -- (Optional) The event body. 23 | -- @string event_type 24 | -- The second portion of the Event-Subclass header (after the double colons). 25 | -- @string header_prefix 26 | -- (Optional) Prefix all header keys with this string. Defaults to 'Jester-'. 27 | -- @tab headers 28 | -- (Optional) A table of event headers, key = header name, value = header 29 | -- description. Note that some headers will need to use the full table key 30 | -- syntax. 31 | -- @string subclass 32 | -- (Optional) The first portion of the Event-Subclass header (before the 33 | -- double colons). Default is 'jester'. 34 | -- @usage 35 | -- { 36 | -- action = "fire_event", 37 | -- body = "some message body, if you need it...", 38 | -- event_type = "messages_checked", 39 | -- header_prefix = "Checked-Messages-", 40 | -- headers = { 41 | -- Mailbox = mailbox, 42 | -- Domain = profile.domain, 43 | -- ["New-Message-Count"] = message_count, 44 | -- }, 45 | -- subclass = "messages-checked", 46 | -- }, 47 | 48 | 49 | local core = require "jester.core" 50 | 51 | local _M = {} 52 | 53 | --[[ 54 | Fires a custom event. 55 | ]] 56 | function _M.fire_event(action) 57 | local subclass = action.subclass or "jester" 58 | local event_type = action.event_type 59 | local headers = action.headers 60 | local header_prefix = action.header_prefix or "Jester-" 61 | local body = action.body 62 | if event_type then 63 | local event = freeswitch.Event("custom", subclass .. "::" .. event_type) 64 | if headers then 65 | for k, v in pairs(headers) do 66 | event:addHeader(header_prefix .. k, v) 67 | end 68 | end 69 | if body then 70 | -- Ensure that the event body has terminating newlines. 71 | event:addBody(body .. "\n\n") 72 | end 73 | core.log.debug("Firing custom event 'jester::%s'", event_type) 74 | event:fire() 75 | end 76 | end 77 | 78 | return _M 79 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/forward_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Set up a message for forwarding, optionally recording a custom message to 3 | prepend to it. 4 | ]] 5 | 6 | -- Whether to prepend a message or not. 7 | prepend_message = args(1) 8 | 9 | -- Message data. 10 | message_number = storage("counter", "message_number") 11 | recording_name = storage("message", "recording_" .. message_number) 12 | timestamp = storage("message", "timestamp_" .. message_number) 13 | duration = storage("message", "duration_" .. message_number) 14 | caller_id_name = storage("message", "caller_id_name_" .. message_number) 15 | caller_id_number = storage("message", "caller_id_number_" .. message_number) 16 | caller_domain = storage("message", "caller_domain_" .. message_number) 17 | 18 | -- The extension to forward to. 19 | mailbox = storage("get_digits", "extension") 20 | 21 | return 22 | { 23 | -- Copy the message to the temp recording area. 24 | { 25 | action = "move_file", 26 | source = profile.mailbox_dir .. "/" .. recording_name, 27 | destination = profile.temp_recording_dir .. "/" .. recording_name, 28 | copy = true, 29 | }, 30 | -- Call the prepend sequence if necessary. 31 | { 32 | action = "conditional", 33 | value = prepend_message, 34 | compare_to = "prepend", 35 | comparison = "equal", 36 | if_true = "sub:forward_message_prepend", 37 | }, 38 | -- Fake the last recording name here so the standard message saving 39 | -- sequences can be used. 40 | { 41 | action = "set_storage", 42 | storage_area = "record", 43 | data = { 44 | last_recording_name = recording_name, 45 | }, 46 | }, 47 | -- Set up the message info for saving. 48 | { 49 | action = "set_storage", 50 | storage_area = "message_info", 51 | data = { 52 | mailbox = mailbox, 53 | domain = profile.domain, 54 | caller_id_number = caller_id_number, 55 | caller_id_name = caller_id_name, 56 | caller_domain = caller_domain, 57 | recording_name = recording_name, 58 | timestamp = timestamp, 59 | duration = duration, 60 | }, 61 | }, 62 | -- The standard message saving sequences can be used to save the message. 63 | { 64 | action = "call_sequence", 65 | sequence = "sub:save_individual_message" 66 | }, 67 | { 68 | action = "play_phrase", 69 | phrase = "thank_you", 70 | }, 71 | { 72 | action = "play_phrase", 73 | phrase = "greeting_saved", 74 | }, 75 | { 76 | action = "call_sequence", 77 | sequence = "message_options" 78 | }, 79 | } 80 | 81 | -------------------------------------------------------------------------------- /profiles/voicemail/voicemail.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Mailbox settings. 3 | -- 4 | CREATE TABLE mailbox ( 5 | domain char(80) NOT NULL DEFAULT '', 6 | mailbox char(80) NOT NULL DEFAULT '', 7 | `password` char(80) NOT NULL DEFAULT '', 8 | customer_id char(80) NOT NULL DEFAULT '', 9 | full_name char(80) NOT NULL DEFAULT '', 10 | mailbox_setup_complete char(3) NOT NULL DEFAULT 'no', 11 | mailbox_provisioned char(3) NOT NULL DEFAULT 'no', 12 | message_lifetime int(11) NOT NULL DEFAULT '-1', 13 | max_messages int(5) NOT NULL DEFAULT '100', 14 | default_language char(20) NOT NULL DEFAULT 'en', 15 | default_timezone char(50) NOT NULL DEFAULT 'Etc/UTC', 16 | email char(255) NOT NULL DEFAULT '', 17 | email_template char(255) NOT NULL DEFAULT '', 18 | email_messages char(20) NOT NULL DEFAULT 'no', 19 | play_caller_id char(3) NOT NULL DEFAULT 'no', 20 | play_envelope char(3) NOT NULL DEFAULT 'no', 21 | review_messages char(3) NOT NULL DEFAULT 'yes', 22 | next_after_command char(3) NOT NULL DEFAULT 'yes', 23 | directory_entry char(3) NOT NULL DEFAULT 'yes', 24 | temp_greeting_warn char(3) NOT NULL DEFAULT 'no', 25 | force_name char(3) NOT NULL DEFAULT 'no', 26 | force_greetings char(3) NOT NULL DEFAULT 'no', 27 | operator_extension char(80) NOT NULL DEFAULT '', 28 | callback_extension char(80) NOT NULL DEFAULT '', 29 | outdial_extension char(80) NOT NULL DEFAULT '', 30 | exit_extension char(80) NOT NULL DEFAULT '', 31 | stamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 32 | PRIMARY KEY (domain,mailbox) 33 | ) DEFAULT CHARSET=utf8; 34 | 35 | -- 36 | -- Voicemail messages. 37 | -- 38 | CREATE TABLE message ( 39 | id int(11) NOT NULL auto_increment, 40 | domain varchar(80) NOT NULL default '', 41 | mailbox varchar(80) NOT NULL default '0', 42 | folder tinyint(2) NOT NULL default '1', 43 | caller_id_number varchar(40) NOT NULL default '', 44 | caller_id_name varchar(80) NOT NULL default '', 45 | caller_domain varchar(80) NOT NULL default '', 46 | timestamp bigint(11) NOT NULL default '0', 47 | duration int(11) NOT NULL default '0', 48 | deleted tinyint(1) NOT NULL default '0', 49 | recording text, 50 | PRIMARY KEY (id), 51 | KEY domain_mailbox (domain, mailbox), 52 | KEY timestamp (timestamp), 53 | KEY deleted (deleted) 54 | ); 55 | 56 | -- 57 | -- Voicemail message groups. 58 | -- 59 | CREATE TABLE message_group ( 60 | group_name varchar(30) NOT NULL default '', 61 | domain varchar(255) NOT NULL default '', 62 | mailbox varchar(255) NOT NULL default '', 63 | PRIMARY KEY (group_name, domain, mailbox) 64 | ); 65 | 66 | -------------------------------------------------------------------------------- /modules/hangup/init.lua: -------------------------------------------------------------------------------- 1 | --- Actions related to hanging up a channel. 2 | -- 3 | -- This module provides actions that deal with hanging up a channel, or dealing 4 | -- with a channel in a hung up state. 5 | -- 6 | -- @module hangup 7 | -- @author Chad Phillips 8 | -- @copyright 2011-2015 Chad Phillips 9 | 10 | 11 | --- Hang up a call. 12 | -- 13 | -- This action hangs up the call. No more regular sequences or actions run 14 | -- after this action is called (registered exit/hangup sequences/actions will 15 | -- still run). 16 | -- 17 | -- @action hangup 18 | -- @string action 19 | -- hangup 20 | -- @string play 21 | -- (Optional) The path to a file, or a phrase, to play before hanging up. 22 | -- @usage 23 | -- { 24 | -- action = "hangup", 25 | -- play = "phrase:goodbye", 26 | -- } 27 | 28 | 29 | --- Registers a sequence to be executed on hangup. 30 | -- 31 | -- This action registers a sequence to be executed after the call has been hung 32 | -- up. Channel variables and storage values are available when the registered 33 | -- sequence is run. 34 | -- 35 | -- Sequences registered here are run after the sequences registered on exit, and 36 | -- are only run if the caller hangups up the call before Jester finishes running 37 | -- all active sequences related to the call. If you want to guarantee that the 38 | -- sequence will run regardless of user hangup, it's best to put it in the exit 39 | -- loop instead of here. 40 | -- 41 | -- @action hangup_sequence 42 | -- @string action 43 | -- hangup_sequence 44 | -- @string sequence 45 | -- The sequence to execute. 46 | -- @usage 47 | -- { 48 | -- action = "hangup_sequence", 49 | -- sequence = "cleanup_temp_recording", 50 | -- } 51 | -- @see core_actions.exit_sequence 52 | 53 | local core = require "jester.core" 54 | 55 | local _M = {} 56 | 57 | --[[ 58 | Hangup the call. 59 | ]] 60 | function _M.hangup(action) 61 | -- Clean key map to prevent any key presses here. 62 | core.keys = {} 63 | -- Play a hangup file if specified. 64 | if action.play then 65 | session:streamFile(action.play) 66 | end 67 | core.log.debug("Hangup called in sequence action") 68 | session:hangup(); 69 | end 70 | 71 | --[[ 72 | Register a sequence to run in the hangup sequence loop. 73 | ]] 74 | function _M.register_hangup_sequence(action) 75 | if action.sequence then 76 | local event = {} 77 | event.event_type = "sequence" 78 | event.sequence = action.sequence 79 | table.insert(core.channel.stack.hangup, event) 80 | core.log.debug("Registered hangup sequence: %s", event.sequence) 81 | end 82 | end 83 | 84 | return _M 85 | -------------------------------------------------------------------------------- /profiles/socket/conf.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Profile configuration file. All variables put in here will be processed once 3 | during the jester bootstrap. 4 | 5 | If you have a variable foo that you want to have a value of "bar", do: 6 | foo = "bar" 7 | 8 | Array/record syntax is like this: 9 | foo = { bar = "baz", bing = "bong" } 10 | 11 | Variables from the main configuration may be used in values, by accessing 12 | them through the global. namespace. 13 | 14 | Channel variables may be used in values, by accessing them through the 15 | variable("") function. Note that it's doubtful you'll have a 16 | channel when connecting via the socket, so you probably shouldn't use this. 17 | 18 | Storage variables may be used in values, by accessing them through the 19 | storage("") function. 20 | 21 | Initial arguments may be used in values, by accessing them through the 22 | args() function. 23 | ]] 24 | 25 | --[[ 26 | Everything in this section should not be edited unless you know what you are 27 | doing! 28 | ]] 29 | 30 | -- Overrides the global debug configuration for this profile only. 31 | debug = true 32 | 33 | -- Modules to load. 34 | -- Overrides the global module configuration for this profile only. 35 | -- Modules that primarily use the session object are not included. Some of 36 | -- the included modules still have actions that use the session object, and 37 | -- these actions should probably be avoided. 38 | modules = { 39 | "core_actions", 40 | "data", 41 | "email", 42 | "event", 43 | "file", 44 | "format", 45 | "log", 46 | "navigation", 47 | "tracker", 48 | } 49 | 50 | -- Overrides the global sequence path for this profile only. 51 | sequence_path = global.profile_path .. "/socket/sequences" 52 | 53 | -- Main directory that stores voicemail messages. 54 | -- NOTE: This directory must already be created and writable by the FreeSWITCH 55 | -- user. 56 | voicemail_dir = global.base_dir .. "/storage/voicemail/default" 57 | 58 | --[[ 59 | The sections below can be customized safely. 60 | ]] 61 | 62 | --[[ 63 | Directory paths. 64 | ]] 65 | 66 | -- The directory where recordings are stored temporarily while recording. 67 | temp_dir = "/tmp" 68 | 69 | --[[ 70 | ODBC database table configurations. 71 | ]] 72 | 73 | -- Table that stores mailbox configurations. 74 | db_config_mailbox = { 75 | database_type = "mysql", 76 | database = "jester", 77 | table = "mailbox", 78 | } 79 | 80 | -- Table that stores messages. 81 | db_config_message = { 82 | database_type = "mysql", 83 | database = "jester", 84 | table = "message", 85 | } 86 | 87 | -- Table that stores messages. 88 | db_config_message_group = { 89 | database_type = "mysql", 90 | database = "jester", 91 | table = "message_group", 92 | } 93 | 94 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/save_group_message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Save a message to a message group. 3 | ]] 4 | 5 | -- Total number of users in the message group. 6 | total_mailboxes = storage("message_group", "__count") 7 | -- Which mailbox in the group we're currently on. 8 | row_count = storage("counter", "message_group_row") 9 | -- Mailbox info for the current mailbox. 10 | mailbox = storage("message_group", "mailbox_" .. row_count) 11 | domain = storage("message_group", "domain_" .. row_count) 12 | mailbox_dir = profile.voicemail_dir .. "/" .. domain .. "/" .. mailbox 13 | 14 | -- Result of the attempt to load the mailbox. 15 | loaded_mailbox = storage("mailbox_settings_message", "mailbox") 16 | 17 | -- Mailbox settings for the loaded mailbox. 18 | email_messages = storage("mailbox_settings_message", "email_messages") 19 | mailbox_provisioned = storage("mailbox_settings_message", "mailbox_provisioned") 20 | 21 | return 22 | { 23 | -- Increment the group counter by one. If we're past the total mailboxes, 24 | -- then clean up. 25 | { 26 | action = "counter", 27 | increment = 1, 28 | storage_key = "message_group_row", 29 | compare_to = total_mailboxes, 30 | if_greater = "cleanup_temp_recording", 31 | }, 32 | -- Try to load the mailbox. 33 | { 34 | action = "call_sequence", 35 | sequence = "sub:load_mailbox_settings " .. mailbox .. "," .. domain .. ",mailbox_settings_message", 36 | }, 37 | -- No mailbox found, so just re-call the sequence to move on to the next 38 | -- mailbox. 39 | { 40 | action = "conditional", 41 | value = loaded_mailbox, 42 | compare_to = "", 43 | comparison = "equal", 44 | if_true = "save_group_message", 45 | }, 46 | -- Provision the mailbox if it's not provisioned. 47 | { 48 | action = "conditional", 49 | value = mailbox_provisioned, 50 | compare_to = "no", 51 | comparison = "equal", 52 | if_true = "sub:provision_mailbox " .. mailbox .. "," .. domain, 53 | }, 54 | -- Set up the mailbox data for message storage. 55 | { 56 | action = "set_storage", 57 | storage_area = "message_info", 58 | data = { 59 | mailbox = mailbox, 60 | domain = domain, 61 | }, 62 | }, 63 | -- Email the message if necessary. 64 | { 65 | action = "conditional", 66 | value = email_messages, 67 | compare_to = "no", 68 | comparison = "equal", 69 | if_false = "sub:email_message", 70 | }, 71 | -- Save the message to the mailbox if necessary. 72 | { 73 | action = "conditional", 74 | value = email_messages, 75 | compare_to = "email_only", 76 | comparison = "equal", 77 | if_false = "sub:save_recorded_message copy", 78 | }, 79 | -- Call the sequence again to trigger the next user in the group. 80 | { 81 | action = "call_sequence", 82 | sequence = "save_group_message", 83 | }, 84 | } 85 | 86 | -------------------------------------------------------------------------------- /profiles/voicemail/INSTALL.md: -------------------------------------------------------------------------------- 1 | Brief instructions for setting up the default 'voicemail' profile. They 2 | assume you already have Jester installed correctly: 3 | 4 | 1. Create a database. 5 | 6 | 2. Use the included 'voicemail.sql' file to create the necessary tables in the 7 | new database. Currently only MySQL tables are provided, patches welcome 8 | for other databases. 9 | 10 | NOTE: table structure is totally subject to change in future releases, 11 | you're on your own with that for now! 12 | 13 | 3. Create an ODBC resource to connect to the database. See the data module's 14 | 'Handlers -> odbc' section in the Jester help for more information. 15 | 16 | 4. If necessary, edit the database configuration settings in 17 | 'profiles/voicemail/conf.lua' 18 | 19 | 5. Download the Asterisk core sounds and place them in a subdirectory of the 20 | FreeSWITCH 'sounds' directory. 21 | 22 | 6. From within the Asterisk sounds directory, create a symlink from the digits 23 | directory to 'time'. If you're on a Linux/Unix system this should do it: 24 | 25 | ``` 26 | ln -s digits time 27 | ``` 28 | 29 | This step is necessary for FreeSWITCH's say engine to properly find the 30 | correct Asterisk sound files. 31 | 32 | 7. Edit the 'conf/lang/en/en.xml' file to point at the Asterisk sounds, and 33 | the 'phrases.xml' file found in this profile. If your sounds are located at 34 | 'sounds/asterisk', then the configuration would look something like this: 35 | ```xml 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | 8. Call Jester from the dialplan, passing the voicemail profile as the first 48 | argument, the sequence to call as the second argument, and optional 49 | arguments for the sequence as the third argument -- see 50 | 'Intro -> Running Jester' and 'Sequences -> Passing arguments' 51 | in the Jester help for more information. The 'example_dialplan.xml' included 52 | with the profile illustrates how to set up the extensions, check it out for 53 | usage info. 54 | 55 | 9. The voicemail profile fires three types of events: 56 | new_message: 57 | Fired when a new message is stored in a mailbox. 58 | mailbox_updated: 59 | Fired when the user updates mailbox settings (currently only password 60 | updates). 61 | messages_checked: 62 | Fired after a user checks their messages. 63 | 64 | You can register for these events as follows: 65 | 66 | ``` 67 | event plain CUSTOM jester::new_message jester::mailbox_updated jester::messages_checked 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | --- Global configuration file. 2 | -- 3 | -- Probably not a good idea to change any of these settings unless you know 4 | -- what you're doing. 5 | -- 6 | -- @module core.conf 7 | -- @author Chad Phillips 8 | -- @copyright 2011-2021 Chad Phillips 9 | 10 | 11 | --- Global configuration table. 12 | -- 13 | -- Check the conf.lua file for further descriptions of these values. 14 | -- 15 | -- @table conf 16 | -- @field debug 17 | -- Enable this setting to turn on debuggging by default. 18 | -- Default: false 19 | -- @field debug_output 20 | -- Control debug output. 21 | -- @field base_dir 22 | -- Full path to FreeSWITCH base directory. 23 | -- @field sounds_dir 24 | -- Full path to FreeSWITCH sounds directory. 25 | -- @field scripts_dir 26 | -- Full path to FreeSWITCH scripts directory. 27 | -- @field jester_dir 28 | -- Full path to Jester directory. 29 | -- @field profile_path 30 | -- Full path to Jester's global profile directory. 31 | -- @field key_order 32 | -- The order that keys are played in for announcements. 33 | -- This value can be overridden per profile. 34 | -- This value can be overridden in actions. 35 | -- @field modules 36 | -- The modules to load. 37 | -- This value can be overridden per profile. 38 | local conf = {} 39 | 40 | DEFAULT_LOG_LEVEL = "debug" 41 | conf.log = {} 42 | 43 | -- These settings control what debugging information is output, only edit the 44 | -- values of the table, not the keys. 45 | conf.debug_output = { 46 | -- Ongoing progress. 47 | log = true, 48 | -- These are output right before Jester exits. 49 | jester_object = false, 50 | executed_sequences = true, 51 | run_actions = false, 52 | } 53 | 54 | -- This file can be loaded from the shell, so only build these settings if we 55 | -- have access to the API. 56 | if freeswitch then 57 | local api = freeswitch.API() 58 | conf.base_dir = api:executeString("global_getvar base_dir") 59 | conf.sounds_dir = api:executeString("global_getvar sounds_dir") 60 | -- Override this if scripts are hosted in a non-standard location. 61 | conf.scripts_dir = api:executeString("global_getvar script_dir") 62 | conf.jester_dir = conf.scripts_dir .. "/jester" 63 | conf.sequence_path = conf.jester_dir .. "/sequences" 64 | conf.profile_path = conf.jester_dir .. "/profiles" 65 | conf.log.level = api:executeString("global_getvar jester_log_level") 66 | end 67 | 68 | if not conf.log.level or conf.log.level == "" then 69 | conf.log.level = DEFAULT_LOG_LEVEL 70 | end 71 | 72 | conf.key_order = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "*", "#" } 73 | 74 | conf.modules = { 75 | "core_actions", 76 | "couchdb", 77 | "data", 78 | "dialplan_tools", 79 | "email", 80 | "event", 81 | "file", 82 | "format", 83 | "get_digits", 84 | "hangup", 85 | "log", 86 | "navigation", 87 | "play", 88 | "record", 89 | "service", 90 | "speech_to_text", 91 | "system", 92 | "tracker", 93 | } 94 | 95 | return conf 96 | -------------------------------------------------------------------------------- /spec/unit/core_spec.lua: -------------------------------------------------------------------------------- 1 | require "jester.support.file" 2 | local core = require("jester.core") 3 | describe("core", function() 4 | setup(function() 5 | core.bootstrap_test() 6 | end) 7 | describe("logger", function() 8 | it("outputs correct level", function() 9 | local level, output = core.log.info("test") 10 | assert.is.equal(level, "info") 11 | end) 12 | it("outputs default prefix", function() 13 | local level, output = core.log.info("test") 14 | assert.is.equal(output, "[JESTER] test") 15 | end) 16 | it("doesn't log on debug with info level", function() 17 | local level, output = core.log.debug("test") 18 | assert.is_nil(output) 19 | end) 20 | it("uses custom level when passed", function() 21 | core.log.level = "err" 22 | local level, output = core.log.warning("test") 23 | assert.is_nil(output) 24 | local level, output = core.log.err("test") 25 | assert.is.equal(level, "err") 26 | finally(function() core.log.level = "info" end) 27 | end) 28 | it("uses custom prefix when passed", function() 29 | core.log.prefix = "test" 30 | local level, output = core.log.info("test") 31 | assert.is.equal(output, "[TEST] test") 32 | finally(function() core.log.prefix = "jester" end) 33 | end) 34 | it("correctly handles string tokens", function() 35 | local level, output = core.log.info("test %d %s", 1, "test") 36 | assert.is.equal(output, "[JESTER] test 1 test") 37 | end) 38 | it("correctly handles formatter function", function() 39 | local level, output = core.log.info(function(t) return t end, "test") 40 | assert.is.equal(output, "[JESTER] test") 41 | end) 42 | it("throws on missing level", function() 43 | core.log.level = "test" 44 | assert.has_error(function() core.log.info("test") end, "ERROR: missing log level 'test'") 45 | finally(function() core.log.level = "info" end) 46 | end) 47 | describe("custom", function() 48 | it("correctly loads with default level", function() 49 | local logger = core.logger() 50 | local level, output = logger.info("test") 51 | assert.is.equal(level, "info") 52 | end) 53 | it("correctly handles custom modes", function() 54 | local modes = { 55 | { name = "test", color = "\27[36m", }, 56 | } 57 | local logger = core.logger({level = "test", modes = modes}) 58 | local level, output = logger.test("test") 59 | assert.is.equal(level, "test") 60 | end) 61 | it("correctly logs to file", function() 62 | local filepath = "/tmp/core_logger_test" 63 | os.remove(filepath) 64 | local logger = core.logger({outfile = filepath}) 65 | local level, output = logger.info("test") 66 | assert.is_true(file_exists(filepath)) 67 | finally(function() os.remove(filepath) end) 68 | end) 69 | end) 70 | end) 71 | end) 72 | -------------------------------------------------------------------------------- /profiles/voicemail/sequences/main_menu.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Play the main menu to the caller. 3 | ]] 4 | 5 | uuid = storage("channel", "uuid") 6 | 7 | outdial_extension = storage("mailbox_settings", "outdial_extension") 8 | 9 | -- Message data. 10 | new_message_count = storage("messagenew", "__count") 11 | old_message_count = storage("messageold", "__count") 12 | 13 | -- Determine which folder will be selected by default. 14 | current_folder = "" 15 | messages = "" 16 | if new_message_count > 0 then 17 | current_folder = "0" 18 | messages = "messagenew" 19 | elseif old_message_count > 0 then 20 | current_folder = "1" 21 | messages = "messageold" 22 | end 23 | 24 | -- Set up the initial key press options. 25 | main_menu_keys = { 26 | ["2"] = "change_folders", 27 | ["4"] = "prev_message", 28 | ["5"] = "repeat_message", 29 | ["6"] = "next_message", 30 | ["0"] = "mailbox_options", 31 | ["*"] = "help skip_folder_announcement", 32 | } 33 | 34 | -- New or old messages exist, so provide a play option. 35 | if current_folder ~= "" then 36 | main_menu_keys["1"] = "play_messages" 37 | end 38 | 39 | -- Outdial is enabled, so provide access to the advanced options from 40 | -- the main menu. 41 | if outdial_extension ~= "" then 42 | main_menu_keys["3"] = "main_menu_advanced_options" 43 | end 44 | 45 | return 46 | { 47 | -- Store the current folder -- it remains the same until the user changes it. 48 | { 49 | action = "set_storage", 50 | storage_area = "message_settings", 51 | data = { 52 | current_folder = current_folder, 53 | }, 54 | }, 55 | -- If new or old messages exist, copy whichever was selected as the default 56 | -- into the message storage area. 57 | { 58 | action = "conditional", 59 | value = current_folder, 60 | compare_to = "", 61 | if_false = "sub:copy_new_old_messages " .. messages, 62 | }, 63 | -- Start the message counter on the first message. 64 | { 65 | action = "counter", 66 | storage_key = "message_number", 67 | increment = 1, 68 | reset = true, 69 | }, 70 | -- Register an exit sequence for firing the 'messages_checked' event. 71 | { 72 | action = "exit_sequence", 73 | sequence = "messages_checked", 74 | }, 75 | -- Register an exit sequence for automatically removing deleted messages. 76 | { 77 | action = "exit_sequence", 78 | sequence = "auto_delete_messages", 79 | }, 80 | -- Announce new/old messages. 81 | { 82 | action = "play_phrase", 83 | phrase = "announce_new_old_messages", 84 | phrase_arguments = new_message_count .. ":" .. old_message_count, 85 | keys = main_menu_keys, 86 | }, 87 | -- Clear the DTMF queue, in case a stray terminator key was pressed for 88 | -- password validation. 89 | { 90 | action = "api_command", 91 | command = "uuid_flush_dtmf " .. uuid, 92 | }, 93 | -- Send the user to help. 94 | { 95 | action = "call_sequence", 96 | sequence = "help skip_folder_announcement", 97 | }, 98 | } 99 | 100 | -------------------------------------------------------------------------------- /modules/speech_to_text/support.lua: -------------------------------------------------------------------------------- 1 | --- Speech to text translation support functions. 2 | -- 3 | -- This module provides support functions for speech to text translation. 4 | -- 5 | -- @module speech_to_text_support 6 | -- @author Chad Phillips 7 | -- @copyright 2011-2021 Chad Phillips 8 | 9 | --- Attributes calculated for @{load_file_attributes}. 10 | -- 11 | -- @table file_attributes 12 | -- 13 | -- @field file 14 | -- The opened file object for the filepath being transcribed. 15 | -- @field content_type_type 16 | -- Mime type of the file, default "audio/wav". 17 | -- @field content_length 18 | -- File size in bytes. 19 | -- @field dirname 20 | -- Directory file is in. 21 | -- @field basename 22 | -- Base name of file. 23 | -- @field ext 24 | -- File extension. 25 | 26 | 27 | 28 | require("jester.support.file") 29 | require("jester.support.table") 30 | 31 | local core = require("jester.core") 32 | core.bootstrap() 33 | 34 | local log = core.logger({prefix = "JESTER::MODULE::SPEECH_TO_TEXT::SUPPORT"}) 35 | 36 | local DEFAULT_CONTENT_TYPE = "audio/wav" 37 | 38 | function stt_set_start_end_timestamps(params) 39 | params.start_timestamp = params.start_timestamp and tonumber(params.start_timestamp) or os.time() 40 | if not params.end_timestamp and params.timeout_seconds then 41 | params.end_timestamp = params.start_timestamp + tonumber(params.timeout_seconds) 42 | log.debug("Set end_timestamp to: %d", params.end_timestamp) 43 | end 44 | return params 45 | end 46 | 47 | function stt_format_timeout_message(timestamp) 48 | return string.format([[Request timed out at: %s]], os.date("!%Y-%m-%dT%TZ", timestamp)) 49 | end 50 | 51 | function stt_merge_params(...) 52 | return table.merge(...) 53 | end 54 | 55 | --- Load a file and calculate some file attributes. 56 | -- 57 | -- @param file_params 58 | -- Required. Table of file parameters. 59 | -- @param file_params.path 60 | -- Path to file. 61 | -- @param file_params.content_type 62 | -- Optional. Default "audio/wav". 63 | -- @return 64 | -- @{file_attributes} table. 65 | -- @usage 66 | -- local file_params = { 67 | -- path = "/tmp/myfile.wav", 68 | -- content_type = "audio/wav", 69 | -- } 70 | -- local attributes = load_file_attributes(file_params) 71 | function load_file_attributes(file_params) 72 | local filepath = file_params.path 73 | if not filepath then 74 | return true 75 | end 76 | local content_type = file_params.content_type or DEFAULT_CONTENT_TYPE 77 | local content_length 78 | local file, data = load_file(filepath) 79 | if file then 80 | log.debug("Loaded file attributes from: %s", filepath) 81 | local dirname, basename, ext = filepath_elements(filepath) 82 | local attributes = { 83 | path = filepath, 84 | file = file, 85 | content_type = content_type, 86 | content_length = data.filesize, 87 | dirname = dirname, 88 | basename = basename, 89 | ext = ext, 90 | } 91 | return file, attributes 92 | else 93 | local message = string.format([[Could not open '%s': %s]], filepath, data) 94 | log.err(message) 95 | return false, message 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /modules/speech_to_text/att.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | NOTE: This handler is left for historical purposes, it's doubtful that it 3 | still works. 4 | ]] 5 | 6 | local core = require "jester.core" 7 | 8 | local _M = {} 9 | 10 | --[[ 11 | Get an access token from an AT&T API call. 12 | ]] 13 | local function att_get_access_token(action) 14 | local app_key = action.app_key 15 | local app_secret = action.app_secret 16 | 17 | local post_data = string.format("client_id=%s&client_secret=%s&grant_type=client_credentials&scope=SPEECH,TTS", app_key, app_secret) 18 | 19 | local response = {} 20 | 21 | local body, status_code, headers, status_description = https.request({ 22 | method = "POST", 23 | headers = { 24 | ["content-length"] = post_data:len(), 25 | }, 26 | url = "https://api.att.com/oauth/v4/token", 27 | sink = ltn12.sink.table(response), 28 | source = ltn12.source.string(post_data), 29 | protocol = "tlsv1", 30 | }) 31 | 32 | if status_code == 200 then 33 | local response_string = table.concat(response) 34 | core.log.debug("JSON response string '%s'", response_string) 35 | local data = cjson.decode(response_string) 36 | for key, value in pairs(data) do 37 | if key == "access_token" then 38 | return value 39 | end 40 | end 41 | core.log.debug("ERROR: No access token found") 42 | else 43 | core.log.debug("ERROR: Request to AT&T token server failed: %s", status_description) 44 | end 45 | end 46 | 47 | local https = require 'ssl.https' 48 | local ltn12 = require("ltn12") 49 | local cjson = require("cjson") 50 | 51 | --[[ 52 | Speech to text using AT&T's API. 53 | ]] 54 | function _M.speech_to_text_from_file(action, attributes) 55 | local status = 1 56 | local translations = {} 57 | 58 | local access_token = att_get_access_token(action) 59 | 60 | if access_token then 61 | local response = {} 62 | 63 | local body, status_code, headers, status_description = https.request({ 64 | method = "POST", 65 | headers = { 66 | ["content-length"] = attributes.filesize, 67 | ["content-type"] = "audio/x-wav", 68 | ["accept"] = "application/json", 69 | ["authorization"] = "Bearer " .. access_token, 70 | }, 71 | url = "https://api.att.com/speech/v3/speechToText", 72 | sink = ltn12.sink.table(response), 73 | source = ltn12.source.file(attributes.file), 74 | protocol = "tlsv1", 75 | }) 76 | 77 | if status_code == 200 then 78 | local response_string = table.concat(response) 79 | core.log.debug("JSON response string '%s'", response_string) 80 | local data = cjson.decode(response_string) 81 | status = data.Recognition.Status == "OK" and 0 or 1 82 | if status == 0 and type(data.Recognition.NBest) == "table" then 83 | for k, chunk in ipairs(data.Recognition.NBest) do 84 | translations[k] = {} 85 | translations[k].text = chunk.ResultText 86 | translations[k].confidence = chunk.Confidence 87 | end 88 | end 89 | else 90 | core.log.debug("ERROR: Request to AT&T API server failed: %s", status_description) 91 | end 92 | end 93 | 94 | return status, translations 95 | end 96 | 97 | return _M 98 | -------------------------------------------------------------------------------- /modules/log/init.lua: -------------------------------------------------------------------------------- 1 | --- Custom logger for sequences. 2 | -- 3 | -- This module provides custom logging functionality for sequences. It can be 4 | -- used to log data somewhere from within a sequence. 5 | -- 6 | -- @module log 7 | -- @author Chad Phillips 8 | -- @copyright 2011-2015 Chad Phillips 9 | 10 | 11 | --- The console handler (default). 12 | -- 13 | -- Logs to the FreeSWITCH console. 14 | -- 15 | -- When using this handler, the 'level' argument for the action can be any 16 | -- valid level used by 17 | -- [freeswitch.consoleLog](https://freeswitch.org/confluence/display/FREESWITCH/Lua+API+Reference#LuaAPIReference-freeswitch.consoleLog) 18 | -- 19 | -- @handler console 20 | -- @usage 21 | -- { 22 | -- action = "log", 23 | -- handler = "console", 24 | -- level = "info", 25 | -- -- other params... 26 | -- } 27 | 28 | 29 | --- The file handler. 30 | -- 31 | -- Logs to a file on the local filesystem. 32 | -- 33 | -- When using this handler, the 'level' argument for the action can be any 34 | -- arbitrary value the user decides. 35 | -- 36 | -- @handler file 37 | -- @usage 38 | -- { 39 | -- action = "log", 40 | -- handler = "file", 41 | -- level = "WARN", 42 | -- -- other params... 43 | -- } 44 | 45 | 46 | --- Log a custom message. 47 | -- 48 | -- Allows logging a message from within a sequence, with a custom level. 49 | -- 50 | -- @action log 51 | -- @string action 52 | -- log 53 | -- @string file 54 | -- (Optional) Required only for handlers that log to a file. Provide a full 55 | -- path to the file. Default is '/tmp/jester.log'. 56 | -- @string level 57 | -- (Optional) The log level of the message. This value will vary depending on 58 | -- the handler, see [handlers](#Handlers). Default is 'info'. 59 | -- @string message 60 | -- The message to log. 61 | -- @string handler 62 | -- The handler to use, see [handlers](#Handlers). If not specified, defaults 63 | -- to the default handler for the module. 64 | -- @usage 65 | -- { 66 | -- action = "log", 67 | -- file = "/tmp/jester.log", 68 | -- level = "info", 69 | -- message = "A custom log message", 70 | -- handler = "file", 71 | -- } 72 | 73 | 74 | local core = require "jester.core" 75 | 76 | local _M = {} 77 | 78 | --[[ 79 | Log to the console. 80 | ]] 81 | function _M.log_console(action) 82 | local message = action.message 83 | local level = action.level or "info" 84 | if message then 85 | core.log(message, "JESTER LOG", level) 86 | end 87 | end 88 | 89 | --[[ 90 | Log to a file on the local filesystem. 91 | ]] 92 | function _M.log_file(action) 93 | local message = action.message 94 | local file = action.file or '/tmp/jester.log' 95 | local level = action.level or "INFO" 96 | if message then 97 | -- Try to open the log file in append mode. 98 | local destination, file_error = io.open(file, "a") 99 | if destination then 100 | message = os.date("%Y-%m-%d %H:%M:%S") .. " " .. level .. ": " .. message .. "\n" 101 | destination:write(message) 102 | destination:close() 103 | else 104 | core.log.debug("Failed writing to log file '%s'!: %s", file, file_error) 105 | end 106 | end 107 | end 108 | 109 | return _M 110 | -------------------------------------------------------------------------------- /support/string.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Support functions for strings. 3 | ]] 4 | 5 | --[[ 6 | Trims whitespace from either end of a string. 7 | ]] 8 | function string:trim() 9 | return (string.gsub(self, "^%s*(.-)%s*$", "%1")) 10 | end 11 | 12 | --[[ 13 | Trims whitespace from either end of a string. 14 | ]] 15 | function trim(s) 16 | return s:trim() 17 | end 18 | 19 | --[[ 20 | Splits a string by a given delimter and returns a table of ordered pieces. 21 | ]] 22 | function string:split(delimiter, notrim) 23 | local result = {} 24 | local from = 1 25 | local piece 26 | local delim_from, delim_to = string.find(self, delimiter, from) 27 | while delim_from do 28 | piece = string.sub(self, from , delim_from-1) 29 | if not notrim then 30 | piece = piece:trim() 31 | end 32 | table.insert(result, piece) 33 | from = delim_to + 1 34 | delim_from, delim_to = string.find(self, delimiter, from) 35 | end 36 | piece = string.sub(self, from) 37 | if not notrim then 38 | piece = piece:trim() 39 | end 40 | table.insert(result, piece) 41 | return result 42 | end 43 | 44 | 45 | --[[ 46 | Provides word wrapping with variable boundary, respecting existing 47 | indentation, and allowing additional indentation to be added. 48 | ]] 49 | function string:wrap(boundary, indent) 50 | local output = {} 51 | local index, words, leading_space, full_indent 52 | local indent = indent or "" 53 | local buffer = indent 54 | local lines = self:split("\n", true) 55 | 56 | for _, line in pairs(lines) do 57 | if line:match("^%s*$") then 58 | table.insert(output, "") 59 | else 60 | words = line:split(" ", true) 61 | leading_space = line:match("^(%s+)%S") 62 | if (#words > 0) then 63 | if leading_space then 64 | full_indent = leading_space .. indent 65 | else 66 | full_indent = indent 67 | end 68 | index = 1 69 | while words[index] do 70 | local word = " " .. words[index] 71 | if (buffer:len() >= boundary) then 72 | table.insert(output, buffer:sub(1, boundary)) 73 | buffer = full_indent .. buffer:sub(boundary + 1) 74 | else 75 | if (word:len() > boundary) then 76 | table.insert(output, buffer) 77 | table.insert(output, full_indent .. word) 78 | buffer = indent 79 | index = index + 1 80 | elseif (buffer:len() + word:len() >= boundary) then 81 | table.insert(output, buffer) 82 | buffer = full_indent 83 | else 84 | if (buffer == full_indent) then 85 | buffer = full_indent .. word:sub(2) 86 | else 87 | buffer = buffer .. word 88 | end 89 | index = index + 1 90 | end 91 | end 92 | end 93 | if (buffer:match("%S")) then 94 | table.insert(output, buffer) 95 | buffer = indent 96 | end 97 | end 98 | end 99 | end 100 | return table.concat(output, "\n") 101 | end 102 | 103 | --[[ 104 | Replaces tokens in a string with their token values. 105 | ]] 106 | function string:token_replace(tokens) 107 | local substitutions 108 | local total = 0 109 | for token, replacement in pairs(tokens) do 110 | self, substitutions = self:gsub(":" .. token, replacement) 111 | total = total + substitutions 112 | end 113 | return self, total 114 | end 115 | 116 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | ## System requirements 2 | 3 | * Most recent Jester code 4 | * FreeSWITCH 1.0.7 or later 5 | * Lua 5.1 or later. 6 | * LuaSocket 2.0.2 or later 7 | * LuaFilesystem 1.4.2 or later 8 | * For data module support: 9 | * Any database that supports standard SQL queries. A recent version of 10 | MySQL, Postgres, or SQLite should work just fine. 11 | * ODBC and the ODBC driver for the above database. 12 | * For email module support: 13 | * A mailserver with an open socket that you can send messages through. 14 | * For Jester's event listener support via socket.lua (experimental): 15 | * A mod_event_socket connection to the FreeSWITCH server. 16 | * *Note that some other core modules have other dependencies, these are 17 | documented in the main description for each particular module.* 18 | 19 | 20 | ## Installation 21 | 22 | Jester is relatively easy to install, especially if you have a modern package-based Linux distribution. Here are the basic steps: 23 | 24 | 1. Install Lua (should be readily available from your packaging system) 25 | eg. ```yum install lua``` or ```apt-get install lua5.2``` 26 | 27 | 2. Install the [LuaFileSystem](http://keplerproject.github.com/luafilesystem) 28 | and [LuaSocket](http://w3.impa.br/~diego/software/luasocket) packages (also 29 | probably available in your packaging system, they're in the EPEL repository for 30 | RHEL/CentOS/Fedora, and in the Debian repos as well. eg. 31 | ```yum install lua-filesystem lua-socket``` or ```apt-get install 32 | lua-filesystem lua-socket``` 33 | If you can't find these in a package, you can use the 34 | [LuaRocks](http://luarocks.org) package manager, or install from source: 35 | 36 | 3. Set your LUA_PATH environment variable to include the FreeSWITCH 'scripts' 37 | directory. This is for easy access when you are logged in at the command 38 | line. For the bash shell, in a typical installation, it would look like 39 | this: 40 | ``` 41 | export LUA_PATH=";;/usr/local/freeswitch/scripts/?.lua" 42 | ``` 43 | 44 | 45 | The two semicolons at the beginning of the path are not a typo! Lua 46 | interprets those as 'include my default paths too'. 47 | 48 | 4. Drop the entire 'jester' directory inside the FreeSWITCH 'scripts' 49 | directory. In a typical installation, it would be at: 50 | 51 | ``` 52 | /usr/local/freeswitch/scripts/jester 53 | ``` 54 | 55 | 56 | 57 | 5. Depending on where you install the packages from step 2, you may need to 58 | fiddle with the LUA_PATH and LUA_CPATH settings in the lua.conf.xml file 59 | found in the ```conf/autoload_configs``` directory of your FreeSWITCH 60 | installation. 61 | If you're seeing errors at the FreeSWITCH console about not being able 62 | load "lfs" or "socket", then this is the most likely cause. 63 | For example, the configuration for LUA_CPATH should be somewhat like this: 64 | 65 | 66 | ```xml 67 | 68 | ``` 69 | or 70 | 71 | ```xml 72 | 73 | ``` 74 | 75 | 76 | 77 | 6. If your FreeSWITCH 'scripts' directory is in a non-standard location, edit 78 | the value of the ```jester_dir``` variable in ```jester/conf.lua``` 79 | appropriately. 80 | 81 | 7. Jester is now installed. 82 | 83 | 84 | #### Note to Windows users: 85 | 86 | Jester won't work on Windows unless you figure out the path separator 87 | issue, as Jester assumes that paths are ```/path/to/blah```, and not 88 | ```C:\\path\to\blah```. If you can solve that, it *should* work. 89 | -------------------------------------------------------------------------------- /doc/02-Profiles.md: -------------------------------------------------------------------------------- 1 | # Jester's profile system 2 | 3 | A profile is a high-level configuration tool that allows you to override certain Jester global configurations, in addition to providing your own custom constants that you can access in any sequences loaded within the profile. 4 | 5 | Jester must be given a valid profile to run when it is called. 6 | 7 | The main advatages of profiles are: 8 | 9 | 1. They provide an easy way to store constants that are used over and over again in sequences, such as database connection configurations, or paths to custom sound files, etc. 10 | 2. They allow you to only load the modules you need for the sequences you are running, so you can load different sets of modules at different times depending on what sequences you are running. Used properly, this can make Jester more efficient. 11 | 3. If you design your sequences intelligently, you can make them behave in different ways by loading different profiles with different settings. 12 | 13 | 14 | ## Profile configuration 15 | 16 | The profile configuration lives at jester/profiles/[name]/conf.lua ([name] being the name of the profile). The configuration is loaded after the global configuration to allow overrides, but also loaded fairly early in the bootstrap process to allow for maximum control. 17 | 18 | Profiles can use variables from two places: 19 | 20 | 1. **Global configuration:** Variables defined in jester/conf.lua can be accessed through the global namespace, eg. 21 | global.base_dir 22 | Accesses the base_dir variable from the global configuration. 23 | 2. **Channel variables:** Variables defined in the current FreeSWITCH channel that Jester is running in can be accessed through the get_variable() function, eg. 24 | get_variable("caller_id_name") 25 | Accesses the caller\_id\_name variable from the channel. 26 | 27 | Profile configurations are allowed to override the main configuration for the following variables: 28 | 29 | * modules 30 | * sequence_path 31 | * key_order 32 | * debug 33 | 34 | The default 'voicemail' profile configuration file is well commented, check it out for more details. 35 | 36 | 37 | ## Things typically stored in a profile 38 | 39 | Profiles are meant to be an easy way to keep everything together. 40 | 41 | In typical practice, the sequences, phrase macros, database schemas, etc. that a profile uses are all stored under the main profile directory, to make it centralized and portable. 42 | 43 | This does require a bit of extra configuration in some areas: 44 | 45 | **Sequences:** 46 | 47 | The global sequence\_path variable will need to be overridden, and instead pointed to a location inside the profile. A common line would be: 48 | 49 | ```lua 50 | sequence_path = global.profile_path .. "/[name]/sequences" 51 | ``` 52 | 53 | **Phrase macros:** 54 | 55 | These are normally kept in the various 'lang' folders in the main FreeSWITCH configuration, but they can be stored in a custom location. A typical configuration line for that in, for example, the conf/lang/en/en.xml FreeSWITCH configuration file, would be: 56 | 57 | ```xml 58 | 62 | ``` 63 | 64 | 65 | ## Default profile 66 | 67 | The included default 'voicemail' profile (located at profiles/voicemail) is a replica of [Asterisk's Comedian Mail](http://www.voip-info.org/wiki/index.php?page_id=502). 68 | 69 | It is intended to be an exact replica of the original version shipped with [Asterisk](http://www.asterisk.org/) 1.2/1.4, a showcase of the power and flexibility of the Jester system, and a template to use as a starting place for learning and building other workflows. 70 | 71 | Everything needed to set up the profile is included in the profile directory. Check out the INSTALL.txt there for more details. 72 | 73 | -------------------------------------------------------------------------------- /support/table.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Support functions for tables. 3 | ]] 4 | 5 | local url = require("socket.url") 6 | 7 | --[[ 8 | Given an associative array table, returns a new table with an ordered 9 | list of the values of the passed table. 10 | ]] 11 | function table.orderkeys(t) 12 | local list = {} 13 | for k, _ in pairs(t) do 14 | table.insert(list, k) 15 | end 16 | table.sort(list) 17 | return list 18 | end 19 | 20 | --[[ 21 | Given an associative array table, returns a new table with an ordered 22 | list of the keys of the passed table. 23 | ]] 24 | function table.ordervalues(t) 25 | local list = {} 26 | for _, v in pairs(t) do 27 | table.insert(list, v) 28 | end 29 | table.sort(list) 30 | return list 31 | end 32 | 33 | --[[ 34 | Checks for the existence of a value in a table. 35 | ]] 36 | function table.contains(table, element) 37 | for _, value in pairs(table) do 38 | if value == element then 39 | return true 40 | end 41 | end 42 | return false 43 | end 44 | 45 | --[[ 46 | Given an associative array table, returns a properly escaped query string. 47 | ]] 48 | function table.stringify(params, sep, eq) 49 | if not sep then sep = '&' end 50 | if not eq then eq = '=' end 51 | if type(params) == "table" then 52 | local fields = {} 53 | for key,value in pairs(params) do 54 | local keyString = url.escape(tostring(key)) .. eq 55 | if type(value) == "table" then 56 | for _, v in ipairs(value) do 57 | table.insert(fields, keyString .. url.escape(tostring(v))) 58 | end 59 | else 60 | table.insert(fields, keyString .. url.escape(tostring(value))) 61 | end 62 | end 63 | return table.concat(fields, sep) 64 | end 65 | return '' 66 | end 67 | 68 | --[[ 69 | Clone tables, internal function. 70 | ]] 71 | local function table_clone_internal(t, copies) 72 | if type(t) ~= "table" then return t end 73 | copies = copies or {} 74 | if copies[t] then return copies[t] end 75 | local copy = {} 76 | copies[t] = copy 77 | for k, v in pairs(t) do 78 | copy[table_clone_internal(k, copies)] = table_clone_internal(v, copies) 79 | end 80 | setmetatable(copy, table_clone_internal(getmetatable(t), copies)) 81 | return copy 82 | end 83 | 84 | --[[ 85 | Clone tables. 86 | ]] 87 | function table.clone(t) 88 | -- We need to implement this with a helper function to make sure that 89 | -- user won't call this function with a second parameter as it can cause 90 | -- unexpected troubles 91 | return table_clone_internal(t) 92 | end 93 | 94 | --[[ 95 | Merge tables. 96 | ]] 97 | function table.merge(...) 98 | local tables_to_merge = {...} 99 | local recurse = true 100 | if type(tables_to_merge[#tables_to_merge]) == "boolean" then 101 | recurse = tables_to_merge[#tables_to_merge] 102 | table.remove(tables_to_merge, #tables_to_merge) 103 | end 104 | assert(#tables_to_merge > 1, "There should be at least two tables to merge them") 105 | for k, t in ipairs(tables_to_merge) do 106 | assert(type(t) == "table", string.format("Expected a table as function parameter %d", k)) 107 | end 108 | local result = table.clone(tables_to_merge[1]) 109 | for i = 2, #tables_to_merge do 110 | local from = tables_to_merge[i] 111 | for k, v in pairs(from) do 112 | if recurse and type(v) == "table" then 113 | result[k] = result[k] or {} 114 | assert(type(result[k]) == "table", string.format("Expected a table: '%s'", k)) 115 | result[k] = table.merge(result[k], v) 116 | else 117 | result[k] = v 118 | end 119 | end 120 | end 121 | return result 122 | end 123 | 124 | --[[ 125 | Joins indexed tables. 126 | ]] 127 | function table.join(...) 128 | local tables_to_concat = {...} 129 | local concat_table = {} 130 | for _, tab in ipairs(tables_to_concat) do 131 | for _, v in ipairs(tab) do 132 | table.insert(concat_table, v) 133 | end 134 | end 135 | return concat_table 136 | end 137 | -------------------------------------------------------------------------------- /socket.lua: -------------------------------------------------------------------------------- 1 | -- TODO: Rewrite for library. 2 | --- Socket listener. 3 | -- 4 | -- This is a socket listener for running Jester sequences via the FreeSWITCH 5 | -- event system. It is experimental, use at your own risk. 6 | -- 7 | -- To start listener, set it as a startup script in 8 | -- conf/autoload_configs/lua.conf.xml: 9 | --```xml 10 | -- 14 | --``` 15 | -- 16 | -- Or via luarun: 17 | --```sh 18 | -- luarun jester/socket.lua [server] [port] [password] 19 | --``` 20 | -- 21 | -- It listens for CUSTOM events of subclass 'jester::socket'. 22 | -- 23 | -- Firing an event for the listener looks something like this: 24 | -- 25 | --``` 26 | -- sendevent CUSTOM 27 | -- Event-Subclass: jester::socket 28 | -- Jester-Profile: socket 29 | -- Jester-Sequence: mysequence 30 | -- Jester-Sequence-Args: arg1,arg2 31 | --``` 32 | -- 33 | -- Params for the event are as follows: 34 | -- 35 | --``` 36 | -- Jester-Sequence: 37 | -- Required. The name of the sequence to run. 38 | -- Jester-Profile: 39 | -- Optional. The profile to run the sequence under. Defaults to 'socket'. 40 | -- Jester-Sequence-Args: 41 | -- Optional. Arguments to pass to the sequence, in the same form that normal 42 | -- sequence arguments are passed. 43 | --``` 44 | -- 45 | -- To exit the listener, you can send this event: 46 | -- 47 | --``` 48 | -- sendevent CUSTOM 49 | -- Event-Subclass: jester::socket 50 | -- Jester-Socket-Exit: yes 51 | --``` 52 | -- 53 | -- **WARNING:** there is no session object available with this approach, so be 54 | -- careful not to use actions that need a session (play, record, get_digits, 55 | -- etc.) or the listener will crash! The sequences should be more along the 56 | -- lines of performing database/file manipulation, logging to file, etc. 57 | -- 58 | -- @script socket.lua 59 | -- @author Chad Phillips 60 | -- @copyright 2011-2015 Chad Phillips 61 | 62 | local core = require "jester.core" 63 | local conf = require "jester.conf" 64 | -- The ESL library in older versions doesn't return the ESL object, so leave 65 | -- it as a global for compat. 66 | require "ESL" 67 | 68 | local _M = {} 69 | -- Login information for the event socket. 70 | _M.host = argv[1] or "localhost" 71 | _M.port = argv[2] or "8021" 72 | _M.password = argv[3] or "ClueCon" 73 | 74 | --[[ 75 | Logs socket information to the FreeSWITCH console. 76 | ]] 77 | function _M.socket_log(message) 78 | core.log(message, "JESTER SOCKET") 79 | end 80 | 81 | --[[ 82 | Connect to FreeSWITCH. 83 | ]] 84 | function _M.socket_connect() 85 | _M.sock = ESL.ESLconnection(_M.host, _M.port, _M.password) 86 | end 87 | 88 | -- This is always true on a socket connection, setting it here allows early 89 | -- logging. 90 | core.is_freeswitch = true 91 | _M.socket_log("connecting") 92 | _M.socket_connect() 93 | 94 | if _M.sock and _M.sock:connected() then 95 | _M.socket_log("connected") 96 | -- Subscribe only to Jester socket events. 97 | _M.sock:events("plain", "CUSTOM jester::socket") 98 | _M.continue_socket = true 99 | while _M.sock and _M.sock:connected() and _M.continue_socket do 100 | local event = _M.sock:recvEvent() 101 | -- Provide a way to exit the listener. 102 | if event:getHeader("Jester-Socket-Exit") then 103 | _M.continue_socket = false 104 | _M.socket_log("received disconnect command") 105 | else 106 | local sequence = event:getHeader("Jester-Sequence") 107 | if sequence then 108 | local profile = event:getHeader("Jester-Profile") or "socket" 109 | local sequence_args = event:getHeader("Jester-Sequence-Args") or "" 110 | _M.socket_log(string.format([[received Jester event: %s %s %s]], profile, sequence, sequence_args)) 111 | core.bootstrap(conf, profile, sequence, sequence_args) 112 | -- Main loop. 113 | core.main() 114 | end 115 | end 116 | end 117 | _M.socket_log("disconnecting") 118 | if _M.sock and _M.sock:connected() then 119 | _M.sock:disconnect() 120 | end 121 | end 122 | 123 | return _M 124 | 125 | -------------------------------------------------------------------------------- /modules/system/init.lua: -------------------------------------------------------------------------------- 1 | --- Access to operating system commands. 2 | -- 3 | -- This module provides access to various commands available at the operating 4 | -- system level. 5 | -- 6 | -- @module system 7 | -- @author Chad Phillips 8 | -- @copyright 2011-2015 Chad Phillips 9 | 10 | 11 | --- Execute a shell command. 12 | -- 13 | -- This action executes a system shell command, storing the return code in the 14 | -- 'return\_code' key of the specificed storage area. The environment the shell 15 | -- command runs in is the same environment FreeSWITCH provides to Lua. 16 | -- 17 | -- This action is preferred over the @{shell_command_with_output} action if 18 | -- the output of the command is not needed. 19 | -- 20 | -- @action shell_command 21 | -- @string action 22 | -- shell_command 23 | -- @string command 24 | -- The shell command to run. Arguments can be provided if needed. 25 | -- @string storage_area 26 | -- The storage area to store the return code. Default is 'system'. 27 | -- @usage 28 | -- { 29 | -- action = "shell_command", 30 | -- command = "service foo start", 31 | -- storage_area = "service_return_code", 32 | -- } 33 | 34 | 35 | --- Execute a shell command, saving the output. 36 | -- 37 | -- This action executes a system shell command, storing the return code in the 38 | -- 'return\_code' key, and the command output in the 'output' key of the 39 | -- specificed storage area. The environment the shell command runs in is the 40 | -- same environment FreeSWITCH provides to Lua. 41 | -- 42 | -- If the command output is not needed, the @{shell_command} action is 43 | -- preferred. 44 | -- 45 | -- NOTE: Due to limitations in Lua 5.x, this action has a slightly hackish 46 | -- implementation -- it's not portable, doubtful it will work on Windows, 47 | -- mileage may vary. 48 | -- 49 | -- @action shell_command_with_output 50 | -- @string action 51 | -- shell\_command\_with\_output 52 | -- @string command 53 | -- The shell command to run. Arguments can be provided if needed. 54 | -- @string storage_area 55 | -- The storage area to store the return code and output. Default is 'system'. 56 | -- @usage 57 | -- { 58 | -- action = "shell_command_with_output", 59 | -- command = "ls -1 /tmp/*.wav", 60 | -- storage_area = "ls_return", 61 | -- } 62 | 63 | 64 | local core = require "jester.core" 65 | 66 | local _M = {} 67 | 68 | --[[ 69 | Executes an operating system shell command and stores the return value. 70 | ]] 71 | function _M.shell_command(action) 72 | local command = action.command 73 | local area = action.storage_area or "system" 74 | if command then 75 | local ret 76 | local val1, val2, val3 = os.execute(command) 77 | -- Return signature of os.execute changed in 5.2. 78 | if _VERSION == "Lua 5.1" then 79 | ret = val1 80 | else 81 | ret = val3 82 | end 83 | core.log.debug("Executed command: %s, return code: %s", command, ret) 84 | core.set_storage(area, "return_code", tonumber(ret)) 85 | end 86 | end 87 | 88 | --[[ 89 | Executes an operating system shell command and stores the output and return 90 | value. 91 | ]] 92 | function _M.shell_command_with_output(action) 93 | local command = action.command 94 | local area = action.storage_area or "system" 95 | if command then 96 | local file = io.popen(command .. ' 2>&1; echo "-retcode:$?"', 'r') 97 | local full_output = file:read('*a') 98 | file:close() 99 | local i1, i2, output, newline, ret = full_output:find('(.*)(.)-retcode:(%d+)\n$') 100 | if ret then 101 | -- We're expecting the last character of the shell output to be a newline, 102 | -- which we want to trim, but if it's not, then stick it back on. 103 | if newline ~= "\n" then 104 | output = output .. newline 105 | end 106 | else 107 | output = "Unable to parse command output" 108 | ret = 1 109 | end 110 | core.log.debug("Executed command: %s, output: %s, return code: %s", command, output, ret) 111 | core.set_storage(area, "output", output) 112 | core.set_storage(area, "return_code", tonumber(ret)) 113 | end 114 | end 115 | 116 | return _M 117 | -------------------------------------------------------------------------------- /modules/tracker/init.lua: -------------------------------------------------------------------------------- 1 | --- Track various states. 2 | -- 3 | -- This module provides actions to assist in tracking various states during 4 | -- sequence execution. 5 | -- 6 | -- @module tracker 7 | -- @author Chad Phillips 8 | -- @copyright 2011-2015 Chad Phillips 9 | 10 | 11 | --- Incremental custom variable counter. 12 | -- 13 | -- This action provides a simple method to keep a count of any arbitrary value, 14 | -- and provides access to calling sequences by comparing a number against the 15 | -- total in the counter. It's useful for storing how many times you've done 16 | -- something, eg. on 3rd failed login attempt, hang up. Counters are 17 | -- initialized with a value of zero, and placed in storage area 'counter'. 18 | -- 19 | -- @action counter 20 | -- @string action 21 | -- counter 22 | -- @int compare_to 23 | -- (Optional) The value to compare the current counter value against. 24 | -- @string if_equal 25 | -- (Optional) The sequence to call if the counter value is equal to the 26 | -- 'compare_to' value. 27 | -- @string if_greater 28 | -- (Optional) The sequence to call if the counter value is greater than the 29 | -- 'compare_to' value. 30 | -- @string if_less 31 | -- (Optional) The sequence to call if the counter value is less than the 32 | -- 'compare_to' value. 33 | -- @int increment 34 | -- (Optional) Increment the counter by this amount before performing the 35 | -- comparison to the 'compare_to' parameter. Negative increments are allowed. 36 | -- Default is 1. To disable counting, set this to 0. 37 | -- @bool reset 38 | -- (Optional) Set to true to reset the counter to zero. This happens before 39 | -- any incrementing, so it can be used with incrementing to set a new initial 40 | -- value for the counter. 41 | -- @string storage_key 42 | -- (Optional) The key in the 'counter' storage area where the counter value is 43 | -- stored and checked. Default is 'counter' 44 | -- @usage 45 | -- { 46 | -- action = "counter", 47 | -- compare_to = profile.max_login_attempts, 48 | -- if_equal = "one_more_try", 49 | -- if_greater = "mailbox_login_failed", 50 | -- if_less = "login", 51 | -- increment = 1, 52 | -- reset = false, 53 | -- storage_key = "failed_login_counter", 54 | -- } 55 | 56 | 57 | local core = require "jester.core" 58 | 59 | local _M = {} 60 | 61 | --[[ 62 | Incremental counter that compares the value of the counter against another 63 | value, and optionally run sequences based on the comparison. 64 | ]] 65 | function _M.counter(action) 66 | local key = action.storage_key or "counter" 67 | local increment = action.increment and tonumber(action.increment) or 1 68 | local compare_to = action.compare_to and tonumber(action.compare_to) or nil 69 | -- Perform reset first to allow increment to be used to set the initial 70 | -- value of the counter. 71 | if action.reset then core.clear_storage("counter", key) end 72 | -- Grab the current count. 73 | local current_count = core.get_storage("counter", key) 74 | -- No current count, so initialize with a zero value. 75 | if not current_count then 76 | current_count = 0 77 | core.set_storage("counter", key, current_count) 78 | end 79 | -- Increment the counter if specified. 80 | if increment ~= 0 then 81 | current_count = current_count + increment 82 | core.log.debug("Incremented counter '%s' by %d, new value %d", key, increment, current_count) 83 | core.set_storage("counter", key, current_count) 84 | end 85 | -- Perform comparisons if specified, and run sequences if there's a match. 86 | if compare_to then 87 | core.log.debug("Comparing counter '%s' (%d) to %d", key, current_count, compare_to) 88 | if current_count == compare_to then 89 | core.log.debug("Comparison result: equal") 90 | if action.if_equal then 91 | core.queue_sequence(action.if_equal) 92 | end 93 | elseif current_count < compare_to then 94 | core.log.debug("Comparison result: less") 95 | if action.if_less then 96 | core.queue_sequence(action.if_less) 97 | end 98 | else 99 | core.log.debug("Comparison result: greater") 100 | if action.if_greater then 101 | core.queue_sequence(action.if_greater) 102 | end 103 | end 104 | end 105 | end 106 | 107 | return _M 108 | -------------------------------------------------------------------------------- /utilities/startup_script.lua: -------------------------------------------------------------------------------- 1 | --- Startup script controller. 2 | -- 3 | -- This script provides a basic controller for the 'startup-script' 4 | -- functionality found in FreeSWITCH's mod_lua. 5 | -- 6 | -- It serves the following functions: 7 | -- 8 | -- 1. Calls a specified module on a regular, configurable interval. 9 | -- 2. Reloads the module on every invocation (allows editing of your module). 10 | -- 3. Calls the module in protected mode, preserving the infinite loop on 11 | -- crash. 12 | -- 4. Provides basic logging during the loop for successful/failed execution. 13 | -- 5. Examines a special global variable that, when present, allows 'pausing' 14 | -- your module execution in the loop. 15 | -- 16 | -- Note that the clearing/reloading approach does come with the cost of a 17 | -- small performance hit, and the benefit of being able to live edit your 18 | -- module even though it is being run by the controller in an infinite loop. 19 | -- 20 | -- INPORTANT: Even though the controller reloads your main module on every 21 | -- invocation, you are still responsible for clearing the package cache for 22 | -- any dependent scripts/modules that your module loads. This is not strictly 23 | -- necessary, but will allow those dependent modules to be edited during the 24 | -- infinite loop of the controller as well. 25 | -- 26 | -- @module startup_script 27 | -- @author Chad Phillips 28 | -- @copyright 2021 Chad Phillips 29 | 30 | local LOG_PREFIX = "JESTER::MODULE::STARTUP_SCRIPT" 31 | local DEFAULT_SLEEP_SECONDS = 60 32 | 33 | local api = freeswitch.API() 34 | 35 | local function log(level, ...) 36 | freeswitch.consoleLog(level, string.format("[%s] %s\n", LOG_PREFIX, string.format(...))) 37 | end 38 | 39 | local function getvar(name) 40 | return freeswitch.getGlobalVariable(name) 41 | end 42 | 43 | local function sleep(milliseconds) 44 | freeswitch.msleep(milliseconds) 45 | end 46 | 47 | local function require_module(name) 48 | local success, M = pcall(require, name) 49 | if success then 50 | return M 51 | else 52 | log("err", "Could not require module '%s': make sure the module is available via a standard require() call, error output follows: %s", name, M) 53 | return false 54 | end 55 | end 56 | 57 | local function verify_module(name, M) 58 | if type(M) ~= "table" or type(M.main) ~= "function" then 59 | log("err", "Could not execute module '%s': the module must return a table, with a 'main' key that is the function to execute", name) 60 | return false 61 | end 62 | return true 63 | end 64 | 65 | local function execute_module_protected(M) 66 | return M.main() 67 | end 68 | 69 | local function execute_module(name, M) 70 | local success, data = pcall(execute_module_protected, M) 71 | if success then 72 | log("debug", "Executed module '%s' successfully, returned: %s", name, data) 73 | else 74 | log("err", "Could not execute module '%s': %s", name, data) 75 | end 76 | end 77 | 78 | local function unload_module(name) 79 | package.loaded[name] = nil 80 | end 81 | 82 | --- Start the main loop of the controller. 83 | -- 84 | -- @string name 85 | -- Name of the module to call, in the format expected by require(). 86 | -- @int sleep_seconds 87 | -- Optional. Seconds to sleep between module invocations. If provided, must 88 | -- be a positive integer or float. 89 | local function main_loop(name, sleep_seconds) 90 | local sleep_milliseconds = math.floor(sleep_seconds * 1000) 91 | local module_pause_var = string.format([[%s_pause]], name) 92 | log("info", "Starting main loop for module '%s', sleeping %d seconds between invocations, pause module invocation with 'global_setvar %s=1'", name, sleep_seconds, module_pause_var) 93 | local M 94 | while true do 95 | if getvar(module_pause_var) then 96 | log("info", "Module '%s' has been paused, execute 'global_setvar %s=' to resume", name, module_pause_var) 97 | else 98 | M = require_module(name) 99 | if M and verify_module(name, M) then 100 | execute_module(name, M) 101 | end 102 | end 103 | unload_module(name) 104 | sleep(sleep_milliseconds) 105 | end 106 | end 107 | 108 | -- Make script arguments consistent. 109 | local args = {} 110 | if argv then 111 | args = argv 112 | elseif arg then 113 | args = arg 114 | end 115 | 116 | local name = args[1] 117 | local sleep_seconds = tonumber(args[2]) 118 | 119 | if not name then 120 | error("Module name is required, must be specified in the format needed by require()") 121 | end 122 | if not sleep_seconds or sleep_seconds == 0 then 123 | log("info", "No sleep_seconds argument provided, defaulting to %d seconds", DEFAULT_SLEEP_SECONDS) 124 | sleep_seconds = DEFAULT_SLEEP_SECONDS 125 | end 126 | 127 | main_loop(name, sleep_seconds) 128 | -------------------------------------------------------------------------------- /profiles/voicemail/example_dialplan.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 43 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 80 | 81 | 82 | 87 | 88 | 89 | 90 | 97 | 98 | 99 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /doc/01-Intro.md: -------------------------------------------------------------------------------- 1 | # High-level introduction 2 | 3 | Jester is VoIP toolkit for FreeSWITCH written in Lua. The goal of Jester is to provide a standardized set of tools that allow you to accomplish most of the common tasks you'll face when putting together phone trees, voicemail systems, etc. And, if Jester can't do something you need, it's modular, extensible design allows you to easily add the functionality in a way that not only you but others can benefit from! 4 | 5 | 6 | ## Running Jester 7 | 8 | Jester is designed to be executed as a standard Lua script from the FreeSWITCH dialplan. The general format is as follows: 9 | 10 | ```xml 11 | 15 | ``` 16 | 17 | 18 | ## Configuration system 19 | 20 | Configurations are stored in three different places in Jester: 21 | 22 | 1. jester/conf.lua - Global configuration 23 | 2. profiles/[name]/conf.lua - Profile configuration 24 | 3. modules/[name]/conf.lua - Module configuration 25 | 26 | The global configuration file and the voicemail profile's configuration file are well commented, check them out for more details. 27 | 28 | The main configuration gets loaded for all calls to Jester, while the profile configuration only gets loaded for the profile that Jester is currently running. 29 | 30 | One important thing to note about these configurations is that any variables in them are only processed once, when Jester initially loads. If you have variables that change throughout the course of the call, you'll need to put them in storage or in channel variables. 31 | 32 | 33 | ## Brief Lua language tutorial 34 | 35 | For a definitive explanation of the Lua language, the online [Lua manual](http://www.lua.org/manual/5.2) is the place to go. 36 | 37 | 38 | 39 | This will be a very quick overview of the basics that you'll most likely use in sequences. 40 | 41 | Lua is loosely typed, and will automatically convert types based on the operation you're trying to perform -- eg. if you concatenate a string with an integer, the integer will be converted to a string. 42 | 43 | **Define a variable:** 44 | 45 | ```lua 46 | -- Assign the 'name' variable an integer value of zero. 47 | name = 0 48 | -- Assign the 'name' variable the string value of "foo" 49 | name = "foo" 50 | -- Another way to Assign the 'name' variable the string value of "foo" 51 | name = [[foo]] 52 | -- Assign one variable to another 53 | name = value 54 | ``` 55 | 56 | Variable names can be any string of letters, digits and underscores, not beginning with a digit. 57 | 58 | **Math:** 59 | 60 | ```lua 61 | foo = foo + 1 62 | foo = foo - 1 63 | foo = foo * 1 64 | foo = foo / 1 65 | ``` 66 | 67 | **Relational operators:** 68 | 69 | ```lua 70 | == -- equal 71 | ~= -- not equal 72 | < -- less than 73 | > -- greater than 74 | <= -- less than or equal to 75 | >= -- greater than or equal to 76 | ``` 77 | 78 | **String concatenation:** 79 | 80 | ```lua 81 | newstring = oldstring .. "more" 82 | ``` 83 | 84 | **Conditional statements:** 85 | 86 | ```lua 87 | if foo ~= "yes" then 88 | -- Do something 89 | end 90 | if foo == 1 then 91 | -- Do some stuff 92 | elseif foo == "bar" then 93 | -- Do something else 94 | else 95 | -- Ok, do this 96 | end 97 | ``` 98 | 99 | **Boolean:** 100 | 101 | ```lua 102 | foo = true 103 | bar = false 104 | ``` 105 | 106 | **Non-value:** 107 | 108 | ```lua 109 | baz = nil 110 | ``` 111 | 112 | **Logical operators:** 113 | 114 | ```lua 115 | foo = true and false -- returns false 116 | foo = true and "bar" -- returns "bar" 117 | foo = false or true -- returns true 118 | foo = "foo" or "bar" returns "foo" 119 | foo = false or nil -- returns nil 120 | foo = true and "bar" or "baz" -- returns bar 121 | foo = false and "bar" or "baz" -- returns baz 122 | ``` 123 | 124 | These return false when what they compare is false or nil, all others return true. 125 | 126 | **Tables:** 127 | 128 | The only structured data in Lua. Table keys that are strings have the same restrictions as variable names (in the Jester world, anyways). 129 | 130 | ```lua 131 | -- Create an empty table. 132 | foo = {} 133 | -- Create an ordered list. 134 | table1 = {10, 20, 30} 135 | -- Create a record-style table, this is not ordered! 136 | table2 = { 137 | -- String table keys have the same restrictions as variable names 138 | bar = "baz", 139 | bang = "zoom", 140 | } 141 | value = table1[1] -- value is 10 142 | value = table2.bar -- value is "baz" 143 | complex = { 144 | { 145 | one = "two", 146 | }, 147 | { 148 | three = "four", 149 | }, 150 | } 151 | -- Value is two 152 | value = complex[1].one 153 | ``` 154 | 155 | -------------------------------------------------------------------------------- /modules/speech_to_text/init.lua: -------------------------------------------------------------------------------- 1 | --- Speech to text translation. 2 | -- 3 | -- This module provides speech to text translation. Specific services are 4 | -- supported by the various handlers: 5 | -- 6 | -- @{speech_to_text_watson_handler|Watson Speech to Text API} 7 | -- 8 | -- The module requires the Lua 9 | -- [lua-cjson](https://luarocks.org/modules/luarocks/lua-cjson) and 10 | -- [luasec](https://luarocks.org/modules/brunoos/luasec) packages. 11 | -- 12 | -- @module speech_to_text 13 | -- @author Chad Phillips 14 | -- @copyright 2011-2021 Chad Phillips 15 | 16 | require "jester.modules.speech_to_text.support" 17 | 18 | local socket = require "socket" 19 | local core = require "jester.core" 20 | core.bootstrap() 21 | 22 | local LOG_PREFIX = "JESTER::MODULE::SPEECH_TO_TEXT" 23 | local DEFAULT_PARAMS = { 24 | retries = 3, 25 | retry_wait_seconds = 60, 26 | } 27 | 28 | local _M = {} 29 | 30 | local function parse_response(self, data) 31 | local success, data = self.handler:parse_transcriptions(data) 32 | if success then 33 | return success, data 34 | else 35 | return false, string.format([[Parsing Speech to Text API response failed: %s]], data) 36 | end 37 | end 38 | 39 | local function make_request_using_handler(self, attributes) 40 | local success, data = self.handler:make_request(attributes) 41 | if success then 42 | return parse_response(self, data) 43 | else 44 | return false, string.format([[Speech to Text API failed: %s]], data) 45 | end 46 | end 47 | 48 | local function retry_wait(self, attempt) 49 | if attempt < self.params.retries then 50 | self.log.warning([[Attempt #%d failed, re-trying Speech to Text API in %d seconds]], attempt, self.params.retry_wait_seconds) 51 | socket.sleep(self.params.retry_wait_seconds) 52 | end 53 | end 54 | 55 | local function make_request_with_retry(self, file_params) 56 | local success, data 57 | for attempt = 1, self.params.retries do 58 | success, data = make_request_using_handler(self, file_params) 59 | if success then 60 | break 61 | else 62 | if self.params.end_timestamp and os.time() > self.params.end_timestamp then 63 | return false, stt_format_timeout_message(self.params.end_timestamp) 64 | end 65 | self.log.warning([[Request failed: %s]], data) 66 | retry_wait(self, attempt) 67 | end 68 | end 69 | return success, data 70 | end 71 | 72 | --- Translates a sound file to text. 73 | -- 74 | -- @param file_params 75 | -- Table of file parameters, as passed to @{speech_to_text_support.load_file_attributes}. 76 | -- @treturn bool success 77 | -- Indicates if operation succeeded. 78 | -- @return data 79 | -- Table of transcriptions on success, error message on fail. 80 | -- @usage 81 | -- local file_params = { 82 | -- path = "/tmp/myfile.wav", 83 | -- } 84 | -- success, data = stt_obj:speech_to_text_from_file(file_params) 85 | function _M:speech_to_text_from_file(file_params) 86 | self.params = stt_set_start_end_timestamps(self.params) 87 | local success, data = make_request_with_retry(self, file_params) 88 | return success, data 89 | end 90 | 91 | --- Create a new speech to text object. 92 | -- 93 | -- @param self 94 | -- @tab handler 95 | -- Required. Speech to text handler module. 96 | -- @param params 97 | -- Optional. Table of configuration parameters. 98 | -- @param params.retries 99 | -- Number of times to try the request. Default is 3. 100 | -- @param params.retry_wait_seconds 101 | -- Number of seconds to wait between retries. Default is 60. 102 | -- @param params.end_timestamp 103 | -- Unix timestamp after which the request should time out. Default is no end 104 | -- timestamp. 105 | -- @param params.timeout_seconds 106 | -- Number of seconds to wait before timing out the request. This can be 107 | -- provided instead of end_timestamp, in which case end_timestamp will be 108 | -- calculated by adding timeout_seconds to the current UNIX time of the request. 109 | -- Default is no timeout. 110 | -- @return A speech to text object. 111 | -- @usage 112 | -- stt = require("jester.modules.speech_to_text") 113 | -- rev_ai = require("jester.modules.speech_to_text.rev_ai") 114 | -- local handler_params = { 115 | -- api_key = "some_api_key", 116 | -- -- other params... 117 | -- } 118 | -- local handler = rev_ai:new(handler_params) 119 | -- local stt_params = { 120 | -- retries = 3, 121 | -- -- other params... 122 | -- } 123 | -- stt_obj = stt:new(handler, stt_params) 124 | function _M.new(self, handler, params) 125 | if not handler then 126 | error("Handler is required") 127 | end 128 | local stt = {} 129 | stt.log = core.logger({prefix = LOG_PREFIX}) 130 | stt.handler = handler 131 | stt.params = table.merge(DEFAULT_PARAMS, params or {}) 132 | stt.handler.params = stt_merge_params(stt.handler.params, stt.params) 133 | setmetatable(stt, self) 134 | self.__index = self 135 | stt.log.debug("New speech to text object") 136 | return stt 137 | end 138 | 139 | return _M 140 | -------------------------------------------------------------------------------- /modules/format/init.lua: -------------------------------------------------------------------------------- 1 | --- Custom formatters for sequences. 2 | -- 3 | -- This module provides custom formatting functionality for sequences. It can 4 | -- be used to alter the formatting of certain data within a sequence. 5 | -- 6 | -- @module format 7 | -- @author Chad Phillips 8 | -- @copyright 2011-2015 Chad Phillips 9 | 10 | 11 | --- Formats a Unix timestamp as a date string. 12 | -- 13 | -- The format string is configurable, and timezones are supported. The 14 | -- formatted result is placed in the 'format' storage area. 15 | -- 16 | -- @action format_date 17 | -- @string action 18 | -- format_date 19 | -- @string format 20 | -- (Optional) The format string to use. Should be a string in the form taken 21 | -- by [strftime](http://linux.die.net/man/3/strftime). 22 | -- Default is '%Y-%m-%d %H:%M:%S'. 23 | -- @string storage_key 24 | -- (Optional) The key to store the formatted result under in the 'format' 25 | -- storage area. Default is 'date'. 26 | -- @int timestamp 27 | -- The Unix timestamp to format. 28 | -- @string timezone 29 | -- (Optional) The timezone to use to calculate the time. This should be a 30 | -- string that represents the [timezone name](https://en.wikipedia.org/wiki/Tz_database#Names_of_time_zones) 31 | -- as found in server's timezone database (often in '/usr/share/zoneinfo'). 32 | -- Default is 'Etc/UTC'. 33 | -- @usage 34 | -- { 35 | -- action = "format_date", 36 | -- format = "%Y-%m-%d %H:%M:%S", 37 | -- storage_key = "formatted_date", 38 | -- timestamp = 1452792192, 39 | -- timezone = "Etc/UTC", 40 | -- }, 41 | 42 | 43 | --- Formats a string using a given mask. 44 | -- 45 | -- The mask can be used to exclude specific characters, and to add additional 46 | -- formatting. 47 | -- 48 | -- A specific use case is formatting the number '+15555551212' into the more 49 | -- readable '(555) 555-1212'. The formatted result is placed in the 'format' 50 | -- storage area. 51 | -- 52 | -- Note that currently, due to limitations in the parser, the string to be 53 | -- formatted cannot contain any of the mask special placeholder characters, 54 | -- see the 'mask' parameter below for those. 55 | -- 56 | -- @action format_string 57 | -- @string action 58 | -- format_string 59 | -- @string mask 60 | -- (Optional) The mask to apply to the string. Default is to do no 61 | -- formatting. The mask has two special placeholder characters, the 62 | -- exclamation point and the underscore. To ignore a character, use the 63 | -- exclamation point. To place a character, use the underscore. 64 | -- @string string 65 | -- The string to format. 66 | -- @string storage_key 67 | -- (Optional) The key to store the formatted result under in the 'format' 68 | -- storage area. Default is 'formatted_string'. 69 | -- @usage 70 | -- { 71 | -- action = "format_string", 72 | -- mask = "!!(___) ___-____", 73 | -- string = "+15555551212", 74 | -- storage_key = "formatted_phone_number", 75 | -- }, 76 | 77 | 78 | local core = require "jester.core" 79 | 80 | local _M = {} 81 | 82 | --[[ 83 | Formats a string according to the supplied mask. 84 | ]] 85 | function _M.format_string(action) 86 | local f_string = action.string and tostring(action.string) 87 | local mask = action.mask 88 | local key = action.storage_key or "formatted_string" 89 | if f_string then 90 | local formatted_string = '' 91 | if mask and mask ~= "" then 92 | local pos = 1 93 | local string_placeholder, mask_placeholder 94 | for i = 1, string.len(mask) do 95 | string_placeholder = string.sub(f_string, pos, pos) 96 | mask_placeholder = string.sub(mask, i, i) 97 | -- Skip character placeholder, increment to the next character if it 98 | -- exists. 99 | if mask_placeholder == '!' and string_placeholder then 100 | pos = pos + 1; 101 | -- Character placeholder, insert the next character if it exists. 102 | elseif mask_placeholder == '_' and string_placeholder then 103 | formatted_string = formatted_string .. string_placeholder 104 | pos = pos + 1; 105 | -- Non-placeholder, output directly. 106 | else 107 | formatted_string = formatted_string .. mask_placeholder 108 | end 109 | end 110 | -- Extra digits get output at the end if they exist. 111 | if string.len(f_string) > pos - 1 then 112 | formatted_string = formatted_string .. string.sub(f_string, pos) 113 | end 114 | else 115 | formatted_string = f_string 116 | end 117 | core.log.debug("Formatted string '%s' to '%s'", f_string, formatted_string) 118 | core.set_storage("format", key, formatted_string) 119 | end 120 | end 121 | 122 | --[[ 123 | Formats a Unix timestamp as a date string, with timezone support. 124 | ]] 125 | function _M.format_date(action) 126 | local timestamp = action.timestamp and tostring(action.timestamp) 127 | local timezone = action.timezone or "Etc/UTC" 128 | local format = action.format or "%Y-%m-%d %H:%M:%S" 129 | local key = action.storage_key or "date" 130 | if timestamp then 131 | local api = freeswitch.API() 132 | local command = string.format("strftime_tz %s %s|%s", timezone, timestamp, format) 133 | local formatted = api:executeString(command) 134 | core.log.debug("Formatted timestamp '%s' to '%s'", timestamp, formatted) 135 | core.set_storage("format", key, formatted) 136 | end 137 | end 138 | 139 | return _M 140 | --------------------------------------------------------------------------------