2023-02-03 09:08:22 +09:00

156 lines
4.1 KiB
TypeScript

import Message from '@/message';
import Module from '@/module';
import autobind from 'autobind-decorator';
import { z } from 'zod';
import humanizeDuration from 'humanize-duration';
export default class extends Module {
public readonly name = 'trace-moe';
private readonly itemSchema = z.object({
anilist: z.object({
title: z.object({
native: z.string().nullable(),
romaji: z.string().nullable(),
english: z.string().nullable()
}),
isAdult: z.boolean().nullable()
}),
episode: z.number().or(z.string()).or(z.array(z.number())).nullable(),
from: z.number().nullable(),
to: z.number().nullable(),
similarity: z.number()
});
private readonly schema = z.object({
error: z.string(),
result: z.array(this.itemSchema)
});
@autobind
private getImageUrl(message: Message) {
if (!message.files) {
this.log('No files found.');
return null;
}
const filteredImageFiles = message.files.filter(file => file.type.startsWith('image'));
if (!filteredImageFiles.length) {
this.log('No valid images found.');
return null;
}
return filteredImageFiles[0].url;
}
@autobind
private async getFromTraceMoe(imageUrl: string) {
try {
const response = await fetch(`https://api.trace.moe/search?anilistInfo&url=${encodeURIComponent(imageUrl)}`);
const data = await response.json();
const result = this.schema.safeParse(data);
if (!result.success) {
this.log('Validation failed.');
this.log(JSON.stringify(data));
console.warn(result.error);
return null;
}
return result.data.result[0];
} catch (error) {
this.log('Failed to fetch data from Trace Moe.');
console.warn(error);
return null;
}
}
@autobind
public install() {
return {
mentionHook: this.mentionHook
};
}
@autobind
private async mentionHook(message: Message) {
if (!message.includes(['アニメ'])) return false;
if (message.isDm) {
message.reply('僕にアニメのシーンの画像を添付して「アニメ教えて」ってメンションすると、何のアニメか教えるよ!');
return true;
}
const imageUrl = this.getImageUrl(message);
if (!imageUrl) {
message.reply('画像を添付してね!');
return true;
}
const traceMoe = await this.getFromTraceMoe(imageUrl);
if (!traceMoe) {
message.reply('ぬぁ~~~、いまはめんどくさいかも…');
return true;
}
const animeTitle = traceMoe.anilist.title.native || traceMoe.anilist.title.english;
if (!animeTitle) {
message.reply('ごめんね、わかんないや…');
return true;
}
if (typeof traceMoe.episode === 'string') traceMoe.episode = traceMoe.episode.replace(/\|/g, 'か');
else if (Array.isArray(traceMoe.episode)) traceMoe.episode = traceMoe.episode.join('話と');
const options = {
language: 'ja',
round: true,
delimiter: '',
spacer: ''
};
const fromText = traceMoe.from !== null ? humanizeDuration(traceMoe.from * 1000, options) : null;
const toText = traceMoe.to !== null ? humanizeDuration(traceMoe.to * 1000, options) : null;
const pronoun = traceMoe.episode || (traceMoe.from && traceMoe.to) ? 'これは' : 'このアニメは';
const prefix = (() => {
if (traceMoe.similarity >= 0.9) return pronoun;
if (traceMoe.similarity >= 0.8) return `${pronoun}たぶん`;
return 'よくわかんないけど、強いて言うなら';
})();
const suffix = (() => {
if (traceMoe.similarity >= 0.9) return 'だよ!';
if (traceMoe.similarity >= 0.8) return 'だと思う!';
return 'に似てるかな';
})();
const time = fromText && toText && fromText === toText ? fromText : `${fromText}から${toText}`;
const detail = (() => {
if (traceMoe.episode && traceMoe.from && traceMoe.to) return `${traceMoe.episode}話の${time}`;
if (traceMoe.from && traceMoe.to) return `${time}`;
if (traceMoe.episode) return `の第${traceMoe.episode}`;
return '';
})();
const content = `${animeTitle}${detail}`;
const messageToReply = `${prefix}${content}${suffix}`;
if (traceMoe.anilist.isAdult) {
message.reply(messageToReply, { cw: 'そぎぎ' });
} else {
message.reply(messageToReply);
}
return true;
}
}