import base from './base.js' import fetch from 'node-fetch' import lodash from 'lodash' import puppeteer from '../../../lib/puppeteer/puppeteer.js' import common from '../../../lib/common/common.js' import gsCfg from '../model/gsCfg.js' import YAML from 'yaml' import fs from 'fs' let emoticon export default class MysNews extends base { constructor(e) { super(e) this.model = 'mysNews' } async getNews(gid) { let type = 1 let typeName = '公告' if (this.e.msg.includes('资讯')) { type = '3' typeName = '资讯' } if (this.e.msg.includes('活动')) { type = '2' typeName = '活动' } const res = await this.postData('getNewsList', { gids: gid, page_size: this.e.msg.includes('列表') ? 5 : 20, type }) if (!res) return const data = res.data.list if (data.length == 0) { return true } let param = {} let game = this.game(gid) if (this.e.msg.includes('列表')) { this.model = 'mysNews-list' data.forEach(element => { element.post.created_at = new Date(element.post.created_at * 1000).toLocaleString() }) param = { ...this.screenData, saveId: this.e.user_id, data, game, typeName } } else { const page = this.e.msg.replace(/#|#|官方|星铁|原神|崩坏三|崩三|绝区零|崩坏二|崩二|崩坏学园二|未定|未定事件簿|公告|资讯|活动/g, '').trim() || 1 if (page > data.length) { await this.e.reply('目前只查前20条最新的公告,请输入1-20之间的整数。') return true } const postId = data[page - 1].post.post_id param = await this.newsDetail(postId, gid) } const img = await this.render(param) return this.replyMsg(img, `${game}${typeName}:${param?.data?.post?.subject || `米游社${game}${typeName}列表`}`) } render(param) { return puppeteer.screenshots(this.model, param) } async newsDetail(postId, gid) { const res = await this.postData('getPostFull', { gids: gid, read: 1, post_id: postId }) if (!res) return const data = await this.detalData(res.data.post, gid) return { ...this.screenData, saveId: postId, dataConent: data.post.content, data } } postApi(type, data) { let host = 'https://bbs-api.miyoushe.com/' let param = [] lodash.forEach(data, (v, i) => param.push(`${i}=${v}`)) param = param.join('&') switch (type) { // 搜索 case 'searchPosts': host = 'https://bbs-api.miyoushe.com/post/wapi/searchPosts?' break case 'userInstantSearchPosts': host = 'https://bbs-api.miyoushe.com/painter/api/user_instant/search/list?' break // 帖子详情 case 'getPostFull': host += 'post/wapi/getPostFull?' break // 公告列表 case 'getNewsList': host += 'post/wapi/getNewsList?' break case 'emoticon': host += 'misc/api/emoticon_set?' break } return host + param } async postData(type, data) { const url = this.postApi(type, data) const headers = { Referer: 'https://www.miyoushe.com', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' } let response try { response = await fetch(url, { method: 'get', headers }) } catch (error) { logger.error(error.toString()) return false } if (!response.ok) { logger.error(`[米游社接口错误][${type}] ${response.status} ${response.statusText}`) return false } const res = await response.json() return res } async detalData(data, gid) { let json try { json = JSON.parse(data.post.content) } catch (error) { } if (typeof json == 'object') { if (json.imgs && json.imgs.length > 0) { for (const val of json.imgs) { data.post.content = `
` } } } else { for (const img of data.post.images) { data.post.content = data.post.content.replace(img, img + '?x-oss-process=image//resize,s_600/quality,q_80/auto-orient,0/interlace,1/format,jpg') } if (!emoticon) { emoticon = await this.mysEmoticon(gid) } data.post.content = data.post.content.replace(/_\([^)]*\)/g, function (t, e) { t = t.replace(/_\(|\)/g, '') if (emoticon.has(t)) { return `` } else { return '' } }) const arrEntities = { lt: '<', gt: '>', nbsp: ' ', amp: '&', quot: '"' } data.post.content = data.post.content.replace(/&(lt|gt|nbsp|amp|quot);/ig, function (all, t) { return arrEntities[t] }) } data.post.created_time = new Date(data.post.created_at * 1000).toLocaleString() for (const i in data.stat) { data.stat[i] = data.stat[i] > 10000 ? (data.stat[i] / 10000).toFixed(2) + '万' : data.stat[i] } return data } async mysEmoticon(gid) { const emp = new Map() const res = await this.postData('emoticon', { gids: gid }) if (res.retcode != 0) { return emp } for (const val of res.data.list) { if (!val.icon) continue for (const list of val.list) { if (!list.icon) continue emp.set(list.name, list.icon) } } return emp } async mysSearch() { let msg = this.e.msg msg = msg.replace(/#|米游社|mys/g, '') if (!msg) { await this.e.reply('请输入关键字,如#米游社七七') return false } let page = msg.match(/.*(\d){1}$/) || 0 if (page && page[1]) { page = page[1] } msg = lodash.trim(msg, page) let res = await this.postData('searchPosts', { gids: 2, size: 20, keyword: msg }) if (!res) return if (res?.data?.posts.length <= 0) { await this.e.reply('搜索不到您要的结果,换个关键词试试呗~') return false } let postId = res.data.posts[page].post.post_id const param = await this.newsDetail(postId) const img = await this.render(param) return this.replyMsg(img, `${param.data.post.subject}`) } async mysUrl() { let msg = this.e.msg let postId = /[0-9]+/g.exec(msg)[0] if (!postId) return false const param = await this.newsDetail(postId) const img = await this.render(param) return this.replyMsg(img, `${param.data.post.subject}`) } async mysEstimate(keyword, uid) { let res = await this.postData('userInstantSearchPosts', { keyword, uid, size: 20, offset: 0, sort_type: 2 }) let postList = res?.data?.list if (postList.length <= 0) { await this.e.reply('暂无数据') return false } let postId = postList[0].post.post.post_id if (!postId) { await this.e.reply('暂无数据') return false } const param = await this.newsDetail(postId) const img = await this.render(param) if (img.length > 1) { img.push(segment.image(param.data.post.images[0] + '?x-oss-process=image//resize,s_600/quality,q_80/auto-orient,0/interlace,1/format,jpg')) } return this.replyMsg(img, `${param.data.post.subject}`) } replyMsg(img, title) { if (!img || img.length <= 0) return false if (title) img = [title, ...img] if (img.length <= 2) return img return common.makeForwardMsg(this.e, [img]) } async mysNewsTask() { let cfg = gsCfg.getConfig('mys', 'pushNews') // 推送2小时内的公告资讯 let interval = 7200 // 最多同时推送两条 this.maxNum = cfg.maxNum for (let gid of [1, 2, 3, 4, 6, 8]) { let type = gid == 1 ? 'bbb' : gid == 2 ? 'gs' : gid == 3 ? 'bb' : gid == 4 ? 'wd' : gid == 6 ? 'sr' : 'zzz' let news = [] if (!lodash.isEmpty(cfg[`${type}announceGroup`])) { let anno = await this.postData('getNewsList', { gids: gid, page_size: 10, type: 1 }) if (anno) anno.data.list.forEach(v => { news.push({ ...v, typeName: '公告', post_id: v.post.post_id }) }) } if (!lodash.isEmpty(cfg[`${type}infoGroup`])) { let info = await this.postData('getNewsList', { gids: gid, page_size: 10, type: 3 }) if (info) info.data.list.forEach(v => { news.push({ ...v, typeName: '资讯', post_id: v.post.post_id }) }) } if (news.length <= 0) continue news = lodash.orderBy(news, ['post_id'], ['asc']) let now = Date.now() / 1000 this.key = `Yz:${type}:mys:newPush:` this.e.isGroup = true this.pushGroup = [] for (let val of news) { if (Number(now - val.post.created_at) > interval) continue if (cfg.banWord[type] && new RegExp(cfg.banWord[type]).test(val.post.subject)) continue if (val.typeName == '公告') for (let botId in cfg[`${type}announceGroup`]) for (let groupId of cfg[`${type}announceGroup`][botId]) await this.sendNews(botId, groupId, val.typeName, val.post.post_id, gid) if (val.typeName == '资讯') for (let botId in cfg[`${type}infoGroup`]) for (let groupId of cfg[`${type}infoGroup`][botId]) await this.sendNews(botId, groupId, val.typeName, val.post.post_id, gid) } } } async ActivityPush() { let now = new Date() now = now.getHours(); if(now < 10) return let pushGroupList try { pushGroupList = YAML.parse(fs.readFileSync(`./plugins/genshin/config/mys.pushNews.yaml`, `utf8`)) } catch (error) { logger.error(`[米游社活动到期推送] 活动到期预警推送失败:无法获取配置文件信息\n${error}`) return } if((!pushGroupList.gsActivityPush || pushGroupList.gsActivityPush == {}) && (!pushGroupList.srActivityPush || pushGroupList.srActivityPush == {})) return let BotidList = [] let ActivityPushYaml = {...pushGroupList.gsActivityPush, ...pushGroupList.srActivityPush} for (let item in ActivityPushYaml) { BotidList.push(item) } let gsActivityList = await this.getGsActivity() let srActivityList = await this.getSrActivity() let ActivityList = [] for (let item of srActivityList) { ActivityList.push({ game: `sr`, subtitle: item.title, banner: item.img, title: item.title, end_time: item.end_time }) } for (let item of gsActivityList) { ActivityList.push({ game: 'gs', subtitle: item.subtitle, banner: item.banner, title: item.title, end_time: item.end_time}) } if(ActivityList.length === 0) return for (let item of BotidList) { let redisapgl = await redis.get(`Yz:apgl:${item}`) let date = await this.getDate() redisapgl = JSON.parse(redisapgl) if(!redisapgl || redisapgl.date !== date) { redisapgl = { date, GroupList: ActivityPushYaml[item] } } if(!Array.isArray(redisapgl.GroupList) || redisapgl.GroupList.length == 0) continue if(!Bot[item]) { redisapgl.GroupList.shift() await redis.set(`Yz:apgl:${item}`, JSON.stringify(redisapgl)) continue } for (let a of ActivityList) { if((!pushGroupList.srActivityPush || !pushGroupList.srActivityPush[item] || !pushGroupList.srActivityPush[item].includes(redisapgl.GroupList[0])) && a.game === `sr`) continue if((!pushGroupList.gsActivityPush || !pushGroupList.gsActivityPush[item] || !pushGroupList.gsActivityPush[item].includes(redisapgl.GroupList[0])) && a.game === `gs`) continue let pushGame if(a.game === `sr`) pushGame = `星铁` if(a.game === `gs`) pushGame = `原神` let endDt = a.end_time endDt = endDt.replace(/\s/, `T`) let todayt = new Date() endDt = new Date(endDt) let sydate = await this.calculateRemainingTime(todayt, endDt) let msgList = [ `【${pushGame}活动即将结束通知】`, `\n活动:${a.subtitle}`, segment.image(a.banner), `描述:${a.title}`, `\n活动剩余时间:${sydate.days}天${sydate.hours}小时${sydate.minutes}分钟${sydate.seconds}秒`, `\n活动结束时间:${a.end_time}` ] logger.mark(`[米游社活动到期推送] 开始推送 ${item}:${redisapgl.GroupList[0]} ${a.subtitle}`) await common.sleep(5000) Bot[item].pickGroup(redisapgl.GroupList[0]).sendMsg(msgList) .then(() => {}).catch((err) => logger.error(`[米游社活动到期推送] ${item}:${redisapgl.GroupList[0]} 推送失败,错误信息${err}`)) } redisapgl.GroupList.shift() await redis.set(`Yz:apgl:${item}`, JSON.stringify(redisapgl)) } return } async getDate() { const currentDate = new Date(); const year = currentDate.getFullYear(); const month = (currentDate.getMonth() + 1).toString().padStart(2, '0'); const day = currentDate.getDate().toString().padStart(2, '0'); return `${year}-${month}-${day}` } async getGsActivity() { let gshd try { gshd = await fetch(`https://hk4e-api.mihoyo.com/common/hk4e_cn/announcement/api/getAnnList?game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000`) gshd = await gshd.json() } catch { return [] } let hdlist = [] let result = [] for (let item of gshd.data.list[1].list) { if(item.tag_label.includes(`活动`) && !item.title.includes(`传说任务`) && !item.title.includes(`游戏公告`)) hdlist.push(item) } for (let item of hdlist) { let endDt = item.end_time endDt = endDt.replace(/\s/, `T`) let todayt = new Date() endDt = new Date(endDt) let sydate = await this.calculateRemainingTime(todayt, endDt) if(sydate.days <= 1) result.push(item) } return result } async getSrActivity() { let srhd try { srhd = await fetch(`https://hkrpg-api.mihoyo.com/common/hkrpg_cn/announcement/api/getAnnList?game=hkrpg&game_biz=hkrpg_cn&lang=zh-cn&auth_appid=announcement&authkey_ver=1&bundle_id=hkrpg_cn&channel_id=1&level=65&platform=pc®ion=prod_gf_cn&sdk_presentation_style=fullscreen&sdk_screen_transparent=true&sign_type=2&uid=100000000`) srhd = await srhd.json() } catch { return [] } let hdlist = [] let result = [] for (let item of srhd.data.pic_list[0].type_list[0].list) { if (item.title) hdlist.push(item) } for (let item of hdlist) { let endDt = item.end_time endDt = endDt.replace(/\s/, `T`) let todayt = new Date() endDt = new Date(endDt) let sydate = await this.calculateRemainingTime(todayt, endDt) if (sydate.days <= 1) result.push(item) } return result } async calculateRemainingTime(startDate, endDate) { const difference = endDate - startDate; const days = Math.floor(difference / (1000 * 60 * 60 * 24)); const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((difference % (1000 * 60)) / 1000); return { days, hours, minutes, seconds }; } async sendNews(botId, groupId, typeName, postId, gid) { if (!this.pushGroup[groupId]) this.pushGroup[groupId] = 0 if (this.pushGroup[groupId] >= this.maxNum) return let sended = await redis.get(`${this.key}${botId}:${groupId}:${postId}`) if (sended) return let game = this.game(gid) // 判断是否存在群关系 this.e.group = Bot[botId]?.pickGroup(groupId) if (!this.e.group) { logger.mark(`[米游社${game}${typeName}推送] 群${botId}:${groupId}未关联`) return } if (!this[postId]) { const param = await this.newsDetail(postId, gid) logger.mark(`[米游社${game}${typeName}推送] ${param.data.post.subject}`) this[postId] = { img: await this.render(param), title: param.data.post.subject } } this.pushGroup[groupId]++ await redis.set(`${this.key}${botId}:${groupId}:${postId}`, '1', { EX: 3600 * 10 }) // 随机延迟10-90秒 await common.sleep(lodash.random(10000, 90000)) const msg = await this.replyMsg(this[postId].img, `${game}${typeName}推送:${this[postId].title}`) return this.e.group.sendMsg(msg) } game(gid) { switch (gid) { case 1: return '崩坏三' case 2: return '原神' case 3: return '崩坏二' case 4: return '未定事件簿' case 6: return '崩坏星穹铁道' case 8: return '绝区零' } return '' } }