summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHubert Chathi <hubert@uhoreg.ca>2020-08-21 19:03:00 -0400
committerHubert Chathi <hubert@uhoreg.ca>2020-08-21 19:03:00 -0400
commit062b89962018f88c9390d7ba0822a99c68756c94 (patch)
tree982cbd9b21194284110c367a65f76890e9a072cc
parentadd lower-level client, acting like the old client (diff)
automatically start sync process
-rw-r--r--lib/polyjuice/client.ex183
-rw-r--r--lib/polyjuice/client/application.ex29
-rw-r--r--lib/polyjuice/client/handler.ex65
-rw-r--r--lib/polyjuice/client/sync.ex66
-rw-r--r--mix.exs1
-rw-r--r--test/polyjuice/client/media_test.exs3
-rw-r--r--test/polyjuice/client/sync_test.exs121
-rw-r--r--test/polyjuice/client_test.exs97
-rw-r--r--tutorial_echo.md54
-rw-r--r--tutorial_welcome.md111
10 files changed, 421 insertions, 309 deletions
diff --git a/lib/polyjuice/client.ex b/lib/polyjuice/client.ex
index 81415d1..2a15d28 100644
--- a/lib/polyjuice/client.ex
+++ b/lib/polyjuice/client.ex
@@ -16,8 +16,8 @@ defmodule Polyjuice.Client do
@moduledoc """
Matrix client functions.
- To create a client, use `start/2`, and to destroy it, use
- `stop/1`.
+ To start a client, use `start_link/2`, and to stop it, use
+ `stop/3`.
The struct in this module, or any struct that implements the
`Polyjuice.Client.API` protocol, can be used to connect to a Matrix server
@@ -28,8 +28,8 @@ defmodule Polyjuice.Client do
- `Polyjuice.Client.Media`: use the media repository
- `Polyjuice.Client.MsgBuilder`: build message contents
- To sync with the homeserver, start a process using the child spec returned by
- `Polyjuice.Client.API.sync_child_spec/3`.
+ The client defined in this module should work for most cases. If you want
+ more control, you can use `Polyjuice.Client.LowLevel` instead.
"""
require Logger
@@ -37,25 +37,28 @@ defmodule Polyjuice.Client do
@typedoc """
Matrix client.
- This struct is created by `start/1`.
+ This struct is created by `start_link/2`.
"""
# - `base_url`: Required. The base URL for the homeserver.
- # - `pid`: PID for the agent that contains the state.
+ # - `id`: An ID for the client.
# - `storage`: Required for some endpoints and for sync. Storage for the client.
# - `test`: if the client is used for a unit test (converts POST requests to PUT,
# to make mod_esi happy)
@opaque t :: %__MODULE__{
base_url: String.t(),
- pid: pid,
+ id: integer,
storage: Polyjuice.Client.Storage.t(),
+ handler: Polyjuice.Client.Handler.t(),
test: boolean
}
- @enforce_keys [:base_url, :pid]
+ @enforce_keys [:base_url, :id]
defstruct [
:base_url,
- :pid,
+ :id,
:storage,
+ :handler,
+ sync: true,
test: false
]
@@ -67,57 +70,84 @@ defmodule Polyjuice.Client do
access token to use.
- `user_id`: (required by some endpoints) the ID of the user
- `device_id`: the device ID
- - `storage`: (required by sync) the storage backend to use (see
+ - `storage`: (required for sync) the storage backend to use (see
`Polyjuice.Client.Storage`)
+ - `handler`: (required for sync) an event handler (see `Polyjuice.Client.Handler`)
+ - `sync`: whether to start a sync process (defaults to true). The sync process
+ will not start if there is no `storage` or `handler` provided.
+ - `sync_filter`: the filter to use for the sync. Defaults to no filter.
"""
- @spec start(base_url :: String.t(), opts :: Keyword.t()) :: t()
- def start(base_url, opts \\ []) when is_binary(base_url) do
- {:ok, pid} =
- Agent.start(fn ->
- %{
- access_token: Keyword.get(opts, :access_token),
- user_id: Keyword.get(opts, :user_id),
- device_id: Keyword.get(opts, :device_id)
- }
- end)
+ @spec start_link(base_url :: String.t(), opts :: Keyword.t()) :: t()
+ def start_link(base_url, opts \\ []) when is_binary(base_url) and is_list(opts) do
+ base_url =
+ if(String.ends_with?(base_url, "/"), do: base_url, else: base_url <> "/")
+ |> URI.parse()
+
+ client_id = Agent.get_and_update(Polyjuice.Client.ID, fn id -> {id, id + 1} end)
+
+ access_token = Keyword.get(opts, :access_token)
+ user_id = Keyword.get(opts, :user_id)
+ device_id = Keyword.get(opts, :device_id)
+ sync = Keyword.get(opts, :sync, true)
+ storage = Keyword.get(opts, :storage)
+ handler = Keyword.get(opts, :handler)
+
+ children = [
+ %{
+ id: Polyjuice.Client,
+ start:
+ {Agent, :start_link,
+ [
+ fn ->
+ %{
+ access_token: access_token,
+ user_id: user_id,
+ device_id: device_id
+ }
+ end,
+ [name: process_name(client_id, :state)]
+ ]}
+ }
+ | if(sync and access_token != nil and handler != nil and storage != nil,
+ do: [sync_child_spec(base_url, client_id, opts)],
+ else: []
+ )
+ ]
+
+ {:ok, _pid} =
+ Supervisor.start_link(children,
+ strategy: :rest_for_one,
+ name: process_name(client_id, :supervisor)
+ )
%__MODULE__{
- base_url:
- if(String.ends_with?(base_url, "/"), do: base_url, else: base_url <> "/")
- |> URI.parse(),
- pid: pid,
- storage: Keyword.get(opts, :storage),
+ base_url: base_url,
+ id: client_id,
+ storage: storage,
+ handler: handler,
+ sync: sync,
test: Keyword.get(opts, :test, false)
}
end
@doc """
- Start a client, linking the client process to the calling process.
-
- See `start/2`.
+ Stop a client.
"""
- @spec start_link(base_url :: String.t(), opts :: Keyword.t()) :: t()
- def start_link(base_url, opts \\ []) when is_binary(base_url) do
- {:ok, pid} =
- Agent.start_link(fn ->
- %{
- access_token: Keyword.get(opts, :access_token),
- user_id: Keyword.get(opts, :user_id)
- }
- end)
+ @spec stop(Polyjuice.Client.t(), reason :: term, timeout()) :: :ok
+ def stop(%__MODULE__{id: id}, reason \\ :normal, timeout \\ :infinity) do
+ Supervisor.stop(process_name(id, :supervisor), reason, timeout)
+ end
- %__MODULE__{
- base_url:
- if(String.ends_with?(base_url, "/"), do: base_url, else: base_url <> "/")
- |> URI.parse(),
- pid: pid,
- storage: Keyword.get(opts, :storage),
- test: Keyword.get(opts, :test, false)
- }
+ @doc false
+ def process_name(id, process) do
+ {:via, Registry, {Polyjuice.Client, {id, process}}}
end
- def stop(%__MODULE__{pid: pid}) do
- Process.exit(pid, :normal)
+ defp sync_child_spec(base_url, client_id, opts) do
+ %{
+ id: Polyjuice.Client.Sync,
+ start: {Task, :start_link, [Polyjuice.Client.Sync, :sync, [base_url, client_id, opts]]}
+ }
end
@doc "The r0 client URL prefix"
@@ -149,36 +179,10 @@ defmodule Polyjuice.Client do
"""
@spec transaction_id(client_api :: Polyjuice.Client.API.t()) :: String.t()
def transaction_id(client_api)
-
- @doc """
- Get the child spec for the sync process.
-
- `listener` will receive messages with the sync results. Messages include:
-
- - `{:connected}`: the process has connected to the homeserver
- - `{:disconnected}`: the process has been disconnected from the homeserver
- - `{:initial_sync_completed}`: the first sync has completed
- - `{:limited, room_id, prev_batch}`: a room's timeline has been limited.
- Previous messages can be fetched using the `prev_batch`
- - `{:message, room_id, event}`: a message event has been received
- - `{:state, room_id, event}`: a state event has been received
- - `{:invite, room_id, inviter, invite_state}`: the user was invited to a room
- - `{:left, room_id}`: the user left a room
-
- `opts` is a keyword list of options:
-
- - `filter:` (string or map) the filter to use with the sync
- """
- @spec sync_child_spec(
- client_api :: Polyjuice.Client.API.t(),
- listener :: pid(),
- opts :: list()
- ) :: map()
- def sync_child_spec(client_api, listener, opts \\ [])
end
defimpl Polyjuice.Client.API do
- def call(%{base_url: base_url, pid: pid, test: test}, endpoint) do
+ def call(%{base_url: base_url, id: id, test: test}, endpoint) do
%Polyjuice.Client.Endpoint.HttpSpec{
method: method,
headers: headers,
@@ -192,7 +196,9 @@ defmodule Polyjuice.Client do
access_token =
if auth_required do
- Agent.get(pid, fn %{access_token: access_token} -> access_token end)
+ Agent.get(Polyjuice.Client.process_name(id, :state), fn %{access_token: access_token} ->
+ access_token
+ end)
else
nil
end
@@ -238,10 +244,6 @@ defmodule Polyjuice.Client do
def transaction_id(_) do
"#{Node.self()}_#{:erlang.system_time(:millisecond)}_#{:erlang.unique_integer()}"
end
-
- def sync_child_spec(client, listener, opts \\ []) do
- Polyjuice.Client.Sync.child_spec([client, listener | opts])
- end
end
@doc false
@@ -260,9 +262,9 @@ defmodule Polyjuice.Client do
@doc """
Synchronize messages from the server.
- Normally, you should create a sync process using
- `Polyjuice.Client.API.sync_child_spec/3` rather than calling this function, but
- this function may be used where more control is needed.
+ Normally, you should use `Polyjuice.Client`'s built-in sync process rather
+ than calling this function, but this function may be used where more control
+ is needed.
`opts` is a keyword list of options:
@@ -291,6 +293,7 @@ defmodule Polyjuice.Client do
)
end
+ @doc false
def make_login_identifier(identifier) do
case identifier do
x when is_binary(x) ->
@@ -353,10 +356,18 @@ defmodule Polyjuice.Client do
}
)
- Agent.cast(client.pid, fn state ->
+ Agent.cast(process_name(client.id, :state), fn state ->
%{state | access_token: access_token, user_id: user_id, device_id: device_id}
end)
+ if client.handler do
+ Polyjuice.Client.Handler.handle(
+ client.handler,
+ :logged_in,
+ {user_id, device_id, Map.drop(ret, ["user_id", "device_id"])}
+ )
+ end
+
ret
end
@@ -371,7 +382,11 @@ defmodule Polyjuice.Client do
%Polyjuice.Client.Endpoint.PostLogout{}
)
- Agent.cast(client.pid, fn state -> %{state | access_token: nil} end)
+ Agent.cast(process_name(client.id, :state), fn state -> %{state | access_token: nil} end)
+
+ if client.handler do
+ Polyjuice.Client.Handler.handle(client.handler, :logged_out)
+ end
{:ok}
end
diff --git a/lib/polyjuice/client/application.ex b/lib/polyjuice/client/application.ex
new file mode 100644
index 0000000..b6c87cc
--- /dev/null
+++ b/lib/polyjuice/client/application.ex
@@ -0,0 +1,29 @@
+# Copyright 2020 Hubert Chathi <hubert@uhoreg.ca>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule Polyjuice.Client.Application do
+ use Application
+
+ def start(_type, _args) do
+ children = [
+ {Registry, keys: :unique, name: Polyjuice.Client},
+ %{
+ id: Polyjuice.Client.ID,
+ start: {Agent, :start_link, [fn -> 0 end, [name: Polyjuice.Client.ID]]}
+ }
+ ]
+
+ Supervisor.start_link(children, strategy: :one_for_one)
+ end
+end
diff --git a/lib/polyjuice/client/handler.ex b/lib/polyjuice/client/handler.ex
new file mode 100644
index 0000000..e58952c
--- /dev/null
+++ b/lib/polyjuice/client/handler.ex
@@ -0,0 +1,65 @@
+# Copyright 2020 Hubert Chathi <hubert@uhoreg.ca>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defprotocol Polyjuice.Client.Handler do
+ @moduledoc """
+ Protocol for client event handlers.
+ """
+
+ @doc """
+ Handle an event from the client.
+
+ Events (in the form `type, args`) may include:
+
+ - `:logged_in`, `{user_id, device_id, login_info}`: the user was logged in.
+ - `:logged_out`, `{soft_logout}`: the user was logged out.
+ - `:sync_connected`, `nil`: the sync process has connected to the homeserver
+ - `:sync_disconnected`, `nil`: the sync process has been disconnected from
+ the homeserver
+ - `:initial_sync_completed`, `nil`: the first sync after login has completed
+ - `:limited`, `{room_id, prev_batch}` a room's timeline has been limited.
+ Previous messages can be fetched using the `prev_batch`
+ - `:message`, `{room_id, event}`: a message event has been received
+ - `:state`, `{room_id, event}`: a state event has been received
+ - `:invite`, `{room_id, inviter, invite_state}`: the user was invited to a room
+ - `:left`, `{room_id}`: the user left a room
+
+ If a PID is used as a handler, the process will be sent a message of the form
+ `{:polyjuice_client, type}`, if `args` is `nil`, or `{:polyjuice_client, type,
+ args}` otherwise.
+
+ If a function is used as a handler, it must have arity 2, and will be given
+ `type` and `args` as its arguments.
+
+ """
+ @spec handle(handler :: Polyjuice.Client.Handler.t(), event_type :: atom, args :: tuple | nil) ::
+ any
+ def handle(handler, event_type, args \\ nil)
+end
+
+defimpl Polyjuice.Client.Handler, for: PID do
+ def handle(pid, event_type, nil) do
+ send(pid, {:polyjuice_client, event_type})
+ end
+
+ def handle(pid, event_type, args) do
+ send(pid, {:polyjuice_client, event_type, args})
+ end
+end
+
+defimpl Polyjuice.Client.Handler, for: Function do
+ def handle(func, event_type, args) do
+ apply(func, [event_type, args])
+ end
+end
diff --git a/lib/polyjuice/client/sync.ex b/lib/polyjuice/client/sync.ex
index df1e13b..1f6e95b 100644
--- a/lib/polyjuice/client/sync.ex
+++ b/lib/polyjuice/client/sync.ex
@@ -21,17 +21,11 @@ defmodule Polyjuice.Client.Sync do
@doc """
Start a sync task.
"""
- @spec start_link([...]) :: {:ok, pid}
- def start_link([client, listener | opts])
- when (is_pid(listener) or is_function(listener)) and is_list(opts) do
- Task.start_link(__MODULE__, :sync, [client, listener, opts])
- end
-
- @enforce_keys [:send, :pid, :homeserver_url, :uri, :storage]
+ @enforce_keys [:client_state, :homeserver_url, :uri, :storage]
defstruct [
- :send,
+ :handler,
:conn_ref,
- :pid,
+ :client_state,
:homeserver_url,
:uri,
:storage,
@@ -49,22 +43,19 @@ defmodule Polyjuice.Client.Sync do
@doc false
def sync(
- %Polyjuice.Client{
- pid: pid,
- base_url: homeserver_url,
- storage: storage,
- test: test
- },
- listener,
+ homeserver_url,
+ id,
opts
) do
- homeserver_url = homeserver_url
+ storage = Keyword.fetch!(opts, :storage)
+ handler = Keyword.fetch!(opts, :handler)
+ test = Keyword.get(opts, :test, false)
# Figure out how to handle the filter (if any): can we pass it in straight
# to the query, or do we need to get its ID. And if we get its ID, do we
# already have it, or do we need to send it to the server?
{filter, set_filter} =
- case Keyword.get(opts, :filter) do
+ case Keyword.get(opts, :sync_filter) do
nil ->
{nil, nil}
@@ -97,8 +88,8 @@ defmodule Polyjuice.Client.Sync do
uri = URI.merge(homeserver_url, @sync_path)
connect(%__MODULE__{
- send: if(is_function(listener), do: listener, else: &send(listener, &1)),
- pid: pid,
+ handler: handler,
+ client_state: Polyjuice.Client.process_name(id, :state),
homeserver_url: homeserver_url,
uri: uri,
query_params: query_params,
@@ -122,7 +113,7 @@ defmodule Polyjuice.Client.Sync do
|> :hackney.connect(options) do
{:ok, conn_ref} ->
Logger.info("Connected to sync")
- state.send.({:connected})
+ Polyjuice.Client.Handler.handle(state.handler, :sync_connected)
if state.set_filter do
set_filter(%{state | conn_ref: conn_ref, backoff: nil})
@@ -146,9 +137,12 @@ defmodule Polyjuice.Client.Sync do
e = &URI.encode_www_form/1
{access_token, user_id} =
- Agent.get(state.pid, fn %{access_token: access_token, user_id: user_id} ->
- {access_token, user_id}
- end)
+ Agent.get(
+ state.client_state,
+ fn %{access_token: access_token, user_id: user_id} ->
+ {access_token, user_id}
+ end
+ )
path =
URI.merge(
@@ -209,7 +203,7 @@ defmodule Polyjuice.Client.Sync do
backoff = calc_backoff(state.backoff)
Logger.error("Set filter error: closed; retrying in #{backoff} seconds.")
connect(%{state | backoff: backoff, conn_ref: nil})
- state.send.({:disconnected})
+ Polyjuice.Client.Handler.handle(state.handler, :sync_disconnected)
# FIXME: what other error codes do we need to handle?
{:error, err} ->
@@ -223,7 +217,11 @@ defmodule Polyjuice.Client.Sync do
defp do_sync(state) do
if state.backoff, do: :timer.sleep(state.backoff * 1000)
- access_token = Agent.get(state.pid, fn %{access_token: access_token} -> access_token end)
+ access_token =
+ Agent.get(
+ state.client_state,
+ fn %{access_token: access_token} -> access_token end
+ )
headers = [
{"Accept", "application/json"},
@@ -249,7 +247,7 @@ defmodule Polyjuice.Client.Sync do
Polyjuice.Client.Storage.set_sync_token(state.storage, next_batch)
if not state.initial_done do
- state.send.({:initial_sync_completed})
+ Polyjuice.Client.Handler.handle(state.handler, :initial_sync_completed)
end
do_sync(%{state | since: next_batch, backoff: nil, initial_done: true})
@@ -276,7 +274,7 @@ defmodule Polyjuice.Client.Sync do
backoff = calc_backoff(state.backoff)
Logger.error("Sync error: closed; retrying in #{backoff} seconds.")
connect(%{state | backoff: backoff, conn_ref: nil})
- state.send.({:disconnected})
+ Polyjuice.Client.Handler.handle(state.handler, :sync_disconnected)
# FIXME: what other error codes do we need to handle?
{:error, err} ->
@@ -302,7 +300,7 @@ defmodule Polyjuice.Client.Sync do
|> Map.get("leave", [])
|> Enum.each(fn {k, v} ->
process_room(k, v, state)
- state.send.({:left, k})
+ Polyjuice.Client.Handler.handle(state.handler, :left, {k})
end)
end
@@ -311,7 +309,7 @@ defmodule Polyjuice.Client.Sync do
if Map.get(timeline, "limited", false) do
with {:ok, prev_batch} <- Map.fetch(timeline, "prev_batch") do
- state.send.({:limited, roomname, prev_batch})
+ Polyjuice.Client.Handler.handle(state.handler, :limited, {roomname, prev_batch})
end
end
@@ -332,7 +330,7 @@ defmodule Polyjuice.Client.Sync do
roomname,
state
) do
- state.send.({:state, roomname, event})
+ Polyjuice.Client.Handler.handle(state.handler, :state, {roomname, event})
state
end
@@ -342,7 +340,7 @@ defmodule Polyjuice.Client.Sync do
roomname,
state
) do
- state.send.({:message, roomname, event})
+ Polyjuice.Client.Handler.handle(state.handler, :message, {roomname, event})
state
end
@@ -373,7 +371,7 @@ defmodule Polyjuice.Client.Sync do
end
)
- user_id = Agent.get(state.pid, fn %{user_id: user_id} -> user_id end)
+ user_id = Agent.get(state.client_state, fn %{user_id: user_id} -> user_id end)
inviter =
invite_state
@@ -382,7 +380,7 @@ defmodule Polyjuice.Client.Sync do
|> Map.get("sender")
if inviter do
- state.send.({:invite, roomname, inviter, invite_state})
+ Polyjuice.Client.Handler.handle(state.handler, :invite, {roomname, inviter, invite_state})
end
state
diff --git a/mix.exs b/mix.exs
index 7f0a4e2..1600134 100644
--- a/mix.exs
+++ b/mix.exs
@@ -42,6 +42,7 @@ defmodule PolyjuiceClient.MixProject do
def application do
[
+ mod: {Polyjuice.Client.Application, []},
extra_applications: [:logger]
]
end
diff --git a/test/polyjuice/client/media_test.exs b/test/polyjuice/client/media_test.exs
index b4899ca..4c545ac 100644
--- a/test/polyjuice/client/media_test.exs
+++ b/test/polyjuice/client/media_test.exs
@@ -87,10 +87,11 @@ defmodule Polyjuice.Client.MediaTest do
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
client =
- Polyjuice.Client.start(
+ Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.Client.MediaTest.Httpd",
access_token: "an_access_token",
user_id: "@alice:example.org",
+ sync: false,
test: true
)
diff --git a/test/polyjuice/client/sync_test.exs b/test/polyjuice/client/sync_test.exs
index c1b4f64..a910c14 100644
--- a/test/polyjuice/client/sync_test.exs
+++ b/test/polyjuice/client/sync_test.exs
@@ -214,100 +214,99 @@ defmodule Polyjuice.Client.SyncTest do
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
client =
- Polyjuice.Client.start(
+ Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.Client.SyncTest.Httpd",
access_token: "an_access_token",
user_id: "@alice:example.org",
+ handler: self(),
storage: storage,
+ sync_filter: %{},
test: true
)
- {:ok, sync_pid} =
- Polyjuice.Client.API.sync_child_spec(client, self(), filter: %{})
- |> (fn %{start: {module, func, args}} -> apply(module, func, args) end).()
-
- assert_receive({:connected})
+ assert_receive({:polyjuice_client, :sync_connected})
assert_receive(
- {:invite, "!room_id", "@bob:example.org",
- %{
- "m.room.member" => %{
- "@alice:example.org" => %{
- "content" => %{
- "membership" => "invite"
- },
- "type" => "m.room.member",
- "state_key" => "@alice:example.org",
- "event_id" => "$invite_event",
- "room_id" => "!room_id",
- "sender" => "@bob:example.org"
- }
- }
- }},
+ {:polyjuice_client, :invite,
+ {"!room_id", "@bob:example.org",
+ %{
+ "m.room.member" => %{
+ "@alice:example.org" => %{
+ "content" => %{
+ "membership" => "invite"
+ },
+ "type" => "m.room.member",
+ "state_key" => "@alice:example.org",
+ "event_id" => "$invite_event",
+ "room_id" => "!room_id",
+ "sender" => "@bob:example.org"
+ }
+ }
+ }}},
1000
)
- assert_receive({:initial_sync_completed})
+ assert_receive({:polyjuice_client, :initial_sync_completed})
assert_receive(
- {:limited, "!room_id", "p1"},
+ {:polyjuice_client, :limited, {"!room_id", "p1"}},
1000
)
assert_receive(
- {:state, "!room_id",
- %{
- "content" => %{
- "membership" => "join"
- },
- "type" => "m.room.member",
- "state_key" => "@alice:example.org",
- "event_id" => "$join_event",
- "room_id" => "!room_id",
- "sender" => "@alice:example.org"
- }},
+ {:polyjuice_client, :state,
+ {"!room_id",
+ %{
+ "content" => %{
+ "membership" => "join"
+ },
+ "type" => "m.room.member",
+ "state_key" => "@alice:example.org",
+ "event_id" => "$join_event",
+ "room_id" => "!room_id",
+ "sender" => "@alice:example.org"
+ }}},
1000
)
assert_receive(
- {:message, "!room_id",
- %{
- "content" => %{
- "msgtype" => "m.text",
- "body" => "Hello World!"
- },
- "type" => "m.room.message",
- "event_id" => "$message_event",
- "room_id" => "!room_id",
- "sender" => "@alice:example.org"
- }},
+ {:polyjuice_client, :message,
+ {"!room_id",
+ %{
+ "content" => %{
+ "msgtype" => "m.text",
+ "body" => "Hello World!"
+ },
+ "type" => "m.room.message",
+ "event_id" => "$message_event",
+ "room_id" => "!room_id",
+ "sender" => "@alice:example.org"
+ }}},
1000
)
assert_receive(
- {:state, "!room_id",
- %{
- "content" => %{
- "membership" => "leave",
- "reason" => "Goodbye Cruel World!"
- },
- "type" => "m.room.member",
- "state_key" => "@alice:example.org",
- "event_id" => "$leave_event",
- "room_id" => "!room_id",
- "sender" => "@alice:example.org"
- }},
+ {:polyjuice_client, :state,
+ {"!room_id",
+ %{
+ "content" => %{
+ "membership" => "leave",
+ "reason" => "Goodbye Cruel World!"
+ },
+ "type" => "m.room.member",
+ "state_key" => "@alice:example.org",
+ "event_id" => "$leave_event",
+ "room_id" => "!room_id",
+ "sender" => "@alice:example.org"
+ }}},
1000
)
assert_receive(
- {:left, "!room_id"},
+ {:polyjuice_client, :left, {"!room_id"}},
1000
)
- Process.unlink(sync_pid)
- Process.exit(sync_pid, :kill)
-
Polyjuice.Client.stop(client)
:inets.stop(:httpd, httpd_pid)
diff --git a/test/polyjuice/client_test.exs b/test/polyjuice/client_test.exs
index 0c880c1..a0e53ec 100644
--- a/test/polyjuice/client_test.exs
+++ b/test/polyjuice/client_test.exs
@@ -75,7 +75,7 @@ defmodule Polyjuice.ClientTest do
test "transaction_id is unique" do
client = %Polyjuice.Client{
base_url: "http://localhost:8008",
- pid: nil
+ id: nil
}
# the best that we can do is test that two calls to transaction_id return
@@ -84,18 +84,6 @@ defmodule Polyjuice.ClientTest do
Polyjuice.Client.API.transaction_id(client)
end
- test "sync child spec" do
- client = %Polyjuice.Client{
- base_url: "http://localhost:8008",
- pid: nil
- }
-
- %{id: Polyjuice.Client.Sync, restart: :permanent, start: start} =
- Polyjuice.Client.API.sync_child_spec(client, "listener")
-
- assert start == {Polyjuice.Client.Sync, :start_link, [[client, "listener"]]}
- end
-
test "sync" do
with client = %DummyClient{
response: {
@@ -153,10 +141,11 @@ defmodule Polyjuice.ClientTest do
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
client =
- Polyjuice.Client.start(
+ Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/",
access_token: "an_access_token",
user_id: "@alice:example.org",
+ sync: false,
test: true
)
@@ -219,25 +208,36 @@ defmodule Polyjuice.ClientTest do
port = :httpd.info(httpd_pid) |> Keyword.fetch!(:port)
client =
- Polyjuice.Client.start(
+ Polyjuice.Client.start_link(
"http://127.0.0.1:#{port}/Elixir.Polyjuice.ClientTest.Httpd/",
access_token: nil,
+ sync: false,
test: true
)
Polyjuice.Client.log_in_with_password(client, "@alice:example.org", "password")
- assert Agent.get(client.pid, fn %{
- access_token: access_token,
- user_id: user_id,
- device_id: device_id
- } ->
- {access_token, user_id, device_id}
- end) == {"m.id.user_login", "@alice:example.org", "foo"}
+ assert Agent.get(
+ Polyjuice.Client.process_name(client.id, :state),
+ fn %{
+ access_token: access_token,
+ user_id: user_id,
+ device_id: device_id
+ } ->
+ {access_token, user_id, device_id}
+ end
+ ) == {"m.id.user_login", "@alice:example.org", "foo"}
Polyjuice.Client.log_out(client)
- assert Agent.get(client.pid, fn %{access_token: access_token} -> access_token end) == nil
+ assert Agent.get(
+ Polyjuice.Client.process_name(client.id, :state),
+ fn %{
+ access_token: access_token
+ } ->
+ access_token
+ end
+ ) == nil
Polyjuice.Client.log_in_with_password(
client,
@@ -245,13 +245,16 @@ defmodule Polyjuice.ClientTest do
"password"
)
- assert Agent.get(client.pid, fn %{
- access_token: access_token,
- user_id: user_id,
- device_id: device_id
- } ->
- {access_token, user_id, device_id}
- end) == {"m.id.thirdparty_login", "@alice:example.org", "foo"}
+ assert Agent.get(
+ Polyjuice.Client.process_name(client.id, :state),
+ fn %{
+ access_token: access_token,
+ user_id: user_id,
+ device_id: device_id
+ } ->
+ {access_token, user_id, device_id}
+ end
+ ) == {"m.id.thirdparty_login", "@alice:example.org", "foo"}
Polyjuice.Client.log_in_with_password(
client,
@@ -259,13 +262,16 @@ defmodule Polyjuice.ClientTest do
"password"
)
- assert Agent.get(client.pid, fn %{
- access_token: access_token,
- user_id: user_id,
- device_id: device_id
- } ->
- {access_token, user_id, device_id}
- end) == {"m.id.phone_login", "@alice:example.org", "foo"}
+ assert Agent.get(
+ Polyjuice.Client.process_name(client.id, :state),
+ fn %{
+ access_token: access_token,
+ user_id: user_id,
+ device_id: device_id
+ } ->
+ {access_token, user_id, device_id}
+ end
+ ) == {"m.id.phone_login", "@alice:example.org", "foo"}
Polyjuice.Client.log_in_with_password(
client,
@@ -275,13 +281,16 @@ defmodule Polyjuice.ClientTest do
"password"
)
- assert Agent.get(client.pid, fn %{
- access_token: access_token,
- user_id: user_id,
- device_id: device_id
- } ->
- {access_token, user_id, device_id}
- end) == {"ca.uhoreg.foo_login", "@alice:example.org", "foo"}
+ assert Agent.get(
+ Polyjuice.Client.process_name(client.id, :state),
+ fn %{
+ access_token: access_token,
+ user_id: user_id,
+ device_id: device_id
+ } ->
+ {access_token, user_id, device_id}
+ end
+ ) == {"ca.uhoreg.foo_login", "@alice:example.org", "foo"}
Polyjuice.Client.stop(client)
diff --git a/tutorial_echo.md b/tutorial_echo.md
index 69cbfca..27e7565 100644
--- a/tutorial_echo.md
+++ b/tutorial_echo.md
@@ -10,19 +10,20 @@ produce a working bot.
## Creating a client
-We start by creating a `Polyjuice.Client` struct:
+We begin by starting a client:
- iex> client = %Polyjuice.Client{
- ...> base_url: "http://localhost:8008",
+ iex> client = Polyjuice.Client.start_link(
+ ...> "http://localhost:8008",
...> access_token: "access_token",
...> user_id: "@echobot:localhost",
- ...> storage: Polyjuice.Client.Storage.Ets.open()
- ...> }
+ ...> storage: Polyjuice.Client.Storage.Ets.open(),
+ ...> handler: self()
+ ...> )
-The `base_url` parameter is the base URL to the homeserver that the bot will be
+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](https://matrix.org/docs/spec/client_server/r0.5.0#post-matrix-client-r0-login)
+[login](https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login)
endpoint on the homeserver, or by retrieving it from an already logged-in
client. `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
@@ -34,19 +35,9 @@ when it is restarted. If you wish to have persistence, you can use the
storage to disk. To use it, call `Polyjuice.Client.Storage.Dets.open/1` with
the name of a file to use for storage.
-## Sync
-
-Once the client has been created, a sync process can be started to receive
-messages. The sync can be started in a supervisor, so that if it dies, it will
-be restarted. Calling `Polyjuice.Client.API.sync_child_spec/3` will generate a
-child spec that can be passed to `Supervisor.start_link`. Its first argument
-is the client struct that we created earlier, and its second argument will be a
-`pid` that will receive messages. In our case, we will send it to `self` since
-we will handle the messages. You can also pass in some options; we will omit
-them for now.
-
- iex> children = [Polyjuice.Client.API.sync_child_spec(client, self)]
- iex> {:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)
+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
@@ -56,8 +47,8 @@ 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 `{:message, room_id, event}`.
-When we receive such a message, we can use the
+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.
@@ -66,21 +57,22 @@ We can simply take the message contents from the incoming message, change the
echo. A `receive` statement to do this would look something like this:
receive do
- {:message, room_id, %{"content" => %{"msgtype" => "m.text"} = content}} ->
+ {: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 `{: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:
+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
- {:invite, room_id, _inviter, _invite_state} ->
+ {:polyjuice_client, :invite, {room_id, _inviter, _invite_state}} ->
Polyjuice.Client.Room.join(client, room_id)
end
@@ -91,13 +83,13 @@ that calls itself recursively:
iex> defmodule EchoBot do
...> def loop(client) do
...> receive do
- ...> {:message, room_id, %{"content" => %{"msgtype" => "m.text"} = content}} ->
+ ...> {:polyjuice_client, :message, {room_id, %{"content" => %{"msgtype" => "m.text"} = content}}} ->
...> Polyjuice.Client.Room.send_message(
...> client, room_id,
...> %{content | "msgtype" => "m.notice"}
...> )
...>
- ...> {:invite, room_id, _inviter, _invite_state} ->
+ ...> {:polyjuice_client, :invite, {room_id, _inviter, _invite_state}} ->
...> Polyjuice.Client.Room.join(client, room_id)
...>
...> _ ->
diff --git a/tutorial_welcome.md b/tutorial_welcome.md
index d77760f..3d91b0c 100644
--- a/tutorial_welcome.md
+++ b/tutorial_welcome.md
@@ -6,25 +6,14 @@ 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. And as before, we start by creating a
-`Polyjuice.Client` struct:
-
- iex> client = %Polyjuice.Client{
- ...> base_url: "http://localhost:8008",
- ...> access_token: "access_token",
- ...> user_id: "@echobot:localhost",
- ...> storage: Polyjuice.Client.Storage.Ets.open()
- ...> }
+-S mix` to produce a working bot.
Before continuing, you should make sure that the bot is already in a room.
-## Filtering
-
-We will also start a sync process as we did before. 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.)
+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
@@ -38,14 +27,17 @@ 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> children = [Polyjuice.Client.API.sync_child_spec(
- ...> client, self,
- ...> filter: Filter.include_presence_types([])
+ iex> client = 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> {:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)
+ ...> )
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
@@ -74,12 +66,14 @@ the code will look something like this:
receive do
{
- :state,
- room_id,
- %{
- "content" => %{"membership" => "join"} = content,
- "state_key" => user_id,
- "type" => "m.room.member"
+ :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
@@ -90,11 +84,13 @@ the code will look something like this:
# ... send welcome message
end
{
- :state,
- room_id,
- %{
- "state_key" => user_id,
- "type" => "m.room.member"
+ :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
@@ -149,11 +145,12 @@ purposes.
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 `{: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 `{:initial_sync_completed}` message
-arrives, then we start sending welcome messages. We will do this by using a
-variable that tells us if the `{:initial_sync_completed}` message was received.
+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.
@@ -165,23 +162,27 @@ The full sync processing code would then look like this:
...> user_id = client.user_id
...> receive do
...> {
- ...> :state,
- ...> _room_id,
- ...> %{
- ...> "state_key" => ^user_id,
- ...> "type" => "m.room.member"
+ ...> :polyjuice_client, :state,
+ ...> {
+ ...> _room_id,
+ ...> %{
+ ...> "state_key" => ^user_id,
+ ...> "type" => "m.room.member"
+ ...> }
...> }
...> } ->
...> # ignore our own room membership
...> loop(client, initial_sync_completed)
...>
...> {
- ...> :state,
- ...> room_id,
- ...> %{
- ...> "content" => %{"membership" => "join"} = content,
- ...> "state_key" => user_id,
- ...> "type" => "m.room.member"
+ ...> :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
@@ -201,11 +202,13 @@ The full sync processing code would then look like this:
...> loop(client, initial_sync_completed)
...>
...> {
- ...> :state,
- ...> room_id,
- ...> %{
- ...> "state_key" => user_id,
- ...> "type" => "m.room.member"
+ ...> :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
@@ -214,7 +217,7 @@ The full sync processing code would then look like this:
...> Polyjuice.Client.Storage.kv_del(client.storage, namespace, user_id)
...> loop(client, initial_sync_completed)
...>
- ...> {:initial_sync_completed} ->
+ ...> {:polyjuice_client, :initial_sync_completed} ->
...> loop(client, true)
...>
...> _ ->