mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(agentworld): explain missing DM key bundle (#3919)
Co-authored-by: MackJack023 <141124084+MackJack023@users.noreply.github.com>
This commit is contained in:
@@ -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(<MessagingSection />);
|
||||
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(<MessagingSection />);
|
||||
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(<MessagingSection />);
|
||||
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.
|
||||
|
||||
@@ -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<DecryptedMessage[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user