|
@@ -1,4 +1,5 @@
|
|
|
-import { Readable } from 'stream';
|
|
|
+import { createWriteStream } from 'fs';
|
|
|
+import { Readable, Stream } from 'stream';
|
|
|
import { promisify } from 'util';
|
|
|
|
|
|
import axios from 'axios';
|
|
@@ -7,9 +8,10 @@ import { XmlEntities } from 'html-entities';
|
|
|
import { PNG } from 'pngjs';
|
|
|
import * as puppeteer from 'playwright';
|
|
|
import * as sharp from 'sharp';
|
|
|
+import * as temp from 'temp';
|
|
|
|
|
|
import { getLogger } from './loggers';
|
|
|
-import { ellipseBase64InMessage, message } from './koishi';
|
|
|
+import { Message } from './koishi';
|
|
|
import { MediaEntity, Tweets } from './twitter';
|
|
|
import { chainPromises } from './utils';
|
|
|
|
|
@@ -73,11 +75,13 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
};
|
|
|
|
|
|
private renderWebshot = (url: string, height: number, webshotDelay: number): Promise<string> => {
|
|
|
+ temp.track();
|
|
|
const jpeg = (data: Readable) => data.pipe(sharp()).jpeg({quality: 90, trellisQuantisation: true});
|
|
|
- const sharpToBase64 = (pic: sharp.Sharp) => new Promise<string>(resolve => {
|
|
|
- pic.toBuffer().then(buffer => resolve(`base64://${buffer.toString('base64')}`));
|
|
|
+ const sharpToFile = (pic: sharp.Sharp) => new Promise<string>(resolve => {
|
|
|
+ const webshotTempFilePath = temp.path({suffix: '.jpg'});
|
|
|
+ pic.toFile(webshotTempFilePath).then(() => resolve(`file://${webshotTempFilePath}`));
|
|
|
});
|
|
|
- const promise = new Promise<{ base64: string, boundary: null | number }>((resolve, reject) => {
|
|
|
+ const promise = new Promise<{ path: string, boundary: null | number }>((resolve, reject) => {
|
|
|
const width = 720;
|
|
|
const zoomFactor = 2;
|
|
|
logger.info(`shooting ${width}*${height} webshot for ${url}`);
|
|
@@ -221,25 +225,25 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
this.height = boundary;
|
|
|
}
|
|
|
|
|
|
- sharpToBase64(jpeg(this.pack())).then(base64 => {
|
|
|
+ sharpToFile(jpeg(this.pack())).then(path => {
|
|
|
logger.info(`finished webshot for ${url}`);
|
|
|
- resolve({base64, boundary});
|
|
|
+ resolve({path, boundary});
|
|
|
});
|
|
|
} else if (height >= 8 * 1920) {
|
|
|
logger.warn('too large, consider as a bug, returning');
|
|
|
- sharpToBase64(jpeg(this.pack())).then(base64 => {
|
|
|
- resolve({base64, boundary: 0});
|
|
|
+ sharpToFile(jpeg(this.pack())).then(path => {
|
|
|
+ resolve({path, boundary: 0});
|
|
|
});
|
|
|
} else {
|
|
|
logger.info('unable to find boundary, try shooting a larger image');
|
|
|
- resolve({base64: '', boundary});
|
|
|
+ resolve({path: '', boundary});
|
|
|
}
|
|
|
}).parse(screenshot);
|
|
|
})
|
|
|
.catch(err => {
|
|
|
if (err instanceof Error && err.name !== 'TimeoutError') throw err;
|
|
|
logger.error(`error shooting webshot for ${url}, could not load web page of tweet`);
|
|
|
- resolve({base64: '', boundary: 0});
|
|
|
+ resolve({path: '', boundary: 0});
|
|
|
})
|
|
|
.finally(() => { page.close(); });
|
|
|
})
|
|
@@ -247,18 +251,18 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
});
|
|
|
return promise.then(data => {
|
|
|
if (data.boundary === null) return this.renderWebshot(url, height + 1920, webshotDelay);
|
|
|
- else return data.base64;
|
|
|
+ else return data.path;
|
|
|
}).catch(error => this.reconnect(error)
|
|
|
.then(() => this.renderWebshot(url, height, webshotDelay))
|
|
|
);
|
|
|
};
|
|
|
|
|
|
- private fetchMedia = (url: string): Promise<string> => new Promise<ArrayBuffer>((resolve, reject) => {
|
|
|
+ private fetchMedia = (url: string): Promise<string> => new Promise<Stream>((resolve, reject) => {
|
|
|
logger.info(`fetching ${url}`);
|
|
|
axios({
|
|
|
method: 'get',
|
|
|
url,
|
|
|
- responseType: 'arraybuffer',
|
|
|
+ responseType: 'stream',
|
|
|
timeout: 150000,
|
|
|
}).then(res => {
|
|
|
if (res.status === 200) {
|
|
@@ -274,13 +278,15 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
});
|
|
|
}).then(data =>
|
|
|
(ext => {
|
|
|
- const base64 = `base64://${Buffer.from(data).toString('base64')}`;
|
|
|
+ const mediaTempFilePath = temp.path({suffix: ext});
|
|
|
+ data.pipe(createWriteStream(mediaTempFilePath));
|
|
|
+ const path = `file://${mediaTempFilePath}`;
|
|
|
switch (ext) {
|
|
|
case 'jpg':
|
|
|
case 'png':
|
|
|
- return message.image(base64);
|
|
|
+ return Message.Image(path);
|
|
|
case 'mp4':
|
|
|
- return message.video(base64);
|
|
|
+ return Message.Video(path);
|
|
|
}
|
|
|
logger.warn('unable to find MIME type of fetched media, failing this fetch');
|
|
|
throw Error();
|
|
@@ -335,8 +341,8 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
};
|
|
|
};
|
|
|
promise = promise.then(() => this.renderWebshot(url, 1920, webshotDelay))
|
|
|
- .then(base64url => {
|
|
|
- if (base64url) return message.image(base64url);
|
|
|
+ .then(fileurl => {
|
|
|
+ if (fileurl) return Message.Image(fileurl);
|
|
|
return author + text;
|
|
|
})
|
|
|
.then(msg => {
|
|
@@ -389,7 +395,7 @@ class Webshot extends CallableInstance<[Tweets, (...args) => void, number], Prom
|
|
|
}
|
|
|
promise.then(() => {
|
|
|
logger.info(`done working on ${twi.user.screen_name}/${twi.id_str}, message chain:`);
|
|
|
- logger.info(JSON.stringify(ellipseBase64InMessage(messageChain)));
|
|
|
+ logger.info(JSON.stringify(Message.ellipseBase64(messageChain)));
|
|
|
callback(messageChain, xmlEntities.decode(text), author);
|
|
|
});
|
|
|
});
|