webshot.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import { writeFileSync } from 'fs';
  2. import axios from 'axios';
  3. import * as CallableInstance from 'callable-instance';
  4. import { XmlEntities } from 'html-entities';
  5. import * as temp from 'temp';
  6. import { getLogger } from './loggers';
  7. import { Message } from './koishi';
  8. import { Fleets, FullUser, MediaEntity } from './twitter';
  9. const xmlEntities = new XmlEntities();
  10. const ZHType = (type: string) => new class extends String {
  11. public type = super.toString();
  12. public toString = () => `[${super.toString()}]`;
  13. }(type);
  14. const typeInZH = {
  15. photo: ZHType('图片'),
  16. video: ZHType('视频'),
  17. animated_gif: ZHType('GIF'),
  18. };
  19. const logger = getLogger('webshot');
  20. class Webshot extends CallableInstance<[FullUser, Fleets, (...args) => void, number], Promise<void>> {
  21. private mode: number;
  22. constructor(_wsUrl: string, mode: number, onready?: (...args) => void) {
  23. super('webshot');
  24. this.mode = mode;
  25. onready();
  26. }
  27. private fetchMedia = (url: string): Promise<string> => new Promise<ArrayBuffer>((resolve, reject) => {
  28. logger.info(`fetching ${url}`);
  29. axios({
  30. method: 'get',
  31. url,
  32. responseType: 'arraybuffer',
  33. timeout: 150000,
  34. }).then(res => {
  35. if (res.status === 200) {
  36. logger.info(`successfully fetched ${url}`);
  37. resolve(res.data);
  38. } else {
  39. logger.error(`failed to fetch ${url}: ${res.status}`);
  40. reject();
  41. }
  42. }).catch (err => {
  43. logger.error(`failed to fetch ${url}: ${err instanceof Error ? err.message : err}`);
  44. reject();
  45. });
  46. }).then(data =>
  47. (ext => {
  48. const mediaTempFilePath = temp.path({suffix: `.${ext}`});
  49. writeFileSync(mediaTempFilePath, Buffer.from(data));
  50. const path = `file://${mediaTempFilePath}`;
  51. switch (ext) {
  52. case 'jpg':
  53. case 'png':
  54. return Message.Image(path);
  55. case 'mp4':
  56. return Message.Video(path);
  57. }
  58. logger.warn('unable to find MIME type of fetched media, failing this fetch');
  59. throw Error();
  60. })(((/\?format=([a-z]+)&/.exec(url)) ?? (/.*\/.*\.([^?]+)/.exec(url)))[1])
  61. );
  62. public webshot(
  63. user: FullUser,
  64. fleets: Fleets,
  65. callback: (msgs: string, text: string) => void,
  66. webshotDelay: number
  67. ): Promise<void> {
  68. let promise = new Promise<void>(resolve => {
  69. resolve();
  70. });
  71. fleets.forEach(fleet => {
  72. promise = promise.then(() => {
  73. logger.info(`working on ${user.screen_name}/${fleet.fleet_id}`);
  74. });
  75. let messageChain = '';
  76. // text processing
  77. const author = `${user.name} (@${user.screen_name}):\n`;
  78. const date = `${new Date(fleet.created_at)}\n`;
  79. let text = author + date + fleet.media_bounding_boxes?.map(box => box.entity.value as string).join('\n') ?? '';
  80. messageChain += author + date;
  81. // fetch extra entities
  82. // tslint:disable-next-line: curly
  83. // eslint-disable-next-line curly
  84. if (1 - this.mode % 2) promise = promise.then(() => {
  85. const media: MediaEntity & {media_info?: MediaEntity} = fleet.media_entity;
  86. let url: string;
  87. if (fleet.media_key.media_category === 'TWEET_IMAGE') {
  88. media.type = 'photo';
  89. url = media.media_url_https.replace(/\.([a-z]+)$/, '?format=$1') + '&name=orig';
  90. } else {
  91. media.type = fleet.media_key.media_category === 'TWEET_VIDEO' ? 'video' : 'animated_gif';
  92. media.video_info = media.media_info.video_info;
  93. text += `[${typeInZH[media.type as keyof typeof typeInZH].type}]`;
  94. url = (media.video_info.variants as (
  95. typeof media.video_info.variants[0] & {bit_rate: number} // bitrate -> bit_rate
  96. )[])
  97. .filter(variant => variant.bit_rate !== undefined)
  98. .sort((var1, var2) => var2.bit_rate - var1.bit_rate)
  99. .map(variant => variant.url)[0]; // largest video
  100. }
  101. const altMessage = `\n[失败的${typeInZH[media.type as keyof typeof typeInZH].type}:${url}]`;
  102. return this.fetchMedia(url)
  103. .catch(error => {
  104. logger.warn('unable to fetch media, sending plain text instead...');
  105. return altMessage;
  106. })
  107. .then(msg => { messageChain += msg; });
  108. });
  109. promise.then(() => {
  110. logger.info(`done working on ${user.screen_name}/${fleet.fleet_id}, message chain:`);
  111. logger.info(JSON.stringify(messageChain));
  112. callback(messageChain, xmlEntities.decode(text));
  113. });
  114. });
  115. return promise;
  116. }
  117. }
  118. export default Webshot;