summaryrefslogblamecommitdiff
path: root/tutorial_welcome.md
blob: ffbf8f81d025241b12475052fbe01ce9ff1ca1c7 (plain) (tree)
1
2
3
4
5
6
7
8
9







                                                                               
                                 


                                                                          



                                                                            












                                                                               
                                                  





                                                         


                                                                
          
                                                  



























                                                                               







                                                             









                                                                                     






                                     





















































                                                                                





                                                                              










                                                                              






                                              





                                                     







                                                                      


















                                                                                                 






                                              







                                                                                        
                                                              












                                                                               
Tutorial #2: Welcome bot
========================

In this tutorial, we will create a bot that sends a welcome message to users
when they join a room.  This tutorial will illustrate setting filters on the
sync, constructing messages, and responding to state events.

As before, the commands in this tutorial can be entered interactively into `iex
-S mix` to produce a working bot.

Before continuing, you should make sure that the bot is already in a room.

We begin by starting a client.  However, this time we will apply a filter to
the sync so that we will only see the events that we are interested in.  (We
could have applied a filter to our previous bot as well, but the goal of the
previous tutorial was to just give a quick feel for the library.)

Filters can be constructed using the `Polyjuice.Client.Filter` module, which we
alias to `Filter` for brevity.  In this bot, we are only interested in
`m.room.member` events, as those are the events that will tell us when a user
has joined the room, so we want to filter out all presence events, and all
ephemeral events.

We use the `Filter.include_*_types` helper functions to create our filter.  By
default, the sync includes all event types, but if we tell it to include
certain types of events, it will only include those event types.  If we give it
an empty list, it will give none of the events.

    iex> alias Polyjuice.Client.Filter
    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(),
    ...>   sync_filter: Filter.include_presence_types([])
    ...>     |> Filter.include_ephemeral_types([])
    ...>     |> Filter.include_state_types(["m.room.member"])
    ...>     |> Filter.include_timeline_types(["m.room.member"])
    ...> )
    iex> client = Polyjuice.Client.get_client(pid)

Note that we include `m.room.member` events in both `state` and `timeline`.
This is because these refer to different sections of the sync response, rather
than to different types of events.  `state` refers to the room state before the
sync result, and `timeline` refers to the events that happened during the time
period represented by the sync, and so `m.room.member` events can show up in
both places.

## Membership events

When an `m.room.member` event is received in the sync, the sync process will
send a message of the form `{:state, room_id, event}`.  When a user joins a
room, the `membership` field will be set to `join`.  However, this also happens
when a user changes their avatar or display name.  To determine which situation
happened, we need to remember the previous room state.  There are many ways
that we could store this, but we will use storage that we have already created:
the `Polyjuice.Client.Storage` protocol allows us to use the client's storage
as an arbitrary key-value store.

The Polyjuice Client key-value storage uses a namespace to ensure that
different things that use the storage will not clash with each other.  Storage
users should ensure that the namespace that they use is unique enough.  For
this tutorial, we will use the namespace `welcomebot.<room_id>`, and for each
member in the room, we will store a `true` using the user's ID as the key.  So
the code will look something like this:

    receive do
      {
        :polyjuice_client, :state,
        {
          room_id,
          %{
            "content" => %{"membership" => "join"} = content,
            "state_key" => user_id,
            "type" => "m.room.member"
          }
        }
      } ->
        # join event: if the user was not previously a member, remember that
        # they are now a member and send them a welcome message
        namespace = "welcomebot.#{room_id}"
        unless Polyjuice.Client.Storage.kv_get(client.storage, namespace, user_id) do
          Polyjuice.Client.Storage.kv_put(client.storage, namespace, user_id, true)
          # ... send welcome message
        end
      {
        :polyjuice_client, :state,
        {
          room_id,
          %{
            "state_key" => user_id,
            "type" => "m.room.member"
          }
        }
      } ->
        # any other `m.room.member` event, i.e. not a join event, so
        # user is not a member of the room: delete their record if it was there
        namespace = "welcomebot.#{room_id}"
        Polyjuice.Client.Storage.kv_del(client.storage, namespace, user_id)
    end

## Sending messages

We omitted the code to actually send the welcome message above.  In the
previous tutorial, we saw that we could send an entire message content object
to `Polyjuice.Client.Room.send_message/3`.  However, building a message content
can be tedious if we want to add any special formatting, as we must generate
the plain text and HTML representations of the message.  So instead of doing
that, we will use the `Polyjuice.Client.MsgBuilder` module.

`Polyjuice.Client.Room.send_message/3` allows you to pass in anything that
implements the `Polyjuice.Client.MsgBuilder.MsgData` protocol.  Polyjuice
Client defines implementations for lists, strings and tuples.  So if you want
to send a simple string, you can just pass a string as the first argument to
`Polyjuice.Client.Room.send_message/3`, and it will convert to a message
content object.  If you want to send some formatted text, you can pass in a
2-tuple, where the first element is the text version and the second element is
the HTML version.  And you can pass in a combination of the above using a
list.  `Polyjuice.Client.MsgBuilder` also defines some functions for creating
other items.  Of primary interest to us is
`Polyjuice.Client.MsgBuilder.mention/2`, which generates a mention of a user.

