import { writeFileSync } from 'fs'; import axios from 'axios'; import * as CallableInstance from 'callable-instance'; import { XmlEntities } from 'html-entities'; import * as temp from 'temp'; import { getLogger } from './loggers'; import { Message } from './koishi'; import { Fleets, FullUser, MediaEntity } from './twitter'; const xmlEntities = new XmlEntities(); const ZHType = (type: string) => new class extends String { public type = super.toString(); public toString = () => `[${super.toString()}]`; }(type); const typeInZH = { photo: ZHType('图片'), video: ZHType('视频'), animated_gif: ZHType('GIF'), }; const logger = getLogger('webshot'); class Webshot extends CallableInstance<[FullUser, Fleets, (...args) => void, number], Promise> { private mode: number; constructor(_wsUrl: string, mode: number, onready?: (...args) => void) { super('webshot'); this.mode = mode; onready(); } private fetchMedia = (url: string): Promise => new Promise((resolve, reject) => { logger.info(`fetching ${url}`); axios({ method: 'get', url, responseType: 'arraybuffer', timeout: 150000, }).then(res => { if (res.status === 200) { logger.info(`successfully fetched ${url}`); resolve(res.data); } else { logger.error(`failed to fetch ${url}: ${res.status}`); reject(); } }).catch (err => { logger.error(`failed to fetch ${url}: ${err instanceof Error ? err.message : err}`); reject(); }); }).then(data => (ext => { const mediaTempFilePath = temp.path({suffix: `.${ext}`}); writeFileSync(mediaTempFilePath, Buffer.from(data)); const path = `file://${mediaTempFilePath}`; switch (ext) { case 'jpg': case 'png': return Message.Image(path); case 'mp4': return Message.Video(path); } logger.warn('unable to find MIME type of fetched media, failing this fetch'); throw Error(); })(((/\?format=([a-z]+)&/.exec(url)) ?? (/.*\/.*\.([^?]+)/.exec(url)))[1]) ); public webshot( user: FullUser, fleets: Fleets, callback: (msgs: string, text: string) => void, webshotDelay: number ): Promise { let promise = new Promise(resolve => { resolve(); }); fleets.forEach(fleet => { promise = promise.then(() => { logger.info(`working on ${user.screen_name}/${fleet.fleet_id}`); }); let messageChain = ''; // text processing const author = `${user.name} (@${user.screen_name}):\n`; const date = `${new Date(fleet.created_at)}\n`; let text = author + date + fleet.media_bounding_boxes?.map(box => box.entity.value as string).join('\n') ?? ''; messageChain += author + date; // fetch extra entities // tslint:disable-next-line: curly // eslint-disable-next-line curly if (1 - this.mode % 2) promise = promise.then(() => { const media: MediaEntity & {media_info?: MediaEntity} = fleet.media_entity; let url: string; if (fleet.media_key.media_category === 'TWEET_IMAGE') { media.type = 'photo'; url = media.media_url_https.replace(/\.([a-z]+)$/, '?format=$1') + '&name=orig'; } else { media.type = fleet.media_key.media_category === 'TWEET_VIDEO' ? 'video' : 'animated_gif'; media.video_info = media.media_info.video_info; text += `[${typeInZH[media.type as keyof typeof typeInZH].type}]`; url = (media.video_info.variants as ( typeof media.video_info.variants[0] & {bit_rate: number} // bitrate -> bit_rate )[]) .filter(variant => variant.bit_rate !== undefined) .sort((var1, var2) => var2.bit_rate - var1.bit_rate) .map(variant => variant.url)[0]; // largest video } const altMessage = `\n[失败的${typeInZH[media.type as keyof typeof typeInZH].type}:${url}]`; return this.fetchMedia(url) .catch(error => { logger.warn('unable to fetch media, sending plain text instead...'); return altMessage; }) .then(msg => { messageChain += msg; }); }); promise.then(() => { logger.info(`done working on ${user.screen_name}/${fleet.fleet_id}, message chain:`); logger.info(JSON.stringify(messageChain)); callback(messageChain, xmlEntities.decode(text)); }); }); return promise; } } export default Webshot;