Send nip17 encrypted messages

This commit is contained in:
koalasat
2025-05-09 12:36:16 +02:00
parent bcd36e61f2
commit 506338cb54
12 changed files with 68 additions and 19 deletions

View File

@ -267,12 +267,18 @@ class OrderDetailSerializer(serializers.ModelSerializer):
maker_hash_id = serializers.CharField(
required=False, help_text="The maker's robot hash"
)
maker_nostr_pubkey = serializers.CharField(
required=False, help_text="The maker's robot nostr hex pubkey"
)
taker_nick = serializers.CharField(
required=False, help_text="The taker's robot hash"
)
taker_hash_id = serializers.CharField(
required=False, help_text="The taker's robot hash"
)
taker_nostr_pubkey = serializers.CharField(
required=False, help_text="The taker's robot nostr hex pubkey"
)
status_message = serializers.CharField(
required=False,
help_text="The current status of the order corresponding to the `status`",
@ -444,8 +450,10 @@ class OrderDetailSerializer(serializers.ModelSerializer):
"is_seller",
"maker_nick",
"maker_hash_id",
"maker_nostr_pubkey",
"taker_nick",
"taker_hash_id",
"taker_nostr_pubkey",
"status_message",
"is_fiat_sent",
"is_disputed",

View File

@ -267,6 +267,7 @@ class OrderView(viewsets.ViewSet):
data["maker_nick"] = str(order.maker)
data["maker_hash_id"] = str(order.maker.robot.hash_id)
data["maker_nostr_pubkey"] = str(order.maker.robot.nostr_pubkey)
# Add activity status of participants based on last_seen
data["maker_status"] = Logics.user_activity_status(order.maker.last_login)
@ -308,6 +309,7 @@ class OrderView(viewsets.ViewSet):
data["taker_nick"] = str(order.taker)
if order.taker:
data["taker_hash_id"] = str(order.taker.robot.hash_id)
data["taker_nostr_pubkey"] = str(order.taker.robot.nostr_pubkey)
data["status_message"] = Order.Status(order.status).label
data["is_fiat_sent"] = order.is_fiat_sent
data["latitude"] = order.latitude

View File

@ -34,7 +34,7 @@
"latlon-geohash": "^2.0.0",
"leaflet": "^1.9.4",
"light-bolt11-decoder": "^3.1.1",
"nostr-tools": "^2.10.4",
"nostr-tools": "^2.12.0",
"npm": "^11.0.0",
"openpgp": "^5.11.0",
"react": "^18.2.0",
@ -12234,9 +12234,9 @@
}
},
"node_modules/nostr-tools": {
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz",
"integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",

View File

@ -75,7 +75,7 @@
"latlon-geohash": "^2.0.0",
"leaflet": "^1.9.4",
"light-bolt11-decoder": "^3.1.1",
"nostr-tools": "^2.10.4",
"nostr-tools": "^2.12.0",
"npm": "^11.0.0",
"openpgp": "^5.11.0",
"react": "^18.2.0",

View File

@ -38,6 +38,7 @@ interface Props {
setMessages: (messages: EncryptedChatMessage[]) => void;
turtleMode: boolean;
setTurtleMode: (state: boolean) => void;
onSendMessage: (content: string) => void;
}
const EncryptedSocketChat: React.FC<Props> = ({
@ -51,6 +52,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
setMessages,
turtleMode,
setTurtleMode,
onSendMessage,
}: Props): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
@ -248,6 +250,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
}
// If input string contains '#' send unencrypted and unlogged message
else if (connection != null && value.substring(0, 1) === '#') {
onSendMessage(value);
connection.send(
JSON.stringify({
type: 'message',
@ -263,6 +266,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
setValue('');
setWaitingEcho(true);
setLastSent(value);
onSendMessage(value);
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, slot.token)
.then((encryptedMessage) => {
if (connection != null) {

View File

@ -32,6 +32,7 @@ interface Props {
setMessages: (messages: EncryptedChatMessage[]) => void;
turtleMode: boolean;
setTurtleMode: (state: boolean) => void;
onSendMessage: (content: string) => void;
}
const audioPath =
@ -50,6 +51,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
setMessages,
setTurtleMode,
turtleMode,
onSendMessage,
}: Props): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
@ -198,6 +200,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
const { url, basePath } = federation
.getCoordinator(garage.getSlot()?.activeOrder?.shortAlias ?? '')
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
onSendMessage(value);
apiClient
.post(
url + basePath,
@ -226,6 +229,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
else if (value !== '' && Boolean(robot?.pubKey)) {
setWaitingEcho(true);
setLastSent(value);
onSendMessage(value);
encryptMessage(value, robot?.pubKey, peerPubKey ?? '', robot?.encPrivKey, slot?.token)
.then((encryptedMessage) => {
const { url, basePath } = federation

View File

@ -1,7 +1,13 @@
import React, { useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { type Order, type Robot } from '../../../models';
import EncryptedSocketChat from './EncryptedSocketChat';
import EncryptedTurtleChat from './EncryptedTurtleChat';
import { nip17 } from 'nostr-tools';
import { GarageContext, type UseGarageStoreType } from '../../../contexts/GarageContext';
import {
FederationContext,
type UseFederationStoreType,
} from '../../../contexts/FederationContext';
interface Props {
order: Order;
@ -36,11 +42,35 @@ const EncryptedChat: React.FC<Props> = ({
status,
}: Props): JSX.Element => {
const [turtleMode, setTurtleMode] = useState<boolean>(false);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
useEffect(() => {
const coordinator = federation.getCoordinator(order.shortAlias);
federation.roboPool.connect([coordinator.getRelayUrl()]);
}, []);
const onSendMessage = (content: string): void => {
const slot = garage.getSlot();
const coordinator = federation.getCoordinator(order.shortAlias);
if (!slot?.nostrSecKey) return;
const recipient = {
publicKey: order.is_maker ? order.taker_nostr_pubkey : order.maker_nostr_pubkey,
relayUrl: coordinator.getRelayUrl(),
};
const wrappedEvent = nip17.wrapEvent(slot?.nostrSecKey, recipient, content);
federation.roboPool.sendEvent(wrappedEvent);
};
return turtleMode ? (
<EncryptedTurtleChat
messages={messages}
setMessages={setMessages}
onSendMessage={onSendMessage}
order={order}
takerNick={order.taker_nick}
takerHashId={order.taker_hash_id}
@ -55,6 +85,7 @@ const EncryptedChat: React.FC<Props> = ({
status={status}
messages={messages}
setMessages={setMessages}
onSendMessage={onSendMessage}
order={order}
takerNick={order.taker_nick}
takerHashId={order.taker_hash_id}

View File

@ -303,6 +303,10 @@ export class Coordinator {
return { url: String(this[network][origin]), basePath: '' };
}
};
getRelayUrl = (): string => {
return `ws://${this.url.replace(/^https?:\/\//, '')}/nostr`;
};
}
export default Coordinator;

View File

@ -65,7 +65,7 @@ export class Federation {
if (tesnetHost) settings.network = 'testnet';
this.connection = null;
this.roboPool = new RoboPool(settings, origin);
this.roboPool = new RoboPool(settings, Object.values(this.coordinators));
}
private coordinators: Record<string, Coordinator>;

View File

@ -93,8 +93,10 @@ class Order {
is_seller: boolean = false;
maker_nick: string = '';
maker_hash_id: string = '';
maker_nostr_pubkey: string = '';
taker_nick: string = '';
taker_hash_id: string = '';
taker_nostr_pubkey: string = '';
status_message: string = '';
is_fiat_sent: boolean = false;
is_disputed: boolean = false;

View File

@ -75,6 +75,7 @@ class Robot {
last_login: data.last_login,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
nostrPubKey: data.nostr_pubkey,
});
})
.catch((e) => {

View File

@ -1,5 +1,5 @@
import { type Event } from 'nostr-tools';
import { type Settings } from '../../models';
import { type Coordinator, type Settings } from '../../models';
import defaultFederation from '../../../static/federation.json';
import { websocketClient, type WebsocketConnection, WebsocketState } from '../Websocket';
import thirdParties from '../../../static/thirdparties.json';
@ -10,19 +10,12 @@ interface RoboPoolEvents {
}
class RoboPool {
constructor(settings: Settings, origin: string) {
constructor(settings: Settings, coordinators: Coordinator[]) {
this.network = settings.network ?? 'mainnet';
this.relays = [];
const federationRelays = Object.values(defaultFederation)
.map((coord) => {
const url: string = coord[this.network]?.[settings.selfhostedClient ? 'onion' : origin];
const federationRelays = coordinators.map((coord) => coord.getRelayUrl());
if (!url) return undefined;
return `ws://${url.replace(/^https?:\/\//, '')}/nostr`;
})
.filter((item) => item !== undefined);
if (settings.host) {
const hostNostr = `ws://${settings.host.replace(/^https?:\/\//, '')}/nostr`;
if (federationRelays.includes(hostNostr)) {
@ -44,8 +37,8 @@ class RoboPool {
public webSockets: Record<string, WebsocketConnection | null> = {};
private readonly messageHandlers: Array<(url: string, event: MessageEvent) => void> = [];
connect = (): void => {
this.relays.forEach((url: string) => {
connect = (relays: string[] = this.relays): void => {
relays.forEach((url: string) => {
if (Object.keys(this.webSockets).find((wUrl) => wUrl === url)) return;
this.webSockets[url] = null;