Add interface to view post and user JSON data

This commit is contained in:
Jesse Wierzbinski 2024-04-08 18:33:59 -10:00
parent 342a8011f1
commit db37510370
No known key found for this signature in database
21 changed files with 248 additions and 122 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -260,7 +260,7 @@ export const createNewStatus = async (data: {
.join("\n"); .join("\n");
} }
let status = await client.status.create({ const status = await client.status.create({
data: { data: {
authorId: data.account.id, authorId: data.account.id,
applicationId: data.application?.id, applicationId: data.application?.id,
@ -290,12 +290,7 @@ export const createNewStatus = async (data: {
quotingPostId: data.quote?.id, quotingPostId: data.quote?.id,
instanceId: data.account.instanceId || undefined, instanceId: data.account.instanceId || undefined,
isReblog: false, isReblog: false,
uri: uri: data.uri || null,
data.uri ||
new URL(
`/statuses/FAKE-${crypto.randomUUID()}`,
config.http.base_url,
).toString(),
mentions: { mentions: {
connect: mentions.map((mention) => { connect: mentions.map((mention) => {
return { return {
@ -307,22 +302,6 @@ export const createNewStatus = async (data: {
include: statusAndUserRelations, include: statusAndUserRelations,
}); });
// Update URI
status = await client.status.update({
where: {
id: status.id,
},
data: {
uri:
data.uri ||
new URL(
`/statuses/${status.id}`,
config.http.base_url,
).toString(),
},
include: statusAndUserRelations,
});
// Create notification // Create notification
if (status.inReplyToPost) { if (status.inReplyToPost) {
await client.notification.create({ await client.notification.create({
@ -503,9 +482,19 @@ export const statusToAPI = async (
sensitive: status.sensitive, sensitive: status.sensitive,
spoiler_text: status.spoilerText, spoiler_text: status.spoilerText,
tags: [], tags: [],
uri: new URL(`/statuses/${status.id}`, config.http.base_url).toString(), uri:
status.uri ||
new URL(
`/@${status.author.username}/${status.id}`,
config.http.base_url,
).toString(),
visibility: "public", visibility: "public",
url: new URL(`/statuses/${status.id}`, config.http.base_url).toString(), url:
status.uri ||
new URL(
`/@${status.author.username}/${status.id}`,
config.http.base_url,
).toString(),
bookmarked: false, bookmarked: false,
quote: status.quotingPost quote: status.quotingPost
? await statusToAPI( ? await statusToAPI(
@ -584,8 +573,8 @@ export const statusToLysand = (status: StatusWithRelations): Note => {
// TODO: Add attachments // TODO: Add attachments
attachments: [], attachments: [],
is_sensitive: status.sensitive, is_sensitive: status.sensitive,
mentions: status.mentions.map((mention) => mention.uri), mentions: status.mentions.map((mention) => mention.uri || ""),
quotes: status.quotingPost ? [status.quotingPost.uri] : [], quotes: status.quotingPost ? [status.quotingPost.uri || ""] : [],
replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [], replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [],
subject: status.spoilerText, subject: status.spoilerText,
extensions: { extensions: {

View file

@ -262,7 +262,6 @@ export const createNewLocalUser = async (data: {
avatar: data.avatar ?? config.defaults.avatar, avatar: data.avatar ?? config.defaults.avatar,
header: data.header ?? config.defaults.avatar, header: data.header ?? config.defaults.avatar,
isAdmin: data.admin ?? false, isAdmin: data.admin ?? false,
uri: "",
publicKey: keys.public_key, publicKey: keys.public_key,
privateKey: keys.private_key, privateKey: keys.private_key,
source: { source: {
@ -273,20 +272,13 @@ export const createNewLocalUser = async (data: {
fields: [], fields: [],
}, },
}, },
include: userRelations,
}); });
// Add to Meilisearch // Add to Meilisearch
await addUserToMeilisearch(user); await addUserToMeilisearch(user);
return await client.user.update({ return user;
where: {
id: user.id,
},
data: {
uri: new URL(`/users/${user.id}`, config.http.base_url).toString(),
},
include: userRelations,
});
}; };
/** /**
@ -408,7 +400,9 @@ export const userToAPI = (
username: user.username, username: user.username,
display_name: user.displayName, display_name: user.displayName,
note: user.note, note: user.note,
url: user.uri, url:
user.uri ||
new URL(`/@${user.username}`, config.http.base_url).toString(),
avatar: getAvatarUrl(user, config), avatar: getAvatarUrl(user, config),
header: getHeaderUrl(user, config), header: getHeaderUrl(user, config),
locked: user.isLocked, locked: user.isLocked,
@ -458,7 +452,7 @@ export const userToLysand = (user: UserWithRelations): LysandUser => {
return { return {
id: user.id, id: user.id,
type: "User", type: "User",
uri: user.uri, uri: user.uri || "",
bio: [ bio: [
{ {
content: user.note, content: user.note,

View file

@ -61,10 +61,12 @@
"@types/jsonld": "^1.5.13", "@types/jsonld": "^1.5.13",
"@typescript-eslint/eslint-plugin": "latest", "@typescript-eslint/eslint-plugin": "latest",
"@unocss/cli": "latest", "@unocss/cli": "latest",
"@unocss/transformer-directives": "^0.59.0",
"@vitejs/plugin-vue": "latest", "@vitejs/plugin-vue": "latest",
"@vueuse/head": "^2.0.0", "@vueuse/head": "^2.0.0",
"activitypub-types": "^1.0.3", "activitypub-types": "^1.0.3",
"bun-types": "latest", "bun-types": "latest",
"shiki": "^1.2.4",
"typescript": "latest", "typescript": "latest",
"unocss": "latest", "unocss": "latest",
"untyped": "^1.4.2", "untyped": "^1.4.2",

View file

@ -0,0 +1,65 @@
<template>
<div class="flex min-h-screen flex-col justify-center px-6 py-12 lg:px-8 relative">
<div class="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true">
<div class="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-30 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]"
style="clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)" />
</div>
<div class="code" v-html="code">
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
import { getHighlighterCore } from "shiki/core";
import getWasm from "shiki/wasm";
const location = window.location;
const route = useRoute();
const data = await fetch(
new URL(`/api/v1/statuses/${route.params.uuid}`, location.origin),
{
headers: {
Accept: "application/json",
},
},
)
.then((res) => res.json())
.catch(() => ({
error: "Failed to fetch status (it probably doesn't exist)",
}));
const highlighter = await getHighlighterCore({
themes: [import("shiki/themes/rose-pine.mjs")],
langs: [import("shiki/langs/json.mjs")],
loadWasm: getWasm,
});
const code = highlighter.codeToHtml(JSON.stringify(data, null, 4), {
lang: "json",
theme: "rose-pine",
});
</script>
<style>
.code pre:has(code) {
word-wrap: normal;
background: transparent;
-webkit-hyphens: none;
hyphens: none;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
white-space: pre;
word-break: normal;
word-spacing: normal;
overflow-x: auto;
--at-apply: "ring-1 ring-white/10 mt-4 bg-white/5 px-4 py-3 rounded";
}
.code pre code {
--at-apply: "block p-0";
}
</style>

View file

@ -0,0 +1,82 @@
<template>
<div class="flex min-h-screen flex-col justify-center px-6 py-12 lg:px-8 relative">
<div class="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true">
<div class="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36.125rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-30 sm:left-[calc(50%-30rem)] sm:w-[72.1875rem]"
style="clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)" />
</div>
<div class="code" v-html="code">
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
import { getHighlighterCore } from "shiki/core";
import getWasm from "shiki/wasm";
const location = window.location;
const route = useRoute();
const username = (route.params.username as string).replace("@", "");
const id = await fetch(
new URL(`/api/v1/accounts/search?q=${username}`, location.origin),
{
headers: {
Accept: "application/json",
},
},
)
.then((res) => res.json())
.catch(() => null);
let data = null;
if (id && id.length > 0) {
data = await fetch(
new URL(`/api/v1/accounts/${id[0].id}`, location.origin),
{
headers: {
Accept: "application/json",
},
},
)
.then((res) => res.json())
.catch(() => ({
error: "Failed to fetch user (it probably doesn't exist)",
}));
}
const highlighter = await getHighlighterCore({
themes: [import("shiki/themes/rose-pine.mjs")],
langs: [import("shiki/langs/json.mjs")],
loadWasm: getWasm,
});
const code = highlighter.codeToHtml(JSON.stringify(data, null, 4), {
lang: "json",
theme: "rose-pine",
});
</script>
<style>
.code pre:has(code) {
word-wrap: normal;
background: transparent;
-webkit-hyphens: none;
hyphens: none;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
white-space: pre;
word-break: normal;
word-spacing: normal;
overflow-x: auto;
--at-apply: "ring-1 ring-white/10 mt-4 bg-white/5 px-4 py-3 rounded";
}
.code pre code {
--at-apply: "block p-0";
}
</style>

View file

@ -7,7 +7,7 @@
</div> </div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" method="POST" <form class="space-y-6" method="POST"
:action="`/auth/login?redirect_uri=${redirect_uri}&response_type=${response_type}&client_id=${client_id}&scope=${scope}`"> :action="`/api/auth/login?redirect_uri=${redirect_uri}&response_type=${response_type}&client_id=${client_id}&scope=${scope}`">
<div> <div>
<h1 class="font-bold text-2xl text-center tracking-tight">Login to your account</h1> <h1 class="font-bold text-2xl text-center tracking-tight">Login to your account</h1>
</div> </div>

View file

@ -7,7 +7,7 @@
</div> </div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" method="POST" <form class="space-y-6" method="POST"
:action="`/auth/redirect?redirect_uri=${encodeURIComponent(redirect_uri)}&client_id=${client_id}&code=${code}`"> :action="`/api/auth/redirect?redirect_uri=${encodeURIComponent(redirect_uri)}&client_id=${client_id}&code=${code}`">
<div class="flex flex-col items-center gap-y-5"> <div class="flex flex-col items-center gap-y-5">
<h1 class="font-bold text-2xl text-center tracking-tight">Allow this application to access your <h1 class="font-bold text-2xl text-center tracking-tight">Allow this application to access your
account?</h1> account?</h1>

View file

@ -4,6 +4,8 @@ import authorizeVue from "./pages/oauth/authorize.vue";
import redirectVue from "./pages/oauth/redirect.vue"; import redirectVue from "./pages/oauth/redirect.vue";
import registerIndexVue from "./pages/register/index.vue"; import registerIndexVue from "./pages/register/index.vue";
import successVue from "./pages/register/success.vue"; import successVue from "./pages/register/success.vue";
import statusVue from "./pages/[username]/[uuid].vue";
import userVue from "./pages/[username]/index.vue";
export default [ export default [
{ path: "/", component: indexVue }, { path: "/", component: indexVue },
@ -11,4 +13,6 @@ export default [
{ path: "/oauth/redirect", component: redirectVue }, { path: "/oauth/redirect", component: redirectVue },
{ path: "/register", component: registerIndexVue }, { path: "/register", component: registerIndexVue },
{ path: "/register/success", component: successVue }, { path: "/register/success", component: successVue },
{ path: "/:username/:uuid", component: statusVue },
{ path: "/:username", component: userVue },
] as RouteRecordRaw[]; ] as RouteRecordRaw[];

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Status" ALTER COLUMN "uri" DROP NOT NULL;
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "uri" DROP NOT NULL;

View file

@ -104,7 +104,7 @@ model Relationship {
model Status { model Status {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique uri String? @unique
author User @relation("UserStatuses", fields: [authorId], references: [id], onDelete: Cascade) author User @relation("UserStatuses", fields: [authorId], references: [id], onDelete: Cascade)
authorId String @db.Uuid authorId String @db.Uuid
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -228,7 +228,7 @@ model Notification {
model User { model User {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique uri String? @unique
username String @unique username String @unique
displayName String displayName String
password String? // Nullable password String? // Nullable

View file

@ -33,8 +33,8 @@ export const rawRoutes = {
"/api/v1/timelines/public": "./server/api/api/v1/timelines/public", "/api/v1/timelines/public": "./server/api/api/v1/timelines/public",
"/api/v2/media": "./server/api/api/v2/media/index", "/api/v2/media": "./server/api/api/v2/media/index",
"/api/v2/search": "./server/api/api/v2/search/index", "/api/v2/search": "./server/api/api/v2/search/index",
"/auth/login": "./server/api/auth/login/index", "/api/auth/login": "./server/api/api/auth/login/index",
"/auth/redirect": "./server/api/auth/redirect/index", "/api/auth/redirect": "./server/api/api/auth/redirect/index",
"/nodeinfo/2.0": "./server/api/nodeinfo/2.0/index", "/nodeinfo/2.0": "./server/api/nodeinfo/2.0/index",
"/oauth/authorize-external": "./server/api/oauth/authorize-external/index", "/oauth/authorize-external": "./server/api/oauth/authorize-external/index",
"/oauth/providers": "./server/api/oauth/providers/index", "/oauth/providers": "./server/api/oauth/providers/index",

View file

@ -124,7 +124,7 @@ export const createServer = (
// Check for allowed requests // Check for allowed requests
// @ts-expect-error Stupid error // @ts-expect-error Stupid error
if (!meta.allowedMethods.includes(req.method as string)) { if (!meta.allowedMethods.includes(req.method)) {
return errorResponse( return errorResponse(
`Method not allowed: allowed methods are: ${meta.allowedMethods.join( `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
", ", ", ",
@ -137,18 +137,16 @@ export const createServer = (
const auth = await getFromRequest(req); const auth = await getFromRequest(req);
// Check for authentication if required // Check for authentication if required
if (meta.auth.required) { if (
if (!auth.user) { (meta.auth.required ||
return errorResponse("Unauthorized", 401); (meta.auth.requiredOnMethods ?? []).includes(
}
} else if (
// @ts-expect-error Stupid error // @ts-expect-error Stupid error
(meta.auth.requiredOnMethods ?? []).includes(req.method) req.method,
)) &&
!auth.user
) { ) {
if (!auth.user) {
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
}
let parsedRequest = {}; let parsedRequest = {};
@ -172,17 +170,17 @@ export const createServer = (
}, },
}); });
} }
if (matchedRoute?.name === "/[...404]" || !matchedRoute) {
if (new URL(req.url).pathname.startsWith("/api")) { if (new URL(req.url).pathname.startsWith("/api")) {
return errorResponse("Route not found", 404); return errorResponse("Route not found", 404);
} }
// Proxy response from Vite at localhost:5173 if in development mode // Proxy response from Vite at localhost:5173 if in development mode
if (isProd) { if (isProd) {
let file = Bun.file("./pages/dist/index.html");
if (new URL(req.url).pathname.startsWith("/assets")) { if (new URL(req.url).pathname.startsWith("/assets")) {
const file = Bun.file( file = Bun.file(`./pages/dist${new URL(req.url).pathname}`);
`./pages/dist${new URL(req.url).pathname}`, }
);
// Serve from pages/dist/assets // Serve from pages/dist/assets
if (await file.exists()) { if (await file.exists()) {
@ -192,22 +190,9 @@ export const createServer = (
} }
return errorResponse("Asset not found", 404); return errorResponse("Asset not found", 404);
} }
if (new URL(req.url).pathname.startsWith("/api")) {
return errorResponse("Route not found", 404);
}
const file = Bun.file("./pages/dist/index.html");
// Serve from pages/dist
return clientResponse(file, 200, {
"Content-Type": file.type,
});
}
const proxy = await fetch( const proxy = await fetch(
req.url.replace( req.url.replace(config.http.base_url, "http://localhost:5173"),
config.http.base_url,
"http://localhost:5173",
),
).catch(async (e) => { ).catch(async (e) => {
await logger.logError( await logger.logError(
LogLevel.ERROR, LogLevel.ERROR,
@ -232,8 +217,6 @@ export const createServer = (
return proxy; return proxy;
} }
return errorResponse("Route not found", 404);
}
return errorResponse("Route not found", 404); return errorResponse("Route not found", 404);
}, },
}); });

View file

@ -17,5 +17,5 @@ export const meta = applyConfig({
* Default catch-all route, returns a 404 error. * Default catch-all route, returns a 404 error.
*/ */
export default apiRoute(() => { export default apiRoute(() => {
return errorResponse("This API route does not exist", 404); return errorResponse("Route not found", 404);
}); });

View file

@ -10,7 +10,7 @@ export const meta = applyConfig({
max: 4, max: 4,
duration: 60, duration: 60,
}, },
route: "/auth/login", route: "/api/auth/login",
auth: { auth: {
required: false, required: false,
}, },

View file

@ -8,7 +8,7 @@ export const meta = applyConfig({
max: 4, max: 4,
duration: 60, duration: 60,
}, },
route: "/auth/redirect", route: "/api/auth/redirect",
auth: { auth: {
required: false, required: false,
}, },

View file

@ -13,7 +13,7 @@ export const meta = applyConfig({
}, },
route: "/api/v1/accounts/:id", route: "/api/v1/accounts/:id",
auth: { auth: {
required: true, required: false,
oauthPermissions: [], oauthPermissions: [],
}, },
}); });

View file

@ -12,7 +12,7 @@ export const meta = applyConfig({
duration: 60, duration: 60,
}, },
auth: { auth: {
required: true, required: false,
oauthPermissions: ["read:accounts"], oauthPermissions: ["read:accounts"],
}, },
}); });
@ -25,11 +25,6 @@ export default apiRoute<{
following?: boolean; following?: boolean;
}>(async (req, matchedRoute, extraData) => { }>(async (req, matchedRoute, extraData) => {
// TODO: Add checks for disabled or not email verified accounts // TODO: Add checks for disabled or not email verified accounts
const { user } = extraData.auth;
if (!user) return errorResponse("Unauthorized", 401);
const { const {
following = false, following = false,
limit = 40, limit = 40,
@ -37,6 +32,10 @@ export default apiRoute<{
q, q,
} = extraData.parsedRequest; } = extraData.parsedRequest;
const { user } = extraData.auth;
if (!user && following) return errorResponse("Unauthorized", 401);
if (limit < 1 || limit > 80) { if (limit < 1 || limit > 80) {
return errorResponse("Limit must be between 1 and 80", 400); return errorResponse("Limit must be between 1 and 80", 400);
} }
@ -60,7 +59,7 @@ export default apiRoute<{
relationshipSubjects: following relationshipSubjects: following
? { ? {
some: { some: {
ownerId: user.id, ownerId: user?.id,
following, following,
}, },
} }

View file

@ -172,7 +172,7 @@ describe("API Tests", () => {
expect(account.statuses_count).toBe(0); expect(account.statuses_count).toBe(0);
expect(account.note).toBe(""); expect(account.note).toBe("");
expect(account.url).toBe( expect(account.url).toBe(
new URL(`/users/${user.id}`, config.http.base_url).toString(), new URL(`/@${user.username}`, config.http.base_url).toString(),
); );
expect(account.avatar).toBeDefined(); expect(account.avatar).toBeDefined();
expect(account.avatar_static).toBeDefined(); expect(account.avatar_static).toBeDefined();

View file

@ -57,7 +57,7 @@ describe("POST /api/v1/apps/", () => {
}); });
}); });
describe("POST /auth/login/", () => { describe("POST /api/auth/login/", () => {
test("should get a code", async () => { test("should get a code", async () => {
const formData = new FormData(); const formData = new FormData();
@ -67,7 +67,7 @@ describe("POST /auth/login/", () => {
const response = await sendTestRequest( const response = await sendTestRequest(
new Request( new Request(
wrapRelativeUrl( wrapRelativeUrl(
`/auth/login/?client_id=${client_id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, `/api/auth/login/?client_id=${client_id}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
base_url, base_url,
), ),
{ {

View file

@ -1,4 +1,6 @@
import { presetForms } from "@julr/unocss-preset-forms"; import { presetForms } from "@julr/unocss-preset-forms";
import transformerDirectives from "@unocss/transformer-directives";
import { import {
defineConfig, defineConfig,
presetTypography, presetTypography,
@ -19,4 +21,5 @@ export default defineConfig({
presetWebFonts(), presetWebFonts(),
presetForms(), presetForms(),
], ],
transformers: [transformerDirectives()],
}); });