twitter.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const fs = require("fs");
  4. const path = require("path");
  5. const Twitter = require("twitter");
  6. const loggers_1 = require("./loggers");
  7. const mirai_1 = require("./mirai");
  8. const webshot_1 = require("./webshot");
  9. const logger = loggers_1.getLogger('twitter');
  10. class default_1 {
  11. constructor(opt) {
  12. this.launch = () => {
  13. this.webshot = new webshot_1.default(this.webshotOutDir, this.mode, () => setTimeout(this.work, this.workInterval * 1000));
  14. };
  15. this.work = () => {
  16. const lock = this.lock;
  17. if (this.workInterval < 1)
  18. this.workInterval = 1;
  19. if (lock.feed.length === 0) {
  20. setTimeout(() => {
  21. this.work();
  22. }, this.workInterval * 1000);
  23. return;
  24. }
  25. if (lock.workon >= lock.feed.length)
  26. lock.workon = 0;
  27. if (!lock.threads[lock.feed[lock.workon]] ||
  28. !lock.threads[lock.feed[lock.workon]].subscribers ||
  29. lock.threads[lock.feed[lock.workon]].subscribers.length === 0) {
  30. logger.warn(`nobody subscribes thread ${lock.feed[lock.workon]}, removing from feed`);
  31. delete lock.threads[lock.feed[lock.workon]];
  32. lock.feed.splice(lock.workon, 1);
  33. fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
  34. this.work();
  35. return;
  36. }
  37. const currentFeed = lock.feed[lock.workon];
  38. logger.debug(`pulling feed ${currentFeed}`);
  39. const promise = new Promise(resolve => {
  40. let match = currentFeed.match(/https:\/\/twitter.com\/([^\/]+)\/lists\/([^\/]+)/);
  41. let config;
  42. let endpoint;
  43. if (match) {
  44. config = {
  45. owner_screen_name: match[1],
  46. slug: match[2],
  47. tweet_mode: 'extended',
  48. };
  49. endpoint = 'lists/statuses';
  50. }
  51. else {
  52. match = currentFeed.match(/https:\/\/twitter.com\/([^\/]+)/);
  53. if (match) {
  54. config = {
  55. screen_name: match[1],
  56. exclude_replies: false,
  57. tweet_mode: 'extended',
  58. };
  59. endpoint = 'statuses/user_timeline';
  60. }
  61. }
  62. if (endpoint) {
  63. const offset = lock.threads[currentFeed].offset;
  64. if (offset > 0)
  65. config.since_id = offset;
  66. if (offset < -1)
  67. config.max_id = offset.slice(1);
  68. this.client.get(endpoint, config, (error, tweets, response) => {
  69. if (error) {
  70. if (error instanceof Array && error.length > 0 && error[0].code === 34) {
  71. logger.warn(`error on fetching tweets for ${currentFeed}: ${JSON.stringify(error)}`);
  72. lock.threads[currentFeed].subscribers.forEach(subscriber => {
  73. logger.info(`sending notfound message of ${currentFeed} to ${JSON.stringify(subscriber)}`);
  74. this.bot.sendTo(subscriber, `链接 ${currentFeed} 指向的用户或列表不存在,请退订。`).catch();
  75. });
  76. }
  77. else {
  78. logger.error(`unhandled error on fetching tweets for ${currentFeed}: ${JSON.stringify(error)}`);
  79. }
  80. resolve();
  81. }
  82. else
  83. resolve(tweets);
  84. });
  85. }
  86. });
  87. promise.then((tweets) => {
  88. logger.debug(`api returned ${JSON.stringify(tweets)} for feed ${currentFeed}`);
  89. const currentThread = lock.threads[currentFeed];
  90. const updateDate = () => currentThread.updatedAt = new Date().toString();
  91. if (!tweets || tweets.length === 0) {
  92. updateDate();
  93. return;
  94. }
  95. const topOfFeed = tweets[0].id_str;
  96. logger.info(`current offset: ${currentThread.offset}, current top of feed: ${topOfFeed}`);
  97. const bottomOfFeed = tweets[tweets.length - 1].id_str;
  98. const setOffset = (offset) => currentThread.offset = offset;
  99. const updateOffset = () => setOffset(topOfFeed);
  100. tweets = tweets.filter(twi => !twi.retweeted_status && twi.extended_entities);
  101. logger.info(`found ${tweets.length} tweets with extended entities`);
  102. if (currentThread.offset === '-1') {
  103. updateOffset();
  104. return;
  105. }
  106. if (currentThread.offset <= 0) {
  107. if (tweets.length === 0) {
  108. setOffset('-' + bottomOfFeed);
  109. lock.workon--;
  110. return;
  111. }
  112. tweets.splice(1);
  113. }
  114. if (tweets.length === 0) {
  115. updateDate();
  116. updateOffset();
  117. return;
  118. }
  119. const maxCount = 3;
  120. let sendTimeout = 10000;
  121. const retryTimeout = 1500;
  122. const ordinal = (n) => {
  123. switch ((~~(n / 10) % 10 === 1) ? 0 : n % 10) {
  124. case 1:
  125. return `${n}st`;
  126. case 2:
  127. return `${n}nd`;
  128. case 3:
  129. return `${n}rd`;
  130. default:
  131. return `${n}th`;
  132. }
  133. };
  134. const sendTweets = (msg, text, author) => {
  135. currentThread.subscribers.forEach(subscriber => {
  136. logger.info(`pushing data of thread ${currentFeed} to ${JSON.stringify(subscriber)}`);
  137. const retry = (reason, count) => {
  138. if (count <= maxCount)
  139. sendTimeout *= (count + 2) / (count + 1);
  140. setTimeout(() => {
  141. msg.forEach((message, pos) => {
  142. if (count > maxCount && message.type === 'Image') {
  143. if (pos === 0) {
  144. logger.warn(`${count - 1} consecutive failures sending webshot, trying plain text instead...`);
  145. msg[pos] = mirai_1.Message.Plain(author + text);
  146. }
  147. else {
  148. msg[pos] = mirai_1.Message.Plain(`[失败的图片:${message.path}]`);
  149. }
  150. }
  151. });
  152. logger.warn(`retry sending to ${subscriber.chatID} for the ${ordinal(count)} time...`);
  153. this.bot.sendTo(subscriber, msg, sendTimeout).catch(error => retry(error, count + 1));
  154. }, retryTimeout);
  155. };
  156. this.bot.sendTo(subscriber, msg, sendTimeout).catch(error => retry(error, 1));
  157. });
  158. };
  159. return this.webshot(tweets, sendTweets, this.webshotDelay).then(updateDate).then(updateOffset);
  160. })
  161. .then(() => {
  162. lock.workon++;
  163. let timeout = this.workInterval * 1000 / lock.feed.length;
  164. if (timeout < 1000)
  165. timeout = 1000;
  166. fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
  167. setTimeout(() => {
  168. this.work();
  169. }, timeout);
  170. });
  171. };
  172. this.client = new Twitter({
  173. consumer_key: opt.consumer_key,
  174. consumer_secret: opt.consumer_secret,
  175. access_token_key: opt.access_token_key,
  176. access_token_secret: opt.access_token_secret,
  177. });
  178. this.lockfile = opt.lockfile;
  179. this.lock = opt.lock;
  180. this.workInterval = opt.workInterval;
  181. this.bot = opt.bot;
  182. this.webshotDelay = opt.webshotDelay;
  183. this.webshotOutDir = opt.webshotOutDir;
  184. this.mode = opt.mode;
  185. }
  186. }
  187. exports.default = default_1;