├── .gitignore ├── LICENSE ├── README.md ├── README_RU.md ├── coveralls.json ├── lib └── bot_ex │ ├── application.ex │ ├── behaviours │ ├── buffering_strategy.ex │ ├── grouping_strategy.ex │ ├── handler.ex │ ├── hook.ex │ ├── middleware.ex │ └── middleware_parser.ex │ ├── config.ex │ ├── core │ ├── messages │ │ ├── default_buffering_strategy.ex │ │ └── default_grouping_strategy.ex │ └── middlware.ex │ ├── exceptions │ ├── behaviour_error.ex │ └── config_error.ex │ ├── handlers │ ├── module_handler.ex │ └── module_init.ex │ ├── helpers │ ├── debug.ex │ ├── tools.ex │ └── user_actions.ex │ ├── middleware │ ├── last_call_updater.ex │ └── message_logger.ex │ ├── models │ ├── button.ex │ ├── menu.ex │ └── message.ex │ ├── routing │ ├── message_handler.ex │ └── router.ex │ └── serivces │ └── analytics │ └── chat_base.ex ├── mix.exs ├── mix.lock ├── priv └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot └── test ├── config_test.exs ├── helpers ├── tools_test.exs └── user_actions_test.exs ├── menu.exs ├── routes.exs ├── routing ├── handler_test.exs └── router_test.exs ├── test_bot ├── handlers │ ├── start.ex │ └── stop.ex ├── middleware │ ├── auth.ex │ ├── message_transformer.ex │ └── text_input.ex ├── test_hook.ex └── updater.ex └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Generated on crash by NPM 11 | npm-debug.log 12 | 13 | # Static artifacts 14 | /assets/node_modules 15 | 16 | # Since we are building assets from assets/, 17 | # we ignore priv/static. You may want to comment 18 | # this depending on your deployment strategy. 19 | /priv/static/ 20 | 21 | # Files matching config/*.secret.exs pattern contain sensitive 22 | # data and you should not commit them into version control. 23 | # 24 | # Alternatively, you may comment the line below and commit the 25 | # secrets files as long as you replace their contents by environment 26 | # variables. 27 | /config/*.secret.exs 28 | .elixir_ls/ 29 | .env 30 | .idea/ 31 | *.log 32 | /logs 33 | !/logs/.gitkeep 34 | ttb_last_config 35 | *.code-workspace 36 | *.iml 37 | .vscode 38 | doc/ 39 | cover/* 40 | 41 | .devcontainer 42 | docker-compose.yml 43 | .env 44 | Dockerfile 45 | entrypoint.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 bot-ex 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 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BotEx 2 | 3 | Bot development core for `Elixir` 4 | 5 | # How it works 6 | The core is built using three key concepts: 7 | - updaters - the main task is to receive a message and send it to the handler 8 | - middleware - receive a message and transform it in some way. The first in the chain should implement the behavior `BotEx.Behaviours.MiddlewareParser`, all next - `BotEx.Behaviours.Middleware` 9 | - handlers - process the message and interact with the user. Each handler must implement a behavior `BotEx.Behaviours.Handler` 10 | 11 | # Existing libs: 12 | - [telegram](https://github.com/bot-ex/botex-telegram) 13 | 14 | # How to start: 15 | 16 | ```elixir 17 | #mix.exs 18 | def deps do 19 | [ 20 | {:botex, "~> 0.1"} 21 | ] 22 | end 23 | 24 | #full available config reference 25 | #this values set to default 26 | config :bot_ex, 27 | #path to file with config for menu buttons 28 | menu_path: "config/menu.exs", 29 | #path to file with routes aliases config 30 | routes_path: "config/routes.exs", 31 | #default time for buffering message 32 | default_buffering_time: 3000, 33 | #default buffering strategy 34 | buffering_strategy: BotEx.Core.Messages.DefaultBufferingStrategy, 35 | #default grouping strategy 36 | grouping_strategy: BotEx.Core.Messages.DefaultGroupingStrategy, 37 | #hooks that will be run after application start 38 | after_start: [], 39 | #show debug messages 40 | show_msg_log: true, 41 | #key for chat base bot analytics service 42 | analytic_key: nil, 43 | #middleware list for bots messages 44 | middleware: [], 45 | #handlers list for bots messages 46 | handlers:[], 47 | #bots list 48 | bots: [] 49 | ``` 50 | 51 | ## Example `config` 52 | ```elixir 53 | config :bot_ex, 54 | middleware: [ 55 | my_bot: [ 56 | MyBot.Middleware.MessageTransformer, 57 | MyBot.Middleware.Auth 58 | ] 59 | ], 60 | handlers: [ 61 | my_bot: [ 62 | {MyBot.Handlers.Start, 1000} # {module, buffering time} 63 | ] 64 | ], 65 | bots: [:my_bot] 66 | ``` 67 | 68 | ```bash 69 | touch config/menu.exs 70 | ``` 71 | 72 | ## Example `menu.exs` 73 | ```elixir 74 | %{ 75 | "main_menu" => %BotEx.Models.Menu{ 76 | buttons: [ 77 | [ 78 | %BotEx.Models.Button{ 79 | action: "some", 80 | data: "data", 81 | module: MyBot.Handlers.Start.get_cmd_name(), 82 | text: "This is button" 83 | } 84 | ] 85 | ] 86 | } 87 | } 88 | ``` 89 | # Routing 90 | Routes create from defined in config handlers. Each handler have function `get_cmd_name/0` that return command name for this handler. When user call `/start` command, router find module for handle this message by answer `get_cmd_name/0` value. 91 | 92 | Optionally you can create file `routes.exs` and redefine or add aliases for your commands 93 | 94 | ### Example `routes.exs` 95 | ```elixir 96 | %{ 97 | my_bot: 98 | %{"s" => MyBot.Handlers.Start} 99 | } 100 | ``` 101 | 102 | ## Example `Updater` 103 | 104 | ```elixir 105 | defmodule MyBot.Updaters.MySource do 106 | @moduledoc false 107 | 108 | use GenServer 109 | 110 | alias BotEx.Routing.MessageHandler 111 | 112 | def child_spec(opts) do 113 | %{ 114 | id: __MODULE__, 115 | start: {__MODULE__, :start_link, [opts]}, 116 | type: :worker 117 | } 118 | end 119 | 120 | @spec start_link(any) :: :ignore | {:error, any} | {:ok, pid} 121 | def start_link(state \\ []) do 122 | GenServer.start_link(__MODULE__, state, name: __MODULE__) 123 | end 124 | 125 | @spec init(any) :: {:ok, :no_state} 126 | def init(_opts) do 127 | cycle() 128 | {:ok, :no_state} 129 | end 130 | 131 | defp cycle() do 132 | Process.send_after(self(), :get_updates, 1000) 133 | end 134 | 135 | @doc """ 136 | Fetch any messages from your source 137 | """ 138 | @spec handle_info(:get_updates, map()) :: {:noreply, map()} 139 | def handle_info(:get_updates, state) do 140 | # fetch any messages from your source 141 | msgs = [] 142 | MessageHandler.handle(msgs, :my_bot) 143 | cycle() 144 | {:noreply, state} 145 | end 146 | end 147 | ``` 148 | 149 | ## Example `MessageTransformer` 150 | 151 | ```elixir 152 | defmodule MyBot.Middleware.MessageTransformer do 153 | @behaviour BotEx.Behaviours.MiddlewareParser 154 | 155 | alias BotEx.Models.Message 156 | 157 | @spec transform({binary(), binary(), binary(), map()}) :: 158 | Message.t() 159 | def transform({command, action, text, _user} = msg) do 160 | %Message{ 161 | msg: msg, 162 | text: text, 163 | date_time: Timex.local(), 164 | module: command, 165 | action: action, 166 | data: nil, 167 | from: :my_bot 168 | } 169 | end 170 | end 171 | ``` 172 | ## Example `Middleware` 173 | 174 | ```elixir 175 | defmodule MyBot.Middleware.Auth do 176 | @behaviour BotEx.Behaviours.Middleware 177 | 178 | alias BotEx.Models.Message 179 | 180 | @spec transform(Message.t()) :: Message.t() 181 | def transform(%Message{msg: {__, _, _, %{"id" => id} = user}} = msg) do 182 | %Message{msg | user: user, user_id: id} 183 | end 184 | end 185 | ``` 186 | 187 | ## Example `Handler` 188 | ```elixir 189 | defmodule MyBot.Handlers.Start do 190 | @moduledoc false 191 | 192 | use BotEx.Handlers.ModuleHandler 193 | 194 | alias BotEx.Models.Message 195 | 196 | def get_cmd_name, do: "start" 197 | 198 | @doc """ 199 | Message handler 200 | ## Parameters 201 | - msg: incoming `BotEx.Models.Message` message. 202 | """ 203 | @spec handle_message(Message.t()) :: any() 204 | def handle_message(%Message{chat_id: ch_id}) do 205 | MyBotApi.send_message(ch_id, "Hello") 206 | 207 | nil 208 | end 209 | end 210 | 211 | ``` 212 | -------------------------------------------------------------------------------- /README_RU.md: -------------------------------------------------------------------------------- 1 | # BotEx 2 | 3 | Ядро для разработки ботов на `Elixir` 4 | 5 | # Существующие библиотеки: 6 | - [telegram](https://github.com/bot-ex/botex-telegram) 7 | 8 | # Как это работает 9 | Ядро построено с использованием трёх ключевых концепций: 10 | - updaters - основная задача получить сообщение и, отправить его обработчику 11 | - middleware - получают сообщение и каким-либо образом трансформируют его. Первый в цепочке должен реализовывать поведение `BotEx.Behaviours.MiddlewareParser`, последующие - `BotEx.Behaviours.Middleware` 12 | - handlers - обрабатывают сообщение и взаимодействуют с пользователем. Каждый обработчик должен реализовывать поведение `BotEx.Behaviours.Handler` 13 | 14 | # Быстрый старт: 15 | 16 | ```elixir 17 | #mix.exs 18 | def deps do 19 | [ 20 | {:botex, "~> 0.1"} 21 | ] 22 | end 23 | 24 | #full available config reference 25 | #this values set to default 26 | config :bot_ex, 27 | #путь к файлу с настройками кнопок меню 28 | menu_path: "config/menu.exs", 29 | #путь к файлу с настройками дополнительных маршрутов 30 | routes_path: "config/routes.exs", 31 | #время по-молчанию для буферизации сообщений 32 | default_buffering_time: 3000, 33 | #стратегия буферизации сообщений 34 | buffering_strategy: BotEx.Core.Messages.DefaultBufferingStrategy, 35 | #стратегия гшруппировки сообщений 36 | grouping_strategy: BotEx.Core.Messages.DefaultGroupingStrategy, 37 | #набор хуков для запуска после старта приложения 38 | after_start: [], 39 | #показывать отладочную информацию 40 | show_msg_log: true, 41 | #ключ для сервиса аналитики ботов chat base 42 | analytic_key: nil, 43 | #список middleware для обработки сообщений от ботов 44 | middleware: [], 45 | #список ботов 46 | bots: [], 47 | #список обработчиков для сообщений 48 | handlers: [] 49 | ``` 50 | 51 | ## Пример конфига 52 | 53 | ```elixir 54 | config :bot_ex, 55 | middleware: [ 56 | my_bot: [ 57 | MyBot.Middleware.MessageTransformer, 58 | MyBot.Middleware.Auth 59 | ] 60 | ], 61 | handlers: [ 62 | my_bot: [ 63 | {MyBot.Handlers.Start, 1000} # {модуль, время буферизации сообщений} 64 | ] 65 | ], 66 | bots: [:my_bot] 67 | ``` 68 | 69 | ```bash 70 | touch config/menu.exs 71 | ``` 72 | 73 | ## Пример `menu.exs` 74 | ```elixir 75 | %{ 76 | "main_menu" => %BotEx.Models.Menu{ 77 | buttons: [ 78 | [ 79 | %BotEx.Models.Button{ 80 | action: "some", 81 | data: "data", 82 | module: MyBot.Handlers.Start.get_cmd_name(), 83 | text: "This is button" 84 | } 85 | ] 86 | ] 87 | } 88 | } 89 | ``` 90 | # Маршрутизация 91 | Маршруты создаются из определенных в конфигах обработчиков. Каждый обработчик имеет функцию `get_cmd_name/0`, которая возвращает имя команды для этого обработчика. Когда пользователь вызывает команду `/start`, маршрутизатор находит модуль для обработки этого сообщения по значению ответа` get_cmd_name/0`. 92 | 93 | При желании вы можете создать файл "routes.exs` и переопределить или добавить псевдонимы для ваших команд 94 | 95 | ### Example `routes.exs` 96 | ```elixir 97 | %{ 98 | my_bot: 99 | %{"s" => MyBot.Handlers.Start} 100 | } 101 | ``` 102 | 103 | ## Пример `Updater` 104 | 105 | ```elixir 106 | defmodule MyBot.Updaters.MySource do 107 | @moduledoc false 108 | 109 | use GenServer 110 | 111 | alias BotEx.Routing.MessageHandler 112 | 113 | def child_spec(opts) do 114 | %{ 115 | id: __MODULE__, 116 | start: {__MODULE__, :start_link, [opts]}, 117 | type: :worker 118 | } 119 | end 120 | 121 | @spec start_link(any) :: :ignore | {:error, any} | {:ok, pid} 122 | def start_link(state \\ []) do 123 | GenServer.start_link(__MODULE__, state, name: __MODULE__) 124 | end 125 | 126 | @spec init(any) :: {:ok, :no_state} 127 | def init(_opts) do 128 | cycle() 129 | {:ok, :no_state} 130 | end 131 | 132 | defp cycle() do 133 | Process.send_after(self(), :get_updates, 1000) 134 | end 135 | 136 | @doc """ 137 | Функция извлекает данные из источника 138 | """ 139 | @spec handle_info(:get_updates, map()) :: {:noreply, map()} 140 | def handle_info(:get_updates, state) do 141 | # fetch any messages from your source 142 | msgs = [] 143 | MessageHandler.handle(msgs, :my_bot) 144 | cycle() 145 | {:noreply, state} 146 | end 147 | end 148 | ``` 149 | 150 | ## Пример `MessageTransformer` 151 | 152 | ```elixir 153 | defmodule MyBot.Middleware.MessageTransformer do 154 | @behaviour BotEx.Behaviours.MiddlewareParser 155 | 156 | alias BotEx.Models.Message 157 | 158 | @spec transform({binary(), binary(), binary(), map()}) :: 159 | Message.t() 160 | def transform({command, action, text, _user} = msg) do 161 | %Message{ 162 | msg: msg, 163 | text: text, 164 | date_time: Timex.local(), 165 | module: command, 166 | action: action, 167 | data: nil 168 | } 169 | end 170 | end 171 | ``` 172 | ## Пример `Middleware` 173 | 174 | ```elixir 175 | defmodule MyBot.Middleware.Auth do 176 | @behaviour BotEx.Behaviours.Middleware 177 | 178 | alias BotEx.Models.Message 179 | 180 | @spec transform(Message.t()) :: Message.t() 181 | def transform(%Message{msg: {__, _, _, %{"id" => id} = user}} = msg) do 182 | %Message{msg | user: user, user_id: id} 183 | end 184 | end 185 | ``` 186 | 187 | ## Пример `Handler` 188 | ```elixir 189 | defmodule MyBot.Handlers.Start do 190 | @moduledoc false 191 | 192 | use BotEx.Handlers.ModuleHandler 193 | alias BotEx.Models.Message 194 | 195 | def get_cmd_name, do: "start" 196 | 197 | @doc """ 198 | Асинхронный обработчик сообщений модуля 199 | 200 | ## Параметры 201 | 202 | - `Message`: обработанное сообщение от бота 203 | """ 204 | @spec handle_message(Message.t()) :: any() 205 | def handle_message(%Message{chat_id: ch_id}) do 206 | MyBotApi.send_message(ch_id, "Hello") 207 | 208 | nil 209 | end 210 | end 211 | 212 | ``` 213 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage_options": { 3 | "treat_no_relevant_lines_as_covered": true 4 | } 5 | } -------------------------------------------------------------------------------- /lib/bot_ex/application.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Application do 2 | use Application 3 | 4 | alias BotEx.Helpers.Tools 5 | alias BotEx.Behaviours.Hook 6 | alias BotEx.Config 7 | alias BotEx.Exceptions.BehaviourError 8 | 9 | @spec start(any(), any()) :: {:error, any()} | {:ok, pid()} 10 | def start(_type, _args) do 11 | :ets.new(:last_call, [:set, :public, :named_table]) 12 | 13 | Config.init() 14 | 15 | Config.get(:grouping_strategy) 16 | |> Tools.is_behaviours?(BotEx.Behaviours.GroupingStrategy) 17 | 18 | opts = [strategy: :one_for_one, name: BotEx.Supervisor] 19 | {:ok, pid} = Supervisor.start_link([BotEx.Routing.MessageHandler], opts) 20 | 21 | run_hooks() 22 | 23 | {:ok, pid} 24 | end 25 | 26 | defp run_hooks() do 27 | Config.get(:after_start) 28 | |> Enum.each(fn hook -> 29 | unless Tools.is_behaviours?(hook, Hook) do 30 | # coveralls-ignore-start 31 | raise(BehaviourError, 32 | message: "Module #{hook} must implement behaviour BotEx.Behaviours.Hook" 33 | ) 34 | # coveralls-ignore-stop 35 | end 36 | 37 | hook.run() 38 | end) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/bot_ex/behaviours/buffering_strategy.ex: -------------------------------------------------------------------------------- 1 | # coveralls-ignore-start 2 | defmodule BotEx.Behaviours.BufferingStrategy do 3 | @moduledoc """ 4 | The behaviour for a module that implements a message buffering strategy 5 | """ 6 | alias BotEx.Models.Message 7 | 8 | @doc """ 9 | This function calling on start `BotEx.Routing.MessageHandler` 10 | and must return struct for saving incoming messages 11 | 12 | ## Parameters 13 | - handlers: handlers map as in config parameter handlers 14 | return buffer struct 15 | """ 16 | @callback create_buffers(handlers :: map()) :: map() 17 | 18 | @doc """ 19 | This function calling on receive new messages and must 20 | put it on them a place in buffer struct from `create_buffers` 21 | 22 | ## Parameters 23 | - msg_list: list of new messages 24 | - current_buffer: buffer with messages 25 | 26 | return new buffer with added messages 27 | """ 28 | @callback update_buffers_from_messages(msg_list :: [Message.t(), ...], current_buffer :: map()) :: 29 | map() 30 | 31 | @doc """ 32 | This function calling on start `BotEx.Routing.MessageHandler` 33 | and create a plan for flush all buffers from `create_buffers` 34 | 35 | ## Parameters: 36 | - handlers: map of handlers as in config 37 | - default_buffering_time: default buffering time from config, can be replaced for the handler 38 | - handler_pid: pid of `BotEx.Routing.MessageHandler` 39 | 40 | return handlers map 41 | """ 42 | @callback schedule_flush_all( 43 | handlers :: map(), 44 | default_buffering_time :: integer(), 45 | handler_pid :: pid() 46 | ) :: map() 47 | 48 | @doc """ 49 | This function call on one buffer flushing 50 | for planning next buffer flush. Must send `{:flush_buffer, bot, handler}` message to `handler_pid` 51 | 52 | ## Parameters: 53 | - key: any key for get value from buffer 54 | - buffering_time: buffering time 55 | - handler_pid: pid of `BotEx.Routing.MessageHandler` 56 | 57 | return reference from `Process.send_after` 58 | """ 59 | @callback schedule_buffer_flush( 60 | key :: any(), 61 | buffering_time :: integer(), 62 | handler_pid :: pid() 63 | ) :: 64 | reference() 65 | 66 | @doc """ 67 | Return messages for send from buffer 68 | 69 | ## Parameters: 70 | - buffer: current buffer 71 | - key: any key for getting messages from buffer 72 | 73 | return list of messages for sending 74 | """ 75 | @callback get_messages(buffer :: map(), key :: any()) :: [ 76 | Message.t(), 77 | ... 78 | ] 79 | 80 | @doc """ 81 | Reset buffer values by key 82 | 83 | ## Parameters: 84 | - buffer: current buffer 85 | - key: any key for delete messages from buffer 86 | """ 87 | @callback reset_buffer(buffer :: map(), key :: any()) :: map() 88 | end 89 | 90 | # coveralls-ignore-stop 91 | -------------------------------------------------------------------------------- /lib/bot_ex/behaviours/grouping_strategy.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Behaviours.GroupingStrategy do 2 | @moduledoc """ 3 | The behaviour for a module that implements a message buffering strategy 4 | """ 5 | alias BotEx.Models.Message 6 | 7 | @doc """ 8 | This function uses `BotEx.Routing.Router` for grouping messages by some parameters 9 | """ 10 | @callback group_and_send(msgs :: [Message.t(), ...]) :: :ok 11 | end 12 | -------------------------------------------------------------------------------- /lib/bot_ex/behaviours/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Behaviours.Handler do 2 | @moduledoc """ 3 | Basic behaviour for the Handler module 4 | """ 5 | # coveralls-ignore-start 6 | alias BotEx.Models.Message 7 | 8 | @doc """ 9 | Returns a command is responsible for module processing 10 | """ 11 | @callback get_cmd_name() :: any() | no_return() 12 | 13 | @doc """ 14 | Message handler 15 | ## Parameters 16 | - msg: incoming `BotEx.Models.Message` message. 17 | - state: current state 18 | return new state 19 | """ 20 | @callback handle_message(Message.t()) :: any() | no_return() 21 | # coveralls-ignore-stop 22 | end 23 | -------------------------------------------------------------------------------- /lib/bot_ex/behaviours/hook.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Behaviours.Hook do 2 | @moduledoc """ 3 | Basic behaviour for the Hook module 4 | """ 5 | # coveralls-ignore-start 6 | @doc """ 7 | Does something at a certain moment 8 | """ 9 | @callback run() :: any() 10 | # coveralls-ignore-stop 11 | end 12 | -------------------------------------------------------------------------------- /lib/bot_ex/behaviours/middleware.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Behaviours.Middleware do 2 | @moduledoc """ 3 | Behaviour for a module that changes the contents of `BotEx.Models.Message` 4 | """ 5 | # coveralls-ignore-start 6 | alias BotEx.Models.Message 7 | 8 | @doc """ 9 | Changes the contents of `BotEx.Models.Message` 10 | ## Parameters 11 | - msg: `BotEx.Models.Message` from `MiddlewareParser` or other `Middleware` 12 | """ 13 | @callback transform(Message.t()) :: Message.t() | atom() 14 | # coveralls-ignore-stop 15 | end 16 | -------------------------------------------------------------------------------- /lib/bot_ex/behaviours/middleware_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Behaviours.MiddlewareParser do 2 | @moduledoc """ 3 | The behaviour for the inbox transformation module 4 | """ 5 | # coveralls-ignore-start 6 | alias BotEx.Models.Message 7 | 8 | @doc """ 9 | Transforms the original message into `BotEx.Models.Message` 10 | ## Parameters 11 | - msg: any message from outer service 12 | """ 13 | @callback transform(msg :: any()) :: Message.t() 14 | # coveralls-ignore-stop 15 | end 16 | -------------------------------------------------------------------------------- /lib/bot_ex/config.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Config do 2 | alias BotEx.Core.Messages.DefaultBufferingStrategy 3 | alias BotEx.Core.Messages.DefaultGroupingStrategy 4 | 5 | @moduledoc """ 6 | Configurations module 7 | 8 | # Example: 9 | ```elixir 10 | config :bot_ex, 11 | middleware: [ 12 | my_bot: [ 13 | MyBot.Middleware.MessageTransformer, 14 | MyBot.Middleware.Auth 15 | ] 16 | ], 17 | handlers: [ 18 | my_bot: [ 19 | {MyBot.Handlers.Start, 1000} # {module, bufering time} 20 | ] 21 | ], 22 | bots: [:my_bot] 23 | ``` 24 | """ 25 | 26 | @defaults [ 27 | menu_path: "config/menu.exs", 28 | routes_path: "config/routes.exs", 29 | default_buffering_time: 3000, 30 | buffering_strategy: DefaultBufferingStrategy, 31 | grouping_strategy: DefaultGroupingStrategy, 32 | after_start: [], 33 | show_msg_log: true, 34 | analytic_key: nil, 35 | middleware: [], 36 | bots: [], 37 | handlers: [] 38 | ] 39 | 40 | @spec init :: :ok 41 | def init() do 42 | @defaults 43 | |> DeepMerge.deep_merge(Application.get_all_env(:bot_ex)) 44 | |> Enum.each(fn {name, value} -> 45 | :persistent_term.put({:bot_ex_settings, name, :config}, value) 46 | end) 47 | end 48 | 49 | @doc """ 50 | Return config value by name 51 | """ 52 | @spec get(atom(), any()) :: any() 53 | def get(param_key, default \\ nil) do 54 | :persistent_term.get({:bot_ex_settings, param_key, :config}, default) 55 | end 56 | 57 | @spec put(atom(), any()) :: any() 58 | def put(param_key, value) do 59 | :persistent_term.put({:bot_ex_settings, param_key, :config}, value) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/bot_ex/core/messages/default_buffering_strategy.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Core.Messages.DefaultBufferingStrategy do 2 | @moduledoc """ 3 | This module buffering messages for each handler of bot 4 | """ 5 | @behaviour BotEx.Behaviours.BufferingStrategy 6 | 7 | require Logger 8 | alias BotEx.Models.Message 9 | alias BotEx.Config 10 | import BotEx.Helpers.Debug, only: [print_debug: 1] 11 | 12 | # create from existings handlers buffers structure 13 | @impl true 14 | def create_buffers(handlers) do 15 | Map.new(handlers, fn {bot, hs} -> 16 | # for each bot handler create structure like 17 | # %{bot_name: %{"module_cmd" => []}} 18 | {bot, Map.new(hs, &put_handler_in_buffer/1)} 19 | end) 20 | end 21 | 22 | @impl true 23 | def update_buffers_from_messages(msg_list, nil), do: update_buffers_from_messages(msg_list, %{}) 24 | 25 | def update_buffers_from_messages(msg_list, current_buffer) do 26 | Enum.reduce(msg_list, current_buffer, &update_buffer/2) 27 | end 28 | 29 | # scheduling flush all buffers 30 | @impl true 31 | def schedule_flush_all(handlers, default_buffering_time, handler_pid) do 32 | Enum.each(handlers, fn {bot, hs} -> 33 | Enum.each(hs, &schedule_buffer_flush(&1, bot, default_buffering_time, handler_pid)) 34 | end) 35 | 36 | handlers 37 | end 38 | 39 | # single buffer flush planning 40 | @impl true 41 | def schedule_buffer_flush([bot, handler], default_buffering_time, handler_pid) do 42 | find_handler_by_name(bot, handler) 43 | |> schedule_buffer_flush(bot, default_buffering_time, handler_pid) 44 | end 45 | 46 | @spec schedule_buffer_flush( 47 | {module(), integer()} | module(), 48 | atom(), 49 | integer(), 50 | pid() 51 | ) :: reference 52 | def schedule_buffer_flush({h, time}, bot, _default_buffering_time, handler_pid), 53 | do: Process.send_after(handler_pid, {:flush_buffer, [bot, h.get_cmd_name()]}, time) 54 | 55 | def schedule_buffer_flush(h, bot, default_buffering_time, handler_pid) when is_atom(h), 56 | do: 57 | schedule_buffer_flush({h, default_buffering_time}, bot, default_buffering_time, handler_pid) 58 | 59 | @impl true 60 | def get_messages(buffer, key), do: get_in(buffer, key) 61 | 62 | @impl true 63 | def reset_buffer(buffer, key), do: put_in(buffer, key, []) 64 | 65 | @spec put_handler_in_buffer({atom(), integer()} | atom() | any()) :: tuple() 66 | defp put_handler_in_buffer({h, _time}), do: {h.get_cmd_name(), []} 67 | 68 | defp put_handler_in_buffer(h) when is_atom(h), do: {h.get_cmd_name(), []} 69 | 70 | # coveralls-ignore-start 71 | defp put_handler_in_buffer(h) do 72 | Logger.warning("Unsupported type #{inspect(h)}") 73 | end 74 | 75 | # coveralls-ignore-stop 76 | 77 | @spec update_buffer(Message.t() | atom(), map()) :: map() 78 | defp update_buffer(:ignore, old_buffer), do: old_buffer 79 | 80 | defp update_buffer(%Message{from: nil, module: nil}, old_buffer), do: old_buffer 81 | defp update_buffer(%Message{from: _, module: nil}, old_buffer), do: old_buffer 82 | defp update_buffer(%Message{from: nil, module: _}, old_buffer), do: old_buffer 83 | 84 | defp update_buffer(%Message{from: bot, module: handler} = msg, old_buffer) do 85 | update_in(old_buffer, [bot, handler], fn 86 | old_msgs when is_list(old_msgs) -> 87 | print_debug("Add message to buffer. Bot: #{bot} handler: #{handler}") 88 | Enum.concat(old_msgs, [msg]) 89 | 90 | error -> 91 | Logger.warning( 92 | "Can not add message to \"#{error}\" buffer. You may not have added this handler (#{handler}) in the bot (#{bot}) configuration" 93 | ) 94 | end) 95 | end 96 | 97 | @spec find_handler_by_name(atom(), String.t()) :: module() 98 | defp find_handler_by_name(bot, name) do 99 | Config.get(:handlers)[bot] 100 | |> Enum.filter(fn 101 | {h, _time} -> 102 | h.get_cmd_name() == name 103 | 104 | h when is_atom(h) -> 105 | h.get_cmd_name() == name 106 | 107 | # coveralls-ignore-start 108 | e -> 109 | Logger.warning("Unsupported type #{inspect(e)}") 110 | false 111 | # coveralls-ignore-stop 112 | end) 113 | |> hd() 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/bot_ex/core/messages/default_grouping_strategy.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Core.Messages.DefaultGroupingStrategy do 2 | @moduledoc """ 3 | Module grouping messages by fields: user, module and bot 4 | """ 5 | 6 | @behaviour BotEx.Behaviours.GroupingStrategy 7 | 8 | alias BotEx.Models.Message 9 | alias BotEx.Routing.Router 10 | 11 | @impl true 12 | def group_and_send(msgs) do 13 | msgs 14 | |> Enum.group_by(fn %Message{user_id: user, module: module, from: bot} -> 15 | {user, module, bot} 16 | end) 17 | |> Enum.each(fn {{_user, module, bot}, list} -> 18 | Router.handle_msgs(module, bot, list) 19 | end) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/bot_ex/core/middlware.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Core.Middleware do 2 | alias BotEx.Models.Message 3 | alias BotEx.Behaviours.{MiddlewareParser, Middleware} 4 | alias BotEx.Helpers.Tools 5 | alias BotEx.Exceptions.BehaviourError 6 | alias BotEx.Middleware.LastCallUpdater 7 | 8 | import BotEx.Helpers.Debug, only: [print_debug: 1] 9 | 10 | @spec apply_to_messages(list(), any) :: [Message.t()] 11 | def apply_to_messages([parser | middleware], msg_list) do 12 | msg_list 13 | |> Enum.map(fn msg -> 14 | print_debug("Handle message #{inspect(msg, limit: :infinity, pretty: true)}\n\nwith parser #{parser}") 15 | 16 | parser.transform(msg) 17 | |> call_middleware(middleware) 18 | end) 19 | end 20 | 21 | # apply middleware modules to one message 22 | @spec call_middleware(Message.t() | atom(), list()) :: Message.t() | atom() 23 | def call_middleware(:ignore, _) do 24 | print_debug("Call middlewares was stoped") 25 | 26 | :ignore 27 | end 28 | 29 | def call_middleware(%Message{} = msg, []), do: msg 30 | 31 | def call_middleware(%Message{} = msg, [module | rest]) do 32 | print_debug("Call middleware #{module}") 33 | 34 | module.transform(msg) 35 | |> call_middleware(rest) 36 | end 37 | 38 | # check middleware modules 39 | @spec check_middleware!(list()) :: list() | no_return() 40 | def check_middleware!([]) do 41 | # coveralls-ignore-start 42 | print_debug("No middleware was set") 43 | 44 | [] 45 | # coveralls-ignore-stop 46 | end 47 | 48 | def check_middleware!(all) do 49 | # coveralls-ignore-start 50 | Enum.each(all, fn {_, [parser | middleware]} -> 51 | unless Tools.is_behaviours?(parser, MiddlewareParser), 52 | do: 53 | raise(BehaviourError, 54 | message: "#{parser} must implement behavior BotEx.Behaviours.MiddlewareParser" 55 | ) 56 | 57 | Enum.each(middleware, fn module -> 58 | unless Tools.is_behaviours?(module, Middleware), 59 | do: 60 | raise(BehaviourError, 61 | message: "#{module} must implement behavior BotEx.Behaviours.Middleware" 62 | ) 63 | end) 64 | end) 65 | 66 | # coveralls-ignore-stop 67 | all 68 | |> add_last_call_updater() 69 | end 70 | 71 | @spec add_last_call_updater(list()) :: list() 72 | defp add_last_call_updater(middleware) do 73 | Enum.map(middleware, fn {bot, mdl} -> 74 | all = 75 | unless LastCallUpdater in mdl do 76 | mdl ++ [LastCallUpdater] 77 | else 78 | # coveralls-ignore-start 79 | mdl 80 | # coveralls-ignore-stop 81 | end 82 | 83 | {bot, all} 84 | end) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/bot_ex/exceptions/behaviour_error.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Exceptions.BehaviourError do 2 | defexception message: "behaviour not implemented" 3 | end 4 | -------------------------------------------------------------------------------- /lib/bot_ex/exceptions/config_error.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Exceptions.ConfigError do 2 | defexception message: "config key not define" 3 | end 4 | -------------------------------------------------------------------------------- /lib/bot_ex/handlers/module_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Handlers.ModuleHandler do 2 | @moduledoc """ 3 | The base macro that all message handlers should implement 4 | """ 5 | 6 | defmacro __using__(_opts) do 7 | quote do 8 | @behaviour BotEx.Behaviours.Handler 9 | 10 | alias BotEx.Models.Message 11 | alias BotEx.Helpers.UserActions 12 | alias BotEx.Exceptions.BehaviourError 13 | 14 | @doc """ 15 | Returns a command is responsible for module processing 16 | """ 17 | @impl true 18 | def get_cmd_name() do 19 | raise(BehaviourError, 20 | message: "Behaviour function #{__MODULE__}.get_cmd_name/0 is not implemented!" 21 | ) 22 | end 23 | 24 | @impl true 25 | def handle_message(_a) do 26 | raise(BehaviourError, 27 | message: "Behaviour function #{__MODULE__}.handle_message/1 is not implemented!" 28 | ) 29 | end 30 | 31 | @doc """ 32 | Changes the current message handler 33 | ## Parameters 34 | - msg: message `BotEx.Models.Message` 35 | """ 36 | @spec change_handler(Message.t()) :: true 37 | def change_handler(%Message{ 38 | user_id: u_id, 39 | module: module, 40 | is_cmd: is_cmd, 41 | action: action, 42 | data: data 43 | }) do 44 | tMsg = UserActions.get_last_call(u_id) 45 | 46 | n_t_msg = %Message{ 47 | tMsg 48 | | module: module, 49 | is_cmd: is_cmd, 50 | action: action, 51 | data: data 52 | } 53 | 54 | UserActions.update_last_call(u_id, n_t_msg) 55 | end 56 | 57 | defoverridable handle_message: 1 58 | defoverridable get_cmd_name: 0 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/bot_ex/handlers/module_init.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Handlers.ModuleInit do 2 | @moduledoc """ 3 | A simple macro to initialize a GenServer. 4 | Takes the current state structure as an argument 5 | Example 6 | ```elixir 7 | use BotEx.Handlers.ModuleInit, state: [menu: []] 8 | ``` 9 | """ 10 | 11 | defmacro __using__(opts) do 12 | quote bind_quoted: [opts: opts], location: :keep do 13 | defmodule State do 14 | defstruct opts[:state] || [] 15 | end 16 | 17 | @doc """ 18 | Module init 19 | """ 20 | def init(_opts), do: {:ok, %State{}} 21 | 22 | defoverridable init: 1 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/bot_ex/helpers/debug.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Helpers.Debug do 2 | require Logger 3 | alias BotEx.Config 4 | 5 | @doc """ 6 | Print debug message if it enabled in config 7 | """ 8 | @spec print_debug(any) :: nil | :ok 9 | def print_debug(message) do 10 | Config.get(:show_msg_log) 11 | |> do_print(message) 12 | end 13 | 14 | # coveralls-ignore-start 15 | defp do_print(true = _enable, message) when is_binary(message), do: Logger.debug(message) 16 | defp do_print(true = _enable, message), do: Logger.debug(inspect(message)) 17 | defp do_print(_, _message), do: nil 18 | # coveralls-ignore-stop 19 | end 20 | -------------------------------------------------------------------------------- /lib/bot_ex/helpers/tools.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Helpers.Tools do 2 | @moduledoc """ 3 | Secondary functions 4 | """ 5 | 6 | alias BotEx.Exceptions.ConfigError 7 | alias BotEx.Exceptions.BehaviourError 8 | 9 | @doc """ 10 | Checks the behavior is implemented by module 11 | ## Parameters 12 | - module: module for checking 13 | - behaviour: behaviour for checking 14 | """ 15 | @spec is_behaviours?(atom() | %{module_info: nil | keyword() | map()}, any()) :: boolean() 16 | def is_behaviours?(module, behaviour) do 17 | module.module_info()[:attributes] 18 | |> Keyword.get_values(:behaviour) 19 | |> List.flatten() 20 | |> Enum.member?(behaviour) 21 | end 22 | 23 | @spec check_behaviours!(atom() | %{module_info: nil | keyword() | map()}, any()) :: 24 | module() | no_return() 25 | def check_behaviours!(module, behaviour) do 26 | unless is_behaviours?(module, behaviour) do 27 | raise(BehaviourError, message: "Module #{module} must implement behaviour #{behaviour}") 28 | end 29 | 30 | module 31 | end 32 | 33 | @doc """ 34 | Checked if the file exists at the given path 35 | ## Parameters 36 | - path: file path in any valid format for `File.exists?/1` 37 | """ 38 | @spec check_path!(nil | Path.t()) :: binary() | no_return() 39 | def check_path!(nil), do: raise(ConfigError, message: "Path not set") 40 | 41 | def check_path!(path) do 42 | if File.exists?(path) do 43 | path 44 | else 45 | raise(ConfigError, message: "File '#{path}' not exists") 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/bot_ex/helpers/user_actions.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Helpers.UserActions do 2 | @moduledoc """ 3 | User actions interacting functions 4 | """ 5 | 6 | alias BotEx.Models.Message 7 | 8 | @doc """ 9 | Find the last user action 10 | ## Parameters: 11 | - u_id: user id 12 | """ 13 | @spec get_last_call(integer | binary) :: Message.t() 14 | def get_last_call(u_id) when is_binary(u_id), do: String.to_integer(u_id) |> get_last_call() 15 | def get_last_call(u_id) when is_integer(u_id) do 16 | case :ets.lookup(:last_call, u_id) do 17 | [] -> %Message{} 18 | [{_, msg}] -> msg 19 | end 20 | end 21 | 22 | @doc """ 23 | Update last user message 24 | ## Parameters: 25 | - user_id: user id 26 | - call: `BotEx.Models.Message` for saving 27 | """ 28 | @spec update_last_call(user_id :: integer() | binary(), call :: Message.t()) :: :true 29 | def update_last_call(user_id, call) when is_binary(user_id), do: String.to_integer(user_id) |> update_last_call(call) 30 | def update_last_call(user_id, %Message{} = call), do: :ets.insert(:last_call, {user_id, call}) 31 | end 32 | -------------------------------------------------------------------------------- /lib/bot_ex/middleware/last_call_updater.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Middleware.LastCallUpdater do 2 | @moduledoc """ 3 | Refresh last calls table 4 | """ 5 | 6 | @behaviour BotEx.Behaviours.Middleware 7 | 8 | alias BotEx.Models.Message 9 | alias BotEx.Helpers.UserActions 10 | 11 | @doc """ 12 | Save last user message to ets table 13 | """ 14 | @impl true 15 | def transform(%Message{is_cmd: false} = t_msg) do 16 | t_msg 17 | end 18 | 19 | def transform(%Message{user_id: user_id} = t_msg) do 20 | UserActions.update_last_call(user_id, t_msg) 21 | 22 | t_msg 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/bot_ex/middleware/message_logger.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Middleware.MessageLogger do 2 | @moduledoc """ 3 | Logs messages 4 | """ 5 | @behaviour BotEx.Behaviours.Middleware 6 | 7 | alias BotEx.Models.Message 8 | import BotEx.Helpers.Debug, only: [print_debug: 1] 9 | 10 | @doc """ 11 | Debug messages in terminal 12 | """ 13 | @impl true 14 | def transform(%Message{} = t_msg) do 15 | debug(t_msg) 16 | 17 | t_msg 18 | end 19 | 20 | defp debug(t_msg) do 21 | t_msg 22 | |> info_to_string 23 | |> print_debug() 24 | 25 | print_debug("==========") 26 | end 27 | 28 | @doc """ 29 | Create debug message from `BotEx.Models.Message` 30 | """ 31 | @spec info_to_string(Message.t()) :: String.t() 32 | def info_to_string(%Message{ 33 | action: action, 34 | module: module, 35 | data: data 36 | }) do 37 | "Route to:\n" <> 38 | " - module: #{inspect(module)} \n - action: #{action}\n - data: #{inspect(data)}" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/bot_ex/models/button.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Models.Button do 2 | @typedoc """ 3 | Module, that represents menu button struct. 4 | ## Fields: 5 | - `text`: visible text, 6 | - `module`: call module, 7 | - `action`: action, 8 | - `data`: some data 9 | """ 10 | @type t() :: %__MODULE__{ 11 | text: nil | binary(), 12 | module: nil | binary(), 13 | action: nil | binary(), 14 | data: nil | binary() 15 | } 16 | 17 | defstruct text: nil, 18 | module: nil, 19 | action: nil, 20 | data: nil 21 | end 22 | -------------------------------------------------------------------------------- /lib/bot_ex/models/menu.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Models.Menu do 2 | @typedoc """ 3 | Module, that represents menu struct. 4 | ## Fields: 5 | - `name`: menu unique identifier, 6 | - `text`: menu text, 7 | - `buttons`: list of buttons `BotEx.Models.Button` or a function that returns list of buttons 8 | - `custom`: any data 9 | """ 10 | @type t() :: %__MODULE__{ 11 | name: binary(), 12 | text: nil | binary(), 13 | buttons: [BotEx.Models.Button.t(), ...] | function(), 14 | custom: any() 15 | } 16 | 17 | defstruct name: nil, 18 | text: nil, 19 | buttons: [], 20 | custom: nil 21 | end 22 | -------------------------------------------------------------------------------- /lib/bot_ex/models/message.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Models.Message do 2 | @typedoc """ 3 | Module, that represents message wrapper struct 4 | 5 | ## 6 | - `is_cmd`: is it a command or not, 7 | - `module`: the name of the module that will be called must match the result of `BotEx.Handlers.ModuleHandler.get_cmd_name/0`, 8 | - `action`: action, 9 | - `data`: data, 10 | - `msg`: original message 11 | - `user`: current user 12 | - `user_id`: user id 13 | - `text`: message text, if any 14 | - `force_new`:- report that editing the message is undesirable 15 | - `chat_id`:- chat id 16 | - `custom_data`:- any additional data 17 | """ 18 | @type t() :: %__MODULE__{ 19 | is_cmd: boolean(), 20 | module: binary(), 21 | action: binary(), 22 | data: binary(), 23 | msg: any(), 24 | user: any(), 25 | user_id: integer(), 26 | text: binary(), 27 | date_time: any(), 28 | force_new: boolean(), 29 | chat_id: integer(), 30 | custom_data: any(), 31 | from: atom() 32 | } 33 | 34 | defstruct is_cmd: false, 35 | module: nil, 36 | action: nil, 37 | data: nil, 38 | msg: nil, 39 | user: nil, 40 | user_id: nil, 41 | text: nil, 42 | date_time: nil, 43 | force_new: false, 44 | chat_id: nil, 45 | custom_data: nil, 46 | from: nil 47 | end 48 | -------------------------------------------------------------------------------- /lib/bot_ex/routing/message_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Routing.MessageHandler do 2 | use GenServer 3 | require Logger 4 | 5 | alias BotEx.Config 6 | alias BotEx.Routing.Router 7 | alias BotEx.Core.Middleware 8 | alias BotEx.Helpers.Tools 9 | alias BotEx.Behaviours.BufferingStrategy 10 | 11 | defmodule State do 12 | @typedoc """ 13 | State for `BotEx.Routing.MessageHandler` 14 | ## Fields: 15 | - `middleware`: list all possible middleware 16 | - `message_buffer`: buffering messages 17 | - `default_buffering_time`: time buffering messages 18 | - `buffering_strategy`: strategy for buffering messages. Must implements `BotEx.Behaviours.BufferingStrategy` 19 | """ 20 | @type t() :: %__MODULE__{ 21 | middleware: list(), 22 | message_buffer: map(), 23 | default_buffering_time: integer(), 24 | buffering_strategy: module() 25 | } 26 | 27 | defstruct middleware: [], 28 | message_buffer: %{}, 29 | default_buffering_time: nil, 30 | buffering_strategy: nil 31 | end 32 | 33 | @doc """ 34 | Apply middleware modules to messages 35 | """ 36 | @spec handle(any, any) :: :ok 37 | def handle(msg_list, bot_key) do 38 | GenServer.cast(__MODULE__, {bot_key, msg_list}) 39 | end 40 | 41 | @doc """ 42 | Update config. 43 | ## Parameters 44 | - config: list of new middleware, default_buffering_time, buffering_strategy. 45 | [middleware: [`Middleware`, ...], default_buffering_time: 2000, buffering_strategy: `BufferingStrategy`] 46 | """ 47 | @spec update_config(keyword()) :: :ok 48 | def update_config(config) do 49 | GenServer.call(__MODULE__, {:update_config, config}) 50 | end 51 | 52 | @doc """ 53 | Reload config from storage 54 | """ 55 | @spec reload_config :: :ok 56 | def reload_config() do 57 | GenServer.call(__MODULE__, :reload_config) 58 | end 59 | 60 | @doc """ 61 | Return current module config 62 | [middleware: middleware, default_buffering_time: time, buffering_strategy: buffering_strategy] 63 | """ 64 | @spec get_config :: [ 65 | middleware: list(), 66 | default_buffering_time: integer(), 67 | buffering_strategy: module() 68 | ] 69 | def get_config() do 70 | GenServer.call(__MODULE__, :get_config) 71 | end 72 | 73 | @spec init(any) :: {:ok, State.t()} 74 | def init(_args) do 75 | {:ok, create_state_from_config()} 76 | end 77 | 78 | def child_spec(opts) do 79 | %{ 80 | id: __MODULE__, 81 | start: {__MODULE__, :start_link, [opts]}, 82 | type: :worker 83 | } 84 | end 85 | 86 | @spec start_link(any) :: :ignore | {:error, any} | {:ok, pid} 87 | def start_link(_) do 88 | GenServer.start_link(__MODULE__, [], name: __MODULE__) 89 | end 90 | 91 | @doc """ 92 | Apply middleware to incoming messages and buffering them 93 | ## Parameters 94 | - {bot_key, msg_list}: bot_key - atom with bot key, 95 | list - list incoming `BotEx.Models.Message` messages 96 | - state: current state 97 | """ 98 | @spec handle_cast({atom(), list()}, State.t()) :: {:noreply, State.t()} 99 | def handle_cast( 100 | {bot_key, msg_list}, 101 | %State{ 102 | middleware: all_middleware, 103 | message_buffer: old_buffer, 104 | buffering_strategy: buffering_strategy 105 | } = state 106 | ) do 107 | new_buffer = 108 | Keyword.get(all_middleware, bot_key) 109 | |> Middleware.apply_to_messages(msg_list) 110 | |> buffering_strategy.update_buffers_from_messages(old_buffer) 111 | 112 | {:noreply, %State{state | message_buffer: new_buffer}} 113 | end 114 | 115 | def handle_call({:update_config, config}, _from, state) do 116 | {:reply, :ok, update_config(state, config)} 117 | end 118 | 119 | def handle_call(:reload_config, _from, state) do 120 | %State{ 121 | middleware: all_middleware, 122 | message_buffer: old_buffer, 123 | buffering_strategy: buffering_strategy 124 | } = create_state_from_config() 125 | 126 | {:reply, :ok, 127 | %State{ 128 | state 129 | | middleware: all_middleware, 130 | message_buffer: old_buffer, 131 | buffering_strategy: buffering_strategy 132 | }} 133 | end 134 | 135 | def handle_call( 136 | :get_config, 137 | _from, 138 | %State{ 139 | middleware: middleware, 140 | default_buffering_time: time, 141 | buffering_strategy: buffering_strategy 142 | } = state 143 | ) do 144 | {:reply, 145 | [ 146 | middleware: middleware, 147 | default_buffering_time: time, 148 | buffering_strategy: buffering_strategy 149 | ], state} 150 | end 151 | 152 | @doc """ 153 | Fluhs messages to handlers 154 | """ 155 | @spec handle_info({:flush_buffer, any()}, State.t()) :: 156 | {:noreply, State.t()} 157 | def handle_info( 158 | {:flush_buffer, key}, 159 | %State{ 160 | message_buffer: buffer, 161 | default_buffering_time: default_buffering_time, 162 | buffering_strategy: buffering_strategy 163 | } = state 164 | ) do 165 | buffering_strategy.get_messages(buffer, key) 166 | |> Router.send_to_handler() 167 | 168 | buffering_strategy.schedule_buffer_flush(key, default_buffering_time, self()) 169 | 170 | {:noreply, %State{state | message_buffer: buffering_strategy.reset_buffer(buffer, key)}} 171 | end 172 | 173 | defp update_config(state, []), do: state 174 | 175 | defp update_config(state, [{:middleware, middleware} | rest]) do 176 | %State{state | middleware: Middleware.check_middleware!(middleware)} 177 | |> update_config(rest) 178 | end 179 | 180 | defp update_config(state, [{:default_buffering_time, default_buffering_time} | rest]) do 181 | %State{state | default_buffering_time: default_buffering_time} 182 | |> update_config(rest) 183 | end 184 | 185 | defp update_config(state, [{:buffering_strategy, buffering_strategy} | rest]) do 186 | %State{ 187 | state 188 | | buffering_strategy: Tools.check_behaviours!(buffering_strategy, BufferingStrategy) 189 | } 190 | |> update_config(rest) 191 | end 192 | 193 | defp update_config(state, [{_, _} | rest]) do 194 | state 195 | |> update_config(rest) 196 | end 197 | 198 | defp create_state_from_config() do 199 | middleware = 200 | Config.get(:middleware) 201 | |> Middleware.check_middleware!() 202 | 203 | default_buffering_time = Config.get(:default_buffering_time) 204 | 205 | buffering_strategy = 206 | Config.get(:buffering_strategy) 207 | |> Tools.check_behaviours!(BufferingStrategy) 208 | 209 | buffers = 210 | Config.get(:handlers) 211 | |> buffering_strategy.schedule_flush_all(default_buffering_time, self()) 212 | |> buffering_strategy.create_buffers() 213 | 214 | %State{ 215 | middleware: middleware, 216 | message_buffer: buffers, 217 | default_buffering_time: default_buffering_time, 218 | buffering_strategy: buffering_strategy 219 | } 220 | end 221 | end 222 | -------------------------------------------------------------------------------- /lib/bot_ex/routing/router.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Routing.Router do 2 | alias BotEx.Models.Message 3 | 4 | require Logger 5 | alias BotEx.Config 6 | import BotEx.Helpers.Debug, only: [print_debug: 1] 7 | 8 | @doc """ 9 | Send messages to handlers. 10 | ## Parameters: 11 | - msgs: list of `BotEx.Models.Message` 12 | """ 13 | @spec send_to_handler([Message.t(), ...]) :: :ok 14 | def send_to_handler(msgs) when is_list(msgs) do 15 | grouping_strategy = Config.get(:grouping_strategy) 16 | grouping_strategy.group_and_send(msgs) 17 | end 18 | 19 | @spec handle_msgs(module(), atom(), [Message.t(), ...]) :: nil 20 | def handle_msgs(m, bot, msgs) do 21 | routes = Map.get(get_routes(), bot) 22 | 23 | unless is_nil(routes[m]) do 24 | send_message(routes[m], msgs) 25 | else 26 | # coveralls-ignore-start 27 | Logger.error("No route found for \"#{m}\"\nAvailable routes:\n#{inspect(routes)}") 28 | msgs 29 | # coveralls-ignore-stop 30 | end 31 | 32 | nil 33 | end 34 | 35 | # Send message to the worker 36 | # ## Parameters 37 | # - info: message `BotEx.Models.Message` for sending 38 | # return `BotEx.Models.Message` 39 | @spec send_message(atom(), [Message.t(), ...]) :: [Message.t(), ...] 40 | defp send_message(module, msgs) do 41 | print_debug("Send messages to #{module}") 42 | 43 | Task.start_link(fn -> 44 | Enum.each(msgs, fn msg -> 45 | module.handle_message(msg) 46 | end) 47 | end) 48 | 49 | msgs 50 | end 51 | 52 | # return list of routes 53 | @spec get_routes() :: map() 54 | defp get_routes() do 55 | case Config.get(:routes, []) do 56 | [] -> load_routes() 57 | routes -> routes 58 | end 59 | end 60 | 61 | # load routes from file 62 | @spec load_routes() :: map() 63 | defp load_routes() do 64 | base_routes = 65 | Config.get(:handlers) 66 | |> Map.new(fn {bot, hs} -> 67 | {bot, Map.new(hs, &put_route/1)} 68 | end) 69 | 70 | path = Config.get(:routes_path) 71 | 72 | full_routes = 73 | if File.exists?(path) do 74 | {add_path, _} = Code.eval_file(path) 75 | DeepMerge.deep_merge(base_routes, add_path) 76 | else 77 | base_routes 78 | end 79 | 80 | Config.put(:routes, full_routes) 81 | 82 | full_routes 83 | end 84 | 85 | @spec put_route({module(), integer()} | module() | any()) :: tuple() 86 | defp put_route({h, _b_t}), do: {h.get_cmd_name(), h} 87 | defp put_route(h) when is_atom(h), do: {h.get_cmd_name(), h} 88 | 89 | defp put_route(error) do 90 | # coveralls-ignore-start 91 | Logger.error("Not supported definition #{inspect(error)}") 92 | # coveralls-ignore-stop 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/bot_ex/serivces/analytics/chat_base.ex: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Services.Analytics.ChatBase do 2 | @moduledoc """ 3 | Analytics Gathering Module 4 | """ 5 | # coveralls-ignore-start 6 | require Logger 7 | 8 | alias BotEx.Config 9 | alias BotEx.Exceptions.ConfigError 10 | 11 | @doc """ 12 | Sends information to analytics collection service 13 | more details about the parameters can be found 14 | in [documentation](https://chatbase.com/documentation/docs-overview) 15 | ## Parameters 16 | - msg: message 17 | - user_id: user id 18 | - intent: intent 19 | - platform: platform 20 | """ 21 | @spec send_data(binary(), binary() | integer(), binary(), binary()) :: boolean() 22 | def send_data(msg, user_id, intent, platform) do 23 | api_key = get_api_key!() 24 | 25 | response = 26 | HTTPoison.post( 27 | "https://chatbase.com/api/message", 28 | Jason.encode!(%{ 29 | "api_key" => api_key, 30 | "type" => "user", 31 | "platform" => platform, 32 | "message" => msg, 33 | "intent" => intent, 34 | "version" => "1.0", 35 | "user_id" => user_id, 36 | "time_stamp" => :os.system_time(:millisecond) 37 | }), 38 | [ 39 | {"Content-Type", "application/json"}, 40 | {"cache-control", "no-cache"} 41 | ], 42 | timeout: 1000, 43 | recv_timeout: 1000 44 | ) 45 | 46 | case response do 47 | {:ok, _} -> 48 | true 49 | 50 | {:error, e} -> 51 | Logger.error("statistics send error: #{inspect(e)}") 52 | false 53 | end 54 | end 55 | 56 | # return api key from config 57 | defp get_api_key!() do 58 | key = Config.get(:analytic_key) 59 | 60 | unless is_binary(key) do 61 | raise(ConfigError, 62 | message: 63 | "You should define a binary key in configuration (:key), to use this module, like:\n" <> 64 | "config :bot_ex, analytic_key: key" 65 | ) 66 | end 67 | end 68 | 69 | # coveralls-ignore-stop 70 | end 71 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BotEx.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :bot_ex, 7 | description: "Bot development core for Elixir", 8 | version: "1.0.2", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps(), 14 | test_coverage: [tool: ExCoveralls], 15 | preferred_cli_env: [ 16 | coveralls: :test, 17 | "coveralls.detail": :test, 18 | "coveralls.post": :test, 19 | "coveralls.html": :test 20 | ], 21 | package: [ 22 | licenses: ["MIT"], 23 | homepage_url: "https://github.com/bot-ex", 24 | links: %{"GitHub" => "https://github.com/bot-ex"} 25 | ] 26 | ] 27 | end 28 | 29 | # Configuration for the OTP application. 30 | # 31 | # Type `mix help compile.app` for more information. 32 | def application do 33 | [ 34 | mod: {BotEx.Application, []}, 35 | extra_applications: [:logger, :runtime_tools, :timex] 36 | ] 37 | end 38 | 39 | # Specifies which paths to compile per environment. 40 | defp elixirc_paths(:test), do: ["lib", "test/support", "test/test_bot"] 41 | defp elixirc_paths(_), do: ["lib"] 42 | 43 | # Specifies your project dependencies. 44 | # 45 | # Type `mix help deps` for examples and options. 46 | defp deps do 47 | [ 48 | {:gettext, "~> 0.20"}, 49 | {:exprintf, "~> 0.2"}, 50 | {:logger_file_backend, "~> 0.0.11"}, 51 | {:timex, "~> 3.7"}, 52 | {:gen_worker, "~> 0.0.5"}, 53 | {:earmark, "~> 1.4", only: :dev}, 54 | {:ex_doc, "~> 0.22", only: :dev}, 55 | {:deep_merge, "~> 1.0"}, 56 | {:jason, "~> 1.3"}, 57 | {:httpoison, "~> 1.7"}, 58 | {:excoveralls, "~> 0.18", only: :test} 59 | ] 60 | end 61 | 62 | # Aliases are shortcuts or tasks specific to the current project. 63 | # For example, to create, migrate and run the seeds file at once: 64 | # 65 | # $ mix ecto.setup 66 | # 67 | # See the documentation for `Mix` for more info on aliases. 68 | defp aliases do 69 | [ 70 | test: ["test --no-start"] 71 | ] 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 3 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, 4 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 5 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, 7 | "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, 9 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 10 | "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, 11 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 12 | "ecto": {:hex, :ecto, "3.2.0", "940e2598813f205223d60c78d66e514afe1db5167ed8075510a59e496619cfb5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, 13 | "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "envy": {:hex, :envy, "1.1.1", "0bc9bd654dec24fcdf203f7c5aa1b8f30620f12cfb28c589d5e9c38fe1b07475", [:mix], [], "hexpm"}, 15 | "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, 16 | "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, 17 | "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, 18 | "exprintf": {:hex, :exprintf, "0.2.1", "b7e895dfb00520cfb7fc1671303b63b37dc3897c59be7cbf1ae62f766a8a0314", [:mix], [], "hexpm", "20a0e8c880be90e56a77fcc82533c5d60c643915c7ce0cc8aa1e06ed6001da28"}, 19 | "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"}, 20 | "gen_worker": {:hex, :gen_worker, "0.0.9", "a4c5a06729156b9184a8b78487e14847d3541ad955be822128f1baea6c51fab6", [:mix], [{:timex, "~> 3.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2ca597309fc5b22a9c95352014ddcc646c2f2af1a267578cf6f44d2449890227"}, 21 | "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, 22 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, 23 | "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, 24 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 25 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 26 | "logger_file_backend": {:hex, :logger_file_backend, "0.0.14", "774bb661f1c3fed51b624d2859180c01e386eb1273dc22de4f4a155ef749a602", [:mix], [], "hexpm", "071354a18196468f3904ef09413af20971d55164267427f6257b52cfba03f9e6"}, 27 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 28 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"}, 29 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 30 | "mariaex": {:hex, :mariaex, "0.8.4", "5dd42a600c3949ec020cfac162a815115c9e9e406abffc1b14ffdc611d6f84bc", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, 31 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 32 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, 33 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, 34 | "nadia": {:hex, :nadia, "0.6.0", "c99b41490ea7920c114b54606451dc7c42b57c38718557e68fe5d0d3c5665cc9", [:mix], [{:httpoison, "~> 1.6.2", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 35 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 36 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 37 | "phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, 38 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 39 | "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 40 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, 41 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, 42 | "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, 43 | "plug_cowboy": {:hex, :plug_cowboy, "1.0.0", "2e2a7d3409746d335f451218b8bb0858301c3de6d668c3052716c909936eb57a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 44 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, 45 | "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm"}, 46 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, 47 | "postgrex": {:hex, :postgrex, "0.15.0", "dd5349161019caeea93efa42f9b22f9d79995c3a86bdffb796427b4c9863b0f0", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, 48 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, 49 | "scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"}, 50 | "scrivener_ecto": {:hex, :scrivener_ecto, "2.2.0", "53d5f1ba28f35f17891cf526ee102f8f225b7024d1cdaf8984875467158c9c5e", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm"}, 51 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 52 | "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, 53 | "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, 54 | "tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"}, 55 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 56 | } 57 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /test/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ConfigTest do 2 | use ExUnit.Case 3 | alias BotEx.Config 4 | 5 | 6 | test "get from config" do 7 | assert "test/menu.exs" == Config.get(:menu_path) 8 | end 9 | 10 | test "put in config" do 11 | Config.put(:test_key, "test value") 12 | assert "test value" == Config.get(:test_key) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/helpers/tools_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToolsTest do 2 | use ExUnit.Case 3 | 4 | alias BotEx.Helpers.Tools 5 | alias BotEx.Exceptions.{BehaviourError, ConfigError} 6 | 7 | test "not implemented behaviour" do 8 | assert_raise BehaviourError, fn -> 9 | Tools.check_behaviours!(TestBot.Handlers.Start, BotEx.Behaviours.Hook) 10 | end 11 | end 12 | 13 | test "not exists path" do 14 | assert_raise ConfigError, fn -> 15 | Tools.check_path!("/not/exists") 16 | end 17 | end 18 | 19 | test "path is nil" do 20 | assert_raise ConfigError, fn -> 21 | Tools.check_path!(nil) 22 | end 23 | end 24 | 25 | test "exists path" do 26 | assert Tools.check_path!("test") == "test" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/helpers/user_actions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule UserActionsTest do 2 | use ExUnit.Case 3 | 4 | alias BotEx.Helpers.UserActions 5 | alias BotEx.Models.Message 6 | 7 | test "user id is binary" do 8 | assert %Message{} == UserActions.get_last_call("1") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/menu.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | "main_menu" => %BotEx.Models.Menu{ 3 | buttons: [ 4 | [ 5 | %BotEx.Models.Button{ 6 | action: "some", 7 | data: "data", 8 | module: TestBot.Handlers.Start.get_cmd_name(), 9 | text: "This is button" 10 | } 11 | ] 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/routes.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | test_bot: 3 | %{"s" => TestBot.Handlers.Start} 4 | } 5 | -------------------------------------------------------------------------------- /test/routing/handler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HandlerTest do 2 | use ExUnit.Case 3 | 4 | alias BotEx.Helpers.UserActions 5 | alias BotEx.Models.Message 6 | 7 | setup do 8 | on_exit(fn -> 9 | :ets.delete_all_objects(:last_call) 10 | end) 11 | end 12 | 13 | test "check send message with selected time" do 14 | msg = {"start", nil, nil, %{"id" => 1}, self()} 15 | 16 | TestBot.Updater.send_message(msg) 17 | assert_receive msg, 1100 18 | end 19 | 20 | test "check send message with default time" do 21 | msg = {"stop", nil, nil, %{"id" => 1}, self()} 22 | 23 | TestBot.Updater.send_message(msg) 24 | assert_receive msg, 600 25 | end 26 | 27 | test "check send text message" do 28 | msg = {"start", nil, nil, %{"id" => 1}, self()} 29 | 30 | TestBot.Updater.send_message(msg) 31 | assert_receive msg, 1100 32 | 33 | msg = {nil, nil, "test", %{"id" => 1}, self()} 34 | TestBot.Updater.send_message(msg) 35 | assert_receive msg, 1100 36 | end 37 | 38 | test "check change handler" do 39 | msg = {"start", nil, nil, %{"id" => 1}, self()} 40 | 41 | TestBot.Updater.send_message(msg) 42 | assert_receive msg, 1100 43 | 44 | old = UserActions.get_last_call(1) 45 | assert %Message{module: "start", action: nil, data: nil} = old 46 | 47 | TestBot.Handlers.Start.change_handler(%Message{old | module: "stop"}) 48 | assert %Message{module: "stop", action: nil, data: nil} = UserActions.get_last_call(1) 49 | end 50 | 51 | test "check binary id" do 52 | msg = {"start", nil, nil, %{"id" => "1"}, self()} 53 | 54 | TestBot.Updater.send_message(msg) 55 | assert_receive msg, 1100 56 | end 57 | 58 | test "check not exists route" do 59 | msg = {"not exists", nil, nil, %{"id" => "1"}, self()} 60 | 61 | TestBot.Updater.send_message(msg) 62 | refute_receive _, 1100 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/routing/router_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RouterTest do 2 | use ExUnit.Case 3 | 4 | alias BotEx.Config 5 | 6 | test "test reset routes" do 7 | old_routes = Config.get(:routes) 8 | old_path = Config.get(:routes_path) 9 | 10 | Application.stop(:bot_ex) 11 | Application.start(:bot_ex) 12 | 13 | Config.put(:routes, []) 14 | Config.put(:routes_path, "/not/exists") 15 | 16 | msg = {"start", nil, nil, %{"id" => 1}, self()} 17 | 18 | TestBot.Updater.send_message(msg) 19 | assert_receive msg, 1100 20 | 21 | Application.stop(:bot_ex) 22 | Application.start(:bot_ex) 23 | 24 | Config.put(:routes, old_routes) 25 | Config.put(:routes_path, old_path) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/test_bot/handlers/start.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBot.Handlers.Start do 2 | @moduledoc false 3 | 4 | use BotEx.Handlers.ModuleHandler 5 | 6 | alias BotEx.Models.Message 7 | 8 | def get_cmd_name, do: "start" 9 | 10 | def handle_message(%Message{text: "test", msg: {_, _, _, _, pid} = msg}) do 11 | send(pid, msg) 12 | end 13 | 14 | def handle_message(%Message{action: nil, msg: {_, _, _, _, pid} = msg}) do 15 | send(pid, msg) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_bot/handlers/stop.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBot.Handlers.Stop do 2 | @moduledoc false 3 | 4 | use BotEx.Handlers.ModuleHandler 5 | 6 | alias BotEx.Models.Message 7 | 8 | def get_cmd_name, do: "stop" 9 | 10 | def handle_message(%Message{msg: {_, _, _, _, pid} = msg}) do 11 | send(pid, msg) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_bot/middleware/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBot.Middleware.Auth do 2 | @behaviour BotEx.Behaviours.Middleware 3 | 4 | alias BotEx.Models.Message 5 | 6 | @spec transform(Message.t()) :: Message.t() 7 | def transform(%Message{msg: {__, _, _, %{"id" => id} = user, _pid}} = msg) do 8 | %Message{msg | user: user, user_id: id} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/test_bot/middleware/message_transformer.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBot.Middleware.MessaegTransformer do 2 | @moduledoc """ 3 | Convert telegram message to `BotEx.Models.Message` 4 | """ 5 | @behaviour BotEx.Behaviours.MiddlewareParser 6 | 7 | alias BotEx.Models.Message 8 | 9 | @spec transform({binary(), binary(), binary(), map()}) :: 10 | Message.t() 11 | def transform({command, action, text, _user, _pid} = msg) do 12 | %Message{ 13 | msg: msg, 14 | text: text, 15 | date_time: Timex.local(), 16 | module: command, 17 | action: action, 18 | data: nil, 19 | from: :test_bot, 20 | is_cmd: not is_nil(command) 21 | } 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/test_bot/middleware/text_input.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBot.Middleware.TextInput do 2 | @moduledoc """ 3 | Refresh last calls table 4 | """ 5 | 6 | @behaviour BotEx.Behaviours.Middleware 7 | 8 | alias BotEx.Models.Message 9 | alias BotEx.Helpers.UserActions 10 | 11 | @spec transform(Message.t()) :: Message.t() 12 | def transform(%Message{user_id: user_id, text: text, is_cmd: false, msg: msg}) do 13 | %Message{ UserActions.get_last_call(user_id) | text: text, is_cmd: false, msg: msg } 14 | end 15 | 16 | def transform(%Message{} = t_msg) do 17 | t_msg 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_bot/test_hook.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBot.TestHook do 2 | @moduledoc false 3 | 4 | @behaviour BotEx.Behaviours.Hook 5 | import BotEx.Helpers.Debug, only: [print_debug: 1] 6 | 7 | def run() do 8 | print_debug("run hook") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/test_bot/updater.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBot.Updater do 2 | @moduledoc false 3 | 4 | use GenServer 5 | 6 | alias BotEx.Routing.MessageHandler 7 | 8 | def send_message(msg) do 9 | GenServer.call(__MODULE__, msg) 10 | end 11 | 12 | @spec start_link(any) :: :ignore | {:error, any} | {:ok, pid} 13 | def start_link(state \\ []) do 14 | GenServer.start_link(__MODULE__, state, name: __MODULE__) 15 | end 16 | 17 | @spec init(any) :: {:ok, :no_state} 18 | def init(_opts) do 19 | {:ok, :no_state} 20 | end 21 | 22 | def handle_call(msg, _from, state) do 23 | msgs = [msg] 24 | MessageHandler.handle(msgs, :test_bot) 25 | {:reply, :ok, state} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | config = [ 2 | {:bot_ex, 3 | show_msg_log: true, 4 | default_buffering_time: 500, 5 | menu_path: "test/menu.exs", 6 | routes_path: "test/routes.exs", 7 | bots: [:test_bot], 8 | middleware: [ 9 | test_bot: [ 10 | TestBot.Middleware.MessaegTransformer, 11 | TestBot.Middleware.Auth, 12 | TestBot.Middleware.TextInput, 13 | BotEx.Middleware.MessageLogger 14 | ] 15 | ], 16 | after_start: [TestBot.TestHook], 17 | handlers: [ 18 | test_bot: [ 19 | {TestBot.Handlers.Start, 100}, 20 | TestBot.Handlers.Stop 21 | ] 22 | ]} 23 | ] 24 | 25 | Application.put_all_env(config) 26 | Application.ensure_all_started(:bot_ex) 27 | 28 | TestBot.Updater.start_link() 29 | 30 | ExUnit.start() 31 | --------------------------------------------------------------------------------