From bd3e3eab007fc5304fddde4d7310488a2ec79128 Mon Sep 17 00:00:00 2001 From: Felix <141124084+Felyx-Fu@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:15:06 +0800 Subject: [PATCH] fix(agentworld): explain missing DM key bundle (#3919) Co-authored-by: MackJack023 <141124084+MackJack023@users.noreply.github.com> --- .../pages/MessagingSection.test.tsx | 70 +++++++++++++++++++ app/src/agentworld/pages/MessagingSection.tsx | 35 ++++++++-- app/src/lib/i18n/ar.ts | 2 + app/src/lib/i18n/bn.ts | 2 + app/src/lib/i18n/de.ts | 2 + app/src/lib/i18n/en.ts | 2 + app/src/lib/i18n/es.ts | 2 + app/src/lib/i18n/fr.ts | 2 + app/src/lib/i18n/hi.ts | 2 + app/src/lib/i18n/id.ts | 2 + app/src/lib/i18n/it.ts | 2 + app/src/lib/i18n/ko.ts | 2 + app/src/lib/i18n/pl.ts | 2 + app/src/lib/i18n/pt.ts | 2 + app/src/lib/i18n/ru.ts | 2 + app/src/lib/i18n/zh-CN.ts | 2 + 16 files changed, 127 insertions(+), 6 deletions(-) diff --git a/app/src/agentworld/pages/MessagingSection.test.tsx b/app/src/agentworld/pages/MessagingSection.test.tsx index 909c230ac..64eba9ece 100644 --- a/app/src/agentworld/pages/MessagingSection.test.tsx +++ b/app/src/agentworld/pages/MessagingSection.test.tsx @@ -279,6 +279,76 @@ describe('DMs panel (E2E enabled)', () => { // The messages namespace has no 'send' method — only signal.sendMessage is callable }); + test('shows a friendly error when the recipient has no published key bundle', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.messages.list).mockResolvedValue({ messages: [] }); + vi.mocked(apiClient.signal.sendMessage).mockRejectedValueOnce( + new Error( + 'CoreRpcError: HTTP 404: HTTP 404: /keys/61KcG5aGLqpnJz2fn4tujFKAdzqsdGR9XqiUeVoT3vPg/bundle: not found' + ) + ); + + render(); + const peerInput = screen.getByPlaceholderText(/Recipient @handle/); + await user.type(peerInput, 'peer456'); + await user.click(screen.getByRole('button', { name: 'Open DM' })); + + const composeInput = await screen.findByPlaceholderText(/Type a message/); + await user.type(composeInput, 'secret'); + await user.click(screen.getByRole('button', { name: 'Send' })); + + expect( + await screen.findByText(/This user hasn't enabled encrypted messaging yet/) + ).toBeInTheDocument(); + expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument(); + expect(screen.queryByText(/\/keys\//)).not.toBeInTheDocument(); + }); + + test('normalizes the core mapped missing encrypted messaging setup error', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.messages.list).mockResolvedValue({ messages: [] }); + vi.mocked(apiClient.signal.sendMessage).mockRejectedValueOnce( + new Error( + 'CoreRpcError: Recipient has not set up encrypted messaging yet. They need to provision Signal keys before receiving DMs. (resolved agent_id: resolved-crypto-id)' + ) + ); + + render(); + const peerInput = screen.getByPlaceholderText(/Recipient @handle/); + await user.type(peerInput, 'peer456'); + await user.click(screen.getByRole('button', { name: 'Open DM' })); + + const composeInput = await screen.findByPlaceholderText(/Type a message/); + await user.type(composeInput, 'secret'); + await user.click(screen.getByRole('button', { name: 'Send' })); + + expect( + await screen.findByText(/This user hasn't enabled encrypted messaging yet/) + ).toBeInTheDocument(); + expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument(); + expect(screen.queryByText(/resolved agent_id/)).not.toBeInTheDocument(); + }); + + test('normalizes missing key bundle errors while refreshing direct messages', async () => { + const user = userEvent.setup(); + vi.mocked(apiClient.messages.list).mockRejectedValueOnce( + new Error( + 'CoreRpcError: HTTP 404: HTTP 404: /keys/61KcG5aGLqpnJz2fn4tujFKAdzqsdGR9XqiUeVoT3vPg/bundle: not found' + ) + ); + + render(); + const peerInput = screen.getByPlaceholderText(/Recipient @handle/); + await user.type(peerInput, 'peer456'); + await user.click(screen.getByRole('button', { name: 'Open DM' })); + + expect( + await screen.findByText(/This user hasn't enabled encrypted messaging yet/) + ).toBeInTheDocument(); + expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument(); + expect(screen.queryByText(/\/keys\//)).not.toBeInTheDocument(); + }); + test('received messages are decrypted before display', async () => { // directory.resolve returns 'resolved-crypto-id'; messages must use that id // in the 'from' field to survive the peerId filter in useDirectMessages. diff --git a/app/src/agentworld/pages/MessagingSection.tsx b/app/src/agentworld/pages/MessagingSection.tsx index 42ce33c50..9cf4e7551 100644 --- a/app/src/agentworld/pages/MessagingSection.tsx +++ b/app/src/agentworld/pages/MessagingSection.tsx @@ -27,6 +27,7 @@ import { PaymentRequiredError, type SignalKeyStatus, } from '../../lib/agentworld/invokeApiClient'; +import { useT } from '../../lib/i18n/I18nContext'; import { fetchWalletStatus } from '../../services/walletApi'; import { apiClient } from '../AgentWorldShell'; import { useTinyplaceStream } from '../hooks/useTinyplaceStream'; @@ -1049,7 +1050,21 @@ interface DecryptedMessage { outgoing?: boolean; } -function useDirectMessages(peerId: string) { +function formatDirectMessageError(err: unknown, missingBundleMessage: string): string { + const message = String(err); + const rawBundle404 = /http\s*404/i.test(message) && /\/keys\/[^/\s]+\/bundle\b/i.test(message); + const mappedMissingBundle = + /recipient/i.test(message) && + /not\s+set\s+up/i.test(message) && + /encrypted\s+messaging/i.test(message); + if (rawBundle404 || mappedMissingBundle) { + log('[agentworld:dm] recipient key bundle missing'); + return missingBundleMessage; + } + return message; +} + +function useDirectMessages(peerId: string, missingBundleMessage: string) { const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -1086,11 +1101,11 @@ function useDirectMessages(peerId: string) { return merged.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); }); } catch (err) { - setError(String(err)); + setError(formatDirectMessageError(err, missingBundleMessage)); } finally { setLoading(false); } - }, [peerId]); + }, [missingBundleMessage, peerId]); const send = useCallback( async (plaintext: string) => { @@ -1113,12 +1128,12 @@ function useDirectMessages(peerId: string) { ); await refresh(); } catch (err) { - setError(String(err)); + setError(formatDirectMessageError(err, missingBundleMessage)); } finally { setSending(false); } }, - [peerId, refresh] + [missingBundleMessage, peerId, refresh] ); useEffect(() => { @@ -1139,7 +1154,15 @@ function ActiveDmView({ composeText: string; setComposeText: (v: string) => void; }) { - const { messages, loading, error, sending, send } = useDirectMessages(peerId); + const { t } = useT(); + const missingBundleMessage = t( + 'agentworld.messaging.missingSignalBundle', + "This user hasn't enabled encrypted messaging yet. Ask them to open Agent World and enable secure DMs before sending a message." + ); + const { messages, loading, error, sending, send } = useDirectMessages( + peerId, + missingBundleMessage + ); const handleSend = useCallback(async () => { if (!composeText.trim()) return; diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 9edd7f082..a1f8caab7 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -5774,6 +5774,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'إلغاء', 'agentworld.jobs.applyModal.submit': 'إرسال الطلب', 'agentworld.jobs.applyModal.submitting': 'جارٍ التقديم…', + 'agentworld.messaging.missingSignalBundle': + 'لم يفعّل هذا المستخدم الرسائل المشفرة بعد. اطلب منه فتح Agent World وتفعيل الرسائل المباشرة الآمنة قبل إرسال رسالة.', }; export default messages; diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index f51cd5aad..d8bf168d0 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -5891,6 +5891,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'বাতিল', 'agentworld.jobs.applyModal.submit': 'আবেদন জমা দিন', 'agentworld.jobs.applyModal.submitting': 'আবেদন করা হচ্ছে…', + 'agentworld.messaging.missingSignalBundle': + 'এই ব্যবহারকারী এখনো এনক্রিপ্টেড মেসেজিং চালু করেননি। বার্তা পাঠানোর আগে তাকে Agent World খুলে নিরাপদ DM চালু করতে বলুন।', }; export default messages; diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 3f30a900e..606a9c735 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -6055,6 +6055,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Abbrechen', 'agentworld.jobs.applyModal.submit': 'Bewerbung einreichen', 'agentworld.jobs.applyModal.submitting': 'Wird eingereicht…', + 'agentworld.messaging.missingSignalBundle': + 'Dieser Benutzer hat verschlüsselte Nachrichten noch nicht aktiviert. Bitte ihn, Agent World zu öffnen und sichere DMs zu aktivieren, bevor du eine Nachricht sendest.', }; export default messages; diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 049ff6b09..42b2842e5 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -6160,6 +6160,8 @@ const en: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Cancel', 'agentworld.jobs.applyModal.submit': 'Submit Application', 'agentworld.jobs.applyModal.submitting': 'Applying…', + 'agentworld.messaging.missingSignalBundle': + "This user hasn't enabled encrypted messaging yet. Ask them to open Agent World and enable secure DMs before sending a message.", }; export default en; diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 26f97b5f1..9f72c54d8 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -6015,6 +6015,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Cancelar', 'agentworld.jobs.applyModal.submit': 'Enviar solicitud', 'agentworld.jobs.applyModal.submitting': 'Enviando…', + 'agentworld.messaging.missingSignalBundle': + 'Este usuario aún no ha activado la mensajería cifrada. Pídele que abra Agent World y active los DM seguros antes de enviarle un mensaje.', }; export default messages; diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index c56135b04..cde0bc198 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -6033,6 +6033,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Annuler', 'agentworld.jobs.applyModal.submit': 'Soumettre la candidature', 'agentworld.jobs.applyModal.submitting': 'Envoi en cours…', + 'agentworld.messaging.missingSignalBundle': + "Cet utilisateur n'a pas encore activé la messagerie chiffrée. Demandez-lui d'ouvrir Agent World et d'activer les messages privés sécurisés avant d'envoyer un message.", }; export default messages; diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 5a6d0ee0c..6e73f84dd 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -5893,6 +5893,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'रद्द करें', 'agentworld.jobs.applyModal.submit': 'आवेदन सबमिट करें', 'agentworld.jobs.applyModal.submitting': 'आवेदन हो रहा है…', + 'agentworld.messaging.missingSignalBundle': + 'इस उपयोगकर्ता ने अभी तक एन्क्रिप्टेड मैसेजिंग चालू नहीं की है। संदेश भेजने से पहले उनसे Agent World खोलकर सुरक्षित DM चालू करने को कहें।', }; export default messages; diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 3f4f921ad..71551410c 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -5909,6 +5909,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Batal', 'agentworld.jobs.applyModal.submit': 'Kirim Lamaran', 'agentworld.jobs.applyModal.submitting': 'Melamar…', + 'agentworld.messaging.missingSignalBundle': + 'Pengguna ini belum mengaktifkan pesan terenkripsi. Minta mereka membuka Agent World dan mengaktifkan DM aman sebelum Anda mengirim pesan.', }; export default messages; diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 4dd0cab6e..f6c1f0fea 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -6002,6 +6002,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Annulla', 'agentworld.jobs.applyModal.submit': 'Invia candidatura', 'agentworld.jobs.applyModal.submitting': 'Candidatura in corso…', + 'agentworld.messaging.missingSignalBundle': + 'Questo utente non ha ancora attivato la messaggistica crittografata. Chiedigli di aprire Agent World e attivare i DM sicuri prima di inviare un messaggio.', }; export default messages; diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 1ebccf6c9..32224c6b2 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -5838,6 +5838,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': '취소', 'agentworld.jobs.applyModal.submit': '지원서 제출', 'agentworld.jobs.applyModal.submitting': '지원 중…', + 'agentworld.messaging.missingSignalBundle': + '이 사용자는 아직 암호화 메시지를 활성화하지 않았습니다. 메시지를 보내기 전에 Agent World를 열고 보안 DM을 활성화해 달라고 요청하세요.', }; export default messages; diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index f4322eddf..581aeac2f 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -5983,6 +5983,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Anuluj', 'agentworld.jobs.applyModal.submit': 'Wyślij aplikację', 'agentworld.jobs.applyModal.submitting': 'Wysyłanie…', + 'agentworld.messaging.missingSignalBundle': + 'Ten użytkownik nie włączył jeszcze szyfrowanych wiadomości. Poproś go, aby otworzył Agent World i włączył bezpieczne DM przed wysłaniem wiadomości.', }; export default messages; diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 29ca49a43..8f6c685cb 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -5995,6 +5995,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Cancelar', 'agentworld.jobs.applyModal.submit': 'Enviar candidatura', 'agentworld.jobs.applyModal.submitting': 'A enviar…', + 'agentworld.messaging.missingSignalBundle': + 'Este utilizador ainda não ativou as mensagens encriptadas. Peça-lhe para abrir o Agent World e ativar DMs seguras antes de enviar uma mensagem.', }; export default messages; diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index f08941a8c..ba0b7470e 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -5956,6 +5956,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': 'Отмена', 'agentworld.jobs.applyModal.submit': 'Отправить заявку', 'agentworld.jobs.applyModal.submitting': 'Отправка…', + 'agentworld.messaging.missingSignalBundle': + 'Этот пользователь еще не включил зашифрованные сообщения. Попросите его открыть Agent World и включить безопасные личные сообщения перед отправкой.', }; export default messages; diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 966aee843..7fe79fdba 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -5599,6 +5599,8 @@ const messages: TranslationMap = { 'agentworld.jobs.applyModal.cancel': '取消', 'agentworld.jobs.applyModal.submit': '提交申请', 'agentworld.jobs.applyModal.submitting': '申请中…', + 'agentworld.messaging.missingSignalBundle': + '此用户尚未启用加密消息。发送消息前,请让对方打开 Agent World 并启用安全私信。', }; export default messages;