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
|
|
|
|
2021-07-26 17:47:38 +00:00
|
|
|
import Ecto.Query
|
2021-07-03 10:30:57 +00:00
|
|
|
|
2021-08-05 11:19:38 +00:00
|
|
|
alias MatrixServer.{Repo, Room, Event, Account, OrderedMap, SigningServer}
|
2021-07-03 10:30:57 +00:00
|
|
|
|
2021-08-04 13:28:27 +00:00
|
|
|
@schema_meta_fields [:__meta__]
|
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
|
2021-07-17 15:38:20 +00:00
|
|
|
field :origin_server_ts, :integer
|
2021-07-10 21:16:00 +00:00
|
|
|
field :state_key, :string
|
|
|
|
field :sender, :string
|
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}
|
2021-08-05 11:19:38 +00:00
|
|
|
# TODO: make these database fields eventually?
|
|
|
|
field :signing_keys, :map, virtual: true, default: %{}
|
|
|
|
field :unsigned, :map, virtual: true, default: %{}
|
|
|
|
field :signatures, :map, virtual: true, default: %{}
|
|
|
|
field :hashes, :map, virtual: true, default: %{}
|
2021-07-17 15:38:20 +00:00
|
|
|
belongs_to :room, Room, type: :string
|
2021-07-03 10:30:57 +00:00
|
|
|
end
|
|
|
|
|
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-07-27 10:55:36 +00:00
|
|
|
sender: MatrixServer.get_mxid(localpart),
|
2021-07-17 15:38:20 +00:00
|
|
|
event_id: generate_event_id(),
|
|
|
|
origin_server_ts: DateTime.utc_now() |> DateTime.to_unix(),
|
|
|
|
prev_events: [],
|
|
|
|
auth_events: []
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
def create_room(room, %Account{localpart: localpart} = creator, room_version, auth_events \\ []) do
|
2021-07-27 10:55:36 +00:00
|
|
|
mxid = MatrixServer.get_mxid(localpart)
|
|
|
|
|
2021-07-17 15:38:20 +00:00
|
|
|
%Event{
|
2021-07-26 21:42:35 +00:00
|
|
|
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-29 14:59:40 +00:00
|
|
|
},
|
|
|
|
auth_events: auth_events
|
2021-07-17 15:38:20 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
def join(room, %Account{localpart: localpart} = sender, auth_events \\ []) do
|
2021-07-27 10:55:36 +00:00
|
|
|
mxid = MatrixServer.get_mxid(localpart)
|
|
|
|
|
2021-07-17 15:38:20 +00:00
|
|
|
%Event{
|
2021-07-26 21:42:35 +00:00
|
|
|
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: %{
|
2021-07-23 19:00:01 +00:00
|
|
|
"membership" => "join"
|
2021-07-29 14:59:40 +00:00
|
|
|
},
|
|
|
|
auth_events: auth_events
|
2021-07-17 15:38:20 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
def power_levels(room, %Account{localpart: localpart} = sender, auth_events \\ []) do
|
2021-07-27 10:55:36 +00:00
|
|
|
mxid = MatrixServer.get_mxid(localpart)
|
|
|
|
|
2021-07-17 16:54:49 +00:00
|
|
|
%Event{
|
2021-07-26 21:42:35 +00:00
|
|
|
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-29 14:59:40 +00:00
|
|
|
},
|
|
|
|
auth_events: auth_events
|
2021-07-17 16:54:49 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
def name(room, sender, name, auth_events \\ []) do
|
2021-07-17 16:54:49 +00:00
|
|
|
%Event{
|
2021-07-26 21:42:35 +00:00
|
|
|
new(room, sender)
|
2021-07-17 16:54:49 +00:00
|
|
|
| type: "m.room.name",
|
|
|
|
state_key: "",
|
|
|
|
content: %{
|
|
|
|
"name" => name
|
2021-07-29 14:59:40 +00:00
|
|
|
},
|
|
|
|
auth_events: auth_events
|
2021-07-17 16:54:49 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
def topic(room, sender, topic, auth_events \\ []) do
|
2021-07-17 16:54:49 +00:00
|
|
|
%Event{
|
2021-07-26 21:42:35 +00:00
|
|
|
new(room, sender)
|
2021-07-17 16:54:49 +00:00
|
|
|
| type: "m.room.topic",
|
|
|
|
state_key: "",
|
|
|
|
content: %{
|
|
|
|
"topic" => topic
|
2021-07-29 14:59:40 +00:00
|
|
|
},
|
|
|
|
auth_events: auth_events
|
2021-07-17 16:54:49 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
def join_rules(room, sender, join_rule, auth_events \\ []) do
|
2021-07-27 10:55:36 +00:00
|
|
|
%Event{
|
|
|
|
new(room, sender)
|
|
|
|
| type: "m.room.join_rules",
|
|
|
|
state_key: "",
|
|
|
|
content: %{
|
|
|
|
"join_rule" => join_rule
|
2021-07-29 14:59:40 +00:00
|
|
|
},
|
|
|
|
auth_events: auth_events
|
2021-07-27 10:55:36 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
def history_visibility(room, sender, history_visibility, auth_events \\ []) do
|
2021-07-27 10:55:36 +00:00
|
|
|
%Event{
|
|
|
|
new(room, sender)
|
|
|
|
| type: "m.room.history_visibility",
|
|
|
|
state_key: "",
|
|
|
|
content: %{
|
|
|
|
"history_visibility" => history_visibility
|
2021-07-29 14:59:40 +00:00
|
|
|
},
|
|
|
|
auth_events: auth_events
|
2021-07-27 10:55:36 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
def guest_access(room, sender, guest_access, auth_events \\ []) do
|
2021-07-27 10:55:36 +00:00
|
|
|
%Event{
|
|
|
|
new(room, sender)
|
|
|
|
| type: "m.room.guest_access",
|
|
|
|
state_key: "",
|
|
|
|
content: %{
|
|
|
|
"guest_access" => guest_access
|
2021-07-29 14:59:40 +00:00
|
|
|
},
|
|
|
|
auth_events: auth_events
|
2021-07-27 10:55:36 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-17 15:38:20 +00:00
|
|
|
def generate_event_id do
|
|
|
|
"$" <> MatrixServer.random_string(17) <> ":" <> MatrixServer.server_name()
|
|
|
|
end
|
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}
|
|
|
|
})
|
|
|
|
when sender != state_key and membership in ["leave", "ban"],
|
|
|
|
do: true
|
|
|
|
|
|
|
|
def is_control_event(_), do: false
|
|
|
|
|
|
|
|
def is_state_event(%Event{state_key: state_key}), do: state_key != nil
|
2021-07-26 17:47:38 +00:00
|
|
|
|
|
|
|
# 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-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,
|
|
|
|
sender: sender
|
|
|
|
}) do
|
2021-07-26 17:47:38 +00:00
|
|
|
# TODO: error check on domains?
|
|
|
|
# TODO: rule 1.3
|
|
|
|
|
|
|
|
# Check rules: 1.1, 1.2
|
|
|
|
prev_events == [] and
|
|
|
|
auth_events == [] and
|
|
|
|
MatrixServer.get_domain(sender) == 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.
|
|
|
|
defp do_prevalidate(%Event{type: "m.room.aliases", sender: sender, state_key: state_key}, _, _) do
|
|
|
|
# Check rule: 4.2
|
|
|
|
MatrixServer.get_domain(sender) == 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(
|
|
|
|
%Event{type: "m.room.member", content: %{"membership" => "join"}, sender: sender},
|
|
|
|
_,
|
|
|
|
[%Event{type: "m.room.create", state_key: sender}]
|
|
|
|
),
|
|
|
|
do: true
|
|
|
|
|
|
|
|
# Check rule: 5.2.2
|
|
|
|
defp do_prevalidate(
|
|
|
|
%Event{
|
|
|
|
type: "m.room.member",
|
|
|
|
content: %{"membership" => "join"},
|
|
|
|
sender: sender,
|
|
|
|
state_key: state_key
|
|
|
|
},
|
|
|
|
_,
|
|
|
|
_
|
|
|
|
)
|
|
|
|
when sender != state_key,
|
|
|
|
do: false
|
|
|
|
|
|
|
|
# All other rules will be checked during state resolution.
|
|
|
|
defp do_prevalidate(_, _, _), do: true
|
|
|
|
|
|
|
|
defp valid_auth_events?(
|
|
|
|
%Event{type: type, sender: sender, state_key: state_key, content: content},
|
|
|
|
auth_events
|
|
|
|
) do
|
|
|
|
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-04 13:28:27 +00:00
|
|
|
|
2021-08-05 11:19:38 +00:00
|
|
|
def sign(event) do
|
|
|
|
content_hash = calculate_content_hash(event)
|
|
|
|
|
|
|
|
event
|
|
|
|
|> Map.put(:hashes, %{"sha256" => content_hash})
|
|
|
|
|> redact()
|
|
|
|
|> SigningServer.sign_event()
|
|
|
|
end
|
|
|
|
|
|
|
|
defp calculate_content_hash(event) do
|
2021-08-04 13:28:27 +00:00
|
|
|
result =
|
|
|
|
event
|
|
|
|
|> to_map()
|
|
|
|
|> Map.drop([:unsigned, :signature, :hashes])
|
|
|
|
|> OrderedMap.from_map()
|
|
|
|
|> Jason.encode()
|
|
|
|
|
|
|
|
case result do
|
|
|
|
{:ok, json} ->
|
|
|
|
:crypto.hash(:sha256, json)
|
2021-08-05 11:19:38 +00:00
|
|
|
|> MatrixServer.encode_unpadded_base64()
|
2021-08-04 13:28:27 +00:00
|
|
|
|
|
|
|
error ->
|
|
|
|
error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-08-05 11:19:38 +00:00
|
|
|
defp redact(%Event{type: type, content: content} = event) do
|
2021-08-04 13:28:27 +00:00
|
|
|
redacted_event =
|
|
|
|
event
|
|
|
|
|> to_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-05 11:19:38 +00:00
|
|
|
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"])
|
2021-08-04 13:28:27 +00:00
|
|
|
|
|
|
|
defp redact_content("m.room.power_levels", content),
|
|
|
|
do:
|
2021-08-05 11:19:38 +00:00
|
|
|
Map.take(content, [
|
2021-08-04 13:28:27 +00:00
|
|
|
"ban",
|
|
|
|
"events",
|
|
|
|
"events_default",
|
|
|
|
"kick",
|
|
|
|
"redact",
|
|
|
|
"state_default",
|
|
|
|
"users",
|
|
|
|
"users_default"
|
|
|
|
])
|
|
|
|
|
|
|
|
# https://stackoverflow.com/questions/41523762/41671211
|
|
|
|
def to_map(event) do
|
|
|
|
association_fields = event.__struct__.__schema__(:associations)
|
|
|
|
waste_fields = association_fields ++ @schema_meta_fields
|
|
|
|
|
|
|
|
event
|
|
|
|
|> Map.from_struct()
|
|
|
|
|> Map.drop(waste_fields)
|
|
|
|
end
|
2021-07-03 10:30:57 +00:00
|
|
|
end
|