Skip to content

Commit

Permalink
Fix includeContentHashtags only including last hashtag
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Jan 13, 2025
1 parent 68d8c03 commit 1d0bba9
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-needles-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"applesauce-factory": patch
---

Fix replaceable loader mixing parameterized replaceable and replaceable pointers
5 changes: 5 additions & 0 deletions .changeset/thin-mails-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"applesauce-factory": patch
---

Fix `includeContentHashtags` only including last hashtag
15 changes: 14 additions & 1 deletion packages/factory/src/helpers/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,24 @@ export function fillAndTrimTag(tag: (string | undefined | null)[], minLength = 2
return tag as string[];
}

/** Ensures a single named tag exists */
export function ensureSingletonTag(tags: string[][], tag: string[], replace = true): string[][] {
const existing = tags.find((t) => t[0] === tag[0]);

if (existing) {
if (replace) return tags.map((t) => (t[0] === tag[0] ? tag : t));
if (replace) return tags.map((t) => (t === existing ? tag : t));
else return tags;
} else {
return [...tags, tag];
}
}

/** Ensures a single named / value tag exists */
export function ensureNamedValueTag(tags: string[][], tag: string[], replace = true): string[][] {
const existing = tags.find((t) => t[0] === tag[0] && t[1] === tag[1]);

if (existing) {
if (replace) return tags.map((t) => (t === existing ? tag : t));
else return tags;
} else {
return [...tags, tag];
Expand Down
41 changes: 41 additions & 0 deletions packages/factory/src/operations/hashtags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { includeContentHashtags, includeHashtags } from "./hashtags.js";
import { unixNow } from "applesauce-core/helpers";

describe("hashtags helpers", () => {
describe("includeContentHashtags", () => {
it("should include all content hashtags", () => {
expect(
includeContentHashtags()(
{ content: "hello world #growNostr #nostr", created_at: unixNow(), tags: [], kind: 1 },
{},
),
).toEqual(
expect.objectContaining({
tags: [
["t", "grownostr"],
["t", "nostr"],
],
}),
);
});
});

describe("includeHashtags", () => {
it("should include all hashtags", () => {
expect(
includeHashtags(["nostr", "growNostr"])(
{ content: "hello world", created_at: unixNow(), tags: [], kind: 1 },
{},
),
).toEqual(
expect.objectContaining({
tags: [
["t", "nostr"],
["t", "grownostr"],
],
}),
);
});
});
});
6 changes: 3 additions & 3 deletions packages/factory/src/operations/hashtags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Expressions } from "applesauce-content/helpers";
import { EventFactoryOperation } from "../event-factory.js";
import { ensureSingletonTag } from "../helpers/tag.js";
import { ensureNamedValueTag } from "../helpers/tag.js";

/** Adds "t" tags for every #hashtag in the content */
export function includeContentHashtags(): EventFactoryOperation {
Expand All @@ -11,7 +11,7 @@ export function includeContentHashtags(): EventFactoryOperation {
const matches = draft.content.matchAll(Expressions.hashtag);
for (const [_, hashtag] of matches) {
const lower = hashtag.toLocaleLowerCase();
tags = ensureSingletonTag(tags, ["t", lower]);
tags = ensureNamedValueTag(tags, ["t", lower]);
}

return { ...draft, tags };
Expand All @@ -25,7 +25,7 @@ export function includeHashtags(hashtags: string[]): EventFactoryOperation {

for (const hashtag of hashtags) {
const lower = hashtag.toLocaleLowerCase();
tags = ensureSingletonTag(tags, ["t", lower]);
tags = ensureNamedValueTag(tags, ["t", lower]);
}

return { ...draft, tags };
Expand Down
6 changes: 3 additions & 3 deletions packages/factory/src/operations/picture-post.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { MediaAttachment } from "applesauce-core/helpers";

import { EventFactoryOperation } from "../event-factory.js";
import { ensureSingletonTag } from "../helpers/tag.js";
import { ensureNamedValueTag } from "../helpers/tag.js";

/** Includes the "x" and "m" tags for kind 20 picture posts */
export function includePicturePostImageTags(pictures: MediaAttachment[]): EventFactoryOperation {
return (draft) => {
let tags = Array.from(draft.tags);

for (const image of pictures) {
if (image.sha256) tags = ensureSingletonTag(tags, ["x", image.sha256]);
if (image.type) tags = ensureSingletonTag(tags, ["m", image.type]);
if (image.sha256) tags = ensureNamedValueTag(tags, ["x", image.sha256]);
if (image.type) tags = ensureNamedValueTag(tags, ["m", image.type]);
}

return { ...draft, tags };
Expand Down
9 changes: 8 additions & 1 deletion packages/factory/src/operations/tags.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EventFactoryOperation } from "../event-factory.js";
import { ensureSingletonTag } from "../helpers/tag.js";
import { ensureNamedValueTag, ensureSingletonTag } from "../helpers/tag.js";

/** Includes only a single instance of tag */
export function includeSingletonTag(tag: string[], replace = true): EventFactoryOperation {
Expand All @@ -8,6 +8,13 @@ export function includeSingletonTag(tag: string[], replace = true): EventFactory
};
}

/** Includes only a single name / value tag */
export function includeNameValueTag(tag: string[], replace = true): EventFactoryOperation {
return (draft) => {
return { ...draft, tags: ensureNamedValueTag(draft.tags, tag, replace) };
};
}

/** Includes a NIP-31 alt tag */
export function includeAltTag(description: string): EventFactoryOperation {
return includeSingletonTag(["alt", description]);
Expand Down
24 changes: 24 additions & 0 deletions packages/loaders/src/helpers/address-pointer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, it, expect } from "vitest";
import { createFiltersFromAddressPointers } from "./address-pointer.js";
import { kinds } from "nostr-tools";

describe("address pointer helpers", () => {
describe("createFiltersFromAddressPointers", () => {
it("should separate replaceable and parameterized replaceable pointers", () => {
expect(
createFiltersFromAddressPointers([
{ kind: kinds.BookmarkList, pubkey: "pubkey" },
{ kind: kinds.Metadata, pubkey: "pubkey" },
{ kind: kinds.Metadata, pubkey: "pubkey2" },
{ kind: kinds.Bookmarksets, identifier: "funny", pubkey: "pubkey" },
]),
).toEqual(
expect.arrayContaining([
{ kinds: [kinds.Metadata], authors: ["pubkey", "pubkey2"] },
{ kinds: [kinds.BookmarkList], authors: ["pubkey"] },
{ "#d": ["funny"], authors: ["pubkey"], kinds: [kinds.Bookmarksets] },
]),
);
});
});
});
23 changes: 19 additions & 4 deletions packages/loaders/src/helpers/address-pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { Filter } from "nostr-tools";
import { isParameterizedReplaceableKind, isReplaceableKind } from "nostr-tools/kinds";

import { unique } from "./array.js";
import { AddressPointer } from "nostr-tools/nip19";

/** Converts an array of address pointers to a filter */
export function createFilterFromAddressPointers(pointers: AddressPointerWithoutD[]): Filter {
export function createFilterFromAddressPointers(pointers: AddressPointerWithoutD[] | AddressPointer[]): Filter {
const filter: Filter = {};

filter.kinds = unique(pointers.map((p) => p.kind));
Expand All @@ -18,9 +19,23 @@ export function createFilterFromAddressPointers(pointers: AddressPointerWithoutD

/** Takes a set of address pointers, groups them, then returns filters for the groups */
export function createFiltersFromAddressPointers(pointers: AddressPointerWithoutD[]): Filter[] {
const groups = groupAddressPointersByPubkeyOrKind(pointers);
// split the points in to two groups so they they don't mix in the filters
const parameterizedReplaceable = pointers.filter((p) => isParameterizedReplaceableKind(p.kind) && !!p.identifier);
const replaceable = pointers.filter((p) => isReplaceableKind(p.kind) && !p.identifier);

return Array.from(groups.values()).map((pointers) => createFilterFromAddressPointers(pointers));
const filters: Filter[] = [];

if (replaceable.length > 0) {
const groups = groupAddressPointersByPubkeyOrKind(replaceable);
filters.push(...Array.from(groups.values()).map(createFilterFromAddressPointers));
}

if (parameterizedReplaceable.length > 0) {
const groups = groupAddressPointersByPubkeyOrKind(parameterizedReplaceable);
filters.push(...Array.from(groups.values()).map(createFilterFromAddressPointers));
}

return filters;
}

/** Checks if a relay will understand an address pointer */
Expand Down Expand Up @@ -60,7 +75,7 @@ export function groupAddressPointersByPubkeyOrKind(pointers: AddressPointerWitho
const kinds = new Set(pointers.map((p) => p.kind));
const pubkeys = new Set(pointers.map((p) => p.pubkey));

return pubkeys.size > kinds.size ? groupAddressPointersByKind(pointers) : groupAddressPointersByPubkey(pointers);
return pubkeys.size < kinds.size ? groupAddressPointersByPubkey(pointers) : groupAddressPointersByKind(pointers);
}

export function getRelaysFromPointers(pointers: AddressPointerWithoutD[]) {
Expand Down

0 comments on commit 1d0bba9

Please sign in to comment.