diff --git a/README.md b/README.md index 0d513ee..38a9619 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Automatic notification bot for uwuzu - Follow back - Unfollow - Weather Repost + - Make it a quote - Startup requirements check - Check package existence - Required package version check diff --git a/examples/config.ts b/examples/config.ts index 2fad399..ecc3242 100644 --- a/examples/config.ts +++ b/examples/config.ts @@ -21,6 +21,8 @@ const config: configTypes = { weather: { splitCount: 4, // 返信の分割数 }, + // Make it a quote設定 + miq: true, // 有効/無効 // 緊急時設定 emergency: { diff --git a/miq/fonts/LICENSE b/miq/fonts/LICENSE new file mode 100644 index 0000000..927d970 --- /dev/null +++ b/miq/fonts/LICENSE @@ -0,0 +1,93 @@ +Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/miq/fonts/NotoSansJP.ttf b/miq/fonts/NotoSansJP.ttf new file mode 100644 index 0000000..4769abc Binary files /dev/null and b/miq/fonts/NotoSansJP.ttf differ diff --git a/miq/main.ts b/miq/main.ts new file mode 100644 index 0000000..3136efc --- /dev/null +++ b/miq/main.ts @@ -0,0 +1,134 @@ +import { createCanvas, loadImage, registerFont } from "canvas"; +import { writeFileSync } from "fs"; +import sharp from "sharp"; +import { MiQOptions } from "./miq"; + +function textReplace( + text: string +) { + let result = ""; + + // 改行削除 + text = text.replaceAll("\n", ""); + + // 10文字/1回で改行を追加 + let maxLength = 10; + if (text.length > maxLength) { + for (let i = 0; i < text.length; i += maxLength) { + result += text.substring(i, i + maxLength) + "\n"; + } + result = result.trimEnd(); + } + + // 80文字以上は「...」で省略 + maxLength = 80; + if (result.length > maxLength) { + result = result.substring(0, maxLength) + + "..."; + } + + return result; +} + +async function iconReplace( + color: boolean, + iconURL: string, +) { + let result = ""; + + const buffer = await(await fetch(iconURL, { + method: "GET", + cache: "no-store", + })).arrayBuffer(); + + if (color) { + const img = await sharp(Buffer.from(buffer)) + .png() + .toBuffer(); + + result = `data:image/png;base64,${img.toString("base64")}`; + } else { + const img = await sharp(Buffer.from(buffer)) + .png() + .grayscale() + .toBuffer(); + + result = `data:image/png;base64,${img.toString("base64")}`; + } + + return result; +} + +/** + * A function to generate + * Make it a quote on Node.js. +*/ +export default async function MiQ({ + type, + color, + text, + iconURL, + userName, + userID, +}: MiQOptions) { + // フォント読み込み + registerFont("miq/fonts/NotoSansJP.ttf", { family: "Noto Sans JP" }); + + // 初期化 + const canvas = createCanvas(1200, 630); + const ctx = canvas.getContext("2d"); + + // 背景描画 + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // アイコン描画 + const iconImg = await loadImage(await iconReplace( + color, + iconURL, + )); + const iconSize = canvas.height; + ctx.drawImage(iconImg, 0, 0, iconSize, iconSize); + + // ユーザー名描画 + ctx.font = "38px Noto Sans JP"; + ctx.fillStyle = "white"; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + let x = 670; + let y = 480; + ctx.fillText("-", x, y); + ctx.fillText(userName, x+40, y, canvas.width-(x+40)); + + // ユーザーID描画 + ctx.font = "28px Noto Sans JP"; + ctx.fillStyle = "#3c3c3c"; + ctx.fillText(`@${userID}`, x+120, y+50, canvas.width-(x+120)); + + // 本文描画 + ctx.font = "48px Noto Sans JP"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = "white"; + text = textReplace(text); + x = 870; + y = 30; + ctx.fillText(text, x, y+50); + + // フェード描画 + const fadeColor = "black"; + const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); + gradient.addColorStop(0, "rgba(0, 0, 0, 0)"); + gradient.addColorStop(0.5, fadeColor); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, iconSize, canvas.height); + + // 返答 + if (type === "Buffer") { + return canvas.toBuffer(); + } else if (type === "Base64") { + return canvas.toDataURL(); + } else { + return "Error: The type property is invalid."; + } +} diff --git a/miq/miq.d.ts b/miq/miq.d.ts new file mode 100644 index 0000000..04b297e --- /dev/null +++ b/miq/miq.d.ts @@ -0,0 +1,12 @@ +type MiQType = +"Buffer" | +"Base64" + +export interface MiQOptions { + type: MiQType; + color: boolean; + text: string; + iconURL: string; + userName: string; + userID: string; +} diff --git a/package.json b/package.json index 342de6c..9301da0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "notice-uwuzu", - "version": "v8.1.2@uwuzu1.6.1", - "tag": "v8.1.2", + "version": "v25.8.5@uwuzu1.6.4", + "tag": "v25.8.5", "description": "Notice Bot for uwuzu", "main": "dist/main.js", "scripts": { @@ -42,13 +42,16 @@ "@types/node": "^24.0.7", "@types/node-cron": "^3.0.11", "@types/nodemailer": "^6.4.17", + "@types/sharp": "^0.31.1", "@types/ws": "^8.18.1", + "canvas": "^3.2.0", "child_process": "^1.0.2", "date-fns": "^4.1.0", "express": "^5.1.0", "fs": "^0.0.1-security", "node-cron": "^4.1.1", "nodemailer": "^7.0.4", + "sharp": "^0.34.3", "timers": "^0.1.1", "typescript": "^5.9.2", "ws": "^8.18.3" diff --git a/scripts/commands/main.ts b/scripts/commands/main.ts index 0c68db4..b5124ca 100644 --- a/scripts/commands/main.ts +++ b/scripts/commands/main.ts @@ -6,10 +6,11 @@ const initialFile: Array = []; // コマンド読み込み import Info from "./info.js"; +import Help from "./help.js"; import Follow from "./follow.js"; import UnFollow from "./unfollow.js"; import Weather from "./weather.js"; -import Help from "./help.js"; +import MakeItAQuote from "./miq.js"; import Report from "./report.js"; import Terms from "./legal/terms.js" import PrivacyPolicy from "./legal/privacy.js"; @@ -36,6 +37,43 @@ function cutAfterChar(str: string, char: string) { return str.substring(index + 1); } +async function commandSearch(text: string) { + // /のある行を特定 + const lines = text.split(/\n/); + let slashLine: number = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i].indexOf("/") !== -1) { + slashLine = i; + } + } + + // /がない場合は無を返答 + if (slashLine === -1) { + return ""; + } + + // BOTのユーザーIDを取得 + const userid: string = (await (await fetch(`${config.uwuzu.host}/api/me/`, { + method: "POST", + cache: "no-store", + body: JSON.stringify({ + token: config.uwuzu.apiToken, + }), + })).json()).userid; + + // BOTへのメンションを削除 + let slashLineText = lines[slashLine]; + slashLineText = slashLineText.replace(`@${userid}`, ""); + + // /以降の文字を取得 + slashLineText = cutAfterChar(slashLineText, "/"); + + // 前後の空白を削除 + slashLineText = slashLineText.trimStart().trimEnd(); + + return slashLineText; +} + export async function Reply(text: string, reply: string) { const req = await fetch(`${config.uwuzu.host}/api/ueuse/create`, { method: "POST", @@ -99,16 +137,19 @@ export default async function Commands() { break; } + if (data.text.indexOf("/") === -1) { + break; + } + // コマンド処理 console.log("--------"); - const commandName = cutAfterChar(data.text, "/"); + const commandName = await commandSearch(data.text); + console.log(commandName); alreadyAdd(data.uniqid); switch (commandName) { - case "": - break; case "info": Info(data); break; @@ -133,6 +174,9 @@ export default async function Commands() { case "weather": Weather(data); break; + case "miq": + MakeItAQuote(data); + break; default: const reply = await Reply(` 不明なコマンドです。 diff --git a/scripts/commands/miq.ts b/scripts/commands/miq.ts new file mode 100644 index 0000000..2b5f2df --- /dev/null +++ b/scripts/commands/miq.ts @@ -0,0 +1,56 @@ +import { ueuse } from "../../types/types"; +import MiQ from "../../miq/main.js"; +import config from "../../config.js"; + +export default async function MakeItAQuote(data: ueuse) { + let color: boolean; + let msg: string; + + if (data.abi === "color: true") { + msg = "カラーモードでMake it a quoteを生成しました。"; + color = true; + } else if (data.abi === "color: false") { + msg = "モノクロモードでMake it a quoteを生成しました。"; + color = false; + } else { + msg = "ご指定がないためモノクロモードでMake it a quoteを生成しました。"; + color = false; + } + + const ueuseData: ueuse = (await ( + await fetch(`${config.uwuzu.host}/api/ueuse/get`, { + method: "POST", + cache: "no-store", + body: JSON.stringify({ + token: config.uwuzu.apiToken, + uniqid: data.replyid, + }), + }) + ).json())["0"]; + + console.log(ueuseData) + + const img = await MiQ({ + type: "Base64", + color: color, + text: ueuseData.text, + iconURL: ueuseData.account.user_icon, + userName: ueuseData.account.username, + userID: ueuseData.account.userid, + }); + + const req = await fetch(`${config.uwuzu.host}/api/ueuse/create`, { + method: "POST", + body: JSON.stringify({ + token: config.uwuzu.apiToken, + text: msg, + image1: img, + replyid: data.uniqid, + }), + cache: "no-store", + }); + + const res = await req.json(); + + console.log("MiQ:", res); +} diff --git a/types/config.d.ts b/types/config.d.ts index 11d8fa4..cfa91ae 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -76,6 +76,7 @@ export interface configTypes { time: timeTypes, earthquake: earthquakeTypes; weather: weatherTypes; + miq: boolean; emergency: emergencyFullTypes | emergencyMinTypes; report: reportTypes; diff --git a/types/types.d.ts b/types/types.d.ts index 85b7e6e..dd4f95d 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -25,7 +25,7 @@ export interface meApi { export interface ueuse { uniqid: string; - relpyid: string; + replyid: string; reuseid: string; text: string; account: {