mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-06-22 06:42:47 +02:00
add setting to always send authorization on blossom uploads
This commit is contained in:
parent
37bada4390
commit
37fe7a05f4
@ -60,7 +60,7 @@
|
||||
"applesauce-signers": "^1.2.0",
|
||||
"applesauce-wallet": "^1.0.0",
|
||||
"bech32": "^2.0.0",
|
||||
"blossom-client-sdk": "^4.0.0",
|
||||
"blossom-client-sdk": "next",
|
||||
"blurhash": "^2.0.5",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"chart.js": "^4.4.9",
|
||||
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -137,8 +137,8 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
blossom-client-sdk:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250516125901
|
||||
blurhash:
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
@ -2325,8 +2325,8 @@ packages:
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
blossom-client-sdk@4.0.0:
|
||||
resolution: {integrity: sha512-1tjFVwSRPo2fq1HskHzfneWSBa4JmfyveFSiyPas/weyLpMKD7lgN8/F2AxdB9v7TaEFO/xhPwE/KTBGcyofWw==}
|
||||
blossom-client-sdk@0.0.0-next-20250516125901:
|
||||
resolution: {integrity: sha512-yd44m5TeET5G1TBCrdphJK5at0xcpXTi8cIGZd538DPKhJcIxC3Te2Jbi2wUP9y68bSkti5+6XhFj3Xc6tVUvQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
blurhash@2.0.5:
|
||||
@ -8193,7 +8193,7 @@ snapshots:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
blossom-client-sdk@4.0.0:
|
||||
blossom-client-sdk@0.0.0-next-20250516125901:
|
||||
dependencies:
|
||||
'@cashu/cashu-ts': 2.5.2
|
||||
'@noble/hashes': 1.8.0
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { BlobDescriptor, createUploadAuth, ServerType, Signer } from "blossom-client-sdk";
|
||||
import { multiServerUpload, MultiServerUploadOptions } from "blossom-client-sdk/actions/multi-server";
|
||||
import localSettings from "../../services/local-settings";
|
||||
|
||||
export async function simpleMultiServerUpload<T extends ServerType = ServerType>(
|
||||
servers: T[],
|
||||
@ -7,10 +8,14 @@ export async function simpleMultiServerUpload<T extends ServerType = ServerType>
|
||||
signer: Signer,
|
||||
opts?: MultiServerUploadOptions<T, File>,
|
||||
): Promise<BlobDescriptor> {
|
||||
const isMedia = file.type.startsWith("image/") || file.type.startsWith("video/");
|
||||
const auth = localSettings.alwaysAuthUpload.value || undefined;
|
||||
|
||||
const results = await multiServerUpload(servers, file, {
|
||||
isMedia: file.type.startsWith("image/") || file.type.startsWith("video/"),
|
||||
isMedia,
|
||||
mediaUploadBehavior: "any",
|
||||
mediaUploadFallback: true,
|
||||
auth,
|
||||
...opts,
|
||||
onAuth: (_server, blob, type) => createUploadAuth(signer, blob, { type }),
|
||||
});
|
||||
|
@ -52,6 +52,7 @@ const enableKeyboardShortcuts = new BooleanLocalStorageEntry("enable-keyboard-sh
|
||||
|
||||
// privacy
|
||||
const debugApi = new BooleanLocalStorageEntry("debug-api", false);
|
||||
const alwaysAuthUpload = new BooleanLocalStorageEntry("always-auth-upload", true);
|
||||
|
||||
// relay authentication
|
||||
const defaultAuthenticationMode = new LocalStorageEntry<RelayAuthMode>("default-authentication-mode", "ask");
|
||||
@ -90,6 +91,7 @@ const localSettings = {
|
||||
ntfyTopic,
|
||||
ntfyServer,
|
||||
cacheRelayURL,
|
||||
alwaysAuthUpload,
|
||||
};
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
|
@ -1,15 +1,31 @@
|
||||
import { getProfilePointersFromList } from "applesauce-core/helpers";
|
||||
import { getProfilePointersFromList, mergeRelaySets } from "applesauce-core/helpers";
|
||||
import { SerializedSocialGraph, SocialGraph } from "nostr-social-graph";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { Filter, NostrEvent, kinds as nostrKinds } from "nostr-tools";
|
||||
import { ProfilePointer } from "nostr-tools/nip19";
|
||||
import { distinct, filter, mergeMap, share, startWith, Subject, take, tap } from "rxjs";
|
||||
import {
|
||||
bufferTime,
|
||||
combineLatest,
|
||||
distinct,
|
||||
filter,
|
||||
ignoreElements,
|
||||
merge,
|
||||
mergeMap,
|
||||
Observable,
|
||||
of,
|
||||
scan,
|
||||
startWith,
|
||||
Subject,
|
||||
Subscription,
|
||||
take,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { SOCIAL_GRAPH_FALLBACK_PUBKEY } from "../const";
|
||||
import { logger } from "../helpers/debug";
|
||||
import accounts from "./accounts";
|
||||
import idbKeyValueStore from "./database/kv";
|
||||
import { eventStore } from "./event-store";
|
||||
import replaceableEventLoader from "./replaceable-loader";
|
||||
import { logger } from "../helpers/debug";
|
||||
|
||||
const log = logger.extend("SocialGraph");
|
||||
const cacheKey = "social-graph";
|
||||
@ -28,38 +44,161 @@ export async function saveSocialGraph() {
|
||||
await idbKeyValueStore.setItem(cacheKey, socialGraph.serialize());
|
||||
}
|
||||
|
||||
export function crawlFollowGraph(root: string | ProfilePointer, maxDistance: number = 2) {
|
||||
log(`Started crawling follow graph`);
|
||||
const loadUser = (user: ProfilePointer) => {
|
||||
const pubkey = typeof user === "string" ? user : user.pubkey;
|
||||
/** Make a request to load a list of users in the social graph and return an observable that completes with finished loading */
|
||||
type GraphLoaderRequest = (pointers: ProfilePointer) => Observable<NostrEvent>;
|
||||
|
||||
replaceableEventLoader.next({ kind: kinds.Contacts, ...user });
|
||||
return eventStore.filters({ kinds: [kinds.Contacts], authors: [pubkey] });
|
||||
export function createBatchUserLoader(
|
||||
request: (relays: string[], filters: Filter[]) => Observable<NostrEvent>,
|
||||
kinds: number[] = [nostrKinds.Contacts, nostrKinds.Metadata, nostrKinds.Mutelist],
|
||||
): GraphLoaderRequest {
|
||||
const input = new Subject<ProfilePointer>();
|
||||
|
||||
const output = input.pipe(
|
||||
// skip duplicates
|
||||
distinct((user) => user.pubkey),
|
||||
// buffer for 1 second
|
||||
bufferTime(1_000),
|
||||
// merge users into a single request
|
||||
mergeMap((users) => {
|
||||
const filter: Filter = { kinds, authors: users.map((u) => u.pubkey) };
|
||||
const relays = mergeRelaySets(...users.map((u) => u.relays));
|
||||
return request(relays, [filter]);
|
||||
}),
|
||||
);
|
||||
|
||||
return (user) => {
|
||||
input.next(user);
|
||||
return output;
|
||||
};
|
||||
}
|
||||
|
||||
const queue = new Subject<[ProfilePointer, number]>();
|
||||
export function graphLoader(root: string | ProfilePointer, maxDistance: number = 2, request: GraphLoaderRequest) {
|
||||
return new Observable<{ total: number; loaded: number }>((observer) => {
|
||||
// Keep track of all loaded and discovered users
|
||||
const loaded = new Set<string>();
|
||||
const found = new Set<string>();
|
||||
|
||||
return queue.pipe(
|
||||
// Queue of users to load
|
||||
const queue: [ProfilePointer, number][] = [];
|
||||
|
||||
// Add root to queue
|
||||
queue.push([typeof root === "string" ? { pubkey: root } : root, 0]);
|
||||
|
||||
// Keep track of all subscriptions
|
||||
const subscriptions: Subscription[] = [];
|
||||
|
||||
// Start loading users
|
||||
while (queue.length > 0) {
|
||||
const [pointer, distance] = queue.shift()!;
|
||||
if (distance > maxDistance) continue;
|
||||
|
||||
// Don't load the same user twice
|
||||
if (loaded.has(pointer.pubkey)) continue;
|
||||
loaded.add(pointer.pubkey);
|
||||
found.add(pointer.pubkey);
|
||||
|
||||
// Update progress
|
||||
observer.next({ total: found.size, loaded: loaded.size });
|
||||
|
||||
// Load the user
|
||||
const sub = request(pointer)
|
||||
.pipe(
|
||||
filter((e) => e.kind === nostrKinds.Contacts && e.pubkey === pointer.pubkey),
|
||||
take(1),
|
||||
)
|
||||
.subscribe((event) => {
|
||||
// Temp: add events to the store
|
||||
eventStore.add(event);
|
||||
|
||||
const contacts = getProfilePointersFromList(event);
|
||||
log(`Loaded contacts for ${pointer.pubkey}`, contacts);
|
||||
|
||||
if (distance < maxDistance) {
|
||||
for (const pointer of contacts) {
|
||||
if (found.has(pointer.pubkey)) continue;
|
||||
found.add(pointer.pubkey);
|
||||
|
||||
// Add the user to the queue to be loaded
|
||||
queue.push([pointer, distance + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress
|
||||
observer.next({ total: found.size, loaded: loaded.size });
|
||||
});
|
||||
|
||||
subscriptions.push(sub);
|
||||
}
|
||||
|
||||
// complete the loader when queue is empty
|
||||
// observer.complete();
|
||||
|
||||
// return cleanup
|
||||
return () => {
|
||||
for (const subscription of subscriptions) subscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function crawlFollowGraph(
|
||||
root: string | ProfilePointer,
|
||||
maxDistance: number = 2,
|
||||
): Observable<{ total: number; loaded: number }> {
|
||||
log(`Started crawling follow graph`);
|
||||
|
||||
const loaded = new Subject<NostrEvent>();
|
||||
const input = new Subject<[ProfilePointer, number]>();
|
||||
|
||||
const queue = input.pipe(
|
||||
// Don't load users who are too far
|
||||
filter(([_user, distance]) => distance <= maxDistance),
|
||||
// only load users once
|
||||
distinct(([user]) => user.pubkey),
|
||||
// Start with the root user
|
||||
startWith([typeof root === "string" ? { pubkey: root } : root, 0] as const),
|
||||
// create new observable to load each user
|
||||
mergeMap(([user, distance]) =>
|
||||
loadUser(user).pipe(
|
||||
take(1),
|
||||
tap((event) => {
|
||||
const contacts = getProfilePointersFromList(event);
|
||||
// Add the user to the queue to be loaded
|
||||
for (const pointer of contacts) queue.next([pointer, distance + 1]);
|
||||
}),
|
||||
),
|
||||
),
|
||||
// only create a single loader
|
||||
share(),
|
||||
);
|
||||
|
||||
// Create observable for progress
|
||||
const progress = combineLatest({
|
||||
total: queue.pipe(scan((acc) => acc + 1, 0)),
|
||||
loaded: loaded.pipe(scan((acc) => acc + 1, 0)),
|
||||
});
|
||||
|
||||
const loader = queue.pipe(
|
||||
// create new observable to load each user
|
||||
mergeMap(([user, distance]) => {
|
||||
log(`Loading contacts for ${user.pubkey}`);
|
||||
replaceableEventLoader.next({ kind: nostrKinds.Contacts, ...user });
|
||||
|
||||
const event = eventStore.getReplaceable(nostrKinds.Contacts, user.pubkey);
|
||||
|
||||
return (
|
||||
event
|
||||
? of(event)
|
||||
: eventStore.inserts.pipe(
|
||||
// Wait for the contacts event to be loaded
|
||||
filter((e) => e.kind === nostrKinds.Contacts && e.pubkey === user.pubkey),
|
||||
// Take the first event
|
||||
take(1),
|
||||
)
|
||||
).pipe(
|
||||
// Add contacts to the queue
|
||||
tap((event) => {
|
||||
loaded.next(event);
|
||||
|
||||
const contacts = getProfilePointersFromList(event);
|
||||
log(`Loaded contacts for ${user.pubkey}`, contacts);
|
||||
|
||||
// Add the user to the queue to be loaded
|
||||
for (const pointer of contacts) input.next([pointer, distance + 1]);
|
||||
}),
|
||||
// Ignore loaded events
|
||||
ignoreElements(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return merge(loader, progress);
|
||||
}
|
||||
|
||||
/** Exports the social graph to a file */
|
||||
|
@ -30,6 +30,7 @@ export default function PostSettings() {
|
||||
watch("mediaUploadService");
|
||||
|
||||
const addClientTag = useObservable(localSettings.addClientTag);
|
||||
const alwaysAuthUpload = useObservable(localSettings.alwaysAuthUpload);
|
||||
|
||||
return (
|
||||
<SimpleView
|
||||
@ -120,11 +121,25 @@ export default function PostSettings() {
|
||||
<FormControl>
|
||||
<Flex alignItems="center">
|
||||
<FormLabel htmlFor="mirrorBlobsOnShare" mb="0">
|
||||
Always mirror media
|
||||
Mirror media when sharing
|
||||
</FormLabel>
|
||||
<Switch id="mirrorBlobsOnShare" {...register("mirrorBlobsOnShare")} />
|
||||
</Flex>
|
||||
<FormHelperText>Copy all media to your personal blossom servers when sharing notes</FormHelperText>
|
||||
<FormHelperText>Mirror all media to your personal blossom servers when sharing notes</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<Flex alignItems="center">
|
||||
<FormLabel htmlFor="alwaysAuthUpload" mb="0">
|
||||
Always authenticate media uploads
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="alwaysAuthUpload"
|
||||
isChecked={alwaysAuthUpload}
|
||||
onChange={() => localSettings.alwaysAuthUpload.next(!localSettings.alwaysAuthUpload.value)}
|
||||
/>
|
||||
</Flex>
|
||||
<FormHelperText>Always authenticate with media servers when uploading media</FormHelperText>
|
||||
</FormControl>
|
||||
</SimpleView>
|
||||
);
|
||||
|
@ -10,20 +10,29 @@ import {
|
||||
NumberInputField,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { useActiveAccount, useObservable } from "applesauce-react/hooks";
|
||||
import { SocialGraph } from "nostr-social-graph";
|
||||
import { useActiveAccount } from "applesauce-react/hooks";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { tap, throttleTime } from "rxjs";
|
||||
|
||||
import SimpleView from "../../../components/layout/presets/simple-view";
|
||||
import UserAvatar from "../../../components/user/user-avatar";
|
||||
import { UserAvatarLink } from "../../../components/user/user-avatar-link";
|
||||
import UserDnsIdentity from "../../../components/user/user-dns-identity";
|
||||
import UserName from "../../../components/user/user-name";
|
||||
import { crawlFollowGraph, exportGraph, importGraph, socialGraph } from "../../../services/social-graph";
|
||||
import { useAppTitle } from "../../../hooks/use-app-title";
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
import pool from "../../../services/pool";
|
||||
import {
|
||||
createBatchUserLoader,
|
||||
exportGraph,
|
||||
graphLoader,
|
||||
importGraph,
|
||||
socialGraph,
|
||||
} from "../../../services/social-graph";
|
||||
import { useUnmount } from "react-use";
|
||||
import { Subscription } from "rxjs";
|
||||
|
||||
export default function SocialGraphSettings() {
|
||||
useAppTitle("Social Graph");
|
||||
const [followDistances, setFollowDistances] = useState<{ distance: number; count: number; randomUsers: string[] }[]>(
|
||||
[],
|
||||
);
|
||||
@ -50,28 +59,21 @@ export default function SocialGraphSettings() {
|
||||
setFollowDistances(generateFollowDistances());
|
||||
}, [socialGraph, generateFollowDistances]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState<Subscription>();
|
||||
const handleLoadGraph = () => {
|
||||
if (account?.pubkey && socialGraph) {
|
||||
setLoading(true);
|
||||
|
||||
// Create and start the loader
|
||||
crawlFollowGraph(account.pubkey, distance)
|
||||
.pipe(
|
||||
// send event to graph
|
||||
tap((e) => socialGraph.handleEvent(e)),
|
||||
// Update the follow distance every second while loading
|
||||
throttleTime(1000),
|
||||
tap(() => {
|
||||
socialGraph.recalculateFollowDistances();
|
||||
setFollowDistances(generateFollowDistances());
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => setLoading(false),
|
||||
});
|
||||
setLoading(
|
||||
graphLoader(account.pubkey, distance, createBatchUserLoader(pool.request.bind(pool))).subscribe({
|
||||
next: (progress) => {
|
||||
console.log(progress);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
useUnmount(() => {
|
||||
loading?.unsubscribe();
|
||||
});
|
||||
|
||||
const displayMaxPeople = useBreakpointValue({ base: 4, lg: 5, xl: 10 }) || 4;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user