Packages

Utility functions for the Matrix.org communications protocol

Current section

Files

Jump to
polyjuice_util lib polyjuice util room_version.ex
Raw

lib/polyjuice/util/room_version.ex

# Copyright 2019-2021 Hubert Chathi <hubert@uhoreg.ca>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-FileCopyrightText: 2019-2021 Hubert Chathi <hubert@uhoreg.ca>
# SPDX-License-Identifier: Apache-2.0
defmodule Polyjuice.Util.RoomVersion do
@moduledoc """
Functions related to room versions.
"""
@typedoc """
The state of a room.
A map from `{event_type, state_key}` to state event.
"""
@type state() :: %{optional({String.t(), String.t()}) => map}
@doc false
def safe_module_for_room_ver(room_version) do
Module.safe_concat(Polyjuice.Util.RoomVersion, "V" <> room_version)
end
@doc false
def module_for_room_ver(room_version) do
Module.concat(Polyjuice.Util.RoomVersion, "V" <> room_version)
end
@doc false
def get_domain(id) do
with [_, domain] <- String.split(id, ":", parts: 2) do
domain
else
_ -> nil
end
end
@doc false
def as_integer(num) when is_integer(num), do: num
def as_integer(num) when is_binary(num), do: String.to_integer(num)
@doc ~S"""
Get the room version from an `m.room.create` event.
Examples:
iex> Polyjuice.Util.RoomVersion.get_room_version(%{
...> "type" => "m.room.create",
...> "sender" => "@alice:example.org",
...> "content" => %{
...> "room_version" => "2"
...> }
...> })
"2"
"""
@spec get_room_version(event :: map()) :: String.t()
def get_room_version(%{"type" => "m.room.create"} = event) do
Map.get(event, "content", %{}) |> Map.get("room_version", "1")
end
@doc ~S"""
Calculate the redacted version of an event for the given room version.
Examples:
iex> Polyjuice.Util.RoomVersion.redact(
...> "1",
...> %{
...> "content" => %{
...> "body" => "Here is the message content"
...> },
...> "event_id" => "$0:domain",
...> "origin" => "domain",
...> "origin_server_ts" => 1000000,
...> "type" => "m.room.message",
...> "room_id" => "!r:domain",
...> "sender" => "@u:domain",
...> "signatures" => %{},
...> "unsigned" => %{
...> "age_ts" => 1000000
...> }
...> }
...> )
{
:ok,
%{
"content" => %{},
"event_id" => "$0:domain",
"origin" => "domain",
"origin_server_ts" => 1000000,
"type" => "m.room.message",
"room_id" => "!r:domain",
"sender" => "@u:domain",
"signatures" => %{}
}
}
iex> Polyjuice.Util.RoomVersion.redact(
...> "unknown room version",
...> %{}
...> )
:error
"""
@spec redact(room_version :: String.t(), event :: map) :: {:ok, map} | :error
def redact(room_version, event) when is_binary(room_version) and is_map(event) do
try do
module = safe_module_for_room_ver(room_version)
{:ok, apply(module, :redact, [event])}
rescue
_ -> :error
end
end
@doc ~S"""
Compute the content hash for an event.
"""
@spec compute_content_hash(room_version :: String.t(), event :: map) ::
{:ok, binary} | :error
def compute_content_hash(room_version, event) when is_binary(room_version) and is_map(event) do
try do
module = safe_module_for_room_ver(room_version)
{:ok, apply(module, :compute_content_hash, [event])}
rescue
_ -> :error
end
end
@doc ~S"""
Compute the reference hash for an event.
"""
@spec compute_reference_hash(room_version :: String.t(), event :: map) ::
{:ok, binary} | :error
def compute_reference_hash(room_version, event)
when is_binary(room_version) and is_map(event) do
with {:ok, redacted} <- redact(room_version, event),
{:ok, event_json_bytes} <-
redacted
|> Map.drop(~w(signatures age_ts unsigned))
|> Polyjuice.Util.JSON.canonical_json() do
{:ok, :crypto.hash(:sha256, event_json_bytes)}
else
_ -> :error
end
end
@doc ~S"""
Get the user's power level, given the room state.
Examples:
iex> Polyjuice.Util.RoomVersion.get_user_pl(
...> "1",
...> "@alice:example.org",
...> %{
...> {"m.room.power_levels", ""} => %{
...> "type" => "m.room.power_levels",
...> "state_key" => "",
...> "sender" => "@alice:example.org",
...> "content" => %{
...> "users" => %{
...> "@alice:example.org" => 75
...> }
...> }
...> }
...> }
...> )
{:ok, 75}
iex> Polyjuice.Util.RoomVersion.get_user_pl(
...> "1",
...> "@bob:example.org",
...> %{
...> {"m.room.power_levels", ""} => %{
...> "type" => "m.room.power_levels",
...> "state_key" => "",
...> "sender" => "@alice:example.org",
...> "content" => %{
...> "users" => %{
...> "@alice:example.org" => 75
...> }
...> }
...> }
...> }
...> )
{:ok, 0}
"""
@spec get_user_pl(
room_version :: String.t(),
user :: String.t(),
state :: state()
) :: {:ok, integer} | :error
def get_user_pl(room_version, user, state)
when is_binary(room_version) and is_binary(user) and is_map(state) do
try do
module = safe_module_for_room_ver(room_version)
pl =
case apply(module, :get_user_pl, [user, state]) do
x when is_integer(x) -> x
end
{:ok, pl}
rescue
_ -> :error
end
end
@doc ~S"""
Get the required power level for sending an event.
`event_type` is either a string indicating one of the keys at the top level
of the `m.room.power_levels` event (`ban`, `invite`, `kick`, `redact`), or a
list indicating the path within the `m.room.power_levels` event to check
(e.g. `["events", "m.room.topic"]` or `["notifications", "room"]`).
Examples:
iex> Polyjuice.Util.RoomVersion.get_required_pl(
...> "1",
...> ["events", "m.room.message"],
...> false,
...> %{
...> {"m.room.power_levels", ""} => %{
...> "type" => "m.room.power_levels",
...> "state_key" => "",
...> "sender" => "@alice:example.org",
...> "content" => %{
...> "events" => %{
...> "m.room.topic" => 75
...> }
...> }
...> }
...> }
...> )
{:ok, 0}
iex> Polyjuice.Util.RoomVersion.get_required_pl(
...> "1",
...> ["events", "m.room.name"],
...> true,
...> %{
...> {"m.room.power_levels", ""} => %{
...> "type" => "m.room.power_levels",
...> "state_key" => "",
...> "sender" => "@alice:example.org",
...> "content" => %{
...> "events" => %{
...> "m.room.topic" => 75
...> }
...> }
...> }
...> }
...> )
{:ok, 50}
iex> Polyjuice.Util.RoomVersion.get_required_pl(
...> "1",
...> ["events", "m.room.topic"],
...> true,
...> %{
...> {"m.room.power_levels", ""} => %{
...> "type" => "m.room.power_levels",
...> "state_key" => "",
...> "sender" => "@alice:example.org",
...> "content" => %{
...> "events" => %{
...> "m.room.topic" => 75
...> }
...> }
...> }
...> }
...> )
{:ok, 75}
"""
@spec get_required_pl(
room_version :: String.t(),
event_type :: String.t() | list(String.t()),
is_state :: boolean,
state :: state()
) :: {:ok, integer} | :error
def get_required_pl(room_version, event_type, is_state, state)
when is_binary(room_version) and (is_binary(event_type) or is_list(event_type)) and
is_boolean(is_state) and is_map(state) do
try do
module = safe_module_for_room_ver(room_version)
{:ok, apply(module, :get_required_pl, [event_type, is_state, state])}
rescue
_ -> :error
end
end
@doc ~S"""
Determine if an event is allowed to be sent to a room, given the room state.
Example:
iex> Polyjuice.Util.RoomVersion.authorized?(
...> "1",
...> %{
...> "sender" => "@u:domain",
...> "type" => "m.room.create",
...> "room_id" => "!foo:domain",
...> "content" => %{
...> "creator" => "@u:domain",
...> }
...> },
...> %{}
...> )
true
"""
@spec authorized?(
room_version :: String.t(),
event :: map,
state :: state(),
auth_events :: list(map) | nil
) :: boolean
def authorized?(
room_version,
%{"type" => _type, "sender" => _sender} = event,
state,
auth_events \\ nil
) do
try do
module = safe_module_for_room_ver(room_version)
apply(module, :authorized?, [event, state, auth_events])
rescue
_ -> false
end
end
end
defmodule Polyjuice.Util.RoomVersion.V1 do
@moduledoc false
import Polyjuice.Util.RoomVersion, only: [get_domain: 1, as_integer: 1]
# the common keys that should be kept in the top level of all events
# (except for "content", which has special handling).
@keys_to_keep ~w(
event_id
type
room_id
sender
state_key
hashes
signatures
depth
prev_events
prev_state
auth_events
origin
origin_server_ts
membership
)
@content_hash_keys_to_remove ~w(signatures unsigned hashes)
def redact(event) do
redacted_no_content = Map.take(event, @keys_to_keep)
case Map.fetch(event, "content") do
{:ok, content} ->
content_keys_to_keep =
case Map.get(event, "type", nil) do
"m.room.member" ->
~w(membership)
"m.room.create" ->
~w(creator)
"m.room.join_rules" ->
~w(join_rule)
"m.room.power_levels" ->
~w(ban events events_default kick redact state_default users users_default)
"m.room.aliases" ->
~w(aliases)
"m.room.history_visibility" ->
~w(history_visibility)
_ ->
[]
end
Map.put(
redacted_no_content,
"content",
Map.take(content, content_keys_to_keep)
)
_ ->
redacted_no_content
end
end
def compute_content_hash(event) do
{:ok, event_json_bytes} =
event
|> Map.drop(@content_hash_keys_to_remove)
|> Polyjuice.Util.JSON.canonical_json()
:crypto.hash(:sha256, event_json_bytes)
end
def unique_auth_events(events) do
try do
Enum.reduce(events, MapSet.new(), fn %{"type" => type, "state_key" => state_key}, map ->
if MapSet.member?(map, {type, state_key}),
do: throw(:dup),
else: MapSet.put(map, {type, state_key})
end)
true
catch
_ -> false
end
end
def get_user_pl(user, state) do
room_pl =
state
|> Map.get({"m.room.power_levels", ""}, %{})
|> Map.get("content", %{})
room_pl
|> Map.get("users", %{})
|> Map.get(user, Map.get(room_pl, "users_default", 0))
|> as_integer()
end
def get_required_pl(event_type, is_state, state) do
default_pl = %{
"ban" => 50,
"events_default" => 0,
"invite" => 50,
"kick" => 50,
"notifications" => %{
"room" => 50
},
"redact" => 50,
"state_default" => 50,
"users_default" => 0
}
room_pl =
state
|> Map.get({"m.room.power_levels", ""}, %{})
|> Map.get("content", %{})
# the required power level if given explicitly in the state
required_power_level =
if is_list(event_type) do
get_in(room_pl, event_type) || get_in(default_pl, event_type)
else
Map.get(room_pl, event_type, Map.get(default_pl, event_type))
end
# get default if no explicit PL given
required_power_level =
if required_power_level == nil do
k = if is_state, do: "state_default", else: "events_default"
Map.get(room_pl, k, Map.get(default_pl, k))
else
required_power_level
end
as_integer(required_power_level)
end
def has_power?(user, event_type, is_state, state) do
get_user_pl(user, state) >= get_required_pl(event_type, is_state, state)
end
def allowed_auth_event(%{"type" => type, "state_key" => state_key}) do
state_key == "" &&
Enum.member?(
~w(
m.room.create
m.room.member
m.room.join_rules
m.room.power_levels
m.room.third_party_invite
),
type
)
end
def authorized_room_create?(
%{"type" => type, "sender" => sender} = event,
_state,
_auth_events
) do
# 1. If type is m.room.create:
if type == "m.room.create" do
cond do
# a. If it has any previous events, reject.
Map.get(event, "prev_events", []) != [] ->
{:ok, false}
# b. If the domain of the room_id does not match the domain of
# the sender, reject.
get_domain(Map.fetch!(event, "room_id")) != get_domain(sender) ->
{:ok, false}
# c. If content.room_version is present and is not a recognised
# version, reject.
# NOTE: If this function is called, we have already determined
# the room version from the field, so we don't need to check it.
# d. If content has no creator field, reject.
!Map.has_key?(Map.get(event, "content", %{}), "creator") ->
{:ok, false}
# e. Otherwise, allow.
true ->
{:ok, true}
end
end
end
def authorized_by_auth_events?(_event, _state, auth_events) do
# if auth_events is nil, we're called from a client that doesn't know
# auth events, so just skip the check
if auth_events != nil do
cond do
# 2. Reject if event has auth_events that:
# a. have duplicate entries for a given type and state_key pair
# (Note: we only check auth events if they're supplied, because if we're
# called from a client, they don't know about auth events, so we just
# assume that it passes.)
!unique_auth_events(auth_events) ->
{:ok, false}
# b. have entries whose type and state_key don't match those
# specified by the auth events selection algorithm described in
# the server specification.
!Enum.all?(auth_events, &allowed_auth_event/1) ->
{:ok, false}
# 3. If event does not have a m.room.create in its auth_events, reject.
!Enum.any?(auth_events, fn %{"type" => type, "state_key" => state_key} ->
type == "m.room.create" && state_key == ""
end) ->
{:ok, false}
end
end
end
def authorized_room_aliases?(
%{"type" => type, "sender" => sender} = event,
_state,
_auth_events
) do
# 4. If type is m.room.aliases:
# a. If event has no state_key, reject.
# b. If sender's domain doesn't matches state_key, reject.
# c. Otherwise, allow.
if type == "m.room.aliases" do
{:ok, get_domain(sender) == Map.fetch!(event, "state_key")}
end
end
def authorized_room_membership?(
%{"type" => type, "sender" => sender} = event,
state,
_auth_events
) do
# 5. If type is m.room.member:
if type === "m.room.membership" do
# a. If no state_key key or membership key in content, reject.
user = event |> Map.fetch!("state_key")
content = event |> Map.fetch!("content")
membership = Map.fetch!(content, "membership")
case membership do
# b. If membership is join:
"join" ->
cond do
# i. If the only previous event is an m.room.create and the
# state_key is the creator, allow.
Map.delete(state, {"m.room.create", ""}) == %{} and
state |> Map.get({"m.room.create", ""}) |> Map.fetch!("sender") == user ->
{:ok, true}
# ii. If the sender does not match state_key, reject.
user != sender ->
{:ok, false}
# iii. If the sender is banned, reject.
state
|> Map.get({"m.room.membership", sender}, %{})
|> Map.get("content", %{})
|> Map.get("membership") == "ban" ->
{:ok, false}
# iv. If the join_rule is invite then allow if membership
# state is invite or join.
state
|> Map.fetch!({"m.room.join_rules", ""})
|> Map.fetch!("content")
|> Map.fetch!("join_rule") == "invite" ->
res =
(state
|> Map.get({"m.room.membership", sender}, %{})
|> Map.get("content", %{})
|> Map.get("membership")) in ["invite", "join"]
{:ok, res}
# v. If the join_rule is public, allow.
state
|> Map.fetch!({"m.room.join_rules", ""})
|> Map.fetch!("content")
|> Map.fetch!("join_rule") == "public" ->
{:ok, true}
# vi. Otherwise, reject.
true ->
{:ok, false}
end
# c. If membership is invite:
"invite" ->
cond do
# i. If content has third_party_invite key:
content |> Map.has_key?("third_party_invite") ->
# NOTE: we re-order the checks here to fit what's easiest to do.
# Since the checks that we reorder are all rejections, the order
# does not matter.
# 2. If content.third_party_invite does not have a signed
# key, reject.
# 3. If signed does not have mxid and token keys, reject.
signed = content |> Map.fetch!("third_party_invite") |> Map.fetch!("signed")
mxid = signed |> Map.fetch!("mxid")
token = signed |> Map.fetch!("token")
# 5. If there is no m.room.third_party_invite event in the
# current room state with state_key matching token, reject.
invite = Map.fetch!(state, {"m.room.third_party_invite", token})
{:ok, canonical} =
signed
|> Map.drop(["unsigned", "signatures"])
|> Polyjuice.Util.JSON.canonical_json()
# FIXME: catch errors
verify_key =
Map.fetch!(invite, "public_key")
|> Polyjuice.Util.Ed25519.VerifyKey.from_base64("")
cond do
# 1. If the target user is banned, reject.
state
|> Map.get({"m.room.membership", user}, %{})
|> Map.get("content", %{})
|> Map.get("membership") == "ban" ->
{:ok, false}
# 4. If mxid does not match state_key, reject.
mxid != user ->
{:ok, false}
# 6. If sender does not match sender of the
# m.room.third_party_invite, reject.
sender != Map.fetch!(invite, "sender") ->
{:ok, false}
# 7. If any signature in signed matches any public key in
# the m.room.third_party_invite event, allow. The
# public keys are in content of
# m.room.third_party_invite as:
# - single public key in the public_key field.
Enum.any?(signed, fn {_, v} ->
Enum.any?(v, fn {_, sig} ->
Polyjuice.Util.VerifyKey.verify(verify_key, canonical, sig)
end)
end) ->
{:ok, true}
# - list of public keys in the public_keys field.
Map.fetch!(invite, "public_keys")
|> Enum.any?(fn %{"public_key" => public_key} ->
# FIXME: catch errors
verify_key = Polyjuice.Util.Ed25519.VerifyKey.from_base64(public_key, "")
Enum.any?(signed, fn {_, v} ->
Enum.any?(v, fn {_, sig} ->
Polyjuice.Util.VerifyKey.verify(verify_key, canonical, sig)
false
end)
end)
end) ->
{:ok, true}
# 8. Otherwise, reject
true ->
{:ok, false}
end
# ii. If the sender’s current membership state is not join, reject.
state
|> Map.get({"m.room.membership", sender}, %{})
|> Map.get("content", %{})
|> Map.get("membership") == "join" ->
{:ok, false}
# iii. If target user’s current membership state is join or ban, reject.
(state
|> Map.get({"m.room.membership", user}, %{})
|> Map.get("content", %{})
|> Map.get("membership")) in ["join", "ban"] ->
{:ok, false}
# iv. If the sender’s power level is greater than or equal to the invite level, allow.
has_power?(sender, "invite", true, state) ->
{:ok, true}
# v. Otherwise, reject.
{:ok, false}
end
# d. If membership is leave
"leave" ->
cond do
# i. If the sender matches state_key, allow if and only if that
# user’s current membership state is invite or join.
sender == user ->
result =
(state
|> Map.get({"m.room.membership", user}, %{})
|> Map.get("content", %{})
|> Map.get("membership")) in ["invite", "join"]
{:ok, result}
# ii. If the sender’s current membership state is not join, reject.
state
|> Map.get({"m.room.membership", user}, %{})
|> Map.get("content", %{})
|> Map.get("membership") != "join" ->
{:ok, false}
# iii. If the target user’s current membership state is ban, and
# the sender’s power level is less than the ban level, reject.
state
|> Map.get({"m.room.membership", user}, %{})
|> Map.get("content", %{})
|> Map.get("membership") != "join" and !has_power?(sender, "ban", true, state) ->
{:ok, false}
# iv. If the sender’s power level is greater than or equal to the
# kick level, and the target user’s power level is less than the
# sender’s power level, allow.
has_power?(sender, "kick", true, state) and
get_user_pl(user, state) < get_user_pl(sender, state) ->
{:ok, true}
# v. Otherwise, reject
true ->
{:ok, false}
end
# e. If membership is ban
"ban" ->
cond do
# i. If the sender’s current membership state is not join, reject.
state
|> Map.get({"m.room.membership", sender}, %{})
|> Map.get("content", %{})
|> Map.get("membership") != "join" ->
{:ok, false}
# ii. If the sender’s power level is greater than or equal to the
# ban level, and the target user’s power level is less than the
# sender’s power level, allow.
has_power?(sender, "ban", true, state) and
get_user_pl(user, state) < get_user_pl(sender, state) ->
{:ok, true}
# iii. Otherwise, reject.
true ->
{:ok, false}
end
# f. Otherwise, the membership is unknown. Reject.
_ ->
{:ok, false}
end
end
end
def authorized_by_join?(%{"sender" => sender}, state, _auth_events) do
# 6. If the sender’s current membership state is not join, reject.
membership =
state
|> Map.get({"m.room.membership", sender}, %{})
|> Map.get("content", %{})
|> Map.get("membership")
if membership != "join", do: {:ok, false}
end
def authorized_third_party_invite?(%{"type" => type, "sender" => sender}, state, _auth_events) do
# 7. If type is m.room.third_party_invite:
if type == "m.room.third_party_invite" do
# a. Allow if and only if sender’s current power level is greater than
# or equal to the invite level.
{:ok, has_power?(sender, "invite", true, state)}
end
end
def authorized_by_pl?(%{"type" => type, "sender" => sender} = event, state, _auth_events) do
# 8. If the event type’s required power level is greater than the
# sender’s power level, reject.
is_state = Map.has_key?(event, "state_key")
if !has_power?(sender, ["events", type], is_state, state), do: {:ok, false}
end
def authorized_at_state?(
%{"sender" => sender, "state_key" => <<"@", target::binary>>},
_state,
_auth_events
) do
# 9. If the event has a state_key that starts with an @ and does not
# match the sender, reject.
if sender != target, do: {:ok, false}
end
def authorized_power_levels?(
%{"type" => type, "sender" => sender} = event,
state,
_auth_events
) do
# 10. If type is m.room.power_levels:
if type == "m.room.power_levels" do
curr_pl_event = Map.get(state, {"m.room_power_levels", ""}, :unset)
curr_content = if curr_pl_event != :unset, do: Map.get(curr_pl_event, "content", %{})
curr_events = Map.get(curr_content, "events", %{})
curr_users = Map.get(curr_content, "users", %{})
new_content = Map.fetch!(event, "content")
new_events = Map.get(new_content, "events", %{})
new_users = Map.fetch!(new_content, "users")
sender_pl = get_user_pl(sender, state)
# FIXME: convert integer-as-string to integer
cond do
# a. If users key in content is not a dictionary with keys that are
# valid user IDs with values that are integers (or a string that is
# an integer), reject.
Enum.all?(new_users, fn {user, val} ->
Polyjuice.Util.Identifiers.V0.UserIdentifier.valid?(user) and
(is_integer(val) or String.match?(val, ~r/\d+/))
end) ->
{:ok, false}
# b. If there is no previous m.room.power_levels event in the room,
# allow.
curr_pl_event == :unset ->
{:ok, true}
# c. For the keys users_default, events_default, state_default, ban,
# redact, kick, invite check if they were added, changed or
# removed. For each found alteration:
~w(users_default events_default state_default ban redact kick invite)
|> Enum.any?(fn k ->
curr_k_pl = Map.get(curr_content, k, :unset)
new_k_pl = Map.get(new_content, k, :unset)
if curr_k_pl != new_k_pl do
cond do
# i. If the current value is higher than the sender’s current
# power level, reject.
curr_k_pl != :unset and curr_k_pl > sender_pl -> {:ok, false}
# ii. If the new value is higher than the sender’s current
# power level, reject.
new_k_pl != :unset and new_k_pl > sender_pl -> {:ok, false}
end
end
end) ->
{:ok, false}
# d. For each entry being added, changed or removed in both the
# events and users keys:
# i. If the current value is higher than the sender’s current power
# level, reject.
Enum.any?(curr_events, fn {k, value} ->
if Map.get(new_events, k, :unset) != value do
value > sender_pl
end
end) ->
{:ok, false}
Enum.any?(curr_users, fn {k, value} ->
if k == sender do
if Map.get(new_users, k, :unset) != value do
value > sender_pl
end
else
# this branch also does the check for e. i.
if Map.get(new_users, k, :unset) != value do
value >= sender_pl
end
end
end) ->
{:ok, false}
# ii. If the new value is higher than the sender’s current power
# level, reject.
Enum.any?(new_events, fn {k, value} ->
if Map.get(curr_events, k, :unset) != value do
value > sender_pl
end
end) ->
{:ok, false}
Enum.any?(new_users, fn {k, value} ->
if Map.get(curr_users, k, :unset) != value do
value > sender_pl
end
end) ->
{:ok, false}
# e. For each entry being changed under the users key, other than the
# sender’s own entry:
# i. If the current value is equal to the sender’s current power
# level, reject.
# (this is done as part of the check in d. i.)
# f. Otherwise, allow.
true ->
{:ok, true}
end
end
end
def authorized_redaction?(%{"type" => type, "sender" => sender} = event, state, _auth_events) do
if type == "m.room.redaction" do
cond do
# 1. If the sender’s power level is greater than or equal to the
# redact level, allow.
has_power?(sender, "redact", false, state) ->
{:ok, true}
# 2. If the domain of the event_id of the event being redacted is the
# same as the domain of the event_id of the m.room.redaction, allow.
get_domain(Map.get(event, "event_id")) == get_domain(Map.get(event, "redacts")) ->
{:ok, true}
# 3. Otherwise, reject.
true ->
{:ok, false}
end
end
end
def authorized?(%{"type" => _, "sender" => _} = event, state, auth_events) do
try do
# FIXME: get authorization from auth_events? if present
{:ok, result} =
Enum.find_value(
[
&authorized_room_create?/3,
&authorized_by_auth_events?/3,
&authorized_room_aliases?/3,
&authorized_room_membership?/3,
&authorized_by_join?/3,
&authorized_third_party_invite?/3,
&authorized_by_pl?/3,
&authorized_at_state?/3,
&authorized_power_levels?/3,
&authorized_redaction?/3,
fn _, _, _ -> {:ok, false} end
],
fn f ->
f.(event, state, auth_events)
end
)
result
rescue
_ -> false
end
end
def authorized?(_event, _state, _auth_events), do: false
end
defmodule Polyjuice.Util.RoomVersion.V2 do
@moduledoc false
defdelegate redact(event), to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate get_user_pl(user, state),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate get_required_pl(event_type, is_state, state),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate compute_content_hash(event),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate authorized?(event, state, auth_events),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
end
defmodule Polyjuice.Util.RoomVersion.V3 do
@moduledoc false
defdelegate redact(event), to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate get_user_pl(user, state),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate get_required_pl(event_type, is_state, state),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate compute_content_hash(event),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate authorized?(event, state, auth_events),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
end
defmodule Polyjuice.Util.RoomVersion.V4 do
@moduledoc false
defdelegate redact(event), to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate get_user_pl(user, state),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate get_required_pl(event_type, is_state, state),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate compute_content_hash(event),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate authorized?(event, state, auth_events),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
end
defmodule Polyjuice.Util.RoomVersion.V5 do
@moduledoc false
defdelegate redact(event), to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate get_user_pl(user, state),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate get_required_pl(event_type, is_state, state),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate compute_content_hash(event),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
defdelegate authorized?(event, state, auth_events),
to: Polyjuice.Util.RoomVersion.module_for_room_ver("1")
end