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 { MediaItem } 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('视频'), }; const logger = getLogger('webshot'); class Webshot extends CallableInstance<[MediaItem[], (...args) => void, number], Promise> { constructor(_wsUrl: string, _mode: number, onready?: (...args) => void) { super('webshot'); 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(); })(/\/.+\.(?:.*?(?<=[?&])stp=dst-(jpg)|(.+?)\?)/.exec(url).filter(g => g)[1]) ); public fetchBestCandidate = ({image_versions2, video_versions}: MediaItem) => { const candidates: ( Partial & typeof image_versions2.candidates[0] )[] = video_versions || image_versions2.candidates; const url = candidates .sort((var1, var2) => var2.width + (var2?.type || 0) - var1.width - (var1?.type || 0)) .map(variant => variant.url)[0]; // largest media const altMessage = `\n[失败的${typeInZH[video_versions ? 'video' : 'photo'].type}:${url}]`; return this.fetchMedia(url) .catch(error => { logger.warn('unable to fetch media, sending plain text instead...'); return altMessage; }); }; public webshot( mediaItems: MediaItem[], callback: (msgs: string, text: string, author: string) => void, webshotDelay: number ): Promise { const promises = mediaItems.map(item => { let promise = Promise.resolve(); logger.info(`working on ${item.user.username}/${item.code}`); let messageChain = ''; // text processing const author = `${item.user.full_name} (@${item.user.username}):\n`; const date = `${new Date(item.taken_at * 1000)}\n`; messageChain += author + date; // fetch extra entities promise = promise.then(() => this.fetchBestCandidate(item)) .then(msg => { messageChain += msg; }); return promise.then(() => { logger.info(`done working on ${item.user.username}/${item.code}, message chain:`); logger.info(JSON.stringify(Message.ellipseBase64(messageChain))); callback(messageChain, xmlEntities.decode(item.caption), author); }); }); return Promise.all(promises).then(); } } export default Webshot;