Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement IRCv3 draft/message-redaction #53

Merged
merged 4 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion lib/irc/command.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
##
# Copyright (C) 2021-2022 Valentin Lorentz
# Copyright (C) 2021-2023 Valentin Lorentz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License version 3,
Expand Down Expand Up @@ -251,6 +251,8 @@ defmodule M51.Irc.Command do

"""
def downgrade(command, capabilities) do
original_tags = command.tags

# downgrade echo-message
command =
if Enum.member?(capabilities, :echo_message) do
Expand Down Expand Up @@ -319,6 +321,47 @@ defmodule M51.Irc.Command do
nil
end

%{command: "REDACT"} ->
if Enum.member?(capabilities, :message_redaction) do
command
else
sender = Map.get(original_tags, "account")

display_name =
case Map.get(original_tags, "+draft/display-name", nil) do
dn when is_binary(dn) -> " (#{dn})"
_ -> ""
end

tags = Map.drop(command.tags, ["+draft/display-name", "account"])

command =
case command do
%{params: [channel, msgid, reason]} ->
%M51.Irc.Command{
tags: Map.put(tags, "+draft/reply", msgid),
source: "server.",
command: "NOTICE",
params: [channel, "#{sender}#{display_name} deleted an event: #{reason}"]
}

%{params: [channel, msgid]} ->
%M51.Irc.Command{
tags: Map.put(tags, "+draft/reply", msgid),
source: "server.",
command: "NOTICE",
params: [channel, "#{sender}#{display_name} deleted an event"]
}

_ ->
# shouldn't happen
nil
end

# run downgrade() recursively in order to drop the new tags if necessary
downgrade(command, capabilities)
end

%{command: "TAGMSG"} ->
if Enum.member?(capabilities, :message_tags) do
command
Expand Down
79 changes: 79 additions & 0 deletions lib/irc/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ defmodule M51.IrcConn.Handler do
# https://ircv3.net/specs/extensions/multiline
"draft/multiline" => {:multiline, "max-bytes=#{@multiline_max_bytes}"},

# https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
"draft/message-redaction" => {:message_redaction, nil},

# https://ircv3.net/specs/extensions/sasl-3.1
"sasl" => {:sasl, "PLAIN"},

Expand Down Expand Up @@ -903,6 +906,27 @@ defmodule M51.IrcConn.Handler do
{"TAGMSG", _} ->
send_needmoreparams.()

{"REDACT", [channel, targetmsgid, reason | _]} ->
send_redact(
sup_pid,
channel,
Map.get(command.tags, "label"),
targetmsgid,
reason
)

{"REDACT", [channel, targetmsgid | _]} ->
send_redact(
sup_pid,
channel,
Map.get(command.tags, "label"),
targetmsgid,
nil
)

{"REDACT", _} ->
send_needmoreparams.()

{"CHATHISTORY", ["TARGETS", _ts1, _ts2, _limit | _]} ->
# This is mainly used for PMs, and we don't support those yet; so there
# is little point in storing state to actually implement it
Expand Down Expand Up @@ -1374,6 +1398,61 @@ defmodule M51.IrcConn.Handler do
end
end

defp send_redact(sup_pid, channel, label, targetmsgid, reason) do
writer = M51.IrcConn.Supervisor.writer(sup_pid)
matrix_client = M51.IrcConn.Supervisor.matrix_client(sup_pid)
matrix_state = M51.IrcConn.Supervisor.matrix_state(sup_pid)
send = fn cmd -> M51.IrcConn.Writer.write_command(writer, cmd) end

# If the client provided a label, use it as txnId on Matrix's side.
# This way we can parse it when receiving the echo from Matrix's event
# stream instead of storing state.
# Otherwise, generate a random transaction id.

nicklist =
case M51.MatrixClient.State.room_from_irc_channel(matrix_state, channel) do
{_room_id, room} -> room.members |> Map.keys()
nil -> []
end

reason =
case reason do
nil ->
nil

reason ->
{reason, _formatted_reason} = M51.Format.irc2matrix(reason, nicklist)
reason
end

result =
M51.MatrixClient.Client.send_redact(
matrix_client,
channel,
label,
targetmsgid,
reason
)

case result do
{:ok, _event_id} ->
nil

{:error, error} ->
send.(%M51.Irc.Command{
source: "server.",
command: "FAIL",
params: [
"REDACT",
"UNKNOWN_ERROR",
channel,
targetmsgid,
"Error while redacting message: " <> Kernel.inspect(error)
]
})
end
end

defp close_connection(sup_pid) do
writer = M51.IrcConn.Supervisor.writer(sup_pid)
M51.IrcConn.Writer.close(writer)
Expand Down
48 changes: 47 additions & 1 deletion lib/matrix_client/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,42 @@ defmodule M51.MatrixClient.Client do
end
end

@impl true
def handle_call({:send_redact, channel, label, event_id, reason}, _from, state) do
%M51.MatrixClient.Client{
state: :connected,
irc_pid: irc_pid,
raw_client: raw_client
} = state

matrix_state = M51.IrcConn.Supervisor.matrix_state(irc_pid)

transaction_id = label_to_transaction_id(label)

reply =
case M51.MatrixClient.State.room_from_irc_channel(matrix_state, channel) do
nil ->
{:reply, {:error, {:room_not_found, channel}}, state}

{room_id, _room} ->
path =
"/_matrix/client/r0/rooms/#{urlquote(room_id)}/redact/#{urlquote(event_id)}/#{transaction_id}"

body =
case reason do
reason when is_binary(reason) -> Jason.encode!(%{"reason" => reason})
_ -> Jason.encode!({})
end

case M51.Matrix.RawClient.put(raw_client, path, body) do
{:ok, %{"event_id" => event_id}} -> {:ok, event_id}
{:error, error} -> {:error, error}
end
end

{:reply, reply, state}
end

@impl true
def handle_call({:get_event_context, channel, event_id, limit}, _from, state) do
%M51.MatrixClient.Client{
Expand Down Expand Up @@ -543,13 +579,23 @@ defmodule M51.MatrixClient.Client do
@doc """
Sends the given event object.

