Compare commits

...

24 Commits

Author SHA1 Message Date
last2014 4e2f587681 Merge pull request 'v25.12.0-alpha.1' (#5) from config-change-251130 into main
Reviewed-on: #5
2026-01-01 11:07:16 +00:00
last2014 83857ce102 25.12.0-alpha.1全部やっちゃった 2026-01-01 20:06:20 +09:00
last2014 c9d2da5aba Merge pull request '25.12.0-alpha.0' (#3) from alias-import-20251130 into main
Reviewed-on: #3
2025-11-30 08:45:10 +00:00
last2014 0ee2728590 25.12.0-alpha.0 2025-11-30 17:44:44 +09:00
last2014 a53f8f3023 フォルダ構成だけ変えた 2025-11-30 16:21:11 +09:00
last2014 a1a6ef40b8 v25.8.11 2025-08-27 01:29:40 +09:00
last2014 81e9bea8ab v25.8.10 2025-08-27 01:21:23 +09:00
last2014 b9a4fe8763 v25.8.9 2025-08-26 22:54:56 +09:00
last2014 ed3606778e v25.8.8 2025-08-26 22:43:07 +09:00
last2014 fb3e233244 v25.8.7 2025-08-26 22:29:05 +09:00
last2014 ca96f83a44 Git LFS廃止 2025-08-25 16:51:05 +09:00
last2014 97af8da4dd v25.8.6 2025-08-25 16:30:46 +09:00
last2014 a83054f5ce Git LFS 2025-08-25 06:15:14 +09:00
last2014 bbf2d66b57 v25.8.5@uwuzu1.6.4 2025-08-25 06:11:36 +09:00
last2014 db719b8312 v8.1.2 2025-08-08 21:34:30 +09:00
last2014 2966eee889 v8.1.1 2025-08-08 21:31:43 +09:00
last2014 db5e174dd4 v8.1 2025-08-08 21:29:19 +09:00
last2014 2b53b8cd3d v8.0.2 2025-08-07 08:56:49 +09:00
last2014 d20f630e1b v8.0.1 2025-08-07 08:47:11 +09:00
last2014 644ba912b1 v8.0 2025-08-06 21:27:29 +09:00
last2014 1ff4b2c429 Merge branch 'main' of https://gitea.last2014.com/last2014/noticeUwuzu 2025-08-05 20:01:36 +09:00
last2014 0a3bb241a6 v7.5 2025-08-05 19:58:09 +09:00
last2014 f14999be13 デバッグ用コード削除 2025-08-04 23:45:52 +09:00
last2014 6ec9831ed4 v7.3@uwuzu1.5.4をリリース 2025-08-04 20:08:23 +09:00
75 changed files with 5409 additions and 490 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
/dist/ /dist/
/.env* /.env*
/node_modules/ /node_modules/
/package-lock.json
/config.ts /config.ts
/config/**
!/config/example.config.yaml
+36
View File
@@ -0,0 +1,36 @@
# 25.12.0-alpha.1
- New: configをTypeScriptファイルからyamlへ移行しました
- Note: 新しいファイルパスはconfig/config.yamlです
- New: それに伴って複数の依存関係が追加されました
- Delete: また、examples/config.tsを削除しました
- Note: ただし、互換性のためgitignoreは維持しています
- New: package-lock.jsonをgitignoreから削除しました
- New: import.meta.dirnameによるパスの安全性を確保しました
- New: /miqのエイリアスを作成しました
- Chg: tsconfig.json:20に末尾カンマを追加し統一しました
- Fix: earthquakeNotice.tsで条件分岐が誤っていた問題を修正しました
- Delete: 余分な型定義を削除しました
- Delete: tsc-aliasによるresolveFullPathsが実装されているにもかかわらず.jsを使用したimportの.jsを削除しました
# 25.12.0-alpha.0
- New: リリースノートを記録する方針を取り始めました
- Change: tsc-aliasによるエイリアスインポートを導入しました
- New: resolveFullPathsが有効です
- Del: それに伴いインポートから末尾の.jsを削除しました
- New: エイリアスに`@/`(`./src`)が追加されました
- Change: config.d.tsの名称をconfigTypeからConfigへ変更しました
- Change: config.d.tsの余分なinterface定義を1つのConfigとして再定義しました
- Change: moduleResolutionをnodeからbundlerへ変更しました
- Change: typeRootのsrc/typesを./src/typesへ変更しました
- Change: 同一ファイルでの使用数が1つのみのインポートを静的インポートから動的インポートへ変更しました
- Change: ディレクトリ構造を大幅に変更しました
- Migrate: /src/wiatherId.tsを/src/constantsへ移動しました
- Migrate: /scriptを/srcへ移動しました
- Migrate: /typesを/src/typesへ移動しました
- Migrate: /src/mailer.tsを/src/libへ移動しました
- Migrate: /checksを/src/scriptsへ移動しました
- Change: eventdayData.tsを単一のexport文のみに短縮しました
- Delete: asciiart.txtの末尾に含まれている改行を削除しました
- Delete: それに伴いasciiart.tsの改行を削除する正規表現を削除しました
- Delete: package.jsonのscriptsからmainを削除しました
- Delete: successExit.tsに含まれていたデバッグ用の関数を即時実行するコードを削除しました
+1
View File
@@ -19,6 +19,7 @@ Automatic notification bot for uwuzu
- Follow back - Follow back
- Unfollow - Unfollow
- Weather Repost - Weather Repost
- Make it a quote
- Startup requirements check - Startup requirements check
- Check package existence - Check package existence
- Required package version check - Required package version check
-9
View File
@@ -1,9 +0,0 @@
import * as fs from "fs";
import { styleText } from "util";
export default function ConfigCheck() {
if (!fs.existsSync("config.ts")) {
console.log(styleText("red", "config.tsがありません"));
process.exit();
}
}
-13
View File
@@ -1,13 +0,0 @@
import PackagesIsExist from "./packagesExist.js";
import PackagesCheck from "./packages.js";
import ConfigCheck from "./config.js";
import APICheck from "./api.js";
import VersionCheck from "./version.js";
export default async function Check() {
PackagesIsExist();
PackagesCheck();
ConfigCheck();
await APICheck();
await VersionCheck();
}
+57
View File
@@ -0,0 +1,57 @@
time:
# 時報休止期間
stopTimes:
start: 23 # 開始
stop: 6 # 停止
# 地震速報設定
earthquake:
reconnectTimes: 5000 # 再接続時間(ミリ秒)
websocketUrl: "wss://api.p2pquake.net/v2/ws" # WebSocketのURL
areasCsvUrl: "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv" # 対象地域CSVファイルのURL
maxScaleMin: 30 # 地震発生の際の最低震度(10-70)
# 天気お知らせ設定
weather:
splitCount: 4 # 返信の分割数
# Make it a quote設定
miq: true # 有効/無効
# 緊急時設定
emergency:
isEnabled: true # 緊急時のコンソール表示
mail:
isEnabled: true # 緊急時のメール送信
host: "smtp.example.com" # SMTPサーバー
port: 465 # SMTPポート
user: "mailUser@example.com" # BOTメール送信元
password: "mailPassword" # SMTPパスワード
secure: false # SMTPsecure設定
to: "admin@noticeuwuzu.example.com" # 緊急時メール送信先(配列可)
# /report設定
report:
isEnabled: true # 有効/無効
message: "" # 報告者へのメッセージ
# 規約等
legal:
terms: | # 利用規約
利用規約
privacy: | # プライバシーポリシー
プライバシーポリシー
# 管理者情報設定
admin:
name: "あどみん" # BOT管理者名
showMail: false # メールアドレスを公開するか(false:非公開/文字列:メールアドレス)
panel: # 管理パネル
isEnabled: true # 有効/無効
port: 74919 # 配信ポート
# uwuzuサーバー設定
uwuzu:
apiToken: "TOKEN_EXAMPLE" # APIトークン
host: "https://uwuzu.example.com" # サーバーホスト
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
-45
View File
@@ -1,45 +0,0 @@
import type { configTypes } from "types/config";
// READMEの設定項目を参照
const config: configTypes = {
// 時報設定
time: {
// 時報休止期間
stopTimes: {
start: 23, // 開始
stop: 6, // 停止
},
},
// 地震速報設定
earthquake: {
reconnectTimes: 5000, // 再接続時間(ミリ秒)
websocketUrl: "wss://api.p2pquake.net/v2/ws", // WebSocketのURL
areasCsvUrl: "https://raw.githubusercontent.com/p2pquake/epsp-specifications/master/epsp-area.csv", // 対象地域CSVファイルのURL
maxScaleMin: 30, // 地震発生の際の最低震度(10-70)
},
weather: {
splitCount: 4, // 返信の分割数
},
// 緊急時設定
emergency: {
function: true, // 緊急時のコンソール表示
report: false, // reportコマンド
mail: {
function: true, // 緊急時のメール送信
host: "smtp.example.com", // SMTPサーバー
port: 465, // SMTPポート
user: "mailUser@example.com", // BOTメール送信元
password: "mailPassword", // SMTPパスワード
secure: false, // SMTPsecure設定
to: "admin@noticeuwuzu.example.com", // 緊急時メール送信先(配列可)
},
},
uwuzu: {
apiToken: "TOKEN_EXAMPLE",
clientToken: "TOKEN_EXAMPLE",
host: "uwuzu.example.com",
},
};
export default config;
+34 -27
View File
@@ -1,41 +1,48 @@
// 起動チェック // 起動チェック
import Check from "./checks/main.js"; (await import("@/scripts/checks/main")).default();
(async () => { (await import("@/scripts/successExit")).default();
await Check();
})();
// 定期実行読み込み // アスキーアート
import * as cron from "node-cron"; (await import("@/scripts/asciiart")).default();
// 機能読み込み
import timeNotice from "./scripts/timeNotice.js";
import { weatherNotice } from "./scripts/weatherNotice.js";
import earthquakeNotice from "./scripts/earthquakeNotice.js";
import Commands from "./scripts/commands/main.js";
// その他機能
import asciiArt from "./scripts/asciiart.js";
asciiArt();
import successExit from "./scripts/successExit.js";
successExit();
// 地震情報観測開始 // 地震情報観測開始
earthquakeNotice(); (await import("@/scripts/earthquakeNotice")).default();
// 時報(1時間/1回) // 定期実行
cron.schedule("0 * * * *", () => { import * as cron from "node-cron";
timeNotice();
/// 時報(1時間毎)
cron.schedule("0 * * * *", async () => {
(await import("@/scripts/timeNotice")).default();
}); });
// コマンド(10分/1回) // コマンド(10分)
cron.schedule('*/10 * * * *', () => { cron.schedule("*/10 * * * *", async () => {
Commands(); (await import("@/scripts/commands/main")).default();
});
// 祝日など(毎日0:00)
cron.schedule("0 0 * * *", async () => {
(await import("@/scripts/eventday")).default();
}); });
// 天気お知らせ(毎日7:00) // 天気お知らせ(毎日7:00)
cron.schedule("0 7 * * *", () => { cron.schedule("0 7 * * *", async () => {
weatherNotice(); (await import("@/scripts/weatherNotice")).weatherNotice();
}); });
// 管理パネル
await (await import("./panel/main")).default()
// 起動表示 // 起動表示
console.log("BOTサーバーが起動しました"); console.log("BOTサーバーが起動しました");
if ((await import("./config")).default.debug) {
// TLSを任意に設定
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
console.log((await import("node:util")).styleText(
["bgRed", "cyan", "bold"],
"デバッグモードで起動中"
));
}
File diff suppressed because one or more lines are too long
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+91
View File
@@ -0,0 +1,91 @@
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.
Binary file not shown.
+181
View File
@@ -0,0 +1,181 @@
import { createCanvas, loadImage, registerFont } from "canvas";
import sharp from "sharp";
import { MiQOptions } from "./miq";
import { readFileSync } from "fs";
// フォント読み込み
registerFont("miq/fonts/MPLUS.ttf", {
family: "M PLUS Rounded 1c",
weight: "400",
});
function maxLengthCut(
text: string,
maxLength: number,
) {
if (text.length > maxLength) {
text = text.substring(0, maxLength)
+ "...";
}
return text;
}
function autoLineBreak(
text: string,
maxWidth: number,
font: string = '48px "M PLUS Rounded 1c"'
): string {
const ctx = createCanvas(maxWidth, 100).getContext("2d");
ctx.font = font;
const lines: string[] = [];
let currentLine = "";
for (let i = 0; i < text.length; i++) {
const char = text[i];
const testLine = currentLine + char;
const width = ctx.measureText(testLine).width;
if (width > maxWidth) {
lines.push(currentLine);
currentLine = char;
while (ctx.measureText(currentLine).width > maxWidth && currentLine.length > 1) {
const cutPoint = currentLine.length - 1;
lines.push(currentLine.slice(0, cutPoint));
currentLine = currentLine.slice(cutPoint);
}
} else {
currentLine = testLine;
}
}
if (currentLine) lines.push(currentLine);
return lines.join("\n");
}
function textReplace(
text: string,
maxWidth: number,
font?: string
) {
// 改行削除
text = text.replaceAll("\n", "");
text = text.replaceAll("\r", "");
// 自動改行
text = autoLineBreak(text, maxWidth, font);
// 100文字以上を省略
text = maxLengthCut(text, 100);
return text;
}
async function iconReplace(
color: boolean,
iconURL: string
) {
let result = "";
const bufferReq = await fetch(iconURL, {
method: "GET",
cache: "no-store",
});
if (bufferReq.status < 200 || bufferReq.status > 299) {
return readFileSync("miq/PersonIcon.txt", "utf-8");
}
const buffer = await bufferReq.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) {
// 初期化
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 "M PLUS Rounded 1c"';
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let x = 910;
let y = 480;
ctx.fillText(`- ${userName}`, x, y, canvas.width-iconSize);
// ユーザーID描画
ctx.font = '28px "M PLUS Rounded 1c"';
ctx.fillStyle = "#b4b4b4";
ctx.fillText(`@${userID}`, x, y+50, canvas.width-iconSize);
// 本文描画
const maxWidth = canvas.width - iconSize;
ctx.font = '48px "M PLUS Rounded 1c"';
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "white";
text = textReplace(text, maxWidth, ctx.font);
ctx.fillText(text, x, 80);
// フェード描画
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);
// 返答
switch (type) {
case "Buffer":
return canvas.toBuffer();
case "Base64Data":
return canvas.toDataURL()
.replace("data:image/png;base64,", "");
case "Base64URL":
return canvas.toDataURL();
default:
return "Error: The type property is invalid.";
}
}
+13
View File
@@ -0,0 +1,13 @@
type MiQType =
"Buffer" |
"Base64Data" |
"Base64URL";
export interface MiQOptions {
type: MiQType;
color: boolean;
text: string;
iconURL: string;
userName: string;
userID: string;
}
+3084
View File
File diff suppressed because it is too large Load Diff
+22 -8
View File
@@ -1,15 +1,14 @@
{ {
"name": "notice-uwuzu", "name": "notice-uwuzu",
"version": "v7.2@uwuzu1.5.4", "version": "v25.8.11@uwuzu1.6.4",
"tag": "v7.2", "tag": "v25.8.11",
"description": "Notice Bot for uwuzu", "description": "Notice Bot for uwuzu",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"start": "node .", "start": "node .",
"build": "tsc", "build": "tsc && tsc-alias",
"main": "tsc && node .",
"dev": "tsx main.ts", "dev": "tsx main.ts",
"clean": "tsc && node dist/scripts/clean/main.js" "clean": "npm run build && node dist/scripts/clean/main.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -20,31 +19,46 @@
"bot", "bot",
"cron", "cron",
"notice", "notice",
"mail",
"weather", "weather",
"time", "time",
"earthquake" "earthquake",
"command",
"commands"
], ],
"author": { "author": {
"name": "Last2014", "name": "Last2014",
"url": "https://last2014.com", "url": "https://last2014.com",
"email": "info@last2014.com" "email": "info@last2014.com"
}, },
"contributors": [],
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@types/date-fns": "^2.5.3", "@types/date-fns": "^2.5.3",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
"@types/express": "^5.0.3",
"@types/node": "^24.0.7", "@types/node": "^24.0.7",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/sharp": "^0.31.1",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"canvas": "^3.2.0",
"chardet": "^2.1.1",
"child_process": "^1.0.2", "child_process": "^1.0.2",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"esm-path": "git+https://gitea.last2014.com/last2014/esm-path.git#1.0.0",
"express": "^5.1.0",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"iconv-lite": "^0.7.0",
"node-cron": "^4.1.1", "node-cron": "^4.1.1",
"nodemailer": "^7.0.4", "nodemailer": "^7.0.4",
"typescript": "^5.8.3", "sharp": "^0.34.3",
"ws": "^8.18.3" "tsc-alias": "^1.8.16",
"typescript": "^5.9.2",
"ws": "^8.18.3",
"yaml": "^2.8.2",
"zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"tsx": "^4.20.3" "tsx": "^4.20.3"
+46
View File
@@ -0,0 +1,46 @@
import express from "express";
import * as os from "os";
import config from "../config";
import { NetworkInterfaceDetails } from "@/types/types";
export default async function AdminPanel() {
// 無効
if (!config.admin.panel.isEnabled) {
return;
}
// 管理パネル
const app = express();
const port = config.admin.panel.port;
// ルーティング
app.use((await import("./route/ueuse")).default);
app.use((await import("./route/command")).default);
app.use((await import("./route/weather")).default);
app.use((await import("./route/api")).default);
app.use((await import("./route/token")).default);
app.use((await import("./route/debug")).default);
app.use(express.static("panel/public"));
app.listen(port, () => {
console.log(`http://${LocalIP()}:${port} で管理パネルを起動しました`);
});
}
function LocalIP() {
const interfaces = os.networkInterfaces();
for (const name in interfaces) {
const iface: any = interfaces[name];
for (const i of iface) {
const details: NetworkInterfaceDetails = i;
if (details.family === 'IPv4' && details.internal !== true) {
return details.address;
}
}
}
return "localhost";
}
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理パネル</title>
<!-- パッケージ -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://code.iconify.design/iconify-icon/3.0.0/iconify-icon.min.js"></script>
<script>
import "iconify-icon";
</script>
</head>
<body class="dark:bg-gray-950 dark:text-white text-center">
<h1 class="text-4xl font-bold">noticeUwuzu管理パネル</h1>
<button id="commandExec" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
コマンド実行
</button>
<button id="weatherUeuse" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
天気お知らせ
</button>
<button id="eventdayUeuse" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
祝日等お知らせ
</button>
<button id="ueuse" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
ユーズ投稿
</button>
<button id="api" class="border rounded-[10px] p-[5px] m-[10px] mt-[20px] cursor-pointer">
API使用
</button>
<script src="/script.js"></script>
</body>
</html>
+106
View File
@@ -0,0 +1,106 @@
document.getElementById("commandExec").addEventListener("click", async () => {
const req = await fetch("/actions/command-execute", {
method: "POST",
});
const res = await req.text();
if (res === "Accepted") {
alert("コマンド実行を受け付けました");
} else {
alert(`コマンド実行の要求にエラーが発生しました:${res}`);
}
});
document.getElementById("weatherUeuse").addEventListener("click", async () => {
const req = await fetch("/actions/weather", {
method: "POST",
});
const res = await req.text();
if (res === "Accepted") {
alert("天気お知らせを受け付けました");
} else {
alert(`天気お知らせの要求にエラーが発生しました:${res}`);
}
});
document.getElementById("eventdayUeuse").addEventListener("click", async () => {
const req = await fetch("/actions/eventday", {
method: "POST",
});
const res = await req.text();
if (res === "Accepted") {
alert("祝日等お知らせを受け付けました");
} else {
alert(`祝日等お知らせの要求にエラーが発生しました:${res}`);
}
});
document.getElementById("ueuse").addEventListener("click", async () => {
const text = prompt("ユーズ内容").toLowerCase();
if (text === "") {
alert("ユーズ内容がありません。");
return;
}
const nsfw = confirm("NSFWにしますか?");
const req = await fetch("/actions/ueuse", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text, text,
nsfw: nsfw,
}),
});
const res = await req.text();
if (res === "Success") {
alert("ユーズ投稿を受け付けました");
} else {
alert(`ユーズ投稿の要求にエラーが発生しました:${res}`);
}
});
document.getElementById("api").addEventListener("click", async () => {
const token = await (await fetch("/actions/token", {
method: "GET",
})).text();
const endpoint = prompt("エンドポイント", "/serverinfo-api").toLowerCase();
if (endpoint === "") {
alert("エンドポイントが設定されていません。");
return;
}
const body = prompt("body(JSON)", `{"token": "${token}"}`).toLowerCase();
if (body === "") {
alert("bodyが設定されていません。");
return;
}
const req = await fetch("/actions/api", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
endpoint: endpoint,
body: JSON.parse(body),
}),
});
const res = await req.text();
alert(res);
});
+34
View File
@@ -0,0 +1,34 @@
import express from "express";
const API = express.Router();
import config from "../../config";
API.use(express.json());
API.use(express.urlencoded({ extended: true }));
API.post("/actions/api", async (req, res, next) => {
const endpoint = req.body.endpoint;
const body = req.body.body;
try {
const apiReq = await fetch(`${config.uwuzu.host}/api${endpoint}`, {
method: "POST",
body: JSON.stringify(body),
});
const apiRes = await apiReq.json();
res.status(200)
.send(apiRes);
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
API.get("/actions/api", (req, res) => {
res.status(501)
.send("POST Only");
});
export default API;
+25
View File
@@ -0,0 +1,25 @@
import express from "express";
const CommandExecute = express.Router();
import Commands from "@/scripts/commands/main";
CommandExecute.post("/actions/command-execute", (req, res) => {
try {
(async () => {
await Commands();
})();
res.status(202)
.send("Accepted");
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
CommandExecute.get("/actions/command-execute", (req, res) => {
res.status(501)
.send("POST Only");
});
export default CommandExecute;
+24
View File
@@ -0,0 +1,24 @@
import express from "express";
const Debug = express.Router();
import config from "../../config";
Debug.post("/actions/debug", (req, res, next) => {
res.status(501)
.send("GET Only");
});
Debug.get("/actions/debug", (req, res) => {
let debug;
if (config.debug === undefined) {
debug = false;
} else {
debug = true;
}
res.status(200)
.send(debug);
});
export default Debug;
+25
View File
@@ -0,0 +1,25 @@
import express from "express";
const EventdayUeuse = express.Router();
import EventDays from "@/scripts/eventday";
EventdayUeuse.post("/actions/eventday", (req, res) => {
try {
(async () => {
await EventDays();
})();
res.status(202)
.send("Accepted");
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
EventdayUeuse.get("/actions/eventday", (req, res) => {
res.status(501)
.send("POST Only");
});
export default EventdayUeuse;
+16
View File
@@ -0,0 +1,16 @@
import express from "express";
const Token = express.Router();
import config from "../../config";
Token.post("/actions/token", (req, res, next) => {
res.status(501)
.send("GET Only");
});
Token.get("/actions/token", (req, res) => {
res.status(200)
.send(config.uwuzu.apiToken);
});
export default Token;
+44
View File
@@ -0,0 +1,44 @@
import express from "express";
const ueusePost = express.Router();
import config from "../../config";
ueusePost.use(express.json());
ueusePost.use(express.urlencoded({ extended: true }));
ueusePost.post("/actions/ueuse", async (req, res, next) => {
const text = req.body.text;
const nsfw = req.body.nsfw;
try {
const ueuseReq = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: `
${text}
このユーズはnoticeUwuzuの管理パネルから投稿されました
`,
nsfw: nsfw,
}),
});
const ueuseRes = await ueuseReq.json();
console.log(`ユーズ(管理パネル)${JSON.stringify(ueuseRes)}`);
res.status(200)
.send("Success");
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
ueusePost.get("/actions/ueuse", (req, res) => {
res.status(501)
.send("POST Only");
});
export default ueusePost;
+25
View File
@@ -0,0 +1,25 @@
import express from "express";
const WeatherUeuse = express.Router();
import { weatherNotice } from "@/scripts/weatherNotice";
WeatherUeuse.post("/actions/weather", (req, res) => {
try {
(async () => {
await weatherNotice();
})();
res.status(202)
.send("Accepted");
} catch(err) {
res.status(500)
.send(`Error: ${err}`);
}
});
WeatherUeuse.get("/actions/weather", (req, res) => {
res.status(501)
.send("POST Only");
});
export default WeatherUeuse;
-8
View File
@@ -1,8 +0,0 @@
import * as fs from "fs";
const version = JSON.parse(fs.readFileSync("package.json", "utf-8")).version;
export default function asciiArt() {
console.log(fs.readFileSync("asciiart.txt", "utf-8").replace(/(\r?\n)$/, ''));
console.log(`${version}\n`);
}
-7
View File
@@ -1,7 +0,0 @@
import logsDelete from "./logsDel.js";
import packageLockJsonDelete from "./packageLockDel.js";
import npmInstall from "./npmInstall.js";
logsDelete();
packageLockJsonDelete();
npmInstall();
-28
View File
@@ -1,28 +0,0 @@
import { ueuse } from "types/types.js";
import { readFileSync } from "fs";
import { Reply } from "./main.js";
const helps = {
"info": "このBOTについての概要を返信するコマンドです。",
"help": "このコマンドです。コマンドの概要を返信します。",
"follow": "コマンド送信者をフォローします。",
"unfollow": "コマンド送信者をフォロー解除します。",
"weather": "天気を返信します。7:00に投稿されるものとは異なり再取得します。",
} as { [key: string]: string };
export default async function Help(data: ueuse) {
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
const helpMsg =
Object.entries(helps)
.map(([command, message]) =>
`\`/${command}\`${message}`
).join('\n');
const ueuse = await Reply(`
${helpMsg}
機能を見る:${packageJson.repository.url}/wiki
`, data.uniqid);
console.log("ヘルプ:", ueuse);
}
-26
View File
@@ -1,26 +0,0 @@
import { ueuse } from "types/types.js";
import { readFileSync } from "fs";
import { Reply } from "./main.js";
export default async function Info(data: ueuse) {
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
const releaseUrl = `${packageJson.repository.url}/releases/tag/${packageJson.tag}`;
let editor = "";
if (packageJson.author.name !== "Last2014") {
editor = `\nEdited by ${packageJson.author.name}`;
}
const ueuse = await Reply(`
バージョン:${packageJson.version}
リリース詳細:${releaseUrl}
コマンドのヘルプをお探しですか?
\`/help\`をご利用ください。
Created by Last2014${editor}
`, data.uniqid);
console.log("概要:", ueuse);
}
-140
View File
@@ -1,140 +0,0 @@
import * as fs from "fs";
import config from "../../config.js";
import type { ueuse } from "types/types.js";
const initialFile: Array<string> = [];
// コマンド読み込み
import Info from "./info.js";
import Follow from "./follow.js";
import UnFollow from "./unfollow.js";
import Weather from "./weather.js";
import Help from "./help.js";
import Report from "./report.js";
// 初期化
if (!fs.existsSync("logs/alreadyCommands.json")) {
fs.writeFileSync(
"logs/alreadyCommands.json",
JSON.stringify(initialFile),
"utf-8",
);
}
// 対応済みユーズ一覧
const alreadyCommands: Array<string> = JSON.parse(fs.readFileSync("logs/alreadyCommands.json", "utf-8"));
function cutAfterChar(str: string, char: string) {
const index = str.indexOf(char);
if (index === -1) {
return "";
}
return str.substring(index + 1);
}
export async function Reply(text: string, reply: string) {
const req = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: text,
replyid: reply,
}),
cache: "no-store",
});
const res = await req.json();
return res;
}
function alreadyAdd(data: string) {
alreadyCommands[alreadyCommands.length] = data;
fs.writeFileSync(
"logs/alreadyCommands.json",
JSON.stringify(alreadyCommands),
"utf-8",
);
}
export default async function Commands() {
const mentionsReq = await fetch(
`https://${config.uwuzu.host}/api/ueuse/mentions`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
}),
cache: "no-store",
}
);
const mentions: Array<ueuse> = await mentionsReq.json();
console.log("----------------");
console.log("コマンド処理");
for (let i = 0; i < mentions.length; i++) {
const data = mentions[i];
// 除外ユーズ
if (alreadyCommands.indexOf(data.uniqid) !== -1) {
break;
}
if (
data.text.charAt(0) === "!" ||
data.text.charAt(0) === "" ||
data.abi === "ignore"
) {
break;
}
// コマンド処理
console.log("--------");
const commandName = cutAfterChar(data.text, "/");
switch (commandName) {
case "":
alreadyAdd(data.uniqid);
break;
case "info":
alreadyAdd(data.uniqid);
Info(data);
break;
case "help":
alreadyAdd(data.uniqid);
Help(data);
break;
case "report":
alreadyAdd(data.uniqid);
Report(data);
break;
case "follow":
alreadyAdd(data.uniqid);
Follow(data);
break;
case "unfollow":
alreadyAdd(data.uniqid);
UnFollow(data);
break;
case "weather":
alreadyAdd(data.uniqid);
Weather(data);
break;
default:
alreadyAdd(data.uniqid);
const reply = await Reply(`
不明なコマンドです。
コマンド実行を除外する場合は1文字目に\`!\`を入れてください。
`, data.uniqid);
console.log("未対応コマンド: ", reply);
break;
}
}
}
-6
View File
@@ -1,6 +0,0 @@
import { weatherReply } from "../weatherNotice.js";
import { ueuse } from "types/types.js";
export default function Weather(data: ueuse) {
weatherReply(data.uniqid);
}
+66
View File
@@ -0,0 +1,66 @@
export default {
"01/01": {
name: "元日",
message: "はい年越した瞬間地球にいなかった~",
},
"02/11": {
name: "建国記念日",
message: "建国記念日とかいう強制休暇",
},
"04/29": {
name: "昭和の日",
message: "平成の日は???",
},
"05/03": {
name: "憲法記念日",
message: "憲法決めた日とか祝日じゃなくていいだろ",
},
"05/04": {
name: "みどりの日",
message:
`なんだよみどりの日って
単色全部作れよ`,
},
"05/05": {
name: "こどもの日",
message: "こどもの日あるならおとなの日もあっていいだろ",
},
"07/07": {
name: "七夕",
message:
`祭りでも行っとけ
どーせ開発者のLast2014は家でサーバーいじってるから`,
},
"08/11": {
name: "山の日",
message:
`空の日と山の日も作れよ
水の循環に重要な3つ`,
},
"11/03": {
name: "文化の日",
message: "ネットミームできるたびに休みになればいいのになぁ...",
},
"11/23": {
name: "勤労感謝の日",
message: "学生は神!!",
},
"12/24": {
name: "クリスマスイブ",
message:
`リア充爆破します
by 開発者`,
},
"12/25": {
name: "クリスマス",
message: `リア充爆破します(2回目)
by 開発者`,
},
"12/31": {
name: "大晦日",
message: "大掃除!!大掃除!!",
},
} as { [key: string]: {
name: string;
message: string;
} };
@@ -1,4 +1,4 @@
export const cityList: Array<String> = [ export const cityList = [
"016010", "016010",
"020010", "020010",
"030010", "030010",
+94
View File
@@ -0,0 +1,94 @@
import * as z from "zod/v3";
import { readFileSync } from "fs";
import { autoDecode } from "@/lib/encode";
import { getDirname } from "esm-path";
import * as yaml from "yaml";
const schema = z.object({
time: z.object({
stopTimes: z.object({
start: z.number().min(0).max(23),
stop: z.number().min(0).max(23),
}),
}),
earthquake: z.object({
reconnectTimes: z.number().min(0),
websocketUrl: z.string().url(),
areasCsvUrl: z.string().url(),
maxScaleMin: z.union([
z.literal(-1),
z.literal(10),
z.literal(20),
z.literal(30),
z.literal(40),
z.literal(45),
z.literal(50),
z.literal(55),
z.literal(60),
z.literal(70),
]), // z.enum()は文字列のみ(https://github.com/colinhacks/zod/issues/338)
}),
weather: z.object({
splitCount: z.number().min(1),
}),
miq: z.boolean(),
emergency: z.union([z.object({
isEnabled: z.literal(true),
mail: z.union([z.object({
isEnabled: z.literal(true),
host: z.string(),
port: z.number().min(0).max(65535),
user: z.string(),
password: z.string(),
secure: z.boolean(),
to: z.union([
z.string(),
z.array(z.string())
]),
}), z.object({
isEnabled: z.literal(false),
})]),
}), z.object({
isEnabled: z.literal(false),
})]),
report: z.union([z.object({
isEnabled: z.literal(true),
message: z.string(),
}), z.object({
isEnabled: z.literal(false),
})]),
legal: z.object({
terms: z.string(),
privacy: z.string(),
}),
admin: z.object({
name: z.string(),
showMail: z.union([
z.string(),
z.literal(false)
]),
panel: z.union([z.object({
isEnabled: z.literal(true),
port: z.number().min(0).max(65535),
}), z.object({
isEnabled: z.literal(false),
})]),
}),
uwuzu: z.object({
apiToken: z.string(),
host: z.string(),
}),
debug: z.literal(true).optional(),
});
const file = readFileSync(`${import.meta.dirname}/../../config/config.yaml`);
const text = autoDecode(file);
const configObj = yaml.parse(text);
const parsed = schema.safeParse(configObj);
if (!parsed.success) {
throw new Error("Config is invalid.")
}
const config = parsed.data;
export default config;
+7
View File
@@ -0,0 +1,7 @@
import { decode } from "iconv-lite";
import { detect } from "chardet";
export function autoDecode(buffer: Buffer) {
const encoding = detect(buffer) ?? "utf-8";
return decode(buffer, encoding);
}
+64
View File
@@ -0,0 +1,64 @@
import config from "@/lib/config";
import * as nodemailer from "nodemailer";
import type SMTPTransport from "nodemailer/lib/smtp-transport";
export interface EmailMessage {
to: string | string[];
subject: string;
text?: string;
html?: string;
}
async function createTransporter() {
if (
config.emergency.isEnabled &&
config.emergency.mail.isEnabled
) {
const transporter = nodemailer.createTransport({
host: config.emergency.mail.host,
port: config.emergency.mail.port,
secure: config.emergency.mail.secure,
auth: {
user: config.emergency.mail.user,
pass: config.emergency.mail.password,
},
} as SMTPTransport.Options);
// 接続テスト
try {
await transporter.verify();
console.log("SMTPサーバーに接続できました");
} catch (error) {
console.error("SMTP接続テストに失敗:", error);
throw error;
}
return transporter;
}
}
export default async function sendMail(message: EmailMessage): Promise<void> {
if (
config.emergency.isEnabled &&
config.emergency.mail.isEnabled
) {
try {
const transporter: any = await createTransporter();
await transporter.sendMail({
from: config.emergency.mail.user,
to: Array.isArray(message.to) ? message.to.join(",") : message.to,
subject: message.subject,
text: message.text,
html: message.html,
});
console.log("メール送信成功");
} catch (error) {
console.error("メール送信に失敗しました:", error);
throw error;
}
} else {
return;
}
}
-52
View File
@@ -1,52 +0,0 @@
import config from "../config.js";
import * as nodemailer from "nodemailer";
import type SMTPTransport from "nodemailer/lib/smtp-transport";
export interface EmailMessage {
to: string | string[];
subject: string;
text?: string;
html?: string;
}
async function createTransporter() {
const transporter = nodemailer.createTransport({
host: config.emergency.mail.host,
port: config.emergency.mail.port,
secure: config.emergency.mail.secure,
auth: {
user: config.emergency.mail.user,
pass: config.emergency.mail.password,
},
} as SMTPTransport.Options);
// 接続テスト
try {
await transporter.verify();
console.log("SMTPサーバーに接続できました");
} catch (error) {
console.error("SMTP接続テストに失敗:", error);
throw error;
}
return transporter;
}
export default async function sendMail(message: EmailMessage): Promise<void> {
try {
const transporter = await createTransporter();
await transporter.sendMail({
from: config.emergency.mail.user,
to: Array.isArray(message.to) ? message.to.join(",") : message.to,
subject: message.subject,
text: message.text,
html: message.html,
});
console.log("メール送信成功");
} catch (error) {
console.error("メール送信に失敗しました:", error);
throw error;
}
}
+8
View File
@@ -0,0 +1,8 @@
import * as fs from "fs";
const version = JSON.parse(fs.readFileSync(`${import.meta.dirname}/../../package.json`, "utf-8")).version;
export default function asciiArt() {
console.log(fs.readFileSync(`${import.meta.dirname}/../../asciiart.txt`, "utf-8"));
console.log(`${version}\n`);
}
+9 -3
View File
@@ -1,15 +1,21 @@
import { styleText } from "util"; import { styleText } from "util";
import config from "../config.js"; import config from "@/lib/config";
export default async function APICheck() { export default async function APICheck() {
try { try {
const req = await fetch(`https://${config.uwuzu.host}/api/me/`, { const req = await fetch(`${config.uwuzu.host}/api/me/`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
}) }),
cache: "no-store",
}); });
if (req.status < 200 || req.status > 299) {
console.log(styleText("red", "uwuzuサーバーから正常な返答がありませんでした"));
process.exit();
}
const res = await req.json(); const res = await req.json();
if (res.error_code !== undefined) { if (res.error_code !== undefined) {
+9
View File
@@ -0,0 +1,9 @@
import * as fs from "fs";
import { styleText } from "util";
export default function ConfigCheck() {
if (!fs.existsSync(`${import.meta.dirname}/../../../config/config.yaml`)) {
console.log(styleText("red", "config/config.yamlがありません"));
process.exit();
}
}
+12
View File
@@ -0,0 +1,12 @@
import config from "@/lib/config";
import { styleText } from "util";
export default function LegalCheck() {
if (
config.legal.terms.length <= 50 ||
config.legal.terms.length <= 50
) {
console.log(styleText("red", "利用規約とプライバシーポリシーは50文字以上にしてください。"));
process.exit();
}
}
+18
View File
@@ -0,0 +1,18 @@
import PackagesIsExist from "@/scripts/checks/packagesExist";
import PackagesCheck from "@/scripts/checks/packages";
import ConfigCheck from "@/scripts/checks/config";
import APICheck from "@/scripts/checks/api";
import VersionCheck from "@/scripts/checks/version";
import LegalCheck from "@/scripts/checks/legal";
import config from "@/lib/config";
export default async function Check() {
PackagesIsExist();
PackagesCheck();
ConfigCheck();
if (config.debug === undefined) {
LegalCheck()
}
await APICheck();
await VersionCheck();
}
@@ -3,13 +3,13 @@ import { styleText } from "util";
export default function PackagesCheck() { export default function PackagesCheck() {
try { try {
if (!fs.existsSync("package.json")) { if (!fs.existsSync(`${import.meta.dirname}/../../../package.json`)) {
console.log(styleText("red", "package.jsonがありません。正規のリポジトリでgit pullを実行してください。")); console.log(styleText("red", "package.jsonがありません。正規のリポジトリでgit pullを実行してください。"));
process.exit(); process.exit();
} }
// package.json取得 // package.json取得
const packages = JSON.parse(fs.readFileSync("package.json", "utf-8")); const packages = JSON.parse(fs.readFileSync(`${import.meta.dirname}/../../../package.json`, "utf-8"));
const dependencies = packages.dependencies; const dependencies = packages.dependencies;
const packageNames: Array<string> = []; const packageNames: Array<string> = [];
@@ -31,7 +31,7 @@ export default function PackagesCheck() {
const mismatchPackages: Array<string> = []; const mismatchPackages: Array<string> = [];
packageNames.forEach((packageName) => { packageNames.forEach((packageName) => {
const packagePath = `node_modules/${packageName}/package.json`; const packagePath = `${import.meta.dirname}/../../../node_modules/${packageName}/package.json`;
if (fs.existsSync(packagePath)) { if (fs.existsSync(packagePath)) {
const modulePackage = JSON.parse(fs.readFileSync(packagePath, "utf-8")); const modulePackage = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
@@ -3,7 +3,7 @@ import { styleText } from "util";
export default function PackagesIsExist() { export default function PackagesIsExist() {
try { try {
if (!fs.existsSync("node_modules/.package-lock.json")) { if (!fs.existsSync(`${import.meta.dirname}/../../../node_modules/.package-lock.json`)) {
console.log(styleText("red", ` console.log(styleText("red", `
node_modules/.package-lock.jsonがありません node_modules/.package-lock.jsonがありません
npm installを実行してください npm installを実行してください
@@ -1,32 +1,32 @@
import config from "../config.js"; import config from "@/lib/config";
import { readFileSync, writeFileSync, existsSync } from "fs"; import { readFileSync, writeFileSync, existsSync } from "fs";
export default async function VersionCheck() { export default async function VersionCheck() {
const packageJson = JSON.parse(readFileSync("package.json", "utf-8")); const packageJson = JSON.parse(readFileSync(`${import.meta.dirname}/../../../package.json`, "utf-8"));
// 初期化 // 初期化
if (!existsSync("logs/version.txt")) { if (!existsSync(`${import.meta.dirname}/../../../logs/version.txt`)) {
writeFileSync( writeFileSync(
"logs/version.txt", `${import.meta.dirname}/../../../logs/version.txt`,
packageJson.version, packageJson.version,
"utf-8", "utf-8",
); );
} }
// 最終起動バージョン取得 // 最終起動バージョン取得
const oldVersion = readFileSync("logs/version.txt", "utf-8"); const oldVersion = readFileSync(`${import.meta.dirname}/../../../logs/version.txt`, "utf-8");
if (oldVersion !== packageJson.version) { if (oldVersion !== packageJson.version) {
try { try {
writeFileSync( writeFileSync(
"logs/version.txt", `${import.meta.dirname}/../../../logs/version.txt`,
packageJson.version, packageJson.version,
"utf-8", "utf-8",
); );
const releaseUrl = `${packageJson.repository.url}/releases/tag/${packageJson.tag}`; const releaseUrl = `${packageJson.repository.url}/releases/tag/${packageJson.tag}`;
await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, { const req = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
@@ -37,6 +37,12 @@ export default async function VersionCheck() {
}), }),
cache: "no-store", cache: "no-store",
}); });
if (req.status < 200 || req.status > 299) {
return;
}
console.log("アップデート通知:", await req.json());
} catch (err) { } catch (err) {
console.log("アップデート通知にエラーが発生しました: ", err); console.log("アップデート通知にエラーが発生しました: ", err);
} }
@@ -4,8 +4,8 @@ export default function logsDelete() {
console.log("ログを削除中します"); console.log("ログを削除中します");
try { try {
unlinkSync("logs/boot.json"); unlinkSync(`${import.meta.dirname}/../../../logs/boot.json`);
unlinkSync("logs/version.txt"); unlinkSync(`${import.meta.dirname}/../../../logs/version.txt`);
console.log("ログファイルを削除しました"); console.log("ログファイルを削除しました");
console.log("----------------"); console.log("----------------");
+5
View File
@@ -0,0 +1,5 @@
(await import("@/scripts/clean/logsDel")).default();
(await import("@/scripts/clean/packageLockDel")).default();
(await import("@/scripts/clean/npmInstall")).default();
export {};
@@ -4,7 +4,7 @@ export default function packageLockJsonDelete() {
console.log("package-lock.jsonを削除します"); console.log("package-lock.jsonを削除します");
try { try {
unlinkSync("package-lock.json"); unlinkSync(`${import.meta.dirname}/../../../package-lock.json`);
console.log("package-lock.jsonを削除しました"); console.log("package-lock.jsonを削除しました");
console.log("----------------"); console.log("----------------");
+70
View File
@@ -0,0 +1,70 @@
import { meApi, ueuse } from "@/types/types";
import config from "@/lib/config";
import { Reply } from "@/scripts/commands/main";
export default async function Delete(data: ueuse) {
const meReq = await fetch(`${config.uwuzu.host}/api/me/`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
}),
});
if (meReq.status < 200 || meReq.status > 299) {
return;
}
const me: meApi = await meReq.json();
const replyUeuseReq = await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: data.replyid,
}),
});
if (replyUeuseReq.status < 200 || replyUeuseReq.status > 299) {
return;
}
const replyUeuse: ueuse = (await replyUeuseReq.json())["0"];
if (me.userid === replyUeuse.account.userid) {
const ueuseDeleteReq = await fetch(`${config.uwuzu.host}/api/ueuse/delete`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: data.replyid,
}),
});
if (ueuseDeleteReq.status < 200 || ueuseDeleteReq.status > 299) {
return;
}
const ueuseDelete = await ueuseDeleteReq.json();
console.log("削除:", ueuseDelete);
if (ueuseDelete.success === true) {
console.log("削除通知:", await Reply(`
対象のユーズを削除しました。
`, data.uniqid));
} else {
console.log("削除失敗通知:", await Reply(`
対象のユーズを削除できませんでした。
`, data.uniqid));
}
return;
} else {
console.log("削除失敗通知(他人)", await Reply(`
削除するユーズが${me.username}のものではありません。
そのため削除できませんでした。
`, data.uniqid));
return;
}
}
@@ -1,9 +1,9 @@
import { ueuse } from "types/types.js"; import { ueuse } from "@/types/types";
import config from "../../config.js"; import config from "@/lib/config";
import { Reply } from "./main.js"; import { Reply } from "@/scripts/commands/main";
export default async function Follow(data: ueuse) { export default async function Follow(data: ueuse) {
const followReq = await fetch(`https://${config.uwuzu.host}/api/users/follow`, { const followReq = await fetch(`${config.uwuzu.host}/api/users/follow`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
@@ -12,6 +12,10 @@ export default async function Follow(data: ueuse) {
cache: "no-store", cache: "no-store",
}); });
if (followReq.status < 200 || followReq.status > 299) {
return;
}
const followRes = await followReq.json(); const followRes = await followReq.json();
console.log("フォロー: ", followRes); console.log("フォロー: ", followRes);
+112
View File
@@ -0,0 +1,112 @@
import { ueuse } from "@/types/types";
import { readFileSync } from "fs";
import { Reply } from "@/scripts/commands/main";
const helpsMin = {
"info": "このBOTについての概要を返信するコマンドです。",
"help": "コマンドの概要を返信します。追記に\`/\`抜きのコマンド名を入力することでそのコマンドの詳細(フル)を返信します。",
"follow": "コマンド送信者をフォローします。",
"unfollow": "コマンド送信者をフォロー解除します。",
"weather": "天気を返信します。",
"miq": "Make it a Quoteを生成します。",
"miq permission": "あなたに対してのMake it a Quoteを生成する要求者を制限できます。",
"miq allow": "Make it a Quoteの許可制にて生成を許可します。",
"delete": "ユーズを削除します。",
"report": "運営者に不具合などを報告します。",
"legal terms": "利用規約を返信します。",
"legal privacy": "プライバシーポリシーを返信します。",
} as { [key: string]: string };
const helpsFull = {
"info": `
このBOTについての概要を返信するコマンドです。
バージョン、開発者などが確認できます。
`,
"help": `
このコマンドです。コマンドの概要を返信します。
追記に\`/\`抜きのコマンド名を入力することでそのコマンドの詳細(フル)を返信します。
`,
"follow": `
コマンドを送信したユーザーをフォローします。
既にフォローされているユーザーも使用できます。
`,
"unfollow": `
コマンドを送信したユーザーをフォロー解除します。
既にフォローされていないユーザーも使用できます。
`,
"weather": `
天気を返信します。
毎日7:00の天気を再投稿するわけではなく、
再取得して返信します。
`,
"miq": `
Make it a Quoteを生成します。
追記に\`color: true\`と入力することでカラーモードに変更可能です。
\`/delete\`コマンドを使用して削除できます。
`,
"miq permission": `
あなたに対してのMake it a Quoteを生成する要求者を制限できます。
確認するには追記なしで実行してください。
変更するには以下のいずれかを追記に入力して実行してください。
- \`me\`: 自分自身のみになります。あなたのみが生成でき、あなた以外が生成を要求すると拒否されます。
- \`everyone\`: 全体公開になります。全てのユーザーが許可なしにあなたのユーズでMake it a Quoteを生成できます。
- \`consent\`: 許可制になります。生成を要求されるとあなたにメンションが届き許可するかを選択できます。
`,
"miq allow": `
Make it a Quoteの許可制にて生成を許可します。
形式が異なる場合生成されません。
`,
"delete": `
BOTのユーズにて返信で使用すると返信先のユーズを削除します。
`,
"report": `
不具合などを運営者にメールで報告できます。
運営者によって有効化されていないと使用できません。
\`/report\`を使用してそのユーズの追記に内容を入力することで使用できます。
`,
"legal terms": `
利用規約を返信します。
`,
"legal privacy": `
プライバシーポリシーを返信します。
`,
} as { [key: string]: string };
export default async function Help(data: ueuse) {
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
if (
data.abi === "none" ||
data.abi === ""
) {
const helpMsg =
Object.entries(helpsMin)
.map(([command, message]) =>
`\`/${command}\`${message}`
).join('\n');
const ueuse = await Reply(`
${helpMsg}
BOTの概要は\`/info\`をご利用ください。
Wikiを見る:${packageJson.repository.url}/wiki
`, data.uniqid);
console.log("ヘルプ:", ueuse);
} else {
if (Object.keys(helpsFull).indexOf(data.abi) === -1) {
const ueuse = await Reply(`
不明なコマンドです。
機能を見る:${packageJson.repository.url}/wiki
`, data.uniqid);
console.log("ヘルプ(不明コマンド)", ueuse);
} else {
const ueuse = await Reply(`
${helpsFull[data.abi]}
機能を見る:${packageJson.repository.url}/wiki
`, data.uniqid);
console.log("ヘルプ:", ueuse);
}
}
}
+63
View File
@@ -0,0 +1,63 @@
import { ueuse } from "@/types/types";
import config from "@/lib/config";
export default async function Info(data: ueuse) {
const packageJson = JSON.parse((await import("fs")).readFileSync("package.json", "utf-8"));
const releaseUrl = `${packageJson.repository.url}/releases/tag/${packageJson.tag}`;
let editor = "";
if (packageJson.author.name !== "Last2014") {
editor = `\nEdited by ${packageJson.author.name}`;
}
let adminMail;
if (config.admin.showMail === false) {
adminMail = "非公開";
} else {
adminMail = config.admin.showMail;
}
let isReport;
if (config.report.isEnabled) {
isReport = "有効";
} else {
isReport = "無効";
}
const ueuse = await (await import("@/scripts/commands/main")).Reply(`
【BOTについて】
このBOTはオープンソースソフトウェアであるnoticeUwuzuを利用して運営されています。
noticeUwuzuはApache License 2.0によって保護されています。
ライセンスに違反して使用した場合は著作権法違反となります。
バージョン:${packageJson.version}
リリース詳細:${releaseUrl}
【運営者情報】
運営者名:${config.admin.name}
メールアドレス:${adminMail}
報告機能(\`/report\`)${isReport}
【関連コマンド】
コマンドのヘルプをお探しですか?
\`/help\`をご利用ください。
運営者へ報告が必要ですか?
\`/report\`をご利用ください。
利用規約をお探しですか?
\`/legal terms\`をご利用ください。
プライバシーポリシーをお探しですか?
\`/legal privacy\`をご利用ください。
【クレジット】
Created by Last2014${editor}
`, data.uniqid);
console.log("概要:", ueuse);
}
+8
View File
@@ -0,0 +1,8 @@
import { ueuse } from "@/types/types";
export default async function PrivacyPolicy(data: ueuse) {
(await import("@/scripts/commands/main")).Reply(
(await import("@/lib/config")).default.legal.privacy,
data.uniqid
);
}
+8
View File
@@ -0,0 +1,8 @@
import { ueuse } from "@/types/types";
export default async function Terms(data: ueuse) {
(await import("@/scripts/commands/main")).Reply(
(await import("@/lib/config")).default.legal.terms,
data.uniqid
);
}
+195
View File
@@ -0,0 +1,195 @@
import * as fs from "fs";
import config from "@/lib/config";
import type { ueuse } from "@/types/types";
// 初期化
if (!fs.existsSync("data/alreadyCommands.json")) {
fs.writeFileSync(
"data/alreadyCommands.json",
JSON.stringify([]),
"utf-8",
);
}
// 対応済みユーズ一覧
const alreadyCommands: Array<string> =
JSON.parse(fs.readFileSync("data/alreadyCommands.json", "utf-8"));
function cutAfterChar(str: string, char: string) {
const index = str.indexOf(char);
if (index === -1) {
return "";
}
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",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text: text,
replyid: reply,
}),
cache: "no-store",
});
if (req.status < 200 || req.status > 299) {
return;
}
const res = await req.json();
return res;
}
function alreadyAdd(data: string) {
alreadyCommands[alreadyCommands.length] = data;
fs.writeFileSync(
"data/alreadyCommands.json",
JSON.stringify(alreadyCommands),
"utf-8",
);
}
export default async function Commands() {
const mentionsReq = await fetch(
`${config.uwuzu.host}/api/ueuse/mentions`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
}),
cache: "no-store",
}
);
if (mentionsReq.status < 200 || mentionsReq.status > 299) {
return;
}
const mentions: { [key: string]: ueuse } = await mentionsReq.json();
console.log("----------------");
console.log("コマンド処理");
for (const key in mentions) {
if (mentions.hasOwnProperty(key)) {
const data = mentions[key];
// 除外ユーズ
if (data.text === undefined) {
break;
}
if (alreadyCommands.indexOf(data.uniqid) !== -1) {
break;
}
if (
data.text.charAt(0) === "!" ||
data.text.charAt(0) === "" ||
data.abi.indexOf("ignore") !== -1
) {
break;
}
if (data.text.indexOf("/") === -1) {
break;
}
// コマンド処理
console.log("--------");
const commandName = await commandSearch(data.text);
alreadyAdd(data.uniqid);
switch (commandName) {
case "info":
(await import("@/scripts/commands/info")).default(data);
break;
case "help":
(await import("@/scripts/commands/help")).default(data);
break;
case "legal terms":
(await import("@/scripts/commands/legal/terms")).default(data);
break;
case "legal privacy":
(await import("@/scripts/commands/legal/privacy")).default(data);
break;
case "report":
(await import("@/scripts/commands/report")).default(data);
break;
case "follow":
(await import("@/scripts/commands/follow")).default(data);
break;
case "unfollow":
(await import("@/scripts/commands/unfollow")).default(data);
break;
case "weather":
(await import("@/scripts/commands/weather")).default(data);
break;
case "miq":
(await import("@/scripts/commands/miq/main")).default(data);
break;
case "miq permission":
(await import("@/scripts/commands/miq/permission")).default(data);
break;
case "miq allow":
(await import("@/scripts/commands/miq/allow")).default(data);
break;
case "delete":
(await import("@/scripts/commands/delete")).default(data);
break;
default:
const reply = await Reply(`
不明なコマンドです。
コマンド実行を除外する場合は1文字目に\`!\`を入れてください。
`, data.uniqid);
console.log("未対応コマンド: ", reply);
break;
}
}
}
}
+155
View File
@@ -0,0 +1,155 @@
import { meApi, ueuse } from "@/types/types";
import config from "@/lib/config";
import { Reply } from "@/scripts/commands/main";
import { Permission } from "@/scripts/commands/miq/permission";
import MiQ from "miq/main";
export default async function MiQAllow(data: ueuse) {
if (!config.miq) {
console.log("MiQ(管理者無効)", await Reply(`
BOT管理者によってMake it a quoteが無効化されています。
そのため\`/miq\`はご利用いただけません。
`, data.uniqid));
return;
}
if (data.replyid === "") {
console.log("MiQ許可制(誤ユーズ)", await Reply(`
形式が正規ではありません。
`, data.uniqid));
}
// 権限一覧取得
const permissions: { [user: string]: Permission } =
JSON.parse((await import("fs")).readFileSync("data/miqPermissions.json", "utf-8"));
if (permissions[data.account.userid] !== "consent") {
console.log("MiQ許可制(許可制以外)", await Reply(`
あなたに対してのMake it a Quoteの生成要求者が許可制に設定されていません。
そのため、\`/miq allow\`はご利用いただけません。
`, data.uniqid));
return;
}
const confirmUeuseReq = await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: data.replyid,
}),
});
if (confirmUeuseReq.status < 200 || confirmUeuseReq.status > 299) {
return;
}
const confirmUeuse: ueuse = (await confirmUeuseReq.json())["0"];
const meReq = await fetch(`${config.uwuzu.host}/api/me/`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken
}),
});
if (meReq.status < 200 || meReq.status > 299) {
return;
}
const me: meApi = await meReq.json()
if (confirmUeuse.account.userid !== me.userid) {
console.log("MiQ許可制(誤アカウント)", await Reply(`
返信先がこのBOTではありません。
`, data.uniqid));
return;
}
if (confirmUeuse.replyid === "") {
console.log("MiQ許可制(誤ユーズ)", await Reply(`
形式が正規ではありません。
`, data.uniqid));
return;
}
const requestUeuseReq = await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: confirmUeuse.replyid,
}),
});
if (requestUeuseReq.status < 200 || requestUeuseReq.status > 299) {
return;
}
const requestUeuse: ueuse = (await requestUeuseReq.json())["0"];
if (requestUeuse.replyid === "") {
console.log("MiQ許可制(誤ユーズ)", await Reply(`
形式が正規ではありません。
`, data.uniqid));
return;
}
const miqUeuseReq = await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: requestUeuse.replyid,
}),
});
if (miqUeuseReq.status < 200 || miqUeuseReq.status > 299) {
return;
}
const miqUeuse: ueuse = (await miqUeuseReq.json())["0"];
let color: boolean;
let msg: string;
if (requestUeuse.abi === "color: true") {
msg = "カラーモードでMake it a Quoteを生成しました。";
color = true;
} else if (requestUeuse.abi === "color: false") {
msg = "モノクロモードでMake it a Quoteを生成しました。";
color = false;
} else {
msg = "ご指定がないためモノクロモードでMake it a Quoteを生成しました。";
color = false;
}
const img = await MiQ({
type: "Base64Data",
color: color,
text: miqUeuse.text,
iconURL: miqUeuse.account.user_icon,
userName: miqUeuse.account.username,
userID: miqUeuse.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,
nsfw: miqUeuse.nsfw,
replyid: data.uniqid,
}),
cache: "no-store",
});
if (req.status < 200 || req.status > 299) {
return;
}
const res = await req.json();
console.log("MiQ(許可制)", res);
}
+111
View File
@@ -0,0 +1,111 @@
import { ueuse } from "@/types/types";
import config from "@/lib/config";
import { Reply } from "@/scripts/commands/main";
import type { Permission } from "@/scripts/commands/miq/permission";
export default async function MakeItAQuote(data: ueuse) {
if (!config.miq) {
console.log("MiQ(管理者無効)", await Reply(`
BOT管理者によってMake it a quoteが無効化されています。
そのため\`/miq\`はご利用いただけません。
`, data.uniqid));
return;
}
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 ueuseDataReq = await fetch(`${config.uwuzu.host}/api/ueuse/get`, {
method: "POST",
cache: "no-store",
body: JSON.stringify({
token: config.uwuzu.apiToken,
uniqid: data.replyid,
}),
});
if (ueuseDataReq.status < 200 || ueuseDataReq.status > 299) {
return;
}
const ueuseData: ueuse = (await ueuseDataReq.json())["0"];
console.log(ueuseData);
// 権限一覧取得
const permissions: { [user: string]: Permission } =
JSON.parse((await import("fs")).readFileSync("data/miqPermissions.json", "utf-8"));
// 初期化
if (permissions[ueuseData.account.userid] === undefined) {
permissions[ueuseData.account.userid] = "consent";
(await import("fs")).writeFileSync(
"data/miqPermissions.json",
JSON.stringify(permissions),
"utf-8"
);
}
if (
permissions[ueuseData.account.userid] === "me" &&
ueuseData.account.userid !== data.account.userid
) {
console.log("MiQ(自分自身専用)", await Reply(`
生成元ユーズの投稿者が生成要求者を自分自身のみに設定しています。
しかし、あなたは投稿者自身ではないためこのユーズにMake it a Quoteを使用することはできません。
`, data.uniqid));
return;
}
if (
permissions[ueuseData.account.userid] === "consent" &&
data.account.userid !== ueuseData.account.userid
) {
console.log("MiQ(許可制)", await Reply(`
生成元ユーズの投稿者が生成要求者を許可制に設定しています。
このユーズにMake it a Quoteを使用するには、
@${ueuseData.account.userid}さんがこのユーズに返信で\`/miq allow\`を使用する必要があります。
`, data.uniqid));
return;
}
const img = await (await import("../../../../miq/main")).default({
type: "Base64Data",
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,
nsfw: data.nsfw,
replyid: data.uniqid,
}),
cache: "no-store",
});
if (req.status < 200 || req.status > 299) {
return;
}
const res = await req.json();
console.log("MiQ", res);
}
+78
View File
@@ -0,0 +1,78 @@
import { ueuse } from "@/types/types";
import { Reply } from "@/scripts/commands/main";
import { writeFileSync } from "fs";
// 初期化
const initialFile = {};
if (!(await import("fs")).existsSync("data/miqPermissions.json")) {
writeFileSync(
"data/miqPermissions.json",
JSON.stringify(initialFile),
"utf-8"
);
}
export type Permission =
"consent" |
"everyone" |
"me";
const PermissionsNames: { [name: string]: string } = {
"consent": "許可制",
"everyone": "全体公開",
"me": "自身のみ",
}
export default async function MiQPermission(data: ueuse) {
if (!(await import("../../../../config")).default.miq) {
await Reply(`
BOT管理者によってMake it a quoteが無効化されています。
そのため\`/miq\`はご利用いただけません。
`, data.uniqid);
return;
}
const permissions: { [user: string]: string } =
JSON.parse((await import("fs")).readFileSync("data/miqPermissions.json", "utf-8"));
// 初期化
if (permissions[data.account.userid] === undefined) {
permissions[data.account.userid] = "consent";
writeFileSync(
"data/miqPermissions.json",
JSON.stringify(permissions),
"utf-8"
);
}
if (
data.abi === "" ||
data.abi === "none"
) {
await Reply(`
あなたに対してのMake it a Quoteを生成するための権限は「${PermissionsNames[permissions[data.account.userid]]}」です。
`, data.uniqid);
return;
}
const requestPermission = data.abi.trimEnd();
if (PermissionsNames[requestPermission] === undefined) {
await Reply(`
変更希望の権限「${requestPermission}」はご指定できません。
\`consent\`\`everyone\`\`me\`のいずれかからご選択ください。
`, data.uniqid);
return;
}
permissions[data.account.userid] = requestPermission;
writeFileSync(
"data/miqPermissions.json",
JSON.stringify(permissions),
"utf-8"
);
await Reply(`
あなたに対してのMake it a Quoteを生成するための権限を「${PermissionsNames[requestPermission]}」に変更しました。
`, data.uniqid);
}
@@ -1,7 +1,6 @@
import { ueuse } from "types/types.js"; import { ueuse } from "@/types/types";
import { Reply } from "./main.js"; import { Reply } from "@/scripts/commands/main";
import config from "config.js"; import config from "@/lib/config";
import sendMail from "../../src/mailer.js";
export default async function Report(data: ueuse) { export default async function Report(data: ueuse) {
if ( if (
@@ -15,11 +14,15 @@ export default async function Report(data: ueuse) {
return; return;
} }
if (data.abi === "ignore") { if (!config.emergency.isEnabled) {
console.log("報告(重要通知オフ)", await Reply(`
BOTの運営者によって重要通知が無効化されています
`, data.uniqid));
return; return;
} }
if (!config.emergency.mail.function) { if (!config.emergency.mail.isEnabled) {
console.log("報告(メールオフ)", await Reply(` console.log("報告(メールオフ)", await Reply(`
BOTの運営者によってメール送信機能が無効化されています BOTの運営者によってメール送信機能が無効化されています
@@ -27,7 +30,7 @@ export default async function Report(data: ueuse) {
return; return;
} }
if (!config.emergency.report) { if (!config.report.isEnabled) {
console.log("報告(機能オフ)", await Reply(` console.log("報告(機能オフ)", await Reply(`
BOTの運営者によって報告機能が無効化されています BOTの運営者によって報告機能が無効化されています
@@ -36,7 +39,7 @@ export default async function Report(data: ueuse) {
} }
try { try {
sendMail({ (await import("@/lib/mailer")).default({
to: config.emergency.mail.to, to: config.emergency.mail.to,
subject: "【報告】BOT利用者からの報告", subject: "【報告】BOT利用者からの報告",
text: ` text: `
@@ -44,7 +47,7 @@ export default async function Report(data: ueuse) {
BOT管理者さんnoticeUwuzu自動送信メールです BOT管理者さんnoticeUwuzu自動送信メールです
@${data.account.userid}@${config.uwuzu.host}/reportコマンドを利用した報告がありました @${data.account.userid}@${config.uwuzu.host}/reportコマンドを利用した報告がありました
https://${config.uwuzu.host}/!${data.uniqid} ${config.uwuzu.host}/!${data.uniqid}
${data.abi} ${data.abi}
`, `,
@@ -53,7 +56,8 @@ export default async function Report(data: ueuse) {
console.log("報告(完了)", await Reply(` console.log("報告(完了)", await Reply(`
URL URL
BOTからブロックされる可能性があります ----
${config.report.message}
`, data.uniqid)); `, data.uniqid));
return; return;
@@ -1,9 +1,9 @@
import { ueuse } from "types/types.js"; import { ueuse } from "@/types/types";
import config from "../../config.js"; import config from "@/lib/config";
import { Reply } from "./main.js"; import { Reply } from "@/scripts/commands/main";
export default async function UnFollow(data: ueuse) { export default async function UnFollow(data: ueuse) {
const unfollowReq = await fetch(`https://${config.uwuzu.host}/api/users/unfollow`, { const unfollowReq = await fetch(`${config.uwuzu.host}/api/users/unfollow`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
@@ -12,6 +12,10 @@ export default async function UnFollow(data: ueuse) {
cache: "no-store", cache: "no-store",
}); });
if (unfollowReq.status < 200 || unfollowReq.status > 299) {
return;
}
const unfollowRes = await unfollowReq.json(); const unfollowRes = await unfollowReq.json();
console.log("フォロー解除: ", unfollowRes); console.log("フォロー解除: ", unfollowRes);
+6
View File
@@ -0,0 +1,6 @@
import { weatherReply } from "@/scripts/weatherNotice";
import { ueuse } from "@/types/types";
export default function Weather(data: ueuse) {
weatherReply(data.uniqid);
}
@@ -1,7 +1,6 @@
import WebSocket from "ws"; import WebSocket from "ws";
import sendMail from "../src/mailer.js"; import sendMail from "@/lib/mailer";
import config from "@/lib/config";
import config from "../config.js";
class P2PEarthquakeClient { class P2PEarthquakeClient {
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
@@ -276,7 +275,7 @@ async function event(earthquakeInfo: any): Promise<void> {
if ( if (
earthquakeInfo.earthquake.maxScale !== undefined && earthquakeInfo.earthquake.maxScale !== undefined &&
earthquakeInfo.earthquake.maxScale >= 60 && earthquakeInfo.earthquake.maxScale >= 60 &&
config.emergency.function config.emergency.isEnabled
) { ) {
console.log("----------------"); console.log("----------------");
@@ -284,7 +283,7 @@ async function event(earthquakeInfo: any): Promise<void> {
console.log("サーバーがダウンする可能性があります"); console.log("サーバーがダウンする可能性があります");
// メール送信 // メール送信
if (config.emergency.function) { if (config.emergency.mail.isEnabled) {
sendMail({ sendMail({
to: config.emergency.mail.to, to: config.emergency.mail.to,
subject: "【警告】震度6強以上の地震を受信しました", subject: "【警告】震度6強以上の地震を受信しました",
@@ -485,7 +484,7 @@ async function event(earthquakeInfo: any): Promise<void> {
} }
async function ueuse(text: string) { async function ueuse(text: string) {
const res = await fetch(`https://${config.uwuzu.host}/api/ueuse/create`, { const res = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
token: config.uwuzu.apiToken, token: config.uwuzu.apiToken,
@@ -494,6 +493,10 @@ async function ueuse(text: string) {
cache: "no-store", cache: "no-store",
}); });
if (res.status < 200 || res.status > 299) {
return;
}
const resData = await res.json(); const resData = await res.json();
console.log(`地震情報投稿:${JSON.stringify(resData)}`); console.log(`地震情報投稿:${JSON.stringify(resData)}`);
+34
View File
@@ -0,0 +1,34 @@
import { format } from "date-fns";
import eventdays from "@/constants/eventday";
import config from "@/lib/config";
export default async function EventDays() {
const now = format(new Date(), "MM/dd");
for (let i = 0; i < Object.keys(eventdays).length; i++) {
const day = Object.keys(eventdays)[i];
const value = Object.values(eventdays)[i];
const name = value.name;
const message = value.message;
if (day === now) {
const req = await fetch(`${config.uwuzu.host}/api/ueuse/create`, {
method: "POST",
body: JSON.stringify({
token: config.uwuzu.apiToken,
text:
`今日は${name}です
${message}`,
}),
});
if (req.status < 200 || req.status > 299) {
return;
}
const res = await req.json();
console.log("祝日等ユーズ:", res);
}
}
}
@@ -1,8 +1,7 @@
import * as fs from "fs"; import * as fs from "fs";
import { isBefore } from "date-fns/fp"; import { isAfter } from "date-fns";
import config from "@/lib/config";
import config from "../config.js"; import sendMail from "@/lib/mailer";
import sendMail from "../src/mailer.js";
export default function successExit() { export default function successExit() {
// 初期化 // 初期化
@@ -15,15 +14,15 @@ export default function successExit() {
const iolog = JSON.parse(fs.readFileSync("logs/boot.json", "utf-8")); const iolog = JSON.parse(fs.readFileSync("logs/boot.json", "utf-8"));
if (config.emergency.function) { if (config.emergency.isEnabled) {
// 前回の終了確認 // 前回の終了確認
const start = iolog.start; const start = iolog.start;
const stop = iolog.stop; const stop = iolog.stop;
if (isBefore(start, stop)) { if (isAfter(start, stop)) {
console.log("前回の終了が適切でない可能性があります"); console.log("前回の終了が適切でない可能性があります");
if (config.emergency.mail.function) { if (config.emergency.mail.isEnabled) {
sendMail({ sendMail({
to: config.emergency.mail.to, to: config.emergency.mail.to,
subject: "【警告】前回終了が不適切な可能性", subject: "【警告】前回終了が不適切な可能性",
@@ -56,5 +55,3 @@ export default function successExit() {
fs.writeFileSync("logs/boot.json", JSON.stringify(iolog), "utf-8"); fs.writeFileSync("logs/boot.json", JSON.stringify(iolog), "utf-8");
}); });
} }
successExit();
@@ -1,7 +1,6 @@
import { format } from "date-fns"; import { format } from "date-fns";
import type * as types from "@/types/types";
import type * as types from "types/types"; import config from "@/lib/config";
import config from "../config.js";
export default async function timeNotice() { export default async function timeNotice() {
// 停止時間 // 停止時間
@@ -28,7 +27,7 @@ export default async function timeNotice() {
} else { } else {
// 投稿 // 投稿
const resUeuse = await fetch( const resUeuse = await fetch(
`https://${config.uwuzu.host}/api/ueuse/create`, `${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@@ -39,6 +38,10 @@ export default async function timeNotice() {
}, },
); );
if (resUeuse.status < 200 || resUeuse.status > 299) {
return;
}
const ueuseData: types.ueuseCreateApi = await resUeuse.json(); const ueuseData: types.ueuseCreateApi = await resUeuse.json();
console.log("----------------"); console.log("----------------");
@@ -1,15 +1,13 @@
import { cityList } from "../src/weatherId.js"; import { cityList } from "@/constants/weather";
import type * as types from "@/types/types";
import type * as types from "types/types.js"; import config from "@/lib/config";
import config from "../config.js";
export async function weatherNotice() { export async function weatherNotice() {
console.log("----------------"); console.log("----------------");
// 仮投稿 // 仮投稿
const resUeuse = await fetch( const resUeuse = await fetch(
`https://${config.uwuzu.host}/api/ueuse/create`, `${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@@ -23,6 +21,10 @@ export async function weatherNotice() {
}, },
); );
if (resUeuse.status < 200 || resUeuse.status > 299) {
return;
}
const ueuseData: types.ueuseCreateApi = await resUeuse.json(); const ueuseData: types.ueuseCreateApi = await resUeuse.json();
console.log(`天気仮投稿:${JSON.stringify(ueuseData)}`); console.log(`天気仮投稿:${JSON.stringify(ueuseData)}`);
@@ -107,7 +109,7 @@ export async function weatherReply(uniqid: string) {
// 分割投稿 // 分割投稿
for (let i = 0; i < splitCount; i++) { for (let i = 0; i < splitCount; i++) {
const resReply = await fetch( const resReply = await fetch(
`https://${config.uwuzu.host}/api/ueuse/create`, `${config.uwuzu.host}/api/ueuse/create`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@@ -119,6 +121,10 @@ export async function weatherReply(uniqid: string) {
}, },
); );
if (resReply.status < 200 || resReply.status > 299) {
return;
}
const replyData: types.ueuseCreateApi = await resReply.json(); const replyData: types.ueuseCreateApi = await resReply.json();
console.log(`天気返信:${JSON.stringify(replyData)}`); console.log(`天気返信:${JSON.stringify(replyData)}`);
+58
View File
@@ -0,0 +1,58 @@
export default interface Config {
time: {
stopTimes: {
start: number;
stop: number;
};
};
earthquake: {
reconnectTimes: number;
websocketUrl: string;
areasCsvUrl: string;
maxScaleMin: number;
};
weather: {
splitCount: number;
};
miq: boolean;
emergency: {
isEnabled: true;
mail: {
isEnabled: true;
host: string;
port: number;
user: string;
password: string;
secure: boolean;
to: string | string[];
} | {
isEnabled: false;
};
} | {
isEnabled: false;
};
report: {
isEnabled: boolean;
message: string;
};
legal: {
terms: string;
privacy: string;
};
admin: {
name: string;
showMail: string | false;
panel: {
isEnabled: true;
port: number;
} | {
isEnabled: false;
};
};
uwuzu: {
apiToken: string;
host: string;
};
debug?: true;
}
+14 -5
View File
@@ -25,18 +25,21 @@ export interface meApi {
export interface ueuse { export interface ueuse {
uniqid: string; uniqid: string;
replyid: string;
reuseid: string;
text: string; text: string;
account: { account: {
username: string; username: string;
userid: string; userid: string;
user_icon: string; user_icon: string;
user_header: string; user_header: string;
is_bot: boolean;
}; };
photo1: Base64URLString; photo1: string;
photo2: Base64URLString; photo2: string;
photo3: Base64URLString; photo3: string;
photo4: Base64URLString; photo4: string;
video1: Base64URLString; video1: string;
favorite: Array<string>; favorite: Array<string>;
favorite_cnt: string; favorite_cnt: string;
datetime: string; datetime: string;
@@ -53,3 +56,9 @@ export interface ueuseCreateApi {
export interface followApi { export interface followApi {
userid: string; userid: string;
} }
export interface NetworkInterfaceDetails {
family: string;
internal: boolean;
address: string;
}
View File
+7 -2
View File
@@ -12,12 +12,17 @@
"baseUrl": "./", "baseUrl": "./",
"typeRoots": [ "typeRoots": [
"./node_modules/@types", "./node_modules/@types",
"./types" "./src/types"
], ],
"paths": { "paths": {
"@/*": ["./src/*"],
"miq": ["./miq/*"],
"ws": ["./node_modules/ws/index.js"], "ws": ["./node_modules/ws/index.js"],
"@types/ws": ["./node_modules/@types/ws/index.d.ts"] "@types/ws": ["./node_modules/@types/ws/index.d.ts"],
}, },
"removeComments": true, "removeComments": true,
}, },
"tsc-alias": {
"resolveFullPaths": true,
},
} }
-50
View File
@@ -1,50 +0,0 @@
interface earthquakeTypes {
reconnectTimes: number;
websocketUrl: string;
areasCsvUrl: string;
maxScaleMin: number;
}
interface weatherTypes {
splitCount: number;
}
interface stopsTypes {
start: number;
stop: number;
}
interface timeTypes {
stopTimes: stopsTypes;
}
interface emergencyMailTypes {
function: Boolean;
host: string | undefined;
port: number;
user: string;
password: string;
secure: Boolean;
to: string;
}
interface emergencyTypes {
function: Boolean;
mail: emergencyMailTypes;
report: Boolean;
}
interface uwuzuTypes {
apiToken: string;
clientToken?: string;
host: string;
}
export interface configTypes {
time: timeTypes,
earthquake: earthquakeTypes;
weather: weatherTypes;
emergency: emergencyTypes;
uwuzu: uwuzuTypes;
}