twitter.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. "use strict";
  2. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  4. return new (P || (P = Promise))(function (resolve, reject) {
  5. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  6. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  7. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  8. step((generator = generator.apply(thisArg, _arguments || [])).next());
  9. });
  10. };
  11. Object.defineProperty(exports, "__esModule", { value: true });
  12. exports.sendAllStories = exports.ScreenNameNormalizer = exports.SessionManager = exports.parseLink = exports.linkBuilder = void 0;
  13. const fs = require("fs");
  14. const path = require("path");
  15. const instagram_private_api_1 = require("instagram-private-api");
  16. const loggers_1 = require("./loggers");
  17. const koishi_1 = require("./koishi");
  18. const utils_1 = require("./utils");
  19. const webshot_1 = require("./webshot");
  20. const parseLink = (link) => {
  21. let match = /instagram\.com\/stories\/([^\/?#]+)\/(\d+)/.exec(link);
  22. if (match)
  23. return { userName: ScreenNameNormalizer.normalize(match[1]).split(':')[0], storyId: match[2] };
  24. match =
  25. /instagram\.com\/([^\/?#]+)/.exec(link) ||
  26. /^([^\/?#]+)$/.exec(link);
  27. if (match)
  28. return { userName: ScreenNameNormalizer.normalize(match[1]).split(':')[0] };
  29. return;
  30. };
  31. exports.parseLink = parseLink;
  32. const linkBuilder = (config) => {
  33. if (!config.userName)
  34. return;
  35. if (!config.storyId)
  36. return `https://www.instagram.com/${config.userName}/`;
  37. return `https://www.instagram.com/stories/${config.userName}/${config.storyId}/`;
  38. };
  39. exports.linkBuilder = linkBuilder;
  40. class SessionManager {
  41. constructor(client, file, credentials) {
  42. this.init = () => {
  43. this.ig.state.generateDevice(this.username);
  44. this.ig.request.end$.subscribe(() => { this.save(); });
  45. const filePath = path.resolve(this.lockfile);
  46. if (fs.existsSync(filePath)) {
  47. try {
  48. const serialized = JSON.parse(fs.readFileSync(filePath, 'utf8'));
  49. return this.ig.state.deserialize(serialized).then(() => {
  50. logger.info(`successfully loaded client session cookies for user ${this.username}`);
  51. });
  52. }
  53. catch (err) {
  54. logger.error(`failed to load client session cookies from file ${this.lockfile}: `, err);
  55. return Promise.resolve();
  56. }
  57. }
  58. else
  59. return this.login();
  60. };
  61. this.login = () => this.ig.simulate.preLoginFlow()
  62. .then(() => this.ig.account.login(this.username, this.password))
  63. .then(() => new Promise(resolve => {
  64. logger.info(`successfully logged in as ${this.username}`);
  65. process.nextTick(() => resolve(this.ig.simulate.postLoginFlow()));
  66. }));
  67. this.save = () => this.ig.state.serialize()
  68. .then((serialized) => {
  69. delete serialized.constants;
  70. return fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(serialized, null, 2), 'utf-8');
  71. });
  72. this.ig = client;
  73. this.lockfile = file;
  74. [this.username, this.password] = credentials;
  75. }
  76. }
  77. exports.SessionManager = SessionManager;
  78. class ScreenNameNormalizer {
  79. static normalizeLive(username) {
  80. return __awaiter(this, void 0, void 0, function* () {
  81. if (this._queryUser) {
  82. return yield this._queryUser(username)
  83. .catch((err) => {
  84. if (!(err instanceof instagram_private_api_1.IgExactUserNotFoundError)) {
  85. logger.warn(`error looking up user: ${err.message}`);
  86. return `${username}:`;
  87. }
  88. return null;
  89. });
  90. }
  91. return this.normalize(username);
  92. });
  93. }
  94. }
  95. exports.ScreenNameNormalizer = ScreenNameNormalizer;
  96. ScreenNameNormalizer.normalize = (username) => `${username.toLowerCase().replace(/^@/, '')}:`;
  97. let sendAllStories = (segmentId, receiver) => {
  98. throw Error();
  99. };
  100. exports.sendAllStories = sendAllStories;
  101. const logger = loggers_1.getLogger('instagram');
  102. const maxTrials = 3;
  103. const retryInterval = 1500;
  104. const ordinal = (n) => {
  105. switch ((Math.trunc(n / 10) % 10 === 1) ? 0 : n % 10) {
  106. case 1:
  107. return `${n}st`;
  108. case 2:
  109. return `${n}nd`;
  110. case 3:
  111. return `${n}rd`;
  112. default:
  113. return `${n}th`;
  114. }
  115. };
  116. const retryOnError = (doWork, onRetry) => new Promise(resolve => {
  117. const retry = (reason, count) => {
  118. setTimeout(() => {
  119. let terminate = false;
  120. onRetry(reason, count, defaultValue => { terminate = true; resolve(defaultValue); });
  121. if (!terminate)
  122. doWork().then(resolve).catch(error => retry(error, count + 1));
  123. }, retryInterval);
  124. };
  125. doWork().then(resolve).catch(error => retry(error, 1));
  126. });
  127. class default_1 {
  128. constructor(opt) {
  129. this.launch = () => {
  130. this.webshot = new webshot_1.default(this.wsUrl, this.mode, () => {
  131. setTimeout(this.workForAll, this.workInterval * 1000);
  132. setTimeout(() => {
  133. this.work();
  134. setInterval(this.workForAll, this.workInterval * 10000);
  135. }, this.workInterval * 1200);
  136. });
  137. };
  138. this.queryUser = (rawUserName) => {
  139. const username = ScreenNameNormalizer.normalize(rawUserName).split(':')[0];
  140. if (username in this.cache) {
  141. return Promise.resolve(`${username}:${this.cache[username].user.pk}`);
  142. }
  143. return this.client.user.searchExact(username)
  144. .then(user => {
  145. this.cache[user.username] = { user, stories: {} };
  146. return `${user.username}:${user.pk}`;
  147. });
  148. };
  149. this.workOnMedia = (mediaItems, sendMedia) => this.webshot(mediaItems, sendMedia, this.webshotDelay);
  150. this.sendStories = (source, ...to) => (msg, text, author) => {
  151. to.forEach(subscriber => {
  152. logger.info(`pushing data${source ? ` of ${koishi_1.Message.ellipseBase64(source)}` : ''} to ${JSON.stringify(subscriber)}`);
  153. retryOnError(() => this.bot.sendTo(subscriber, msg), (_, count, terminate) => {
  154. if (count <= maxTrials) {
  155. logger.warn(`retry sending to ${subscriber.chatID} for the ${ordinal(count)} time...`);
  156. }
  157. else {
  158. logger.warn(`${count - 1} consecutive failures while sending` +
  159. 'message chain, trying plain text instead...');
  160. terminate(this.bot.sendTo(subscriber, author + text));
  161. }
  162. });
  163. });
  164. };
  165. this.cache = {};
  166. this.workForAll = () => {
  167. const idToUserMap = {};
  168. Promise.all(Object.entries(this.lock.threads).map(entry => {
  169. const id = entry[1].id;
  170. const userName = parseLink(entry[0]).userName;
  171. logger.debug(`preparing to add user @${userName} to next pull task...`);
  172. if (userName in this.cache)
  173. return Promise.resolve(idToUserMap[id] = this.cache[userName].user);
  174. return this.client.user.info(id).then(user => {
  175. logger.debug(`initialized cache item for user ${user.full_name} (@${userName})`);
  176. this.cache[userName] = { user, stories: {} };
  177. return idToUserMap[id] = user;
  178. });
  179. }))
  180. .then(() => {
  181. logger.debug(`pulling stories for users: ${Object.values(idToUserMap).map(user => user.username)}`);
  182. this.client.feed.reelsMedia({
  183. userIds: Object.keys(idToUserMap),
  184. }).items()
  185. .then(storyItems => storyItems.forEach(item => {
  186. if (!(item.pk in this.cache[idToUserMap[item.user.pk].username].stories)) {
  187. this.cache[idToUserMap[item.user.pk].username].stories[item.pk] = item;
  188. }
  189. }))
  190. .catch((error) => {
  191. if (error instanceof instagram_private_api_1.IgNetworkError) {
  192. logger.warn(`error on fetching stories for all: ${JSON.stringify(error.cause)}`);
  193. }
  194. else if (error instanceof instagram_private_api_1.IgLoginRequiredError) {
  195. logger.warn('login required, logging in again...');
  196. this.session.login().then(this.workForAll);
  197. }
  198. else {
  199. logger.error(`unhandled error on fetching media for all: ${error}`);
  200. }
  201. });
  202. });
  203. };
  204. this.work = () => {
  205. const lock = this.lock;
  206. logger.debug(`current cache: ${JSON.stringify(this.cache)}`);
  207. if (this.workInterval < 1)
  208. this.workInterval = 1;
  209. if (lock.feed.length === 0) {
  210. setTimeout(this.work, this.workInterval * 1000);
  211. return;
  212. }
  213. if (lock.workon >= lock.feed.length)
  214. lock.workon = 0;
  215. if (!lock.threads[lock.feed[lock.workon]] ||
  216. !lock.threads[lock.feed[lock.workon]].subscribers ||
  217. lock.threads[lock.feed[lock.workon]].subscribers.length === 0) {
  218. logger.warn(`nobody subscribes thread ${lock.feed[lock.workon]}, removing from feed`);
  219. delete lock.threads[lock.feed[lock.workon]];
  220. lock.feed.splice(lock.workon, 1);
  221. fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
  222. this.work();
  223. return;
  224. }
  225. const currentFeed = lock.feed[lock.workon];
  226. logger.debug(`searching for new items from ${currentFeed} in cache`);
  227. const promise = new Promise(resolve => {
  228. const match = /https:\/\/www\.instagram\.com\/([^\/]+)/.exec(currentFeed);
  229. if (match) {
  230. const cachedFeed = this.cache[match[1]];
  231. if (!cachedFeed) {
  232. setTimeout(this.work, this.workInterval * 1000);
  233. resolve([]);
  234. }
  235. const newer = (item) => utils_1.BigNumOps.compare(item.pk, lock.threads[currentFeed].offset) > 0;
  236. resolve(Object.values(cachedFeed.stories)
  237. .filter(newer)
  238. .map(story => (Object.assign(Object.assign({}, story), { user: cachedFeed.user })))
  239. .sort((i1, i2) => utils_1.BigNumOps.compare(i2.pk, i1.pk)));
  240. }
  241. });
  242. promise.then((mediaItems) => {
  243. const currentThread = lock.threads[currentFeed];
  244. const updateDate = () => currentThread.updatedAt = new Date().toString();
  245. if (!mediaItems || mediaItems.length === 0) {
  246. updateDate();
  247. return;
  248. }
  249. const topOfFeed = mediaItems[0].pk;
  250. const updateOffset = () => currentThread.offset = topOfFeed;
  251. if (currentThread.offset === '-1') {
  252. updateOffset();
  253. return;
  254. }
  255. if (currentThread.offset === '0')
  256. mediaItems.splice(1);
  257. return this.workOnMedia(mediaItems, this.sendStories(`thread ${currentFeed}`, ...currentThread.subscribers))
  258. .then(updateDate).then(updateOffset);
  259. })
  260. .then(() => {
  261. lock.workon++;
  262. let timeout = this.workInterval * 1000 / lock.feed.length;
  263. if (timeout < 1000)
  264. timeout = 1000;
  265. fs.writeFileSync(path.resolve(this.lockfile), JSON.stringify(lock));
  266. setTimeout(() => {
  267. this.work();
  268. }, timeout);
  269. });
  270. };
  271. this.client = new instagram_private_api_1.IgApiClient();
  272. this.session = new SessionManager(this.client, opt.sessionLockfile, opt.credentials);
  273. this.lockfile = opt.lockfile;
  274. this.lock = opt.lock;
  275. this.workInterval = opt.workInterval;
  276. this.bot = opt.bot;
  277. this.webshotDelay = opt.webshotDelay;
  278. this.mode = opt.mode;
  279. this.wsUrl = opt.wsUrl;
  280. ScreenNameNormalizer._queryUser = this.queryUser;
  281. exports.sendAllStories = (rawUserName, receiver) => {
  282. const sender = this.sendStories(`instagram stories for ${rawUserName}`, receiver);
  283. this.queryUser(rawUserName)
  284. .then(userNameId => {
  285. const [userName, userId] = userNameId.split(':');
  286. if (userName in this.cache && Object.keys(this.cache[userName].stories).length > 0) {
  287. return Promise.resolve(Object.values(this.cache[userName].stories)
  288. .map(story => (Object.assign(Object.assign({}, story), { user: this.cache[userName].user })))
  289. .sort((i1, i2) => utils_1.BigNumOps.compare(i2.pk, i1.pk)));
  290. }
  291. return this.client.feed.reelsMedia({ userIds: [userId] }).items()
  292. .then(storyItems => {
  293. storyItems = storyItems.map(story => (Object.assign(Object.assign({}, story), { user: this.cache[userName].user })));
  294. storyItems.forEach(item => {
  295. if (!(item.pk in this.cache[userName].stories)) {
  296. this.cache[userName].stories[item.pk] = item;
  297. }
  298. });
  299. if (storyItems.length === 0)
  300. this.bot.sendTo(receiver, `当前用户 (@${userName}) 没有可用的推特故事。`);
  301. return storyItems;
  302. });
  303. })
  304. .then(storyItems => this.workOnMedia(storyItems, sender))
  305. .catch((error) => {
  306. if (error instanceof instagram_private_api_1.IgNetworkError) {
  307. logger.warn(`error on fetching stories for ${rawUserName}: ${JSON.stringify(error.cause)}`);
  308. this.bot.sendTo(receiver, `获取 Fleets 时出现错误:原因: ${error.cause}`);
  309. }
  310. else if (error instanceof instagram_private_api_1.IgLoginRequiredError) {
  311. logger.warn('login required, logging in again...');
  312. this.session.login().then(() => exports.sendAllStories(rawUserName, receiver));
  313. }
  314. else {
  315. logger.error(`unhandled error on fetching media for ${rawUserName}: ${error}`);
  316. this.bot.sendTo(receiver, `获取 Fleets 时发生未知错误: ${error}`);
  317. }
  318. });
  319. };
  320. }
  321. }
  322. exports.default = default_1;