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 | 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 ''' 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