koishi-plugin-nitter-rss
Version:
订阅 X (Twitter) 内容,使用 nitter.cz,支持ChatGPT与Gradio Chatbot翻译
341 lines (340 loc) • 19 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.apply = exports.Config = exports.name = exports.inject = void 0;
const koishi_1 = require("koishi");
const parseLinkInfo_1 = require("./parseLinkInfo");
const utils_1 = require("./utils");
const RSS_1 = require("./RSS");
exports.inject = ['puppeteer'];
exports.name = 'nitter-rss';
// 配置字段
exports.Config = koishi_1.Schema.intersect([
koishi_1.Schema.object({
nitterUrl: koishi_1.Schema.string().description('nitter服务器地址,必填,可以在这个列表中选择 https://status.d420.de \n 注意需要对应网站支持RSS功能,填写错误会导致插件崩溃').default('nitter.esmailelbob.xyz'),
translateType: koishi_1.Schema.union(['不翻译', /* '翻译API', */ 'gradio-chatbot', 'ChatGPT']).default('不翻译').description('翻译类型'),
screenshot: koishi_1.Schema.boolean().default(true).description('是否在发送消息时发送截图'),
sendImage: koishi_1.Schema.boolean().default(false).description('是否在发送消息时单独发送推文内所有图片素材'),
sendLink: koishi_1.Schema.boolean().default(true).description('是否在发送消息时发送推文链接'),
sendNewTweetAlert: koishi_1.Schema.boolean().default(false).description('是否在发现新推文时发送消息提醒,以避免转发失败时毫无消息'),
timeInterval: koishi_1.Schema.number().role('slider').min(5).max(240).step(1).default(5).description('每次检测新推文时间间隔,单位为分钟'),
sendingInterval: koishi_1.Schema.number().role('slider').min(5).max(240).step(1).default(20).description('每次转发推文的时间间隔,单位为秒。如果你使用ChatGPT翻译且ChatGPT API为免费版本,则一分钟只能请求3次API,请设置至少20秒以避免翻译失败'),
translateTimeout: koishi_1.Schema.number().role('slider').min(5).max(240).step(1).default(60).description('获取翻译等待的超时时间,单位为秒'),
skipRetweet: koishi_1.Schema.boolean().default(true).description('是否跳过转推'),
text2image: koishi_1.Schema.boolean().default(false).description('是否将翻译等文本内容转为图片发送,避免文字过多触发一些平台的风控限制'),
}).description('基础配置'),
koishi_1.Schema.union([
//gradio-chatbot
koishi_1.Schema.object({
translateType: koishi_1.Schema.const('gradio-chatbot').required(),
//选择模型
GradioChatBotModule: koishi_1.Schema.union([
koishi_1.Schema.const('0').description('ChatGPT https://huggingface.co/spaces/yizhangliu/chatGPT').hidden(true),
koishi_1.Schema.const('1').description('GPT Free (https://huggingface.co/spaces/justest/gpt4free)'),
koishi_1.Schema.const('2').description('Llama2 Spaces(不推荐) (https://huggingface.co/spaces/ysharma/Explore_llamav2_with_TGI)'),
koishi_1.Schema.const('3').description('MosaicML MPT-30B-Chat (https://huggingface.co/spaces/mosaicml/mpt-30b-chat)'),
koishi_1.Schema.const('4').description('Falcon Chat (https://huggingface.co/spaces/HuggingFaceH4/falcon-chat)'),
koishi_1.Schema.const('5').description('Star Chat (https://huggingface.co/spaces/HuggingFaceH4/starchat-playground)'),
koishi_1.Schema.const('6').description('ChatGLM2(不推荐) (https://huggingface.co/spaces/mikeee/chatglm2-6b-4bit)'),
koishi_1.Schema.const('7').description('ChatGLM(不推荐) (https://huggingface.co/spaces/multimodalart/ChatGLM-6B)'),
koishi_1.Schema.const('8').description('Vicuna (https://chat.lmsys.org/)'),
koishi_1.Schema.const('9').description('通义千问 7B (https://huggingface.co/spaces/mikeee/qwen-7b-chat)'),
koishi_1.Schema.const('10').description('通义千问 (https://modelscope.cn/studios/qwen/Qwen-7B-Chat-Demo/summary)'),
koishi_1.Schema.const('11').description('ChatGLM2(不推荐) (https://modelscope.cn/studios/AI-ModelScope/ChatGLM6B-unofficial/summary)'),
koishi_1.Schema.const('12').description('姜子牙V1.1(不推荐) (https://modelscope.cn/studios/Fengshenbang/Ziya_LLaMA_13B_v1_online/summary)'),
koishi_1.Schema.const('13').description('达魔院(不推荐) (https://modelscope.cn/studios/damo/role_play_chat/summary)'),
]).role('radio').description('AI模型选择'),
//提示词
GradioChatBotPrompt: koishi_1.Schema.string().description('gradio-chatbot用提示词,将被放在内容前面').default('请帮我将推文内容翻译成简体中文。所有疑似专有名词,人名,曲名与书名等的内容请保留原文,带有#的关键词请不要翻译。你的回答将被直接输入数据库,请不要提供翻译结果以外的任何内容。以下是需要翻译的内容:\n'),
}).description('gradio-chatbot配置'),
//ChatGPT
koishi_1.Schema.object({
translateType: koishi_1.Schema.const('ChatGPT').required(),
//apiUrl
ChatGPTBaseUrl: koishi_1.Schema.string().description('ChatGPT API Url, 必填').default('https://api.openai.com/v1'),
//apiKey
ChatGPTKey: koishi_1.Schema.string().description('ChatGPT API Key, 必填').default(''),
//选择模型
ChatGPTModule: koishi_1.Schema.union([
koishi_1.Schema.const('gpt-3.5-turbo').description('gpt-3.5-turbo'),
koishi_1.Schema.const('gpt-4').description('gpt-4'),
]).role('radio').description('ChatGPT模型选择,必选,只有付费账号支持gpt4'),
//提示词
ChatGPTPrompt: koishi_1.Schema.string().description('ChatGPT用提示词,将被放在内容前面').default('请帮我将推文内容翻译成简体中文。所有疑似专有名词,人名,曲名与书名等的内容请保留原文,带有#的关键词请不要翻译。你的回答将被直接输入数据库,请不要提供翻译结果以外的任何内容。以下是需要翻译的内容:\n'),
}).description('ChatGPT配置'),
koishi_1.Schema.object({}),
]),
]);
// apply主函数
function apply(ctx, config) {
const logger = new koishi_1.Logger('nitter-rss-apply');
ctx.model.extend("channel", {
'twitterAccounts': { type: 'json', initial: [] }
});
//获取所有需要转发的账号
function getAllAccounts(channels) {
const accounts = [];
channels.forEach(channel => {
if (channel.twitterAccounts) {
channel.twitterAccounts.forEach(account => {
if (!accounts.some(item => item.account === account.account)) {
accounts.push({
account: account.account,
translate: account.translate
});
}
else {
//如果已经存在,且translate为true,则改为true
const existingAccount = accounts.find(item => item.account === account.account);
if (account.translate) {
existingAccount.translate = true;
}
}
});
}
});
return accounts;
}
let accountsLastUpdateTimeList = {};
//获取时间范围内的所有推文:
async function getRecentTweets(accounts) {
const time = new Date();
const allTweets = [];
for (const account of accounts) {
let afterTime;
if (!accountsLastUpdateTimeList[account.account]) {
afterTime = time.getTime() - config.timeInterval * 60 * 1000;
accountsLastUpdateTimeList[account.account] = afterTime;
}
else {
afterTime = accountsLastUpdateTimeList[account.account];
}
try {
const tweets = await (0, RSS_1.getTwitterList)(config.nitterUrl, ctx, account.account);
for (const tweet of tweets) {
const tweetTime = tweet.pubDate;
const isExist = allTweets.some(item => item.rss.link === tweet.link);
if (!isExist && tweetTime > afterTime) {
allTweets.push({ rss: tweet, translate: account.translate });
}
accountsLastUpdateTimeList[account.account] = Math.max(tweetTime, accountsLastUpdateTimeList[account.account]);
}
}
catch (error) {
logger.error(error);
}
}
return allTweets.sort((a, b) => new Date(b.rss.pubDate).getTime() - new Date(a.rss.pubDate).getTime());
}
//发送消息
async function sendMessages(tweets, channels, ctx, config) {
for (const tweet of tweets) {
//跳过转推
if (tweet.rss.isRetweet && config.skipRetweet) {
continue;
}
const parsedTwitterLink = await (0, utils_1.parseTwitterLink)(tweet.rss.link);
const tempAccount = parsedTwitterLink.account;
const messageContent = await (0, parseLinkInfo_1.parseLinkInfo)(ctx, parsedTwitterLink, config, tweet.translate);
for (const channel of channels) {
const botId = `${channel.platform}:${channel.assignee}`;
if (channel.twitterAccounts && channel.twitterAccounts.some(account => account.account === tempAccount)) {
try {
if (config.sendNewTweetAlert) {
ctx.bots[botId].sendMessage(channel.id, `发现新推文推文:\n${tempAccount}\n${tweet.rss.link}`);
}
logger.info(`正在发送消息: ${tweet.rss.link}至${botId}`);
ctx.bots[`${channel.platform}:${channel.assignee}`].sendMessage(channel.id, messageContent);
}
catch (e) {
logger.error(`发送消息失败: ${e.message}`);
}
await new Promise(resolve => {
logger.info(`正在等待${config.sendingInterval}秒`);
setTimeout(() => resolve(''), config.sendingInterval * 1000);
});
}
}
}
}
let intervaling = false;
//循环
async function interval() {
const time = new Date();
logger.info(`正在循环${(0, utils_1.formatLocalTime)(time.getTime())}`);
if (intervaling) {
return;
}
intervaling = true;
const channels = await ctx.database.get('channel', {});
const accounts = getAllAccounts(channels);
const recentTweets = await getRecentTweets(accounts);
await sendMessages(recentTweets, channels, ctx, config);
intervaling = false;
logger.info(`循环结束`);
}
ctx.setInterval(interval, config.timeInterval * 60 * 1000);
ctx.command('开始循环', 'nitter-rss: 测试用,立刻开始转发轮询').action(async ({ session }) => {
if (intervaling) {
session.send(`开始循环`);
}
else {
session.send(`正在循环中`);
}
await interval();
session.send(`循环结束`);
});
// 添加account到channel
ctx.command('订阅添加 <account>', 'nitter-rss: 订阅新的推特账号')
.channelFields(['twitterAccounts'])
.alias('订阅', '订阅推特', '添加订阅')
.example('订阅添加 LinusTech 订阅LinusTech的推特')
.action(async ({ session }, account) => {
//判断是否是channel
if (session.channel?.twitterAccounts == undefined) {
session.send(`此命令只能在channel(群聊)中使用`);
return;
}
//判断是否已经添加过
for (let i = 0; i < session.channel.twitterAccounts.length; i++) {
if (session.channel.twitterAccounts[i].account == account) {
session.send(`此账号已经添加过了`);
return;
}
}
//判断账号是否存在
try {
await (0, RSS_1.getTwitterList)(config.nitterUrl, ctx, account);
}
catch (e) {
logger.error(e);
session.send(`此账号不存在`);
return;
}
//添加
session.channel.twitterAccounts.push({ account: account, translate: config.translateType != '不翻译' });
session.send(`添加成功`);
});
//查询当前channel的account
ctx.command('订阅查询', 'nitter-rss: 查询当前channel的订阅的推特账号')
.channelFields(['twitterAccounts'])
.alias('订阅列表', '查询订阅')
.action(async ({ session }) => {
//判断是否是channel
if (session.channel?.twitterAccounts == undefined) {
session.send(`此命令只能在channel(群聊)中使用`);
return;
}
//查询
let finalText = `当前channel订阅的推特账号有:\n`;
for (let i = 0; i < session.channel.twitterAccounts.length; i++) {
finalText += `${i + 1}: ${session.channel.twitterAccounts[i].account}: ${session.channel.twitterAccounts[i].translate ? '翻译' : '不翻译'} \n`;
}
session.send(finalText);
});
//删除channel的account
ctx.command('订阅删除 <account>', 'nitter-rss: 删除当前channel的订阅的推特账号')
.channelFields(['twitterAccounts'])
.alias('删除订阅', '删除订阅推特', '取消订阅', '取消订阅推特')
.example('订阅删除 LinusTech 删除LinusTech的订阅推特')
.action(async ({ session }, account) => {
//判断是否是channel
if (session.channel?.twitterAccounts == undefined) {
session.send(`此命令只能在channel(群聊)中使用`);
return;
}
//删除
for (let i = 0; i < session.channel.twitterAccounts.length; i++) {
if (session.channel.twitterAccounts[i].account == account) {
session.channel.twitterAccounts.splice(i, 1);
session.send(`删除成功`);
return;
}
}
session.send(`未找到此账号`);
});
//调整channel的account的翻译状态为翻译
ctx.command('订阅修改翻译 <account>', 'nitter-rss: 调整当前channel的订阅的推特账号的翻译状态为翻译')
.channelFields(['twitterAccounts'])
.alias('翻译订阅', '订阅翻译')
.example('订阅修改翻译 LinusTech 调整LinusTech的订阅推特的翻译状态为翻译')
.action(async ({ session }, account) => {
//判断是否是channel
if (session.channel?.twitterAccounts == undefined) {
session.send(`此命令只能在channel(群聊)中使用`);
return;
}
//调整
for (let i = 0; i < session.channel.twitterAccounts.length; i++) {
if (session.channel.twitterAccounts[i].account == account) {
session.channel.twitterAccounts[i].translate = true;
session.send(`调整成功`);
return;
}
}
session.send(`未找到此账号`);
});
//调整channel的account的翻译状态为不翻译
ctx.command('订阅修改不翻译 <account>', 'nitter-rss: 调整当前channel的订阅的推特账号的翻译状态为不翻译')
.channelFields(['twitterAccounts'])
.alias('不翻译订阅', '订阅不翻译')
.example('订阅修改不翻译 LinusTech 调整LinusTech的订阅推特的翻译状态为不翻译')
.action(async ({ session }, account) => {
//判断是否是channel
if (session.channel?.twitterAccounts == undefined) {
session.send(`此命令只能在channel(群聊)中使用`);
return;
}
//调整
for (let i = 0; i < session.channel.twitterAccounts.length; i++) {
if (session.channel.twitterAccounts[i].account == account) {
session.channel.twitterAccounts[i].translate = false;
session.send(`调整成功`);
return;
}
}
session.send(`未找到此账号`);
});
// 通过account获得近期推文列表
ctx.command('推文列表 <account>', 'nitter-rss: 获取指定account的近期4条推文')
.channelFields(['twitterAccounts'])
.alias('twitter-list', '推文列表', 'twitter列表', 't-l')
.example('推文列表 LinusTech 获取LinusTech的近期4条推文')
.action(async ({ session }, account) => {
logger.info(`正在处理推文列表: ${account}`);
let result;
try {
result = await (0, RSS_1.getTwitterList)(config.nitterUrl, ctx, account);
}
catch (e) {
logger.error(e);
session.send(`获取推文列表失败:${e.message}`);
return;
}
let finalText = '';
for (let i = 0; i < 4; i++) {
//创建临时文本,如果result.title超过20个字符,则截断,后面换成...
let tempText = result[i].title;
if (tempText.length > 20) {
tempText = tempText.substring(0, 20) + '...';
}
finalText += `${i + 1}: ${(0, utils_1.formatLocalTime)(result[i].pubDate)}\n${tempText}\n${result[i].link}\n\n`;
}
session.send(finalText);
});
// 通过链接获得推文内容
ctx.command('获取推文 <link>', 'nitter-rss: 获取推文内容')
.alias('twitter', '推文', 'twitter内容', 't')
.option('forceUpdate', '-f', { fallback: false })
.example('获取推文 https://twitter.com/LinusTech/status/1716561166288453951 获取链接的推文内容\ntwitter https://nitter.cz/LinusTech/status/1716561166288453951 获取链接的推文内容\ntwitter https://nitter.cz/LinusTech/status/1716561166288453951 -f 获取链接的推文内容,强制重新翻译')
.action(async ({ session, options }, link, forceTranslate) => {
logger.info(`正在处理链接: ${link}`);
const parsedTwitterLink = await (0, utils_1.parseTwitterLink)(link);
if (!parsedTwitterLink.isTwitterLink) {
session.send(`链接格式不正确`);
return;
}
session.send(`正在处理`);
return (await (0, parseLinkInfo_1.parseLinkInfo)(ctx, parsedTwitterLink, config, config.translateType != '不翻译', options.forceUpdate));
});
}
exports.apply = apply;