From 99defc6d79d63e8333ebea39013f2a847d362491 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 3 Sep 2025 09:02:54 -0700 Subject: [PATCH] Allow users to opt-in to spaces that strip signatures --- src/app/components/Chat.svelte | 4 +- src/app/components/Content.svelte | 6 +- src/app/components/InfoSignatures.svelte | 32 +++++++ src/app/components/SpaceExit.svelte | 3 +- src/app/components/SpaceTrustRelay.svelte | 86 +++++++++++++++++++ src/app/core/commands.ts | 35 +++++++- src/app/core/state.ts | 43 ++++++---- src/lib/components/ModalFooter.svelte | 4 +- src/routes/+layout.svelte | 79 +++++++++++++++-- src/routes/settings/+page.svelte | 29 ++----- src/routes/spaces/[relay]/+layout.svelte | 10 ++- src/routes/spaces/[relay]/[room]/+page.svelte | 4 +- src/routes/spaces/[relay]/chat/+page.svelte | 4 +- 13 files changed, 281 insertions(+), 58 deletions(-) create mode 100644 src/app/components/InfoSignatures.svelte create mode 100644 src/app/components/SpaceTrustRelay.svelte diff --git a/src/app/components/Chat.svelte b/src/app/components/Chat.svelte index e5589549..1d079b5d 100644 --- a/src/app/components/Chat.svelte +++ b/src/app/components/Chat.svelte @@ -49,7 +49,7 @@ import ThunkToast from "@app/components/ThunkToast.svelte" import { INDEXER_RELAYS, - userSettingValues, + userSettingsValues, deriveChat, splitChatId, PLATFORM_NAME, @@ -130,7 +130,7 @@ const template = templates[i] thunks.push( - await sendWrapped({pubkeys, template, delay: $userSettingValues.send_delay + ms(i)}), + await sendWrapped({pubkeys, template, delay: $userSettingsValues.send_delay + ms(i)}), ) } diff --git a/src/app/components/Content.svelte b/src/app/components/Content.svelte index 53bc125e..8788562e 100644 --- a/src/app/components/Content.svelte +++ b/src/app/components/Content.svelte @@ -31,7 +31,7 @@ import ContentQuote from "@app/components/ContentQuote.svelte" import ContentTopic from "@app/components/ContentTopic.svelte" import ContentMention from "@app/components/ContentMention.svelte" - import {entityLink, userSettingValues} from "@app/core/state" + import {entityLink, userSettingsValues} from "@app/core/state" interface Props { event: any @@ -68,7 +68,7 @@ if (!parsed || hideMediaAtDepth <= depth) return false - if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) { + if (isLink(parsed) && $userSettingsValues.show_media && isStartOrEnd(i)) { return true } @@ -101,7 +101,7 @@ } let warning = $state( - $userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1], + $userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1], ) const shortContent = $derived( diff --git a/src/app/components/InfoSignatures.svelte b/src/app/components/InfoSignatures.svelte new file mode 100644 index 00000000..12c48964 --- /dev/null +++ b/src/app/components/InfoSignatures.svelte @@ -0,0 +1,32 @@ + + +
+ + {#snippet title()} +
What are digital signatures?
+ {/snippet} +
+

+ Most online services ask their users to trust them that they're being honest, and they usually + are. However, traditional social media platforms have the ability to create forged content that can appear to be genuinely authored, but which are actually + counterfeit. +

+

+ On Nostr, all your content is authenticated + using digital signatures, which cryptographically tie a particular person to a + given post or message. +

+

+ The result is that you don't normally have to trust service providers not to tamper with the + information flowing through the network — instead, your client software can prove that a given + piece of data is authentic. +

+ +
diff --git a/src/app/components/SpaceExit.svelte b/src/app/components/SpaceExit.svelte index c7f29ac4..78839380 100644 --- a/src/app/components/SpaceExit.svelte +++ b/src/app/components/SpaceExit.svelte @@ -7,7 +7,7 @@ import Icon from "@lib/components/Icon.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte" - import {removeSpaceMembership} from "@app/core/commands" + import {removeSpaceMembership, removeTrustedRelay} from "@app/core/commands" const {url} = $props() @@ -18,6 +18,7 @@ try { await removeSpaceMembership(url) + await removeTrustedRelay(url) } finally { loading = false } diff --git a/src/app/components/SpaceTrustRelay.svelte b/src/app/components/SpaceTrustRelay.svelte new file mode 100644 index 00000000..efac21f4 --- /dev/null +++ b/src/app/components/SpaceTrustRelay.svelte @@ -0,0 +1,86 @@ + + +
+ + {#snippet title()} + Do you trust this space? + {/snippet} + {#snippet info()} +
+ Only join {displayRelayUrl(url)} if you trust the adminstrator +
+ {/snippet} +
+
+

+ This space has opted not to publish , which means that they have the ability to forge messages from other users. +

+

+ If you trust this space's admin, you can continue. Otherwise, it may be safer not to join this + space. +

+
+
+ + +
+
diff --git a/src/app/core/commands.ts b/src/app/core/commands.ts index 8ca54082..6ea4b0f5 100644 --- a/src/app/core/commands.ts +++ b/src/app/core/commands.ts @@ -1,7 +1,17 @@ import {nwc} from "@getalby/sdk" import * as nip19 from "nostr-tools/nip19" import {get} from "svelte/store" -import {randomId, flatten, poll, uniq, equals, TIMEZONE, LOCALE} from "@welshman/lib" +import { + randomId, + append, + remove, + flatten, + poll, + uniq, + equals, + TIMEZONE, + LOCALE, +} from "@welshman/lib" import type {Feed} from "@welshman/feeds" import type {TrustedEvent, EventContent} from "@welshman/util" import { @@ -19,6 +29,7 @@ import { ALERT_WEB, ALERT_IOS, ALERT_ANDROID, + APP_DATA, isSignedEvent, makeEvent, displayProfile, @@ -56,13 +67,16 @@ import { tagEventForQuote, getThunkError, } from "@welshman/app" +import type {SettingsValues} from "@app/core/state" import { + SETTINGS, PROTECTED, userMembership, INDEXER_RELAYS, NOTIFIER_PUBKEY, NOTIFIER_RELAY, userRoomsByUrl, + userSettingsValues, } from "@app/core/state" // Utils @@ -461,6 +475,25 @@ export const makeAlert = async (params: AlertParams) => { export const publishAlert = async (params: AlertParams) => publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]}) +// Settings + +export const makeSettings = async (params: Partial) => { + const json = JSON.stringify({...userSettingsValues.get(), ...params}) + const content = await signer.get().nip44.encrypt(pubkey.get()!, json) + const tags = [["d", SETTINGS]] + + return makeEvent(APP_DATA, {content, tags}) +} + +export const publishSettings = async (params: Partial) => + publishThunk({event: await makeSettings(params), relays: Router.get().FromUser().getUrls()}) + +export const addTrustedRelay = async (url: string) => + publishSettings({trusted_relays: append(url, userSettingsValues.get().trusted_relays)}) + +export const removeTrustedRelay = async (url: string) => + publishSettings({trusted_relays: remove(url, userSettingsValues.get().trusted_relays)}) + // Lightning export const getWebLn = () => (window as any).webln diff --git a/src/app/core/state.ts b/src/app/core/state.ts index cb9b9bee..40bc7a30 100644 --- a/src/app/core/state.ts +++ b/src/app/core/state.ts @@ -1,6 +1,6 @@ import twColors from "tailwindcss/colors" import {Capacitor} from "@capacitor/core" -import {get, derived} from "svelte/store" +import {get, derived, writable} from "svelte/store" import * as nip19 from "nostr-tools/nip19" import { on, @@ -23,7 +23,7 @@ import { always, } from "@welshman/lib" import type {Socket} from "@welshman/net" -import {Pool, load, AuthStateEvent, SocketEvent} from "@welshman/net" +import {Pool, load, AuthStateEvent, SocketEvent, netContext} from "@welshman/net" import { collection, custom, @@ -56,6 +56,7 @@ import { ALERT_IOS, ALERT_ANDROID, ALERT_STATUS, + APP_DATA, getGroupTags, getRelayTagValues, getPubkeyTagValues, @@ -68,6 +69,7 @@ import { getTag, getTagValue, getTagValues, + verifyEvent, } from "@welshman/util" import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util" import {Nip59, decrypt} from "@welshman/signer" @@ -304,6 +306,9 @@ appContext.dufflepudUrl = DUFFLEPUD_URL routerContext.getIndexerRelays = always(INDEXER_RELAYS) +netContext.isEventValid = (event: TrustedEvent, url: string) => + getSetting("trusted_relays").includes(url) || verifyEvent(event) + // Settings export const canDecrypt = synced({ @@ -312,23 +317,27 @@ export const canDecrypt = synced({ storage: localStorageProvider, }) -export const SETTINGS = 38489 +export const SETTINGS = "flotilla/settings" + +export type SettingsValues = { + show_media: boolean + hide_sensitive: boolean + trusted_relays: string[] + report_usage: boolean + report_errors: boolean + send_delay: number + font_size: number +} export type Settings = { event: TrustedEvent - values: { - show_media: boolean - hide_sensitive: boolean - report_usage: boolean - report_errors: boolean - send_delay: number - font_size: number - } + values: SettingsValues } export const defaultSettings = { show_media: true, hide_sensitive: true, + trusted_relays: [], report_usage: true, report_errors: true, send_delay: 3000, @@ -336,7 +345,7 @@ export const defaultSettings = { } export const settings = deriveEventsMapped(repository, { - filters: [{kinds: [SETTINGS]}], + filters: [{kinds: [APP_DATA], "#d": [SETTINGS]}], itemToEvent: item => item.event, eventToItem: async (event: TrustedEvent) => ({ event, @@ -352,9 +361,13 @@ export const { name: "settings", store: settings, getKey: settings => settings.event.pubkey, - load: makeOutboxLoader(SETTINGS), + load: makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}), }) +// Relays sending events with empty signatures that the user has to choose to trust + +export const relaysPendingTrust = writable([]) + // Alerts export type Alert = { @@ -610,11 +623,11 @@ export const userSettings = withGetter( }), ) -export const userSettingValues = withGetter( +export const userSettingsValues = withGetter( derived(userSettings, $s => $s?.values || defaultSettings), ) -export const getSetting = (key: keyof Settings["values"]) => userSettingValues.get()[key] as T +export const getSetting = (key: keyof Settings["values"]) => userSettingsValues.get()[key] as T export const userMembership = withGetter( derived([pubkey, membershipsByPubkey], ([$pubkey, $membershipsByPubkey]) => { diff --git a/src/lib/components/ModalFooter.svelte b/src/lib/components/ModalFooter.svelte index 92860932..a2852f3e 100644 --- a/src/lib/components/ModalFooter.svelte +++ b/src/lib/components/ModalFooter.svelte @@ -1,6 +1,8 @@ diff --git a/src/routes/spaces/[relay]/+layout.svelte b/src/routes/spaces/[relay]/+layout.svelte index be60fe64..96e4226b 100644 --- a/src/routes/spaces/[relay]/+layout.svelte +++ b/src/routes/spaces/[relay]/+layout.svelte @@ -5,14 +5,16 @@ import {ago, MONTH} from "@welshman/lib" import {ROOM_META, EVENT_TIME, THREAD, COMMENT, MESSAGE} from "@welshman/util" import Page from "@lib/components/Page.svelte" + import Dialog from "@lib/components/Dialog.svelte" import SecondaryNav from "@lib/components/SecondaryNav.svelte" import MenuSpace from "@app/components/MenuSpace.svelte" import SpaceAuthError from "@app/components/SpaceAuthError.svelte" + import SpaceTrustRelay from "@app/components/SpaceTrustRelay.svelte" import {pushToast} from "@app/util/toast" import {pushModal} from "@app/util/modal" import {setChecked} from "@app/util/notifications" import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/core/commands" - import {decodeRelay, userRoomsByUrl} from "@app/core/state" + import {decodeRelay, userRoomsByUrl, relaysPendingTrust} from "@app/core/state" import {pullConservatively} from "@app/core/requests" import {notifications} from "@app/util/notifications" @@ -82,3 +84,9 @@ {@render children?.()} {/key} + +{#if $relaysPendingTrust.includes(url)} + + + +{/if} diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte index dd36481d..f88c5d0a 100644 --- a/src/routes/spaces/[relay]/[room]/+page.svelte +++ b/src/routes/spaces/[relay]/[room]/+page.svelte @@ -31,7 +31,7 @@ import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte" import { userRoomsByUrl, - userSettingValues, + userSettingsValues, decodeRelay, getEventsForUrl, deriveUserMembershipStatus, @@ -128,7 +128,7 @@ const thunk = publishThunk({ relays: [url], event: makeEvent(MESSAGE, template), - delay: $userSettingValues.send_delay, + delay: $userSettingsValues.send_delay, }) pushToast({ diff --git a/src/routes/spaces/[relay]/chat/+page.svelte b/src/routes/spaces/[relay]/chat/+page.svelte index 1c978ddf..a7473627 100644 --- a/src/routes/spaces/[relay]/chat/+page.svelte +++ b/src/routes/spaces/[relay]/chat/+page.svelte @@ -20,7 +20,7 @@ import ChannelCompose from "@app/components/ChannelCompose.svelte" import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte" import { - userSettingValues, + userSettingsValues, decodeRelay, getEventsForUrl, PROTECTED, @@ -69,7 +69,7 @@ const thunk = publishThunk({ relays: [url], event: makeEvent(MESSAGE, template), - delay: $userSettingValues.send_delay, + delay: $userSettingsValues.send_delay, }) pushToast({