architex/lib/matrix_server/schema/event.ex

462 lines
12 KiB
Elixir
Raw Normal View History

2021-07-03 10:30:57 +00:00
defmodule MatrixServer.Event do
2021-07-10 21:16:00 +00:00
use Ecto.Schema
2021-07-03 10:30:57 +00:00
import Ecto.Query
2021-07-03 10:30:57 +00:00
alias MatrixServer.{Repo, Room, Event, Account, EncodableMap, KeyServer}
2021-08-19 21:42:24 +00:00
alias MatrixServer.Types.UserId
# TODO: Could refactor to also always set prev_events, but not necessary.
@type t :: %__MODULE__{
type: String.t(),
origin_server_ts: integer(),
2021-08-19 21:42:24 +00:00
state_key: String.t(),
sender: UserId.t(),
content: map(),
prev_events: [String.t()] | nil,
auth_events: [String.t()],
unsigned: map() | nil,
signatures: map() | nil,
hashes: map() | nil
}
2021-07-03 10:30:57 +00:00
2021-07-17 15:38:20 +00:00
@primary_key {:event_id, :string, []}
2021-07-10 21:16:00 +00:00
schema "events" do
field :type, :string
field :origin_server_ts, :integer
2021-07-10 21:16:00 +00:00
field :state_key, :string
2021-08-19 21:42:24 +00:00
field :sender, UserId
2021-07-17 15:38:20 +00:00
field :content, :map
2021-07-10 21:16:00 +00:00
field :prev_events, {:array, :string}
field :auth_events, {:array, :string}
field :unsigned, :map
field :signatures, {:map, {:map, :string}}
field :hashes, {:map, :string}
2021-07-17 15:38:20 +00:00
belongs_to :room, Room, type: :string
2021-07-03 10:30:57 +00:00
end
defimpl Jason.Encoder, for: Event do
@pdu_keys [
:auth_events,
:content,
:depth,
:hashes,
:origin,
:origin_server_ts,
:prev_events,
:redacts,
:room_id,
:sender,
:signatures,
:state_key,
:type,
:unsigned
]
def encode(event, opts) do
event
|> Map.take(@pdu_keys)
|> Map.update!(:sender, &Kernel.to_string/1)
|> Jason.Encode.map(opts)
end
end
2021-08-19 21:42:24 +00:00
@spec new(Room.t(), Account.t()) :: %Event{}
2021-07-27 10:55:36 +00:00
def new(%Room{id: room_id}, %Account{localpart: localpart}) do
2021-07-17 15:38:20 +00:00
%Event{
room_id: room_id,
2021-08-19 21:42:24 +00:00
sender: %UserId{localpart: localpart, domain: MatrixServer.server_name()},
origin_server_ts: DateTime.utc_now() |> DateTime.to_unix(:millisecond),
2021-07-17 15:38:20 +00:00
prev_events: [],
auth_events: []
}
end
2021-08-19 21:42:24 +00:00
@spec create_room(Room.t(), Account.t(), String.t()) :: t()
def create_room(room, %Account{localpart: localpart} = creator, room_version) do
2021-07-27 10:55:36 +00:00
mxid = MatrixServer.get_mxid(localpart)
%Event{
new(room, creator)
2021-07-17 15:38:20 +00:00
| type: "m.room.create",
state_key: "",
content: %{
2021-07-27 10:55:36 +00:00
"creator" => mxid,
2021-07-17 16:54:49 +00:00
"room_version" => room_version || MatrixServer.default_room_version()
}
2021-07-17 15:38:20 +00:00
}
end
@spec join(Room.t(), Account.t()) :: t()
def join(room, %Account{localpart: localpart} = sender) do
2021-07-27 10:55:36 +00:00
mxid = MatrixServer.get_mxid(localpart)
%Event{
new(room, sender)
2021-07-17 15:38:20 +00:00
| type: "m.room.member",
2021-07-27 10:55:36 +00:00
state_key: mxid,
2021-07-17 15:38:20 +00:00
content: %{
"membership" => "join"
}
2021-07-17 15:38:20 +00:00
}
end
@spec power_levels(Room.t(), Account.t()) :: t()
def power_levels(room, %Account{localpart: localpart} = sender) do
2021-07-27 10:55:36 +00:00
mxid = MatrixServer.get_mxid(localpart)
%Event{
new(room, sender)
2021-07-17 16:54:49 +00:00
| type: "m.room.power_levels",
state_key: "",
content: %{
"ban" => 50,
"events" => %{},
"events_default" => 0,
"invite" => 50,
"kick" => 50,
"redact" => 50,
"state_default" => 50,
"users" => %{
2021-07-27 10:55:36 +00:00
mxid => 50
2021-07-17 16:54:49 +00:00
},
"users_default" => 0,
"notifications" => %{
"room" => 50
}
}
2021-07-17 16:54:49 +00:00
}
end
@spec name(Room.t(), Account.t(), String.t()) :: %Event{}
def name(room, sender, name) do
%Event{
new(room, sender)
2021-07-17 16:54:49 +00:00
| type: "m.room.name",
state_key: "",
content: %{
"name" => name
}
2021-07-17 16:54:49 +00:00
}
end
@spec topic(Room.t(), Account.t(), String.t()) :: t()
def topic(room, sender, topic) do
%Event{
new(room, sender)
2021-07-17 16:54:49 +00:00
| type: "m.room.topic",
state_key: "",
content: %{
"topic" => topic
}
2021-07-17 16:54:49 +00:00
}
end
@spec join_rules(Room.t(), Account.t(), String.t()) :: t()
def join_rules(room, sender, join_rule) do
%Event{
2021-07-27 10:55:36 +00:00
new(room, sender)
| type: "m.room.join_rules",
state_key: "",
content: %{
"join_rule" => join_rule
}
2021-07-27 10:55:36 +00:00
}
end
@spec history_visibility(Room.t(), Account.t(), String.t()) :: t()
def history_visibility(room, sender, history_visibility) do
%Event{
2021-07-27 10:55:36 +00:00
new(room, sender)
| type: "m.room.history_visibility",
state_key: "",
content: %{
"history_visibility" => history_visibility
}
2021-07-27 10:55:36 +00:00
}
end
@spec guest_access(Room.t(), Account.t(), String.t()) :: t()
def guest_access(room, sender, guest_access) do
%Event{
2021-07-27 10:55:36 +00:00
new(room, sender)
| type: "m.room.guest_access",
state_key: "",
content: %{
"guest_access" => guest_access
}
}
end
@spec invite(Room.t(), Account.t(), String.t()) :: t()
def invite(room, sender, user_id) do
%Event{
new(room, sender)
| type: "m.room.member",
state_key: user_id,
content: %{
"membership" => "invite"
}
2021-07-27 10:55:36 +00:00
}
2021-07-17 15:38:20 +00:00
end
2021-07-25 12:57:52 +00:00
@spec leave(Room.t(), Account.t()) :: t()
def leave(room, sender) do
%Event{
new(room, sender)
| type: "m.room.member",
state_key: Account.get_mxid(sender),
content: %{
"membership" => "leave"
}
}
end
2021-08-19 21:42:24 +00:00
@spec is_control_event(t()) :: boolean()
2021-07-25 12:57:52 +00:00
def is_control_event(%Event{type: "m.room.power_levels", state_key: ""}), do: true
def is_control_event(%Event{type: "m.room.join_rules", state_key: ""}), do: true
def is_control_event(%Event{
type: "m.room.member",
state_key: state_key,
sender: sender,
content: %{membership: membership}
2021-08-19 21:42:24 +00:00
}) do
to_string(sender) != state_key and membership in ["leave", "ban"]
end
2021-07-25 12:57:52 +00:00
def is_control_event(_), do: false
2021-08-19 21:42:24 +00:00
@spec is_state_event(t()) :: boolean()
2021-07-25 12:57:52 +00:00
def is_state_event(%Event{state_key: state_key}), do: state_key != nil
# Perform validations that can be done before state resolution.
# For example checking the domain of the sender.
# We assume that required keys, as well as in the content, is already validated.
# Rule 1.4 is left to changeset validation.
2021-08-19 21:42:24 +00:00
@spec prevalidate(t()) :: boolean()
2021-07-27 10:55:36 +00:00
def prevalidate(%Event{
type: "m.room.create",
prev_events: prev_events,
auth_events: auth_events,
room_id: room_id,
2021-08-19 21:42:24 +00:00
sender: %UserId{domain: domain}
2021-07-27 10:55:36 +00:00
}) do
# TODO: error check on domains?
# TODO: rule 1.3
# Check rules: 1.1, 1.2
prev_events == [] and
auth_events == [] and
2021-08-19 21:42:24 +00:00
domain == MatrixServer.get_domain(room_id)
end
def prevalidate(%Event{auth_events: auth_event_ids, prev_events: prev_event_ids} = event) do
prev_events =
Event
|> where([e], e.event_id in ^prev_event_ids)
|> Repo.all()
auth_events =
Event
|> where([e], e.event_id in ^auth_event_ids)
|> Repo.all()
state_pairs = Enum.map(auth_events, &{&1.type, &1.state_key})
# Check rules: 2.1, 2.2, 3
length(auth_events) == length(auth_event_ids) and
length(prev_events) == length(prev_event_ids) and
not MatrixServer.has_duplicates?(state_pairs) and
valid_auth_events?(event, auth_events) and
Enum.find_value(state_pairs, &(&1 == {"m.room.create", ""})) and
do_prevalidate(event, auth_events, prev_events)
end
# Rule 4.1 is left to changeset validation.
2021-08-19 21:42:24 +00:00
@spec do_prevalidate(t(), [t()], [t()]) :: boolean()
defp do_prevalidate(
%Event{type: "m.room.aliases", sender: %UserId{domain: domain}, state_key: state_key},
_,
_
) do
# Check rule: 4.2
2021-08-19 21:42:24 +00:00
domain == MatrixServer.get_domain(state_key)
end
# Rule 5.1 is left to changeset validation.
# Rules 5.2.3, 5.2.4, 5.2.5 is left to state resolution.
# Check rule: 5.2.1
defp do_prevalidate(
2021-08-19 21:42:24 +00:00
%Event{
type: "m.room.member",
content: %{"membership" => "join"},
sender: %UserId{localpart: localpart, domain: domain}
},
_,
2021-08-19 21:42:24 +00:00
[%Event{type: "m.room.create", state_key: %UserId{localpart: localpart, domain: domain}}]
),
do: true
# Check rule: 5.2.2
defp do_prevalidate(
%Event{
type: "m.room.member",
content: %{"membership" => "join"},
sender: sender,
state_key: state_key
},
_,
_
2021-08-19 21:42:24 +00:00
) do
to_string(sender) == state_key
end
# All other rules will be checked during state resolution.
defp do_prevalidate(_, _, _), do: true
2021-08-19 21:42:24 +00:00
@spec valid_auth_events?(t(), [t()]) :: boolean()
defp valid_auth_events?(
%Event{type: type, sender: sender, state_key: state_key, content: content},
auth_events
) do
2021-08-19 21:42:24 +00:00
sender = to_string(sender)
Enum.all?(auth_events, fn
%Event{type: "m.room.create", state_key: ""} ->
true
%Event{type: "m.room.power_levels", state_key: ""} ->
true
%Event{type: "m.room.member", state_key: ^sender} ->
true
%Event{type: auth_type, state_key: auth_state_key} ->
if type == "m.room.member" do
%{"membership" => membership} = content
(auth_type == "m.room.member" and auth_state_key == state_key) or
(membership in ["join", "invite"] and auth_type == "m.room.join_rules" and
auth_state_key == "") or
(membership == "invite" and auth_type == "m.room.third_party_invite" and
auth_state_key == "")
else
false
end
end)
end
2021-08-19 21:42:24 +00:00
@spec calculate_content_hash(t()) :: {:ok, binary()} | {:error, Jason.EncodeError.t()}
defp calculate_content_hash(event) do
m =
event
|> MatrixServer.to_serializable_map()
|> Map.drop([:unsigned, :signature, :hashes])
|> EncodableMap.from_map()
with {:ok, json} <- Jason.encode(m) do
{:ok, :crypto.hash(:sha256, json)}
end
end
2021-08-19 21:42:24 +00:00
@spec redact(t()) :: map()
defp redact(%Event{type: type, content: content} = event) do
redacted_event =
event
|> MatrixServer.to_serializable_map()
|> Map.take([
:event_id,
:type,
:room_id,
:sender,
:state_key,
:content,
:hashes,
:signatures,
:depth,
:prev_events,
:prev_state,
:auth_events,
:origin,
:origin_server_ts,
:membership
])
%{redacted_event | content: redact_content(type, content)}
end
2021-08-19 21:42:24 +00:00
@spec redact_content(String.t(), map()) :: map()
defp redact_content("m.room.member", content), do: Map.take(content, ["membership"])
defp redact_content("m.room.create", content), do: Map.take(content, ["creator"])
defp redact_content("m.room.join_rules", content), do: Map.take(content, ["join_rule"])
defp redact_content("m.room.aliases", content), do: Map.take(content, ["aliases"])
defp redact_content("m.room.history_visibility", content),
do: Map.take(content, ["history_visibility"])
defp redact_content("m.room.power_levels", content),
do:
Map.take(content, [
"ban",
"events",
"events_default",
"kick",
"redact",
"state_default",
"users",
"users_default"
])
defp redact_content(_, _), do: %{}
# Adds content hash, adds signature and calculates event id.
2021-08-19 21:42:24 +00:00
@spec post_process(t()) :: {:ok, t()} | :error
def post_process(event) do
with {:ok, content_hash} <- calculate_content_hash(event) do
encoded_hash = MatrixServer.encode_unpadded_base64(content_hash)
event = %Event{event | hashes: %{"sha256" => encoded_hash}}
with {:ok, sig, key_id} <- KeyServer.sign_object(redact(event)) do
event = %Event{event | signatures: %{MatrixServer.server_name() => %{key_id => sig}}}
with {:ok, event} <- set_event_id(event) do
{:ok, event}
2021-08-19 21:42:24 +00:00
else
_ -> :error
end
end
2021-08-19 21:42:24 +00:00
else
_ -> :error
end
end
2021-08-19 21:42:24 +00:00
@spec set_event_id(t()) :: {:ok, t()} | {:error, Jason.EncodeError.t()}
def set_event_id(event) do
with {:ok, event_id} <- generate_event_id(event) do
{:ok, %Event{event | event_id: event_id}}
end
end
2021-08-19 21:42:24 +00:00
@spec generate_event_id(t()) :: {:ok, String.t()} | {:error, Jason.EncodeError.t()}
defp generate_event_id(event) do
with {:ok, hash} <- calculate_reference_hash(event) do
{:ok, "$" <> MatrixServer.encode_url_safe_base64(hash)}
end
end
2021-08-19 21:42:24 +00:00
@spec calculate_reference_hash(t()) :: {:ok, binary()} | {:error, Jason.EncodeError.t()}
defp calculate_reference_hash(event) do
redacted_event =
event
|> redact()
|> Map.drop([:unsigned, :signature, :age_ts])
with {:ok, json} <- MatrixServer.encode_canonical_json(redacted_event) do
{:ok, :crypto.hash(:sha256, json)}
end
end
2021-07-03 10:30:57 +00:00
end