Merge branch 'legacy-quote' into 'main'
Fix display of legacy quote tags, add tests for tags.ts See merge request soapbox-pub/ditto!291
This commit is contained in:
commit
f21a7191ef
|
@ -7,7 +7,6 @@ import { Conf } from '@/config.ts';
|
||||||
import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
|
import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
|
||||||
import { booleanParamSchema, fileSchema } from '@/schema.ts';
|
import { booleanParamSchema, fileSchema } from '@/schema.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts';
|
|
||||||
import { uploadFile } from '@/utils/upload.ts';
|
import { uploadFile } from '@/utils/upload.ts';
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
import { createEvent, paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
import { createEvent, paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
||||||
|
@ -18,6 +17,7 @@ import { renderRelationship } from '@/views/mastodon/relationships.ts';
|
||||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { bech32ToPubkey } from '@/utils.ts';
|
import { bech32ToPubkey } from '@/utils.ts';
|
||||||
|
import { addTag, deleteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
const usernameSchema = z
|
const usernameSchema = z
|
||||||
.string().min(1).max(30)
|
.string().min(1).max(30)
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { Conf } from '@/config.ts';
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { booleanParamSchema } from '@/schema.ts';
|
import { booleanParamSchema } from '@/schema.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { addTag } from '@/tags.ts';
|
|
||||||
import { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts';
|
import { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts';
|
||||||
|
import { addTag } from '@/utils/tags.ts';
|
||||||
import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts';
|
import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts';
|
||||||
|
|
||||||
const adminAccountQuerySchema = z.object({
|
const adminAccountQuerySchema = z.object({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { getTagSet } from '@/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { renderStatuses } from '@/views.ts';
|
import { renderStatuses } from '@/views.ts';
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/bookmarks/#get */
|
/** https://docs.joinmastodon.org/methods/bookmarks/#get */
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { getTagSet } from '@/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { renderAccounts } from '@/views.ts';
|
import { renderAccounts } from '@/views.ts';
|
||||||
|
|
||||||
/** https://docs.joinmastodon.org/methods/mutes/#get */
|
/** https://docs.joinmastodon.org/methods/mutes/#get */
|
||||||
|
|
|
@ -8,15 +8,15 @@ import { Conf } from '@/config.ts';
|
||||||
import { DittoDB } from '@/db/DittoDB.ts';
|
import { DittoDB } from '@/db/DittoDB.ts';
|
||||||
import { getUnattachedMediaByIds } from '@/db/unattached-media.ts';
|
import { getUnattachedMediaByIds } from '@/db/unattached-media.ts';
|
||||||
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
|
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
|
||||||
import { addTag, deleteTag } from '@/tags.ts';
|
|
||||||
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
|
||||||
import { renderEventAccounts } from '@/views.ts';
|
import { renderEventAccounts } from '@/views.ts';
|
||||||
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { getLnurl } from '@/utils/lnurl.ts';
|
|
||||||
import { asyncReplaceAll } from '@/utils/text.ts';
|
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
|
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
|
||||||
|
import { getLnurl } from '@/utils/lnurl.ts';
|
||||||
import { lookupPubkey } from '@/utils/lookup.ts';
|
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||||
|
import { addTag, deleteTag } from '@/utils/tags.ts';
|
||||||
|
import { asyncReplaceAll } from '@/utils/text.ts';
|
||||||
|
|
||||||
const createStatusSchema = z.object({
|
const createStatusSchema = z.object({
|
||||||
in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(),
|
in_reply_to_id: z.string().regex(/[0-9a-f]{64}/).nullish(),
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { NStore } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { AppController } from '@/app.ts';
|
import { AppController } from '@/app.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { getTagSet } from '@/tags.ts';
|
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
|
|
||||||
export const suggestionsV1Controller: AppController = async (c) => {
|
export const suggestionsV1Controller: AppController = async (c) => {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { RelayError } from '@/RelayError.ts';
|
||||||
import { updateStats } from '@/stats.ts';
|
import { updateStats } from '@/stats.ts';
|
||||||
import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts';
|
import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { getTagSet } from '@/tags.ts';
|
|
||||||
import { eventAge, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts';
|
import { eventAge, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts';
|
||||||
import { fetchWorker } from '@/workers/fetch.ts';
|
import { fetchWorker } from '@/workers/fetch.ts';
|
||||||
import { policyWorker } from '@/workers/policy.ts';
|
import { policyWorker } from '@/workers/policy.ts';
|
||||||
|
@ -22,6 +21,7 @@ import { verifyEventWorker } from '@/workers/verify.ts';
|
||||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||||
import { lnurlCache } from '@/utils/lnurl.ts';
|
import { lnurlCache } from '@/utils/lnurl.ts';
|
||||||
import { nip05Cache } from '@/utils/nip05.ts';
|
import { nip05Cache } from '@/utils/nip05.ts';
|
||||||
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
|
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { NostrEvent, NostrRelayOK, NPolicy, NStore } from '@nostrify/nostrify';
|
import { NostrEvent, NostrRelayOK, NPolicy, NStore } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { getTagSet } from '@/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
export class MuteListPolicy implements NPolicy {
|
export class MuteListPolicy implements NPolicy {
|
||||||
constructor(private pubkey: string, private store: NStore) {}
|
constructor(private pubkey: string, private store: NStore) {}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { Conf } from '@/config.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { type DittoRelation } from '@/interfaces/DittoFilter.ts';
|
import { type DittoRelation } from '@/interfaces/DittoFilter.ts';
|
||||||
import { findReplyTag, getTagSet } from '@/tags.ts';
|
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
|
import { findReplyTag, getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
const debug = Debug('ditto:queries');
|
const debug = Debug('ditto:queries');
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { SetRequired } from 'type-fest';
|
||||||
import { DittoDB } from '@/db/DittoDB.ts';
|
import { DittoDB } from '@/db/DittoDB.ts';
|
||||||
import { DittoTables } from '@/db/DittoTables.ts';
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { findReplyTag, getTagSet } from '@/tags.ts';
|
import { findReplyTag, getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
type AuthorStat = keyof Omit<DittoTables['author_stats'], 'pubkey'>;
|
type AuthorStat = keyof Omit<DittoTables['author_stats'], 'pubkey'>;
|
||||||
type EventStat = keyof Omit<DittoTables['event_stats'], 'event_id'>;
|
type EventStat = keyof Omit<DittoTables['event_stats'], 'event_id'>;
|
||||||
|
|
|
@ -7,11 +7,11 @@ import { Kysely } from 'kysely';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { DittoTables } from '@/db/DittoTables.ts';
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
import { normalizeFilters } from '@/filter.ts';
|
import { normalizeFilters } from '@/filter.ts';
|
||||||
|
import { RelayError } from '@/RelayError.ts';
|
||||||
import { purifyEvent } from '@/storages/hydrate.ts';
|
import { purifyEvent } from '@/storages/hydrate.ts';
|
||||||
import { getTagSet } from '@/tags.ts';
|
|
||||||
import { isNostrId, isURL } from '@/utils.ts';
|
import { isNostrId, isURL } from '@/utils.ts';
|
||||||
import { abortError } from '@/utils/abort.ts';
|
import { abortError } from '@/utils/abort.ts';
|
||||||
import { RelayError } from '@/RelayError.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
/** Function to decide whether or not to index a tag. */
|
/** Function to decide whether or not to index a tag. */
|
||||||
type TagCondition = ({ event, count, value }: {
|
type TagCondition = ({ event, count, value }: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
||||||
|
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { getTagSet } from '@/tags.ts';
|
import { getTagSet } from '@/utils/tags.ts';
|
||||||
|
|
||||||
export class UserStore implements NStore {
|
export class UserStore implements NStore {
|
||||||
constructor(private pubkey: string, private store: NStore) {}
|
constructor(private pubkey: string, private store: NStore) {}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { DittoTables } from '@/db/DittoTables.ts';
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { refreshAuthorStatsDebounced } from '@/stats.ts';
|
import { refreshAuthorStatsDebounced } from '@/stats.ts';
|
||||||
|
import { findQuoteTag } from '@/utils/tags.ts';
|
||||||
|
|
||||||
interface HydrateOpts {
|
interface HydrateOpts {
|
||||||
events: DittoEvent[];
|
events: DittoEvent[];
|
||||||
|
@ -81,7 +82,7 @@ function assembleEvents(
|
||||||
event.user = b.find((e) => matchFilter({ kinds: [30361], authors: [admin], '#d': [event.pubkey] }, e));
|
event.user = b.find((e) => matchFilter({ kinds: [30361], authors: [admin], '#d': [event.pubkey] }, e));
|
||||||
|
|
||||||
if (event.kind === 1) {
|
if (event.kind === 1) {
|
||||||
const id = event.tags.find(([name]) => name === 'q')?.[1];
|
const id = findQuoteTag(event.tags)?.[1];
|
||||||
if (id) {
|
if (id) {
|
||||||
event.quote = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
|
event.quote = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
|
||||||
}
|
}
|
||||||
|
@ -169,7 +170,7 @@ function gatherQuotes({ events, store, signal }: HydrateOpts): Promise<DittoEven
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (event.kind === 1) {
|
if (event.kind === 1) {
|
||||||
const id = event.tags.find(([name]) => name === 'q')?.[1];
|
const id = findQuoteTag(event.tags)?.[1];
|
||||||
if (id) {
|
if (id) {
|
||||||
ids.add(id);
|
ids.add(id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { assertEquals } from '@std/assert';
|
|
||||||
|
|
||||||
import { addTag, deleteTag, getTagSet } from './tags.ts';
|
|
||||||
|
|
||||||
Deno.test('getTagSet', () => {
|
|
||||||
assertEquals(getTagSet([], 'p'), new Set());
|
|
||||||
assertEquals(getTagSet([['p', '123']], 'p'), new Set(['123']));
|
|
||||||
assertEquals(getTagSet([['p', '123'], ['p', '456']], 'p'), new Set(['123', '456']));
|
|
||||||
assertEquals(getTagSet([['p', '123'], ['p', '456'], ['q', '789']], 'p'), new Set(['123', '456']));
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test('addTag', () => {
|
|
||||||
assertEquals(addTag([], ['p', '123']), [['p', '123']]);
|
|
||||||
assertEquals(addTag([['p', '123']], ['p', '123']), [['p', '123']]);
|
|
||||||
assertEquals(addTag([['p', '123'], ['p', '456']], ['p', '123']), [['p', '123'], ['p', '456']]);
|
|
||||||
assertEquals(addTag([['p', '123'], ['p', '456']], ['p', '789']), [['p', '123'], ['p', '456'], ['p', '789']]);
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test('deleteTag', () => {
|
|
||||||
assertEquals(deleteTag([], ['p', '123']), []);
|
|
||||||
assertEquals(deleteTag([['p', '123']], ['p', '123']), []);
|
|
||||||
assertEquals(deleteTag([['p', '123']], ['p', '456']), [['p', '123']]);
|
|
||||||
assertEquals(deleteTag([['p', '123'], ['p', '123']], ['p', '123']), []);
|
|
||||||
assertEquals(deleteTag([['p', '123'], ['p', '456']], ['p', '456']), [['p', '123']]);
|
|
||||||
});
|
|
42
src/tags.ts
42
src/tags.ts
|
@ -1,42 +0,0 @@
|
||||||
/** Get the values for a tag in a `Set`. */
|
|
||||||
function getTagSet(tags: string[][], tagName: string): Set<string> {
|
|
||||||
const set = new Set<string>();
|
|
||||||
|
|
||||||
tags.forEach((tag) => {
|
|
||||||
if (tag[0] === tagName) {
|
|
||||||
set.add(tag[1]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if the tag exists by its name and value. */
|
|
||||||
function hasTag(tags: string[][], tag: string[]): boolean {
|
|
||||||
return tags.some(([name, value]) => name === tag[0] && value === tag[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Delete all occurences of the tag by its name/value pair. */
|
|
||||||
function deleteTag(tags: readonly string[][], tag: string[]): string[][] {
|
|
||||||
return tags.filter(([name, value]) => !(name === tag[0] && value === tag[1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Add a tag to the list, replacing the name/value pair if it already exists. */
|
|
||||||
function addTag(tags: readonly string[][], tag: string[]): string[][] {
|
|
||||||
const tagIndex = tags.findIndex(([name, value]) => name === tag[0] && value === tag[1]);
|
|
||||||
if (tagIndex === -1) {
|
|
||||||
return [...tags, tag];
|
|
||||||
} else {
|
|
||||||
return [...tags.slice(0, tagIndex), tag, ...tags.slice(tagIndex + 1)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isReplyTag = (tag: string[]) => tag[0] === 'e' && tag[3] === 'reply';
|
|
||||||
const isRootTag = (tag: string[]) => tag[0] === 'e' && tag[3] === 'root';
|
|
||||||
const isLegacyReplyTag = (tag: string[]) => tag[0] === 'e' && !tag[3];
|
|
||||||
|
|
||||||
function findReplyTag(tags: string[][]) {
|
|
||||||
return tags.find(isReplyTag) || tags.find(isRootTag) || tags.findLast(isLegacyReplyTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { addTag, deleteTag, findReplyTag, getTagSet, hasTag };
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
|
import { addTag, deleteTag, findQuoteTag, findReplyTag, getTagSet, hasTag } from './tags.ts';
|
||||||
|
|
||||||
|
Deno.test('addTag', () => {
|
||||||
|
const tags = [['p', 'alex']];
|
||||||
|
assertEquals(addTag(tags, ['p', 'alex']), [['p', 'alex']]);
|
||||||
|
assertEquals(addTag(tags, ['p', 'fiatjaf']), [['p', 'alex'], ['p', 'fiatjaf']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('deleteTag', () => {
|
||||||
|
const tags = [['p', 'alex'], ['p', 'fiatjaf']];
|
||||||
|
assertEquals(deleteTag(tags, ['p', 'alex']), [['p', 'fiatjaf']]);
|
||||||
|
assertEquals(deleteTag(tags, ['p', 'fiatjaf']), [['p', 'alex']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('findQuoteTag', () => {
|
||||||
|
assertEquals(findQuoteTag([['q', '123']]), ['q', '123']);
|
||||||
|
assertEquals(findQuoteTag([['e', '', '', 'mention', '456']]), ['e', '', '', 'mention', '456']);
|
||||||
|
assertEquals(findQuoteTag([['e', '', '', 'mention', '456'], ['q', '123']]), ['q', '123']);
|
||||||
|
assertEquals(findQuoteTag([['q', '123'], ['e', '', '', 'mention', '456']]), ['q', '123']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('findReplyTag', () => {
|
||||||
|
const root = ['e', '123', '', 'root'];
|
||||||
|
const reply = ['e', '456', '', 'reply'];
|
||||||
|
|
||||||
|
assertEquals(findReplyTag([root]), root);
|
||||||
|
assertEquals(findReplyTag([reply]), reply);
|
||||||
|
assertEquals(findReplyTag([root, reply]), reply);
|
||||||
|
assertEquals(findReplyTag([reply, root]), reply);
|
||||||
|
assertEquals(findReplyTag([['e', '321'], ['e', '789']]), ['e', '789']);
|
||||||
|
assertEquals(findReplyTag([reply, ['e', '789']]), reply);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('getTagSet', () => {
|
||||||
|
const tags = [['p', 'alex'], ['p', 'fiatjaf'], ['p', 'alex']];
|
||||||
|
assertEquals(getTagSet(tags, 'p'), new Set(['alex', 'fiatjaf']));
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('hasTag', () => {
|
||||||
|
const tags = [['p', 'alex']];
|
||||||
|
assertEquals(hasTag(tags, ['p', 'alex']), true);
|
||||||
|
assertEquals(hasTag(tags, ['p', 'fiatjaf']), false);
|
||||||
|
});
|
|
@ -0,0 +1,71 @@
|
||||||
|
/** Get the values for a tag in a `Set`. */
|
||||||
|
function getTagSet(tags: string[][], tagName: string): Set<string> {
|
||||||
|
const set = new Set<string>();
|
||||||
|
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
if (tag[0] === tagName) {
|
||||||
|
set.add(tag[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if the tag exists by its name and value. */
|
||||||
|
function hasTag(tags: string[][], tag: string[]): boolean {
|
||||||
|
return tags.some(([name, value]) => name === tag[0] && value === tag[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete all occurences of the tag by its name/value pair. */
|
||||||
|
function deleteTag(tags: readonly string[][], tag: string[]): string[][] {
|
||||||
|
return tags.filter(([name, value]) => !(name === tag[0] && value === tag[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a tag to the list, replacing the name/value pair if it already exists. */
|
||||||
|
function addTag(tags: readonly string[][], tag: string[]): string[][] {
|
||||||
|
const tagIndex = tags.findIndex(([name, value]) => name === tag[0] && value === tag[1]);
|
||||||
|
if (tagIndex === -1) {
|
||||||
|
return [...tags, tag];
|
||||||
|
} else {
|
||||||
|
return [...tags.slice(0, tagIndex), tag, ...tags.slice(tagIndex + 1)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tag is a NIP-10 root tag. */
|
||||||
|
function isRootTag(tag: string[]): tag is ['e', string, string, 'root', ...string[]] {
|
||||||
|
return tag[0] === 'e' && tag[3] === 'root';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tag is a NIP-10 reply tag. */
|
||||||
|
function isReplyTag(tag: string[]): tag is ['e', string, string, 'reply', ...string[]] {
|
||||||
|
return tag[0] === 'e' && tag[3] === 'reply';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tag is a legacy "e" tag with a "mention" marker. */
|
||||||
|
function isLegacyQuoteTag(tag: string[]): tag is ['e', string, string, 'mention', ...string[]] {
|
||||||
|
return tag[0] === 'e' && tag[3] === 'mention';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tag is an "e" tag without a NIP-10 marker. */
|
||||||
|
function isLegacyReplyTag(tag: string[]): tag is ['e', string, string] {
|
||||||
|
return tag[0] === 'e' && !tag[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tag is a "q" tag. */
|
||||||
|
function isQuoteTag(tag: string[]): tag is ['q', ...string[]] {
|
||||||
|
return tag[0] === 'q';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the "e" tag for the event being replied to, first according to the NIPs then falling back to the legacy way. */
|
||||||
|
function findReplyTag(tags: string[][]): ['e', ...string[]] | undefined {
|
||||||
|
return tags.find(isReplyTag) || tags.find(isRootTag) || tags.findLast(isLegacyReplyTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the "q" tag, falling back to the legacy "e" tag with a "mention" marker. */
|
||||||
|
function findQuoteTag(
|
||||||
|
tags: string[][],
|
||||||
|
): ['q', ...string[]] | ['e', string, string, 'mention', ...string[]] | undefined {
|
||||||
|
return tags.find(isQuoteTag) || tags.find(isLegacyQuoteTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { addTag, deleteTag, findQuoteTag, findReplyTag, getTagSet, hasTag };
|
|
@ -1,5 +1,5 @@
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { hasTag } from '@/tags.ts';
|
import { hasTag } from '@/utils/tags.ts';
|
||||||
|
|
||||||
async function renderRelationship(sourcePubkey: string, targetPubkey: string) {
|
async function renderRelationship(sourcePubkey: string, targetPubkey: string) {
|
||||||
const db = await Storages.db();
|
const db = await Storages.db();
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { NostrEvent } from '@nostrify/nostrify';
|
import { NostrEvent } from '@nostrify/nostrify';
|
||||||
import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
|
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { findReplyTag } from '@/tags.ts';
|
|
||||||
import { nostrDate } from '@/utils.ts';
|
import { nostrDate } from '@/utils.ts';
|
||||||
import { getMediaLinks, parseNoteContent, stripimeta } from '@/utils/note.ts';
|
import { getMediaLinks, parseNoteContent, stripimeta } from '@/utils/note.ts';
|
||||||
|
import { findQuoteTag, findReplyTag } from '@/utils/tags.ts';
|
||||||
import { unfurlCardCached } from '@/utils/unfurl.ts';
|
import { unfurlCardCached } from '@/utils/unfurl.ts';
|
||||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { renderAttachment } from '@/views/mastodon/attachments.ts';
|
import { renderAttachment } from '@/views/mastodon/attachments.ts';
|
||||||
|
@ -30,6 +29,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
: await accountFromPubkey(event.pubkey);
|
: await accountFromPubkey(event.pubkey);
|
||||||
|
|
||||||
const replyTag = findReplyTag(event.tags);
|
const replyTag = findReplyTag(event.tags);
|
||||||
|
const quoteTag = findQuoteTag(event.tags);
|
||||||
|
|
||||||
const mentionedPubkeys = [
|
const mentionedPubkeys = [
|
||||||
...new Set(
|
...new Set(
|
||||||
|
@ -73,8 +73,8 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
|
|
||||||
const content = buildInlineRecipients(mentions) + html;
|
const content = buildInlineRecipients(mentions) + html;
|
||||||
|
|
||||||
const cw = event.tags.find(isCWTag);
|
const cw = event.tags.find(([name]) => name === 'content-warning');
|
||||||
const subject = event.tags.find((tag) => tag[0] === 'subject');
|
const subject = event.tags.find(([name]) => name === 'subject');
|
||||||
|
|
||||||
const imeta: string[][][] = event.tags
|
const imeta: string[][][] = event.tags
|
||||||
.filter(([name]) => name === 'imeta')
|
.filter(([name]) => name === 'imeta')
|
||||||
|
@ -88,7 +88,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
card,
|
card,
|
||||||
content,
|
content,
|
||||||
created_at: nostrDate(event.created_at).toISOString(),
|
created_at: nostrDate(event.created_at).toISOString(),
|
||||||
in_reply_to_id: replyTag ? replyTag[1] : null,
|
in_reply_to_id: replyTag?.[1] ?? null,
|
||||||
in_reply_to_account_id: null,
|
in_reply_to_account_id: null,
|
||||||
sensitive: !!cw,
|
sensitive: !!cw,
|
||||||
spoiler_text: (cw ? cw[1] : subject?.[1]) || '',
|
spoiler_text: (cw ? cw[1] : subject?.[1]) || '',
|
||||||
|
@ -110,7 +110,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
emojis: renderEmojis(event),
|
emojis: renderEmojis(event),
|
||||||
poll: null,
|
poll: null,
|
||||||
quote: !event.quote ? null : await renderStatus(event.quote, { depth: depth + 1 }),
|
quote: !event.quote ? null : await renderStatus(event.quote, { depth: depth + 1 }),
|
||||||
quote_id: event.tags.find(([name]) => name === 'q')?.[1] ?? null,
|
quote_id: quoteTag?.[1] ?? null,
|
||||||
uri: Conf.external(note),
|
uri: Conf.external(note),
|
||||||
url: Conf.external(note),
|
url: Conf.external(note),
|
||||||
zapped: Boolean(zapEvent),
|
zapped: Boolean(zapEvent),
|
||||||
|
|
Loading…
Reference in New Issue