|
@@ -14,10 +14,10 @@ interface IWorkerOption {
|
|
|
bot: QQBot;
|
|
|
workInterval: number;
|
|
|
webshotDelay: number;
|
|
|
- consumer_key: string;
|
|
|
- consumer_secret: string;
|
|
|
- access_token_key: string;
|
|
|
- access_token_secret: string;
|
|
|
+ consumerKey: string;
|
|
|
+ consumerSecret: string;
|
|
|
+ accessTokenKey: string;
|
|
|
+ accessTokenSecret: string;
|
|
|
mode: number;
|
|
|
}
|
|
|
|
|
@@ -31,13 +31,13 @@ export class ScreenNameNormalizer {
|
|
|
public static async normalizeLive(username: string) {
|
|
|
if (this._queryUser) {
|
|
|
return await this._queryUser(username)
|
|
|
- .catch((err: {code: number, message: string}[]) => {
|
|
|
- if (err[0].code !== 50) {
|
|
|
- logger.warn(`error looking up user: ${err[0].message}`);
|
|
|
- return username;
|
|
|
- }
|
|
|
- return null;
|
|
|
- });
|
|
|
+ .catch((err: {code: number, message: string}[]) => {
|
|
|
+ if (err[0].code !== 50) {
|
|
|
+ logger.warn(`error looking up user: ${err[0].message}`);
|
|
|
+ return username;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ });
|
|
|
}
|
|
|
return this.normalize(username);
|
|
|
}
|
|
@@ -64,9 +64,8 @@ export let sendTimeline = (
|
|
|
};
|
|
|
|
|
|
const TWITTER_EPOCH = 1288834974657;
|
|
|
-const snowflake = (epoch: number) =>
|
|
|
- Number.isNaN(epoch) ? undefined :
|
|
|
- BigNumOps.lShift(String(epoch - 1 - TWITTER_EPOCH), 22);
|
|
|
+const snowflake = (epoch: number) => Number.isNaN(epoch) ? undefined :
|
|
|
+ BigNumOps.lShift(String(epoch - 1 - TWITTER_EPOCH), 22);
|
|
|
|
|
|
const logger = getLogger('twitter');
|
|
|
const maxTrials = 3;
|
|
@@ -124,10 +123,10 @@ export default class {
|
|
|
|
|
|
constructor(opt: IWorkerOption) {
|
|
|
this.client = new Twitter({
|
|
|
- consumer_key: opt.consumer_key,
|
|
|
- consumer_secret: opt.consumer_secret,
|
|
|
- access_token_key: opt.access_token_key,
|
|
|
- access_token_secret: opt.access_token_secret,
|
|
|
+ consumer_key: opt.consumerKey,
|
|
|
+ consumer_secret: opt.consumerSecret,
|
|
|
+ access_token_key: opt.accessTokenKey,
|
|
|
+ access_token_secret: opt.accessTokenSecret,
|
|
|
});
|
|
|
this.lockfile = opt.lockfile;
|
|
|
this.lock = opt.lock;
|
|
@@ -138,13 +137,13 @@ export default class {
|
|
|
ScreenNameNormalizer._queryUser = this.queryUser;
|
|
|
sendTweet = (id, receiver) => {
|
|
|
this.getTweet(id, this.sendTweets(`tweet ${id}`, receiver))
|
|
|
- .catch((err: {code: number, message: string}[]) => {
|
|
|
- if (err[0].code !== 144) {
|
|
|
- logger.warn(`error retrieving tweet: ${err[0].message}`);
|
|
|
- this.bot.sendTo(receiver, `获取推文时出现错误:${err[0].message}`);
|
|
|
- }
|
|
|
- this.bot.sendTo(receiver, '找不到请求的推文,它可能已被删除。');
|
|
|
- });
|
|
|
+ .catch((err: {code: number, message: string}[]) => {
|
|
|
+ if (err[0].code !== 144) {
|
|
|
+ logger.warn(`error retrieving tweet: ${err[0].message}`);
|
|
|
+ this.bot.sendTo(receiver, `获取推文时出现错误:${err[0].message}`);
|
|
|
+ }
|
|
|
+ this.bot.sendTo(receiver, '找不到请求的推文,它可能已被删除。');
|
|
|
+ });
|
|
|
};
|
|
|
sendTimeline = ({username, count, since, until, noreps, norts}, receiver) => {
|
|
|
const countNum = Number(count) || 10;
|
|
@@ -156,25 +155,25 @@ export default class {
|
|
|
noreps: {on: true, off: false}[noreps],
|
|
|
norts: {on: true, off: false}[norts],
|
|
|
})
|
|
|
- .then(tweets => chainPromises(
|
|
|
- tweets.map(tweet => this.bot.sendTo(receiver, `\
|
|
|
+ .then(tweets => chainPromises(
|
|
|
+ tweets.map(tweet => this.bot.sendTo(receiver, `\
|
|
|
编号:${tweet.id_str}
|
|
|
时间:${tweet.created_at}
|
|
|
媒体:${tweet.extended_entities ? '有' : '无'}
|
|
|
正文:\n${tweet.full_text.replace(/^([\s\S\n]{50})[\s\S\n]+?( https:\/\/t.co\/.*)?$/, '$1…$2')}`
|
|
|
+ ))
|
|
|
+ .concat(this.bot.sendTo(receiver, tweets.length ?
|
|
|
+ '时间线查询完毕,使用 /twitter_view <编号> 查看推文详细内容。' :
|
|
|
+ '时间线查询完毕,没有找到符合条件的推文。'
|
|
|
+ ))
|
|
|
))
|
|
|
- .concat(this.bot.sendTo(receiver, tweets.length ?
|
|
|
- '时间线查询完毕,使用 /twitter_view <编号> 查看推文详细内容。' :
|
|
|
- '时间线查询完毕,没有找到符合条件的推文。'
|
|
|
- ))
|
|
|
- ))
|
|
|
- .catch((err: {code: number, message: string}[]) => {
|
|
|
- if (err[0]?.code !== 34) {
|
|
|
- logger.warn(`error retrieving timeline: ${err[0]?.message || err}`);
|
|
|
- return this.bot.sendTo(receiver, `获取时间线时出现错误:${err[0]?.message || err}`);
|
|
|
- }
|
|
|
- this.bot.sendTo(receiver, `找不到用户 ${username.replace(/^@?(.*)$/, '@$1')}。`);
|
|
|
- });
|
|
|
+ .catch((err: {code: number, message: string}[]) => {
|
|
|
+ if (err[0]?.code !== 34) {
|
|
|
+ logger.warn(`error retrieving timeline: ${err[0]?.message || err}`);
|
|
|
+ return this.bot.sendTo(receiver, `获取时间线时出现错误:${err[0]?.message || err}`);
|
|
|
+ }
|
|
|
+ this.bot.sendTo(receiver, `找不到用户 ${username.replace(/^@?(.*)$/, '@$1')}。`);
|
|
|
+ });
|
|
|
};
|
|
|
}
|
|
|
|
|
@@ -183,35 +182,32 @@ export default class {
|
|
|
this.mode,
|
|
|
() => setTimeout(this.work, this.workInterval * 1000)
|
|
|
);
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
- public queryUser = (username: string) =>
|
|
|
- this.client.get('users/show', {screen_name: username})
|
|
|
- .then((user: FullUser) => user.screen_name)
|
|
|
+ public queryUser = (username: string) => this.client.get('users/show', {screen_name: username})
|
|
|
+ .then((user: FullUser) => user.screen_name);
|
|
|
|
|
|
public queryTimelineReverse = (conf: ITimelineQueryConfig) => {
|
|
|
if (!conf.since) return this.queryTimeline(conf);
|
|
|
const count = conf.count;
|
|
|
const maxID = conf.until;
|
|
|
conf.count = undefined;
|
|
|
- const until = () =>
|
|
|
- BigNumOps.min(maxID, BigNumOps.plus(conf.since, String(7 * 24 * 3600 * 1000 * 2 ** 22)));
|
|
|
+ const until = () => BigNumOps.min(maxID, BigNumOps.plus(conf.since, String(7 * 24 * 3600 * 1000 * 2 ** 22)));
|
|
|
conf.until = until();
|
|
|
- const promise = (tweets: ITweet[]): Promise<ITweet[]> =>
|
|
|
- this.queryTimeline(conf).then(newTweets => {
|
|
|
- tweets = newTweets.concat(tweets);
|
|
|
- conf.since = conf.until;
|
|
|
- conf.until = until();
|
|
|
- if (
|
|
|
- tweets.length >= count ||
|
|
|
+ const promise = (tweets: ITweet[]): Promise<ITweet[]> =>this.queryTimeline(conf).then(newTweets => {
|
|
|
+ tweets = newTweets.concat(tweets);
|
|
|
+ conf.since = conf.until;
|
|
|
+ conf.until = until();
|
|
|
+ if (
|
|
|
+ tweets.length >= count ||
|
|
|
BigNumOps.compare(conf.since, conf.until) >= 0
|
|
|
- ) {
|
|
|
- return tweets.slice(-count);
|
|
|
- }
|
|
|
- return promise(tweets);
|
|
|
- });
|
|
|
+ ) {
|
|
|
+ return tweets.slice(-count);
|
|
|
+ }
|
|
|
+ return promise(tweets);
|
|
|
+ });
|
|
|
return promise([]);
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
public queryTimeline = (
|
|
|
{ username, count, since, until, noreps, norts }: ITimelineQueryConfig
|
|
@@ -224,7 +220,7 @@ export default class {
|
|
|
...(until && {until}),
|
|
|
...(noreps && {noreps}),
|
|
|
...(norts && {norts}),
|
|
|
- })}`);
|
|
|
+ })}`);
|
|
|
const fetchTimeline = (
|
|
|
config = {
|
|
|
screen_name: username.slice(1),
|
|
@@ -236,27 +232,26 @@ export default class {
|
|
|
tweet_mode: 'extended',
|
|
|
},
|
|
|
tweets: ITweet[] = []
|
|
|
- ): Promise<ITweet[]> =>
|
|
|
- this.client.get('statuses/user_timeline', config)
|
|
|
- .then((newTweets: ITweet[]) => {
|
|
|
- if (newTweets.length) {
|
|
|
- logger.debug(`fetched tweets: ${JSON.stringify(newTweets)}`);
|
|
|
- config.max_id = BigNumOps.plus('-1', newTweets[newTweets.length - 1].id_str);
|
|
|
- logger.info(`timeline query of ${username} yielded ${
|
|
|
- newTweets.length
|
|
|
- } new tweets, next query will start at offset ${config.max_id}`);
|
|
|
- tweets.push(...newTweets);
|
|
|
- }
|
|
|
- if (!newTweets.length || tweets.length >= count) {
|
|
|
- logger.info(`timeline query of ${username} finished successfully, ${
|
|
|
- tweets.length
|
|
|
- } tweets have been fetched`);
|
|
|
- return tweets.slice(0, count);
|
|
|
- }
|
|
|
- return fetchTimeline(config, tweets);
|
|
|
- });
|
|
|
+ ): Promise<ITweet[]> => this.client.get('statuses/user_timeline', config)
|
|
|
+ .then((newTweets: ITweet[]) => {
|
|
|
+ if (newTweets.length) {
|
|
|
+ logger.debug(`fetched tweets: ${JSON.stringify(newTweets)}`);
|
|
|
+ config.max_id = BigNumOps.plus('-1', newTweets[newTweets.length - 1].id_str);
|
|
|
+ logger.info(`timeline query of ${username} yielded ${
|
|
|
+ newTweets.length
|
|
|
+ } new tweets, next query will start at offset ${config.max_id}`);
|
|
|
+ tweets.push(...newTweets);
|
|
|
+ }
|
|
|
+ if (!newTweets.length || tweets.length >= count) {
|
|
|
+ logger.info(`timeline query of ${username} finished successfully, ${
|
|
|
+ tweets.length
|
|
|
+ } tweets have been fetched`);
|
|
|
+ return tweets.slice(0, count);
|
|
|
+ }
|
|
|
+ return fetchTimeline(config, tweets);
|
|
|
+ });
|
|
|
return fetchTimeline();
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
private workOnTweets = (
|
|
|
tweets: Tweets,
|
|
@@ -267,20 +262,19 @@ export default class {
|
|
|
lastResort: (...args) => ReturnType<typeof Message.Plain>
|
|
|
) => {
|
|
|
let timeout = uploadTimeout;
|
|
|
- return retryOnError(() =>
|
|
|
- this.bot.uploadPic(message, timeout).then(() => message),
|
|
|
- (_, count, terminate: (defaultValue: ReturnType<typeof Message.Plain>) => void) => {
|
|
|
- if (count <= maxTrials) {
|
|
|
- timeout *= (count + 2) / (count + 1);
|
|
|
- logger.warn(`retry uploading for the ${ordinal(count)} time...`);
|
|
|
- } else {
|
|
|
- logger.warn(`${count - 1} consecutive failures while uploading, trying plain text instead...`);
|
|
|
- terminate(lastResort());
|
|
|
- }
|
|
|
- });
|
|
|
+ return retryOnError(() => this.bot.uploadPic(message, timeout).then(() => message),
|
|
|
+ (_, count, terminate: (defaultValue: ReturnType<typeof Message.Plain>) => void) => {
|
|
|
+ if (count <= maxTrials) {
|
|
|
+ timeout *= (count + 2) / (count + 1);
|
|
|
+ logger.warn(`retry uploading for the ${ordinal(count)} time...`);
|
|
|
+ } else {
|
|
|
+ logger.warn(`${count - 1} consecutive failures while uploading, trying plain text instead...`);
|
|
|
+ terminate(lastResort());
|
|
|
+ }
|
|
|
+ });
|
|
|
};
|
|
|
return this.webshot(tweets, uploader, sendTweets, this.webshotDelay);
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
public getTweet = (id: string, sender: (msg: MessageChain, text: string, author: string) => void) => {
|
|
|
const endpoint = 'statuses/show';
|
|
@@ -289,29 +283,28 @@ export default class {
|
|
|
tweet_mode: 'extended',
|
|
|
};
|
|
|
return this.client.get(endpoint, config)
|
|
|
- .then((tweet: Tweet) => {
|
|
|
- logger.debug(`api returned tweet ${JSON.stringify(tweet)} for query id=${id}`);
|
|
|
- return this.workOnTweets([tweet], sender);
|
|
|
- });
|
|
|
- }
|
|
|
+ .then((tweet: Tweet) => {
|
|
|
+ logger.debug(`api returned tweet ${JSON.stringify(tweet)} for query id=${id}`);
|
|
|
+ return this.workOnTweets([tweet], sender);
|
|
|
+ });
|
|
|
+ };
|
|
|
|
|
|
- private sendTweets = (source?: string, ...to: IChat[]) =>
|
|
|
- (msg: MessageChain, text: string, author: string) => {
|
|
|
+ private sendTweets = (source?: string, ...to: IChat[]) => (msg: MessageChain, text: string, author: string) => {
|
|
|
to.forEach(subscriber => {
|
|
|
logger.info(`pushing data${source ? ` of ${source}` : ''} to ${JSON.stringify(subscriber)}`);
|
|
|
retryOnError(
|
|
|
() => this.bot.sendTo(subscriber, msg),
|
|
|
- (_, count, terminate: (doNothing: Promise<void>) => void) => {
|
|
|
- if (count <= maxTrials) {
|
|
|
- logger.warn(`retry sending to ${subscriber.chatID} for the ${ordinal(count)} time...`);
|
|
|
- } else {
|
|
|
- logger.warn(`${count - 1} consecutive failures while sending` +
|
|
|
+ (_, count, terminate: (doNothing: Promise<void>) => void) => {
|
|
|
+ if (count <= maxTrials) {
|
|
|
+ logger.warn(`retry sending to ${subscriber.chatID} for the ${ordinal(count)} time...`);
|
|
|
+ } else {
|
|
|
+ logger.warn(`${count - 1} consecutive failures while sending` +
|
|
|
'message chain, trying plain text instead...');
|
|
|
- terminate(this.bot.sendTo(subscriber, author + text));
|
|
|
- }
|
|
|
- });
|
|
|
+ terminate(this.bot.sendTo(subscriber, author + text));
|
|
|
+ }
|
|
|
+ });
|
|
|
});
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
public work = () => {
|
|
|
const lock = this.lock;
|
|
@@ -338,8 +331,8 @@ export default class {
|
|
|
logger.debug(`pulling feed ${currentFeed}`);
|
|
|
|
|
|
const promise = new Promise(resolve => {
|
|
|
- let match = currentFeed.match(/https:\/\/twitter.com\/([^\/]+)\/lists\/([^\/]+)/);
|
|
|
- let config: any;
|
|
|
+ let match = /https:\/\/twitter.com\/([^\/]+)\/lists\/([^\/]+)/.exec(currentFeed);
|
|
|
+ let config: {[key: string]: any};
|
|
|
let endpoint: string;
|
|
|
if (match) {
|
|
|
if (match[1] === 'i') {
|
|
@@ -356,7 +349,7 @@ export default class {
|
|
|
}
|
|
|
endpoint = 'lists/statuses';
|
|
|
} else {
|
|
|
- match = currentFeed.match(/https:\/\/twitter.com\/([^\/]+)/);
|
|
|
+ match = /https:\/\/twitter.com\/([^\/]+)/.exec(currentFeed);
|
|
|
if (match) {
|
|
|
config = {
|
|
|
screen_name: match[1],
|
|
@@ -370,7 +363,7 @@ export default class {
|
|
|
if (endpoint) {
|
|
|
const offset = lock.threads[currentFeed].offset as unknown as number;
|
|
|
if (offset > 0) config.since_id = offset;
|
|
|
- this.client.get(endpoint, config, (error, tweets, response) => {
|
|
|
+ this.client.get(endpoint, config, (error: {[key: string]: any}[], tweets, response) => {
|
|
|
if (error) {
|
|
|
if (error instanceof Array && error.length > 0 && error[0].code === 34) {
|
|
|
logger.warn(`error on fetching tweets for ${currentFeed}: ${JSON.stringify(error)}`);
|
|
@@ -381,7 +374,7 @@ export default class {
|
|
|
} else {
|
|
|
logger.error(`unhandled error on fetching tweets for ${currentFeed}: ${JSON.stringify(error)}`);
|
|
|
}
|
|
|
- resolve();
|
|
|
+ resolve([]);
|
|
|
} else resolve(tweets);
|
|
|
});
|
|
|
}
|
|
@@ -401,7 +394,7 @@ export default class {
|
|
|
if (currentThread.offset === '0') tweets.splice(1);
|
|
|
|
|
|
return this.workOnTweets(tweets, this.sendTweets(`thread ${currentFeed}`, ...currentThread.subscribers))
|
|
|
- .then(updateDate).then(updateOffset);
|
|
|
+ .then(updateDate).then(updateOffset);
|
|
|
})
|
|
|
.then(() => {
|
|
|
lock.workon++;
|
|
@@ -412,5 +405,5 @@ export default class {
|
|
|
this.work();
|
|
|
}, timeout);
|
|
|
});
|
|
|
- }
|
|
|
+ };
|
|
|
}
|