Add new tags utility
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
export class Tag {
|
||||||
|
constructor(readonly parts: string[]) {}
|
||||||
|
|
||||||
|
key() {
|
||||||
|
return this.parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
val() {
|
||||||
|
return this.parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
mark() {
|
||||||
|
return this.parts.slice(0, 2).slice(-1)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
nth(n: number) {
|
||||||
|
return this.parts[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
for (const x of this.parts) {
|
||||||
|
yield x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
export class Fluent<T> {
|
||||||
|
ItemClass?: Fluent<T>
|
||||||
|
|
||||||
|
constructor(value: T[]) {
|
||||||
|
this.value = value.filter(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(value: T[]) {
|
||||||
|
this.value = value.filter(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(item: T) {
|
||||||
|
const {ItemClass} = this.constructor
|
||||||
|
|
||||||
|
return ItemClass ? ItemClass.create(item) : item
|
||||||
|
}
|
||||||
|
|
||||||
|
valueOf = () => this.value
|
||||||
|
|
||||||
|
count = () => this.value.length
|
||||||
|
|
||||||
|
exists = () => this.value.length > 0
|
||||||
|
|
||||||
|
f = <U>(f: (t: T) => U) => f(this.value)
|
||||||
|
|
||||||
|
any = (f: (t: T) => boolean) => this.value.any(f)
|
||||||
|
|
||||||
|
every = (f: (t: T) => boolean) => this.value.every(f)
|
||||||
|
|
||||||
|
some = (f: (t: T) => boolean) => this.value.some(f)
|
||||||
|
|
||||||
|
first = () => this.item(this.values[0])
|
||||||
|
|
||||||
|
nth = (i: number) => this.item(this.values[i])
|
||||||
|
|
||||||
|
last = () => this.item(last(this.values))
|
||||||
|
|
||||||
|
find = (f: (t: T) => boolean) => this.item(this.value.find(f))
|
||||||
|
|
||||||
|
filter = (f: (t: T) => boolean) => this.constructor.create(this.value.filter(f))
|
||||||
|
|
||||||
|
reject = (f: (t: T) => boolean) => this.constructor.create(this.value.filter(t => !f(t)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Tag extends Fluent<string[]> {
|
||||||
|
type = () => this.value[0]
|
||||||
|
|
||||||
|
value = () => this.value[1]
|
||||||
|
|
||||||
|
mark = () => last(this.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Tags extends Fluent<string[][]> {
|
||||||
|
ItemClass: Tag
|
||||||
|
|
||||||
|
static from (e: Event | Event[]) {
|
||||||
|
const events = Array.isArray(e) ? e : [e]
|
||||||
|
|
||||||
|
return new Tags(events.flatMap(e => e?.tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
where(conditions: Record<string, (x: any) => boolean>) {
|
||||||
|
return this.filter(t => {
|
||||||
|
const tag = new Tag(t)
|
||||||
|
|
||||||
|
for ([k, f] of Object.entries(conditions)) {
|
||||||
|
if (!f(tag[k]())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
whereEq(conditions: Record<string, any>) {
|
||||||
|
return this.filter(t => {
|
||||||
|
const tag = new Tag(t)
|
||||||
|
|
||||||
|
for ([k, v] of Object.entries(conditions)) {
|
||||||
|
v = Array.isArray(v) ? v : [v]
|
||||||
|
|
||||||
|
if (!v.includes(tag[k]())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
value = () => this.value.find(t => t[1])
|
||||||
|
|
||||||
|
values = () => this.value.map(t => t[1])
|
||||||
|
|
||||||
|
relays = () => uniq(flatten(this.value).filter(isShareableRelay))
|
||||||
|
|
||||||
|
topics = () => this.whereEq({type: "t"}).values().map((t: string) => t.replace(/^#/, ""))
|
||||||
|
|
||||||
|
pubkeys = () => this.whereEq({type: "p"}).values()
|
||||||
|
|
||||||
|
urls = () => this.whereEq({type: "r"}).values()
|
||||||
|
|
||||||
|
getDict() {
|
||||||
|
const meta: Record<string, string> = {}
|
||||||
|
|
||||||
|
for (const [k, v] of this.value) {
|
||||||
|
if (!meta[k]) {
|
||||||
|
meta[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
getAncestorsLegacy() {
|
||||||
|
// Legacy only supports e tags. Normalize their length to 3
|
||||||
|
const eTags = this.whereEq({type: "e"}).map(t => {
|
||||||
|
while (t.length < 3) {
|
||||||
|
t.push("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.slice(0, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
roots: eTags.count() > 1 ? new Tags([eTags.first()]) : new Tags([]),
|
||||||
|
replies: new Tags([eTags.last()]),
|
||||||
|
mentions: new Tags(eTags.all().slice(1, -1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAncestors(type = null) {
|
||||||
|
// If we have a mark, we're not using the legacy format
|
||||||
|
if (!this.any(t => t.length === 4 && ["reply", "root", "mention"].includes(last(t)))) {
|
||||||
|
return this.getAncestorsLegacy()
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = new Tags(this.whereEq({type: type || ["}a", "e"]).all().filter(t => !String(t[1]).startsWith('34550:')))
|
||||||
|
|
||||||
|
return {
|
||||||
|
roots: new Tags(tags.mark('root').take(3).all()),
|
||||||
|
replies: new Tags(tags.mark('reply').take(3).all()),
|
||||||
|
mentions: new Tags(tags.mark('mention').take(3).all()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roots = (type = null) => this.getAncestors(type).roots
|
||||||
|
|
||||||
|
replies = (type = null) => this.getAncestors(type).replies
|
||||||
|
|
||||||
|
communities = () => this.whereEq({type: "a"}).values().filter(a => a.startsWith('34550:'))
|
||||||
|
|
||||||
|
getReply = (type = null) => this.replies(type).values().first()
|
||||||
|
|
||||||
|
getRoot = (type = null) => this.roots(type).values().first()
|
||||||
|
|
||||||
|
getReplyHints = (type = null) => this.replies(type).relays().all()
|
||||||
|
|
||||||
|
getRootHints = (type = null) => this.roots(type).relays().all()
|
||||||
|
}
|
||||||
+4
-145
@@ -12,6 +12,10 @@ export const last = <T>(xs: T[]) => xs[xs.length - 1]
|
|||||||
|
|
||||||
export const identity = <T>(x: T) => x
|
export const identity = <T>(x: T) => x
|
||||||
|
|
||||||
|
export const flatten = <T>(xs: T[]) => xs.flatMap(identity)
|
||||||
|
|
||||||
|
export const uniq = <T>(xs: T[]) => Array.from(new Set(xs))
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Relays
|
// Relays
|
||||||
|
|
||||||
@@ -91,151 +95,6 @@ export const hasValidSignature = cached<string, boolean, [Event]>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Tags
|
|
||||||
|
|
||||||
export class Fluent<T> {
|
|
||||||
xs: any[]
|
|
||||||
|
|
||||||
constructor(xs: T[]) {
|
|
||||||
this.xs = xs.filter(identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
as = <U>(f: (xs: T[]) => U) => f(this.xs)
|
|
||||||
|
|
||||||
all = () => this.xs
|
|
||||||
|
|
||||||
count = () => this.xs.length
|
|
||||||
|
|
||||||
exists = () => this.xs.length > 0
|
|
||||||
|
|
||||||
first = () => this.xs[0]
|
|
||||||
|
|
||||||
nth = (i: number) => this.xs[i]
|
|
||||||
|
|
||||||
last = () => last(this.xs)
|
|
||||||
|
|
||||||
flat = () => new Fluent(this.xs.flatMap(identity))
|
|
||||||
|
|
||||||
uniq = () => new Fluent(Array.from(new Set(this.xs)))
|
|
||||||
|
|
||||||
drop = (n: number) => new Fluent(this.xs.map(t => t.slice(n)))
|
|
||||||
|
|
||||||
take = (n: number) => new Fluent(this.xs.map(t => t.slice(0, n)))
|
|
||||||
|
|
||||||
map = <U>(f: (t: T) => U) => new Fluent(this.xs.map(f))
|
|
||||||
|
|
||||||
flatMap = <U>(f: (t: T) => U) => new Fluent(this.xs.flatMap(f))
|
|
||||||
|
|
||||||
pluck = (k: number | string) => new Fluent(this.xs.map(x => x[k]))
|
|
||||||
|
|
||||||
filter = (f: (t: T) => boolean) => new Fluent(this.xs.filter(f))
|
|
||||||
|
|
||||||
reject = (f: (t: T) => boolean) => new Fluent(this.xs.filter(t => !f(t)))
|
|
||||||
|
|
||||||
any = (f: (t: T) => boolean) => this.filter(f).exists()
|
|
||||||
|
|
||||||
find = (f: (t: T) => boolean) => this.xs.find(f)
|
|
||||||
|
|
||||||
has = (x: any) => this.xs.includes(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Tags extends Fluent<string[]> {
|
|
||||||
static from (e: Event | Event[]) {
|
|
||||||
const events = Array.isArray(e) ? e : [e]
|
|
||||||
|
|
||||||
return new Tags(events.flatMap(e => e.tags))
|
|
||||||
}
|
|
||||||
|
|
||||||
nthEq = (i: number, v: string) => new Tags(this.xs.filter(t => t[i] === v))
|
|
||||||
|
|
||||||
values = (k?: string) => this.filter(t => !k || t[0] === k).pluck(1)
|
|
||||||
|
|
||||||
type(t: string | string[]) {
|
|
||||||
const types = Array.isArray(t) ? t : [t]
|
|
||||||
|
|
||||||
return new Tags(this.xs.filter(t => types.includes(t[0])))
|
|
||||||
}
|
|
||||||
|
|
||||||
mark(m: string | string[]) {
|
|
||||||
const marks = Array.isArray(m) ? m : [m]
|
|
||||||
|
|
||||||
return new Tags(this.xs.filter(t => marks.includes(last(t))))
|
|
||||||
}
|
|
||||||
|
|
||||||
relays = () => this.flat().filter(isShareableRelay).uniq()
|
|
||||||
|
|
||||||
topics = () => this.type("t").values().map((t: string) => t.replace(/^#/, ""))
|
|
||||||
|
|
||||||
pubkeys = () => this.type("p").values()
|
|
||||||
|
|
||||||
urls = () => this.type("r").values()
|
|
||||||
|
|
||||||
getValue = (k?: string) => this.values(k).first()
|
|
||||||
|
|
||||||
getDict() {
|
|
||||||
const meta: Record<string, string> = {}
|
|
||||||
|
|
||||||
for (const [k, v] of this.xs) {
|
|
||||||
if (!meta[k]) {
|
|
||||||
meta[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return meta
|
|
||||||
}
|
|
||||||
|
|
||||||
getAncestorsLegacy() {
|
|
||||||
// Legacy only supports e tags. Normalize their length to 3
|
|
||||||
const eTags = this.type("e").map(t => {
|
|
||||||
while (t.length < 3) {
|
|
||||||
t.push("")
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.slice(0, 3)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
roots: eTags.count() > 1 ? new Tags([eTags.first()]) : new Tags([]),
|
|
||||||
replies: new Tags([eTags.last()]),
|
|
||||||
mentions: new Tags(eTags.all().slice(1, -1)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAncestors(type = null) {
|
|
||||||
// If we have a mark, we're not using the legacy format
|
|
||||||
if (!this.any(t => t.length === 4 && ["reply", "root", "mention"].includes(last(t)))) {
|
|
||||||
return this.getAncestorsLegacy()
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = new Tags(this.type(type || ["a", "e"]).all().filter(t => !String(t[1]).startsWith('34550:')))
|
|
||||||
|
|
||||||
return {
|
|
||||||
roots: new Tags(tags.mark('root').take(3).all()),
|
|
||||||
replies: new Tags(tags.mark('reply').take(3).all()),
|
|
||||||
mentions: new Tags(tags.mark('mention').take(3).all()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
roots = (type = null) => this.getAncestors(type).roots
|
|
||||||
|
|
||||||
replies = (type = null) => this.getAncestors(type).replies
|
|
||||||
|
|
||||||
groups = () => this.type("a").values().filter(a => a.startsWith('35834:'))
|
|
||||||
|
|
||||||
communities = () => this.type("a").values().filter(a => a.startsWith('34550:'))
|
|
||||||
|
|
||||||
circles = () => this.type("a").values().filter(a => a.match(/^(34550|35834):/))
|
|
||||||
|
|
||||||
getReply = (type = null) => this.replies(type).values().first()
|
|
||||||
|
|
||||||
getRoot = (type = null) => this.roots(type).values().first()
|
|
||||||
|
|
||||||
getReplyHints = (type = null) => this.replies(type).relays().all()
|
|
||||||
|
|
||||||
getRootHints = (type = null) => this.roots(type).relays().all()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Filters
|
// Filters
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user