From 5110b4992a4ffcd70e180aea8b1f89fa781ee51c Mon Sep 17 00:00:00 2001 From: Sven Sauleau Date: Mon, 13 Feb 2023 11:32:51 +0000 Subject: [PATCH] introduce basic for hashtag This change detects hashtags in statuses and insert them into a new table `note_hashtags`. --- backend/src/activitypub/objects/index.ts | 1 + backend/src/mastodon/hashtag.ts | 29 ++++++++++++++++++++ backend/test/mastodon/statuses.spec.ts | 34 ++++++++++++++++++++++++ functions/api/v1/statuses.ts | 5 ++++ migrations/0006_add_note_hashtags.sql | 9 +++++++ 5 files changed, 78 insertions(+) create mode 100644 backend/src/mastodon/hashtag.ts create mode 100644 migrations/0006_add_note_hashtags.sql diff --git a/backend/src/activitypub/objects/index.ts b/backend/src/activitypub/objects/index.ts index d42edc6..6c6ac87 100644 --- a/backend/src/activitypub/objects/index.ts +++ b/backend/src/activitypub/objects/index.ts @@ -299,6 +299,7 @@ export async function deleteObject(db: D1Database, note: T) db.prepare('DELETE FROM actor_reblogs WHERE object_id=?').bind(nodeId), db.prepare('DELETE FROM actor_replies WHERE object_id=?1 OR in_reply_to_object_id=?1').bind(nodeId), db.prepare('DELETE FROM idempotency_keys WHERE object_id=?').bind(nodeId), + db.prepare('DELETE FROM note_hashtags WHERE object_id=?').bind(nodeId), db.prepare('DELETE FROM objects WHERE id=?').bind(nodeId), ] diff --git a/backend/src/mastodon/hashtag.ts b/backend/src/mastodon/hashtag.ts new file mode 100644 index 0000000..8d9131d --- /dev/null +++ b/backend/src/mastodon/hashtag.ts @@ -0,0 +1,29 @@ +import type { Note } from 'wildebeest/backend/src/activitypub/objects/note' + +export type Hashtag = string + +const HASHTAG_RE = /#([\S]+)/g + +export function getHashtags(input: string): Array { + const matches = input.matchAll(HASHTAG_RE) + if (matches === null) { + return [] + } + + return [...matches].map((match) => match[1]) +} + +export async function insertHashtags(db: D1Database, note: Note, values: Array): Promise { + const queries = [] + const stmt = db.prepare(` + INSERT INTO note_hashtags (value, object_id) + VALUES (?, ?) + `) + + for (let i = 0, len = values.length; i < len; i++) { + const value = values[i] + queries.push(stmt.bind(value, note.id.toString())) + } + + await db.batch(queries) +} diff --git a/backend/test/mastodon/statuses.spec.ts b/backend/test/mastodon/statuses.spec.ts index 639f694..f10ab64 100644 --- a/backend/test/mastodon/statuses.spec.ts +++ b/backend/test/mastodon/statuses.spec.ts @@ -966,5 +966,39 @@ describe('Mastodon APIs', () => { assert.equal(row.count, 1) } }) + + test('hashtag in status adds in note_hashtags table', async () => { + const db = await makeDB() + const queue = makeQueue() + const actor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com') + + const body = { + status: 'hey #hi #car', + visibility: 'public', + } + const req = new Request('https://example.com', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }) + + const res = await statuses.handleRequest(req, db, actor, userKEK, queue, cache) + assert.equal(res.status, 200) + + const data = await res.json() + + const { results, success } = await db + .prepare('SELECT value, object_id FROM note_hashtags') + .all<{ value: string; object_id: string }>() + assert(success) + assert(results) + assert.equal(results!.length, 2) + assert.equal(results![0].value, 'hi') + assert.equal(results![1].value, 'car') + + const note = (await getObjectByMastodonId(db, data.id)) as unknown as Note + assert.equal(results![0].object_id, note.id.toString()) + assert.equal(results![1].object_id, note.id.toString()) + }) }) }) diff --git a/functions/api/v1/statuses.ts b/functions/api/v1/statuses.ts index 8253d7a..28d6846 100644 --- a/functions/api/v1/statuses.ts +++ b/functions/api/v1/statuses.ts @@ -9,6 +9,7 @@ import type { Queue, DeliverMessageBody } from 'wildebeest/backend/src/types/que import type { Document } from 'wildebeest/backend/src/activitypub/objects' import { getObjectByMastodonId } from 'wildebeest/backend/src/activitypub/objects' import { createStatus, getMentions } from 'wildebeest/backend/src/mastodon/status' +import { getHashtags, insertHashtags } from 'wildebeest/backend/src/mastodon/hashtag' import * as activities from 'wildebeest/backend/src/activitypub/activities/create' import type { Env } from 'wildebeest/backend/src/types/env' import type { ContextData } from 'wildebeest/backend/src/types/context' @@ -104,6 +105,8 @@ export async function handleRequest( extraProperties.inReplyTo = inReplyToObject[originalObjectIdSymbol] || inReplyToObject.id.toString() } + const hashtags = getHashtags(body.status) + const content = enrichStatus(body.status) const mentions = await getMentions(body.status, domain) if (mentions.length > 0) { @@ -112,6 +115,8 @@ export async function handleRequest( const note = await createStatus(domain, db, connectedActor, content, mediaAttachments, extraProperties) + await insertHashtags(db, note, hashtags) + if (inReplyToObject !== null) { // after the status has been created, record the reply. await insertReply(db, connectedActor, note, inReplyToObject) diff --git a/migrations/0006_add_note_hashtags.sql b/migrations/0006_add_note_hashtags.sql new file mode 100644 index 0000000..4f27b31 --- /dev/null +++ b/migrations/0006_add_note_hashtags.sql @@ -0,0 +1,9 @@ +-- Migration number: 0006 2023-02-13T11:18:03.485Z + +CREATE TABLE IF NOT EXISTS note_hashtags ( + value TEXT NOT NULL, + object_id TEXT NOT NULL, + cdate DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), + + FOREIGN KEY(object_id) REFERENCES objects(id) +);