# 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.Room do @moduledoc """ Room-related functions. """ require Logger @typedoc """ Represents a position in the timeline, used for paginating events before/after this position. """ @type timeline_pos :: String.t() @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.Room.send_message(client, "text message", "!room_id") Polyjuice.Client.Room.send_message( client, {"message with formatting", "message with formatting"}, "!room_id" ) Polyjuice.Client.Room.send_message( client, ["Hello, ", Polyjuice.Client.MsgBuilder.mention("@world:example.com")], "!room_id" ) Polyjuice.Client.Room.send_message( client, %{"msgtype" => "m.notice", "body" => "using full message content"}, "!room_id" ) """ @spec send_message( client_api :: Polyjuice.Client.API.t(), room :: String.t(), msg :: map | Polyjuice.Client.MsgBuilder.MsgData.t() ) :: {:ok, String.t()} | Any def send_message(client_api, room, msg) 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(), room :: String.t(), event_type :: String.t(), event :: map ) :: {:ok, String.t()} | Any def send_event(client_api, room, event_type, event) 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 """ Send a state event to a room. """ @spec send_state_event( client_api :: Polyjuice.Client.API.t(), room :: String.t(), event_type :: String.t(), state_key :: String.t(), event :: map ) :: {:ok, String.t()} | Any def send_state_event(client_api, room, event_type, state_key \\ "", event) when is_binary(event_type) and is_binary(state_key) and is_map(event) and is_binary(room) do Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.PutRoomsState{ room: room, event_type: event_type, state_key: state_key, content: 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() ) :: {:ok} | 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( client_api :: Polyjuice.Client.API.t(), room :: String.t(), servers :: list(String.t()), third_party_signed :: map | nil ) :: {:ok, String.t()} | Any def join(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 @doc """ Get messages from a room starting from a certain point. """ @spec get_messages( client_api :: Polyjuice.Client.API.t(), room :: String.t(), from :: timeline_pos, dir :: :forward | :backward, opts :: list() ) :: {:ok, map()} | Any def get_messages(client_api, room, from, dir, opts \\ []) when is_binary(room) and is_binary(from) and (dir == :forward or dir == :backward) do Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.GetRoomsMessages{ room: room, from: from, dir: dir, to: Keyword.get(opts, :to), limit: Keyword.get(opts, :limit), filter: Keyword.get(opts, :filter) } ) end @doc """ Paginate messages from a room starting from a certain point. This function returns a stream of message chunks as would be returned by `get_messages`. Examples: Back-paginate until it reaches events before a given timestamp. Polyjuice.Client.Room.stream_messages(client, "!room_id", token, :backward) |> Stream.map(&Map.get(&1, "chunk", [])) |> Stream.concat() |> Stream.take_while(&(Map.get(&1, "origin_server_ts", 0) >= timestamp)) |> Enum.reverse() """ @spec stream_messages( client_api :: Polyjuice.Client.API.t(), room :: String.t(), from :: timeline_pos, dir :: :forward | :backward, opts :: list() ) :: Enumerable.t() def stream_messages(client_api, room, from, dir, opts \\ []) when is_binary(room) and is_binary(from) and (dir == :forward or dir == :backward) and is_list(opts) do to = Keyword.get(opts, :to) limit = Keyword.get(opts, :limit) filter = Keyword.get(opts, :filter) Stream.resource( fn -> from end, fn token -> case Polyjuice.Client.API.call( client_api, %Polyjuice.Client.Endpoint.GetRoomsMessages{ room: room, from: token, dir: dir, to: to, limit: limit, filter: filter } ) do {:ok, res} -> next = Map.get(res, "end", token) if next == token do {:halt, nil} else {[res], next} end _ -> # bail if we get any error {:halt, nil} end end, fn _ -> nil end ) end end