UNPKG

koishi-plugin-nitter-rss

Version:

订阅 X (Twitter) 内容,使用 nitter.cz,支持ChatGPT与Gradio Chatbot翻译

341 lines (340 loc) 19 kB
"use strict"; 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;