Implement client room messages endpoint
This commit is contained in:
parent
40f3eeff7c
commit
0871c3cdd9
9 changed files with 161 additions and 17 deletions
|
@ -271,4 +271,15 @@ defmodule Architex do
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -6,7 +6,7 @@ defmodule Architex.Event do
|
||||||
alias Architex.{Repo, Room, Event, Account, EncodableMap, KeyServer}
|
alias Architex.{Repo, Room, Event, Account, EncodableMap, KeyServer}
|
||||||
alias Architex.Types.UserId
|
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 t :: %__MODULE__{
|
||||||
type: String.t(),
|
type: String.t(),
|
||||||
origin_server_ts: integer(),
|
origin_server_ts: integer(),
|
||||||
|
@ -37,6 +37,7 @@ defmodule Architex.Event do
|
||||||
belongs_to :room, Room, type: :string
|
belongs_to :room, Room, type: :string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Move this to a dedicated function in Event.Formatters.
|
||||||
defimpl Jason.Encoder, for: Event do
|
defimpl Jason.Encoder, for: Event do
|
||||||
@pdu_keys [
|
@pdu_keys [
|
||||||
:auth_events,
|
:auth_events,
|
||||||
|
|
26
lib/architex/schema/event/formatters.ex
Normal file
26
lib/architex/schema/event/formatters.ex
Normal file
|
@ -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
|
|
@ -5,7 +5,7 @@ defmodule Architex.Room do
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias Architex.{Repo, Room, Event, Alias, RoomServer}
|
alias Architex.{Repo, Room, Event, Alias, RoomServer}
|
||||||
alias ArchitexWeb.Client.Request.CreateRoom
|
alias ArchitexWeb.Client.Request.{CreateRoom, Messages}
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
visibility: :public | :private,
|
visibility: :public | :private,
|
||||||
|
@ -22,10 +22,12 @@ defmodule Architex.Room do
|
||||||
has_many :aliases, Alias, foreign_key: :room_id
|
has_many :aliases, Alias, foreign_key: :room_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec changeset(Room.t(), map()) :: Ecto.Changeset.t()
|
||||||
def changeset(room, params \\ %{}) do
|
def changeset(room, params \\ %{}) do
|
||||||
cast(room, params, [:visibility])
|
cast(room, params, [:visibility])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create_changeset(CreateRoom.t()) :: Ecto.Changeset.t()
|
||||||
def create_changeset(%CreateRoom{visibility: visibility}) do
|
def create_changeset(%CreateRoom{visibility: visibility}) do
|
||||||
visibility = visibility || :public
|
visibility = visibility || :public
|
||||||
|
|
||||||
|
@ -33,10 +35,12 @@ defmodule Architex.Room do
|
||||||
|> changeset(%{visibility: visibility})
|
|> changeset(%{visibility: visibility})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec generate_room_id() :: String.t()
|
||||||
def generate_room_id do
|
def generate_room_id do
|
||||||
"!" <> Architex.random_string(18) <> ":" <> Architex.server_name()
|
"!" <> Architex.random_string(18) <> ":" <> Architex.server_name()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update_forward_extremities(Event.t(), Room.t()) :: Room.t()
|
||||||
def update_forward_extremities(
|
def update_forward_extremities(
|
||||||
%Event{
|
%Event{
|
||||||
id: event_id,
|
id: event_id,
|
||||||
|
@ -54,6 +58,7 @@ defmodule Architex.Room do
|
||||||
room
|
room
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create(Account.t(), CreateRoom.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||||
def create(account, input) do
|
def create(account, input) do
|
||||||
with {:ok, %Room{id: room_id}} <- Repo.insert(create_changeset(input)),
|
with {:ok, %Room{id: room_id}} <- Repo.insert(create_changeset(input)),
|
||||||
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
||||||
|
@ -62,4 +67,63 @@ defmodule Architex.Room do
|
||||||
_ -> {:error, :unknown}
|
_ -> {:error, :unknown}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -39,8 +39,7 @@ defmodule ArchitexWeb.Client.LoginController do
|
||||||
|
|
||||||
case Account.login(input) |> Repo.transaction() do
|
case Account.login(input) |> Repo.transaction() do
|
||||||
{:ok,
|
{:ok,
|
||||||
{%Account{localpart: localpart},
|
{%Account{localpart: localpart}, %Device{access_token: access_token, id: device_id}}} ->
|
||||||
%Device{access_token: access_token, id: device_id}}} ->
|
|
||||||
data = %{
|
data = %{
|
||||||
user_id: Architex.get_mxid(localpart),
|
user_id: Architex.get_mxid(localpart),
|
||||||
access_token: access_token,
|
access_token: access_token,
|
||||||
|
|
|
@ -4,9 +4,9 @@ defmodule ArchitexWeb.Client.RoomController do
|
||||||
import ArchitexWeb.Error
|
import ArchitexWeb.Error
|
||||||
import Ecto.{Changeset, Query}
|
import Ecto.{Changeset, Query}
|
||||||
|
|
||||||
alias Architex.{Repo, Room, RoomServer}
|
alias Architex.{Repo, Room, RoomServer, Event}
|
||||||
alias Architex.Types.UserId
|
alias Architex.Types.UserId
|
||||||
alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban}
|
alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban, Messages}
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
alias Plug.Conn
|
alias Plug.Conn
|
||||||
|
|
||||||
|
@ -50,13 +50,9 @@ defmodule ArchitexWeb.Client.RoomController do
|
||||||
|> select([jr], jr.id)
|
|> select([jr], jr.id)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|
||||||
data = %{
|
|
||||||
joined_rooms: joined_room_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_status(200)
|
|> put_status(200)
|
||||||
|> json(data)
|
|> json(%{joined_rooms: joined_room_ids})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -235,10 +231,32 @@ defmodule ArchitexWeb.Client.RoomController do
|
||||||
|
|
||||||
# GET /_matrix/client/r0/rooms/!atYDsyowueiToUvuqY:localhost:4000/messages
|
# GET /_matrix/client/r0/rooms/!atYDsyowueiToUvuqY:localhost:4000/messages
|
||||||
# Parameters: %{"dir" => "b", "from" => "", "limit" => "727", "path" => ["_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))
|
||||||
|
|
||||||
|
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
|
conn
|
||||||
|> send_resp(400, [])
|
|> put_status(200)
|
||||||
|> halt()
|
|> 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
|
||||||
end
|
end
|
||||||
|
|
21
lib/architex_web/client/request/messages.ex
Normal file
21
lib/architex_web/client/request/messages.ex
Normal file
|
@ -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
|
|
@ -6,7 +6,7 @@ defmodule ArchitexWeb.Error do
|
||||||
bad_json: {400, "M_BAD_JSON", "Bad request."},
|
bad_json: {400, "M_BAD_JSON", "Bad request."},
|
||||||
user_in_use: {400, "M_USER_IN_USE", "Username is already taken."},
|
user_in_use: {400, "M_USER_IN_USE", "Username is already taken."},
|
||||||
invalid_username: {400, "M_INVALID_USERNAME", "Invalid username."},
|
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."},
|
unrecognized: {400, "M_UNRECOGNIZED", "Unrecognized request."},
|
||||||
unknown: {400, "M_UNKNOWN", "An unknown error occurred."},
|
unknown: {400, "M_UNKNOWN", "An unknown error occurred."},
|
||||||
invalid_room_state:
|
invalid_room_state:
|
||||||
|
|
|
@ -43,6 +43,7 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do
|
||||||
end
|
end
|
||||||
|
|
||||||
create index(:events, [:id], unique: true)
|
create index(:events, [:id], unique: true)
|
||||||
|
create index(:events, [:origin_server_ts])
|
||||||
|
|
||||||
create table(:server_key_info, primary_key: false) do
|
create table(:server_key_info, primary_key: false) do
|
||||||
add :valid_until, :bigint, default: 0, null: false
|
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
|
create table(:device_transactions, primary_key: false) do
|
||||||
add :txn_id, :string, primary_key: true, null: false
|
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
|
add :event_id, :string, null: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue