From d8e22705e60191e36d21abae7676bfeb4df8ba40 Mon Sep 17 00:00:00 2001 From: Sven Sauleau Date: Mon, 13 Feb 2023 12:13:54 +0000 Subject: [PATCH] view single tag --- backend/src/errors/index.ts | 4 ++++ backend/src/mastodon/hashtag.ts | 21 +++++++++++++++++ backend/src/types/tag.ts | 6 +++++ backend/test/mastodon/tags.spec.ts | 36 ++++++++++++++++++++++++++++++ functions/api/v1/tags/[tag].ts | 25 +++++++++++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 backend/src/types/tag.ts create mode 100644 backend/test/mastodon/tags.spec.ts create mode 100644 functions/api/v1/tags/[tag].ts diff --git a/backend/src/errors/index.ts b/backend/src/errors/index.ts index b402830..e129e78 100644 --- a/backend/src/errors/index.ts +++ b/backend/src/errors/index.ts @@ -46,6 +46,10 @@ export function mediaNotFound(id: string): Response { return generateErrorResponse('Resource not found', 404, `Media "${id}" not found`) } +export function tagNotFound(tag: string): Response { + return generateErrorResponse('Resource not found', 404, `Tag "${tag}" not found`) +} + export function exceededLimit(detail: string): Response { return generateErrorResponse('Limit exceeded', 400, detail) } diff --git a/backend/src/mastodon/hashtag.ts b/backend/src/mastodon/hashtag.ts index 8d9131d..5f8aff0 100644 --- a/backend/src/mastodon/hashtag.ts +++ b/backend/src/mastodon/hashtag.ts @@ -1,4 +1,5 @@ import type { Note } from 'wildebeest/backend/src/activitypub/objects/note' +import type { Tag } from 'wildebeest/backend/src/types/tag' export type Hashtag = string @@ -27,3 +28,23 @@ export async function insertHashtags(db: D1Database, note: Note, values: Array { + const query = ` + SELECT * FROM note_hashtags WHERE value=? + ` + const { results, success, error } = await db.prepare(query).bind(tag).all<{ value: string }>() + if (!success) { + throw new Error('SQL error: ' + error) + } + + if (!results || results.length === 0) { + return null + } + + return { + name: results[0].value, + url: new URL(`/tags/${results[0].value}`, `https://${domain}`), + history: [], + } +} diff --git a/backend/src/types/tag.ts b/backend/src/types/tag.ts new file mode 100644 index 0000000..0b44dfe --- /dev/null +++ b/backend/src/types/tag.ts @@ -0,0 +1,6 @@ +export type Tag = { + name: string + url: URL + history: Array + following?: boolean +} diff --git a/backend/test/mastodon/tags.spec.ts b/backend/test/mastodon/tags.spec.ts new file mode 100644 index 0000000..58737f1 --- /dev/null +++ b/backend/test/mastodon/tags.spec.ts @@ -0,0 +1,36 @@ +import { strict as assert } from 'node:assert/strict' +import { createPerson } from 'wildebeest/backend/src/activitypub/actors' +import { createPublicNote } from 'wildebeest/backend/src/activitypub/objects/note' +import { makeDB, assertCORS, isUrlValid } from '../utils' +import * as tag_id from 'wildebeest/functions/api/v1/tags/[tag]' +import { insertHashtags } from 'wildebeest/backend/src/mastodon/hashtag' + +const domain = 'cloudflare.com' +const userKEK = 'test_kek20' + +describe('Mastodon APIs', () => { + describe('tags', () => { + test('return 404 when non existent tag', async () => { + const db = await makeDB() + const res = await tag_id.handleRequestGet(db, domain, 'non-existent-tag') + assertCORS(res) + assert.equal(res.status, 404) + }) + + test('return tag', async () => { + const db = await makeDB() + const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + + const note = await createPublicNote(domain, db, 'my localnote status', actor) + await insertHashtags(db, note, ['test']) + + const res = await tag_id.handleRequestGet(db, domain, 'test') + assertCORS(res) + assert.equal(res.status, 200) + + const data = await res.json() + assert.equal(data.name, 'test') + assert(isUrlValid(data.url)) + }) + }) +}) diff --git a/functions/api/v1/tags/[tag].ts b/functions/api/v1/tags/[tag].ts new file mode 100644 index 0000000..dd1b65d --- /dev/null +++ b/functions/api/v1/tags/[tag].ts @@ -0,0 +1,25 @@ +// https://docs.joinmastodon.org/methods/tags/#get + +import type { ContextData } from 'wildebeest/backend/src/types/context' +import type { Env } from 'wildebeest/backend/src/types/env' +import { getTag } from 'wildebeest/backend/src/mastodon/hashtag' +import * as errors from 'wildebeest/backend/src/errors' +import { cors } from 'wildebeest/backend/src/utils/cors' + +const headers = { + ...cors(), + 'content-type': 'application/json', +} as const + +export const onRequestGet: PagesFunction = async ({ params, env, request }) => { + const domain = new URL(request.url).hostname + return handleRequestGet(env.DATABASE, domain, params.tag as string) +} + +export async function handleRequestGet(db: D1Database, domain: string, value: string): Promise { + const tag = await getTag(db, domain, value) + if (tag === null) { + return errors.tagNotFound(value) + } + return new Response(JSON.stringify(tag), { headers }) +}