Avatar

lukas

3 public vals
Joined January 29, 2023
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
export async function sampleFirehose() {
const cborx = await import("https://deno.land/x/cbor@v1.5.2/index.js");
const multiformats = await import("npm:multiformats");
const uint8arrays = await import("npm:uint8arrays");
const { z } = await import("https://deno.land/x/zod@v3.21.4/mod.ts");
const xrpc = await import("npm:@atproto/xrpc");
const cborCodec = await import(
"https://cdn.jsdelivr.net/npm/@ipld/dag-cbor/+esm"
);
const cborEncode = cborCodec.encode;
enum FrameType {
Message = 1,
Error = -1,
}
const messageFrameHeader = z.object({
op: z.literal(FrameType.Message),
t: z.string().optional(), // Message body type discriminator
});
type MessageFrameHeader = z.infer<typeof messageFrameHeader>;
const errorFrameHeader = z.object({
op: z.literal(FrameType.Error),
});
const errorFrameBody = z.object({
error: z.string(),
message: z.string().optional(), // Error message
});
type ErrorFrameHeader = z.infer<typeof errorFrameHeader>;
type ErrorFrameBody<T extends string = string> = {
error: T;
} & z.infer<typeof errorFrameBody>;
const frameHeader = z.union([messageFrameHeader, errorFrameHeader]);
type FrameHeader = z.infer<typeof frameHeader>;
abstract class Frame {
header: FrameHeader;
body: unknown;
get op(): FrameType {
return this.header.op;
}
toBytes(): Uint8Array {
return uint8arrays.concat([
cborEncode(this.header),
cborEncode(this.body),
]);
}
isMessage(): this is MessageFrame<unknown> {
return this.op === FrameType.Message;
}
isError(): this is ErrorFrame {
return this.op === FrameType.Error;
}
static fromBytes(bytes: Uint8Array) {
const decoded = cborDecodeMulti(bytes);
if (decoded.length > 2) {
throw new Error("Too many CBOR data items in frame");
}
const header = decoded[0];
let body: unknown = kUnset;
if (decoded.length > 1) {
body = decoded[1];
}
const parsedHeader = frameHeader.safeParse(header);
if (!parsedHeader.success) {
throw new Error(`Invalid frame header: ${parsedHeader.error.message}`);
}
if (body === kUnset) {
throw new Error("Missing frame body");
}
const frameOp = parsedHeader.data.op;
if (frameOp === FrameType.Message) {
return new MessageFrame(body, {
type: parsedHeader.data.t,
});
}
else if (frameOp === FrameType.Error) {
const parsedBody = errorFrameBody.safeParse(body);
if (!parsedBody.success) {
throw new Error(
`Invalid error frame body: ${parsedBody.error.message}`,
);
}
return new ErrorFrame(parsedBody.data);
}
else {
const exhaustiveCheck: never = frameOp;
throw new Error(`Unknown frame op: ${exhaustiveCheck}`);
}
}
}
class MessageFrame<T = Record<string, unknown>> extends Frame {
header: MessageFrameHeader;
body: T;
constructor(body: T, opts?: {
type?: string;
}) {
super();
this.header = opts?.type !== undefined
? { op: FrameType.Message, t: opts?.type }
: { op: FrameType.Message };
this.body = body;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { getFriendsOfFriends } from "https://esm.town/v/lukas/getFriendsOfFriends";
import { validateAuth } from "https://esm.town/v/lukas/validateAuth";
import { feedGenerator } from "https://esm.town/v/lukas/feedGenerator";
type GetFeedSkeleton = {
feed: string;
limit?: number;
cursor?: string;
};
type SkeletonFeedPostReason = SkeletonReasonRepost;
// format: at-uri
type SkeletonReasonRepost = {
repost: string;
};
// };
type SkeletonFeedPost = {
post: string;
};
type GetFeedSkeletonOutput = {
feed: SkeletonFeedPost[];
cursor?: string;
};
export async function friendOfAFriend(req: express.Request, res: express.Response) {
return (await feedGenerator({
feed: {
id: "foaf",
displayName: "Friend of a Friend",
algorithm: async (
ctx,
skeleton: GetFeedSkeleton,
): Promise<GetFeedSkeletonOutput> => {
const requesterDid = await validateAuth(
req,
ctx.serviceDid,
ctx.didResolver,
);
const feed = (await getFriendsOfFriends({
did: requesterDid,
limit: 20,
})).map(({ uri }) => ({ post: uri }));
return {
feed,
};
},
},
publisherDID: "did:plc:fm7e743trsxpnvj2i3zhgfyh",
hostname: "lukas-friendofafriend.express.val.run",
}))(req, res);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// All of this is heavily based on RobPC's feed generator sample
// As well as the official bsky sample feed
type Feed = {
uri: string;
};
type DescribeFeedGeneratorOutput = {
did: string;
feeds: Feed[];
links?: {
privacyPolicy?: string;
termsOfService?: string;
};
};
type GetFeedSkeleton = {
feed: string;
limit?: number;
cursor?: string;
};
type SkeletonFeedPostReason = SkeletonReasonRepost;
// format: at-uri
type SkeletonReasonRepost = {
repost: string;
};
type SkeletonFeedPost = {
post: string;
};
type GetFeedSkeletonOutput = {
feed: SkeletonFeedPost[];
cursor?: string;
}
export async function feedGenerator({ feed, serviceDID, publisherDID, hostname }: {
serviceDID?: string;
publisherDID: string;
hostname: string;
feed: {
id: string;
displayName: string;
description?: string;
algorithm: (ctx, GetFeedSkeleton) => Promise<GetFeedSkeletonOutput>;
};
}) {
const { default: atproto } = await import("https://cdn.jsdelivr.net/npm/@atproto/api/+esm");
const { default: resolver } = await import("npm:@atproto/did-resolver");
const feedUri = atproto.AtUri.make(
publisherDID,
"app.bsky.feed.generator",
feed.id,
);
const query = (req: express.Request, name: string) => {
const q = req.query[name];
if (!q)
return [];
if (Array.isArray(q)) {
return q.map<string>((i) => i.toString());
}
return [q.toString()];
};
return async function (req: express.Request, res: express.Response) {
const serviceDid = serviceDID ?? `did:web:${hostname}`;
const didCache = new resolver.MemoryCache();
const didResolver = new resolver.DidResolver(
{ plcUrl: 'https://plc.directory',didCache, },
);
const ctx = {serviceDid, hostname, didResolver, didCache};
if (req.path === "/.well-known/did.json") {
res.json({
"@context": ["https://www.w3.org/ns/did/v1"],
id: serviceDid,
service: [
{
id: "#bsky_fg",
type: "BskyFeedGenerator",
serviceEndpoint: `https://${hostname}`,
},
],
});
}
else if (req.path === "/xrpc/app.bsky.feed.describeFeedGenerator") {
if (!serviceDid.endsWith(hostname)) {
return res.status(500).json({
error: "This feed generator has an invalid Service DID",
});
}
const feeds: Feed[] = [{ uri: feedUri }];
res.json({ did: serviceDid, feeds } satisfies DescribeFeedGeneratorOutput);
}
else if (req.path === "/xrpc/app.bsky.feed.getFeedSkeleton") {
const atUri = query(req, "feed")[0];
const limit = query(req, "limit")[0];
const cursor = query(req, "cursor")[0];
if (!atUri) {
res.status(400).json({ error: "Missing param 'feed'" });
return
}
const [did, collection, key] = atUri.replace("at://", "").split("/");
if (
did !== publisherDID ||
collection !== "app.bsky.feed.generator"
Next