# Copyright 2019 Hubert Chathi # # 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 do @moduledoc """ Matrix client functions. The struct in this module, or any struct that implements the `Polyjuice.Client.API` protocol, can be used to connect to a Matrix server using the functions from submodules. - `Polyjuice.Client.User`: user-related functions - `Polyjuice.Client.Filter`: build filters for use with sync - `Polyjuice.Client.Room`: interact with rooms, such as sending messages - `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`. """ require Logger @typedoc """ Matrix client data. - `base_url` (string): Required. The base URL for the homeserver. - `access_token` (string): Required to call endpoints that require an authenticated user. The user's access token. - `user_id` (string): Required for some endpoints and for sync. The user's Matrix ID. - `storage` (`Polyjuice.Client.Storage`): Required for some endpoints and for sync. Storage for the client. """ @type t :: %__MODULE__{ base_url: String.t(), access_token: String.t(), user_id: String.t(), storage: Polyjuice.Client.Storage.t() } @enforce_keys [:base_url] defstruct [ :base_url, :access_token, :user_id, :storage ] @doc "The r0 client URL prefix" def prefix_r0, do: "/_matrix/client/r0" @doc "The unstable client URL prefix" def prefix_unstable, do: "/_matrix/client/unstable" defprotocol API do @moduledoc """ Protocol for calling the Matrix client API. """ @doc """ Call a Matrix client API. This is a lower-level function; generally, clients will want to call one of the higher-level functions from `Polyjuice.Client`. """ @spec call( client_api :: Polyjuice.Client.API.t(), endpoint :: Polyjuice.Client.Endpoint.Proto.t() ) :: Any def call(client_api, endpoint) @doc """ Generate a unique transaction ID. """ @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, access_token: access_token}, endpoint) do %Polyjuice.Client.Endpoint.HttpSpec{ method: method, headers: headers, url: url, body: body, auth_required: auth_required } = Polyjuice.Client.Endpoint.Proto.http_spec(endpoint, base_url) Logger.debug("calling #{url}") if auth_required and access_token == nil do {:error, :auth_required} else case :hackney.request( method, url, if access_token do [{"Authorization", "Bearer #{access_token}"} | headers] else headers end, body, [:with_body] ) do {:ok, status_code, resp_headers, body} -> Logger.debug("status code #{status_code}") Polyjuice.Client.Endpoint.Proto.transform_http_result( endpoint, status_code, resp_headers, body ) err -> # anything else is an error -- return as-is err end end end 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 """ Synchronize messages from the server. Normally, you should use `Polyjuice.Client.Sync` rather than calling this function, but this function may be used where more control is needed. `opts` is a keyword list of options: - `filter:` (string or map) a filter to apply to the sync. May be either the ID of a previously uploaded filter, or a new filter. - `since:` (string) where to start the sync from. Should be a token obtained from the `next_batch` of a previous sync. - `full_state:` (boolean) whether to return the full room state instead of just the state that has changed since the last sync - `set_presence:` (one of `:online`, `:offline`, or `:unavailable`) the user's presence to set with this sync - `timeout:` (integer) the number of milliseconds to wait before the server returns """ @spec sync(client_api :: Polyjuice.Client.API.t(), opts :: list()) :: {:ok, map()} | Any def sync(client_api, opts \\ []) when is_list(opts) do Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.GetSync{ filter: Keyword.get(opts, :filter), since: Keyword.get(opts, :since), full_state: Keyword.get(opts, :full_state, false), set_presence: Keyword.get(opts, :set_presence, :online), timeout: Keyword.get(opts, :timeout, 0) } ) end end