|
@@ -8,7 +8,7 @@ import { promisify } from 'util';
|
|
|
import {
|
|
|
IgApiClient,
|
|
|
IgClientError, IgCookieNotFoundError, IgExactUserNotFoundError, IgLoginTwoFactorRequiredError, IgLoginRequiredError, IgNetworkError,
|
|
|
- ReelsMediaFeedResponseItem, UserFeedResponseUser
|
|
|
+ ReelsMediaFeedResponseItem
|
|
|
} from 'instagram-private-api';
|
|
|
import { RequestError } from 'request-promise/errors';
|
|
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
|
@@ -46,6 +46,8 @@ interface IWorkerOption {
|
|
|
proxyUrl: string;
|
|
|
lock: ILock;
|
|
|
lockfile: string;
|
|
|
+ cache: ICache;
|
|
|
+ cachefile: string;
|
|
|
webshotCookiesLockfile: string;
|
|
|
bot: QQBot;
|
|
|
inactiveHours: string[];
|
|
@@ -210,11 +212,23 @@ const retryOnError = <T, U>(
|
|
|
doWork().then(resolve).catch(error => retry(error, 1));
|
|
|
});
|
|
|
|
|
|
+export interface ICache {
|
|
|
+ [userId: string]: {
|
|
|
+ user: ReelsMediaFeedResponseItem['user'];
|
|
|
+ stories: {
|
|
|
+ [storyId: string]: CachedMediaItem;
|
|
|
+ };
|
|
|
+ pullOrder: number; // one-based; -1: subscribed, awaiting shuffle; 0: not subscribed
|
|
|
+ updated?: string; // Date.toString()
|
|
|
+ };
|
|
|
+}
|
|
|
export default class {
|
|
|
|
|
|
private client: IgApiClient;
|
|
|
private lock: ILock;
|
|
|
private lockfile: string;
|
|
|
+ private cache: ICache;
|
|
|
+ private cachefile: string;
|
|
|
private inactiveHours: string[];
|
|
|
private workInterval: number;
|
|
|
private bot: QQBot;
|
|
@@ -245,6 +259,8 @@ export default class {
|
|
|
this.session = new SessionManager(this.client, opt.sessionLockfile, opt.credentials, opt.codeServicePort);
|
|
|
this.lockfile = opt.lockfile;
|
|
|
this.lock = opt.lock;
|
|
|
+ this.cachefile = opt.cachefile;
|
|
|
+ this.cache = opt.cache;
|
|
|
this.inactiveHours = opt.inactiveHours;
|
|
|
this.workInterval = opt.workInterval;
|
|
|
this.bot = opt.bot;
|
|
@@ -262,7 +278,7 @@ export default class {
|
|
|
return this.queryUser(rawUserName)
|
|
|
.then(userNameId => {
|
|
|
const userId = userNameId.split(':')[1];
|
|
|
- if (Date.now() - this.cache[userId]?.updated?.getTime() > this.workInterval * 1000 &&
|
|
|
+ if (Date.now() - new Date(this.cache[userId]?.updated ?? Date()).getTime() > this.workInterval * 1000 &&
|
|
|
Object.keys(this.cache[userId].stories).length > 0) {
|
|
|
return userId;
|
|
|
}
|
|
@@ -270,12 +286,12 @@ export default class {
|
|
|
.then(storyItems => Promise.all(storyItems
|
|
|
.filter(item => !(item.pk in this.cache[userId].stories))
|
|
|
.map(item => this.webshot(
|
|
|
- [{...item, user: this.cache[userId].user}],
|
|
|
+ [{...item, user: this.cache[userId].user}], // guaranteed fresh cache entry
|
|
|
(msgs: string, text: string, author: string) =>
|
|
|
this.cache[userId].stories[item.pk] = {pk: item.pk, msgs, text, author, original: item},
|
|
|
this.webshotDelay
|
|
|
))
|
|
|
- ).then(() => userId).finally(() => this.cache[userId].updated = new Date()));
|
|
|
+ ).then(() => userId).finally(() => this.cache[userId].updated = Date()));
|
|
|
})
|
|
|
.then(action)
|
|
|
.catch((error: IgClientError & Partial<RequestError>) => {
|
|
@@ -383,10 +399,11 @@ export default class {
|
|
|
return this.session.login().then(() => this.client.user.searchExact(username));
|
|
|
} else throw error;
|
|
|
})
|
|
|
- .then(user => {
|
|
|
- logger.info(`initialized cache item for user ${user.full_name} (@${username})`);
|
|
|
- this.cache[user.pk] = {user, stories: {}, pullOrder: 0};
|
|
|
- return `${user.username}:${user.pk}`;
|
|
|
+ .then(({pk, username, full_name}) => {
|
|
|
+ this.cache[pk] = {user: {pk, username, full_name}, stories: {}, pullOrder: 0};
|
|
|
+ fs.writeFileSync(path.resolve(this.cachefile), JSON.stringify(this.cache));
|
|
|
+ logger.info(`initialized cache item for user ${full_name} (@${username})`);
|
|
|
+ return `${username}:${pk}`;
|
|
|
});
|
|
|
};
|
|
|
|
|
@@ -411,15 +428,6 @@ export default class {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
- private cache: {
|
|
|
- [userId: string]: {
|
|
|
- user: UserFeedResponseUser & ReelsMediaFeedResponseItem['user'],
|
|
|
- stories: {[storyId: string]: CachedMediaItem},
|
|
|
- pullOrder: number, // one-based; -1: subscribed, awaiting shuffle; 0: not subscribed
|
|
|
- updated?: Date,
|
|
|
- },
|
|
|
- } = {};
|
|
|
-
|
|
|
private get pullOrders() {
|
|
|
const arr: number[] = [];
|
|
|
Object.values(this.cache).forEach(item => { if (item.pullOrder > 0) arr[item.pullOrder - 1] = item.user.pk; });
|
|
@@ -443,9 +451,10 @@ export default class {
|
|
|
return Promise.resolve();
|
|
|
}
|
|
|
return promisify(setTimeout)((Math.random() * 2 + 1) * 5000).then(() =>
|
|
|
- this.client.user.info(id).then(user => {
|
|
|
- logger.info(`initialized cache item for user ${user.full_name} (@${user.username})`);
|
|
|
- this.cache[id] = {user, stories: {}, pullOrder: -1};
|
|
|
+ this.client.user.info(id).then(({pk, username, full_name}) => {
|
|
|
+ this.cache[id] = {user: {pk, username, full_name}, stories: {}, pullOrder: -1};
|
|
|
+ fs.writeFileSync(path.resolve(this.cachefile), JSON.stringify(this.cache));
|
|
|
+ logger.info(`initialized cache item for user ${full_name} (@${username})`);
|
|
|
})
|
|
|
);
|
|
|
}))
|
|
@@ -456,21 +465,32 @@ export default class {
|
|
|
return chainPromises(
|
|
|
Arr.chunk(userIdCache, 20).map(userIds => () => {
|
|
|
logger.info(`pulling stories from users:${userIds.map(id => ` @${this.cache[id].user.username}`)}`);
|
|
|
- return this.client.feed.reelsMedia({userIds}).items()
|
|
|
- .then(storyItems => Promise.all(storyItems
|
|
|
- .filter(item => !(item.pk in this.cache[item.user.pk].stories))
|
|
|
- .map(item => this.webshot(
|
|
|
- [{...item, user: this.cache[item.user.pk].user}],
|
|
|
- (msgs: string, text: string, author: string) =>
|
|
|
- this.cache[item.user.pk].stories[item.pk] = {pk: item.pk, msgs, text, author, original: item},
|
|
|
- this.webshotDelay
|
|
|
- ))
|
|
|
+ return this.client.feed.reelsMedia({userIds}).request()
|
|
|
+ .then(({reels}) => chainPromises(
|
|
|
+ Object.keys(reels).map(userId => () =>
|
|
|
+ this.client.user.info(userId).then(({pk, username, full_name}) => {
|
|
|
+ const cacheItem = this.cache[pk];
|
|
|
+ cacheItem.user = {pk, username, full_name};
|
|
|
+ return Promise.all(reels[userId].items
|
|
|
+ .filter(item => !(item.pk in cacheItem.stories))
|
|
|
+ .map(item => this.webshot(
|
|
|
+ [{...item, user: cacheItem.user}],
|
|
|
+ (msgs: string, text: string, author: string) =>
|
|
|
+ cacheItem.stories[item.pk] = {pk: item.pk, msgs, text, author, original: item},
|
|
|
+ this.webshotDelay
|
|
|
+ ))
|
|
|
+ );
|
|
|
+ })
|
|
|
+ )
|
|
|
))
|
|
|
- .finally(() => Object.values(this.lock.threads).forEach(thread => {
|
|
|
- if (userIds.includes(thread.id)) {
|
|
|
- thread.updatedAt = (this.cache[thread.id].updated = new Date()).toString();
|
|
|
- }
|
|
|
- })) as unknown as Promise<void>;
|
|
|
+ .finally(() => {
|
|
|
+ fs.writeFileSync(path.resolve(this.cachefile), JSON.stringify(this.cache));
|
|
|
+ Object.values(this.lock.threads).forEach(thread => {
|
|
|
+ if (userIds.includes(thread.id)) {
|
|
|
+ thread.updatedAt = this.cache[thread.id].updated = Date();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }) as unknown as Promise<void>;
|
|
|
}),
|
|
|
(lp1, lp2) => () => lp1().then(() => promisify(setTimeout)(this.workInterval * 1000 / this.lock.feed.length).then(lp2))
|
|
|
);
|