Tutorial #1: Echo bot
In this tutorial, we will construct a simple echo bot: for every message that the echo bot sees, it will repeat that message. This tutorial provides a brief introduction to using the library.
The commands in this tutorial can be entered interactively into iex -S mix
to
produce a working bot.
Creating a client
We begin by starting a client process and obtaining a struct:
iex> {:ok, pid} = Polyjuice.Client.start_link(
...> "http://localhost:8008",
...> access_token: "access_token",
...> user_id: "@echobot:localhost",
...> storage: Polyjuice.Client.Storage.Ets.open(),
...> handler: self()
...> )
iex> client = Polyjuice.Client.get_client(pid)
The first parameter is the base URL to the homeserver that the bot will be
using. access_token
is an access token for the bot to use. The access token
can be obtained by using curl
to call the
login
endpoint on the homeserver, by retrieving it from an already logged-in client,
or by using mix polyjuice.login
. user_id
is the Matrix user ID of the bot.
And storage
provides some persistent storage for the client library. In the
example above, it is using the Erlang Term
Storage, which does not actually provide
any persistence. This means that, for example, the bot will forget where it
was in the sync, and so it may produce duplicate responses when it is
restarted. If you wish to have persistence, you can use the
Polyjuice.Client.Storage.Dets
module instead, which will persist the term
storage to disk. To use it, call Polyjuice.Client.Storage.Dets.open/1
with
the name of a file to use for storage.
If you use Polyjuice.Client.Storage.Dets
, then you only need to specify he
access_token
and user_id
the first time the client is run. This
information will be stored in dets and will be retrieved on subsequent runs.
Alternatively, you can use mix polyjuice.login
to log in and store the access
token and user ID.
The handler
parameter tells the client how client events (such as Matrix
messages) should be handled. By passing a PID (self()
), Erlang messages will
be sent to the given PID.
Reacting to sync messages
The sync process will now start sending several different types of messages, letting us know of many things such as the status of our connection to the Matrix server, new Matrix messages, and state changes. The messages that we are most interested in are new Matrix messages (so that we can echo them), and room invites (so that we can join).
The message for a Matrix message is of the form {:polyjuice_client, :message, {room_id, event}}
. When we receive such a message, we can use the
Polyjuice.Client.Room.send_message/3
function to respond to the message. We
will use pattern matching to make sure to only respond to m.text
messages,
and respond with a m.notice
message, so that we don't create a message loop.
We can simply take the message contents from the incoming message, change the
msgtype
, and pass that to Polyjuice.Client.Room.send_message/3
to send the
echo. A receive
statement to do this would look something like this:
receive do
{:polyjuice_client, :message, {room_id, %{"content" => %{"msgtype" => "m.text"} = content}}} ->
Polyjuice.Client.Room.send_message(
client, room_id,
%{content | "msgtype" => "m.notice"}
)
end
Room invites are of the form {:polyjuice_client, :invite, {room_id, inviter, invite_state}}
. When we get an invite, we can join the room using
Polyjuice.Client.Room.join/4
. Although that function takes four arguments,
the last two are optional, and are not needed when responding to an invite. A
receive
statement that joins a room that we're invited to would look
something like this:
receive do
{:polyjuice_client, :invite, {room_id, _inviter, _invite_state}} ->
Polyjuice.Client.Room.join(client, room_id)
end
If you just enter one of the above, you'll only be able respond to one thing. To be able to continuously respond to messages, we put this all in a function that calls itself recursively:
iex> defmodule EchoBot do
...> def loop(client) do
...> receive do
...> {:polyjuice_client, :message, {room_id, %{"content" => %{"msgtype" => "m.text"} = content}}} ->
...> Polyjuice.Client.Room.send_message(
...> client, room_id,
...> %{content | "msgtype" => "m.notice"}
...> )
...>
...> {:polyjuice_client, :invite, {room_id, _inviter, _invite_state}} ->
...> Polyjuice.Client.Room.join(client, room_id)
...>
...> _ ->
...> nil
...> end
...> loop(client)
...> end
...> end
iex> EchoBot.loop(client)
If you enter the above lines, then you can invite the bot into some rooms and it will repeat any messages that it sees. Note that some of the initial messages may get dropped due to rate limiting from the homeserver.