|
@@ -1,12 +1,12 @@
|
|
|
|
+import { Readable } from 'stream';
|
|
|
|
+import { promisify } from 'util';
|
|
|
|
+
|
|
import axios from 'axios';
|
|
import axios from 'axios';
|
|
import * as CallableInstance from 'callable-instance';
|
|
import * as CallableInstance from 'callable-instance';
|
|
import { XmlEntities } from 'html-entities';
|
|
import { XmlEntities } from 'html-entities';
|
|
import { PNG } from 'pngjs';
|
|
import { PNG } from 'pngjs';
|
|
-import * as puppeteer from 'puppeteer';
|
|
|
|
-import { Browser } from 'puppeteer';
|
|
|
|
|
|
+import * as puppeteer from 'playwright';
|
|
import * as sharp from 'sharp';
|
|
import * as sharp from 'sharp';
|
|
-import { Readable } from 'stream';
|
|
|
|
-import { promisify } from 'util';
|
|
|
|
|
|
|
|
import gifski from './gifski';
|
|
import gifski from './gifski';
|
|
import { getLogger } from './loggers';
|
|
import { getLogger } from './loggers';
|
|
@@ -29,44 +29,51 @@ const typeInZH = {
|
|
|
|
|
|
const logger = getLogger('webshot');
|
|
const logger = getLogger('webshot');
|
|
|
|
|
|
-class Webshot
|
|
|
|
-extends CallableInstance<
|
|
|
|
- [Tweets, (...args) => Promise<any>, (...args) => void, number],
|
|
|
|
- Promise<void>
|
|
|
|
-> {
|
|
|
|
|
|
+class Webshot extends CallableInstance<[
|
|
|
|
+ Tweets, (...args) => Promise<any>, (...args) => void, number
|
|
|
|
+], Promise<void>> {
|
|
|
|
|
|
- private browser: Browser;
|
|
|
|
|
|
+ private browser: puppeteer.Browser;
|
|
private mode: number;
|
|
private mode: number;
|
|
|
|
+ private wsUrl: string;
|
|
|
|
|
|
- constructor(mode: number, onready?: () => any) {
|
|
|
|
|
|
+ constructor(wsUrl: string, mode: number, onready?: (...args) => void) {
|
|
super('webshot');
|
|
super('webshot');
|
|
// tslint:disable-next-line: no-conditional-assignment
|
|
// tslint:disable-next-line: no-conditional-assignment
|
|
|
|
+ // eslint-disable-next-line no-cond-assign
|
|
if (this.mode = mode) {
|
|
if (this.mode = mode) {
|
|
onready();
|
|
onready();
|
|
} else {
|
|
} else {
|
|
|
|
+ this.wsUrl = wsUrl;
|
|
this.connect(onready);
|
|
this.connect(onready);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- // use local Chromium
|
|
|
|
- private connect = (onready) => puppeteer.connect({browserURL: 'http://127.0.0.1:9222'})
|
|
|
|
- .then(browser => this.browser = browser)
|
|
|
|
- .then(() => {
|
|
|
|
- logger.info('launched puppeteer browser');
|
|
|
|
- if (onready) return onready();
|
|
|
|
- })
|
|
|
|
- .catch(error => this.reconnect(error, onready))
|
|
|
|
|
|
+ private connect = (onready?: (...args) => void): Promise<void> =>
|
|
|
|
+ axios.get<{[key in 'chromium' | 'firefox' | 'webkit']?: string}>(this.wsUrl)
|
|
|
|
+ .then(res => {
|
|
|
|
+ logger.info(`received websocket endpoint: ${JSON.stringify(res.data)}`);
|
|
|
|
+ const browserType = Object.keys(res.data)[0] as keyof typeof res.data;
|
|
|
|
+ return (puppeteer[browserType] as puppeteer.BrowserType<puppeteer.Browser>)
|
|
|
|
+ .connect({wsEndpoint: res.data[browserType]});
|
|
|
|
+ })
|
|
|
|
+ .then(browser => this.browser = browser)
|
|
|
|
+ .then(() => {
|
|
|
|
+ logger.info('launched puppeteer browser');
|
|
|
|
+ if (onready) return onready();
|
|
|
|
+ })
|
|
|
|
+ .catch(error => this.reconnect(error, onready));
|
|
|
|
|
|
- private reconnect = (error, onready?) => {
|
|
|
|
|
|
+ private reconnect = (error, onready?: (...args) => void) => {
|
|
logger.error(`connection error, reason: ${error}`);
|
|
logger.error(`connection error, reason: ${error}`);
|
|
logger.warn('trying to reconnect in 2.5s...');
|
|
logger.warn('trying to reconnect in 2.5s...');
|
|
return promisify(setTimeout)(2500)
|
|
return promisify(setTimeout)(2500)
|
|
- .then(() => this.connect(onready));
|
|
|
|
- }
|
|
|
|
|
|
+ .then(() => this.connect(onready));
|
|
|
|
+ };
|
|
|
|
|
|
private extendEntity = (media: MediaEntity) => {
|
|
private extendEntity = (media: MediaEntity) => {
|
|
logger.info('not working on a tweet');
|
|
logger.info('not working on a tweet');
|
|
- }
|
|
|
|
|
|
+ };
|
|
|
|
|
|
private renderWebshot = (url: string, height: number, webshotDelay: number): Promise<string> => {
|
|
private renderWebshot = (url: string, height: number, webshotDelay: number): Promise<string> => {
|
|
const jpeg = (data: Readable) => data.pipe(sharp()).jpeg({quality: 90, trellisQuantisation: true});
|
|
const jpeg = (data: Readable) => data.pipe(sharp()).jpeg({quality: 90, trellisQuantisation: true});
|
|
@@ -77,24 +84,29 @@ extends CallableInstance<
|
|
const width = 720;
|
|
const width = 720;
|
|
const zoomFactor = 2;
|
|
const zoomFactor = 2;
|
|
logger.info(`shooting ${width}*${height} webshot for ${url}`);
|
|
logger.info(`shooting ${width}*${height} webshot for ${url}`);
|
|
- this.browser.newPage()
|
|
|
|
|
|
+ this.browser.newPage({
|
|
|
|
+ bypassCSP: true,
|
|
|
|
+ deviceScaleFactor: zoomFactor,
|
|
|
|
+ locale: 'ja-JP',
|
|
|
|
+ timezoneId: 'Asia/Tokyo',
|
|
|
|
+ userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
|
|
|
|
+ })
|
|
.then(page => {
|
|
.then(page => {
|
|
const startTime = new Date().getTime();
|
|
const startTime = new Date().getTime();
|
|
const getTimerTime = () => new Date().getTime() - startTime;
|
|
const getTimerTime = () => new Date().getTime() - startTime;
|
|
const getTimeout = () => Math.max(500, webshotDelay - getTimerTime());
|
|
const getTimeout = () => Math.max(500, webshotDelay - getTimerTime());
|
|
- page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
|
|
|
|
- .then(() => page.setViewport({
|
|
|
|
- width: width / zoomFactor,
|
|
|
|
- height: height / zoomFactor,
|
|
|
|
- isMobile: true,
|
|
|
|
- deviceScaleFactor: zoomFactor,
|
|
|
|
- }))
|
|
|
|
- .then(() => page.setBypassCSP(true))
|
|
|
|
|
|
+ page.setViewportSize({
|
|
|
|
+ width: width / zoomFactor,
|
|
|
|
+ height: height / zoomFactor,
|
|
|
|
+ })
|
|
.then(() => page.goto(url, {waitUntil: 'load', timeout: getTimeout()}))
|
|
.then(() => page.goto(url, {waitUntil: 'load', timeout: getTimeout()}))
|
|
// hide header, "more options" button, like and retweet count
|
|
// hide header, "more options" button, like and retweet count
|
|
.then(() => page.addStyleTag({
|
|
.then(() => page.addStyleTag({
|
|
content: 'header{display:none!important}path[d=\'M20.207 7.043a1 1 0 0 0-1.414 0L12 13.836 5.207 7.043a1 1 0 0 0-1.414 1.414l7.5 7.5a.996.996 0 0 0 1.414 0l7.5-7.5a1 1 0 0 0 0-1.414z\'],div[role=\'button\']{display: none;}',
|
|
content: 'header{display:none!important}path[d=\'M20.207 7.043a1 1 0 0 0-1.414 0L12 13.836 5.207 7.043a1 1 0 0 0-1.414 1.414l7.5 7.5a.996.996 0 0 0 1.414 0l7.5-7.5a1 1 0 0 0 0-1.414z\'],div[role=\'button\']{display: none;}',
|
|
}))
|
|
}))
|
|
|
|
+ .then(() => page.addStyleTag({
|
|
|
|
+ content: '*{font-family:-apple-system,".Helvetica Neue DeskInterface",Hiragino Sans,Hiragino Sans GB,sans-serif!important}',
|
|
|
|
+ }))
|
|
// remove listeners
|
|
// remove listeners
|
|
.then(() => page.evaluate(() => {
|
|
.then(() => page.evaluate(() => {
|
|
const poll = setInterval(() => {
|
|
const poll = setInterval(() => {
|
|
@@ -118,8 +130,7 @@ extends CallableInstance<
|
|
.then(() => page.evaluate(() => {
|
|
.then(() => page.evaluate(() => {
|
|
const cardImg = document.querySelector('div[data-testid^="card.layout"][data-testid$=".media"] img');
|
|
const cardImg = document.querySelector('div[data-testid^="card.layout"][data-testid$=".media"] img');
|
|
if (typeof cardImg?.getAttribute('src') === 'string') {
|
|
if (typeof cardImg?.getAttribute('src') === 'string') {
|
|
- const match = cardImg?.getAttribute('src')
|
|
|
|
- .match(/^(.*\/card_img\/(\d+)\/.+\?format=.*)&name=/);
|
|
|
|
|
|
+ const match = /^(.*\/card_img\/(\d+)\/.+\?format=.*)&name=/.exec(cardImg?.getAttribute('src'));
|
|
if (match) {
|
|
if (match) {
|
|
// tslint:disable-next-line: variable-name
|
|
// tslint:disable-next-line: variable-name
|
|
const [media_url_https, id_str] = match.slice(1);
|
|
const [media_url_https, id_str] = match.slice(1);
|
|
@@ -137,7 +148,9 @@ extends CallableInstance<
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}))
|
|
- .then(cardImg => { if (cardImg) this.extendEntity(cardImg); })
|
|
|
|
|
|
+ .then(cardImg => {
|
|
|
|
+ if (cardImg) this.extendEntity(cardImg);
|
|
|
|
+ })
|
|
.then(() => page.addScriptTag({
|
|
.then(() => page.addScriptTag({
|
|
content: 'document.documentElement.scrollTop=0;',
|
|
content: 'document.documentElement.scrollTop=0;',
|
|
}))
|
|
}))
|
|
@@ -150,10 +163,11 @@ extends CallableInstance<
|
|
}).on('parsed', function () {
|
|
}).on('parsed', function () {
|
|
// remove comment area
|
|
// remove comment area
|
|
// tslint:disable-next-line: no-shadowed-variable
|
|
// tslint:disable-next-line: no-shadowed-variable
|
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-shadow
|
|
const idx = (x: number, y: number) => (this.width * y + x) << 2;
|
|
const idx = (x: number, y: number) => (this.width * y + x) << 2;
|
|
- let boundary = null;
|
|
|
|
|
|
+ let boundary: number = null;
|
|
let x = zoomFactor * 2;
|
|
let x = zoomFactor * 2;
|
|
- for (let y = 0; y < this.height; y++) {
|
|
|
|
|
|
+ for (let y = 0; y < this.height; y += zoomFactor) {
|
|
if (
|
|
if (
|
|
this.data[idx(x, y)] !== 255 &&
|
|
this.data[idx(x, y)] !== 255 &&
|
|
this.data[idx(x, y)] === this.data[idx(x + zoomFactor * 10, y)]
|
|
this.data[idx(x, y)] === this.data[idx(x + zoomFactor * 10, y)]
|
|
@@ -176,7 +190,7 @@ extends CallableInstance<
|
|
x = Math.floor(16 * zoomFactor);
|
|
x = Math.floor(16 * zoomFactor);
|
|
let flag = false;
|
|
let flag = false;
|
|
let cnt = 0;
|
|
let cnt = 0;
|
|
- for (let y = this.height - 1; y >= 0; y--) {
|
|
|
|
|
|
+ for (let y = this.height - 1 - zoomFactor; y >= 0; y -= zoomFactor) {
|
|
if ((this.data[idx(x, y)] === 255) === flag) {
|
|
if ((this.data[idx(x, y)] === 255) === flag) {
|
|
cnt++;
|
|
cnt++;
|
|
flag = !flag;
|
|
flag = !flag;
|
|
@@ -190,21 +204,21 @@ extends CallableInstance<
|
|
// if there are a "retweet" count and "like" count row, this will be the line above it
|
|
// if there are a "retweet" count and "like" count row, this will be the line above it
|
|
if (cnt === 4) {
|
|
if (cnt === 4) {
|
|
const b = y + 1;
|
|
const b = y + 1;
|
|
- if (this.height - boundary - (boundary - b) <= 1) {
|
|
|
|
|
|
+ if (Math.abs(this.height - boundary - (boundary - b)) <= 3 * zoomFactor) {
|
|
boundary = b;
|
|
boundary = b;
|
|
- // }
|
|
|
|
- // }
|
|
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
|
|
- // // if "retweet" count and "like" count are two rows, this will be the line above the first
|
|
|
|
- // if (cnt === 6) {
|
|
|
|
- // const c = y + 1;
|
|
|
|
- // if (this.height - boundary - 2 * (boundary - c) <= 2) {
|
|
|
|
- // boundary = c;
|
|
|
|
|
|
+ // if "retweet" count and "like" count are two rows, this will be the line above the first
|
|
|
|
+ if (cnt === 6) {
|
|
|
|
+ const c = y + 1;
|
|
|
|
+ if (Math.abs(this.height - boundary - 2 * (boundary - c)) <= 3 * zoomFactor) {
|
|
|
|
+ boundary = c;
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- if (boundary != null) {
|
|
|
|
|
|
+ if (boundary !== null) {
|
|
logger.info(`found boundary at ${boundary}, trimming image`);
|
|
logger.info(`found boundary at ${boundary}, trimming image`);
|
|
this.data = this.data.slice(0, idx(this.width, boundary));
|
|
this.data = this.data.slice(0, idx(this.width, boundary));
|
|
this.height = boundary;
|
|
this.height = boundary;
|
|
@@ -226,26 +240,25 @@ extends CallableInstance<
|
|
}).parse(screenshot);
|
|
}).parse(screenshot);
|
|
})
|
|
})
|
|
.catch(err => {
|
|
.catch(err => {
|
|
- if (err.name !== 'TimeoutError') throw err;
|
|
|
|
|
|
+ if (err instanceof Error && err.name !== 'TimeoutError') throw err;
|
|
logger.error(`error shooting webshot for ${url}, could not load web page of tweet`);
|
|
logger.error(`error shooting webshot for ${url}, could not load web page of tweet`);
|
|
resolve({base64: '', boundary: 0});
|
|
resolve({base64: '', boundary: 0});
|
|
})
|
|
})
|
|
- .finally(() => page.close());
|
|
|
|
|
|
+ .finally(() => { page.close(); });
|
|
})
|
|
})
|
|
.catch(reject);
|
|
.catch(reject);
|
|
});
|
|
});
|
|
return promise.then(data => {
|
|
return promise.then(data => {
|
|
if (data.boundary === null) return this.renderWebshot(url, height + 1920, webshotDelay);
|
|
if (data.boundary === null) return this.renderWebshot(url, height + 1920, webshotDelay);
|
|
else return data.base64;
|
|
else return data.base64;
|
|
- }).catch(error =>
|
|
|
|
- new Promise(resolve => this.reconnect(error, resolve))
|
|
|
|
|
|
+ }).catch(error => this.reconnect(error)
|
|
.then(() => this.renderWebshot(url, height, webshotDelay))
|
|
.then(() => this.renderWebshot(url, height, webshotDelay))
|
|
);
|
|
);
|
|
- }
|
|
|
|
|
|
+ };
|
|
|
|
|
|
private fetchMedia = (url: string): Promise<string> => {
|
|
private fetchMedia = (url: string): Promise<string> => {
|
|
const gif = (data: ArrayBuffer) => {
|
|
const gif = (data: ArrayBuffer) => {
|
|
- const matchDims = url.match(/\/(\d+)x(\d+)\//);
|
|
|
|
|
|
+ const matchDims = /\/(\d+)x(\d+)\//.exec(url);
|
|
if (matchDims) {
|
|
if (matchDims) {
|
|
const [ width, height ] = matchDims.slice(1).map(Number);
|
|
const [ width, height ] = matchDims.slice(1).map(Number);
|
|
const factor = width + height > 1600 ? 0.375 : 0.5;
|
|
const factor = width + height > 1600 ? 0.375 : 0.5;
|
|
@@ -263,47 +276,45 @@ extends CallableInstance<
|
|
timeout: 150000,
|
|
timeout: 150000,
|
|
}).then(res => {
|
|
}).then(res => {
|
|
if (res.status === 200) {
|
|
if (res.status === 200) {
|
|
- logger.info(`successfully fetched ${url}`);
|
|
|
|
- resolve(res.data);
|
|
|
|
|
|
+ logger.info(`successfully fetched ${url}`);
|
|
|
|
+ resolve(res.data);
|
|
} else {
|
|
} else {
|
|
logger.error(`failed to fetch ${url}: ${res.status}`);
|
|
logger.error(`failed to fetch ${url}: ${res.status}`);
|
|
reject();
|
|
reject();
|
|
}
|
|
}
|
|
}).catch (err => {
|
|
}).catch (err => {
|
|
- logger.error(`failed to fetch ${url}: ${err.message}`);
|
|
|
|
|
|
+ logger.error(`failed to fetch ${url}: ${err instanceof Error ? err.message : err}`);
|
|
reject();
|
|
reject();
|
|
});
|
|
});
|
|
- }).then(data =>
|
|
|
|
- (async ext => {
|
|
|
|
- switch (ext) {
|
|
|
|
- case 'jpg':
|
|
|
|
- return {mimetype: 'image/jpeg', data};
|
|
|
|
- case 'png':
|
|
|
|
- return {mimetype: 'image/png', data};
|
|
|
|
- case 'mp4':
|
|
|
|
- try {
|
|
|
|
- return {mimetype: 'image/gif', data: await gif(data)};
|
|
|
|
- } catch (err) {
|
|
|
|
- logger.error(err);
|
|
|
|
- throw Error(err);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- })((url.match(/\?format=([a-z]+)&/) ?? url.match(/.*\/.*\.([^?]+)/))[1])
|
|
|
|
|
|
+ }).then(data => (async ext => {
|
|
|
|
+ switch (ext) {
|
|
|
|
+ case 'jpg':
|
|
|
|
+ return {mimetype: 'image/jpeg', data};
|
|
|
|
+ case 'png':
|
|
|
|
+ return {mimetype: 'image/png', data};
|
|
|
|
+ case 'mp4':
|
|
|
|
+ try {
|
|
|
|
+ return {mimetype: 'image/gif', data: await gif(data)};
|
|
|
|
+ } catch (err) {
|
|
|
|
+ logger.error(err);
|
|
|
|
+ throw Error(err);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ })(((/\?format=([a-z]+)&/.exec(url)) ?? (/.*\/.*\.([^?]+)/.exec(url)))[1])
|
|
.catch(() => {
|
|
.catch(() => {
|
|
logger.warn('unable to find MIME type of fetched media, failing this fetch');
|
|
logger.warn('unable to find MIME type of fetched media, failing this fetch');
|
|
throw Error();
|
|
throw Error();
|
|
})
|
|
})
|
|
- ).then(typedData =>
|
|
|
|
- `data:${typedData.mimetype};base64,${Buffer.from(typedData.data).toString('base64')}`
|
|
|
|
|
|
+ ).then(typedData => `data:${typedData.mimetype};base64,${Buffer.from(typedData.data).toString('base64')}`
|
|
);
|
|
);
|
|
- }
|
|
|
|
|
|
+ };
|
|
|
|
|
|
public webshot(
|
|
public webshot(
|
|
tweets: Tweets,
|
|
tweets: Tweets,
|
|
uploader: (
|
|
uploader: (
|
|
img: ReturnType<typeof Message.Image>,
|
|
img: ReturnType<typeof Message.Image>,
|
|
- lastResort: (...args) => ReturnType<typeof Message.Plain>)
|
|
|
|
- => Promise<ReturnType<typeof Message.Image | typeof Message.Plain>>,
|
|
|
|
|
|
+ lastResort: (...args) => ReturnType<typeof Message.Plain>
|
|
|
|
+ ) => Promise<ReturnType<typeof Message.Image | typeof Message.Plain>>,
|
|
callback: (msgs: MessageChain, text: string, author: string) => void,
|
|
callback: (msgs: MessageChain, text: string, author: string) => void,
|
|
webshotDelay: number
|
|
webshotDelay: number
|
|
): Promise<void> {
|
|
): Promise<void> {
|
|
@@ -314,11 +325,12 @@ extends CallableInstance<
|
|
promise = promise.then(() => {
|
|
promise = promise.then(() => {
|
|
logger.info(`working on ${twi.user.screen_name}/${twi.id_str}`);
|
|
logger.info(`working on ${twi.user.screen_name}/${twi.id_str}`);
|
|
});
|
|
});
|
|
- const originTwi = twi;
|
|
|
|
|
|
+ const originTwi = twi.retweeted_status || twi;
|
|
const messageChain: MessageChain = [];
|
|
const messageChain: MessageChain = [];
|
|
|
|
|
|
// text processing
|
|
// text processing
|
|
- const author = `${twi.user.name} (@${twi.user.screen_name}):\n`;
|
|
|
|
|
|
+ let author = `${twi.user.name} (@${twi.user.screen_name}):\n`;
|
|
|
|
+ if (twi.retweeted_status) author += `RT @${twi.retweeted_status.user.screen_name}: `;
|
|
|
|
|
|
let text = originTwi.full_text;
|
|
let text = originTwi.full_text;
|
|
|
|
|
|
@@ -359,6 +371,7 @@ extends CallableInstance<
|
|
}
|
|
}
|
|
// fetch extra entities
|
|
// fetch extra entities
|
|
// tslint:disable-next-line: curly
|
|
// tslint:disable-next-line: curly
|
|
|
|
+ // eslint-disable-next-line curly
|
|
if (1 - this.mode % 2) promise = promise.then(() => {
|
|
if (1 - this.mode % 2) promise = promise.then(() => {
|
|
if (originTwi.extended_entities) {
|
|
if (originTwi.extended_entities) {
|
|
return chainPromises(originTwi.extended_entities.media.map(media => {
|
|
return chainPromises(originTwi.extended_entities.media.map(media => {
|
|
@@ -371,7 +384,7 @@ extends CallableInstance<
|
|
.sort((var1, var2) => var2.bitrate - var1.bitrate)
|
|
.sort((var1, var2) => var2.bitrate - var1.bitrate)
|
|
.map(variant => variant.url)[0]; // largest video
|
|
.map(variant => variant.url)[0]; // largest video
|
|
}
|
|
}
|
|
- const altMessage = Message.Plain(`\n[失败的${typeInZH[media.type].type}:${url}]`);
|
|
|
|
|
|
+ const altMessage = Message.Plain(`\n[失败的${typeInZH[media.type as keyof typeof typeInZH].type}:${url}]`);
|
|
return this.fetchMedia(url)
|
|
return this.fetchMedia(url)
|
|
.then(base64url =>
|
|
.then(base64url =>
|
|
uploader(Message.Image('', base64url, media.type === 'photo' ? url : `${url} as gif`), () => altMessage)
|
|
uploader(Message.Image('', base64url, media.type === 'photo' ? url : `${url} as gif`), () => altMessage)
|
|
@@ -380,9 +393,7 @@ extends CallableInstance<
|
|
logger.warn('unable to fetch media, sending plain text instead...');
|
|
logger.warn('unable to fetch media, sending plain text instead...');
|
|
return altMessage;
|
|
return altMessage;
|
|
})
|
|
})
|
|
- .then(msg => {
|
|
|
|
- messageChain.push(msg);
|
|
|
|
- });
|
|
|
|
|
|
+ .then(msg => { messageChain.push(msg); });
|
|
}));
|
|
}));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
@@ -403,7 +414,7 @@ extends CallableInstance<
|
|
if (originTwi.is_quote_status) {
|
|
if (originTwi.is_quote_status) {
|
|
promise = promise.then(() => {
|
|
promise = promise.then(() => {
|
|
messageChain.push(
|
|
messageChain.push(
|
|
- Message.Plain(`\n回复此命令查看引用的推文:\n/twitterpic_view ${originTwi.quoted_status_permalink.expanded}`)
|
|
|
|
|
|
+ Message.Plain(`\n回复此命令查看引用的推文:\n/twitter_view ${originTwi.quoted_status_permalink.expanded}`)
|
|
);
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|