command.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import * as fs from 'fs';
  2. import * as path from 'path';
  3. import { relativeDate } from './datetime';
  4. import { getLogger } from './loggers';
  5. import { bigNumPlus, sendTweet, ScreenNameNormalizer as normalizer } from './twitter';
  6. const logger = getLogger('command');
  7. function parseLink(link: string): string[] {
  8. let match =
  9. link.match(/twitter.com\/([^\/?#]+)\/lists\/([^\/?#]+)/) ||
  10. link.match(/^([^\/?#]+)\/([^\/?#]+)$/);
  11. if (match) return [match[1], `/lists/${match[2]}`];
  12. match =
  13. link.match(/twitter.com\/([^\/?#]+)\/status\/(\d+)/);
  14. if (match) return [match[1], `/status/${match[2]}`];
  15. match =
  16. link.match(/twitter.com\/([^\/?#]+)/) ||
  17. link.match(/^([^\/?#]+)$/);
  18. if (match) return [match[1]];
  19. return;
  20. }
  21. function linkBuilder(userName: string, more = ''): string {
  22. if (!userName) return;
  23. return `https://twitter.com/${userName}${more}`;
  24. }
  25. function linkFinder(checkedMatch: string[], chat: IChat, lock: ILock): [string, number] {
  26. const normalizedLink =
  27. linkBuilder(normalizer.normalize(checkedMatch[0]), checkedMatch[1]?.toLowerCase());
  28. const link = Object.keys(lock.threads).find(realLink =>
  29. normalizedLink === realLink.replace(/\/@/, '/').toLowerCase()
  30. );
  31. if (!link) return [null, -1];
  32. const index = lock.threads[link].subscribers.findIndex(({chatID, chatType}) =>
  33. chat.chatID === chatID && chat.chatType === chatType
  34. );
  35. return [link, index];
  36. }
  37. function sub(chat: IChat, args: string[], reply: (msg: string) => any,
  38. lock: ILock, lockfile: string
  39. ): void {
  40. if (args.length === 0) {
  41. return reply('找不到要订阅媒体推文的链接。');
  42. }
  43. const match = parseLink(args[0]);
  44. if (!match) {
  45. return reply(`订阅链接格式错误:
  46. 示例:
  47. https://twitter.com/Saito_Shuka
  48. https://twitter.com/rikakomoe/lists/lovelive
  49. https://twitter.com/TomoyoKurosawa/status/1294613494860361729`);
  50. }
  51. let offset = '0';
  52. if (match[1]) {
  53. const matchStatus = match[1].match(/\/status\/(\d+)/);
  54. if (matchStatus) {
  55. offset = bigNumPlus(matchStatus[1], '-1');
  56. delete match[1];
  57. }
  58. }
  59. const subscribeTo = (link: string, config: {addNew?: boolean, msg?: string} = {}) => {
  60. const {addNew = false, msg = `已为此聊天订阅 ${link} 的媒体推文`} = config;
  61. if (addNew) {
  62. lock.feed.push(link);
  63. lock.threads[link] = {
  64. offset,
  65. subscribers: [],
  66. updatedAt: '',
  67. };
  68. }
  69. lock.threads[link].subscribers.push(chat);
  70. logger.warn(`chat ${JSON.stringify(chat)} has subscribed ${link}`);
  71. fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
  72. reply(msg);
  73. };
  74. const [realLink, index] = linkFinder(match, chat, lock);
  75. if (index > -1) return reply('此聊天已订阅此链接。');
  76. if (realLink) return subscribeTo(realLink);
  77. const [rawUserName, more] = match;
  78. if (rawUserName.toLowerCase() === 'i' && more?.match(/lists\/(\d+)/)) {
  79. return subscribeTo(linkBuilder('i', more), {addNew: true});
  80. }
  81. normalizer.normalizeLive(rawUserName).then(userName => {
  82. if (!userName) return reply(`找不到用户 ${rawUserName.replace(/^@?(.*)$/, '@$1')}。`);
  83. const link = linkBuilder(userName, more);
  84. const msg = (offset === '0') ?
  85. undefined :
  86. `已为此聊天订阅 ${link} 的媒体动态并回溯到此动态 ID(含)之后的第一条媒体动态。
  87. (参见:https://blog.twitter.com/engineering/en_us/a/2010/announcing-snowflake.html)`;
  88. subscribeTo(link, {addNew: true, msg});
  89. });
  90. }
  91. function unsub(chat: IChat, args: string[], reply: (msg: string) => any,
  92. lock: ILock, lockfile: string
  93. ): void {
  94. if (args.length === 0) {
  95. return reply('找不到要退订媒体推文的链接。');
  96. }
  97. const match = parseLink(args[0]);
  98. if (!match) {
  99. return reply('链接格式有误。');
  100. }
  101. const [link, index] = linkFinder(match, chat, lock);
  102. if (index === -1) return list(chat, args, msg => reply('您没有订阅此链接的媒体推文。\n' + msg), lock);
  103. else {
  104. lock.threads[link].subscribers.splice(index, 1);
  105. fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
  106. logger.warn(`chat ${JSON.stringify(chat)} has unsubscribed ${link}`);
  107. return reply(`已为此聊天退订 ${link} 的媒体推文`);
  108. }
  109. }
  110. function list(chat: IChat, _: string[], reply: (msg: string) => any, lock: ILock): void {
  111. const links = [];
  112. Object.keys(lock.threads).forEach(key => {
  113. if (lock.threads[key].subscribers.find(({chatID, chatType}) =>
  114. chat.chatID === chatID && chat.chatType === chatType
  115. )) links.push(`${key} ${relativeDate(lock.threads[key].updatedAt)}`);
  116. });
  117. return reply('此聊天中订阅媒体推文的链接:\n' + links.join('\n'));
  118. }
  119. function view(chat: IChat, args: string[], reply: (msg: string) => any): void {
  120. if (args.length === 0) {
  121. return reply('找不到要查看的链接。');
  122. }
  123. const match = args[0].match(/^(?:.*twitter.com\/[^\/?#]+\/status\/)?(\d+)/);
  124. if (!match) {
  125. return reply('链接格式有误。');
  126. }
  127. try {
  128. sendTweet(match[1], chat);
  129. } catch (e) {
  130. reply('推特机器人尚未加载完毕,请稍后重试。');
  131. }
  132. }
  133. export { sub, list, unsub, view };