defmodule ExMatrixApi.Synapse do @moduledoc """ API level to make requests to Matrix Synapse. """ alias ExMatrixApi.Util.Request alias ExMatrixApi.Synapse.Rooms.Timeline alias ExMatrixApi.Synapse.Rooms.Event alias ExMatrixApi.Synapse.Rooms.InviteState alias ExMatrixApi.Synapse.RegisteredUser @doc """ Get authorization token by login / password. """ @spec get_token(String.t(), String.t()) :: {:ok, String.t()} | {:error, any()} def get_token(user, password) do login_url = api_url!("/login") with {:ok, %{"flows" => [%{"type" => type}]}} <- Request.get(login_url), payload = %{type: type, user: user, password: password}, {:ok, %{"access_token" => access_token}} <- Request.post(login_url, payload) do {:ok, access_token} end end @doc """ Check if synapse allowing to register specific username. """ @spec is_username_available?(String.t()) :: {:ok, boolean()} | {:error, any()} def is_username_available?(username) do username_check_url = api_url!("/register/available?username=#{username}") with {:ok, %{"available" => available}} <- Request.get(username_check_url) do {:ok, available} end end @doc """ Register new user on Matrix synapse. """ @spec register(String.t(), String.t(), map()) :: {:ok, RegisteredUser.t()} | {:error, any()} def register(username, password, params \\ %{}) do # api_url!("/register") register_url = api_url!("/admin/register") payload = Map.merge(params, %{username: username, password: password}) key = config!(:registration_secret) with {:ok, %{"nonce" => nonce}} <- Request.get(register_url), token = "#{nonce}\x00#{username}\x00#{password}\x00notadmin", mac = :crypto.hmac(:sha, key, token) |> Base.encode16(case: :lower), payload = Map.merge(payload, %{nonce: nonce, mac: mac}), {:ok, response} <- Request.post(register_url, payload) do {:ok, ExMatrixApi.Util.to_struct(RegisteredUser, response)} end end @doc """ Update specific field of user's data, currently supported only for display name and avatar. """ @spec update_user(String.t(), String.t(), :name | :avatar, String.t()) :: :ok | {:error, any()} def update_user(token, user_id, :name, new_name) do update_user_url = api_url!("/profile/#{user_id}/displayname") headers = auth_headers(token) payload = %{"displayname" => new_name} with {:ok, _} <- Request.put(update_user_url, payload, headers) do :ok end end def update_user(token, user_id, :avatar, avatar_url) do update_user_url = api_url!("/profile/#{user_id}/avatar_url") headers = auth_headers(token) payload = %{"avatar_url" => avatar_url} with {:ok, _} <- Request.put(update_user_url, payload, headers) do :ok end end @doc """ Update display name, avatar or password. """ @spec update_user(String.t(), :password, String.t(), String.t(), String.t()) :: :ok | {:error, any()} def update_user(token, :password, user_id, old_password, new_password) do update_user_url = api_url!("/account/password") headers = auth_headers(token) payload = %{"new_password" => new_password} with {:error, %{data: %{"session" => session, "flows" => [%{"stages" => [type]}]}}} <- Request.post(update_user_url, payload, headers), payload = Map.put(payload, "auth", %{ session: session, type: type, user: user_id, password: old_password }), {:ok, _} <- Request.post(update_user_url, payload, headers) do :ok end end @doc """ Upload file and return mxc:// URI of the uploaded asset. """ @spec upload_media(String.t(), String.t(), binary(), String.t() | nil) :: {:ok, String.t()} | {:error, any()} def upload_media(token, content_type, bin, filename \\ nil) do upload_url = if is_nil(filename), do: media_api_url!("/upload"), else: media_api_url!("/upload?" <> URI.encode_query(%{filename: filename})) headers = [{"Content-Type", content_type} | auth_headers(token)] with {:ok, %{"content_uri" => content_uri}} <- Request.post(upload_url, bin, headers) do {:ok, content_uri} end end @doc """ Convert mxc URI into http URL for display. """ @spec thumbnail(String.t(), integer(), integer()) :: String.t() def thumbnail("mxc://" <> path, width, height) do media_id = String.split(path, "/") |> List.last() query = URI.encode_query(%{width: width, height: height}) media_api_url!("/thumbnail/#{config!(:host)}/#{media_id}?#{query}") end def thumbnail(_, _, _) do GreetWeb.Endpoint.static_path("/images/no-image.png") end @type timelines :: %{String.t() => Timeline.t()} @type invites :: %{String.t() => InviteState.t()} @doc """ Get major user data in scope synapse. """ @spec sync(String.t(), map()) :: {:ok, timelines, invites} | {:error, any()} def sync(token, params \\ %{}) do rooms_url = api_url!("/sync?#{URI.encode_query(params)}") headers = auth_headers(token) with {:ok, %{ "next_batch" => next_batch, "rooms" => %{"join" => joined_rooms, "invite" => invited_rooms} }} <- Request.get(rooms_url, headers) do room_event_batch = for {key, %{ "state" => %{ "events" => raw_state_events }, "timeline" => %{ "events" => raw_events, "prev_batch" => batch_ref, "limited" => limited } }} <- joined_rooms do relevant_state_events = Enum.filter(raw_state_events, &Event.is_state_event?(Map.get(&1, "type"))) relevant_raw_events = Enum.filter(raw_events, &Event.is_supported_type?(Map.get(&1, "type"))) events = Enum.map(relevant_raw_events, &ExMatrixApi.Util.to_struct(Event, &1)) state_events = Enum.map(relevant_state_events, &ExMatrixApi.Util.to_struct(Event, &1)) timeline = %Timeline{ uid: key, events: events, state_events: state_events, limited: limited, prev_batch: batch_ref, next_batch: next_batch } {key, timeline} end invite_event_batch = for {key, %{"invite_state" => %{"events" => raw_events}}} <- invited_rooms do relevant_raw_events = Enum.filter(raw_events, &Event.is_supported_type?(Map.get(&1, "type"))) events = Enum.map(relevant_raw_events, &ExMatrixApi.Util.to_struct(Event, &1)) invite_state = %InviteState{ uid: key, events: events } {key, invite_state} end {:ok, Enum.into(room_event_batch, %{}), Enum.into(invite_event_batch, %{})} end end @doc """ Create new room for user's conversation. """ @spec create_room(String.t(), map()) :: {:ok, String.t()} | {:error, any()} def create_room(token, payload) do room_creation_url = api_url!("/createRoom") headers = auth_headers(token) with {:ok, %{"room_id" => room_id}} <- Request.post(room_creation_url, payload, headers) do {:ok, room_id} end end @doc """ Change room state, by executing state event. """ @spec update_room_state(String.t(), String.t(), String.t(), map(), String.t()) :: {:ok, String.t()} | {:error, any()} def update_room_state(token, room_id, event_type, payload \\ %{}, state_key \\ "") do room_update_url = api_url!("/rooms/#{room_id}/state/#{event_type}/#{state_key}") headers = auth_headers(token) with {:ok, %{"event_id" => event_id}} <- Request.put(room_update_url, payload, headers) do {:ok, event_id} end end @doc """ Join room by accepting invite. """ @spec join_invited_room(String.t(), String.t()) :: {:ok, String.t()} | {:error, any()} def join_invited_room(token, room_uid) do room_join_url = api_url!("/join/#{room_uid}") headers = auth_headers(token) with {:ok, %{"room_id" => room_id}} <- Request.post(room_join_url, %{}, headers) do {:ok, room_id} end end @doc """ Decline invite to join room. """ @spec forget_invited_room(String.t(), String.t()) :: :ok | {:error, any()} def forget_invited_room(token, room_uid) do decline_invite_url = api_url!("/rooms/#{room_uid}/forget") headers = auth_headers(token) with {:ok, _} <- Request.post(decline_invite_url, %{}, headers) do :ok end end @doc """ Leave room. """ @spec leave_room(String.t(), String.t()) :: :ok | {:error, any()} def leave_room(token, room_uid) do leave_room_url = api_url!("/rooms/#{room_uid}/leave") headers = auth_headers(token) with {:ok, _} <- Request.post(leave_room_url, %{}, headers) do :ok end end @doc """ Get secure room id from it's alias. """ @spec room_id_by_alias(String.t(), String.t()) :: {:ok, String.t()} | {:error, any()} def room_id_by_alias(token, room_alias) do room_url = api_url!("/directory/room/#{room_alias}") headers = auth_headers(token) with {:ok, %{"room_id" => room_id}} <- Request.get(room_url, headers) do {:ok, room_id} end end @doc """ Get list of room names. """ @spec rooms(String.t()) :: {:ok, [String.t()]} | {:error, any()} def rooms(token) do rooms_url = api_url!("/joined_rooms") headers = auth_headers(token) with {:ok, %{"joined_rooms" => rooms}} <- Request.get(rooms_url, headers) do {:ok, rooms} end end @doc """ Get list of messages in a specific room. """ @spec room_messages(Sring.t(), String.t(), String.t(), integer(), String.t()) :: {:ok, {[map()], String.t(), String.t()}} | {:error, any()} def room_messages(token, room_id, from, limit \\ 20, dir \\ "b") do messages_url = api_url!("/rooms/#{room_id}/messages?from=#{from}&dir=#{dir}&limit=#{limit}") headers = auth_headers(token) with {:ok, %{"chunk" => events, "start" => sb, "end" => eb}} <- Request.get(messages_url, headers) do raw_events = Enum.filter(events, &Event.is_supported_type?(Map.get(&1, "type"))) events = Enum.map(raw_events, &ExMatrixApi.Util.to_struct(Event, &1)) {:ok, {events, sb, eb}} end end @doc """ Get full room state. """ @spec room_state(String.t(), String.t()) :: {:ok, [Event.t()]} | {:error, any()} def room_state(token, room_id) do room_state_url = api_url!("/rooms/#{room_id}/state") headers = auth_headers(token) with {:ok, raw_events} when is_list(raw_events) <- Request.get(room_state_url, headers) do relevant_raw_events = Enum.filter(raw_events, &Event.is_supported_type?(Map.get(&1, "type"))) events = Enum.map(relevant_raw_events, &ExMatrixApi.Util.to_struct(Event, &1)) {:ok, events} end end @doc """ Send new text message in a specific room. """ @spec send_text_message(String.t(), String.t(), String.t()) :: {:ok, Event.t()} | {:error, any()} def send_text_message(token, room, text) do headers = auth_headers(token) uuid = Ecto.UUID.autogenerate() message_url = api_url!("/rooms/#{room}/send/m.room.message/#{uuid}") payload = %{"msgtype" => "m.text", "body" => text} with {:ok, %{"event_id" => _event_id} = response} <- Request.put(message_url, payload, headers) do {:ok, ExMatrixApi.Util.to_struct(Event, response)} end end @doc """ Mark room events as read. """ @spec set_read_marker(String.t(), String.t(), String.t()) :: :ok | {:error, any()} def set_read_marker(token, room, to_event_id) do headers = auth_headers(token) read_message_url = api_url!("/rooms/#{room}/read_markers") payload = %{"m.fully_read" => to_event_id, "m.read" => to_event_id} with {:ok, _} <- Request.post(read_message_url, payload, headers) do :ok end end @doc """ Read config settings. """ @spec config!(atom()) :: any() def config!(key) do Application.fetch_env!(:ex_matrix_api, key) end defp api_url!(suffix), do: config!(:api_url) <> suffix defp media_api_url!(suffix), do: config!(:media_api_url) <> suffix defp auth_headers(token), do: [{"Authorization", "Bearer #{token}"}] end