command.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. /* eslint-disable @typescript-eslint/no-unsafe-return */
  2. /* eslint-disable @typescript-eslint/member-delimiter-style */
  3. /* eslint-disable prefer-arrow/prefer-arrow-functions */
  4. import * as fs from 'fs';
  5. import * as path from 'path';
  6. import { relativeDate } from './datetime';
  7. import { getLogger } from './loggers';
  8. import {
  9. sendAllStories, sendStory, sendTimeline, ScreenNameNormalizer as normalizer,
  10. linkBuilder, parseLink
  11. } from './twitter';
  12. const logger = getLogger('command');
  13. function parseCmd(message: string): {
  14. cmd: string;
  15. args: string[];
  16. } {
  17. message = message.trim();
  18. message = message.replace('\\\\', '\\0x5c');
  19. message = message.replace('\\\"', '\\0x22');
  20. message = message.replace('\\\'', '\\0x27');
  21. const strs = message.match(/'[\s\S]*?'|(?:\S+=)?"[\s\S]*?"|\S+/mg);
  22. const cmd = strs?.length ? strs[0].length ? strs[0].substring(0, 1) === '/' ? strs[0].substring(1) : '' : '' : '';
  23. const args = (strs ?? []).slice(1).map(arg => {
  24. arg = arg.replace(/^(\S+=)?["']+(?!.*=)|["']+$/g, '$1');
  25. arg = arg.replace('\\0x27', '\\\'');
  26. arg = arg.replace('\\0x22', '\\\"');
  27. arg = arg.replace('\\0x5c', '\\\\');
  28. return arg;
  29. });
  30. return {
  31. cmd,
  32. args,
  33. };
  34. }
  35. function linkFinder(userName: string, chat: IChat, lock: ILock): [string, number] {
  36. const normalizedLink = linkBuilder({userName});
  37. const link = Object.keys(lock.threads).find(realLink =>
  38. normalizedLink === realLink.replace(/\/@/, '/').toLowerCase()
  39. );
  40. if (!link) return [null, -1];
  41. const index = lock.threads[link].subscribers.findIndex(({chatID, chatType}) =>
  42. chat.chatID === chatID && chat.chatType === chatType
  43. );
  44. return [link, index];
  45. }
  46. function sub(chat: IChat, args: string[], reply: (msg: string) => any,
  47. lock: ILock, lockfile: string
  48. ): void {
  49. if (chat.chatType === ChatType.Temp) {
  50. return reply('请先添加机器人为好友。');
  51. }
  52. if (args.length === 0) {
  53. return reply('找不到要订阅 Instagram 限时动态的链接。');
  54. }
  55. const matched = parseLink(args[0]);
  56. if (!matched) {
  57. return reply(`订阅链接格式错误:
  58. 示例:
  59. https://www.instagram.com/tomoyo_kurosawa_/`);
  60. }
  61. const subscribeTo = (link: string, config: {id?: number, msg?: string} = {}) => {
  62. const {id, msg = `已为此聊天订阅 ${link} 的 Instagram 限时动态`} = config;
  63. if (id) {
  64. lock.feed.push(link);
  65. lock.threads[link] = {
  66. id,
  67. offset: '0',
  68. subscribers: [],
  69. updatedAt: '',
  70. };
  71. }
  72. lock.threads[link].subscribers.push(chat);
  73. logger.warn(`chat ${JSON.stringify(chat)} has subscribed ${link}`);
  74. fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
  75. reply(msg);
  76. };
  77. const tryFindSub = (userName: string) => {
  78. const [realLink, index] = linkFinder(userName, chat, lock);
  79. if (index > -1) { reply('此聊天已订阅此链接的 Instagram 限时动态。'); return true; }
  80. if (realLink) { subscribeTo(realLink); return true; }
  81. return false;
  82. };
  83. const newSub = (userName: string) => {
  84. const link = linkBuilder(matched);
  85. subscribeTo(link, {id: Number(userName.split(':')[1])});
  86. };
  87. if (!tryFindSub(matched.userName)) {
  88. normalizer.normalizeLive(matched.userName).then(userName => {
  89. if (!userName) return reply(`找不到用户 ${matched.userName.replace(/^@?(.*)$/, '@$1')}。`);
  90. else newSub(userName);
  91. });
  92. }
  93. }
  94. function unsubAll(chat: IChat, args: string[], reply: (msg: string) => any,
  95. lock: ILock, lockfile: string
  96. ): void {
  97. if (chat.chatType === ChatType.Temp) {
  98. return reply('请先添加机器人为好友。');
  99. }
  100. Object.entries(lock.threads).forEach(([link, {subscribers}]) => {
  101. const index = subscribers.indexOf(chat);
  102. if (index === -1) return;
  103. subscribers.splice(index, 1);
  104. fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
  105. logger.warn(`chat ${JSON.stringify(chat)} has unsubscribed ${link}`);
  106. });
  107. return reply(`已为此聊天退订所有 Instagram 限时动态链接。`);
  108. }
  109. function unsub(chat: IChat, args: string[], reply: (msg: string) => any,
  110. lock: ILock, lockfile: string
  111. ): void {
  112. if (chat.chatType === ChatType.Temp) {
  113. return reply('请先添加机器人为好友。');
  114. }
  115. if (args.length === 0) {
  116. return reply('找不到要退订 Instagram 限时动态的链接。');
  117. }
  118. const match = parseLink(args[0])?.userName;
  119. if (!match) {
  120. return reply('链接格式有误。');
  121. }
  122. const [link, index] = linkFinder(match, chat, lock);
  123. if (index === -1) return list(chat, args, msg => reply('您没有订阅此链接的 Instagram 限时动态。\n' + msg), lock);
  124. else {
  125. lock.threads[link].subscribers.splice(index, 1);
  126. fs.writeFileSync(path.resolve(lockfile), JSON.stringify(lock));
  127. logger.warn(`chat ${JSON.stringify(chat)} has unsubscribed ${link}`);
  128. return reply(`已为此聊天退订 ${link} 的 Instagram 限时动态`);
  129. }
  130. }
  131. function list(chat: IChat, _: string[], reply: (msg: string) => any, lock: ILock): void {
  132. if (chat.chatType === ChatType.Temp) {
  133. return reply('请先添加机器人为好友。');
  134. }
  135. const links = [];
  136. Object.keys(lock.threads).forEach(key => {
  137. if (lock.threads[key].subscribers.find(({chatID, chatType}) =>
  138. chat.chatID === chatID && chat.chatType === chatType
  139. )) links.push(`${key} ${relativeDate(lock.threads[key].updatedAt)}`);
  140. });
  141. return reply('此聊天中订阅的 Instagram 限时动态动态链接:\n' + links.join('\n'));
  142. }
  143. function view(chat: IChat, args: string[], reply: (msg: string) => any): void {
  144. const promptOnError = <T>(func: (...args: T[]) => void) => (...args: T[]): void => {
  145. try {
  146. func(...args);
  147. } catch (e) {
  148. reply('机器人尚未加载完毕,请稍后重试。');
  149. }
  150. }
  151. if (args.length === 0) {
  152. return reply('找不到要查看 Instagram 限时动态的链接。');
  153. }
  154. const match = parseLink(args[0]);
  155. if (!match?.userName) {
  156. return reply('链接格式有误。');
  157. }
  158. if (match?.storyId) {
  159. return promptOnError(sendStory)(match.userName, match.storyId, chat);
  160. }
  161. const conf: {
  162. skip?: number,
  163. count?: number,
  164. } = {};
  165. const confZH: Record<keyof typeof conf, string> = {
  166. count: '最大查看数量',
  167. skip: '跳过数量',
  168. };
  169. for (const arg of args.slice(1)) {
  170. const optMatch = /^(count|skip)=(.*)/.exec(arg);
  171. if (!optMatch) return reply(`未定义的查看参数:${arg}。`);
  172. const optKey = optMatch[1] as keyof typeof confZH;
  173. if (optMatch.length === 1 || !/^-?\d*$/.test(optMatch[2])) return reply(`${confZH[optKey]}参数应为数值。`);
  174. if (optMatch[2] === '') return reply(`${confZH[optKey]}参数值不可为空。`);
  175. conf[optKey] = Number(optMatch[2]);
  176. }
  177. promptOnError(sendAllStories)(match.userName, chat, conf.skip, conf.count);
  178. }
  179. function query(chat: IChat, args: string[], reply: (msg: string) => any): void {
  180. if (args.length === 0) {
  181. return reply('找不到要查询 Instagram 限时动态的用户。');
  182. }
  183. const match = parseLink(args[0])?.userName;
  184. if (!match) {
  185. return reply('链接格式有误。');
  186. }
  187. try {
  188. sendTimeline(match, chat);
  189. } catch (e) {
  190. logger.error(`error querying timeline, error: ${e}`);
  191. reply('机器人尚未加载完毕,请稍后重试。');
  192. }
  193. }
  194. export { parseCmd, sub, list, unsub, unsubAll, view, query };