Compare commits

...

19 Commits

Author SHA1 Message Date
last2014 391694ca45 Merge pull request '2026.5.0' (#17) from develop into main
Reviewed-on: #17
2026-05-10 21:58:33 +00:00
last2014 ca552be9e9 Merge branch 'main' into develop 2026-05-10 21:58:26 +00:00
last2014 e099522b7e 2026.5.0 2026-05-11 06:57:43 +09:00
last2014 16b1b9d2ab Merge pull request '2026.4.3' (#16) from develop into main
Reviewed-on: #16
2026-05-03 10:19:41 +00:00
last2014 f0246d7ce9 Merge branch 'main' into develop 2026-05-03 10:19:33 +00:00
last2014 165500e71d 2026.4.3 / Chg: コマンド実行の際にメンションと通知を時系列にソートしないように 2026-05-03 19:18:58 +09:00
last2014 4f2be0e0ed Chg: forループ内でlastRead*を記録するように / Chg: lastRead*を時刻ベースに 2026-05-03 19:12:27 +09:00
last2014 7f2ef21557 Chg: コマンド実行の際にメンションと通知を時系列にソートするように 2026-05-03 18:49:01 +09:00
last2014 b33b998ae4 Merge pull request '2026.4.2' (#15) from develop into main
Reviewed-on: #15
2026-05-03 07:36:22 +00:00
last2014 e17da1686d Merge branch 'main' into develop 2026-05-03 07:36:16 +00:00
last2014 ab06f4ceec 2026.4.2 / Fix: id除外が機能しない問題 2026-05-03 16:35:59 +09:00
last2014 47fe174ac6 Merge pull request '2026.4.1' (#14) from develop into main
Reviewed-on: #14
2026-05-03 07:11:02 +00:00
last2014 747bf00e7b Merge branch 'main' into develop 2026-05-03 07:10:56 +00:00
last2014 3baa55bd29 Merge branch 'develop' of https://gitea.last2014.com/last2014/noticeUwuzu into develop 2026-05-03 16:10:06 +09:00
last2014 90e9d614f9 2026.4.1 / Chg: notificationsもfor内でlastReadを更新しないように 2026-05-03 16:09:04 +09:00
last2014 41dc68340f Merge pull request '2026.4.0' (#13) from develop into main
Reviewed-on: #13
2026-05-03 06:14:47 +00:00
last2014 0dcbbf993c Merge branch 'main' into develop 2026-05-03 06:14:42 +00:00
last2014 fbef68ce79 2026.4.0 2026-05-03 15:14:01 +09:00
last2014 953c49583c Chg: 最大震度が投稿に必要な値に満たない場合のコンソール表示を変更 / Feat: 返信ではないユーズの自主的な文字数制限 2026-05-03 15:10:37 +09:00
30 changed files with 551 additions and 36 deletions
+1
View File
@@ -3,3 +3,4 @@
/memory.json
/config/**
!/config/example.yaml
/src/feature/earthquake/generateImage/data.json
+46 -1
View File
@@ -1,3 +1,48 @@
# 2026.5.0
- Feat(dev): 地震情報の画像生成機能は未完成のため、ユーズには適用されません
- Chg: 地震情報でのタイトルを変更
- Fix: 毎日7:00に天気予報が投稿されない問題
# 2026.4.3
- Chg: forループ内でlastReadを記録するように
- Chg: lastReadを時刻ベースに
# 2026.4.2
- Fix: id除外が機能しない問題
# 2026.4.1
- Chg: notificationsもfor内でlastReadを更新しないように
# 2026.4.0
- Breaking: noticeUwuzuを0から作り直しました
- Breaking: 25.12.0-alpha.1までの方針を全て廃止しました
- Chg: パッケージマネージャーをnpmからpnpmへ変更しました
- Chg: 全コードをsrcへ移動しました
- Chg: ユーズのメッセージをyamlとi18nextで管理することで先頭と末尾の改行のある問題などに対策しました
- Chg: 全ての保存データをmemory.jsonとして保管し、memory.tsを実装しました
- Note: 以下のログは全て実装として記録しています
- Feat: ユーザーは以下のコマンドを使用することができます。
- `/weather`: 天気予報を取得できます。
- `/help`: コマンドの利用方法などを取得できます。
- `/miq`: Make it a Quoteを操作できます。
- `/follow`: Botからフォローされます。
- `/unfollow`: Botからフォロー解除されます。
- Feat: 地震情報が利用できます。以下の情報を投稿します。
- 地震発生情報
- 津波予報
- 緊急地震速報(警報)
- Feat: 時報が利用できます。毎時0分に投稿します。
- Feat: 天気予報を利用できます。毎日7:00に投稿します。
- Feat: 新年迎春の投稿を行います。遅延が最低限になるように出来ています。
- Feat: worker_threadsによる並列処理
- Feat: ユーズの再試行
- Feat: ユーズの文字数制限回避
- Feat: コマンドを並列処理
- Feat: 地震情報のWebSocket再接続
- Feat: 最大震度の要求を設定できる機能
- Feat: 最大震度が不明な場合に投稿するかどうかを設定できる機能
- Feat: デバッグ用とで過去の地震情報を読み込み10秒おきに投稿する機能
# 2026.4.0-beta.0
- Feat: 地震発生情報の地域を都道府県でグループ化する機能
- Feat: ユーズの再試行
@@ -31,7 +76,7 @@
- Del: 各地震情報の情報源の信頼できるかどうかの内容を削除
- Chg: 津波予報情報の各時刻情報を実際に取得できる値まで削減(例: 2:03:00 > 2:03)
- Fix: フォロー・フォロー解除に失敗した際にreturnできていない問題
- Feat: デバッグ用で過去の地震情報を読み込み10秒おきに投稿する機能
- Feat: デバッグ用で過去の地震情報を読み込み10秒おきに投稿する機能
# 2026.4.0-alpha.0
- Breaking: noticeUwuzuを0から作り直しました
+3 -1
View File
@@ -1,6 +1,6 @@
# noticeUwuzu
[uwuzu](https://www.uwuzu.com)向けのお知らせBotです。
uwuzu v1.6.3以上で利用可能です。
uwuzu v1.6.7以上で利用可能です。
## 機能
- 時報
@@ -22,6 +22,8 @@ uwuzu v1.6.3以上で利用可能です。
- 地震情報のWebSocketの接続先をテスト用サンドボックスに変更
P2P地震情報の接続先を本番環境から[テスト用サンドボックスのエンドポイント](https://www.p2pquake.net/develop/json_api_v2/)に変更します。
そのため、最新の情報は配信されません。
- 地震情報のid除外を無効化
地震情報のid除外を記録も確認も行わなくなります。
- i18nextのdebugを有効化
[i18nextでのdebug](https://www.i18next.com/overview/configuration-options#logging)が有効化されます。
+6
View File
@@ -33,5 +33,11 @@ ueuse:
# 再試行の間隔(ミリ秒) number
# 正数のみが有効です。
retryInterval: 1000
# 公開されたユーズの自主規制文字数 number
# 返信ではない(公開された)ユーズで使用する自主的な文字数制限です。
# 返信では、サーバーの設定に従います。
# 0にすると、返信ではないユーズでもサーバーの設定に従います。
# 0以上の整数が有効です。
maxLengthWithPublic: 512
# デバッグモードにするかどうか boolean
debug: false
+5 -5
View File
@@ -9,7 +9,7 @@ weatherReply: |
最低気温: {{ minTemp }}
降水確率: {{ chanceOfRain }}
earthquakeNotice: |
### ==地震発生==
### {{ type }}
⏰時刻: {{ occuredTime }}頃
🫨最大震度: {{ maxScale }}
📍震源地: {{ epicenter }}
@@ -26,14 +26,14 @@ tsunamiAreaMsg: |
🌊第1波の状態: {{ condition }}
🗼予想される津波の高さ: {{ maxHeight }}
tsunamiForecastNotice: |
### ==津波予報**発表**==
### 津波予報**発表**
⏰発表時刻: {{ announceTime }}
{{ areasMsg }}
🔬情報源: P2P地震速報 - {{ source }}
tsunamiCancelNotice: |
### ==津波予報**解除**==
### 津波予報**解除**
⏰発表時刻: {{ announceTime }}
🔬情報源: P2P地震速報 - {{ source }}
eewAreaMsg: |
@@ -42,7 +42,7 @@ eewAreaMsg: |
⏰到達予想時刻: {{ arrivalTime }}
{{ kind }}
eewNotice: |
### ==***緊急地震速報(警報)***==
### ***緊急地震速報(警報)***
{{ isTest }}{{isAssume}}
⏰発表時刻: {{ announceTime }}
⏰地震発生時刻: {{ occuredTime }}
@@ -53,7 +53,7 @@ eewNotice: |
🔬情報源: P2P地震速報
eewCancelNotice: |
### ==緊急地震速報(警報)**解除**==
### 緊急地震速報(警報)**解除**
{{ isTest }}
⏰発表時刻: {{ announceTime }}
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "notice-uwuzu",
"version": "26.4.0-beta.0",
"version": "2026.5.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
@@ -23,6 +23,7 @@
"miq": "git+https://gitea.last2014.com/last2014/miq.git#1.0.1",
"node-cron": "^4.2.1",
"os": "^0.1.2",
"sharp": "^0.34.5",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3",
"util": "^0.12.5",
+3
View File
@@ -33,6 +33,9 @@ importers:
specifier: ^4.2.1
version: 4.2.1
os: {specifier: ^0.1.2, version: 0.1.2}
sharp:
specifier: ^0.34.5
version: 0.34.5
tsc-alias:
specifier: ^1.8.16
version: 1.8.16
-2
View File
@@ -1,6 +1,4 @@
onlyBuiltDependencies:
- "better-uwuzu-sdk"
- canvas
- esbuild
- "miq"
- sharp
+16 -9
View File
@@ -32,9 +32,6 @@ try {
if (notification.category !== "reply" || typeof notification.valueid !== "string")
continue;
if (lastReadReply === notification.valueid)
break;
const ueuseResponse = await client.request("ueuse/get", {
uniqid: notification.valueid,
});
@@ -44,12 +41,17 @@ try {
continue;
}
const time = new Date(ueuseResponse.data[0].datetime).getTime();
if (index === 0) {
const mem = Memory.memory;
mem.lastReadReply = ueuseResponse.data[0].uniqid;
mem.lastReadReply = time;
Memory.memory = mem;
}
if (lastReadReply >= time)
break;
ueuses.push(ueuseResponse.data[0]);
}
} else {
@@ -68,14 +70,19 @@ try {
const mem = Memory.memory;
const lastReadMention = mem.lastReadMention;
mem.lastReadMention = mentions[0]?.uniqid ?? lastReadMention;
Memory.memory = mem;
for (const mention of mentions) {
if (lastReadMention === mention.uniqid) {
break;
for (const [index, mention] of mentions.entries()) {
const time = new Date(mention.datetime).getTime();
if (index === 0) {
const mem = Memory.memory;
mem.lastReadMention = time;
Memory.memory = mem;
}
if (lastReadMention >= time)
break;
ueuses.push(mention);
}
} else {
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

@@ -0,0 +1,429 @@
import { readFileSync, writeFileSync } from "node:fs";
import sharp from "sharp";
/**
* 経度からX方向の座標情報を計算します
*
* 指定された経度とズームレベルに基づき、以下の4つの値を返します:
* - 世界座標系でのX(0〜1の範囲、グリニッジ子午線が0)
* - ズームレベルを考慮したピクセル単位のグローバルX座標
* - タイルグリッドにおけるX方向のタイル番号(tileNumberX
* - タイル内(256×256ピクセル)でのX方向ピクセル位置(tileInnerX
* @param lat - 経度
* @param z - ズームレベル(0以上の整数)
* @returns {{
* worldCoordX: number, // 世界座標(01, グリニッジ子午線が原点(0))
* globalPixelX: number, // ズームレベルを考慮したピクセル単位のY座標
* tileNumberX: number, // 該当するタイル番号(0オリジン)
* tileInnerX: number, // タイル内のピクセルY座標(0〜255)
* }}
* @copyright murasuke on Qiita - https://qiita.com/murasuke/items/ad81b7b726a3463fa3fe#%E7%B5%8C%E5%BA%A6
*/
function calcLngToTileX(lng: number, z: number) {
// 経度(-180〜180)を世界座標系のX(0〜1)に変換
const worldCoordX = (lng + 180) / 360;
// ズームレベル0(256×256px)の座標の位置を計算
let globalPixelX = worldCoordX * 256;
// ズームレベル(2^z)を考慮した座標を計算
globalPixelX = globalPixelX * Math.pow(2, z);
// 1つの画像タイルが256pxなので、256で割って左端からのタイルの枚数(位置)を求める
const tileNumberX = Math.floor(globalPixelX / 256);
// 該当タイル内のピクセル位置を算出する(タイル幅合計を引いた余り)
const tileInnerX = Math.floor(globalPixelX - tileNumberX * 256);
// 計算した値を返す
return {
worldCoordX, // 世界座標(0~1, グリニッジ子午線が原点(0))
globalPixelX, // ズームレベルを考慮したピクセル単位のY座標
tileNumberX, // 該当するタイル番号(0オリジン)
tileInnerX, // タイル内のピクセルY座標(0〜255)
};
}
/**
* 緯度からWebメルカトル投影によるY方向の座標情報を計算します。
*
* 指定された緯度とズームレベルに基づき、以下の4つの値を返します:
* - 世界座標系でのY(0〜1の範囲、北極が0)
* - ズームレベルを考慮したピクセル単位のグローバルY座標
* - タイルグリッドにおけるY方向のタイル番号(tileNumberY
* - タイル内(256×256ピクセル)でのY方向ピクセル位置(tileInnerY
*
* メルカトル図法で緯度から位置を算出する式 (https://qiita.com/Seo-4d696b75/items/aa6adfbfba404fcd65aa)
* R ln(tan(π/4 + ϕ/2))
* R: 半径
* ϕ: 緯度(ラジアン)
* @param lat - 緯度(-85〜+85度の範囲が推奨)
* @param z - ズームレベル(0以上の整数)
* @returns {{
* worldCoordY: number, // 世界座標系Y0〜1
* globalPixelY: number, // ズームレベルを考慮したピクセル単位のY座標
* tileNumberY: number, // Y方向のタイル番号(0オリジン)
* tileInnerY: number // タイル内のピクセルY座標(0〜255)
* }}
* @copyright murasuke on Qiita - https://qiita.com/murasuke/items/ad81b7b726a3463fa3fe#%E7%B7%AF%E5%BA%A6
*/
function calcLatToTileY(lat: number, z: number) {
// 緯度からメルカトル図法の座標に変換して、世界座標系のY(0~1)を計算
let worldCoordY = latToWorldY(lat);
// ズームレベル0(256×256px)の座標の位置を計算
let globalPixelY = worldCoordY * 256;
// ズームレベル(2^z)を考慮した座標を計算
globalPixelY = globalPixelY * Math.pow(2, z);
// 1つの画像タイルが256pxなので、256で割って上端からのタイルの枚数(位置)を求める
const tileNumberY = Math.floor(globalPixelY / 256);
// 該当タイル内のピクセル位置を算出する(タイル幅合計を引いた余り)
const tileInnerY = Math.floor(globalPixelY - tileNumberY * 256);
// 計算した値を返す
return {
worldCoordY, // 世界座標(0~1, 北極側が原点(0))
globalPixelY, // ズームレベルを考慮したピクセル単位のY座標
tileNumberY, // 該当するタイル番号(0オリジン)
tileInnerY, // タイル内のピクセルY座標(0〜255)
};
}
/**
* メルカトル図法で投影した位置を元に世界座標系でのY(0〜1、北極が0)を計算する
*
* メルカトル図法の式(半径1の球体を想定)
* ϕ = (π / 180) * 緯度
* y = ln(tan(π/4 + ϕ/2))
* ϕ:緯度(ラジアン)
* y:投影した位置
*
* * 赤道を中心(0)にして、約-85〜+85度の範囲で-π~πの値になる
* * 高緯度では値が急増し無限大になる
* * y = ln((tan(ϕ) + 1) / cos(ϕ)) という式もある(同じ値になる)
*
* 地図で位置を計算するため、北極側が原点(0)で南極側が1となるように正規化した値を返す
*
* @param lat 緯度(-8585)
* @returns {number} 世界座標系でのY0〜1、北極が0)
* @copyright murasuke on Qiita - https://qiita.com/murasuke/items/ad81b7b726a3463fa3fe#%E7%B7%AF%E5%BA%A6
*/
function latToWorldY(lat: number) {
// 経度をラジアンに変換
const latRad = (Math.PI / 180) * lat;
// 緯度からメルカトル図法の座標に変換する
const mercatorY = Math.log(Math.tan(Math.PI / 4 + latRad / 2));
// -1~1 の範囲になるように調整(-85度~85度の範囲)
let worldCoordY = mercatorY / Math.PI;
// 原点(0)を赤道から、北極へ変換(0~1)
worldCoordY = 1 - (worldCoordY + 1) / 2;
return worldCoordY;
}
function getEdge<T extends any[]>(arr: T, property: keyof T[number]) {
if (arr.length === 0) {
return {
most: undefined,
least: undefined,
}
}
const counts = new Map<any, number>();
for (const item of arr) {
const val = item[property];
counts.set(val, (counts.get(val) || 0) + 1);
}
const uniqueValues = Array.from(counts.keys());
const minVal = uniqueValues.reduce((a, b) => (a < b ? a : b));
const maxVal = uniqueValues.reduce((a, b) => (a > b ? a : b));
return {
most: maxVal,
least: minVal,
}
}
export default async function generateImage(message: any) {
if (
message.earthquake.hypocenter === undefined &&
message.points === undefined
)
return "input_lack";
const ZOOM_LEVEL = 8;
// タイル・地点取得
const tiles: {
tileX: number;
tileY: number;
}[] = [];
const positions: ({
tileIndex: number;
innerX: number;
innerY: number;
} & ({
type: "epicenter";
} | {
type: "point";
scale: number;
}))[] = [];
if (
message.earthquake.hypocenter?.latitude !== undefined &&
message.earthquake.hypocenter?.latitude !== -200 &&
message.earthquake.hypocenter?.longitude !== undefined &&
message.earthquake.hypocenter?.longitude !== -200
) {
// 震源地
const lng = message.earthquake.hypocenter.longitude;
const lat = message.earthquake.hypocenter.latitude;
const xData = calcLngToTileX(lng, ZOOM_LEVEL);
const yData = calcLatToTileY(lat, ZOOM_LEVEL);
tiles.push({
tileX: xData.tileNumberX,
tileY: yData.tileNumberY,
});
positions.push({
tileIndex: 0,
innerX: xData.tileInnerX,
innerY: yData.tileInnerY,
type: "epicenter",
});
}
if (
Array.isArray(message.points) &&
message.points.length > 0
) {
// 観測点・区域
const areas = JSON.parse(readFileSync(`${import.meta.dirname}/areas.json`, "utf-8"));
const points = JSON.parse(readFileSync(`${import.meta.dirname}/points.json`, "utf-8"));
message.points.forEach((point: any) => {
let areaData: {
longitude: number;
latitude: number;
}
if (point.isArea) {
areaData = areas[point.addr];
} else {
areaData = points[point.addr];
}
const lng = areaData.longitude;
const lat = areaData.latitude;
const xData = calcLngToTileX(lng, ZOOM_LEVEL);
const yData = calcLatToTileY(lat, ZOOM_LEVEL);
const tilePosition = {
tileX: xData.tileNumberX,
tileY: yData.tileNumberY,
}
let tileIndex: number;
const sameTile = tiles
.map((value, index) => ({ value, index }))
.filter(tile => JSON.stringify(tile.value) === JSON.stringify(tilePosition))[0];
if (sameTile) {
tileIndex = sameTile.index;
} else {
tiles.push(tilePosition);
tileIndex = tiles.length - 1;
}
if (tiles.filter(tile => JSON.stringify(tile) === JSON.stringify(tilePosition)).length === 0) {
tiles.push(tilePosition);
}
positions.push({
tileIndex,
innerX: xData.tileInnerX,
innerY: yData.tileInnerY,
type: "point",
scale: point.scale,
});
});
}
// 画像生成
const WIDTH = 1920;
const HEIGHT = 1080;
let result = sharp({
create: {
width: WIDTH,
height: HEIGHT,
channels: 4,
background: "#ffffff",
}
});
let compounds: (sharp.OverlayOptions & ({
type: "tile" | "epicenter" | "other";
} | {
type: "scale";
scale: number;
}))[] = [];
// タイルのXYの各最大、最小を取得
const tileXCount = getEdge(tiles, "tileX");
const tileYCount = getEdge(tiles, "tileY");
// タイルサイズ決定
let tileSize: number;
const xSize = tileXCount.most - tileXCount.least + 1;
const ySize = tileYCount.most - tileYCount.least + 1;
if (xSize > ySize) {
tileSize = Math.round(WIDTH / xSize);
} else {
tileSize = Math.round(HEIGHT / ySize);
}
// アセット
const assets: Record<string, Buffer<ArrayBuffer>> = {}
const assetsList = [
"scales/10",
"scales/20",
"scales/30",
"scales/40",
"scales/45",
"scales/46",
"scales/50",
"scales/55",
"scales/60",
"scales/70",
"epicenter",
];
assetsList.forEach(name => {
const asset = readFileSync(`${import.meta.dirname}/assets/${name}.png`);
const key = name.split("/").at(-1);
if (!key)
return;
assets[key] = asset;
});
// 各画像処理
await Promise.all(tiles.map(async (tile, index) => {
const TYPE = "blank";
const BASE_URL = "https://cyberjapandata.gsi.go.jp/xyz";
const request = await fetch(`${BASE_URL}/${TYPE}/${ZOOM_LEVEL}/${tile.tileX}/${tile.tileY}.png`);
if (!request.ok)
return;
const tileImg = await sharp(await request.arrayBuffer()).resize({
width: tileSize,
height: tileSize,
}).png().toBuffer();
const top = tileSize * (tile.tileY - tileYCount.least);
const left = tileSize * (tile.tileX - tileXCount.least);
compounds.push({
type: "tile",
input: tileImg,
top,
left,
});
const itTilePositions = positions.filter(position => position.tileIndex === index);
itTilePositions.forEach(position => {
const requireAssetName = position.type === "epicenter"
? "epicenter"
: String(position.scale);
const asset = assets[requireAssetName];
if (!asset)
return;
const tileResizedSize = tileSize / 256;
let about: any;
if (position.type === "epicenter") {
about = {
type: "epicenter",
}
} else {
about = {
type: "epicenter",
scale: position.scale,
}
}
compounds.push({
...about,
input: asset,
top: Math.round(top + position.innerY * tileResizedSize),
left: Math.round(left + position.innerX * tileResizedSize),
});
});
}));
// compounds並び替え
const TYPE_PRIORITY: Record<string, number> = {
tile: 1,
scale: 2,
epicenter: 3,
}
compounds = [...compounds].sort((a, b) => {
if (TYPE_PRIORITY[a.type] !== TYPE_PRIORITY[b.type]) {
return (TYPE_PRIORITY[a.type] ?? 99) - (TYPE_PRIORITY[b.type] ?? 99);
}
if (a.type === "scale" && b.type === "scale") {
return a.scale - b.scale;
}
return 0;
});
// 案内部分
const informationPath = `${import.meta.dirname}/assets/information.png`;
const informationBuffer = readFileSync(informationPath);
const informationMeta = await sharp(informationPath).metadata();
const informationMargin = 20;
compounds.push({
type: "other",
input: informationBuffer,
top: HEIGHT - informationMeta.height - informationMargin,
left: WIDTH - informationMeta.width - informationMargin,
});
// クレジット部分
const creditPath = `${import.meta.dirname}/assets/credit.png`;
const creditBuffer = readFileSync(creditPath);
const creditMeta = await sharp(creditPath).metadata();
compounds.push({
type: "other",
input: creditBuffer,
top: HEIGHT - creditMeta.height,
left: 0,
});
result.composite(compounds);
const buffer = await result.png().toBuffer();
return buffer;
}
writeFileSync("result.png", await generateImage(JSON.parse(readFileSync(`${import.meta.dirname}/data.json`, "utf8"))));
File diff suppressed because one or more lines are too long
@@ -68,7 +68,8 @@ if (config.earthquake?.useHistoryData) {
processMessage(message);
if (!config.debug) {
mem.processedInfo.concat([id]);
const mem = Memory.memory;
mem.processedInfo = mem.processedInfo.concat([id]);
Memory.memory = mem;
}
});
@@ -87,7 +88,7 @@ const processMessage = async (message: any) => {
"30": "震度3",
"40": "震度4",
"45": "震度5弱",
"46": "**推定**震度5弱以上***(正確には不明)***",
"46": "**推定**震度5弱以上***(不明)***",
"50": "震度5強",
"55": "震度6弱",
"60": "震度6強",
@@ -116,10 +117,15 @@ const processMessage = async (message: any) => {
message.earthquake.maxScale !== -1 &&
message.earthquake.maxScale < config.earthquake.requireMaxScale
) {
console.log("投稿に必要な最大震度に満たないため、スキップします");
console.log(`最大震度(${scaleMessages[message.earthquake.maxScale]})が投稿に必要な値(${scaleMessages[config.earthquake.requireMaxScale]})に満たないため、スキップします`);
break;
}
const typeMessage: Record<string, string> = {
"ScalePrompt": "震度速報",
"Destination": "震源情報",
}
const domesticTsunamiMessages: Record<string, string> = {
"None": "😌この地震による**国内**の津波の心配はありません。",
"Unknown": "😕この地震による**国内**の***津波情報は***不明です。",
@@ -163,7 +169,7 @@ const processMessage = async (message: any) => {
.sort((a, b) => b[1].scale - a[1].scale)
.map(([label, { prefs }]) => {
const prefLines = Object.entries(prefs)
.map(([pref, addrs]) => `${pref}: ${addrs.join("・")}`)
.map(([pref, addrs]) => `**${pref}:** ${addrs.join("・")}`)
.join(EOL);
return `${label}${EOL}${prefLines}`;
@@ -173,6 +179,7 @@ const processMessage = async (message: any) => {
await createUeuse({
text: i18next.t("earthquakeNotice", {
type: typeMessage[message.issue.type] ?? "地震情報",
occuredTime: format(new Date(message.earthquake.time), "yyyy年M月d日 H:mm"),
maxScale: scaleMessages[String(message.earthquake.maxScale)],
epicenter: message.earthquake.hypocenter.name === ""
@@ -281,7 +288,7 @@ const processMessage = async (message: any) => {
? `から${scaleMessages[String(Math.floor(area.scaleTo))]}`
: ""),
kind: kindMessages[area.kindCode] ?? "😕主要動の***到達予想は***ありません。",
arrivalTime: area.arrivalTime !== undefined
arrivalTime: typeof area.arrivalTime === "string" && area.arrivalTime !== ""
? format(new Date(area.arrivalTime), "yyyy年M月d日 H:mm:ss")
: "不明",
}) + EOL.repeat(2);
@@ -306,7 +313,7 @@ const processMessage = async (message: any) => {
magnitude: message.earthquake.hypocenter.magnitude === undefined ||
message.earthquake.hypocenter.magnitude === -1
? "不明"
: `M${message.earthquake.hypocenter.magnitude}`,
: `M${message.earthquake.hypocenter.magnitude.toFixed(1)}`,
areas: areasMsg !== ""
? EOL.repeat(2) + areasMsg.trim()
: "",
+4 -2
View File
@@ -20,7 +20,7 @@ try {
await initData();
new Worker(`${import.meta.dirname}/feature/earthquakeNotice.js`);
new Worker(`${import.meta.dirname}/feature/earthquake/index.js`);
console.log("Botが起動しました");
} catch (err: any) {
@@ -36,7 +36,9 @@ try {
});
schedule("0 7 * * *", async () => {
new Worker(`${import.meta.dirname}/feature/weatherNotice.js`);
new Worker(`${import.meta.dirname}/feature/weatherNotice.js`, {
workerData: "scheduledWeatherNotice",
});
});
schedule(`*/${config.command.interval} * * * *`, async () => {
+13 -2
View File
@@ -15,7 +15,6 @@ export default client;
export const createUeuse = async (data: ApiMap["ueuse/create"]["body"], title: string) => {
const mem = Memory.memory;
const maxLength = mem.max_length;
const excessedMessage = "👉返信に続きがあります。";
let lines = data.text.split(EOL);
@@ -26,7 +25,13 @@ export const createUeuse = async (data: ApiMap["ueuse/create"]["body"], title: s
count++;
let currentText = "";
const limit = maxLength - (excessedMessage.length + EOL.length * 2);
const currentMaxLength = (data.replyid !== undefined || firstUniqid !== "")
? mem.max_length
: config.ueuse.maxLengthWithPublic === 0
? mem.max_length
: config.ueuse.maxLengthWithPublic;
const limit = currentMaxLength - (excessedMessage.length + EOL.length * 2);
while (lines.length > 0) {
const nextLine = lines[0];
@@ -50,6 +55,8 @@ export const createUeuse = async (data: ApiMap["ueuse/create"]["body"], title: s
}
}
currentText = currentText.trimEnd();
if (lines.length > 0) {
currentText += EOL.repeat(2) + excessedMessage;
}
@@ -87,5 +94,9 @@ export const createUeuse = async (data: ApiMap["ueuse/create"]["body"], title: s
firstUniqid = postedUniqid;
console.log(`${title}を投稿(${count}):`, postedUniqid);
while (lines.length > 0 && lines[0]?.trim() === "") {
lines.shift();
}
}
}
+1
View File
@@ -37,6 +37,7 @@ const schema = z.object({
ueuse: z.object({
maxRetries: z.number().int().positive(),
retryInterval: z.number().positive(),
maxLengthWithPublic: z.number().int().nonnegative(),
}),
debug: z.boolean().optional(),
});
+2 -2
View File
@@ -11,8 +11,8 @@ class MemoryClass {
writeFileSync(path, JSON.stringify({
processedInfo: [],
permissions: {},
lastReadMention: "",
lastReadReply: "",
lastReadMention: 0,
lastReadReply: 0,
userid: "",
max_length: 0,
}));