Prechádzať zdrojové kódy

precache assets, cap push to 5/10 items, support view range

Mike L 3 rokov pred
rodič
commit
1a71ce45ec
6 zmenil súbory, kde vykonal 162 pridanie a 85 odobranie
  1. 25 9
      dist/command.js
  2. 2 2
      dist/koishi.js
  3. 45 28
      dist/twitter.js
  4. 25 9
      src/command.ts
  5. 2 2
      src/koishi.ts
  6. 63 35
      src/twitter.ts

+ 25 - 9
dist/command.js

@@ -40,7 +40,7 @@ function sub(chat, args, reply, lock, lockfile) {
         return reply('请先添加机器人为好友。');
     }
     if (args.length === 0) {
-        return reply('找不到要订阅 Instagram 故事的链接。');
+        return reply('找不到要订阅 Instagram 限时动态的链接。');
     }
     const matched = twitter_1.parseLink(args[0]);
     if (!matched) {
@@ -49,7 +49,7 @@ function sub(chat, args, reply, lock, lockfile) {
 https://www.instagram.com/tomoyo_kurosawa_/`);
     }
     const subscribeTo = (link, config = {}) => {
-        const { id, msg = `已为此聊天订阅 ${link} 的 Instagram 故事` } = config;
+        const { id, msg = `已为此聊天订阅 ${link} 的 Instagram 限时动态` } = config;
         if (id) {
             lock.feed.push(link);
             lock.threads[link] = {
@@ -67,7 +67,7 @@ https://www.instagram.com/tomoyo_kurosawa_/`);
     const tryFindSub = (userName) => {
         const [realLink, index] = linkFinder(userName, chat, lock);
         if (index > -1) {
-            reply('此聊天已订阅此链接的 Instagram 故事。');
+            reply('此聊天已订阅此链接的 Instagram 限时动态。');
             return true;
         }
         if (realLink) {
@@ -96,7 +96,7 @@ function unsub(chat, args, reply, lock, lockfile) {
         return reply('请先添加机器人为好友。');
     }
     if (args.length === 0) {
-        return reply('找不到要退订 Instagram 故事的链接。');
+        return reply('找不到要退订 Instagram 限时动态的链接。');
     }
     const match = (_a = twitter_1.parseLink(args[0])) === null || _a === void 0 ? void 0 : _a.userName;
     if (!match) {
@@ -104,12 +104,12 @@ function unsub(chat, args, reply, lock, lockfile) {
     }
     const [link, index] = linkFinder(match, chat, lock);
     if (index === -1)
-        return list(chat, args, msg => reply('您没有订阅此链接的 Instagram 故事。\n' + msg), lock);
+        return list(chat, args, msg => reply('您没有订阅此链接的 Instagram 限时动态。\n' + msg), lock);
     else {
         lock.threads[link].subscribers.splice(index, 1);
         fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
         logger.warn(`chat ${JSON.stringify(chat)} has unsubscribed ${link}`);
-        return reply(`已为此聊天退订 ${link} 的 Instagram 故事`);
+        return reply(`已为此聊天退订 ${link} 的 Instagram 限时动态`);
     }
 }
 exports.unsub = unsub;
@@ -122,20 +122,36 @@ function list(chat, _, reply, lock) {
         if (lock.threads[key].subscribers.find(({ chatID, chatType }) => chat.chatID === chatID && chat.chatType === chatType))
             links.push(`${key} ${datetime_1.relativeDate(lock.threads[key].updatedAt)}`);
     });
-    return reply('此聊天中订阅的 Instagram 故事动态链接:\n' + links.join('\n'));
+    return reply('此聊天中订阅的 Instagram 限时动态动态链接:\n' + links.join('\n'));
 }
 exports.list = list;
 function view(chat, args, reply) {
     var _a;
     if (args.length === 0) {
-        return reply('找不到要查看 Instagram 故事的链接。');
+        return reply('找不到要查看 Instagram 限时动态的链接。');
     }
     const match = (_a = twitter_1.parseLink(args[0])) === null || _a === void 0 ? void 0 : _a.userName;
     if (!match) {
         return reply('链接格式有误。');
     }
+    const conf = {};
+    const confZH = {
+        count: '最大查看数量',
+        skip: '跳过数量',
+    };
+    for (const arg of args.slice(1)) {
+        const optMatch = /^(count|skip)=(.*)/.exec(arg);
+        if (!optMatch)
+            return reply(`未定义的查看参数:${arg}。`);
+        const optKey = optMatch[1];
+        if (optMatch.length === 1 || !/^\d*$/.test(optMatch[2]))
+            return reply(`${confZH[optKey]}参数应为数值。`);
+        if (optMatch[2] === '')
+            return reply(`${confZH[optKey]}参数值不可为空。`);
+        conf[optKey] = Number(optMatch[2]);
+    }
     try {
-        twitter_1.sendAllStories(match, chat);
+        twitter_1.sendAllStories(match, chat, conf.skip, conf.count);
     }
     catch (e) {
         reply('机器人尚未加载完毕,请稍后重试。');

+ 2 - 2
dist/koishi.js

@@ -195,10 +195,10 @@ class default_1 {
                         break;
                     case 'help':
                         if (cmdObj.args.length === 0) {
-                            reply(`Instagram 故事搬运机器人:
+                            reply(`Instagram 限时动态搬运机器人:
 /igstory - 查询当前聊天中的 Instagram Stories 动态订阅
 /igstory_sub[scribe]〈链接|用户名〉- 订阅 Instagram Stories 搬运
-/igstory_unsub[scribe]〈链接|用户名〉- 退订 Instagram Stories 媒体搬运
+/igstory_unsub[scribe]〈链接|用户名〉- 退订 Instagram Stories 搬运
 /igstory_view〈链接|用户名〉- 查看该用户所有 Stories\
 ${chat.chatType === "temp" ?
                                 '\n(当前游客模式下无法使用订阅功能,请先添加本账号为好友。)' : ''}`);

+ 45 - 28
dist/twitter.js

@@ -140,7 +140,7 @@ class ScreenNameNormalizer {
 }
 exports.ScreenNameNormalizer = ScreenNameNormalizer;
 ScreenNameNormalizer.normalize = (username) => `${username.toLowerCase().replace(/^@/, '')}:`;
-let sendAllStories = (segmentId, receiver) => {
+let sendAllStories = (segmentId, receiver, startIndex, count) => {
     throw Error();
 };
 exports.sendAllStories = sendAllStories;
@@ -194,7 +194,7 @@ class default_1 {
                 return `${user.username}:${user.pk}`;
             });
         };
-        this.workOnMedia = (mediaItems, sendMedia) => this.webshot(mediaItems, sendMedia, this.webshotDelay);
+        this.workOnMedia = (mediaItems, sendMedia) => Promise.resolve(mediaItems.forEach(({ msgs, text, author }) => sendMedia(msgs, text, author)));
         this.sendStories = (source, ...to) => (msg, text, author) => {
             to.forEach(subscriber => {
                 logger.info(`pushing data${source ? ` of ${source}` : ''} to ${JSON.stringify(subscriber)}`);
@@ -237,13 +237,12 @@ class default_1 {
                     this.pullOrders = utils_1.Arr.shuffle(Object.keys(idToUserMap)).map(Number) :
                     this.pullOrders;
                 return utils_1.chainPromises(utils_1.Arr.chunk(userIdCache, 20).map(userIds => () => {
+                    const itemToUserName = (item) => 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([Object.assign(Object.assign({}, item), { user: this.cache[itemToUserName(item)].user })], (msgs, text, author) => 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();
@@ -269,7 +268,10 @@ class default_1 {
             if (this.workInterval < 1)
                 this.workInterval = 1;
             if (this.isInactiveTime || lock.feed.length === 0) {
-                setTimeout(this.work, this.workInterval * 1000);
+                setTimeout(() => {
+                    this.workForAll();
+                    setTimeout(this.work, this.workInterval * 200);
+                }, this.workInterval * 1000);
                 return;
             }
             if (lock.workon >= lock.feed.length)
@@ -302,13 +304,14 @@ class default_1 {
             const newer = (item) => utils_1.BigNumOps.compare(item.pk, lock.threads[currentFeed].offset) > 0;
             const promise = Promise.resolve(Object.values(cachedFeed.stories)
                 .filter(newer)
-                .map(story => (Object.assign(Object.assign({}, story), { user: cachedFeed.user })))
-                .sort((i1, i2) => utils_1.BigNumOps.compare(i2.pk, i1.pk)));
+                .sort((i1, i2) => utils_1.BigNumOps.compare(i2.pk, i1.pk))
+                .slice(-5));
             promise.then((mediaItems) => {
                 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();
@@ -316,7 +319,15 @@ class default_1 {
                 }
                 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(() => {
@@ -357,7 +368,11 @@ class default_1 {
         this.mode = opt.mode;
         this.wsUrl = opt.wsUrl;
         ScreenNameNormalizer._queryUser = this.queryUser;
-        exports.sendAllStories = (rawUserName, receiver) => {
+        exports.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 => {
@@ -365,25 +380,27 @@ class default_1 {
                 const [userName, userId] = userNameId.split(':');
                 if (Date.now() - ((_b = (_a = this.cache[userName]) === null || _a === void 0 ? void 0 : _a.updated) === null || _b === void 0 ? void 0 : _b.getTime()) > this.workInterval * 10000 &&
                     Object.keys(this.cache[userName].stories).length > 0) {
-                    return Promise.resolve(Object.values(this.cache[userName].stories)
-                        .map(story => (Object.assign(Object.assign({}, story), { user: this.cache[userName].user })))
-                        .sort((i1, i2) => utils_1.BigNumOps.compare(i2.pk, i1.pk)));
+                    return userName;
                 }
                 return this.client.feed.reelsMedia({ userIds: [userId] }).items()
-                    .then(storyItems => {
-                    storyItems = storyItems.map(story => (Object.assign(Object.assign({}, story), { user: this.cache[userName].user })));
-                    storyItems.forEach(item => {
-                        if (!(item.pk in this.cache[userName].stories)) {
-                            this.cache[userName].stories[item.pk] = item;
-                        }
-                    });
+                    .then(storyItems => Promise.all(storyItems
+                    .filter(item => !(item.pk in this.cache[userName].stories))
+                    .map(item => this.webshot([Object.assign(Object.assign({}, item), { user: this.cache[userName].user })], (msgs, text, author) => 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}) 没有可用的推特故事。`);
-                    return storyItems;
-                });
+                        this.bot.sendTo(receiver, `当前用户 (@${userName}) 没有可用的 Instagram 限时动态。`);
+                }));
+            })
+                .then(userName => {
+                const storyItems = Object.values(this.cache[userName].stories)
+                    .sort((i1, i2) => -utils_1.BigNumOps.compare(i2.pk, i1.pk));
+                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) => {
                 if (error instanceof instagram_private_api_1.IgNetworkError) {
                     logger.warn(`error while fetching stories for ${rawUserName}: ${JSON.stringify(error.cause)}`);
@@ -391,7 +408,7 @@ class default_1 {
                 }
                 else if (error instanceof instagram_private_api_1.IgLoginRequiredError) {
                     logger.warn('login required, logging in again...');
-                    this.session.login().then(() => exports.sendAllStories(rawUserName, receiver));
+                    this.session.login().then(() => exports.sendAllStories(rawUserName, receiver, startIndex, count));
                 }
                 else {
                     logger.error(`unhandled error while fetching stories for ${rawUserName}: ${error}`);

+ 25 - 9
src/command.ts

@@ -55,7 +55,7 @@ function sub(chat: IChat, args: string[], reply: (msg: string) => any,
     return reply('请先添加机器人为好友。');
   }
   if (args.length === 0) {
-    return reply('找不到要订阅 Instagram 故事的链接。');
+    return reply('找不到要订阅 Instagram 限时动态的链接。');
   }
   const matched = parseLink(args[0]);
   if (!matched) {
@@ -64,7 +64,7 @@ function sub(chat: IChat, args: string[], reply: (msg: string) => any,
 https://www.instagram.com/tomoyo_kurosawa_/`);
   }
   const subscribeTo = (link: string, config: {id?: number, msg?: string} = {}) => {
-    const {id, msg = `已为此聊天订阅 ${link} 的 Instagram 故事`} = config;
+    const {id, msg = `已为此聊天订阅 ${link} 的 Instagram 限时动态`} = config;
     if (id) {
       lock.feed.push(link);
       lock.threads[link] = {
@@ -81,7 +81,7 @@ https://www.instagram.com/tomoyo_kurosawa_/`);
   };
   const tryFindSub = (userName: string) => {
     const [realLink, index] = linkFinder(userName, chat, lock);
-    if (index > -1) { reply('此聊天已订阅此链接的 Instagram 故事。'); return true; }
+    if (index > -1) { reply('此聊天已订阅此链接的 Instagram 限时动态。'); return true; }
     if (realLink) { subscribeTo(realLink); return true; }
     return false;
   };
@@ -104,19 +104,19 @@ function unsub(chat: IChat, args: string[], reply: (msg: string) => any,
     return reply('请先添加机器人为好友。');
   }
   if (args.length === 0) {
-    return reply('找不到要退订 Instagram 故事的链接。');
+    return reply('找不到要退订 Instagram 限时动态的链接。');
   }
   const match = parseLink(args[0])?.userName;
   if (!match) {
     return reply('链接格式有误。');
   }
   const [link, index] = linkFinder(match, chat, lock);
-  if (index === -1) return list(chat, args, msg => reply('您没有订阅此链接的 Instagram 故事。\n' + msg), lock);
+  if (index === -1) return list(chat, args, msg => reply('您没有订阅此链接的 Instagram 限时动态。\n' + msg), lock);
   else {
     lock.threads[link].subscribers.splice(index, 1);
     fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
     logger.warn(`chat ${JSON.stringify(chat)} has unsubscribed ${link}`);
-    return reply(`已为此聊天退订 ${link} 的 Instagram 故事`);
+    return reply(`已为此聊天退订 ${link} 的 Instagram 限时动态`);
   }
 }
 
@@ -130,19 +130,35 @@ function list(chat: IChat, _: string[], reply: (msg: string) => any, lock: ILock
       chat.chatID === chatID && chat.chatType === chatType
     )) links.push(`${key} ${relativeDate(lock.threads[key].updatedAt)}`);
   });
-  return reply('此聊天中订阅的 Instagram 故事动态链接:\n' + links.join('\n'));
+  return reply('此聊天中订阅的 Instagram 限时动态动态链接:\n' + links.join('\n'));
 }
 
 function view(chat: IChat, args: string[], reply: (msg: string) => any): void {
   if (args.length === 0) {
-    return reply('找不到要查看 Instagram 故事的链接。');
+    return reply('找不到要查看 Instagram 限时动态的链接。');
   }
   const match = parseLink(args[0])?.userName;
   if (!match) {
     return reply('链接格式有误。');
   }
+  const conf: {
+    skip?: number,
+    count?: number,
+  } = {};
+  const confZH: Record<keyof typeof conf, string> = {
+    count: '最大查看数量',
+    skip: '跳过数量',
+  };
+  for (const arg of args.slice(1)) {
+    const optMatch = /^(count|skip)=(.*)/.exec(arg);
+    if (!optMatch) return reply(`未定义的查看参数:${arg}。`);
+    const optKey = optMatch[1] as keyof typeof confZH;
+    if (optMatch.length === 1 || !/^\d*$/.test(optMatch[2])) return reply(`${confZH[optKey]}参数应为数值。`);
+    if (optMatch[2] === '') return reply(`${confZH[optKey]}参数值不可为空。`);
+    conf[optKey] = Number(optMatch[2]);
+  }
   try {
-    sendAllStories(match, chat);
+    sendAllStories(match, chat, conf.skip, conf.count);
   } catch (e) {
     reply('机器人尚未加载完毕,请稍后重试。');
   }

+ 2 - 2
src/koishi.ts

@@ -214,10 +214,10 @@ export default class {
           break;
         case 'help':
           if (cmdObj.args.length === 0) {
-            reply(`Instagram 故事搬运机器人:
+            reply(`Instagram 限时动态搬运机器人:
 /igstory - 查询当前聊天中的 Instagram Stories 动态订阅
 /igstory_sub[scribe]〈链接|用户名〉- 订阅 Instagram Stories 搬运
-/igstory_unsub[scribe]〈链接|用户名〉- 退订 Instagram Stories 媒体搬运
+/igstory_unsub[scribe]〈链接|用户名〉- 退订 Instagram Stories 搬运
 /igstory_view〈链接|用户名〉- 查看该用户所有 Stories\
 ${chat.chatType === ChatType.Temp ?
     '\n(当前游客模式下无法使用订阅功能,请先添加本账号为好友。)' : ''

+ 63 - 35
src/twitter.ts

@@ -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(() => {