Browse Source

add view by query and reshoot last tweet of user

Mike L 3 years ago
parent
commit
2d91fe2d49
6 changed files with 107 additions and 38 deletions
  1. 25 8
      dist/command.js
  2. 5 1
      dist/koishi.js
  3. 20 9
      dist/twitter.js
  4. 25 8
      src/command.ts
  5. 6 2
      src/koishi.ts
  6. 26 10
      src/twitter.ts

+ 25 - 8
dist/command.js

@@ -1,6 +1,6 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.query = exports.view = exports.unsubAll = exports.unsub = exports.list = exports.sub = exports.parseCmd = void 0;
+exports.query = exports.resendLast = exports.view = exports.unsubAll = exports.unsub = exports.list = exports.sub = exports.parseCmd = void 0;
 const fs = require("fs");
 const path = require("path");
 const datetime_1 = require("./datetime");
@@ -167,29 +167,46 @@ function list(chat, _, reply, lock) {
 }
 exports.list = list;
 function view(chat, args, reply) {
-    if (args.length === 0) {
-        return reply('找不到要查看的链接。');
+    if (args.length === 0 || !args[0]) {
+        return reply('找不到要查看的链接或表达式。');
     }
-    const match = /^(?:.*twitter.com\/[^\/?#]+\/status\/)?(\d+)/.exec(args[0]);
+    const match = /^(last(?:|-\d+)@[^\/?#]+)$/.exec(args[0]) ||
+        /^(?:.*twitter.com\/[^\/?#]+\/status\/)?(\d+)/.exec(args[0]);
     if (!match) {
-        return reply('链接格式有误。');
+        return reply(`链接或表达式格式有误。
+示例:
+https://twitter.com/TomoyoKurosawa/status/1294613494860361729
+last@TomoyoKurosawa
+last-2@sunflower930316,noreps=off,norts=on
+(表达式筛选参数详见 /help twitter_query)`);
+    }
+    let forceRefresh;
+    for (const arg of args.slice(1)) {
+        const optMatch = /^(force|refresh)=(.*)/.exec(arg);
+        if (!optMatch)
+            return reply(`未定义的查看参数:${arg}。`);
+        forceRefresh = { on: true, off: false }[optMatch[2]];
     }
     try {
-        twitter_1.sendTweet(match[1], chat);
+        twitter_1.sendTweet(match[1], chat, forceRefresh);
     }
     catch (e) {
         reply('推特机器人尚未加载完毕,请稍后重试。');
     }
 }
 exports.view = view;
+function resendLast(chat, args, reply) {
+    view(chat, [(args[0] || '').replace(/^@?(.+)$/, 'last@$1'), 'refresh=on'], reply);
+}
+exports.resendLast = resendLast;
 function query(chat, args, reply) {
-    if (args.length === 0) {
+    if (args.length === 0 || !args[0]) {
         return reply('找不到要查询的用户。');
     }
     const match = /twitter.com\/([^\/?#]+)/.exec(args[0]) ||
         /^([^\/?#]+)$/.exec(args[0]);
     if (!match) {
-        return reply('链接格式有误。');
+        return reply('链接或用户名格式有误。');
     }
     const conf = { username: match[1], noreps: 'on', norts: 'off' };
     const confZH = {

+ 5 - 1
dist/koishi.js

@@ -181,6 +181,9 @@ class default_1 {
                     case 'twitter_get':
                         command_1.view(chat, cmdObj.args, reply);
                         break;
+                    case 'twitter_resendlast':
+                        command_1.resendLast(chat, cmdObj.args, reply);
+                        break;
                     case 'twitter_query':
                     case 'twitter_gettimeline':
                         command_1.query(chat, cmdObj.args, reply);
@@ -207,7 +210,8 @@ class default_1 {
 /twitter - 查询当前聊天中的推文订阅
 /twitter_sub[scribe]〈链接|用户名〉- 订阅 Twitter 推文搬运
 /twitter_unsub[scribe]〈链接|用户名〉- 退订 Twitter 推文搬运
-/twitter_view〈链接〉- 查看推文
+/twitter_view〈链接|表达式〉[{force|refresh}={on|off}] - 查看推文(可选强制重新载入)
+/twitter_resendlast〈用户名〉- 强制重发该用户最后一条推文
 /twitter_query〈链接|用户名〉[参数列表...] - 查询时间线(详见 /help twitter_query)\
 ${chat.chatType === "temp" ?
                                 '\n(当前游客模式下无法使用订阅功能,请先添加本账号为好友。)' : ''}`);

+ 20 - 9
dist/twitter.js

@@ -36,7 +36,7 @@ class ScreenNameNormalizer {
 }
 exports.ScreenNameNormalizer = ScreenNameNormalizer;
 ScreenNameNormalizer.normalize = (username) => username.toLowerCase().replace(/^@/, '');
-let sendTweet = (id, receiver) => {
+let sendTweet = (id, receiver, forceRefresh) => {
     throw Error();
 };
 exports.sendTweet = sendTweet;
@@ -127,7 +127,7 @@ class default_1 {
             });
             return fetchTimeline();
         };
-        this.workOnTweets = (tweets, sendTweets) => Promise.all(tweets.map(tweet => (this.redis ? this.redis.getContent(`webshot/${tweet.id_str}`) : Promise.reject())
+        this.workOnTweets = (tweets, sendTweets, refresh = false) => Promise.all(tweets.map(tweet => ((this.redis && !refresh) ? this.redis.getContent(`webshot/${tweet.id_str}`) : Promise.reject())
             .then(content => {
             if (content === null)
                 throw Error();
@@ -144,7 +144,7 @@ class default_1 {
             })
                 .then(() => sendTweets(id, msg, text, author));
         }, this.webshotDelay))));
-        this.getTweet = (id, sender) => {
+        this.getTweet = (id, sender, refresh = false) => {
             const endpoint = 'statuses/show';
             const config = {
                 id,
@@ -153,12 +153,12 @@ class default_1 {
             return this.client.get(endpoint, config)
                 .then((tweet) => {
                 logger.debug(`api returned tweet ${JSON.stringify(tweet)} for query id=${id}`);
-                return this.workOnTweets([tweet], sender);
+                return this.workOnTweets([tweet], sender, refresh);
             });
         };
-        this.sendTweets = (config = { reportOnSkip: false }, ...to) => (id, msg, text, author) => {
+        this.sendTweets = (config = { reportOnSkip: false, force: false }, ...to) => (id, msg, text, author) => {
             to.forEach(subscriber => {
-                const { sourceInfo: source, reportOnSkip } = config;
+                const { sourceInfo: source, reportOnSkip, force } = config;
                 const targetStr = JSON.stringify(subscriber);
                 const send = () => retryOnError(() => this.bot.sendTo(subscriber, msg), (_, count, terminate) => {
                     if (count <= maxTrials) {
@@ -174,7 +174,7 @@ class default_1 {
                         return this.redis.cacheForChat(id, subscriber);
                     }
                 });
-                (this.redis ? this.redis.isCachedForChat(id, subscriber) : Promise.resolve(false))
+                ((this.redis && !force) ? this.redis.isCachedForChat(id, subscriber) : Promise.resolve(false))
                     .then(isCached => {
                     if (isCached) {
                         logger.info(`skipped subscriber ${targetStr} as this tweet or the origin of this retweet has been sent already`);
@@ -312,15 +312,26 @@ class default_1 {
         if (opt.redis)
             this.redis = new redis_1.default(opt.redis);
         ScreenNameNormalizer._queryUser = this.queryUser;
-        exports.sendTweet = (id, receiver) => {
-            this.getTweet(id, this.sendTweets({ sourceInfo: `tweet ${id}`, reportOnSkip: true }, receiver))
+        exports.sendTweet = (idOrQuery, receiver, forceRefresh) => {
+            const send = (id) => this.getTweet(id, this.sendTweets({ sourceInfo: `tweet ${id}`, reportOnSkip: true, force: forceRefresh }, receiver), forceRefresh)
                 .catch((err) => {
+                var _a;
+                if (((_a = err[0]) === null || _a === void 0 ? void 0 : _a.code) === 34)
+                    return this.bot.sendTo(receiver, `找不到用户 ${match[2].replace(/^@?(.*)$/, '@$1')}。`);
                 if (err[0].code !== 144) {
                     logger.warn(`error retrieving tweet: ${err[0].message}`);
                     this.bot.sendTo(receiver, `获取推文时出现错误:${err[0].message}`);
                 }
                 this.bot.sendTo(receiver, '找不到请求的推文,它可能已被删除。');
             });
+            const match = /^last(|-\d+)@([^\/?#,]+)((?:,no.*?=[^,]*)*)$/.exec(idOrQuery);
+            const query = () => this.queryTimeline({
+                username: match[2],
+                count: 0 - Number(match[1] || -1),
+                noreps: { on: true, off: false }[match[3].replace(/.*,noreps=([^,]*).*/, '$1')],
+                norts: { on: true, off: false }[match[3].replace(/.*,norts=([^,]*).*/, '$1')],
+            }).then(tweets => tweets.slice(-1)[0].id_str);
+            (match ? query() : Promise.resolve(idOrQuery)).then(send);
         };
         exports.sendTimeline = ({ username, count, since, until, noreps, norts }, receiver) => {
             const countNum = Number(count) || 10;

+ 25 - 8
src/command.ts

@@ -178,29 +178,46 @@ function list(chat: IChat, _: string[], reply: (msg: string) => any, lock: ILock
 }
 
 function view(chat: IChat, args: string[], reply: (msg: string) => any): void {
-  if (args.length === 0) {
-    return reply('找不到要查看的链接。');
+  if (args.length === 0 || !args[0]) {
+    return reply('找不到要查看的链接或表达式。');
   }
-  const match = /^(?:.*twitter.com\/[^\/?#]+\/status\/)?(\d+)/.exec(args[0]);
+  const match =
+    /^(last(?:|-\d+)@[^\/?#]+)$/.exec(args[0]) ||
+    /^(?:.*twitter.com\/[^\/?#]+\/status\/)?(\d+)/.exec(args[0]);
   if (!match) {
-    return reply('链接格式有误。');
+    return reply(`链接或表达式格式有误。
+示例:
+https://twitter.com/TomoyoKurosawa/status/1294613494860361729
+last@TomoyoKurosawa
+last-2@sunflower930316,noreps=off,norts=on
+(表达式筛选参数详见 /help twitter_query)`);
+  }
+  let forceRefresh: boolean;
+  for (const arg of args.slice(1)) {
+    const optMatch = /^(force|refresh)=(.*)/.exec(arg);
+    if (!optMatch) return reply(`未定义的查看参数:${arg}。`);
+    forceRefresh = {on: true, off: false}[optMatch[2]];
   }
   try {
-    sendTweet(match[1], chat);
+    sendTweet(match[1], chat, forceRefresh);
   } catch (e) {
     reply('推特机器人尚未加载完毕,请稍后重试。');
   }
 }
 
+function resendLast(chat: IChat, args: string[], reply: (msg: string) => any): void {
+  view(chat, [(args[0] || '').replace(/^@?(.+)$/, 'last@$1'), 'refresh=on'], reply);
+}
+
 function query(chat: IChat, args: string[], reply: (msg: string) => any): void {
-  if (args.length === 0) {
+  if (args.length === 0 || !args[0]) {
     return reply('找不到要查询的用户。');
   }
   const match = 
     /twitter.com\/([^\/?#]+)/.exec(args[0]) ||
     /^([^\/?#]+)$/.exec(args[0]);
   if (!match) {
-    return reply('链接格式有误。');
+    return reply('链接或用户名格式有误。');
   }
   const conf: {
     username: string,
@@ -236,4 +253,4 @@ function query(chat: IChat, args: string[], reply: (msg: string) => any): void {
   }
 }
 
-export { parseCmd, sub, list, unsub, unsubAll, view, query };
+export { parseCmd, sub, list, unsub, unsubAll, view, resendLast, query };

+ 6 - 2
src/koishi.ts

@@ -2,7 +2,7 @@ import { App, Bot, segment, Session, sleep } from 'koishi';
 import 'koishi-adapter-onebot';
 import { Message as CQMessage, SenderInfo } from 'koishi-adapter-onebot';
 
-import { parseCmd, query, view } from './command';
+import { parseCmd, query, view, resendLast } from './command';
 import { getLogger } from './loggers';
 import { chainPromises } from './utils';
 
@@ -201,6 +201,9 @@ export default class {
         case 'twitter_get':
           view(chat, cmdObj.args, reply);
           break;
+        case 'twitter_resendlast':
+          resendLast(chat, cmdObj.args, reply);
+          break;
         case 'twitter_query':
         case 'twitter_gettimeline':
           query(chat, cmdObj.args, reply);
@@ -227,7 +230,8 @@ export default class {
 /twitter - 查询当前聊天中的推文订阅
 /twitter_sub[scribe]〈链接|用户名〉- 订阅 Twitter 推文搬运
 /twitter_unsub[scribe]〈链接|用户名〉- 退订 Twitter 推文搬运
-/twitter_view〈链接〉- 查看推文
+/twitter_view〈链接|表达式〉[{force|refresh}={on|off}] - 查看推文(可选强制重新载入)
+/twitter_resendlast〈用户名〉- 强制重发该用户最后一条推文
 /twitter_query〈链接|用户名〉[参数列表...] - 查询时间线(详见 /help twitter_query)\
 ${chat.chatType === ChatType.Temp ?
     '\n(当前游客模式下无法使用订阅功能,请先添加本账号为好友。)' : ''

+ 26 - 10
src/twitter.ts

@@ -46,7 +46,7 @@ export class ScreenNameNormalizer {
   }
 }
 
-export let sendTweet = (id: string, receiver: IChat): void => {
+export let sendTweet = (id: string, receiver: IChat, forceRefresh: boolean): void => {
   throw Error();
 };
 
@@ -141,15 +141,29 @@ export default class {
     this.wsUrl = opt.wsUrl;
     if (opt.redis) this.redis = new RedisSvc(opt.redis);
     ScreenNameNormalizer._queryUser = this.queryUser;
-    sendTweet = (id, receiver) => {
-      this.getTweet(id, this.sendTweets({sourceInfo: `tweet ${id}`, reportOnSkip: true}, receiver))
+    sendTweet = (idOrQuery, receiver, forceRefresh) => {
+      const send = (id: string) => this.getTweet(
+        id,
+        this.sendTweets({sourceInfo: `tweet ${id}`, reportOnSkip: true, force: forceRefresh}, receiver),
+        forceRefresh
+      )
         .catch((err: {code: number, message: string}[]) => {
+          if (err[0]?.code === 34)
+            return this.bot.sendTo(receiver, `找不到用户 ${match[2].replace(/^@?(.*)$/, '@$1')}。`);
           if (err[0].code !== 144) {
             logger.warn(`error retrieving tweet: ${err[0].message}`);
             this.bot.sendTo(receiver, `获取推文时出现错误:${err[0].message}`);
           }
           this.bot.sendTo(receiver, '找不到请求的推文,它可能已被删除。');
         });
+      const match = /^last(|-\d+)@([^\/?#,]+)((?:,no.*?=[^,]*)*)$/.exec(idOrQuery);
+      const query = () => this.queryTimeline({
+          username: match[2],
+          count: 0 - Number(match[1] || -1),
+          noreps: {on: true, off: false}[match[3].replace(/.*,noreps=([^,]*).*/, '$1')],
+          norts: {on: true, off: false}[match[3].replace(/.*,norts=([^,]*).*/, '$1')],
+        }).then(tweets => tweets.slice(-1)[0].id_str);
+      (match ? query() : Promise.resolve(idOrQuery)).then(send);
     };
     sendTimeline = ({username, count, since, until, noreps, norts}, receiver) => {
       const countNum = Number(count) || 10;
@@ -262,9 +276,10 @@ export default class {
 
   private workOnTweets = (
     tweets: Tweets,
-    sendTweets: (id: string, msg: string, text: string, author: string) => void
+    sendTweets: (id: string, msg: string, text: string, author: string) => void,
+    refresh = false
   ) => Promise.all(tweets.map(tweet => 
-    (this.redis ? this.redis.getContent(`webshot/${tweet.id_str}`) : Promise.reject())
+    ((this.redis && !refresh) ? this.redis.getContent(`webshot/${tweet.id_str}`) : Promise.reject())
       .then(content => {
         if (content === null) throw Error();
         logger.info(`retrieved cached webshot of tweet ${tweet.id_str} from redis database`);
@@ -283,7 +298,7 @@ export default class {
       )
   ));
 
-  public getTweet = (id: string, sender: (id: string, msg: string, text: string, author: string) => void) => {
+  public getTweet = (id: string, sender: (id: string, msg: string, text: string, author: string) => void, refresh = false) => {
     const endpoint = 'statuses/show';
     const config = {
       id,
@@ -292,15 +307,16 @@ export default class {
     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);
+        return this.workOnTweets([tweet], sender, refresh);
       });
   };
 
   private sendTweets = (
-    config: {sourceInfo?: string, reportOnSkip?: boolean} = {reportOnSkip: false}, ...to: IChat[]
+    config: {sourceInfo?: string, reportOnSkip?: boolean, force?: boolean} = {reportOnSkip: false, force: false},
+    ...to: IChat[]
   ) => (id: string, msg: string, text: string, author: string) => {
     to.forEach(subscriber => {
-      const {sourceInfo: source, reportOnSkip} = config;
+      const {sourceInfo: source, reportOnSkip, force} = config;
       const targetStr = JSON.stringify(subscriber);
       const send = () => retryOnError(
         () => this.bot.sendTo(subscriber, msg),
@@ -318,7 +334,7 @@ export default class {
           return this.redis.cacheForChat(id, subscriber);
         }
       });
-      (this.redis ? this.redis.isCachedForChat(id, subscriber) : Promise.resolve(false))
+      ((this.redis && !force) ? this.redis.isCachedForChat(id, subscriber) : Promise.resolve(false))
         .then(isCached => {
           if (isCached) {
             logger.info(`skipped subscriber ${targetStr} as this tweet or the origin of this retweet has been sent already`);