From bb87715f110655927809cc116ccbc1bf1a9aa5e6 Mon Sep 17 00:00:00 2001 From: Yunochi Date: Thu, 19 Dec 2024 18:58:15 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20=EB=85=B8=ED=8A=B8/=ED=88=BF?= =?UTF-8?q?=20=EA=B8=80=EC=9E=90=EC=88=98=20=EC=A0=9C=ED=95=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - https://github.com/serafuku/neo-quesdon/issues/111 --- src/app/api/_service/answer/answer-service.ts | 145 ++---------------- src/app/api/_utils/uploadNote/clampText.ts | 19 +++ src/app/api/_utils/uploadNote/mastodonToot.ts | 66 ++++++++ src/app/api/_utils/uploadNote/misskeyNote.ts | 59 +++++++ 4 files changed, 161 insertions(+), 128 deletions(-) create mode 100644 src/app/api/_utils/uploadNote/clampText.ts create mode 100644 src/app/api/_utils/uploadNote/mastodonToot.ts create mode 100644 src/app/api/_utils/uploadNote/misskeyNote.ts diff --git a/src/app/api/_service/answer/answer-service.ts b/src/app/api/_service/answer/answer-service.ts index 429eef3..3ba2e1d 100644 --- a/src/app/api/_service/answer/answer-service.ts +++ b/src/app/api/_service/answer/answer-service.ts @@ -6,7 +6,7 @@ import { Logger } from '@/utils/logger/Logger'; import type { jwtPayloadType } from '@/api/_utils/jwt/jwtPayloadType'; import { Auth, JwtPayload } from '@/api/_utils/jwt/decorator'; import { RateLimit } from '@/_service/ratelimiter/decorator'; -import { blocking, user, server, PrismaClient, answer } from '@prisma/client'; +import { blocking, PrismaClient, answer } from '@prisma/client'; import { AnswerDto, AnswerListWithProfileDto, AnswerWithProfileDto } from '@/app/_dto/answers/Answers.dto'; import { FetchAllAnswersReqDto } from '@/app/_dto/answers/fetch-all-answers.dto'; import { FetchUserAnswersDto } from '@/app/_dto/answers/fetch-user-answers.dto'; @@ -15,11 +15,12 @@ import { RedisPubSubService } from '@/_service/redis-pubsub/redis-event.service' import { AnswerDeletedEvPayload, QuestionDeletedPayload } from '@/app/_dto/websocket-event/websocket-event.dto'; import { CreateAnswerDto } from '@/app/_dto/answers/create-answer.dto'; import { profileToDto } from '@/api/_utils/profileToDto'; -import { mastodonTootAnswers, MkNoteAnswers } from '@/app'; -import { createHash } from 'crypto'; import { isString } from 'class-validator'; import RE2 from 're2'; import { NotificationService } from '@/app/api/_service/notification/notification.service'; +import { mkMisskeyNote } from '@/app/api/_utils/uploadNote/misskeyNote'; +import { mastodonToot } from '@/app/api/_utils/uploadNote/mastodonToot'; +import { clampText } from '@/app/api/_utils/uploadNote/clampText'; export class AnswerService { private static instance: AnswerService; @@ -96,30 +97,37 @@ export class AnswerService { let title; let text; if (data.nsfwedAnswer === true) { - title = `⚠️ 이 질문은 NSFW한 질문이에요! #neo_quesdon`; + title = `⚠️ 이 질문은 NSFW한 질문이에요! `; if (createdAnswer.questioner) { - text = `질문자:${createdAnswer.questioner}\nQ:${createdAnswer.question}\nA: ${createdAnswer.answer}\n#neo_quesdon ${answerUrl}`; + text = `질문자:${createdAnswer.questioner}\nQ:${createdAnswer.question}\nA: ${createdAnswer.answer}\n`; } else { - text = `Q: ${createdAnswer.question}\nA: ${createdAnswer.answer}\n#neo_quesdon ${answerUrl}`; + text = `Q: ${createdAnswer.question}\nA: ${createdAnswer.answer}\n`; } } else { - title = `Q: ${createdAnswer.question} #neo_quesdon`; + title = `Q: ${createdAnswer.question} `; if (createdAnswer.questioner) { - text = `질문자:${createdAnswer.questioner}\nA: ${createdAnswer.answer}\n#neo_quesdon ${answerUrl}`; + text = `질문자:${createdAnswer.questioner}\nA: ${createdAnswer.answer}\n `; } else { - text = `A: ${createdAnswer.answer}\n#neo_quesdon ${answerUrl}`; + text = `A: ${createdAnswer.answer}\n`; } } try { + const textEnd = `${answerUrl} #neo_quesdon`; + const titleEnd = ' #neo_quesdon'; + const more = ' ...(더보기)'; switch (server.instanceType) { case 'misskey': case 'cherrypick': + text = clampText(text, 3000, textEnd, more); + title = clampText(title, 100, titleEnd, more); await mkMisskeyNote( { user: answeredUser, server: server }, { title: title, text: text, visibility: data.visibility }, ); break; case 'mastodon': + text = clampText(text, 500, textEnd, more); + title = clampText(title, 500, textEnd, more); await mastodonToot({ user: answeredUser }, { title: title, text: text, visibility: data.visibility }); break; default: @@ -388,125 +396,6 @@ export class AnswerService { } } -async function mkMisskeyNote( - { - user, - server, - }: { - user: user; - server: server; - }, - { - title, - text, - visibility, - }: { - title: string; - text: string; - visibility: MkNoteAnswers['visibility']; - }, -) { - const NoteLogger = new Logger('mkMisskeyNote'); - // 미스키 CW길이제한 처리 - if (title.length > 100) { - title = title.substring(0, 90) + '.....'; - } - const i = createHash('sha256') - .update(user.token + server.appSecret, 'utf-8') - .digest('hex'); - const newAnswerNote: MkNoteAnswers = { - i: i, - cw: title, - text: text, - visibility: visibility, - }; - try { - const res = await fetch(`https://${user.hostName}/api/notes/create`, { - method: 'POST', - headers: { - Authorization: `Bearer ${i}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(newAnswerNote), - }); - if (res.status === 401 || res.status === 403) { - NoteLogger.warn('User Revoked Access token. JWT를 Revoke합니다... Detail:', await res.text()); - const prisma = GetPrismaClient.getClient(); - await prisma.user.update({ where: { handle: user.handle }, data: { jwtIndex: user.jwtIndex + 1 } }); - throw new Error('Note Create Fail! (Token Revoked)'); - } else if (!res.ok) { - throw new Error(`Note Create Fail! ${await res.text()}`); - } else { - NoteLogger.log(`Note Created! ${res.statusText}`); - } - } catch (err) { - NoteLogger.warn(err); - throw err; - } -} - -async function mastodonToot( - { - user, - }: { - user: user; - }, - { - title, - text, - visibility, - }: { - title: string; - text: string; - visibility: MkNoteAnswers['visibility']; - }, -) { - const tootLogger = new Logger('mastodonToot'); - let newVisibility: 'public' | 'unlisted' | 'private'; - switch (visibility) { - case 'public': - newVisibility = 'public'; - break; - case 'home': - newVisibility = 'unlisted'; - break; - case 'followers': - newVisibility = 'private'; - break; - default: - newVisibility = 'public'; - break; - } - const newAnswerToot: mastodonTootAnswers = { - spoiler_text: title, - status: text, - visibility: newVisibility, - }; - try { - const res = await fetch(`https://${user.hostName}/api/v1/statuses`, { - method: 'POST', - headers: { - Authorization: `Bearer ${user.token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(newAnswerToot), - }); - if (res.status === 401 || res.status === 403) { - tootLogger.warn('User Revoked Access token. JWT를 Revoke합니다.. Detail:', await res.text()); - const prisma = GetPrismaClient.getClient(); - await prisma.user.update({ where: { handle: user.handle }, data: { jwtIndex: user.jwtIndex + 1 } }); - throw new Error('Toot Create Fail! (Token Revoked)'); - } else if (!res.ok) { - throw new Error(`HTTP Error! status:${await res.text()}`); - } else { - tootLogger.log(`Toot Created! ${res.statusText}`); - } - } catch (err) { - tootLogger.warn(`Toot Create Fail!`, err); - throw err; - } -} - function answerEntityToDto(answer: answer): AnswerDto { const dto: AnswerDto = { id: answer.id, diff --git a/src/app/api/_utils/uploadNote/clampText.ts b/src/app/api/_utils/uploadNote/clampText.ts new file mode 100644 index 0000000..c81c643 --- /dev/null +++ b/src/app/api/_utils/uploadNote/clampText.ts @@ -0,0 +1,19 @@ +export function clampText(text: string, max_len: number, mandatoryTailString?: string, moreString?: string) { + const mandatoryLen = mandatoryTailString?.length ?? 0; + const textLen = text.length; + const moreStringLen = moreString?.length ?? 0; + if (textLen + mandatoryLen > max_len) { + if (mandatoryLen + moreStringLen > max_len) { + console.error(`Text length error! moreStringLen + moreStringLen > ${max_len}`); + throw new Error(`Text length error! moreStringLen + moreStringLen > ${max_len}`); + } + const newText = + text.substring(0, max_len - (mandatoryLen + moreStringLen)) + (moreString ?? '') + (mandatoryTailString ?? ''); + console.debug('trim string ', newText.length, newText); + return newText; + } else { + const newText = text + mandatoryTailString; + console.debug('return string ', newText.length, newText); + return newText; + } +} diff --git a/src/app/api/_utils/uploadNote/mastodonToot.ts b/src/app/api/_utils/uploadNote/mastodonToot.ts new file mode 100644 index 0000000..80cf575 --- /dev/null +++ b/src/app/api/_utils/uploadNote/mastodonToot.ts @@ -0,0 +1,66 @@ +import { mastodonTootAnswers, MkNoteAnswers } from '@/app'; +import { GetPrismaClient } from '@/app/api/_utils/getPrismaClient/get-prisma-client'; +import { Logger } from '@/utils/logger/Logger'; +import { user } from '@prisma/client'; + +export async function mastodonToot( + { + user, + }: { + user: user; + }, + { + title, + text, + visibility, + }: { + title: string; + text: string; + visibility: MkNoteAnswers['visibility']; + }, +) { + const tootLogger = new Logger('mastodonToot'); + let newVisibility: 'public' | 'unlisted' | 'private'; + switch (visibility) { + case 'public': + newVisibility = 'public'; + break; + case 'home': + newVisibility = 'unlisted'; + break; + case 'followers': + newVisibility = 'private'; + break; + default: + newVisibility = 'public'; + break; + } + const newAnswerToot: mastodonTootAnswers = { + spoiler_text: title, + status: text, + visibility: newVisibility, + }; + try { + const res = await fetch(`https://${user.hostName}/api/v1/statuses`, { + method: 'POST', + headers: { + Authorization: `Bearer ${user.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newAnswerToot), + }); + if (res.status === 401 || res.status === 403) { + tootLogger.warn('User Revoked Access token. JWT를 Revoke합니다.. Detail:', await res.text()); + const prisma = GetPrismaClient.getClient(); + await prisma.user.update({ where: { handle: user.handle }, data: { jwtIndex: user.jwtIndex + 1 } }); + throw new Error('Toot Create Fail! (Token Revoked)'); + } else if (!res.ok) { + throw new Error(`HTTP Error! status:${await res.text()}`); + } else { + tootLogger.log(`Toot Created! ${res.statusText}`); + } + } catch (err) { + tootLogger.warn(`Toot Create Fail!`, err); + throw err; + } +} diff --git a/src/app/api/_utils/uploadNote/misskeyNote.ts b/src/app/api/_utils/uploadNote/misskeyNote.ts new file mode 100644 index 0000000..6825de9 --- /dev/null +++ b/src/app/api/_utils/uploadNote/misskeyNote.ts @@ -0,0 +1,59 @@ +import { MkNoteAnswers } from '@/app'; +import { GetPrismaClient } from '@/app/api/_utils/getPrismaClient/get-prisma-client'; +import { Logger } from '@/utils/logger/Logger'; +import { createHash } from 'crypto'; +import { user, server } from '@prisma/client'; + +export async function mkMisskeyNote( + { + user, + server, + }: { + user: user; + server: server; + }, + { + title, + text, + visibility, + }: { + title: string; + text: string; + visibility: MkNoteAnswers['visibility']; + }, +) { + const NoteLogger = new Logger('mkMisskeyNote'); + + const i = createHash('sha256') + .update(user.token + server.appSecret, 'utf-8') + .digest('hex'); + const newAnswerNote: MkNoteAnswers = { + i: i, + cw: title, + text: text, + visibility: visibility, + }; + try { + const res = await fetch(`https://${user.hostName}/api/notes/create`, { + method: 'POST', + headers: { + Authorization: `Bearer ${i}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newAnswerNote), + }); + if (res.status === 401 || res.status === 403) { + NoteLogger.warn('User Revoked Access token. JWT를 Revoke합니다... Detail:', await res.text()); + const prisma = GetPrismaClient.getClient(); + await prisma.user.update({ where: { handle: user.handle }, data: { jwtIndex: user.jwtIndex + 1 } }); + throw new Error('Note Create Fail! (Token Revoked)'); + } else if (!res.ok) { + throw new Error(`Note Create Fail! ${await res.text()}`); + } else { + NoteLogger.log(`Note Created! ${res.statusText}`); + } + } catch (err) { + NoteLogger.warn(err); + throw err; + } +} From 07858f6a945364b042be2aefa9b6eac475ea9b6d Mon Sep 17 00:00:00 2001 From: Yuno Date: Thu, 19 Dec 2024 20:52:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A9=B9=20=EC=A4=84=EC=9E=84=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/_service/answer/answer-service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/_service/answer/answer-service.ts b/src/app/api/_service/answer/answer-service.ts index 3ba2e1d..7aadc44 100644 --- a/src/app/api/_service/answer/answer-service.ts +++ b/src/app/api/_service/answer/answer-service.ts @@ -112,9 +112,9 @@ export class AnswerService { } } try { - const textEnd = `${answerUrl} #neo_quesdon`; + const textEnd = ` ${answerUrl}\n#neo_quesdon`; const titleEnd = ' #neo_quesdon'; - const more = ' ...(더보기)'; + const more = '...'; switch (server.instanceType) { case 'misskey': case 'cherrypick': @@ -418,4 +418,4 @@ function isHandle(str: string | null | undefined) { return true; } return false; -} \ No newline at end of file +} From 39ad572ceb581ffad8e3f84cccb71d3e8631fd72 Mon Sep 17 00:00:00 2001 From: Yuno Date: Thu, 19 Dec 2024 21:31:38 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20=EB=A7=88=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=8F=88=20=EA=B8=B8=EC=9D=B4=EC=A0=9C=ED=95=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/_service/answer/answer-service.ts | 13 +++++++++++-- src/app/api/_utils/uploadNote/clampText.ts | 2 -- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/api/_service/answer/answer-service.ts b/src/app/api/_service/answer/answer-service.ts index 7aadc44..6759d0e 100644 --- a/src/app/api/_service/answer/answer-service.ts +++ b/src/app/api/_service/answer/answer-service.ts @@ -126,8 +126,17 @@ export class AnswerService { ); break; case 'mastodon': - text = clampText(text, 500, textEnd, more); - title = clampText(title, 500, textEnd, more); + const titleTotalLen = title.length + titleEnd.length; + const textTotalLen = text.length + textEnd.length; + const needTrim = titleTotalLen + textTotalLen > 500; + let titleMax = 500; + let textMax = 500; + if (needTrim) { + titleMax = Math.min(titleTotalLen, 200); + textMax -= titleMax; + } + title = clampText(title, titleMax, titleEnd, more); + text = clampText(text, textMax, textEnd, more); await mastodonToot({ user: answeredUser }, { title: title, text: text, visibility: data.visibility }); break; default: diff --git a/src/app/api/_utils/uploadNote/clampText.ts b/src/app/api/_utils/uploadNote/clampText.ts index c81c643..84d586d 100644 --- a/src/app/api/_utils/uploadNote/clampText.ts +++ b/src/app/api/_utils/uploadNote/clampText.ts @@ -9,11 +9,9 @@ export function clampText(text: string, max_len: number, mandatoryTailString?: s } const newText = text.substring(0, max_len - (mandatoryLen + moreStringLen)) + (moreString ?? '') + (mandatoryTailString ?? ''); - console.debug('trim string ', newText.length, newText); return newText; } else { const newText = text + mandatoryTailString; - console.debug('return string ', newText.length, newText); return newText; } }