|
@@ -15,13 +15,14 @@ interface IWorkerOption {
|
|
|
bot: QQBot;
|
|
|
workInterval: number;
|
|
|
webshotDelay: number;
|
|
|
- consumer_key: string;
|
|
|
- consumer_secret: string;
|
|
|
- access_token_key: string;
|
|
|
- access_token_secret: string;
|
|
|
- private_csrf_token: string;
|
|
|
- private_auth_token: string;
|
|
|
+ consumerKey: string;
|
|
|
+ consumerSecret: string;
|
|
|
+ accessTokenKey: string;
|
|
|
+ accessTokenSecret: string;
|
|
|
+ privateCsrfToken: string;
|
|
|
+ privateAuthToken: string;
|
|
|
mode: number;
|
|
|
+ wsUrl: string;
|
|
|
}
|
|
|
|
|
|
export class ScreenNameNormalizer {
|
|
@@ -29,7 +30,7 @@ export class ScreenNameNormalizer {
|
|
|
// tslint:disable-next-line: variable-name
|
|
|
public static _queryUser: (username: string) => Promise<string>;
|
|
|
|
|
|
- public static permaFeeds = {};
|
|
|
+ public static permaFeeds: {[key: string]: string} = {};
|
|
|
|
|
|
public static savePermaFeedForUser(user: FullUser) {
|
|
|
this.permaFeeds[`https://twitter.com/${user.screen_name}`] = `https://twitter.com/i/user/${user.id_str}`;
|
|
@@ -40,13 +41,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);
|
|
|
}
|
|
@@ -92,7 +93,7 @@ export type MediaEntity = TwitterTypes.MediaEntity;
|
|
|
type TwitterMod = {
|
|
|
-readonly [K in keyof Twitter]: Twitter[K];
|
|
|
} & {
|
|
|
- options?: any;
|
|
|
+ options?: Twitter.Options,
|
|
|
};
|
|
|
|
|
|
interface IFleet {
|
|
@@ -102,21 +103,21 @@ interface IFleet {
|
|
|
fleet_id: string;
|
|
|
fleet_thread_id: string;
|
|
|
media_bounding_boxes: [{
|
|
|
- anchor_point_x: number;
|
|
|
- anchor_point_y: number;
|
|
|
- width: number;
|
|
|
- height: number;
|
|
|
- rotation: number;
|
|
|
+ anchor_point_x: number,
|
|
|
+ anchor_point_y: number,
|
|
|
+ width: number,
|
|
|
+ height: number,
|
|
|
+ rotation: number,
|
|
|
entity: {
|
|
|
- type: string;
|
|
|
- value: any;
|
|
|
- }
|
|
|
+ type: string,
|
|
|
+ value: any,
|
|
|
+ },
|
|
|
}];
|
|
|
media_entity: MediaEntity;
|
|
|
media_key: {
|
|
|
- media_category: 'TWEET_IMAGE' | 'TWEET_VIDEO';
|
|
|
- media_id: number;
|
|
|
- media_id_str: string;
|
|
|
+ media_category: 'TWEET_IMAGE' | 'TWEET_VIDEO',
|
|
|
+ media_id: number,
|
|
|
+ media_id_str: string,
|
|
|
};
|
|
|
mentions: any;
|
|
|
mentions_str: any;
|
|
@@ -144,13 +145,14 @@ export default class {
|
|
|
private webshotDelay: number;
|
|
|
private webshot: Webshot;
|
|
|
private mode: number;
|
|
|
+ private wsUrl: string;
|
|
|
|
|
|
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.privateClient = new Twitter({
|
|
|
bearer_token: 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
|
|
@@ -159,8 +161,8 @@ export default class {
|
|
|
headers: {
|
|
|
...this.privateClient.options.request_options.headers,
|
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
|
- Cookie: `auth_token=${opt.private_auth_token}; ct0=${opt.private_csrf_token};`,
|
|
|
- 'X-CSRF-Token': opt.private_csrf_token,
|
|
|
+ Cookie: `auth_token=${opt.privateAuthToken}; ct0=${opt.privateCsrfToken};`,
|
|
|
+ 'X-CSRF-Token': opt.privateCsrfToken,
|
|
|
},
|
|
|
});
|
|
|
this.lockfile = opt.lockfile;
|
|
@@ -168,46 +170,48 @@ export default class {
|
|
|
this.workInterval = opt.workInterval;
|
|
|
this.bot = opt.bot;
|
|
|
this.mode = opt.mode;
|
|
|
+ this.wsUrl = opt.wsUrl;
|
|
|
ScreenNameNormalizer._queryUser = this.queryUser;
|
|
|
sendAllFleets = (username, receiver) => {
|
|
|
this.client.get('users/show', {screen_name: username})
|
|
|
- .then((user: FullUser) => {
|
|
|
- const feed = `https://twitter.com/${user.screen_name}`;
|
|
|
- return this.getFleets(user.id_str)
|
|
|
- .catch(error => {
|
|
|
- logger.error(`unhandled error while fetching fleets for ${feed}: ${JSON.stringify(error)}`);
|
|
|
- this.bot.sendTo(receiver, `获取 Fleets 时出现错误:${error}`);
|
|
|
+ .then((user: FullUser) => {
|
|
|
+ const feed = `https://twitter.com/${user.screen_name}`;
|
|
|
+ return this.getFleets(user.id_str)
|
|
|
+ .catch(error => {
|
|
|
+ logger.error(`unhandled error while fetching fleets for ${feed}: ${JSON.stringify(error)}`);
|
|
|
+ this.bot.sendTo(receiver, `获取 Fleets 时出现错误:${error}`);
|
|
|
+ })
|
|
|
+ .then((fleetFeed: IFleetFeed) => {
|
|
|
+ if (!fleetFeed || fleetFeed.fleet_threads.length === 0) {
|
|
|
+ this.bot.sendTo(receiver, `当前用户(@${user.screen_name})没有可用的 Fleets。`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.workOnFleets(user, fleetFeed.fleet_threads[0].fleets, this.sendFleets(`thread ${feed}`, receiver));
|
|
|
+ });
|
|
|
})
|
|
|
- .then((fleetFeed: IFleetFeed) => {
|
|
|
- if (!fleetFeed || fleetFeed.fleet_threads.length === 0) {
|
|
|
- this.bot.sendTo(receiver, `当前用户(@${user.screen_name})没有可用的 Fleets。`);
|
|
|
- return;
|
|
|
+ .catch((err: {code: number, message: string}[]) => {
|
|
|
+ if (err[0].code !== 50) {
|
|
|
+ logger.warn(`error looking up user: ${err[0].message}, unable to fetch fleets`);
|
|
|
}
|
|
|
- this.workOnFleets(user, fleetFeed.fleet_threads[0].fleets, this.sendFleets(`thread ${feed}`, receiver));
|
|
|
+ this.bot.sendTo(receiver, `找不到用户 ${username.replace(/^@?(.*)$/, '@$1')}。`);
|
|
|
});
|
|
|
- })
|
|
|
- .catch((err: {code: number, message: string}[]) => {
|
|
|
- if (err[0].code !== 50) {
|
|
|
- logger.warn(`error looking up user: ${err[0].message}, unable to fetch fleets`);
|
|
|
- }
|
|
|
- this.bot.sendTo(receiver, `找不到用户 ${username.replace(/^@?(.*)$/, '@$1')}。`);
|
|
|
- });
|
|
|
};
|
|
|
}
|
|
|
|
|
|
public launch = () => {
|
|
|
this.webshot = new Webshot(
|
|
|
+ this.wsUrl,
|
|
|
this.mode,
|
|
|
() => setTimeout(this.work, this.workInterval * 1000)
|
|
|
);
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
public queryUser = (username: string) =>
|
|
|
this.client.get('users/show', {screen_name: username})
|
|
|
- .then((user: FullUser) => {
|
|
|
- ScreenNameNormalizer.savePermaFeedForUser(user);
|
|
|
- return user.screen_name;
|
|
|
- })
|
|
|
+ .then((user: FullUser) => {
|
|
|
+ ScreenNameNormalizer.savePermaFeedForUser(user);
|
|
|
+ return user.screen_name;
|
|
|
+ });
|
|
|
|
|
|
private workOnFleets = (
|
|
|
user: FullUser,
|
|
@@ -219,38 +223,37 @@ 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(user, fleets, uploader, sendFleets, this.webshotDelay);
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
private sendFleets = (source?: string, ...to: IChat[]) =>
|
|
|
- (msg: MessageChain, text: 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` +
|
|
|
+ (msg: MessageChain, text: 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` +
|
|
|
'message chain, trying plain text instead...');
|
|
|
- terminate(this.bot.sendTo(subscriber, text));
|
|
|
- }
|
|
|
+ terminate(this.bot.sendTo(subscriber, text));
|
|
|
+ }
|
|
|
+ });
|
|
|
});
|
|
|
- });
|
|
|
- }
|
|
|
+ };
|
|
|
|
|
|
private getFleets = (userID: string) => new Promise<IFleetFeed | void>((resolve, reject) => {
|
|
|
const endpoint = `https://api.twitter.com/fleets/v1/user_fleets?user_id=${userID}`;
|
|
@@ -258,7 +261,7 @@ export default class {
|
|
|
if (error) reject(error);
|
|
|
else resolve(fleetFeed);
|
|
|
});
|
|
|
- })
|
|
|
+ });
|
|
|
|
|
|
public work = () => {
|
|
|
const lock = this.lock;
|
|
@@ -285,49 +288,49 @@ export default class {
|
|
|
logger.debug(`pulling feed ${currentFeed}`);
|
|
|
|
|
|
let user: FullUser;
|
|
|
- let match = currentFeed.match(/https:\/\/twitter.com\/([^\/]+)/);
|
|
|
- if (match) match = lock.threads[currentFeed].permaFeed.match(/https:\/\/twitter.com\/i\/user\/([^\/]+)/);
|
|
|
+ let match = /https:\/\/twitter.com\/([^\/]+)/.exec(currentFeed);
|
|
|
+ if (match) match = /https:\/\/twitter.com\/i\/user\/([^\/]+)/.exec(lock.threads[currentFeed].permaFeed);
|
|
|
if (!match) {
|
|
|
logger.error(`cannot get endpoint for feed ${currentFeed}`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
this.client.get('users/show', {user_id: match[1]})
|
|
|
- .then((fullUser: FullUser) => { user = fullUser; return this.getFleets(match[1]); })
|
|
|
- .catch(error => {
|
|
|
- logger.error(`unhandled error on fetching fleets for ${currentFeed}: ${JSON.stringify(error)}`);
|
|
|
- })
|
|
|
- .then((fleetFeed: IFleetFeed) => {
|
|
|
- logger.debug(`private api returned ${JSON.stringify(fleetFeed)} for feed ${currentFeed}`);
|
|
|
- logger.debug(`api returned ${JSON.stringify(user)} for owner of feed ${currentFeed}`);
|
|
|
- const currentThread = lock.threads[currentFeed];
|
|
|
+ .then((fullUser: FullUser) => { user = fullUser; return this.getFleets(match[1]); })
|
|
|
+ .catch(error => {
|
|
|
+ logger.error(`unhandled error on fetching fleets for ${currentFeed}: ${JSON.stringify(error)}`);
|
|
|
+ })
|
|
|
+ .then((fleetFeed: IFleetFeed) => {
|
|
|
+ logger.debug(`private api returned ${JSON.stringify(fleetFeed)} for feed ${currentFeed}`);
|
|
|
+ logger.debug(`api returned ${JSON.stringify(user)} for owner of feed ${currentFeed}`);
|
|
|
+ const currentThread = lock.threads[currentFeed];
|
|
|
|
|
|
- const updateDate = () => currentThread.updatedAt = new Date().toString();
|
|
|
- if (!fleetFeed || fleetFeed.fleet_threads.length === 0) { updateDate(); return; }
|
|
|
+ const updateDate = () => currentThread.updatedAt = new Date().toString();
|
|
|
+ if (!fleetFeed || fleetFeed.fleet_threads.length === 0) { updateDate(); return; }
|
|
|
|
|
|
- let fleets = fleetFeed.fleet_threads[0].fleets;
|
|
|
- const bottomOfFeed = fleets.slice(-1)[0].fleet_id.substring(3);
|
|
|
- const updateOffset = () => currentThread.offset = bottomOfFeed;
|
|
|
+ let fleets = fleetFeed.fleet_threads[0].fleets;
|
|
|
+ const bottomOfFeed = fleets.slice(-1)[0].fleet_id.substring(3);
|
|
|
+ const updateOffset = () => currentThread.offset = bottomOfFeed;
|
|
|
|
|
|
- if (currentThread.offset === '-1') { updateOffset(); return; }
|
|
|
- if (currentThread.offset !== '0') {
|
|
|
- const readCount = fleets.findIndex(fleet =>
|
|
|
- Number(BigNumOps.plus(fleet.fleet_id.substring(3), `-${currentThread.offset}`)) > 0);
|
|
|
- if (readCount === -1) return;
|
|
|
- fleets = fleets.slice(readCount);
|
|
|
- }
|
|
|
+ if (currentThread.offset === '-1') { updateOffset(); return; }
|
|
|
+ if (currentThread.offset !== '0') {
|
|
|
+ const readCount = fleets.findIndex(fleet =>
|
|
|
+ Number(BigNumOps.plus(fleet.fleet_id.substring(3), `-${currentThread.offset}`)) > 0);
|
|
|
+ if (readCount === -1) return;
|
|
|
+ fleets = fleets.slice(readCount);
|
|
|
+ }
|
|
|
|
|
|
- return this.workOnFleets(user, fleets, this.sendFleets(`thread ${currentFeed}`, ...currentThread.subscribers))
|
|
|
- .then(updateDate).then(updateOffset);
|
|
|
- })
|
|
|
- .then(() => {
|
|
|
- lock.workon++;
|
|
|
- let timeout = this.workInterval * 1000 / lock.feed.length;
|
|
|
- if (timeout < 1000) timeout = 1000;
|
|
|
- fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
|
|
|
- setTimeout(() => {
|
|
|
- this.work();
|
|
|
- }, timeout);
|
|
|
- });
|
|
|
- }
|
|
|
+ return this.workOnFleets(user, fleets, this.sendFleets(`thread ${currentFeed}`, ...currentThread.subscribers))
|
|
|
+ .then(updateDate).then(updateOffset);
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ lock.workon++;
|
|
|
+ let timeout = this.workInterval * 1000 / lock.feed.length;
|
|
|
+ if (timeout < 1000) timeout = 1000;
|
|
|
+ fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
|
|
|
+ setTimeout(() => {
|
|
|
+ this.work();
|
|
|
+ }, timeout);
|
|
|
+ });
|
|
|
+ };
|
|
|
}
|