mirror of
https://github.com/RoboSats/robosats.git
synced 2025-09-13 00:56:22 +00:00
Send nip17 encrypted messages
This commit is contained in:
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user