twitter.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. '新曲一部公開|3DLIVE映像公開': '新曲',
  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 dateMatch = /(\d+\/\d+)\(.\).*(?:から|~)\s*\n/.exec(body);
  41. const date = (dateMatch ?
  42. new Date(`${new Date(tweet.created_at).getFullYear()}/${dateMatch[1]}`) :
  43. new Date(tweet.created_at))
  44. .toLocaleDateString('en-CA', { timeZone: 'Asia/Tokyo' })
  45. .replace(/-/g, '');
  46. const pageTitle = title.replace(/[【】\/\n]/g, '') + `${exports.recurringKeywords.some(keyword => new RegExp(keyword).exec(title)) ? `-${date}` : ''}`;
  47. return { title, body, pageTitle, date };
  48. };
  49. exports.processTweetBody = processTweetBody;
  50. const parseAction = (action) => {
  51. if (!action || !Object.keys(action))
  52. return '(无)';
  53. return `\n
  54. 标题:${action.title}
  55. 操作:${action.new ? '新建' : '更新'}
  56. 媒体:${action.mediafiles.map((fileName, index) => `\n ${index + 1}. ${fileName}`).join('') || '(无)'}
  57. 链接:${action.pageid ? `https://wiki.biligame.com/idolypride/index.php?curid=${action.pageid}` : '(无)'}
  58. `;
  59. };
  60. exports.parseAction = parseAction;
  61. const TWITTER_EPOCH = 1288834974657;
  62. const snowflake = (epoch) => Number.isNaN(epoch) ? undefined :
  63. utils_1.BigNumOps.lShift(String(epoch - 1 - TWITTER_EPOCH), 22);
  64. exports.snowflake = snowflake;
  65. const logger = (0, loggers_1.getLogger)('twitter');
  66. class default_1 {
  67. constructor(opt) {
  68. this.launch = () => {
  69. this.publisher.login(this.wikiSessionCookie).then(() => {
  70. setTimeout(this.work, this.workInterval * 1000);
  71. });
  72. };
  73. this.work = () => {
  74. const lock = this.lock;
  75. if (this.workInterval < 1)
  76. this.workInterval = 1;
  77. const currentFeed = 'https://twitter.com/idolypride';
  78. logger.debug(`pulling feed ${currentFeed}`);
  79. const promise = new Promise(resolve => {
  80. let config;
  81. let endpoint;
  82. const match = /https:\/\/twitter.com\/([^\/]+)/.exec(currentFeed);
  83. if (match) {
  84. config = {
  85. screen_name: match[1],
  86. exclude_replies: true,
  87. include_rts: false,
  88. tweet_mode: 'extended',
  89. };
  90. endpoint = 'statuses/user_timeline';
  91. }
  92. if (endpoint) {
  93. const offset = lock.offset;
  94. if (offset > 0)
  95. config.since_id = offset;
  96. const getMore = (lastTweets = []) => this.client.get(endpoint, config, (error, tweets) => {
  97. if (error) {
  98. if (error instanceof Array && error.length > 0 && error[0].code === 34) {
  99. logger.warn(`error on fetching tweets for ${currentFeed}: ${JSON.stringify(error)}`);
  100. lock.subscribers.forEach(subscriber => {
  101. logger.info(`sending notfound message of ${currentFeed} to ${JSON.stringify(subscriber)}`);
  102. this.bot.sendTo(subscriber, `错误:链接 ${currentFeed} 指向的用户或列表不存在。`).catch();
  103. });
  104. }
  105. else {
  106. logger.error(`unhandled error on fetching tweets for ${currentFeed}: ${JSON.stringify(error)}`);
  107. }
  108. }
  109. if (!(tweets instanceof Array) || tweets.length === 0)
  110. return resolve(lastTweets);
  111. if (offset <= 0)
  112. return resolve(lastTweets.concat(tweets));
  113. config.max_id = utils_1.BigNumOps.plus(tweets.slice(-1)[0].id_str, '-1');
  114. getMore(lastTweets.concat(tweets));
  115. });
  116. getMore();
  117. }
  118. });
  119. promise.then((tweets) => {
  120. logger.debug(`api returned ${JSON.stringify(tweets)} for feed ${currentFeed}`);
  121. const updateDate = () => lock.updatedAt = new Date().toString();
  122. if (tweets.length === 0) {
  123. updateDate();
  124. return;
  125. }
  126. const updateOffset = (offset) => lock.offset = offset;
  127. if (lock.offset === '-1') {
  128. updateOffset(tweets[0].id_str);
  129. return;
  130. }
  131. if (lock.offset === '0')
  132. tweets.splice(1);
  133. return (0, utils_1.chainPromises)(tweets.slice(0).reverse().map(tweet => () => {
  134. const match = /(.*?)\n\n(.*)/s.exec(tweet.full_text);
  135. if (match)
  136. for (const keyword in exports.keywordMap) {
  137. if (new RegExp(keyword).exec(match[1])) {
  138. const tweetUrl = `${currentFeed}/status/${tweet.id_str}`;
  139. logger.info(`working on ${tweetUrl}`);
  140. return this.publisher.post(tweet, exports.keywordMap[keyword])
  141. .then(action => {
  142. if (action.result === 'Success') {
  143. this.lock.lastActions.push(action);
  144. logger.info(`successfully posted content of ${tweetUrl} to bwiki, link:`);
  145. logger.info(`https://wiki.biligame.com/idolypride/index.php?curid=${action.pageid}`);
  146. const message = `已更新如下页面:${(0, exports.parseAction)(action)}`;
  147. return Promise.all(this.lock.subscribers.map(subscriber => this.bot.sendTo(subscriber, message))).then(() => { updateDate(); updateOffset(tweet.id_str); return true; });
  148. }
  149. });
  150. }
  151. }
  152. return Promise.resolve(false);
  153. })).then(updated => { if (updated)
  154. return; updateDate(); updateOffset(tweets[0].id_str); });
  155. })
  156. .then(() => {
  157. let timeout = this.workInterval * 1000;
  158. if (timeout < 1000)
  159. timeout = 1000;
  160. fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
  161. setTimeout(() => {
  162. this.work();
  163. }, timeout);
  164. });
  165. };
  166. this.client = new Twitter({
  167. consumer_key: opt.consumerKey,
  168. consumer_secret: opt.consumerSecret,
  169. access_token_key: opt.accessTokenKey,
  170. access_token_secret: opt.accessTokenSecret,
  171. });
  172. this.bot = opt.bot;
  173. this.publisher = new wiki_1.default(opt.lock);
  174. this.lockfile = opt.lockfile;
  175. this.lock = opt.lock;
  176. this.workInterval = opt.workInterval;
  177. this.wikiSessionCookie = opt.wikiSessionCookie;
  178. }
  179. }
  180. exports.default = default_1;