# Copyright 2019-2021 Hubert Chathi # # 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 # 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