Skip to content

Commit 73bc7e7

Browse files
authored
Merge pull request #162 from dahlia/public-statuses-endpoints
2 parents a8d5cf1 + 5e5d27c commit 73bc7e7

9 files changed

Lines changed: 381 additions & 160 deletions

File tree

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ To be released.
6969
with the `GET /api/v1/accounts/verify_credentials` endpoint.
7070
[[#45], [#156] by Emelia Smith]
7171

72+
- Made few Mastodon API endpoints publicly accessible without
73+
authentication so that they behave more similarly to Mastodon:
74+
75+
- `GET /api/v1/statuses/:id`
76+
- `GET /api/v1/statuses/:id/context`
77+
7278
- Upgraded Fedify to 1.5.3 and *@fedify/postgres* to 0.3.0.
7379

7480
- The minimum required version of Node.js is now 24.0.0.

src/api/v1/statuses.ts

Lines changed: 137 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
and,
1515
eq,
1616
gt,
17+
inArray,
1718
isNotNull,
1819
isNull,
1920
notInArray,
@@ -40,6 +41,7 @@ import {
4041
toUpdate,
4142
} from "../../federation/post";
4243
import { appendPostToTimelines } from "../../federation/timeline";
44+
import { getAccessToken } from "../../oauth/helpers";
4345
import {
4446
type Variables,
4547
scopeRequired,
@@ -375,16 +377,21 @@ app.put(
375377
},
376378
);
377379

378-
app.get("/:id", tokenRequired, scopeRequired(["read:statuses"]), async (c) => {
379-
const owner = c.get("token").accountOwner;
380-
if (owner == null) {
381-
return c.json({ error: "This method requires an authenticated user" }, 422);
382-
}
380+
app.get("/:id", async (c) => {
381+
const token = await getAccessToken(c);
382+
const owner = token?.scopes.includes("read:statuses")
383+
? token?.accountOwner
384+
: null;
383385
const id = c.req.param("id");
384386
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
385387
const post = await db.query.posts.findFirst({
386-
where: eq(posts.id, id),
387-
with: getPostRelations(owner.id),
388+
where: and(
389+
eq(posts.id, id),
390+
owner == null
391+
? inArray(posts.visibility, ["public", "unlisted"])
392+
: undefined,
393+
),
394+
with: getPostRelations(owner?.id),
388395
});
389396
if (post == null) return c.json({ error: "Record not found" }, 404);
390397
return c.json(serializePost(post, owner, c.req.url));
@@ -461,121 +468,136 @@ app.get(
461468
},
462469
);
463470

464-
app.get(
465-
"/:id/context",
466-
tokenRequired,
467-
scopeRequired(["read:statuses"]),
468-
async (c) => {
469-
const owner = c.get("token").accountOwner;
470-
if (owner == null) {
471-
return c.json(
472-
{ error: "This method requires an authenticated user" },
473-
422,
474-
);
475-
}
476-
const id = c.req.param("id");
477-
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
478-
const post = await db.query.posts.findFirst({
479-
where: eq(posts.id, id),
480-
with: getPostRelations(owner.id),
481-
});
482-
if (post == null) return c.json({ error: "Record not found" }, 404);
483-
const ancestors: (typeof post)[] = [];
484-
let p: typeof post | undefined = post;
485-
while (p.replyTargetId != null) {
486-
p = await db.query.posts.findFirst({
487-
where: and(
488-
eq(posts.id, p.replyTargetId),
489-
notInArray(
490-
posts.accountId,
491-
db
492-
.select({ accountId: mutes.mutedAccountId })
493-
.from(mutes)
494-
.where(
495-
and(
496-
eq(mutes.accountId, owner.id),
497-
or(
498-
isNull(mutes.duration),
499-
gt(
500-
sql`${mutes.created} + ${mutes.duration}`,
501-
sql`CURRENT_TIMESTAMP`,
471+
app.get("/:id/context", async (c) => {
472+
const token = await getAccessToken(c);
473+
const owner = token?.scopes.includes("read:statuses")
474+
? token?.accountOwner
475+
: null;
476+
const id = c.req.param("id");
477+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
478+
const post = await db.query.posts.findFirst({
479+
where: and(
480+
eq(posts.id, id),
481+
owner == null
482+
? inArray(posts.visibility, ["public", "unlisted"])
483+
: undefined,
484+
),
485+
with: getPostRelations(owner?.id),
486+
});
487+
if (post == null) return c.json({ error: "Record not found" }, 404);
488+
const ancestors: (typeof post)[] = [];
489+
let p: typeof post | undefined = post;
490+
while (p.replyTargetId != null) {
491+
p = await db.query.posts.findFirst({
492+
where: and(
493+
eq(posts.id, p.replyTargetId),
494+
owner == null
495+
? inArray(posts.visibility, ["public", "unlisted"])
496+
: undefined,
497+
owner == null
498+
? undefined
499+
: notInArray(
500+
posts.accountId,
501+
db
502+
.select({ accountId: mutes.mutedAccountId })
503+
.from(mutes)
504+
.where(
505+
and(
506+
eq(mutes.accountId, owner.id),
507+
or(
508+
isNull(mutes.duration),
509+
gt(
510+
sql`${mutes.created} + ${mutes.duration}`,
511+
sql`CURRENT_TIMESTAMP`,
512+
),
502513
),
503514
),
504515
),
505-
),
506-
),
507-
notInArray(
508-
posts.accountId,
509-
db
510-
.select({ accountId: blocks.blockedAccountId })
511-
.from(blocks)
512-
.where(eq(blocks.accountId, owner.id)),
513-
),
514-
notInArray(
515-
posts.accountId,
516-
db
517-
.select({ accountId: blocks.accountId })
518-
.from(blocks)
519-
.where(eq(blocks.blockedAccountId, owner.id)),
520-
),
521-
),
522-
with: getPostRelations(owner.id),
523-
});
524-
if (p == null) break;
525-
ancestors.unshift(p);
526-
}
527-
const descendants: (typeof post)[] = [];
528-
const ps: (typeof post)[] = [post];
529-
while (true) {
530-
const p = ps.shift();
531-
if (p == null) break;
532-
const replies = await db.query.posts.findMany({
533-
where: and(
534-
eq(posts.replyTargetId, p.id),
535-
notInArray(
536-
posts.accountId,
537-
db
538-
.select({ accountId: mutes.mutedAccountId })
539-
.from(mutes)
540-
.where(
541-
and(
542-
eq(mutes.accountId, owner.id),
543-
or(
544-
isNull(mutes.duration),
545-
gt(
546-
sql`${mutes.created} + ${mutes.duration}`,
547-
sql`CURRENT_TIMESTAMP`,
516+
),
517+
owner == null
518+
? undefined
519+
: notInArray(
520+
posts.accountId,
521+
db
522+
.select({ accountId: blocks.blockedAccountId })
523+
.from(blocks)
524+
.where(eq(blocks.accountId, owner.id)),
525+
),
526+
owner == null
527+
? undefined
528+
: notInArray(
529+
posts.accountId,
530+
db
531+
.select({ accountId: blocks.accountId })
532+
.from(blocks)
533+
.where(eq(blocks.blockedAccountId, owner.id)),
534+
),
535+
),
536+
with: getPostRelations(owner?.id),
537+
});
538+
if (p == null) break;
539+
ancestors.unshift(p);
540+
}
541+
const descendants: (typeof post)[] = [];
542+
const ps: (typeof post)[] = [post];
543+
while (true) {
544+
const p = ps.shift();
545+
if (p == null) break;
546+
const replies = await db.query.posts.findMany({
547+
where: and(
548+
eq(posts.replyTargetId, p.id),
549+
owner == null
550+
? inArray(posts.visibility, ["public", "unlisted"])
551+
: undefined,
552+
owner == null
553+
? undefined
554+
: notInArray(
555+
posts.accountId,
556+
db
557+
.select({ accountId: mutes.mutedAccountId })
558+
.from(mutes)
559+
.where(
560+
and(
561+
eq(mutes.accountId, owner.id),
562+
or(
563+
isNull(mutes.duration),
564+
gt(
565+
sql`${mutes.created} + ${mutes.duration}`,
566+
sql`CURRENT_TIMESTAMP`,
567+
),
548568
),
549569
),
550570
),
551-
),
552-
),
553-
notInArray(
554-
posts.accountId,
555-
db
556-
.select({ accountId: blocks.blockedAccountId })
557-
.from(blocks)
558-
.where(eq(blocks.accountId, owner.id)),
559-
),
560-
notInArray(
561-
posts.accountId,
562-
db
563-
.select({ accountId: blocks.accountId })
564-
.from(blocks)
565-
.where(eq(blocks.blockedAccountId, owner.id)),
566-
),
567-
),
568-
with: getPostRelations(owner.id),
569-
});
570-
descendants.push(...replies);
571-
ps.push(...replies);
572-
}
573-
return c.json({
574-
ancestors: ancestors.map((p) => serializePost(p, owner, c.req.url)),
575-
descendants: descendants.map((p) => serializePost(p, owner, c.req.url)),
571+
),
572+
owner == null
573+
? undefined
574+
: notInArray(
575+
posts.accountId,
576+
db
577+
.select({ accountId: blocks.blockedAccountId })
578+
.from(blocks)
579+
.where(eq(blocks.accountId, owner.id)),
580+
),
581+
owner == null
582+
? undefined
583+
: notInArray(
584+
posts.accountId,
585+
db
586+
.select({ accountId: blocks.accountId })
587+
.from(blocks)
588+
.where(eq(blocks.blockedAccountId, owner.id)),
589+
),
590+
),
591+
with: getPostRelations(owner?.id),
576592
});
577-
},
578-
);
593+
descendants.push(...replies);
594+
ps.push(...replies);
595+
}
596+
return c.json({
597+
ancestors: ancestors.map((p) => serializePost(p, owner, c.req.url)),
598+
descendants: descendants.map((p) => serializePost(p, owner, c.req.url)),
599+
});
600+
});
579601

580602
app.post(
581603
"/:id/favourite",

src/db.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getLogger } from "@logtape/logtape";
22
import type { ExtractTablesWithRelations } from "drizzle-orm";
33
import type { Logger } from "drizzle-orm/logger";
4-
import type { PgTransaction } from "drizzle-orm/pg-core";
4+
import type { PgDatabase, PgTransaction } from "drizzle-orm/pg-core";
55
import {
66
type PostgresJsQueryResultHKT,
77
drizzle,
@@ -67,6 +67,12 @@ export const postgres = createPostgres(databaseUrl, {
6767
});
6868
export const db = drizzle(postgres, { schema, logger: new LogTapeLogger() });
6969

70+
export type Database = PgDatabase<
71+
PostgresJsQueryResultHKT,
72+
typeof schema,
73+
ExtractTablesWithRelations<typeof schema>
74+
>;
75+
7076
// This is necessary for passing a transaction into a function:
7177
export type Transaction = PgTransaction<
7278
PostgresJsQueryResultHKT,

src/entities/emoji.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ export function serializeEmoji(
2323

2424
export function serializeReaction(
2525
reaction: Reaction & { account: Account },
26-
currentAccountOwner: { id: string },
26+
currentAccountOwner: { id: string } | undefined | null,
2727
): Record<string, unknown> {
2828
const [result] = serializeReactions([reaction], currentAccountOwner);
2929
return result;
3030
}
3131

3232
export function serializeReactions(
3333
reactions: (Reaction & { account: Account })[],
34-
currentAccountOwner: { id: string },
34+
currentAccountOwner: { id: string } | undefined | null,
3535
): Record<string, unknown>[] {
3636
const result: Record<
3737
string,
@@ -49,7 +49,9 @@ export function serializeReactions(
4949
reaction.customEmoji == null
5050
? reaction.emoji
5151
: `${reaction.emoji}\n${domain}`;
52-
const me = reaction.account.id === currentAccountOwner.id;
52+
const me =
53+
currentAccountOwner != null &&
54+
reaction.account.id === currentAccountOwner.id;
5355
if (key in result) {
5456
result[key].count++;
5557
result[key].me ||= me;

src/entities/poll.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function serializePoll(
55
options: PollOption[];
66
votes: PollVote[];
77
},
8-
currentAccountOwner: { id: string },
8+
currentAccountOwner: { id: string } | undefined | null,
99
// biome-ignore lint/suspicious/noExplicitAny: JSON
1010
): Record<string, any> {
1111
return {
@@ -18,10 +18,15 @@ export function serializePoll(
1818
0,
1919
),
2020
voters_count: poll.multiple ? poll.votersCount : null,
21-
voted: poll.votes.some((v) => v.accountId === currentAccountOwner.id),
22-
own_votes: poll.votes
23-
.filter((v) => v.accountId === currentAccountOwner.id)
24-
.map((v) => v.optionIndex),
21+
voted:
22+
currentAccountOwner != null &&
23+
poll.votes.some((v) => v.accountId === currentAccountOwner.id),
24+
own_votes:
25+
currentAccountOwner == null
26+
? []
27+
: poll.votes
28+
.filter((v) => v.accountId === currentAccountOwner.id)
29+
.map((v) => v.optionIndex),
2530
options: poll.options
2631
.toSorted((a, b) => (a.index < b.index ? -1 : 1))
2732
.map(serializePollOption),

0 commit comments

Comments
 (0)