twitter.js 16 KB

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