Files
self c1f4339dde refactor: move Aether into TribeOne plugin namespace
Use TribeOne.TribesPlugin.Aether modules throughout the plugin and expose chat@1 from the entry module for capability-based consumers.
2026-05-26 01:13:28 +02:00

245 lines
7.6 KiB
Elixir

defmodule TribeOne.TribesPlugin.Aether.ChatBackendTest do
use Tribes.PluginTest.PageCase, plugin: TribeOne.TribesPlugin.Aether.Plugin
alias TribeOne.TribesPlugin.Aether.Chat
alias TribeOne.TribesPlugin.Aether.Chat.Nostr.Nak
alias Parrhesia.API.Events
alias Parrhesia.API.RequestContext
test "NIP-17 DM flow sends a user-to-user message", %{
signed_in_conn: sender_conn,
current_user: sender,
conn: conn
} do
recipient = create_user!()
{:ok, view, _html} = live(sender_conn, "/aether/chat")
assert has_element?(view, "#chat-recipient-picker")
view
|> form("#chat-recipient-search-form", %{"recipient" => %{"query" => recipient.username}})
|> render_change()
view
|> element("#chat-recipient-#{recipient.id}")
|> render_click()
assert_redirect(
view,
Chat.standalone_path(Chat.direct_slug(sender.pubkey_hex, recipient.pubkey_hex))
)
{:ok, sender_view, _html} =
live(
sender_conn,
Chat.standalone_path(Chat.direct_slug(sender.pubkey_hex, recipient.pubkey_hex))
)
sender_view
|> form("#chat-message-form", %{"message" => %{"body" => "hello recipient"}})
|> render_submit()
assert has_element?(sender_view, "#chat-messages", "hello recipient")
recipient_conn = sign_in(conn, recipient.username)
{:ok, recipient_view, _html} =
live(
recipient_conn,
Chat.standalone_path(Chat.direct_slug(sender.pubkey_hex, recipient.pubkey_hex))
)
assert has_element?(recipient_view, "#chat-messages", "hello recipient")
end
test "Aether publishes real NIP-17 giftwraps that nak can unwrap" do
require_executable!("nak")
{sender_pubkey, sender_privkey} = Tribes.Keyring.generate_keypair()
recipient_privkey = nak_key_generate!()
recipient_pubkey = nak_key_public!(recipient_privkey)
{:ok, channel} =
Chat.ensure_direct_conversation(sender_pubkey, recipient_pubkey, %{
title: "NIP-17 interop"
})
assert {:ok, message} =
Chat.send_message(
channel,
%{body: "hello nak recipient", author_pubkey: sender_pubkey},
session_privkey: sender_privkey
)
assert message.body == "hello nak recipient"
[giftwrap] = giftwraps_for(recipient_pubkey)
rumor = nak_gift_unwrap!(recipient_privkey, giftwrap)
assert giftwrap["kind"] == 1059
assert ["p", recipient_pubkey] in Enum.map(giftwrap["tags"], &Enum.take(&1, 2))
assert rumor["kind"] == 14
assert rumor["pubkey"] == sender_pubkey
assert rumor["content"] == "hello nak recipient"
assert ["p", recipient_pubkey] in Enum.map(rumor["tags"], &Enum.take(&1, 2))
end
test "Aether reads external NIP-17 giftwraps produced by nak" do
require_executable!("nak")
external_sender_privkey = nak_key_generate!()
external_sender_pubkey = nak_key_public!(external_sender_privkey)
{recipient_pubkey, recipient_privkey} = Tribes.Keyring.generate_keypair()
{:ok, channel} =
Chat.ensure_direct_conversation(recipient_pubkey, external_sender_pubkey, %{
title: "External NIP-17"
})
giftwrap =
nak_nip17_giftwrap!(external_sender_privkey, recipient_pubkey, "hello from external nak")
:ok = publish_event!(giftwrap)
assert {:ok, [message]} =
Chat.list_conversation_messages(channel, session_privkey: recipient_privkey)
assert message.author_pubkey == external_sender_pubkey
assert message.body == "hello from external nak"
end
test "NIP-04 backend imports decryptable legacy DMs as read-only" do
require_executable!("nak")
external_sender_privkey = nak_key_generate!()
external_sender_pubkey = nak_key_public!(external_sender_privkey)
{recipient_pubkey, recipient_privkey} = Tribes.Keyring.generate_keypair()
{:ok, channel} =
Chat.ensure_direct_conversation(recipient_pubkey, external_sender_pubkey, %{
title: "Legacy NIP-04",
backend: :nostr_nip04,
conversation_kind: :legacy_dm
})
{:ok, ciphertext} =
Nak.nip04_encrypt(external_sender_privkey, recipient_pubkey, "legacy hello from nak")
legacy_event = nak_event!(external_sender_privkey, 4, recipient_pubkey, ciphertext)
:ok = publish_event!(legacy_event)
assert {:ok, [message]} =
Chat.list_conversation_messages(channel, session_privkey: recipient_privkey)
assert message.author_pubkey == external_sender_pubkey
assert message.body == "legacy hello from nak"
assert {:error, :read_only_backend} =
Chat.send_message(channel, %{body: "nope"}, session_privkey: recipient_privkey)
end
test "backend capabilities prepare Nostr-compatible non-custodial backends" do
assert %{canonical_store: :parrhesia_events, non_custodial_signing?: true} =
Chat.backend_capabilities(:nostr_nip17)
assert %{read_only?: true, required_protocols: [:nip04]} =
Chat.backend_capabilities(:nostr_nip04)
end
defp create_user! do
username = "chat_recipient_#{System.unique_integer([:positive])}"
{:ok, user} =
Ash.create(
Tribes.Accounts.User,
%{username: username, password: "password_123", password_confirmation: "password_123"},
action: :register_with_password,
domain: Tribes.Accounts
)
Tribes.Plugin.User.from_host(user)
end
defp publish_event!(event) do
assert {:ok, result} =
Events.publish(event,
context: %RequestContext{
caller: :local,
authenticated_pubkeys: MapSet.new([event["pubkey"]])
}
)
assert result.accepted
:ok
end
defp giftwraps_for(recipient_pubkey) do
assert {:ok, events} =
Events.query(
[%{"kinds" => [1059], "#p" => [recipient_pubkey], "limit" => 10}],
context: %RequestContext{
caller: :local,
authenticated_pubkeys: MapSet.new([recipient_pubkey])
}
)
events
end
defp nak_nip17_giftwrap!(sender_privkey, recipient_pubkey, body) do
script = ~S'''
set -euo pipefail
nak event --sec "$1" -k 14 -p "$2" -c "$3" </dev/null 2>/dev/null |
nak gift wrap --sec "$1" -p "$2" 2>/dev/null
'''
script |> run_script!([sender_privkey, recipient_pubkey, body]) |> JSON.decode!()
end
defp nak_event!(sender_privkey, kind, recipient_pubkey, content) do
script = ~S'''
set -euo pipefail
nak event --sec "$1" -k "$2" -p "$3" -c "$4" </dev/null 2>/dev/null
'''
script
|> run_script!([sender_privkey, to_string(kind), recipient_pubkey, content])
|> JSON.decode!()
end
defp nak_gift_unwrap!(recipient_privkey, giftwrap) do
script = ~S'''
set -euo pipefail
printf '%s\n' "$2" | nak gift unwrap --sec "$1" 2>/dev/null
'''
script
|> run_script!([recipient_privkey, JSON.encode!(giftwrap)])
|> JSON.decode!()
end
defp nak_key_generate!, do: run_script!("nak key generate", [])
defp nak_key_public!(privkey), do: run_script!("nak key public \"$1\"", [privkey])
defp require_executable!(name) do
unless System.find_executable(name) do
flunk("expected #{name} in PATH")
end
end
defp run_script!(script, args) do
task =
Task.async(fn ->
System.cmd("bash", ["-lc", script, "bash" | args], stderr_to_stdout: true)
end)
case Task.yield(task, 30_000) || Task.shutdown(task, :brutal_kill) do
{:ok, {output, 0}} -> String.trim(output)
{:ok, {output, status}} -> flunk("script failed with status #{status}:\n#{output}")
nil -> flunk("script timed out")
end
end
end