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(
|
maker_hash_id = serializers.CharField(
|
||||||
required=False, help_text="The maker's robot hash"
|
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(
|
taker_nick = serializers.CharField(
|
||||||
required=False, help_text="The taker's robot hash"
|
required=False, help_text="The taker's robot hash"
|
||||||
)
|
)
|
||||||
taker_hash_id = serializers.CharField(
|
taker_hash_id = serializers.CharField(
|
||||||
required=False, help_text="The taker's robot hash"
|
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(
|
status_message = serializers.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
help_text="The current status of the order corresponding to the `status`",
|
help_text="The current status of the order corresponding to the `status`",
|
||||||
@ -444,8 +450,10 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
|||||||
"is_seller",
|
"is_seller",
|
||||||
"maker_nick",
|
"maker_nick",
|
||||||
"maker_hash_id",
|
"maker_hash_id",
|
||||||
|
"maker_nostr_pubkey",
|
||||||
"taker_nick",
|
"taker_nick",
|
||||||
"taker_hash_id",
|
"taker_hash_id",
|
||||||
|
"taker_nostr_pubkey",
|
||||||
"status_message",
|
"status_message",
|
||||||
"is_fiat_sent",
|
"is_fiat_sent",
|
||||||
"is_disputed",
|
"is_disputed",
|
||||||
|
|||||||
@ -267,6 +267,7 @@ class OrderView(viewsets.ViewSet):
|
|||||||
|
|
||||||
data["maker_nick"] = str(order.maker)
|
data["maker_nick"] = str(order.maker)
|
||||||
data["maker_hash_id"] = str(order.maker.robot.hash_id)
|
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
|
# Add activity status of participants based on last_seen
|
||||||
data["maker_status"] = Logics.user_activity_status(order.maker.last_login)
|
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)
|
data["taker_nick"] = str(order.taker)
|
||||||
if order.taker:
|
if order.taker:
|
||||||
data["taker_hash_id"] = str(order.taker.robot.hash_id)
|
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["status_message"] = Order.Status(order.status).label
|
||||||
data["is_fiat_sent"] = order.is_fiat_sent
|
data["is_fiat_sent"] = order.is_fiat_sent
|
||||||
data["latitude"] = order.latitude
|
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",
|
"latlon-geohash": "^2.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"light-bolt11-decoder": "^3.1.1",
|
"light-bolt11-decoder": "^3.1.1",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.12.0",
|
||||||
"npm": "^11.0.0",
|
"npm": "^11.0.0",
|
||||||
"openpgp": "^5.11.0",
|
"openpgp": "^5.11.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -12234,9 +12234,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nostr-tools": {
|
"node_modules/nostr-tools": {
|
||||||
"version": "2.10.4",
|
"version": "2.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz",
|
||||||
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
"integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/ciphers": "^0.5.1",
|
"@noble/ciphers": "^0.5.1",
|
||||||
|
|||||||
@ -75,7 +75,7 @@
|
|||||||
"latlon-geohash": "^2.0.0",
|
"latlon-geohash": "^2.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"light-bolt11-decoder": "^3.1.1",
|
"light-bolt11-decoder": "^3.1.1",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.12.0",
|
||||||
"npm": "^11.0.0",
|
"npm": "^11.0.0",
|
||||||
"openpgp": "^5.11.0",
|
"openpgp": "^5.11.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@ -38,6 +38,7 @@ interface Props {
|
|||||||
setMessages: (messages: EncryptedChatMessage[]) => void;
|
setMessages: (messages: EncryptedChatMessage[]) => void;
|
||||||
turtleMode: boolean;
|
turtleMode: boolean;
|
||||||
setTurtleMode: (state: boolean) => void;
|
setTurtleMode: (state: boolean) => void;
|
||||||
|
onSendMessage: (content: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EncryptedSocketChat: React.FC<Props> = ({
|
const EncryptedSocketChat: React.FC<Props> = ({
|
||||||
@ -51,6 +52,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
|||||||
setMessages,
|
setMessages,
|
||||||
turtleMode,
|
turtleMode,
|
||||||
setTurtleMode,
|
setTurtleMode,
|
||||||
|
onSendMessage,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -248,6 +250,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
// If input string contains '#' send unencrypted and unlogged message
|
// If input string contains '#' send unencrypted and unlogged message
|
||||||
else if (connection != null && value.substring(0, 1) === '#') {
|
else if (connection != null && value.substring(0, 1) === '#') {
|
||||||
|
onSendMessage(value);
|
||||||
connection.send(
|
connection.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'message',
|
type: 'message',
|
||||||
@ -263,6 +266,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
|||||||
setValue('');
|
setValue('');
|
||||||
setWaitingEcho(true);
|
setWaitingEcho(true);
|
||||||
setLastSent(value);
|
setLastSent(value);
|
||||||
|
onSendMessage(value);
|
||||||
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, slot.token)
|
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, slot.token)
|
||||||
.then((encryptedMessage) => {
|
.then((encryptedMessage) => {
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
|
|||||||
@ -32,6 +32,7 @@ interface Props {
|
|||||||
setMessages: (messages: EncryptedChatMessage[]) => void;
|
setMessages: (messages: EncryptedChatMessage[]) => void;
|
||||||
turtleMode: boolean;
|
turtleMode: boolean;
|
||||||
setTurtleMode: (state: boolean) => void;
|
setTurtleMode: (state: boolean) => void;
|
||||||
|
onSendMessage: (content: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioPath =
|
const audioPath =
|
||||||
@ -50,6 +51,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
|||||||
setMessages,
|
setMessages,
|
||||||
setTurtleMode,
|
setTurtleMode,
|
||||||
turtleMode,
|
turtleMode,
|
||||||
|
onSendMessage,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -198,6 +200,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
|||||||
const { url, basePath } = federation
|
const { url, basePath } = federation
|
||||||
.getCoordinator(garage.getSlot()?.activeOrder?.shortAlias ?? '')
|
.getCoordinator(garage.getSlot()?.activeOrder?.shortAlias ?? '')
|
||||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||||
|
onSendMessage(value);
|
||||||
apiClient
|
apiClient
|
||||||
.post(
|
.post(
|
||||||
url + basePath,
|
url + basePath,
|
||||||
@ -226,6 +229,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
|||||||
else if (value !== '' && Boolean(robot?.pubKey)) {
|
else if (value !== '' && Boolean(robot?.pubKey)) {
|
||||||
setWaitingEcho(true);
|
setWaitingEcho(true);
|
||||||
setLastSent(value);
|
setLastSent(value);
|
||||||
|
onSendMessage(value);
|
||||||
encryptMessage(value, robot?.pubKey, peerPubKey ?? '', robot?.encPrivKey, slot?.token)
|
encryptMessage(value, robot?.pubKey, peerPubKey ?? '', robot?.encPrivKey, slot?.token)
|
||||||
.then((encryptedMessage) => {
|
.then((encryptedMessage) => {
|
||||||
const { url, basePath } = federation
|
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 { type Order, type Robot } from '../../../models';
|
||||||
import EncryptedSocketChat from './EncryptedSocketChat';
|
import EncryptedSocketChat from './EncryptedSocketChat';
|
||||||
import EncryptedTurtleChat from './EncryptedTurtleChat';
|
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 {
|
interface Props {
|
||||||
order: Order;
|
order: Order;
|
||||||
@ -36,11 +42,35 @@ const EncryptedChat: React.FC<Props> = ({
|
|||||||
status,
|
status,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [turtleMode, setTurtleMode] = useState<boolean>(false);
|
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 ? (
|
return turtleMode ? (
|
||||||
<EncryptedTurtleChat
|
<EncryptedTurtleChat
|
||||||
messages={messages}
|
messages={messages}
|
||||||
setMessages={setMessages}
|
setMessages={setMessages}
|
||||||
|
onSendMessage={onSendMessage}
|
||||||
order={order}
|
order={order}
|
||||||
takerNick={order.taker_nick}
|
takerNick={order.taker_nick}
|
||||||
takerHashId={order.taker_hash_id}
|
takerHashId={order.taker_hash_id}
|
||||||
@ -55,6 +85,7 @@ const EncryptedChat: React.FC<Props> = ({
|
|||||||
status={status}
|
status={status}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
setMessages={setMessages}
|
setMessages={setMessages}
|
||||||
|
onSendMessage={onSendMessage}
|
||||||
order={order}
|
order={order}
|
||||||
takerNick={order.taker_nick}
|
takerNick={order.taker_nick}
|
||||||
takerHashId={order.taker_hash_id}
|
takerHashId={order.taker_hash_id}
|
||||||
|
|||||||
@ -303,6 +303,10 @@ export class Coordinator {
|
|||||||
return { url: String(this[network][origin]), basePath: '' };
|
return { url: String(this[network][origin]), basePath: '' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getRelayUrl = (): string => {
|
||||||
|
return `ws://${this.url.replace(/^https?:\/\//, '')}/nostr`;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Coordinator;
|
export default Coordinator;
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export class Federation {
|
|||||||
if (tesnetHost) settings.network = 'testnet';
|
if (tesnetHost) settings.network = 'testnet';
|
||||||
this.connection = null;
|
this.connection = null;
|
||||||
|
|
||||||
this.roboPool = new RoboPool(settings, origin);
|
this.roboPool = new RoboPool(settings, Object.values(this.coordinators));
|
||||||
}
|
}
|
||||||
|
|
||||||
private coordinators: Record<string, Coordinator>;
|
private coordinators: Record<string, Coordinator>;
|
||||||
|
|||||||
@ -93,8 +93,10 @@ class Order {
|
|||||||
is_seller: boolean = false;
|
is_seller: boolean = false;
|
||||||
maker_nick: string = '';
|
maker_nick: string = '';
|
||||||
maker_hash_id: string = '';
|
maker_hash_id: string = '';
|
||||||
|
maker_nostr_pubkey: string = '';
|
||||||
taker_nick: string = '';
|
taker_nick: string = '';
|
||||||
taker_hash_id: string = '';
|
taker_hash_id: string = '';
|
||||||
|
taker_nostr_pubkey: string = '';
|
||||||
status_message: string = '';
|
status_message: string = '';
|
||||||
is_fiat_sent: boolean = false;
|
is_fiat_sent: boolean = false;
|
||||||
is_disputed: boolean = false;
|
is_disputed: boolean = false;
|
||||||
|
|||||||
@ -75,6 +75,7 @@ class Robot {
|
|||||||
last_login: data.last_login,
|
last_login: data.last_login,
|
||||||
pubKey: data.public_key,
|
pubKey: data.public_key,
|
||||||
encPrivKey: data.encrypted_private_key,
|
encPrivKey: data.encrypted_private_key,
|
||||||
|
nostrPubKey: data.nostr_pubkey,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { type Event } from 'nostr-tools';
|
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 defaultFederation from '../../../static/federation.json';
|
||||||
import { websocketClient, type WebsocketConnection, WebsocketState } from '../Websocket';
|
import { websocketClient, type WebsocketConnection, WebsocketState } from '../Websocket';
|
||||||
import thirdParties from '../../../static/thirdparties.json';
|
import thirdParties from '../../../static/thirdparties.json';
|
||||||
@ -10,19 +10,12 @@ interface RoboPoolEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RoboPool {
|
class RoboPool {
|
||||||
constructor(settings: Settings, origin: string) {
|
constructor(settings: Settings, coordinators: Coordinator[]) {
|
||||||
this.network = settings.network ?? 'mainnet';
|
this.network = settings.network ?? 'mainnet';
|
||||||
|
|
||||||
this.relays = [];
|
this.relays = [];
|
||||||
const federationRelays = Object.values(defaultFederation)
|
const federationRelays = coordinators.map((coord) => coord.getRelayUrl());
|
||||||
.map((coord) => {
|
|
||||||
const url: string = coord[this.network]?.[settings.selfhostedClient ? 'onion' : origin];
|
|
||||||
|
|
||||||
if (!url) return undefined;
|
|
||||||
|
|
||||||
return `ws://${url.replace(/^https?:\/\//, '')}/nostr`;
|
|
||||||
})
|
|
||||||
.filter((item) => item !== undefined);
|
|
||||||
if (settings.host) {
|
if (settings.host) {
|
||||||
const hostNostr = `ws://${settings.host.replace(/^https?:\/\//, '')}/nostr`;
|
const hostNostr = `ws://${settings.host.replace(/^https?:\/\//, '')}/nostr`;
|
||||||
if (federationRelays.includes(hostNostr)) {
|
if (federationRelays.includes(hostNostr)) {
|
||||||
@ -44,8 +37,8 @@ class RoboPool {
|
|||||||
public webSockets: Record<string, WebsocketConnection | null> = {};
|
public webSockets: Record<string, WebsocketConnection | null> = {};
|
||||||
private readonly messageHandlers: Array<(url: string, event: MessageEvent) => void> = [];
|
private readonly messageHandlers: Array<(url: string, event: MessageEvent) => void> = [];
|
||||||
|
|
||||||
connect = (): void => {
|
connect = (relays: string[] = this.relays): void => {
|
||||||
this.relays.forEach((url: string) => {
|
relays.forEach((url: string) => {
|
||||||
if (Object.keys(this.webSockets).find((wUrl) => wUrl === url)) return;
|
if (Object.keys(this.webSockets).find((wUrl) => wUrl === url)) return;
|
||||||
|
|
||||||
this.webSockets[url] = null;
|
this.webSockets[url] = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user