diff --git a/packages/feeds/core.ts b/packages/feeds/core.ts index 6f63b88..27af9cc 100644 --- a/packages/feeds/core.ts +++ b/packages/feeds/core.ts @@ -44,7 +44,7 @@ export type DVMItem = { } export type ListItem = { - addresses: string, + addresses: string[], mappings?: TagFeedMapping[], } diff --git a/packages/net/Publish.ts b/packages/net/Publish.ts index f5b0844..d1d2528 100644 --- a/packages/net/Publish.ts +++ b/packages/net/Publish.ts @@ -1,7 +1,7 @@ import type {Event} from 'nostr-tools' import {Emitter, now, randomId, defer} from '@welshman/lib' import type {Deferred} from '@welshman/lib' -import {asEvent,} from '@welshman/util' +import {asSignedEvent} from '@welshman/util' import {NetworkContext} from './Context' export enum PublishStatus { @@ -43,7 +43,7 @@ export const makePublish = (request: PublishRequest) => { export const publish = (request: PublishRequest) => { const pub = makePublish(request) - const event = asEvent(request.event) + const event = asSignedEvent(request.event) const executor = NetworkContext.getExecutor(request.relays) const abort = (reason: PublishStatus) => { diff --git a/packages/util/Address.ts b/packages/util/Address.ts index d8dd31a..c22ad5d 100644 --- a/packages/util/Address.ts +++ b/packages/util/Address.ts @@ -1,6 +1,6 @@ import type {UnsignedEvent} from 'nostr-tools' import {nip19} from 'nostr-tools' -import {Kind} from './Kinds' +import {GROUP, COMMUNITY} from './Kinds' export type Address = { kind: number, @@ -51,8 +51,8 @@ export const addressFromEvent = (e: UnsignedEvent, relays: string[] = []) => // Utils -export const isGroupAddress = (a: Address) => a.kind === Kind.GroupDefinition +export const isGroupAddress = (a: Address) => a.kind === GROUP -export const isCommunityAddress = (a: Address) => a.kind === Kind.CommunityDefinition +export const isCommunityAddress = (a: Address) => a.kind === COMMUNITY -export const isContextAddress = (a: Address) => [Kind.GroupDefinition, Kind.CommunityDefinition].includes(a.kind) +export const isContextAddress = (a: Address) => [GROUP, COMMUNITY].includes(a.kind) diff --git a/packages/util/Events.ts b/packages/util/Events.ts index 9ef34ba..7bf3b41 100644 --- a/packages/util/Events.ts +++ b/packages/util/Events.ts @@ -1,13 +1,38 @@ -import type {Event, EventTemplate, UnsignedEvent} from 'nostr-tools' -export type {Event, EventTemplate, UnsignedEvent} from 'nostr-tools' +import {verifiedSymbol} from 'nostr-tools' import {verifyEvent, getEventHash} from 'nostr-tools' -import {cached, now} from '@welshman/lib' +import {cached, pick, now} from '@welshman/lib' import {Tags} from './Tags' import {addressFromEvent, encodeAddress} from './Address' import {isEphemeralKind, isReplaceableKind, isPlainReplaceableKind, isParameterizedReplaceableKind} from './Kinds' -export type Rumor = Pick & { - wrap?: Event +export type EventTemplate = { + kind: number + tags: string[][] + content: string + created_at: number +} + +export type OwnedEvent = EventTemplate & { + pubkey: string +} + +export type HashedEvent = OwnedEvent & { + id: string +} + +export type SignedEvent = HashedEvent & { + sig: string + [verifiedSymbol]?: boolean +} + +export type UnwrappedEvent = HashedEvent & { + wrap: SignedEvent +} + +export type TrustedEvent = HashedEvent & { + sig?: string + wrap?: SignedEvent + [verifiedSymbol]?: boolean } export type CreateEventOpts = { @@ -19,28 +44,52 @@ export type CreateEventOpts = { export const createEvent = (kind: number, {content = "", tags = [], created_at = now()}: CreateEventOpts) => ({kind, content, tags, created_at}) -export const asEventTemplate = ({kind, tags, content, created_at}: EventTemplate): EventTemplate => - ({kind, tags, content, created_at}) +export const isEventTemplate = (e: EventTemplate): e is EventTemplate => + Boolean(e.kind && e.tags && e.content && e.created_at) -export const asUnsignedEvent = ({kind, tags, content, created_at, pubkey}: UnsignedEvent): UnsignedEvent => - ({kind, tags, content, created_at, pubkey}) +export const isOwnedEvent = (e: OwnedEvent): e is OwnedEvent => + Boolean(isEventTemplate(e) && e.pubkey) -export const asRumor = ({kind, tags, content, created_at, pubkey, id}: Rumor): Rumor => - ({kind, tags, content, created_at, pubkey, id}) +export const isHashedEvent = (e: HashedEvent): e is HashedEvent => + Boolean(isOwnedEvent(e) && e.id) -export const asEvent = ({kind, tags, content, created_at, pubkey, id, sig}: Event): Event => - ({kind, tags, content, created_at, pubkey, id, sig}) +export const isSignedEvent = (e: TrustedEvent): e is SignedEvent => + Boolean(isHashedEvent(e) && e.sig) -export const hasValidSignature = cached({ +export const isUnwrappedEvent = (e: TrustedEvent): e is UnwrappedEvent => + Boolean(isHashedEvent(e) && e.wrap) + +export const isTrustedEvent = (e: TrustedEvent): e is TrustedEvent => + isSignedEvent(e) || isUnwrappedEvent(e) + +export const asEventTemplate = (e: EventTemplate): EventTemplate => + pick(['kind', 'tags', 'content', 'created_at'], e) + +export const asOwnedEvent = (e: OwnedEvent): OwnedEvent => + pick(['kind', 'tags', 'content', 'created_at', 'pubkey'], e) + +export const asHashedEvent = (e: HashedEvent): HashedEvent => + pick(['kind', 'tags', 'content', 'created_at', 'pubkey', 'id'], e) + +export const asSignedEvent = (e: SignedEvent): SignedEvent => + pick(['kind', 'tags', 'content', 'created_at', 'pubkey', 'id', 'sig'], e) + +export const asUnwrappedEvent = (e: UnwrappedEvent): UnwrappedEvent => + pick(['kind', 'tags', 'content', 'created_at', 'pubkey', 'id', 'wrap'], e) + +export const asTrustedEvent = (e: TrustedEvent): TrustedEvent => + pick(['kind', 'tags', 'content', 'created_at', 'pubkey', 'id', 'sig', 'wrap'], e) + +export const hasValidSignature = cached({ maxSize: 10000, - getKey: ([e]: [Event]) => { + getKey: ([e]: [SignedEvent]) => { try { return [getEventHash(e), e.sig].join(":") } catch (err) { return 'invalid' } }, - getValue: ([e]: [Event]) => { + getValue: ([e]: [SignedEvent]) => { try { return verifyEvent(e) } catch (err) { @@ -49,11 +98,11 @@ export const hasValidSignature = cached({ }, }) -export const getAddress = (e: UnsignedEvent) => encodeAddress(addressFromEvent(e)) +export const getAddress = (e: HashedEvent) => encodeAddress(addressFromEvent(e)) -export const getIdOrAddress = (e: Rumor) => isReplaceable(e) ? getAddress(e) : e.id +export const getIdOrAddress = (e: HashedEvent) => isReplaceable(e) ? getAddress(e) : e.id -export const getIdAndAddress = (e: Rumor) => isReplaceable(e) ? [e.id, getAddress(e)] : [e.id] +export const getIdAndAddress = (e: HashedEvent) => isReplaceable(e) ? [e.id, getAddress(e)] : [e.id] export const isEphemeral = (e: EventTemplate) => isEphemeralKind(e.kind) @@ -63,7 +112,7 @@ export const isPlainReplaceable = (e: EventTemplate) => isPlainReplaceableKind(e export const isParameterizedReplaceable = (e: EventTemplate) => isParameterizedReplaceableKind(e.kind) -export const isChildOf = (child: EventTemplate, parent: Rumor) => { +export const isChildOf = (child: EventTemplate, parent: HashedEvent) => { const {roots, replies} = Tags.fromEvent(child).ancestors() const parentIds = (replies.exists() ? replies : roots).values().valueOf() diff --git a/packages/util/Filters.ts b/packages/util/Filters.ts index e5ef04c..5a5cab7 100644 --- a/packages/util/Filters.ts +++ b/packages/util/Filters.ts @@ -1,7 +1,7 @@ -import type {Event} from 'nostr-tools' +import {Event} from 'nostr-tools' import {matchFilter as nostrToolsMatchFilter} from 'nostr-tools' import {prop, avg, hash, groupBy, randomId, uniq} from '@welshman/lib' -import type {Rumor} from './Events' +import type {HashedEvent, TrustedEvent} from './Events' import {decodeAddress, addressFromEvent, encodeAddress} from './Address' import {isReplaceableKind} from './Kinds' @@ -18,7 +18,7 @@ export type Filter = { [key: `#${string}`]: string[] } -export const matchFilter = (filter: Filter, event: E) => { +export const matchFilter = (filter: Filter, event: E) => { if (!nostrToolsMatchFilter(filter, event as unknown as Event)) { return false } @@ -39,7 +39,7 @@ export const matchFilter = (filter: Filter, event: E) => { return true } -export const matchFilters = (filters: Filter[], event: E) => { +export const matchFilters = (filters: Filter[], event: E) => { for (const filter of filters) { if (matchFilter(filter, event)) { return true @@ -155,7 +155,7 @@ export const getIdFilters = (idsOrAddresses: string[]) => { return filters } -export const getReplyFilters = (events: Rumor[], filter: Filter) => { +export const getReplyFilters = (events: TrustedEvent[], filter: Filter) => { const a = [] const e = [] diff --git a/packages/util/Kinds.ts b/packages/util/Kinds.ts index c906b8d..3eaf388 100644 --- a/packages/util/Kinds.ts +++ b/packages/util/Kinds.ts @@ -7,79 +7,141 @@ export const isParameterizedReplaceableKind = kinds.isParameterizedReplaceableKi export const isReplaceableKind = (kind: number) => isPlainReplaceableKind(kind) || isParameterizedReplaceableKind(kind) -export enum Kind { - Profile = 0, - Note = 1, - Relay = 2, - DM = 4, - Delete = 5, - Repost = 6, - Reaction = 7, - BadgeAward = 8, - GenericRepost = 16, - ChannelCreation = 40, - ChannelMetadata = 41, - ChannelMessage = 42, - ChannelHideMessage = 43, - ChannelMuteUser = 44, - OpenTimestamp = 1040, - GiftWrap = 1059, - FileMetadata = 1063, - LiveChatMessage = 1311, - Remix = 1808, - ProblemTracker = 1971, - Report = 1984, - Label = 1985, - CommunityPostApproval = 4550, - JobRequest = 5999, - JobResult = 6999, - JobFeedback = 7000, - ZapGoal = 9041, - ZapRequest = 9734, - ZapResponse = 9735, - Highlight = 9802, - UserListMutes = 10000, - UserListPins = 10001, - UserListRelays = 10002, - UserListBookmarks = 10003, - UserListCommunities = 10004, - UserListPublicChats = 10005, - UserListBlockedRelays = 10006, - UserListSearchRelays = 10007, - UserListInterests = 10015, - UserListEmojis = 10030, - LightningPubRpc = 21000, - ClientAuth = 22242, - NWCInfo = 13194, - NWCRequest = 23194, - NWCResponse = 23195, - NostrConnect = 24133, - HttpAuth = 27235, - ListFollows = 3, - ListPeople = 30000, - ListGeneric = 30001, - ListRelays = 30002, - ListBookmarks = 30003, - ListCurations = 30004, - ProfileBadges = 30008, - BadgeDefinition = 30009, - ListEmojis = 30030, - ListInterests = 30015, - LongFormArticle = 30023, - LongFormArticleDraft = 30024, - Application = 30078, - LiveEvent = 30311, - UserStatuses = 30315, - ClassifiedListing = 30402, - DraftClassifiedListing = 30403, - Audio = 31337, - Feed = 31890, - Calendar = 31924, - CalendarEventDate = 31922, - CalendarEventTime = 31923, - CalendarEventRsvp = 31925, - HandlerRecommendation = 31989, - HandlerInformation = 31990, - CommunityDefinition = 34550, - GroupDefinition = 35834, -} +export const PROFILE = 0 +export const NOTE = 1 +export const FOLLOWS = 3 +export const DELETE = 5 +export const REPOST = 6 +export const REACTION = 7 +export const BADGE_AWARD = 8 +export const GROUP_CHAT_MESSAGE = 9 +export const GROUP_CHAT_REPLY = 10 +export const GROUP_CHAT_THREAD = 11 +export const GROUP_CHAT_THREAD_REPLY = 12 +export const SEAL = 13 +export const DIRECT_MESSAGE = 14 +export const GENERIC_REPOST = 16 +export const CHANNEL_CREATE = 40 +export const CHANNEL_UPDATE = 41 +export const CHANNEL_MESSAGE = 42 +export const CHANNEL_HIDE_MESSAGE = 43 +export const CHANNEL_MUTE_USER = 44 +export const BID = 1021 +export const BID_CONFIRMATION = 1022 +export const OTS = 1040 +export const WRAP = 1059 +export const WRAP_NIP04 = 1060 +export const FILE_METADATA = 1063 +export const LIVE_CHAT_MESSAGE = 1311 +export const GIT_PATCH = 1617 +export const GIT_ISSUE = 1617 +export const GIT_REPLY = 1617 +export const GIT_STATUS_OPEN = 1630 +export const GIT_STATUS_COMPLETE = 1631 +export const GIT_STATUS_CLOSED = 1632 +export const GIT_STATUS_DRAFT = 1633 +export const GIT_REPOSITORY = 30403 +export const REMIX = 1808 +export const NOSTROCKER_PROBLEM = 1971 +export const REPORT = 1984 +export const LABEL = 1985 +export const APPROVAL = 4550 +export const DVM_REQUEST_TEXT_EXTRACTION = 5000 +export const DVM_REQUEST_TEXT_SUMMARY = 5001 +export const DVM_REQUEST_TEXT_TRANSLATION = 5002 +export const DVM_REQUEST_TEXT_GENERATION = 5050 +export const DVM_REQUEST_IMAGE_GENERATION = 5100 +export const DVM_REQUEST_VIDEO_CONVERSION = 5200 +export const DVM_REQUEST_VIDEO_TRANSLATION = 5201 +export const DVM_REQUEST_IMAGE_TO_VIDEO_CONVERSION = 5202 +export const DVM_REQUEST_TEXT_TO_SPEECH = 5250 +export const DVM_REQUEST_DISCOVER_CONTENT = 5300 +export const DVM_REQUEST_DISCOVER_PEOPLE = 5301 +export const DVM_REQUEST_SEARCH_CONTENT = 5302 +export const DVM_REQUEST_SEARCH_PEOPLE = 5303 +export const DVM_REQUEST_COUNT = 5400 +export const DVM_REQUEST_MALWARE_SCAN = 5500 +export const DVM_REQUEST_OTS = 5900 +export const DVM_REQUEST_OP_RETURN = 5901 +export const DVM_REQUEST_PUBLISH_SCHEDULE = 5905 +export const DVM_RESPONSE_TEXT_EXTRACTION = 6000 +export const DVM_RESPONSE_TEXT_SUMMARY = 6001 +export const DVM_RESPONSE_TEXT_TRANSLATION = 6002 +export const DVM_RESPONSE_TEXT_GENERATION = 6050 +export const DVM_RESPONSE_IMAGE_GENERATION = 6100 +export const DVM_RESPONSE_VIDEO_CONVERSION = 6200 +export const DVM_RESPONSE_VIDEO_TRANSLATION = 6201 +export const DVM_RESPONSE_IMAGE_TO_VIDEO_CONVERSION = 6202 +export const DVM_RESPONSE_TEXT_TO_SPEECH = 6250 +export const DVM_RESPONSE_DISCOVER_CONTENT = 6300 +export const DVM_RESPONSE_DISCOVER_PEOPLE = 6301 +export const DVM_RESPONSE_SEARCH_CONTENT = 6302 +export const DVM_RESPONSE_SEARCH_PEOPLE = 6303 +export const DVM_RESPONSE_COUNT = 6400 +export const DVM_RESPONSE_MALWARE_SCAN = 6500 +export const DVM_RESPONSE_OTS = 6900 +export const DVM_RESPONSE_OP_RETURN = 6901 +export const DVM_RESPONSE_PUBLISH_SCHEDULE = 6905 +export const DVM_FEEDBACK = 7000 +export const ZAP_GOAL = 9041 +export const ZAP_REQUEST = 9734 +export const ZAP_RESPONSE = 9735 +export const HIGHLIGHT = 9802 +export const MUTES = 10000 +export const PINS = 10001 +export const RELAYS = 10002 +export const BOOKMARKS = 10003 +export const COMMUNITIES = 10004 +export const CHANNELS = 10005 +export const BLOCKED_RELAYS = 10006 +export const SEARCH_RELAYS = 10007 +export const GROUPS = 10009 +export const TOPICS = 10015 +export const EMOJIS = 10030 +export const DM_INBOX_RELAYS = 10050 +export const FILE_SERVERS = 10096 +export const LIGHTNING_PUB_RPC = 21000 +export const CLIENT_AUTH = 22242 +export const WALLET_INFO = 13194 +export const WALLET_REQUEST = 23194 +export const WALLET_RESPONSE = 23195 +export const NOSTR_CONNECT = 24133 +export const HTTP_AUTH = 27235 +export const NAMED_PEOPLE = 30000 +export const NAMED_RELAYS = 30002 +export const NAMED_BOOKMARKS = 30003 +export const NAMED_CURATIONS = 30004 +export const NAMED_WIKI_AUTHORS = 30101 +export const NAMED_WIKI_RELAYS = 30102 +export const NAMED_EMOJIS = 30030 +export const NAMED_TOPICS = 30015 +export const NAMED_ARTIFACTS = 30063 +export const BADGES = 30008 +export const BADGE_DEFINITION = 30009 +export const STALL = 30017 +export const PRODUCT = 30018 +export const MARKET_UI = 30019 +export const PRODUCT_SOLD_AS_AUCTION = 30020 +export const WIKI = 30818 +export const LONG_FORM = 30023 +export const LONG_FORM_DRAFT = 30024 +export const APPLICATION = 30078 +export const LIVE_EVENT = 30311 +export const STATUSES = 30315 +export const CLASSIFIED = 30402 +export const DRAFT_CLASSIFIED = 30403 +export const AUDIO = 31337 +export const FEED = 31890 +export const CALENDAR = 31924 +export const EVENT_DATE = 31922 +export const EVENT_TIME = 31923 +export const EVENT_RSVP = 31925 +export const HANDLER_RECOMMENDATION = 31989 +export const HANDLER_INFORMATION = 31990 +export const COMMUNITY = 34550 +export const GROUP = 35834 + +export const DEPRECATED_RELAY_RECOMMENDATION = 2 +export const DEPRECATED_DIRECT_MESSAGE = 4 +export const DEPRECATED_NAMED_GENERIC = 30001 + diff --git a/packages/util/Relay.ts b/packages/util/Relay.ts index 2991c5a..d7ee63b 100644 --- a/packages/util/Relay.ts +++ b/packages/util/Relay.ts @@ -2,9 +2,9 @@ import {Emitter} from '@welshman/lib' import {matchFilters} from './Filters' import type {Repository} from './Repository' import type {Filter} from './Filters' -import type {Rumor} from './Events' +import type {TrustedEvent} from './Events' -export class Relay extends Emitter { +export class Relay extends Emitter { subs = new Map() constructor(readonly repository: Repository) { diff --git a/packages/util/Repository.ts b/packages/util/Repository.ts index 6388ed3..efb93d8 100644 --- a/packages/util/Repository.ts +++ b/packages/util/Repository.ts @@ -1,12 +1,12 @@ import {throttle} from 'throttle-debounce' import type {IReadable, Subscriber, Invalidator} from '@welshman/lib' import {Derived, Emitter, writable, first, always, chunk, sleep, uniq, omit, now, range, identity} from '@welshman/lib' -import {Kind} from './Kinds' +import {DELETE} from './Kinds' import {matchFilter, getIdFilters, matchFilters} from './Filters' import {encodeAddress, addressFromEvent} from './Address' import {isReplaceable} from './Events' import type {Filter} from './Filters' -import type {Rumor} from './Events' +import type {TrustedEvent} from './Events' export const DAY = 86400 @@ -16,7 +16,7 @@ export type RepositoryOptions = { throttle?: number } -export class Repository extends Emitter implements IReadable> { +export class Repository extends Emitter implements IReadable> { eventsById = new Map() eventsByAddress = new Map() eventsByTag = new Map() @@ -225,7 +225,7 @@ export class Repository extends Emitter implements IReadable extends Emitter implements IReadable + Event = (event: TrustedEvent) => this.scenario(this.forceValue(event.id, [ this.getPubkeySelection(event.pubkey, RelayMode.Write), ...this.getContextSelections(Tags.fromEvent(event).context()), ])) - EventChildren = (event: Rumor) => + EventChildren = (event: TrustedEvent) => this.scenario(this.forceValue(event.id, [ this.getPubkeySelection(event.pubkey, RelayMode.Read), ...this.getContextSelections(Tags.fromEvent(event).context()), ])) - EventAncestors = (event: Rumor, type: "mentions" | "replies" | "roots") => { + EventAncestors = (event: TrustedEvent, type: "mentions" | "replies" | "roots") => { const tags = Tags.fromEvent(event) const ancestors = tags.ancestors()[type] const pubkeys = tags.whereKey("p").values().valueOf() @@ -207,13 +207,13 @@ export class Router { return this.product(ancestors.values().valueOf(), relays) } - EventMentions = (event: Rumor) => this.EventAncestors(event, "mentions") + EventMentions = (event: TrustedEvent) => this.EventAncestors(event, "mentions") - EventParents = (event: Rumor) => this.EventAncestors(event, "replies") + EventParents = (event: TrustedEvent) => this.EventAncestors(event, "replies") - EventRoots = (event: Rumor) => this.EventAncestors(event, "roots") + EventRoots = (event: TrustedEvent) => this.EventAncestors(event, "roots") - PublishEvent = (event: Rumor) => { + PublishEvent = (event: TrustedEvent) => { const tags = Tags.fromEvent(event) const mentions = tags.values("p").valueOf() @@ -279,13 +279,13 @@ export class Router { tagPubkey = (pubkey: string) => Tag.from(["p", pubkey, this.FromPubkeys([pubkey]).getUrl()]) - tagEventId = (event: Rumor, mark = "") => + tagEventId = (event: TrustedEvent, mark = "") => Tag.from(["e", event.id, this.Event(event).getUrl(), mark, event.pubkey]) - tagEventAddress = (event: Rumor, mark = "") => + tagEventAddress = (event: TrustedEvent, mark = "") => Tag.from(["a", getAddress(event), this.Event(event).getUrl(), mark, event.pubkey]) - tagEvent = (event: Rumor, mark = "") => { + tagEvent = (event: TrustedEvent, mark = "") => { const tags = [this.tagEventId(event, mark)] if (isReplaceable(event)) { @@ -295,7 +295,7 @@ export class Router { return new Tags(tags) } - address = (event: Rumor) => + address = (event: TrustedEvent) => addressFromEvent(event, this.Event(event).redundancy(3).getUrls()) } diff --git a/packages/util/Tags.ts b/packages/util/Tags.ts index 9d3eff0..ffa2497 100644 --- a/packages/util/Tags.ts +++ b/packages/util/Tags.ts @@ -4,7 +4,7 @@ import {Fluent, ensurePlural} from '@welshman/lib' import {isShareableRelayUrl, normalizeRelayUrl} from './Relays' import type {Address} from './Address' import {encodeAddress, decodeAddress} from './Address' -import {Kind} from './Kinds' +import {GROUP, COMMUNITY} from './Kinds' export class Tag extends (Fluent as OmitStatics, 'from'>) { static from = (xs: Iterable) => new Tag(Array.from(xs)) @@ -33,11 +33,11 @@ export class Tag extends (Fluent as OmitStatics, ' isAddress = (kind?: number) => this.key() === "a" && this.value()?.startsWith(`${kind}:`) - isGroup = () => this.isAddress(Kind.GroupDefinition) + isGroup = () => this.isAddress(GROUP) - isCommunity = () => this.isAddress(Kind.CommunityDefinition) + isCommunity = () => this.isAddress(COMMUNITY) - isContext = () => this.isAddress(Kind.GroupDefinition) || this.isAddress(Kind.CommunityDefinition) + isContext = () => this.isAddress(GROUP) || this.isAddress(COMMUNITY) } export class Tags extends (Fluent as OmitStatics, 'from'>) {