command.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.query = exports.resendLast = exports.view = exports.unsubAll = exports.unsub = exports.list = exports.sub = exports.parseCmd = void 0;
  4. const fs = require("fs");
  5. const path = require("path");
  6. const datetime_1 = require("./datetime");
  7. const loggers_1 = require("./loggers");
  8. const twitter_1 = require("./twitter");
  9. const utils_1 = require("./utils");
  10. const logger = (0, loggers_1.getLogger)('command');
  11. function parseCmd(message) {
  12. message = message.trim();
  13. message = message.replace('\\\\', '\\0x5c');
  14. message = message.replace('\\\"', '\\0x22');
  15. message = message.replace('\\\'', '\\0x27');
  16. const strs = message.match(/'[\s\S]*?'|(?:\S+=)?"[\s\S]*?"|\S+/mg);
  17. const cmd = (strs === null || strs === void 0 ? void 0 : strs.length) ? strs[0].length ? strs[0].substring(0, 1) === '/' ? strs[0].substring(1) : '' : '' : '';
  18. const args = (strs !== null && strs !== void 0 ? strs : []).slice(1).map(arg => {
  19. arg = arg.replace(/^(\S+=)?["']+(?!.*=)|["']+$/g, '$1');
  20. arg = arg.replace('\\0x27', '\\\'');
  21. arg = arg.replace('\\0x22', '\\\"');
  22. arg = arg.replace('\\0x5c', '\\\\');
  23. return arg;
  24. });
  25. return {
  26. cmd,
  27. args,
  28. };
  29. }
  30. exports.parseCmd = parseCmd;
  31. function linkFinder(checkedMatch, chat, lock) {
  32. var _a;
  33. const normalizedLink = (0, twitter_1.linkBuilder)(twitter_1.ScreenNameNormalizer.normalize(checkedMatch[0]), (_a = checkedMatch[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase());
  34. const link = Object.keys(lock.threads).find(realLink => normalizedLink === realLink.replace(/\/@/, '/').toLowerCase());
  35. if (!link)
  36. return [null, -1];
  37. const index = lock.threads[link].subscribers.findIndex(({ chatID, chatType }) => chat.chatID.toString() === chatID.toString() && chat.chatType === chatType);
  38. return [link, index];
  39. }
  40. function sub(chat, args, reply, lock, lockfile) {
  41. if (chat.chatType === 'temp') {
  42. return reply('请先添加机器人为好友。');
  43. }
  44. if (args.length === 0) {
  45. return reply('找不到要订阅的链接。');
  46. }
  47. const match = (0, twitter_1.parseLink)(args[0]);
  48. if (!match) {
  49. return reply(`订阅链接格式错误:
  50. 示例:
  51. https://twitter.com/Saito_Shuka
  52. https://twitter.com/_satou_riko/lists/lovelive
  53. https://twitter.com/TomoyoKurosawa/status/1294613494860361729`);
  54. }
  55. let offset = '0';
  56. if (match[1]) {
  57. const matchStatus = /\/status\/(\d+)/.exec(match[1]);
  58. if (matchStatus) {
  59. offset = utils_1.BigNumOps.plus(matchStatus[1], '-1');
  60. delete match[1];
  61. }
  62. }
  63. const subscribeTo = (link, config = {}) => {
  64. const { addNew = false, msg = `已为此聊天订阅 ${link}` } = config;
  65. if (addNew) {
  66. lock.feed.push(link);
  67. lock.threads[link] = {
  68. offset,
  69. subscribers: [],
  70. updatedAt: '',
  71. };
  72. }
  73. lock.threads[link].subscribers.push(chat);
  74. logger.warn(`chat ${JSON.stringify(chat)} has subscribed ${link}`);
  75. fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
  76. reply(msg);
  77. };
  78. const [realLink, index] = linkFinder(match, chat, lock);
  79. if (index > -1)
  80. return reply('此聊天已订阅此链接。');
  81. if (realLink)
  82. return subscribeTo(realLink);
  83. const [rawUserName, more] = match;
  84. if (rawUserName.toLowerCase() === 'i' && /lists\/(\d+)/.exec(more)) {
  85. return subscribeTo((0, twitter_1.linkBuilder)('i', more), { addNew: true });
  86. }
  87. twitter_1.ScreenNameNormalizer.normalizeLive(rawUserName).then(userName => {
  88. if (!userName)
  89. return reply(`找不到用户 ${rawUserName.replace(/^@?(.*)$/, '@$1')}。`);
  90. const link = (0, twitter_1.linkBuilder)(userName, more);
  91. const msg = (offset === '0') ?
  92. undefined :
  93. `已为此聊天订阅 ${link} 并回溯到此动态 ID(含)之后的第一条动态。
  94. (参见:https://blog.twitter.com/engineering/en_us/a/2010/announcing-snowflake.html)`;
  95. subscribeTo(link, { addNew: true, msg });
  96. });
  97. }
  98. exports.sub = sub;
  99. function unsubAll(chat, args, reply, lock, lockfile) {
  100. if (chat.chatType === 'temp') {
  101. return reply('请先添加机器人为好友。');
  102. }
  103. Object.entries(lock.threads).forEach(([link, { subscribers }]) => {
  104. const index = subscribers.findIndex(({ chatID, chatType }) => chat.chatID.toString() === chatID.toString() && chat.chatType === chatType);
  105. if (index === -1)
  106. return;
  107. subscribers.splice(index, 1);
  108. fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
  109. logger.warn(`chat ${JSON.stringify(chat)} has unsubscribed ${link}`);
  110. });
  111. return reply(`已为此聊天退订所有推特链接。`);
  112. }
  113. exports.unsubAll = unsubAll;
  114. function unsub(chat, args, reply, lock, lockfile) {
  115. if (chat.chatType === 'temp') {
  116. return reply('请先添加机器人为好友。');
  117. }
  118. if (args.length === 0) {
  119. return reply('找不到要退订的链接。');
  120. }
  121. const match = (0, twitter_1.parseLink)(args[0]);
  122. if (!match) {
  123. return reply('链接格式有误。');
  124. }
  125. const [link, index] = linkFinder(match, chat, lock);
  126. if (index === -1)
  127. return list(chat, args, msg => reply('您没有订阅此链接。\n' + msg), lock);
  128. else {
  129. lock.threads[link].subscribers.splice(index, 1);
  130. fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
  131. logger.warn(`chat ${JSON.stringify(chat)} has unsubscribed ${link}`);
  132. return reply(`已为此聊天退订 ${link}`);
  133. }
  134. }
  135. exports.unsub = unsub;
  136. function list(chat, _, reply, lock) {
  137. if (chat.chatType === 'temp') {
  138. return reply('请先添加机器人为好友。');
  139. }
  140. const links = [];
  141. Object.keys(lock.threads).forEach(key => {
  142. if (lock.threads[key].subscribers.find(({ chatID, chatType }) => chat.chatID === chatID && chat.chatType === chatType))
  143. links.push(`${key} ${(0, datetime_1.relativeDate)(lock.threads[key].updatedAt)}`);
  144. });
  145. return reply('此聊天中订阅的链接:\n' + links.join('\n'));
  146. }
  147. exports.list = list;
  148. function view(chat, args, reply) {
  149. if (args.length === 0 || !args[0]) {
  150. return reply('找不到要查看的链接或表达式。');
  151. }
  152. const match = /^(last(?:|-(\d+))@[^\/?#]+)$/.exec(args[0]) ||
  153. /^(?:.*twitter.com\/[^\/?#]+\/status\/)?(\d+)/.exec(args[0]);
  154. if (!match) {
  155. return reply(`链接或表达式格式有误。
  156. 示例:
  157. https://twitter.com/TomoyoKurosawa/status/1486136914864345092
  158. 1486136914864345092
  159. last@TomoyoKurosawa
  160. last-1@sunflower930316,noreps=off,norts=on
  161. (表达式筛选参数详见 /help twitter_query)`);
  162. }
  163. if (Math.abs(Number(match[2])) >= 50) {
  164. return reply('表达式中指定的回溯数量超出取值范围。');
  165. }
  166. let forceRefresh;
  167. for (const arg of args.slice(1)) {
  168. const optMatch = /^(force|refresh)=(.*)/.exec(arg);
  169. if (!optMatch)
  170. return reply(`未定义的查看参数:${arg}。`);
  171. forceRefresh = { on: true, off: false }[optMatch[2]];
  172. }
  173. try {
  174. (0, twitter_1.sendTweet)(match[1], chat, forceRefresh);
  175. }
  176. catch (e) {
  177. reply('推特机器人尚未加载完毕,请稍后重试。');
  178. }
  179. }
  180. exports.view = view;
  181. function resendLast(chat, args, reply) {
  182. view(chat, [(args[0] || '').replace(/^@?(.+)$/, 'last@$1'), 'refresh=on'], reply);
  183. }
  184. exports.resendLast = resendLast;
  185. function query(chat, args, reply) {
  186. if (args.length === 0 || !args[0]) {
  187. return reply('找不到要查询的用户。');
  188. }
  189. const match = /twitter.com\/([^\/?#]+)/.exec(args[0]) ||
  190. /^([^\/?#]+)$/.exec(args[0]);
  191. if (!match) {
  192. return reply('链接或用户名格式有误。');
  193. }
  194. const conf = { username: match[1], noreps: 'on', norts: 'off' };
  195. const confZH = {
  196. count: '数量上限',
  197. since: '起始点',
  198. until: '结束点',
  199. noreps: '忽略回复推文(on/off)',
  200. norts: '忽略原生转推(on/off)',
  201. };
  202. for (const arg of args.slice(1)) {
  203. const optMatch = /^(count|since|until|noreps|norts)=(.*)/.exec(arg);
  204. if (!optMatch)
  205. return reply(`未定义的查询参数:${arg}。`);
  206. const optKey = optMatch[1];
  207. if (optMatch.length === 1)
  208. return reply(`查询${confZH[optKey]}参数格式有误。`);
  209. conf[optKey] = optMatch[2];
  210. if (optMatch[2] === '')
  211. return reply(`查询${confZH[optKey]}参数值不可为空。`);
  212. }
  213. if (conf.count !== undefined && !Number(conf.count) || Math.abs(Number(conf.count)) > 50) {
  214. return reply('查询数量上限参数为零、非数值或超出取值范围。');
  215. }
  216. try {
  217. (0, twitter_1.sendTimeline)(conf, chat);
  218. }
  219. catch (e) {
  220. logger.error(`error querying timeline, error: ${e}`);
  221. reply('推特机器人尚未加载完毕,请稍后重试。');
  222. }
  223. }
  224. exports.query = query;