diff --git a/lib/architex.ex b/lib/architex.ex index 8e282c7..414248f 100644 --- a/lib/architex.ex +++ b/lib/architex.ex @@ -271,4 +271,15 @@ defmodule Architex do end end end + + # https://stackoverflow.com/a/45754361 + def validate_not_nil(changeset, fields) do + Enum.reduce(fields, changeset, fn field, changeset -> + if Ecto.Changeset.get_field(changeset, field) == nil do + Ecto.Changeset.add_error(changeset, field, "nil") + else + changeset + end + end) + end end diff --git a/lib/architex/schema/event.ex b/lib/architex/schema/event.ex index a77c8a6..e924ae8 100644 --- a/lib/architex/schema/event.ex +++ b/lib/architex/schema/event.ex @@ -6,7 +6,7 @@ defmodule Architex.Event do alias Architex.{Repo, Room, Event, Account, EncodableMap, KeyServer} alias Architex.Types.UserId - # TODO: Could refactor to also always set prev_events, but not necessary. + # TODO: It seems unsigned is always set, even though it is not specified? @type t :: %__MODULE__{ type: String.t(), origin_server_ts: integer(), @@ -37,6 +37,7 @@ defmodule Architex.Event do belongs_to :room, Room, type: :string end + # TODO: Move this to a dedicated function in Event.Formatters. defimpl Jason.Encoder, for: Event do @pdu_keys [ :auth_events, diff --git a/lib/architex/schema/event/formatters.ex b/lib/architex/schema/event/formatters.ex new file mode 100644 index 0000000..02407ca --- /dev/null +++ b/lib/architex/schema/event/formatters.ex @@ -0,0 +1,26 @@ +defmodule Architex.Event.Formatters do + alias Architex.Event + + def for_client(%Event{ + content: content, + type: type, + id: event_id, + sender: sender, + origin_server_ts: origin_server_ts, + unsigned: unsigned, + room_id: room_id + }) do + data = %{ + content: content, + type: type, + event_id: event_id, + sender: to_string(sender), + origin_server_ts: origin_server_ts, + room_id: room_id + } + + data = if unsigned, do: Map.put(data, :unsigned, unsigned), else: data + + data + end +end diff --git a/lib/architex/schema/room.ex b/lib/architex/schema/room.ex index 5df8e4a..24fc07c 100644 --- a/lib/architex/schema/room.ex +++ b/lib/architex/schema/room.ex @@ -5,7 +5,7 @@ defmodule Architex.Room do import Ecto.Query alias Architex.{Repo, Room, Event, Alias, RoomServer} - alias ArchitexWeb.Client.Request.CreateRoom + alias ArchitexWeb.Client.Request.{CreateRoom, Messages} @type t :: %__MODULE__{ visibility: :public | :private, @@ -22,10 +22,12 @@ defmodule Architex.Room do has_many :aliases, Alias, foreign_key: :room_id end + @spec changeset(Room.t(), map()) :: Ecto.Changeset.t() def changeset(room, params \\ %{}) do cast(room, params, [:visibility]) end + @spec create_changeset(CreateRoom.t()) :: Ecto.Changeset.t() def create_changeset(%CreateRoom{visibility: visibility}) do visibility = visibility || :public @@ -33,10 +35,12 @@ defmodule Architex.Room do |> changeset(%{visibility: visibility}) end + @spec generate_room_id() :: String.t() def generate_room_id do "!" <> Architex.random_string(18) <> ":" <> Architex.server_name() end + @spec update_forward_extremities(Event.t(), Room.t()) :: Room.t() def update_forward_extremities( %Event{ id: event_id, @@ -54,6 +58,7 @@ defmodule Architex.Room do room end + @spec create(Account.t(), CreateRoom.t()) :: {:ok, String.t()} | {:error, atom()} def create(account, input) do with {:ok, %Room{id: room_id}} <- Repo.insert(create_changeset(input)), {:ok, pid} <- RoomServer.get_room_server(room_id) do @@ -62,4 +67,63 @@ defmodule Architex.Room do _ -> {:error, :unknown} end end + + def get_messages(room, %Messages{from: from, to: to, dir: dir, limit: limit}) do + # TODO: Quaternion seems to show events in the wrong order? + # TODO: Check 'from' and 'to' formats. + limit = limit || 10 + + events = + room + |> Ecto.assoc(:events) + |> order_by_direction(dir) + |> events_from(from, dir) + |> events_to(to, dir) + |> limit(^limit) + |> Repo.all() + + {events, get_start(events), get_end(events, limit)} + end + + defp order_by_direction(query, "b"), do: order_by(query, desc: :origin_server_ts, desc: :nid) + defp order_by_direction(query, "f"), do: order_by(query, asc: :origin_server_ts, asc: :nid) + + # When 'from' is empty, we return events from the start or end + # of the room's history. + defp events_from(query, "", _), do: query + + defp events_from(query, from, "b") do + from = String.to_integer(from) + where(query, [e], e.nid < ^from) + end + + defp events_from(query, from, "f") do + from = String.to_integer(from) + where(query, [e], e.nid > ^from) + end + + defp events_to(query, nil, _), do: query + + defp events_to(query, to, "b") do + to = String.to_integer(to) + where(query, [e], e.nid >= ^to) + end + + defp events_to(query, to, "f") do + to = String.to_integer(to) + where(query, [e], e.nid <= ^to) + end + + defp get_start([]), do: nil + + defp get_start([%Event{nid: first_nid} | _]) do + Integer.to_string(first_nid) + end + + defp get_end(events, limit) when length(events) < limit, do: nil + + defp get_end(events, _) do + %Event{nid: last_nid} = List.last(events) + Integer.to_string(last_nid) + end end diff --git a/lib/architex_web/client/controllers/login_controller.ex b/lib/architex_web/client/controllers/login_controller.ex index 853bdc4..a9ab1b3 100644 --- a/lib/architex_web/client/controllers/login_controller.ex +++ b/lib/architex_web/client/controllers/login_controller.ex @@ -39,8 +39,7 @@ defmodule ArchitexWeb.Client.LoginController do case Account.login(input) |> Repo.transaction() do {:ok, - {%Account{localpart: localpart}, - %Device{access_token: access_token, id: device_id}}} -> + {%Account{localpart: localpart}, %Device{access_token: access_token, id: device_id}}} -> data = %{ user_id: Architex.get_mxid(localpart), access_token: access_token, diff --git a/lib/architex_web/client/controllers/room_controller.ex b/lib/architex_web/client/controllers/room_controller.ex index fdf895b..c330bdd 100644 --- a/lib/architex_web/client/controllers/room_controller.ex +++ b/lib/architex_web/client/controllers/room_controller.ex @@ -4,9 +4,9 @@ defmodule ArchitexWeb.Client.RoomController do import ArchitexWeb.Error import Ecto.{Changeset, Query} - alias Architex.{Repo, Room, RoomServer} + alias Architex.{Repo, Room, RoomServer, Event} alias Architex.Types.UserId - alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban} + alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban, Messages} alias Ecto.Changeset alias Plug.Conn @@ -50,13 +50,9 @@ defmodule ArchitexWeb.Client.RoomController do |> select([jr], jr.id) |> Repo.all() - data = %{ - joined_rooms: joined_room_ids - } - conn |> put_status(200) - |> json(data) + |> json(%{joined_rooms: joined_room_ids}) end @doc """ @@ -235,10 +231,32 @@ defmodule ArchitexWeb.Client.RoomController do # GET /_matrix/client/r0/rooms/!atYDsyowueiToUvuqY:localhost:4000/messages # Parameters: %{"dir" => "b", "from" => "", "limit" => "727", "path" => ["_matrix", "client", "r0", "rooms", "!atYDsyowueiToUvuqY:localhost:4000", "messages"]} - def message(conn, params) do + def messages(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do + # IO.inspect(Messages.changeset(%Messages{}, params)) - conn - |> send_resp(400, []) - |> halt() + with {:ok, request} <- Messages.parse(params) do + room_query = + account + |> Ecto.assoc(:joined_rooms) + |> where([r], r.id == ^room_id) + + case Repo.one(room_query) do + %Room{} = room -> + {events, start, end_} = Room.get_messages(room, request) + events = Enum.map(events, &Event.Formatters.for_client/1) + data = %{chunk: events} + data = if start, do: Map.put(data, :start, start), else: data + data = if end_, do: Map.put(data, :end, end_), else: data + + conn + |> put_status(200) + |> json(data) + + nil -> + put_error(conn, :forbidden, "You are not participating in this room.") + end + else + {:error, _} -> put_error(conn, :bad_json) + end end end diff --git a/lib/architex_web/client/request/messages.ex b/lib/architex_web/client/request/messages.ex new file mode 100644 index 0000000..e02c7e3 --- /dev/null +++ b/lib/architex_web/client/request/messages.ex @@ -0,0 +1,21 @@ +defmodule ArchitexWeb.Client.Request.Messages do + use ArchitexWeb.Request + + @primary_key false + embedded_schema do + field :from, :string + field :to, :string + field :dir, :string + field :limit, :integer + field :filter, :string + end + + def changeset(data, params) do + data + |> cast(params, [:from, :to, :dir, :limit, :filter], empty_values: []) + |> validate_required([:dir]) + |> Architex.validate_not_nil([:from]) + |> validate_inclusion(:dir, ["b", "f"]) + |> validate_number(:limit, greater_than: 0) + end +end diff --git a/lib/architex_web/error.ex b/lib/architex_web/error.ex index 12a4e9a..b8abc5e 100644 --- a/lib/architex_web/error.ex +++ b/lib/architex_web/error.ex @@ -6,7 +6,7 @@ defmodule ArchitexWeb.Error do bad_json: {400, "M_BAD_JSON", "Bad request."}, user_in_use: {400, "M_USER_IN_USE", "Username is already taken."}, invalid_username: {400, "M_INVALID_USERNAME", "Invalid username."}, - forbidden: {400, "M_FORBIDDEN", "The requested action is forbidden."}, + forbidden: {403, "M_FORBIDDEN", "The requested action is forbidden."}, unrecognized: {400, "M_UNRECOGNIZED", "Unrecognized request."}, unknown: {400, "M_UNKNOWN", "An unknown error occurred."}, invalid_room_state: diff --git a/priv/repo/migrations/20210830160818_create_initial_tables.exs b/priv/repo/migrations/20210830160818_create_initial_tables.exs index f285edc..5f28c00 100644 --- a/priv/repo/migrations/20210830160818_create_initial_tables.exs +++ b/priv/repo/migrations/20210830160818_create_initial_tables.exs @@ -43,6 +43,7 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do end create index(:events, [:id], unique: true) + create index(:events, [:origin_server_ts]) create table(:server_key_info, primary_key: false) do add :valid_until, :bigint, default: 0, null: false @@ -80,7 +81,10 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do create table(:device_transactions, primary_key: false) do add :txn_id, :string, primary_key: true, null: false - add :device_nid, references(:devices, column: :nid, on_delete: :delete_all), primary_key: true + + add :device_nid, references(:devices, column: :nid, on_delete: :delete_all), + primary_key: true + add :event_id, :string, null: false end end