One slight hitch for bot writers is that if you simply pass a
`Polyjuice.Client.MsgBuilder.MsgData.t` to
`Polyjuice.Client.Room.send_message/3`, it will generate a message with
`msgtype` `m.text`.  However, bots are supposed to send `m.notice` messages.
So we first need to use `Polyjuice.Client.MsgBuilder.to_message/2` to create a
message object with the correct type.  If you are writing a bot that sends many
messages, you may want to write a wrapper function around
`Polyjuice.Client.Room.send_message/3`.

Our message sending code will look something like:

    alias Polyjuice.Client.MsgBuilder
    user = MsgBuilder.mention(user_id, Map.get(content, "displayname", user_id))
    [{"Welcome", "<i><b>W</b>elcome</i>"}, ", ", user, "!"]
    |> MsgBuilder.to_message("m.notice")
    |> (&Polyjuice.Client.Room.send_message(client, room_id, &1)).()

You can change the welcome message as you wish.  In a real bot, you may not
want to add in random formatting; this was only done here for illustration
purposes.

## Avoiding stale welcome messages

When we first start the bot, there may have already been users in the room; we
don't want to send welcome messages to these users.  The sync process sends a
message of the form `{:polyjuice_client, :initial_sync_completed}` after the
first sync response has been processed.  So we can start the bot off by simply
recording the `m.room.member` events, and when the `{:polyjuice_client,
:initial_sync_completed}` message arrives, then we start sending welcome
messages.  We will do this by using a variable that tells us if the
`{:polyjuice_client, :initial_sync_completed}` message was received.

Finally, we want to ignore membership events for the bot itself; it would be a
bit silly if the bot entered a room and then sent a welcome message to itself.

The full sync processing code would then look like this:

    iex> defmodule WelcomeBot do
    ...>   def loop(client, initial_sync_completed \\ false) do
    ...>     user_id = client.user_id
    ...>     receive do
    ...>       {
    ...>         :polyjuice_client, :state,
    ...>         {
    ...>           _room_id,
    ...>           %{
    ...>             "state_key" => ^user_id,
    ...>             "type" => "m.room.member"
    ...>           }
    ...>         }
    ...>       } ->
    ...>         # ignore our own room membership
    ...>         loop(client, initial_sync_completed)
    ...>
    ...>       {
    ...>         :polyjuice_client, :state,
    ...>         {
    ...>           room_id,
    ...>           %{
    ...>             "content" => %{"membership" => "join"} = content,
    ...>             "state_key" => user_id,
    ...>             "type" => "m.room.member"
    ...>           }
    ...>         }
    ...>       } ->
    ...>         # join event: if the user was not previously a member, remember that
    ...>         # they are now a member and send them a welcome message
    ...>         namespace = "welcomebot.#{room_id}"
    ...>         unless Polyjuice.Client.Storage.kv_get(client.storage, namespace, user_id) do
    ...>           Polyjuice.Client.Storage.kv_put(client.storage, namespace, user_id, true)
    ...>
    ...>           if initial_sync_completed do
    ...>             alias Polyjuice.Client.MsgBuilder
    ...>             user = MsgBuilder.mention(user_id, Map.get(content, "displayname", user_id))
    ...>             [{"Welcome", "<i><b>W</b>elcome</i>"}, ", ", user, "!"]
    ...>             |> MsgBuilder.to_message("m.notice")
    ...>             |> (&Polyjuice.Client.Room.send_message(client, room_id, &1)).()
    ...>           end
    ...>         end
    ...>         loop(client, initial_sync_completed)
    ...>
    ...>       {
    ...>         :polyjuice_client, :state,
    ...>         {
    ...>           room_id,
    ...>           %{
    ...>             "state_key" => user_id,
    ...>             "type" => "m.room.member"
    ...>           }
    ...>         }
    ...>       } ->
    ...>         # any other `m.room.member` event, i.e. not a join event, so
    ...>         # user is not a member of the room: delete their record if it was there
    ...>         namespace = "welcomebot.#{room_id}"
    ...>         Polyjuice.Client.Storage.kv_del(client.storage, namespace, user_id)
    ...>         loop(client, initial_sync_completed)
    ...>
    ...>       {:polyjuice_client, :initial_sync_completed} ->
    ...>         loop(client, true)
    ...>
    ...>       _ ->
    ...>         loop(client, initial_sync_completed)
    ...>     end
    ...>   end
    ...> end
    iex> WelcomeBot.loop(client)

If you enter the above lines and join a room, the bot should welcome you to the
room.  Note that if you change your display name or avatar, it should not send
you a message.  However, if you leave the room and re-join, then you should
receive another welcome message.