+66
-11
apps/api/src/xrpc/app/rocksky/feed/getFeed.ts
+66
-11
apps/api/src/xrpc/app/rocksky/feed/getFeed.ts
···
10
10
import type { SelectTrack } from "schema/tracks";
11
11
import type { SelectUser } from "schema/users";
12
12
import axios from "axios";
13
+
import { HandlerAuth } from "@atproto/xrpc-server";
14
+
import { env } from "lib/env";
13
15
14
16
export default function (server: Server, ctx: Context) {
15
-
const getFeed = (params: QueryParams) =>
17
+
const getFeed = (params: QueryParams, auth: HandlerAuth) =>
16
18
pipe(
17
-
{ params, ctx },
19
+
{ params, ctx, did: auth.credentials?.did },
18
20
retrieve,
19
21
Effect.flatMap(hydrate),
20
22
Effect.flatMap(presentation),
···
26
28
}),
27
29
);
28
30
server.app.rocksky.feed.getFeed({
29
-
handler: async ({ params }) => {
30
-
const result = await Effect.runPromise(getFeed(params));
31
+
auth: ctx.authVerifier,
32
+
handler: async ({ params, auth }) => {
33
+
const result = await Effect.runPromise(getFeed(params, auth));
31
34
return {
32
35
encoding: "application/json",
33
36
body: result,
···
36
39
});
37
40
}
38
41
39
-
const retrieve = ({ params, ctx }: { params: QueryParams; ctx: Context }) => {
42
+
const retrieve = ({
43
+
params,
44
+
ctx,
45
+
did,
46
+
}: {
47
+
params: QueryParams;
48
+
ctx: Context;
49
+
did?: string;
50
+
}) => {
40
51
return Effect.tryPromise({
41
52
try: async () => {
42
53
const [feed] = await ctx.db
···
47
58
if (!feed) {
48
59
throw new Error(`Feed not found`);
49
60
}
50
-
const feedUrl = `https://${feed.did.split("did:web:")[1]}`;
61
+
const feedUrl = env.PUBLIC_URL.includes("localhost")
62
+
? "http://localhost:8002"
63
+
: `https://${feed.did.split("did:web:")[1]}`;
51
64
const response = await axios.get<{
52
65
cusrsor: string;
53
66
feed: { scrobble: string }[];
···
58
71
cursor: params.cursor,
59
72
},
60
73
});
61
-
return { uris: response.data.feed.map(({ scrobble }) => scrobble), ctx };
74
+
return {
75
+
uris: response.data.feed.map(({ scrobble }) => scrobble),
76
+
ctx,
77
+
did,
78
+
};
62
79
},
63
80
catch: (error) => new Error(`Failed to retrieve feed: ${error}`),
64
81
});
···
67
84
const hydrate = ({
68
85
uris,
69
86
ctx,
87
+
did,
70
88
}: {
71
89
uris: string[];
72
90
ctx: Context;
91
+
did?: string;
73
92
}): Effect.Effect<Scrobbles | undefined, Error> => {
74
93
return Effect.tryPromise({
75
-
try: () =>
76
-
ctx.db
94
+
try: async () => {
95
+
const scrobbles = await ctx.db
77
96
.select()
78
97
.from(tables.scrobbles)
79
98
.leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id))
80
99
.leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id))
81
100
.where(inArray(tables.scrobbles.uri, uris))
82
101
.orderBy(desc(tables.scrobbles.timestamp))
83
-
.execute(),
102
+
.execute();
103
+
104
+
const trackIds = scrobbles.map((row) => row.tracks?.id).filter(Boolean);
105
+
106
+
const likes = await ctx.db
107
+
.select()
108
+
.from(tables.lovedTracks)
109
+
.leftJoin(tables.users, eq(tables.lovedTracks.userId, tables.users.id))
110
+
.where(inArray(tables.lovedTracks.trackId, trackIds))
111
+
.execute();
112
+
113
+
const likesMap = new Map<string, { count: number; liked: boolean }>();
114
+
115
+
for (const trackId of trackIds) {
116
+
const trackLikes = likes.filter(
117
+
(l) => l.loved_tracks.trackId === trackId,
118
+
);
119
+
likesMap.set(trackId, {
120
+
count: trackLikes.length,
121
+
liked: trackLikes.some((l) => l.users.did === did),
122
+
});
123
+
}
124
+
125
+
const result = scrobbles.map((row) => ({
126
+
...row,
127
+
likesCount: likesMap.get(row.tracks?.id)?.count ?? 0,
128
+
liked: likesMap.get(row.tracks?.id)?.liked ?? false,
129
+
}));
130
+
131
+
return result;
132
+
},
84
133
85
134
catch: (error) => new Error(`Failed to hydrate feed: ${error}`),
86
135
});
···
88
137
89
138
const presentation = (data: Scrobbles): Effect.Effect<FeedView, never> => {
90
139
return Effect.sync(() => ({
91
-
feed: data.map(({ scrobbles, tracks, users }) => ({
140
+
feed: data.map(({ scrobbles, tracks, users, likesCount, liked }) => ({
92
141
scrobble: {
93
142
...R.omit(["albumArt", "id", "lyrics"])(tracks),
94
143
cover: tracks.albumArt,
···
98
147
userAvatar: users.avatar,
99
148
uri: scrobbles.uri,
100
149
tags: [],
150
+
likesCount,
151
+
liked,
152
+
createdAt: scrobbles.createdAt.toISOString(),
153
+
updatedAt: scrobbles.updatedAt.toISOString(),
101
154
id: scrobbles.id,
102
155
},
103
156
})),
···
108
161
scrobbles: SelectScrobble;
109
162
tracks: SelectTrack;
110
163
users: SelectUser;
164
+
likesCount: number;
165
+
liked: boolean;
111
166
}[];