Switch to monorepo setup

This commit is contained in:
Jon Staab
2024-03-25 14:22:33 -07:00
parent 74b926e227
commit 54e0775453
49 changed files with 3677 additions and 2321 deletions
+2
View File
@@ -0,0 +1,2 @@
build
normalize-url
+61
View File
@@ -0,0 +1,61 @@
import type {UnsignedEvent} from 'nostr-tools'
import {nip19} from 'nostr-tools'
import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds'
export type Address = {
kind: number,
pubkey: string
identifier: string
relays: string[]
}
// Plain text format
export const decodeAddress = (a: string, relays: string[] = []): Address => {
const [kind, pubkey, identifier = ""] = a.split(":")
return {kind: parseInt(kind), pubkey, identifier, relays}
}
export const encodeAddress = (a: Address) => [a.kind, a.pubkey, a.identifier].join(":")
// Naddr encoding
export const addressFromNaddr = (naddr: string): Address => {
let type
let data = {} as any
try {
({type, data} = nip19.decode(naddr) as {
type: "naddr"
data: any
})
} catch (e) {
// pass
}
if (type !== "naddr") {
throw new Error(`Invalid naddr ${naddr}`)
}
return data
}
export const addressToNaddr = (a: Address): string => nip19.naddrEncode(a)
// Get from event, encode to filter
export const getIdentifier = (e: UnsignedEvent) => e.tags.find(t => t[0] === "d")?.[1] || ""
export const addressFromEvent = (e: UnsignedEvent, relays: string[] = []) =>
({kind: e.kind, pubkey: e.pubkey, identifier: getIdentifier(e), relays})
export const addressToFilter = (a: Address) =>
({kinds: [a.kind], authors: [a.pubkey], "#d": [a.identifier]})
// Utils
export const isGroupAddress = (a: Address) => a.kind === GROUP_DEFINITION
export const isCommunityAddress = (a: Address) => a.kind === COMMUNITY_DEFINITION
export const isContextAddress = (a: Address) => [GROUP_DEFINITION, COMMUNITY_DEFINITION].includes(a.kind)
+69
View File
@@ -0,0 +1,69 @@
import type {Event, EventTemplate, UnsignedEvent} from 'nostr-tools'
import {verifyEvent, getEventHash} from 'nostr-tools'
import {cached, now} from '@coracle.social/lib'
import {Tags} from './Tags'
import {addressFromEvent, encodeAddress} from './Address'
import {isEphemeralKind, isReplaceableKind, isPlainReplaceableKind, isParameterizedReplaceableKind} from './Kinds'
export type Rumor = Pick<Event, 'kind' | 'tags' | 'content' | 'created_at' | 'pubkey' | 'id'>
export type CreateEventOpts = {
content?: string
tags?: string[][]
created_at?: number
}
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 asUnsignedEvent = ({kind, tags, content, created_at, pubkey}: UnsignedEvent): UnsignedEvent =>
({kind, tags, content, created_at, pubkey})
export const asRumor = ({kind, tags, content, created_at, pubkey, id}: Rumor): Rumor =>
({kind, tags, content, created_at, pubkey, id})
export const asEvent = ({kind, tags, content, created_at, pubkey, id, sig}: Event): Event =>
({kind, tags, content, created_at, pubkey, id, sig})
export const hasValidSignature = cached<string, boolean, [Event]>({
maxSize: 10000,
getKey: ([e]: [Event]) => {
try {
return [getEventHash(e), e.sig].join(":")
} catch (err) {
return 'invalid'
}
},
getValue: ([e]: [Event]) => {
try {
return verifyEvent(e)
} catch (err) {
return false
}
},
})
export const getAddress = (e: UnsignedEvent) => encodeAddress(addressFromEvent(e))
export const getIdOrAddress = (e: Rumor) => isReplaceable(e) ? getAddress(e) : e.id
export const getIdAndAddress = (e: Rumor) => isReplaceable(e) ? [e.id, getAddress(e)] : [e.id]
export const isEphemeral = (e: EventTemplate) => isEphemeralKind(e.kind)
export const isReplaceable = (e: EventTemplate) => isReplaceableKind(e.kind)
export const isPlainReplaceable = (e: EventTemplate) => isPlainReplaceableKind(e.kind)
export const isParameterizedReplaceable = (e: EventTemplate) => isParameterizedReplaceableKind(e.kind)
export const isChildOf = (child: EventTemplate, parent: Rumor) => {
const {roots, replies} = Tags.fromEvent(child).ancestors()
const parentIds = (replies.exists() ? replies : roots).values().valueOf()
return getIdAndAddress(parent).some(x => parentIds.includes(x))
}
+44
View File
@@ -0,0 +1,44 @@
import type {Event} from 'nostr-tools'
import {matchFilter as nostrToolsMatchFilter} from 'nostr-tools'
export type Filter = {
ids?: string[]
kinds?: number[]
authors?: string[]
since?: number
until?: number
limit?: number
search?: string
[key: `#${string}`]: string[]
}
export const matchFilter = (filter: Filter, event: Event) => {
if (!nostrToolsMatchFilter(filter, event)) {
return false
}
if (filter.search) {
const content = event.content.toLowerCase()
const terms = filter.search.toLowerCase().split(/\s+/g)
for (const term of terms) {
if (content.includes(term)) {
return true
}
return false
}
}
return true
}
export const matchFilters = (filters: Filter[], event: Event) => {
for (const filter of filters) {
if (matchFilter(filter, event)) {
return true
}
}
return false
}
+81
View File
@@ -0,0 +1,81 @@
import {between} from '@coracle.social/lib'
export const isEphemeralKind = (kind: number) => between(19999, 29999, kind)
export const isPlainReplaceableKind = (kind: number) => between(9999, 20000, kind)
export const isParameterizedReplaceableKind = (kind: number) => between(29999, 40000, kind)
export const isReplaceableKind = (kind: number) => isPlainReplaceableKind(kind) || isParameterizedReplaceableKind(kind)
export const PROFILE = 0
export const NOTE = 1
export const RELAY = 2
export const DM = 4
export const EVENT_DELETION = 5
export const REPOST = 6
export const REACTION = 7
export const BADGE_AWARD = 8
export const GENERIC_REPOST = 16
export const CHANNEL_CREATION = 40
export const CHANNEL_METADATA = 41
export const CHANNEL_MESSAGE = 42
export const CHANNEL_HIDE_MESSAGE = 43
export const CHANNEL_MUTE_USER = 44
export const OPEN_TIMESTAMP = 1040
export const GIFT_WRAP = 1059
export const FILE_METADATA = 1063
export const LIVE_CHAT_MESSAGE = 1311
export const PROBLEM_TRACKER = 1971
export const REPORT = 1984
export const LABEL = 1985
export const COMMUNITY_POST_APPROVAL = 4550
export const JOB_REQUEST = 5999
export const JOB_RESULT = 6999
export const JOB_FEEDBACK = 7000
export const ZAP_GOAL = 9041
export const ZAP_REQUEST = 9734
export const ZAP_RESPONSE = 9735
export const HIGHLIGHT = 9802
export const USER_LIST_MUTES = 10000
export const USER_LIST_PINS = 10001
export const USER_LIST_RELAYS = 10002
export const USER_LIST_BOOKMARKS = 10003
export const USER_LIST_COMMUNITIES = 10004
export const USER_LIST_PUBLIC_CHATS = 10005
export const USER_LIST_BLOCKED_RELAYS = 10006
export const USER_LIST_SEARCH_RELAYS = 10007
export const USER_LIST_INTERESTS = 10015
export const USER_LIST_EMOJIS = 10030
export const LIGHTNING_PUB_RPC = 21000
export const CLIENT_AUTH = 22242
export const NWC_INFO = 13194
export const NWC_REQUEST = 23194
export const NWC_RESPONSE = 23195
export const NOSTR_CONNECT = 24133
export const HTTP_AUTH = 27235
export const LIST_FOLLOWS = 3
export const LIST_PEOPLE = 30000
export const LIST_GENERIC = 30001
export const LIST_RELAYS = 30002
export const LIST_BOOKMARKS = 30003
export const LIST_CURATIONS = 30004
export const PROFILE_BADGES = 30008
export const BADGE_DEFINITION = 30009
export const LIST_EMOJIS = 30030
export const LIST_INTERESTS = 30015
export const LONG_FORM_ARTICLE = 30023
export const LONG_FORM_ARTICLE_DRAFT = 30024
export const APPLICATION = 30078
export const LIVE_EVENT = 30311
export const USER_STATUSES = 30315
export const CLASSIFIED_LISTING = 30402
export const DRAFT_CLASSIFIED_LISTING = 30403
export const CALENDAR = 31924
export const CALENDAR_EVENT_DATE = 31922
export const CALENDAR_EVENT_TIME = 31923
export const CALENDAR_EVENT_RSVP = 31925
export const HANDLER_RECOMMENDATION = 31989
export const HANDLER_INFORMATION = 31990
export const COMMUNITY_DEFINITION = 34550
export const GROUP_DEFINITION = 35834
+3
View File
@@ -0,0 +1,3 @@
export const fromNostrURI = (s: string) => s.replace(/^[\w+]+:\/?\/?/, "")
export const toNostrURI = (s: string) => `nostr:${s}`
+33
View File
@@ -0,0 +1,33 @@
import {normalizeUrl, stripProtocol} from '@coracle.social/lib'
export const isShareableRelayUrl = (url: string) =>
Boolean(
typeof url === 'string' &&
// Is it actually a websocket url and has a dot
url.match(/^wss:\/\/.+\..+/) &&
// Sometimes bugs cause multiple relays to get concatenated
url.match(/:\/\//g)?.length === 1 &&
// It shouldn't have any whitespace, url-encoded or otherwise
!url.match(/\s|%/) &&
// Don't match stuff with a port number
!url.slice(6).match(/:\d+/) &&
// Don't match raw ip addresses
!url.slice(6).match(/\d+\.\d+\.\d+\.\d+/) &&
// Skip nostr.wine's virtual relays
!url.slice(6).match(/\/npub/)
)
export const normalizeRelayUrl = (url: string) => {
// Use our library to normalize
url = normalizeUrl(url, {stripHash: true, stripAuthentication: false})
// Strip the protocol since only wss works
url = stripProtocol(url)
// Urls without pathnames are supposed to have a trailing slash
if (!url.includes("/")) {
url += "/"
}
return "wss://" + url
}
+277
View File
@@ -0,0 +1,277 @@
import type {EventTemplate, UnsignedEvent} from 'nostr-tools'
import {first, uniq, shuffle} from '@coracle.social/lib'
import type {Rumor} from './Events'
import {getAddress, isReplaceable} from './Events'
import {Tag, Tags} from './Tags'
import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds'
import {addressFromEvent, decodeAddress} from './Address'
const isGroupAddress = (a: string) => decodeAddress(a).kind === GROUP_DEFINITION
const isCommunityAddress = (a: string) => decodeAddress(a).kind === COMMUNITY_DEFINITION
export enum RelayMode {
Read = "read",
Write = "write",
}
export type RouterOptions = {
getUserPubkey: () => string | null
getGroupRelays: (address: string) => string[]
getCommunityRelays: (address: string) => string[]
getPubkeyRelays: (pubkey: string, mode?: RelayMode) => string[]
getDefaultRelays: (mode?: RelayMode) => string[]
getRelayQuality?: (url: string) => number
getDefaultLimit: () => number
}
export type RouteScores = Record<string, {score: number, count: number}>
export type FallbackPolicy = (urls: string[], limit: number) => number
export class Router {
constructor(readonly options: RouterOptions) {}
// Utilities derived from options
getUserRelays = (mode?: RelayMode) => {
const pubkey = this.options.getUserPubkey()
return pubkey ? this.options.getPubkeyRelays(pubkey, mode) : []
}
getContextRelayGroups = (event: EventTemplate) => {
const addresses = Tags.fromEvent(event).context().values().valueOf()
return [
...addresses.filter(isCommunityAddress).map(this.options.getCommunityRelays),
...addresses.filter(isGroupAddress).map(this.options.getGroupRelays),
]
}
// Utilities for processing hints
scoreGroups = (groups: string[][]) => {
const scores: RouteScores = {}
groups.filter(g => g?.length > 0).forEach((urls, i) => {
for (const url of shuffle(uniq(urls))) {
if (!scores[url]) {
scores[url] = {score: 0, count: 0}
}
scores[url].score += 1 / (i + 1)
scores[url].count += 1
}
})
for (const [url, score] of Object.entries(scores)) {
const quality = this.options.getRelayQuality?.(url) || 1
score.score = score.score * Math.cbrt(score.count) * quality
}
const items = Object.entries(scores)
.filter(([url, {score}]) => score > 0)
.sort((a, b) => b[1].score - a[1].score)
.map(([url, {score, count}]) => ({url, score, count}))
return items
}
scenario = (groups: string[][]) => new RouterScenario(this, groups)
merge = (scenarios: RouterScenario[]) =>
this.scenario(scenarios.map(scenario => scenario.policy(this.addNoFallbacks).getUrls()))
// Routing scenarios
User = () => this.scenario([this.getUserRelays()])
ReadRelays = () => this.scenario([this.getUserRelays()]).mode(RelayMode.Read)
WriteRelays = () => this.scenario([this.getUserRelays()]).mode(RelayMode.Write)
AllMessages = () => this.scenario([this.getUserRelays()])
Messages = (pubkeys: string[]) =>
this.scenario([
this.getUserRelays(),
...pubkeys.map(pubkey => this.options.getPubkeyRelays(pubkey))
])
PublishMessage = (pubkey: string) =>
this.scenario([
this.getUserRelays(RelayMode.Write),
this.options.getPubkeyRelays(pubkey, RelayMode.Read)
]).policy(this.addMinimalFallbacks)
Event = (event: UnsignedEvent) =>
this.scenario([
this.options.getPubkeyRelays(event.pubkey, RelayMode.Write),
...this.getContextRelayGroups(event),
])
EventChildren = (event: UnsignedEvent) =>
this.scenario([
this.options.getPubkeyRelays(event.pubkey, RelayMode.Read),
...this.getContextRelayGroups(event),
])
EventParent = (event: UnsignedEvent) => {
const tags = Tags.fromEvent(event)
return this.scenario([
tags.replies().relays().valueOf(),
tags.roots().relays().valueOf(),
...this.getContextRelayGroups(event),
...tags.whereKey("p").values().valueOf()
.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Write)),
tags.whereKey("p").relays().valueOf(),
this.options.getPubkeyRelays(event.pubkey, RelayMode.Read),
])
}
EventRoot = (event: UnsignedEvent) => {
const tags = Tags.fromEvent(event)
return this.scenario([
tags.roots().relays().valueOf(),
tags.replies().relays().valueOf(),
...this.getContextRelayGroups(event),
...tags.whereKey("p").values().valueOf()
.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Write)),
tags.whereKey("p").relays().valueOf(),
this.options.getPubkeyRelays(event.pubkey, RelayMode.Read),
])
}
PublishEvent = (event: UnsignedEvent) => {
const tags = Tags.fromEvent(event)
const mentions = tags.values("p").valueOf()
const addresses = tags.context().values().valueOf()
const groupAddresses = addresses.filter(isGroupAddress)
const communityAddresses = addresses.filter(isCommunityAddress)
// If we're publishing only to private groups, only publish to those groups' relays.
// Otherwise, publish to all relays, because it's essentially public.
if (groupAddresses.length > 0 && communityAddresses.length === 0) {
return this.scenario(groupAddresses.map(this.options.getGroupRelays))
}
return this.scenario([
this.options.getPubkeyRelays(event.pubkey, RelayMode.Write),
...groupAddresses.map(this.options.getGroupRelays),
...communityAddresses.map(this.options.getCommunityRelays),
...mentions.map((pk: string) => this.options.getPubkeyRelays(pk, RelayMode.Read)),
])
}
FromPubkeys = (pubkeys: string[]) =>
this.scenario(pubkeys.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Write)))
ForPubkeys = (pubkeys: string[]) =>
this.scenario(pubkeys.map(pk => this.options.getPubkeyRelays(pk, RelayMode.Read)))
WithinGroup = (address: string) =>
this.scenario([this.options.getGroupRelays(address)]).policy(this.addNoFallbacks)
WithinCommunity = (address: string) =>
this.scenario([this.options.getCommunityRelays(address)])
WithinContext = (address: string) => {
if (isGroupAddress(address)) {
return this.WithinGroup(address)
}
if (isCommunityAddress(address)) {
return this.WithinCommunity(address)
}
throw new Error(`Unknown context ${address}`)
}
WithinMultipleContexts = (addresses: string[]) =>
this.merge(addresses.map(this.WithinContext))
// Fallback policies
addNoFallbacks = (urls: string[], limit: number) => 0
addMinimalFallbacks = (urls: string[], limit: number) => Math.max(0, 1 - urls.length)
addMaximalFallbacks = (urls: string[], limit: number) => Math.max(0, limit - urls.length)
// Higher level utils that use hints
tagPubkey = (pubkey: string) =>
Tag.from(["p", pubkey, this.FromPubkeys([pubkey]).getUrl()])
tagEventId = (event: Rumor, ...extra: string[]) =>
Tag.from(["e", event.id, this.Event(event).getUrl(), ...extra])
tagEventAddress = (event: UnsignedEvent, ...extra: string[]) =>
Tag.from(["a", getAddress(event), this.Event(event).getUrl(), ...extra])
tagEvent = (event: Rumor, ...extra: string[]) => {
const tags = [this.tagEventId(event, ...extra)]
if (isReplaceable(event)) {
tags.push(this.tagEventAddress(event, ...extra))
}
return new Tags(tags)
}
address = (event: UnsignedEvent) =>
addressFromEvent(event, this.Event(event).limit(3).getUrls())
}
// Router Scenario
export type RouterScenarioOptions = {
mode?: RelayMode
limit?: number
policy?: FallbackPolicy
}
export class RouterScenario {
constructor(readonly router: Router, readonly groups: string[][], readonly options: RouterScenarioOptions = {}) {}
clone = (options: RouterScenarioOptions) =>
new RouterScenario(this.router, this.groups, {...this.options, ...options})
limit = (limit: number) => this.clone({limit})
mode = (mode: RelayMode) => this.clone({mode})
policy = (policy: FallbackPolicy) => this.clone({policy})
getLimit = () => this.options.limit || this.router.options.getDefaultLimit()
getPolicy = () => this.options.policy || this.router.addMaximalFallbacks
getFallbackRelays = () =>
shuffle(this.router.options.getDefaultRelays(this.options.mode))
getUrls = () => {
const fallbackPolicy = this.getPolicy()
const urls = this.router.scoreGroups(this.groups).map(s => s.url)
const limit = this.getLimit()
const limitWithFallbacks = Math.min(limit, urls.length) + fallbackPolicy(urls, limit)
for (const url of this.getFallbackRelays()) {
if (urls.length >= limitWithFallbacks) {
break
}
if (!urls.includes(url)) {
urls.push(url)
}
}
return urls.slice(0, limitWithFallbacks)
}
getUrl = () => first(this.limit(1).getUrls())
}
+210
View File
@@ -0,0 +1,210 @@
import {EventTemplate} from 'nostr-tools'
import type {OmitStatics} from '@coracle.social/lib'
import {Fluent, last} from '@coracle.social/lib'
import {isShareableRelayUrl, normalizeRelayUrl} from './Relays'
import type {Address} from './Address'
import {encodeAddress, decodeAddress} from './Address'
import {GROUP_DEFINITION, COMMUNITY_DEFINITION} from './Kinds'
export class Tag extends (Fluent<string> as OmitStatics<typeof Fluent<string>, 'from'>) {
static from(xs: Iterable<string>) {
return new Tag(Array.from(xs))
}
static fromAddress = (a: Address) => new Tag(["a", encodeAddress(a), a.relays[0] || ""])
valueOf = () => this.xs
key = () => this.xs[0]
value = () => this.xs[1]
mark = () => last(this.xs.slice(2))
entry = () => this.xs.slice(0, 2)
setKey = (k: string) => this.set(0, k)
setValue = (v: string) => this.set(1, v)
setMark = (m: string) => this.xs.length > 2 ? this.set(this.xs.length - 2, m) : this.append(m)
asAddress = () => decodeAddress(this.value())
isAddress = (kind?: number) => this.key() === "a" && this.value()?.startsWith(`${kind}:`)
isGroup = () => this.isAddress(GROUP_DEFINITION)
isCommunity = () => this.isAddress(COMMUNITY_DEFINITION)
isContext = () => this.isAddress(GROUP_DEFINITION) || this.isAddress(COMMUNITY_DEFINITION)
}
export class Tags extends (Fluent<Tag> as OmitStatics<typeof Fluent<Tag>, 'from'>) {
static from(p: Iterable<string[]>) {
return new Tags(Array.from(p).map(Tag.from))
}
static fromEvent(event: EventTemplate) {
return Tags.from(event.tags || [])
}
static fromEvents(events: EventTemplate[]) {
return Tags.from(events.flatMap(e => e.tags || []))
}
// @ts-ignore
valueOf = () => this.xs.map(tag => tag.valueOf())
whereKey = (key: string) => this.filter(t => t.key() === key)
whereValue = (value: string) => this.filter(t => t.value() === value)
whereMark = (mark: string) => this.filter(t => t.mark() === mark)
removeKey = (key: string) => this.reject(t => t.key() === key)
removeValue = (value: string) => this.reject(t => t.value() === value)
removeMark = (mark: string) => this.reject(t => t.mark() === mark)
get = (key: string) => this.whereKey(key).first()
keys = () => this.mapTo(t => t.key())
values = (key?: string) => (key ? this.whereKey(key) : this).mapTo(t => t.value())
marks = () => this.mapTo(t => t.mark())
entries = () => this.mapTo(t => t.entry())
relays = () => this.flatMap((t: Tag) => t.valueOf().filter(isShareableRelayUrl).map(normalizeRelayUrl)).uniq()
topics = () => this.whereKey("t").values().map((t: string) => t.replace(/^#/, ""))
ancestors = () => {
const tags = this.filter(t => ["a", "e", "q"].includes(t.key()) && !t.isContext())
const parentTags = tags.filter(t => ["a", "e"].includes(t.key()))
const mentionTags = tags.whereKey("q")
const roots: string[][] = []
const replies: string[][] = []
const mentions: string[][] = []
parentTags
.forEach((t: Tag, i: number) => {
if (t.mark() === 'root') {
roots.push(t.valueOf())
} else if (t.mark() === 'reply') {
replies.push(t.valueOf())
} else if (t.mark() === 'mention') {
mentions.push(t.valueOf())
} else if (i === 0) {
roots.push(t.valueOf())
} else if (i === parentTags.count() - 1) {
replies.push(t.valueOf())
} else {
mentions.push(t.valueOf())
}
})
// Add quotes as mentions separately so positional logic above works
mentionTags.forEach((t: Tag) => mentions.push(t.valueOf()))
return {
roots: Tags.from(roots),
replies: Tags.from(replies),
mentions: Tags.from(mentions),
}
}
roots = () => this.ancestors().roots
replies = () => this.ancestors().replies
mentions = () => this.ancestors().mentions
root = () => {
const roots = this.roots()
return roots.get("e") || roots.get("a")
}
reply = () => {
const replies = this.replies()
return replies.get("e") || replies.get("a")
}
parents = () => {
const {roots, replies} = this.ancestors()
return replies.exists() ? replies: roots
}
parent = () => {
const parents = this.parents()
return parents.get("e") || parents.get("a")
}
groups = () => this.whereKey("a").filter(t => t.isGroup())
communities = () => this.whereKey("a").filter(t => t.isCommunity())
context = () => this.whereKey("a").filter(t => t.isContext())
asObject = () => {
const result: Record<string, string> = {}
for (const t of this.xs) {
result[t.key()] = t.value()
}
return result
}
imeta = (url: string) => {
for (const tag of this.whereKey("imeta").xs) {
const tags = Tags.from(tag.drop(1).valueOf().map((m: string) => m.split(" ")))
if (tags.get("url")?.value() === url) {
return tags
}
}
return null
}
// Generic setters
addTag = (...args: string[]) => this.append(Tag.from(args))
setTag = (k: string, ...args: string[]) => this.removeKey(k).addTag(k, ...args)
// Context
addContext = (addresses: string[]) => this.concat(addresses.map(a => Tag.from(["a", a])))
removeContext = () => this.reject(t => t.isContext())
setContext = (addresses: string[]) => this.removeContext().addContext(addresses)
// Images
addImages = (imeta: Tags[]) =>
this.concat(imeta.map(tags => Tag.from(["image", tags.get("url").value()])))
removeImages = () => this.removeKey('image')
setImages = (imeta: Tags[]) => this.removeImages().addImages(imeta)
// IMeta
addIMeta = (imeta: Tags[]) =>
this.concat(imeta.map(tags => Tag.from(["imeta", ...tags.valueOf().map(xs => xs.join(" "))])))
removeIMeta = () => this.removeKey('imeta')
setIMeta = (imeta: Tags[]) => this.removeIMeta().addIMeta(imeta)
}
+8
View File
@@ -0,0 +1,8 @@
export * from './Address'
export * from './Events'
export * from './Filters'
export * from './Kinds'
export * from './Links'
export * from './Relays'
export * from './Router'
export * from './Tags'
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@coracle.social/util",
"version": "0.0.2",
"author": "hodlbod",
"license": "MIT",
"description": "A collection of utilities.",
"publishConfig": {
"access": "public"
},
"type": "module",
"files": [
"build"
],
"types": "./build/index.d.ts",
"exports": {
".": {
"types": "./build/index.d.ts",
"import": "./build/index.mjs",
"require": "./build/index.cjs"
}
},
"scripts": {
"pub": "npm run lint && npm run rebuild && npm publish",
"rebuild": "npm run clean && npm run build",
"build": "tsc-multi",
"clean": "gts clean",
"lint": "gts lint",
"fix": "gts fix"
},
"devDependencies": {
"@types/events": "^3.0.3",
"gts": "^5.0.1",
"tsc-multi": "^1.1.0",
"typescript": "~5.1.6"
},
"dependencies": {
"@coracle.social/lib": "^0.0.1",
"nostr-tools": "^2.3.2"
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"targets": [
{"extname": ".cjs", "module": "commonjs"},
{"extname": ".mjs", "module": "esnext", "moduleResolution": "node"}
],
"projects": ["tsconfig.json"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../node_modules/gts/tsconfig-google.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "build",
"esModuleInterop": true,
"skipLibCheck": true,
"lib": ["es2019"]
},
"include": ["**/*.ts"]
}