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

View file

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

View file

@ -61,10 +61,12 @@
"@types/jsonld": "^1.5.13",
"@typescript-eslint/eslint-plugin": "latest",
"@unocss/cli": "latest",
"@unocss/transformer-directives": "^0.59.0",
"@vitejs/plugin-vue": "latest",
"@vueuse/head": "^2.0.0",
"activitypub-types": "^1.0.3",
"bun-types": "latest",
"shiki": "^1.2.4",
"typescript": "latest",
"unocss": "latest",
"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 class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<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>
<h1 class="font-bold text-2xl text-center tracking-tight">Login to your account</h1>
</div>

View file

@ -7,7 +7,7 @@
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<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">
<h1 class="font-bold text-2xl text-center tracking-tight">Allow this application to access your
account?</h1>

View file

@ -4,6 +4,8 @@ import authorizeVue from "./pages/oauth/authorize.vue";
import redirectVue from "./pages/oauth/redirect.vue";
import registerIndexVue from "./pages/register/index.vue";
import successVue from "./pages/register/success.vue";
import statusVue from "./pages/[username]/[uuid].vue";
import userVue from "./pages/[username]/index.vue";
export default [
{ path: "/", component: indexVue },
@ -11,4 +13,6 @@ export default [
{ path: "/oauth/redirect", component: redirectVue },
{ path: "/register", component: registerIndexVue },
{ path: "/register/success", component: successVue },
{ path: "/:username/:uuid", component: statusVue },
{ path: "/:username", component: userVue },
] 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 {
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)
authorId String @db.Uuid
createdAt DateTime @default(now())
@ -228,7 +228,7 @@ model Notification {
model User {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique
uri String? @unique
username String @unique
displayName String
password String? // Nullable

View file

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

109
server.ts
View file

@ -124,7 +124,7 @@ export const createServer = (
// Check for allowed requests
// @ts-expect-error Stupid error
if (!meta.allowedMethods.includes(req.method as string)) {
if (!meta.allowedMethods.includes(req.method)) {
return errorResponse(
`Method not allowed: allowed methods are: ${meta.allowedMethods.join(
", ",
@ -137,17 +137,15 @@ export const createServer = (
const auth = await getFromRequest(req);
// Check for authentication if required
if (meta.auth.required) {
if (!auth.user) {
return errorResponse("Unauthorized", 401);
}
} else if (
// @ts-expect-error Stupid error
(meta.auth.requiredOnMethods ?? []).includes(req.method)
if (
(meta.auth.required ||
(meta.auth.requiredOnMethods ?? []).includes(
// @ts-expect-error Stupid error
req.method,
)) &&
!auth.user
) {
if (!auth.user) {
return errorResponse("Unauthorized", 401);
}
return errorResponse("Unauthorized", 401);
}
let parsedRequest = {};
@ -172,68 +170,53 @@ export const createServer = (
},
});
}
if (matchedRoute?.name === "/[...404]" || !matchedRoute) {
if (new URL(req.url).pathname.startsWith("/api")) {
return errorResponse("Route not found", 404);
if (new URL(req.url).pathname.startsWith("/api")) {
return errorResponse("Route not found", 404);
}
// Proxy response from Vite at localhost:5173 if in development mode
if (isProd) {
let file = Bun.file("./pages/dist/index.html");
if (new URL(req.url).pathname.startsWith("/assets")) {
file = Bun.file(`./pages/dist${new URL(req.url).pathname}`);
}
// Proxy response from Vite at localhost:5173 if in development mode
if (isProd) {
if (new URL(req.url).pathname.startsWith("/assets")) {
const file = Bun.file(
`./pages/dist${new URL(req.url).pathname}`,
);
// Serve from pages/dist/assets
if (await file.exists()) {
return clientResponse(file, 200, {
"Content-Type": file.type,
});
}
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
// Serve from pages/dist/assets
if (await file.exists()) {
return clientResponse(file, 200, {
"Content-Type": file.type,
});
}
const proxy = await fetch(
req.url.replace(
return errorResponse("Asset not found", 404);
}
const proxy = await fetch(
req.url.replace(config.http.base_url, "http://localhost:5173"),
).catch(async (e) => {
await logger.logError(
LogLevel.ERROR,
"Server.Proxy",
e as Error,
);
await logger.log(
LogLevel.ERROR,
"Server.Proxy",
`The development Vite server is not running or the route is not found: ${req.url.replace(
config.http.base_url,
"http://localhost:5173",
),
).catch(async (e) => {
await logger.logError(
LogLevel.ERROR,
"Server.Proxy",
e as Error,
);
await logger.log(
LogLevel.ERROR,
"Server.Proxy",
`The development Vite server is not running or the route is not found: ${req.url.replace(
config.http.base_url,
"http://localhost:5173",
)}`,
);
return errorResponse("Route not found", 404);
});
if (
proxy.status !== 404 &&
!(await proxy.clone().text()).includes("404 Not Found")
) {
return proxy;
}
)}`,
);
return errorResponse("Route not found", 404);
});
if (
proxy.status !== 404 &&
!(await proxy.clone().text()).includes("404 Not Found")
) {
return proxy;
}
return errorResponse("Route not found", 404);
},
});

View file

@ -17,5 +17,5 @@ export const meta = applyConfig({
* Default catch-all route, returns a 404 error.
*/
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,
duration: 60,
},
route: "/auth/login",
route: "/api/auth/login",
auth: {
required: false,
},

View file

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

View file

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

View file

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

View file

@ -172,7 +172,7 @@ describe("API Tests", () => {
expect(account.statuses_count).toBe(0);
expect(account.note).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_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 () => {
const formData = new FormData();
@ -67,7 +67,7 @@ describe("POST /auth/login/", () => {
const response = await sendTestRequest(
new Request(
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,
),
{

View file

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