├── .gitignore ├── LICENSE.txt ├── README.md ├── config └── config.exs ├── lib ├── bender.ex ├── bender │ ├── bot.ex │ ├── command.ex │ ├── commands │ │ ├── echo.ex │ │ └── ping.ex │ └── parser.ex └── matrix │ ├── client.ex │ ├── config.ex │ ├── content.ex │ ├── event.ex │ ├── event_id.ex │ ├── events.ex │ ├── response_contstructer.ex │ ├── room.ex │ ├── session.ex │ └── user.ex ├── mix.exs ├── mix.lock └── test ├── bender ├── command_test.exs └── parser_test.exs ├── bender_test.exs ├── matrix └── response_constructer_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | /rel/ 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dylan Griffith 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | As I no longer use Matrix personally I'm not maintaining this tool anymore. I am aware of at least one fork that is maintained a bit more recently than this but I cannot endorse it as I do not use it https://github.com/lytedev/bender 4 | 5 | # Bender 6 | 7 | This project attempts to create a simple/flexible bot framework using the best 8 | of Elixir/Erlang/OTP. For now it only supports the [Matrix messaging 9 | protocol](http://matrix.org/), however it would be reasonably straightforward 10 | to introduce some kind of adapter pattern or something to support other chat 11 | services and would happily take PRs for something like that. The recommended 12 | use for this framework is to create your own mix project and bring this in as a 13 | dependency. You can define your own commands or just use the packaged ones and 14 | you configure that in your project. 15 | 16 | ## Commands 17 | Commands are a way of perfoming an action when someone uses the magic keyword. 18 | The command prefix must be configured, but lets assume it is `"bender"`. An 19 | example command (that comes packaged with this project) is the `echo` command. 20 | 21 | The implementation is very simple: 22 | 23 | ```elixir 24 | defmodule Bender.Commands.Echo do 25 | use Bender.Command 26 | 27 | @usage "echo " 28 | @short_description "responds with " 29 | 30 | def handle_event({{:command, "echo", message}, meta}, state) do 31 | respond(message, meta) 32 | {:ok, state} 33 | end 34 | end 35 | ``` 36 | 37 | This framework uses GenEvent for dispatching the commands. You can use all the 38 | deliciousness of pattern matching to match the commands you are interested in. 39 | 40 | The important part of the pattern match is `{:command, "echo", message}`. In 41 | the case that someone says `bender echo hello world` these will come through 42 | like `{:command, "echo", "hello world"}`. The other parts of the pattern match 43 | are just metadata/session data. You can use `meta[:author].user_id` if you want 44 | to know who sent the message. 45 | 46 | The module attributes `@usage` and `@short_description` will be included in the 47 | output of `bender help` if they are defined. 48 | 49 | The commands that your bot actually runs must be configured and you can mix and 50 | match between packaged commands and any commands you define. 51 | 52 | ## Config 53 | The config setup for this project is as follows: 54 | 55 | ```elixir 56 | config :bender, 57 | command_prefix: "bender", 58 | matrix_home_server: "matrix.org", 59 | matrix_user: "bender", 60 | matrix_password: "bender", 61 | commands: [Bender.Commands.Echo, Bender.Commands.Ping], 62 | room_names: ["#bender:matrix.org"] 63 | ``` 64 | 65 | You must set all of these when you include this dependency in your project, 66 | however you can easily run this project as is and the above credentials will 67 | log you into matrix.org and you can play around with your bot. 68 | 69 | 70 | ## Installation 71 | 72 | The package can be installed as: 73 | 74 | 1. Add bender to your list of dependencies in `mix.exs`: 75 | 76 | def deps do 77 | [{:bender, git: "https://github.com/DylanGriffith/bender.git"}] 78 | end 79 | 80 | 2. Ensure bender is started before your application: 81 | 82 | def application do 83 | [applications: [:bender]] 84 | end 85 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | config :bender, 14 | command_prefix: "bender", 15 | matrix_home_server: "matrix.org", 16 | matrix_user: "bender", 17 | matrix_password: "bender", 18 | commands: [Bender.Commands.Echo, Bender.Commands.Ping], 19 | room_names: ["#bender:matrix.org"] 20 | 21 | # 22 | # And access this configuration in your application as: 23 | # 24 | # Application.get_env(:bender, :key) 25 | # 26 | # Or configure a 3rd-party app: 27 | # 28 | # config :logger, level: :info 29 | # 30 | 31 | # It is also possible to import configuration files, relative to this 32 | # directory. For example, you can emulate configuration per environment 33 | # by uncommenting the line below and defining dev.exs, test.exs and such. 34 | # Configuration from the imported file will override the ones defined 35 | # here (which is why it is important to import them last). 36 | # 37 | # import_config "#{Mix.env}.exs" 38 | -------------------------------------------------------------------------------- /lib/bender.ex: -------------------------------------------------------------------------------- 1 | defmodule Bender do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | home_server = Application.get_env(:bender, :matrix_home_server) 10 | user = Application.get_env(:bender, :matrix_user) 11 | password = Application.get_env(:bender, :matrix_password) 12 | room_names = Application.get_env(:bender, :room_names) 13 | commands = Application.get_env(:bender, :commands) 14 | 15 | children = [ 16 | worker(Bender.Bot, [%Matrix.Config{home_server: home_server, user: user, password: password}, room_names, commands]), 17 | ] 18 | 19 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :one_for_one, name: Bender.Supervisor] 22 | Supervisor.start_link(children, opts) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/bender/bot.ex: -------------------------------------------------------------------------------- 1 | defmodule Bender.Bot do 2 | use GenServer 3 | 4 | def start_link(config = %Matrix.Config{}, room_names, commands) do 5 | GenServer.start_link(__MODULE__, [config, room_names, commands], name: __MODULE__) 6 | end 7 | 8 | def init([config = %Matrix.Config{}, room_names, commands]) do 9 | event_manager = setup_event_manager(commands) 10 | 11 | # Login 12 | session = Matrix.Client.login!(config) 13 | 14 | # Join Rooms 15 | rooms = Enum.map room_names, fn(room_name) -> 16 | Matrix.Client.join!(session, room_name) 17 | end 18 | 19 | # Trigger first poll for events 20 | GenServer.cast(self, :poll_matrix) 21 | {:ok, %{session: session, rooms: rooms, event_manager: event_manager, from: nil, commands: commands}} 22 | end 23 | 24 | def handle_cast(:poll_matrix, state = %{session: session = %Matrix.Session{}, event_manager: event_manager, from: from}) do 25 | 26 | # Get latest events 27 | events = Matrix.Client.events!(session, from) 28 | 29 | state = Dict.put state, :from, events.endd 30 | 31 | # Extract commands 32 | command_events = (events.events 33 | |> Enum.reject(fn (e) -> e.user && e.user.user_id == session.user_id end) 34 | |> Enum.filter(fn (e) -> e.type == "m.room.message" end) 35 | |> Enum.filter(fn (e) -> e.content.msg_type == "m.text" end) 36 | |> Enum.map(fn (e) -> {Bender.Parser.try_parse(e.content.body), e} end) 37 | |> Enum.filter(fn ({c, _e}) -> !is_nil(c) end)) 38 | 39 | # Dispatch commands 40 | Enum.each command_events, fn ({command, event}) -> 41 | GenEvent.notify(event_manager, {command, %{session: session, room: event.room, author: event.user}}) 42 | end 43 | 44 | # Poll again for events 45 | GenServer.cast(self, :poll_matrix) 46 | 47 | {:noreply, state} 48 | end 49 | 50 | def handle_info({:gen_event_EXIT, _, _}, state = %{commands: commands}) do 51 | state = Dict.put(state, :event_manager, setup_event_manager(commands)) 52 | {:noreply, state} 53 | end 54 | 55 | def setup_event_manager(commands) do 56 | # Start the GenEvent manager 57 | {:ok, event_manager} = GenEvent.start_link 58 | 59 | Process.monitor(event_manager) 60 | 61 | Enum.each commands, fn(c) -> 62 | :ok = GenEvent.add_mon_handler(event_manager, c, self) 63 | end 64 | 65 | event_manager 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/bender/command.ex: -------------------------------------------------------------------------------- 1 | defmodule Bender.Command do 2 | defmacro __using__(_opts) do 3 | quote do 4 | @before_compile unquote(__MODULE__) 5 | use GenEvent 6 | 7 | def respond(message, %{session: session, room: room, author: author}) do 8 | Matrix.Client.post!(session, room, message) 9 | end 10 | end 11 | end 12 | 13 | defmacro __before_compile__(_env) do 14 | quote do 15 | 16 | if Module.get_attribute(__MODULE__, :usage) && Module.get_attribute(__MODULE__, :short_description) do 17 | def handle_event({{:command, "help", ""}, meta}, parent) do 18 | message = get_help_message(@usage, @short_description) 19 | if message do 20 | respond(message, meta) 21 | end 22 | {:ok, parent} 23 | end 24 | end 25 | 26 | def handle_event(_, parent) do 27 | {:ok, parent} 28 | end 29 | 30 | def get_help_message(usage, short_description) do 31 | "`#{Application.get_env(:bender, :command_prefix)} #{usage}`: #{short_description}" 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/bender/commands/echo.ex: -------------------------------------------------------------------------------- 1 | defmodule Bender.Commands.Echo do 2 | use Bender.Command 3 | 4 | @usage "echo " 5 | @short_description "responds with " 6 | 7 | def handle_event({{:command, "echo", message}, meta}, state) do 8 | respond(message, meta) 9 | {:ok, state} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/bender/commands/ping.ex: -------------------------------------------------------------------------------- 1 | defmodule Bender.Commands.Ping do 2 | use Bender.Command 3 | 4 | @usage "ping" 5 | @short_description "responds with \"pong\"" 6 | 7 | def handle_event({{:command, "ping", _message}, meta}, state) do 8 | respond("pong", meta) 9 | {:ok, state} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/bender/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Bender.Parser do 2 | def try_parse(message, command_prefix \\ Application.get_env(:bender, :command_prefix)) do 3 | match = Regex.named_captures(~r/^\s*@?#{command_prefix}:?\s+(?[\w-]+)\s*(?.*)/i, message) 4 | if match && match["command"] do 5 | {:command, match["command"], match["message"]} 6 | else 7 | nil 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/matrix/client.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.Client do 2 | def login!(%Matrix.Config{home_server: home_server, user: user, password: password}) do 3 | data = %{user: user, password: password, type: "m.login.password"} 4 | response = HTTPoison.post!("https://#{home_server}/_matrix/client/api/v1/login", Poison.encode!(data), [], timeout: 10_000) 5 | Poison.decode!(response.body, as: Matrix.Session) 6 | end 7 | 8 | def join!(session, room_name) do 9 | room_response = HTTPoison.post!("https://#{session.home_server}/_matrix/client/api/v1/join/#{room_name}?access_token=#{session.access_token}", "", [], timeout: 10_000) 10 | Poison.decode!(room_response.body, as: Matrix.Room) 11 | end 12 | 13 | def events!(session, from \\ nil) do 14 | params = [timeout: 30000, access_token: session.access_token] 15 | 16 | if from do 17 | params = Dict.put params, :from, from 18 | end 19 | 20 | response = HTTPoison.get!("https://#{session.home_server}/_matrix/client/api/v1/events", ["Accept": "application/json"], params: params, recv_timeout: 40000, timeout: 10_000) 21 | 22 | data = Poison.decode!(response.body) 23 | Matrix.ResponseConstructer.events(data) 24 | end 25 | 26 | def post!(session = %Matrix.Session{}, room = %Matrix.Room{}, message, msg_type \\ "m.text", event_type \\ "m.room.message") do 27 | data = %{msgtype: msg_type, body: message} 28 | 29 | response = HTTPoison.post!("https://#{session.home_server}/_matrix/client/api/v1/rooms/#{room.room_id}/send/#{event_type}?access_token=#{session.access_token}", Poison.encode!(data)) 30 | 31 | Poison.decode!(response.body, as: Matrix.EventId) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/matrix/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.Config do 2 | defstruct [:home_server, :user, :password] 3 | end 4 | -------------------------------------------------------------------------------- /lib/matrix/content.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.Content do 2 | defstruct [:users, :body, :msg_type] 3 | end 4 | -------------------------------------------------------------------------------- /lib/matrix/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.Event do 2 | defstruct [:event_id, :age, :user, :room, :type, :content, :origin_server_ts] 3 | end 4 | -------------------------------------------------------------------------------- /lib/matrix/event_id.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.EventId do 2 | @derive [Poison.Encoder] 3 | defstruct [:event_id] 4 | end 5 | -------------------------------------------------------------------------------- /lib/matrix/events.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.Events do 2 | @derive [Poison.Encoder] 3 | defstruct [:events, :start, :endd] 4 | end 5 | -------------------------------------------------------------------------------- /lib/matrix/response_contstructer.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.ResponseConstructer do 2 | def events(response) do 3 | %Matrix.Events{ 4 | events: response["chunk"] |> Enum.map(&event/1), 5 | start: response["start"], 6 | endd: response["end"] 7 | } 8 | end 9 | 10 | def event(response) do 11 | %Matrix.Event{ 12 | event_id: response["event_id"], 13 | age: response["age"], 14 | content: content(response["type"], response["content"]), 15 | room: room(response["room_id"]), 16 | type: response["type"], 17 | origin_server_ts: response["origin_server_ts"], 18 | user: user(response["user_id"]), 19 | } 20 | end 21 | 22 | def content("m.typing", response) do 23 | %Matrix.Content{ 24 | users: Enum.map(response["user_ids"] , &user/1) 25 | } 26 | end 27 | 28 | def content("m.room.message", response) do 29 | %Matrix.Content{ 30 | body: response["body"], 31 | msg_type: response["msgtype"] 32 | } 33 | end 34 | 35 | def content(_type, response) do 36 | %Matrix.Content{} 37 | end 38 | 39 | def user(nil) do 40 | nil 41 | end 42 | 43 | def user(user_id) do 44 | %Matrix.User{user_id: user_id} 45 | end 46 | 47 | def room(room_id) do 48 | %Matrix.Room{room_id: room_id} 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/matrix/room.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.Room do 2 | @derive [Poison.Encoder] 3 | defstruct [:room_id] 4 | end 5 | -------------------------------------------------------------------------------- /lib/matrix/session.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.Session do 2 | @derive [Poison.Encoder] 3 | defstruct [:access_token, :home_server, :user_id] 4 | end 5 | -------------------------------------------------------------------------------- /lib/matrix/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Matrix.User do 2 | defstruct [:user_id] 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Bender.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :bender, 6 | version: "0.0.1", 7 | elixir: "~> 1.1", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | [applications: [:logger, :httpoison, :poison], 18 | mod: {Bender, []}] 19 | end 20 | 21 | # Dependencies can be Hex packages: 22 | # 23 | # {:mydep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 28 | # 29 | # Type "mix help deps" for more examples and options 30 | defp deps do 31 | [ 32 | {:httpoison, "~> 0.7"}, 33 | {:poison, "~> 1.5"}, 34 | {:exrm, "~> 0.19"}, 35 | ] 36 | end 37 | 38 | defp package do 39 | [# These are the default files included in the package 40 | files: ["lib", "priv", "mix.exs", "README.md", "LICENSE.txt",], 41 | maintainers: ["Dylan Griffith"], 42 | licenses: ["MIT"], 43 | links: %{"GitHub" => "https://github.com/DylanGriffith/bender"}] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bbmustache": {:hex, :bbmustache, "1.0.3"}, 2 | "conform": {:hex, :conform, "0.17.0"}, 3 | "erlware_commons": {:hex, :erlware_commons, "0.15.0"}, 4 | "exrm": {:hex, :exrm, "0.19.9"}, 5 | "getopt": {:hex, :getopt, "0.8.2"}, 6 | "hackney": {:hex, :hackney, "1.3.2"}, 7 | "httpoison": {:hex, :httpoison, "0.7.4"}, 8 | "idna": {:hex, :idna, "1.0.2"}, 9 | "neotoma": {:hex, :neotoma, "1.7.3"}, 10 | "poison": {:hex, :poison, "1.5.0"}, 11 | "providers": {:hex, :providers, "1.4.1"}, 12 | "relx": {:hex, :relx, "3.5.0"}, 13 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}} 14 | -------------------------------------------------------------------------------- /test/bender/command_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MyCommand do 2 | use Bender.Command 3 | 4 | def handle_event({{:command, "my_command", _m}, _conf}, parent) do 5 | send parent, :MY_COMMAND 6 | {:ok, parent} 7 | end 8 | end 9 | 10 | defmodule Bender.CommandTest do 11 | use ExUnit.Case 12 | 13 | test "it pattern matches a command" do 14 | {:ok, manager} = GenEvent.start_link 15 | GenEvent.add_handler(manager, MyCommand, self) 16 | 17 | GenEvent.notify(manager, {{:command, "my_command", "hello world"}, {}}) 18 | assert_receive :MY_COMMAND, 100 19 | end 20 | 21 | test "does not crash for commands that don't match" do 22 | {:ok, manager} = GenEvent.start_link 23 | GenEvent.add_handler(manager, MyCommand, self) 24 | 25 | :ok = GenEvent.sync_notify(manager, {{:command, "unknown_command", "hello world"}, {}}) 26 | assert GenEvent.which_handlers(manager) == [MyCommand] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/bender/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Bender.ParserTest do 2 | use ExUnit.Case 3 | 4 | test "#try_parse parses out echo command" do 5 | assert Bender.Parser.try_parse("bender echo hello world") == {:command, "echo", "hello world"} 6 | end 7 | 8 | test "#try_parse supports using @bender" do 9 | assert Bender.Parser.try_parse("@bender echo hello world") == {:command, "echo", "hello world"} 10 | end 11 | 12 | test "#try_parse supports using bender:" do 13 | assert Bender.Parser.try_parse("bender: echo hello world") == {:command, "echo", "hello world"} 14 | end 15 | 16 | test "#try_parse supports using uppercase" do 17 | assert Bender.Parser.try_parse("Bender echo hello world") == {:command, "echo", "hello world"} 18 | end 19 | 20 | test "#try_parse supports commands with hyphens" do 21 | assert Bender.Parser.try_parse("Bender foo-bar hello world") == {:command, "foo-bar", "hello world"} 22 | end 23 | 24 | test "#try_parse supports custom command_prefix" do 25 | assert Bender.Parser.try_parse("foobar echo hello world", "foobar") == {:command, "echo", "hello world"} 26 | end 27 | 28 | test "#try_parse supports empty message after command" do 29 | assert Bender.Parser.try_parse("bender ping") == {:command, "ping", ""} 30 | end 31 | 32 | test "#try_parse returns nil for invalid command" do 33 | assert Bender.Parser.try_parse("foo bar hello world") == nil 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/bender_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BenderTest do 2 | use ExUnit.Case 3 | doctest Bender 4 | end 5 | -------------------------------------------------------------------------------- /test/matrix/response_constructer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Matrix.ResponseConstructerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "#events: transforms typing events in Matrix.Events" do 5 | input = %{ 6 | "chunk" => [ 7 | %{ 8 | "content" => %{"user_ids" => ["@bob:matrix.org"]}, 9 | "room_id" => "!abc123:matrix.org", 10 | "type" => "m.typing" 11 | } 12 | ], 13 | "end" => "s12345", 14 | "start" => "s12344" 15 | } 16 | 17 | output = %Matrix.Events{ 18 | events: [ 19 | %Matrix.Event{ 20 | content: %Matrix.Content{users: [%Matrix.User{user_id: "@bob:matrix.org"}]}, 21 | room: %Matrix.Room{room_id: "!abc123:matrix.org"}, 22 | type: "m.typing" 23 | }, 24 | ], 25 | endd: "s12345", 26 | start: "s12344" 27 | } 28 | 29 | assert Matrix.ResponseConstructer.events(input) == output 30 | end 31 | 32 | test "transforms message events in Matrix.Events" do 33 | input = %{ 34 | "chunk" => [ 35 | %{ 36 | "age" => 136, 37 | "content" => %{"body" => "bender echo hello", "msgtype" => "m.text"}, 38 | "event_id" => "$1446024043133ABC:matrix.org", 39 | "origin_server_ts" => 1446024043133, 40 | "room_id" => "!abc123:matrix.org", 41 | "type" => "m.room.message", 42 | "user_id" => "@bob:matrix.org" 43 | } 44 | ], 45 | "end" => "s12345", 46 | "start" => "s12344" 47 | } 48 | 49 | output = %Matrix.Events{ 50 | events: [ 51 | %Matrix.Event{ 52 | event_id: "$1446024043133ABC:matrix.org", 53 | age: 136, 54 | content: %Matrix.Content{ 55 | body: "bender echo hello", 56 | msg_type: "m.text" 57 | }, 58 | origin_server_ts: 1446024043133, 59 | room: %Matrix.Room{room_id: "!abc123:matrix.org"}, 60 | type: "m.room.message", 61 | user: %Matrix.User{user_id: "@bob:matrix.org"} 62 | }, 63 | ], 64 | endd: "s12345", 65 | start: "s12344" 66 | } 67 | 68 | assert Matrix.ResponseConstructer.events(input) == output 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------