Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/custom-collections/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Custom Collections Example
========================

This example demonstrates how to implement custom collections in Fedify.
Custom collections allow you to define your own ActivityPub collections with
custom logic for dispatching items and counting collection sizes.


~~~~ sh
deno task codegen # At very first time only
deno run -A ./main.ts
~~~~
123 changes: 123 additions & 0 deletions examples/custom-collections/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Create, createFederation, MemoryKvStore, Note } from "@fedify/fedify";

// Mock data - in a real application, this would query your database
const POSTS = [
new Create({
id: new URL("https://example.com/posts/post-1"),
content: "ActivityPub is a decentralized social networking protocol...",
tags: [
new URL("https://example.com/tags/ActivityPub"),
new URL("https://example.com/tags/Decentralization"),
],
}),
new Create({
id: new URL("https://example.com/posts/post-2"),
content: "Fedify makes it easy to build federated applications...",
}),

new Create({
id: new URL("https://example.com/posts/post-3"),
content: "WebFinger is a protocol for discovering information...",
tags: [new URL("https://example.com/tags/ActivityPub")],
}),

new Create({
id: new URL("https://example.com/posts/post-4"),
content: "HTTP Signatures provide authentication for ActivityPub...",
}),

new Create({
id: new URL("https://example.com/posts/post-5"),
content: "Understanding ActivityPub's data model is crucial...",
}),
];

function getTagFromUrl(url: string): string {
const parts = url.split("/");
return parts[parts.length - 1];
}

function getTaggedPostsByTag(tag: string): Create[] {
const results = POSTS.filter((post) => {
if (!post.tagIds) {
return false;
}
const postTags = post.tagIds;
const matches = postTags.some((tagId) => {
return getTagFromUrl(tagId.toString()) === tag;
});
return matches;
});

return results;
}

async function demonstrateCustomCollection() {
// Note: federation instance created for demonstration
const federation = createFederation<void>({ kv: new MemoryKvStore() });

federation.setCollectionDispatcher(
"TaggedPosts",
Note,
"/users/{userId}/tags/{tag}",
(
_ctx: { url: URL },
values: Record<string, string>,
cursor: string | null,
) => {
if (!values.userId || !values.tag) {
throw new Error("Missing userId or tag in values");
}
const posts = getTaggedPostsByTag(values.tag);
const items = posts.map((post) => (new Note({
id: new URL(`/posts/${post.id}`, _ctx.url),
content: post.content,
})));

if (cursor != null) {
const idx = Number.parseInt(cursor, 10);
if (Number.isNaN(idx)) {
throw new Error("Invalid cursor");
}
return {
items: [items[idx]],
nextCursor: idx < items.length - 1 ? (idx + 1).toString() : null,
prevCursor: idx > 0 ? (idx - 1).toString() : null,
};
}
return { items };
},
).setCounter(async (_ctx, values) => {
// Return the total count of tagged posts
const count = (await getTaggedPostsByTag(values.tag)).length;
return count;
});

const response = await federation.fetch(
new Request(
"https://example.com/users/someone/tags/ActivityPub",
{
headers: {
Accept: "application/activity+json",
},
},
),
{
contextData: undefined,
},
);

console.log("Custom collection response status:", response.status);

if (response.ok) {
const jsonResponse = await response.json();
console.log("Custom collection data:", jsonResponse);
} else {
const errorText = await response.text();
console.log("Error response:", errorText);
}
}

if (import.meta.main) {
demonstrateCustomCollection().catch(console.error);
}
Loading