summaryrefslogtreecommitdiff
path: root/lib/polyjuice
diff options
context:
space:
mode:
Diffstat (limited to 'lib/polyjuice')
-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
4 files changed, 225 insertions, 118 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