|
@@ -164,11 +164,12 @@ export class ScreenNameNormalizer {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export let sendAllStories = (segmentId: string, receiver: IChat): void => {
|
|
|
+export let sendAllStories = (segmentId: string, receiver: IChat, startIndex: number, count: number): void => {
|
|
|
throw Error();
|
|
|
};
|
|
|
|
|
|
export type MediaItem = ReelsMediaFeedResponseItem;
|
|
|
+type CachedMediaItem = {pk: string, msgs: string, text: string, author: string, original: MediaItem};
|
|
|
|
|
|
const logger = getLogger('instagram');
|
|
|
const maxTrials = 3;
|
|
@@ -242,40 +243,48 @@ export default class {
|
|
|
this.wsUrl = opt.wsUrl;
|
|
|
|
|
|
ScreenNameNormalizer._queryUser = this.queryUser;
|
|
|
- sendAllStories = (rawUserName, receiver) => {
|
|
|
+ sendAllStories = (rawUserName, receiver, startIndex = 0, count = 10) => {
|
|
|
+ if (startIndex < 0) return this.bot.sendTo(receiver, '跳过数量参数值应为非负整数。');
|
|
|
+ if (count < 1) return this.bot.sendTo(receiver, '最大查看数量参数值应为正整数。');
|
|
|
const sender = this.sendStories(`instagram stories for ${rawUserName}`, receiver);
|
|
|
this.queryUser(rawUserName)
|
|
|
.then(userNameId => {
|
|
|
const [userName, userId] = userNameId.split(':');
|
|
|
if (Date.now() - this.cache[userName]?.updated?.getTime() > this.workInterval * 10000 &&
|
|
|
Object.keys(this.cache[userName].stories).length > 0) {
|
|
|
- return Promise.resolve(
|
|
|
- Object.values(this.cache[userName].stories)
|
|
|
- .map(story => ({...story, user: this.cache[userName].user}))
|
|
|
- .sort((i1, i2) => BigNumOps.compare(i2.pk, i1.pk))
|
|
|
- );
|
|
|
+ return userName;
|
|
|
}
|
|
|
return this.client.feed.reelsMedia({userIds: [userId]}).items()
|
|
|
- .then(storyItems => {
|
|
|
- storyItems = storyItems.map(story => ({...story, user: this.cache[userName].user}));
|
|
|
- storyItems.forEach(item => {
|
|
|
- if (!(item.pk in this.cache[userName].stories)) {
|
|
|
- this.cache[userName].stories[item.pk] = item;
|
|
|
- }
|
|
|
- });
|
|
|
- this.cache[userName].updated = new Date();
|
|
|
- if (storyItems.length === 0) this.bot.sendTo(receiver, `当前用户 (@${userName}) 没有可用的推特故事。`);
|
|
|
- return storyItems;
|
|
|
- });
|
|
|
+ .then(storyItems => Promise.all(storyItems
|
|
|
+ .filter(item => !(item.pk in this.cache[userName].stories))
|
|
|
+ .map(item => this.webshot(
|
|
|
+ [{...item, user: this.cache[userName].user}],
|
|
|
+ (msgs: string, text: string, author: string) =>
|
|
|
+ this.cache[userName].stories[item.pk] = {pk: item.pk, msgs, text, author, original: item},
|
|
|
+ this.webshotDelay
|
|
|
+ ))
|
|
|
+ ).then(() => userName)
|
|
|
+ .finally(() => {
|
|
|
+ this.cache[userName].updated = new Date();
|
|
|
+ if (storyItems.length === 0) this.bot.sendTo(receiver, `当前用户 (@${userName}) 没有可用的 Instagram 限时动态。`);
|
|
|
+ })
|
|
|
+ );
|
|
|
+ })
|
|
|
+ .then(userName => {
|
|
|
+ const storyItems = Object.values(this.cache[userName].stories)
|
|
|
+ .sort((i1, i2) => -BigNumOps.compare(i2.pk, i1.pk)); // ascending!
|
|
|
+ if (startIndex + 1 > storyItems.length) return this.bot.sendTo(receiver, '跳过数量到达或超过当前用户可用的限时动态数量。');
|
|
|
+ const sendRangeText = `${startIndex + 1}${count > 1 ? `-${Math.min(storyItems.length, startIndex + count)}` : ''}`;
|
|
|
+ return this.workOnMedia(storyItems.slice(startIndex, startIndex + count), sender)
|
|
|
+ .then(() => this.bot.sendTo(receiver, `已显示当前用户 ${storyItems.length} 条可用限时动态中的第 ${sendRangeText} 条。`));
|
|
|
})
|
|
|
- .then(storyItems => this.workOnMedia(storyItems, sender))
|
|
|
.catch((error: IgClientError & Partial<RequestError>) => {
|
|
|
if (error instanceof IgNetworkError) {
|
|
|
logger.warn(`error while fetching stories for ${rawUserName}: ${JSON.stringify(error.cause)}`);
|
|
|
this.bot.sendTo(receiver, `获取 Stories 时出现错误:原因: ${error.cause}`);
|
|
|
} else if (error instanceof IgLoginRequiredError) {
|
|
|
logger.warn('login required, logging in again...');
|
|
|
- this.session.login().then(() => sendAllStories(rawUserName, receiver));
|
|
|
+ this.session.login().then(() => sendAllStories(rawUserName, receiver, startIndex, count));
|
|
|
} else {
|
|
|
logger.error(`unhandled error while fetching stories for ${rawUserName}: ${error}`);
|
|
|
this.bot.sendTo(receiver, `获取 Stories 时发生未知错误: ${error}`);
|
|
@@ -313,9 +322,9 @@ export default class {
|
|
|
};
|
|
|
|
|
|
private workOnMedia = (
|
|
|
- mediaItems: MediaItem[],
|
|
|
+ mediaItems: CachedMediaItem[],
|
|
|
sendMedia: (msg: string, text: string, author: string) => void
|
|
|
- ) => this.webshot(mediaItems, sendMedia, this.webshotDelay);
|
|
|
+ ) => Promise.resolve(mediaItems.forEach(({msgs, text, author}) => sendMedia(msgs, text, author)));
|
|
|
|
|
|
private sendStories = (source?: string, ...to: IChat[]) => (msg: string, text: string, author: string) => {
|
|
|
to.forEach(subscriber => {
|
|
@@ -336,7 +345,7 @@ export default class {
|
|
|
private cache: {
|
|
|
[userName: string]: {
|
|
|
user: UserFeedResponseUser & ReelsMediaFeedResponseItem['user'],
|
|
|
- stories: {[storyId: string]: MediaItem},
|
|
|
+ stories: {[storyId: string]: CachedMediaItem},
|
|
|
pullOrder: number, // one-based; -1: subscribed, awaiting shuffle; 0: not subscribed
|
|
|
updated?: Date,
|
|
|
},
|
|
@@ -380,18 +389,23 @@ export default class {
|
|
|
this.pullOrders;
|
|
|
return chainPromises(
|
|
|
Arr.chunk(userIdCache, 20).map(userIds => () => {
|
|
|
+ const itemToUserName = (item: MediaItem) => idToUserMap[item.user.pk].username;
|
|
|
logger.info(`pulling stories from users:${userIds.map(id => ` @${idToUserMap[id].username}`)}`);
|
|
|
return this.client.feed.reelsMedia({userIds}).items()
|
|
|
- .then(storyItems => storyItems.forEach(item => {
|
|
|
- if (!(item.pk in this.cache[idToUserMap[item.user.pk].username].stories)) {
|
|
|
- this.cache[idToUserMap[item.user.pk].username].stories[item.pk] = item;
|
|
|
- }
|
|
|
- }))
|
|
|
+ .then(storyItems => Promise.all(storyItems
|
|
|
+ .filter(item => !(item.pk in this.cache[itemToUserName(item)].stories))
|
|
|
+ .map(item => this.webshot(
|
|
|
+ [{...item, user: this.cache[itemToUserName(item)].user}],
|
|
|
+ (msgs: string, text: string, author: string) =>
|
|
|
+ this.cache[itemToUserName(item)].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[idToUserMap[thread.id].username].updated = new Date()).toString();
|
|
|
}
|
|
|
- }));
|
|
|
+ })) as unknown as Promise<void>;
|
|
|
}),
|
|
|
(lp1, lp2) => () => lp1().then(() => promisify(setTimeout)(this.workInterval * 1000).then(lp2))
|
|
|
);
|
|
@@ -421,7 +435,11 @@ export default class {
|
|
|
const lock = this.lock;
|
|
|
if (this.workInterval < 1) this.workInterval = 1;
|
|
|
if (this.isInactiveTime || lock.feed.length === 0) {
|
|
|
- setTimeout(this.work, this.workInterval * 1000); return;
|
|
|
+ setTimeout(() => {
|
|
|
+ this.workForAll();
|
|
|
+ setTimeout(this.work, this.workInterval * 200);
|
|
|
+ }, this.workInterval * 1000);
|
|
|
+ return;
|
|
|
}
|
|
|
if (lock.workon >= lock.feed.length) lock.workon = 0;
|
|
|
|
|
@@ -449,25 +467,35 @@ export default class {
|
|
|
if (!cachedFeed) {
|
|
|
setTimeout(this.work, this.workInterval * 1000); return;
|
|
|
}
|
|
|
- const newer = (item: MediaItem) => BigNumOps.compare(item.pk, lock.threads[currentFeed].offset) > 0;
|
|
|
+ const newer = (item: CachedMediaItem) => BigNumOps.compare(item.pk, lock.threads[currentFeed].offset) > 0;
|
|
|
const promise = Promise.resolve(Object.values(cachedFeed.stories)
|
|
|
.filter(newer)
|
|
|
- .map(story => ({...story, user: cachedFeed.user}))
|
|
|
.sort((i1, i2) => BigNumOps.compare(i2.pk, i1.pk))
|
|
|
+ .slice(-5)
|
|
|
);
|
|
|
|
|
|
- promise.then((mediaItems: MediaItem[]) => {
|
|
|
+ promise.then((mediaItems: CachedMediaItem[]) => {
|
|
|
const currentThread = lock.threads[currentFeed];
|
|
|
|
|
|
if (!mediaItems || mediaItems.length === 0) return;
|
|
|
|
|
|
- const topOfFeed = mediaItems[0].pk;
|
|
|
+ const question = mediaItems.find(story => story.original.story_questions);
|
|
|
+ const topOfFeed = question? question.pk : mediaItems[0].pk;
|
|
|
const updateOffset = () => currentThread.offset = topOfFeed;
|
|
|
|
|
|
if (currentThread.offset === '-1') { updateOffset(); return; }
|
|
|
if (currentThread.offset === '0') mediaItems.splice(1);
|
|
|
|
|
|
- return this.workOnMedia(mediaItems, this.sendStories(`thread ${currentFeed}`, ...currentThread.subscribers))
|
|
|
+ if (question) {
|
|
|
+ currentThread.subscribers.forEach(subscriber => {
|
|
|
+ const username = cachedFeed.user.username;
|
|
|
+ const author = `${cachedFeed.user.full_name} (@${username}) `;
|
|
|
+ this.bot.sendTo(subscriber, `请注意,用户${author}已开启问答互动。本次推送已在此条动态后暂停。需退订请回复:/igstory_unsub ${username}\
|
|
|
+${Object.keys(cachedFeed.stories).some(id => id > topOfFeed) ? `\n下次推送在 ${this.workInterval * 1000 / lock.feed.length} 秒后。` : ''}`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.workOnMedia(mediaItems.reverse(), this.sendStories(`thread ${currentFeed}`, ...currentThread.subscribers))
|
|
|
.then(updateOffset);
|
|
|
})
|
|
|
.then(() => {
|