# 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. """ require Logger @type t :: %__MODULE__{ base_url: String.t(), access_token: String.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. """ @spec sync_child_spec( client_api :: Polyjuice.Client.API.t(), listener :: pid() | fun(), 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, transform: transform, 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 {:ok, status_code, resp_headers, body} = :hackney.request( method, url, if access_token do [{"Authorization", "Bearer #{access_token}"} | headers] else headers end, body, [:with_body] ) Logger.debug("status code #{status_code}") transform.(status_code, resp_headers, body) 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 """ Send a message to a room. `msg` can either be anything that implements the `Polyjuice.Client.MsgBuilder.MsgData` protocol (which will be sent as an `m.message`), or a map (which specifies the full message content). Examples: Polyjuice.Client.send_message(client, "text message", "!room_id") Polyjuice.Client.send_message( client, {"message with formatting", "message with formatting"}, "!room_id" ) Polyjuice.Client.send_message( client, ["Hello, ", Polyjuice.Client.MsgBuilder.mention("@world:example.com")], "!room_id" ) Polyjuice.Client.send_message( client, %{"msgtype" => "m.notice", "body" => "using full message content"}, "!room_id" ) """ @spec send_message( client_api :: Polyjuice.Client.API.t(), msg :: map | Polyjuice.Client.MsgBuilder.MsgData.t(), room :: String.t() ) :: String.t() def send_message(client_api, msg, room) when is_binary(room) do cond do Polyjuice.Client.MsgBuilder.MsgData.impl_for(msg) != nil -> Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.PutRoomsSend{ txn_id: Polyjuice.Client.API.transaction_id(client_api), room: room, event_type: "m.room.message", message: Polyjuice.Client.MsgBuilder.to_message(msg) } ) is_map(msg) and not Map.has_key?(msg, :__struct__) -> Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.PutRoomsSend{ txn_id: Polyjuice.Client.API.transaction_id(client_api), room: room, event_type: "m.room.message", message: msg } ) true -> raise ArgumentError, message: "invalid argument msg" end end @doc """ Send an event to a room. """ @spec send_event( client_api :: Polyjuice.Client.API.t(), event_type :: String.t(), event :: map, room :: String.t() ) :: String.t() def send_event(client_api, event_type, event, room) when is_binary(event_type) and is_map(event) and is_binary(room) do Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.PutRoomsSend{ txn_id: Polyjuice.Client.API.transaction_id(client_api), room: room, event_type: event_type, message: event } ) end @doc """ Update the client's read receipt (of the given type) to the given message in the given room. """ @spec update_read_receipt( client_api :: Polyjuice.Client.API.t(), room :: String.t(), event_id :: String.t(), receipt_type :: String.t() ) :: Any def update_read_receipt(client_api, room, event_id, receipt_type \\ "m.read") when is_binary(room) and is_binary(event_id) and is_binary(receipt_type) do Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.PostRoomsReceipt{ room: room, event_id: event_id, receipt_type: receipt_type } ) end @doc """ Join a room. """ @spec join_room( client_api :: Polyjuice.Client.API.t(), room :: String.t(), servers :: list(String.t()), third_party_join :: map | nil ) :: Any def join_room(client_api, room, servers \\ [], third_party_signed \\ nil) when is_binary(room) and is_list(servers) and (is_map(third_party_signed) or third_party_signed == nil) do Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.PostJoin{ room: room, servers: servers, third_party_signed: third_party_signed } ) end end