twitter.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.snowflake = exports.parseAction = exports.processTweetBody = exports.recurringKeywords = exports.keywordMap = void 0;
  4. const emojiStrip = require("emoji-strip");
  5. const fs = require("fs");
  6. const path = require("path");
  7. const Twitter = require("twitter");
  8. const html_entities_1 = require("html-entities");
  9. const loggers_1 = require("./loggers");
  10. const utils_1 = require("./utils");
  11. const wiki_1 = require("./wiki");
  12. exports.keywordMap = {
  13. 'ガチャ予告': '卡池',
  14. '一日一回無料ガチャ': '卡池',
  15. 'イベント開催決定': '活动',
  16. 'タワー.*追加': '新塔',
  17. '(生放送|配信)予告': '生放',
  18. 'メンテナンス予告': '维护',
  19. '(育成応援|お仕事).*開催': '工作',
  20. '新曲一部公開': '新曲',
  21. 'キャラクター紹介': '组合',
  22. '今後のアップデート情報': '计划',
  23. '(?<!今後の)アップデート情報': '改修',
  24. };
  25. exports.recurringKeywords = [
  26. '(育成応援|お仕事).*開催', 'イベント開催決定', '無料ガチャ',
  27. 'アップデート情報', 'キャラクター紹介',
  28. '(生放送|配信)予告', 'メンテナンス予告'
  29. ];
  30. const xmlEntities = new html_entities_1.XmlEntities();
  31. const processTweetBody = (tweet) => {
  32. const urls = tweet.entities.urls ? tweet.entities.urls.map(url => url.expanded_url) : [];
  33. const [title, body] = emojiStrip(xmlEntities.decode(tweet.full_text))
  34. .replace(/(?<=\n|^)(.*)(?:\: |:)(https?:\/\/.*?)(?=\s|$)/g, '[$2 $1]')
  35. .replace(/https?:\/\/.*?(?=\s|$)/g, () => urls.length ? urls.splice(0, 1)[0] : '')
  36. .replace(/(?<=\n|^)[\/]\n/g, '')
  37. .replace(/((?<=\s)#.*?\s+)+$/g, '')
  38. .trim()
  39. .match(/(.*?)\n\n(.*)/s).slice(1);
  40. const date = new Date(tweet.created_at)
  41. .toLocaleDateString('en-CA', { timeZone: 'Asia/Tokyo' })
  42. .replace(/-/g, '');
  43. const pageTitle = title.replace(/[【】\/]/g, '') + `${exports.recurringKeywords.some(keyword => new RegExp(keyword).exec(title)) ? `-${date}` : ''}`;
  44. return { title, body, pageTitle, date };
  45. };
  46. exports.processTweetBody = processTweetBody;
  47. const parseAction = (action) => {
  48. if (!action || !Object.keys(action))
  49. return '(无)';
  50. return `\n
  51. 标题:${action.title}
  52. 操作:${action.new ? '新建' : '更新'}
  53. 媒体:${action.mediafiles.map((fileName, index) => `\n ${index + 1}. ${fileName}`).join('') || '(无)'}
  54. 链接:${action.pageid ? `https://wiki.biligame.com/idolypride/index.php?curid=${action.pageid}` : '(无)'}
  55. `;
  56. };
  57. exports.parseAction = parseAction;
  58. const TWITTER_EPOCH = 1288834974657;
  59. const snowflake = (epoch) => Number.isNaN(epoch) ? undefined :
  60. utils_1.BigNumOps.lShift(String(epoch - 1 - TWITTER_EPOCH), 22);
  61. exports.snowflake = snowflake;
  62. const logger = (0, loggers_1.getLogger)('twitter');
  63. class default_1 {
  64. constructor(opt) {
  65. this.launch = () => {
  66. this.publisher.login(this.wikiSessionCookie).then(() => {
  67. setTimeout(this.work, this.workInterval * 1000);
  68. });
  69. };
  70. this.work = () => {
  71. const lock = this.lock;
  72. if (this.workInterval < 1)
  73. this.workInterval = 1;
  74. const currentFeed = 'https://twitter.com/idolypride';
  75. logger.debug(`pulling feed ${currentFeed}`);
  76. const promise = new Promise(resolve => {
  77. let config;
  78. let endpoint;
  79. const match = /https:\/\/twitter.com\/([^\/]+)/.exec(currentFeed);
  80. if (match) {
  81. config = {
  82. screen_name: match[1],
  83. exclude_replies: true,
  84. include_rts: false,
  85. tweet_mode: 'extended',
  86. };
  87. endpoint = 'statuses/user_timeline';
  88. }
  89. if (endpoint) {
  90. const offset = lock.offset;
  91. if (offset > 0)
  92. config.since_id = offset;
  93. const getMore = (lastTweets = []) => this.client.get(endpoint, config, (error, tweets) => {
  94. if (error) {
  95. if (error instanceof Array && error.length > 0 && error[0].code === 34) {
  96. logger.warn(`error on fetching tweets for ${currentFeed}: ${JSON.stringify(error)}`);
  97. lock.subscribers.forEach(subscriber => {
  98. logger.info(`sending notfound message of ${currentFeed} to ${JSON.stringify(subscriber)}`);
  99. this.bot.sendTo(subscriber, `错误:链接 ${currentFeed} 指向的用户或列表不存在。`).catch();
  100. });
  101. }
  102. else {
  103. logger.error(`unhandled error on fetching tweets for ${currentFeed}: ${JSON.stringify(error)}`);
  104. }
  105. }
  106. if (!(tweets instanceof Array) || tweets.length === 0)
  107. return resolve(lastTweets);
  108. if (offset <= 0)
  109. return resolve(lastTweets.concat(tweets));
  110. config.max_id = utils_1.BigNumOps.plus(tweets.slice(-1)[0].id_str, '-1');
  111. getMore(lastTweets.concat(tweets));
  112. });
  113. getMore();
  114. }
  115. });
  116. promise.then((tweets) => {
  117. logger.debug(`api returned ${JSON.stringify(tweets)} for feed ${currentFeed}`);
  118. const updateDate = () => lock.updatedAt = new Date().toString();
  119. if (tweets.length === 0) {
  120. updateDate();
  121. return;
  122. }
  123. const topOfFeed = tweets[0].id_str;
  124. const updateOffset = () => lock.offset = topOfFeed;
  125. if (lock.offset === '-1') {
  126. updateOffset();
  127. return;
  128. }
  129. if (lock.offset === '0')
  130. tweets.splice(1);
  131. return (0, utils_1.chainPromises)(tweets.reverse().map(tweet => () => {
  132. const match = /(.*?)\n\n(.*)/s.exec(tweet.full_text);
  133. if (!match)
  134. return Promise.resolve({});
  135. for (const keyword in exports.keywordMap) {
  136. if (new RegExp(keyword).exec(match[1])) {
  137. const tweetUrl = `${currentFeed}/status/${tweet.id_str}`;
  138. logger.info(`working on ${tweetUrl}`);
  139. return this.publisher.post(tweet, exports.keywordMap[keyword])
  140. .then(action => {
  141. if (action.result === 'Success') {
  142. this.lock.lastActions.push(action);
  143. logger.info(`successfully posted content of ${tweetUrl} to bwiki, link:`);
  144. logger.info(`https://wiki.biligame.com/idolypride/index.php?curid=${action.pageid}`);
  145. const message = `已更新如下页面:${(0, exports.parseAction)(action)}`;
  146. return Promise.all(this.lock.subscribers.map(subscriber => this.bot.sendTo(subscriber, message)));
  147. }
  148. });
  149. }
  150. }
  151. })).then(updateDate).then(updateOffset);
  152. })
  153. .then(() => {
  154. let timeout = this.workInterval * 1000;
  155. if (timeout < 1000)
  156. timeout = 1000;
  157. fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
  158. setTimeout(() => {
  159. this.work();
  160. }, timeout);
  161. });
  162. };
  163. this.client = new Twitter({
  164. consumer_key: opt.consumerKey,
  165. consumer_secret: opt.consumerSecret,
  166. access_token_key: opt.accessTokenKey,
  167. access_token_secret: opt.accessTokenSecret,
  168. });
  169. this.bot = opt.bot;
  170. this.publisher = new wiki_1.default(opt.lock);
  171. this.lockfile = opt.lockfile;
  172. this.lock = opt.lock;
  173. this.workInterval = opt.workInterval;
  174. this.wikiSessionCookie = opt.wikiSessionCookie;
  175. }
  176. }
  177. exports.default = default_1;