If 'label' is not nil, it will be passed as a 'label' message tagt when
If 'label' is not nil, it will be passed as a 'label' message tag when
the event is seen in the event stream.
"""
def send_event(pid, channel, label, event_type, event) do
GenServer.call(pid, {:send_event, channel, event_type, label, event}, @timeout)
end

@doc """
Asks the server to redact the event with the given id

If 'label' is not nil, it will be passed as a 'label' message tag when
the event is seen in the event stream.
"""
def send_redact(pid, channel, label, event_id, reason) do
GenServer.call(pid, {:send_redact, channel, label, event_id, reason}, @timeout)
end

@doc """
Returns events that happened just before or after the specified event_id.

Expand Down
49 changes: 26 additions & 23 deletions lib/matrix_client/poller.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
##
# Copyright (C) 2021-2022 Valentin Lorentz
# Copyright (C) 2021-2023 Valentin Lorentz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License version 3,
Expand Down Expand Up @@ -884,38 +884,41 @@ defmodule M51.MatrixClient.Poller do
member = M51.MatrixClient.State.room_member(state, room_id, sender)
send = make_send_function(sup_pid, event, write)

reason =
case event["content"] do
%{"reason" => reason} when is_binary(reason) -> ": #{reason}"
_ -> ""
end
tags = %{"account" => sender}

# TODO: dedup this with m.reaction handler
display_name =
tags =
case member do
%M51.Matrix.RoomMember{display_name: display_name} when display_name != nil ->
" (#{display_name})"
Map.put(tags, "+draft/display-name", display_name)

_ ->
""
tags
end

tags =
case event do
%{"redacts" => redacts_id}
when is_binary(redacts_id) ->
%{"+draft/reply" => redacts_id}
case event do
%{"redacts" => redacts_id} when is_binary(redacts_id) ->
case event["content"] do
%{"reason" => reason} when is_binary(reason) ->
send.(%M51.Irc.Command{
tags: tags,
source: nick2nuh(sender),
command: "REDACT",
params: [channel, redacts_id, reason]
})

_ ->
%{}
end
_ ->
send.(%M51.Irc.Command{
tags: tags,
source: nick2nuh(sender),
command: "REDACT",
params: [channel, redacts_id]
})
end

send.(%M51.Irc.Command{
tags: tags,
source: "server.",
command: "NOTICE",
params: [channel, "#{sender}#{display_name} deleted an event#{reason}"]
})
_ ->
nil
end
end

def handle_event(
Expand Down
20 changes: 18 additions & 2 deletions test/irc/handler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ defmodule M51.IrcConn.HandlerTest do
use ExUnit.Case, async: false
doctest M51.IrcConn.Handler

@cap_ls_302 ":server. CAP * LS :account-tag batch draft/account-registration=before-connect draft/channel-rename draft/chathistory draft/multiline=max-bytes=8192 draft/sasl-ir echo-message extended-join labeled-response message-tags sasl=PLAIN server-time soju.im/account-required standard-replies userhost-in-names\r\n"
@cap_ls ":server. CAP * LS :account-tag batch draft/account-registration draft/channel-rename draft/chathistory draft/multiline draft/sasl-ir echo-message extended-join labeled-response message-tags sasl server-time soju.im/account-required standard-replies userhost-in-names\r\n"
@cap_ls_302 ":server. CAP * LS :account-tag batch draft/account-registration=before-connect draft/channel-rename draft/chathistory draft/message-redaction draft/multiline=max-bytes=8192 draft/sasl-ir echo-message extended-join labeled-response message-tags sasl=PLAIN server-time soju.im/account-required standard-replies userhost-in-names\r\n"
@cap_ls ":server. CAP * LS :account-tag batch draft/account-registration draft/channel-rename draft/chathistory draft/message-redaction draft/multiline draft/sasl-ir echo-message extended-join labeled-response message-tags sasl server-time soju.im/account-required standard-replies userhost-in-names\r\n"
@isupport "CASEMAPPING=rfc3454 CLIENTTAGDENY=*,-draft/react,-draft/reply CHANLIMIT= CHANTYPES=#! CHATHISTORY=100 MAXTARGETS=1 MSGREFTYPES=msgid PREFIX= TARGMAX=JOIN:1,PART:1 UTF8ONLY :are supported by this server\r\n"

setup do
Expand Down Expand Up @@ -1078,4 +1078,20 @@ defmodule M51.IrcConn.HandlerTest do

assert_line("BATCH :-#{batch_id}\r\n")
end

test "redact a message for no reason", %{handler: handler} do
do_connection_registration(handler)

send(handler, cmd("REDACT #existing_room:example.org $event1"))

assert_message({:send_redact, "#existing_room:example.org", nil, "$event1", nil})
end

test "redact a message for a reason", %{handler: handler} do
do_connection_registration(handler)

send(handler, cmd("REDACT #existing_room:example.org $event1 :spam"))

assert_message({:send_redact, "#existing_room:example.org", nil, "$event1", "spam"})
end
end
Loading