webshot.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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 { MediaItem } 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. };
  18. const logger = getLogger('webshot');
  19. class Webshot extends CallableInstance<[MediaItem[], (...args) => void, number], Promise<void>> {
  20. constructor(_wsUrl: string, _mode: number, onready?: (...args) => void) {
  21. super('webshot');
  22. onready();
  23. }
  24. private fetchMedia = (url: string): Promise<string> => new Promise<ArrayBuffer>((resolve, reject) => {
  25. logger.info(`fetching ${url}`);
  26. axios({
  27. method: 'get',
  28. url,
  29. responseType: 'arraybuffer',
  30. timeout: 150000,
  31. }).then(res => {
  32. if (res.status === 200) {
  33. logger.info(`successfully fetched ${url}`);
  34. resolve(res.data);
  35. } else {
  36. logger.error(`failed to fetch ${url}: ${res.status}`);
  37. reject();
  38. }
  39. }).catch (err => {
  40. logger.error(`failed to fetch ${url}: ${err instanceof Error ? err.message : err}`);
  41. reject();
  42. });
  43. }).then(data =>
  44. (ext => {
  45. const mediaTempFilePath = temp.path({suffix: `.${ext}`});
  46. writeFileSync(mediaTempFilePath, Buffer.from(data));
  47. const path = `file://${mediaTempFilePath}`;
  48. switch (ext) {
  49. case 'jpg':
  50. case 'png':
  51. return Message.Image(path);
  52. case 'mp4':
  53. return Message.Video(path);
  54. }
  55. logger.warn('unable to find MIME type of fetched media, failing this fetch');
  56. throw Error();
  57. })(/\/.*\.(.+?)\?/.exec(url)[1])
  58. );
  59. public webshot(
  60. mediaItems: MediaItem[],
  61. callback: (msgs: string, text: string, author: string) => void,
  62. webshotDelay: number
  63. ): Promise<void> {
  64. let promise = new Promise<void>(resolve => {
  65. resolve();
  66. });
  67. mediaItems.forEach(item => {
  68. promise = promise.then(() => {
  69. logger.info(`working on ${item.user.username}/${item.code}`);
  70. });
  71. let messageChain = '';
  72. // text processing
  73. const author = `${item.user.full_name} (@${item.user.username}):\n`;
  74. const date = `${new Date(item.taken_at * 1000)}\n`;
  75. messageChain += author + date;
  76. // fetch extra entities
  77. const type = (mediaItem): keyof typeof typeInZH =>
  78. (mediaItem as MediaItem).video_versions ? 'video' : 'photo';
  79. const fetchBestCandidate =(
  80. candidates: (Partial<typeof item.video_versions[0]> & typeof item.image_versions2.candidates[0])[],
  81. mediaType: keyof typeof typeInZH
  82. ) => {
  83. const url = candidates
  84. .sort((var1, var2) => var2.width + (var2?.type || 0) - var1.width - (var1?.type || 0))
  85. .map(variant => variant.url)[0]; // largest media
  86. const altMessage = `\n[失败的${typeInZH[mediaType].type}:${url}]`;
  87. return this.fetchMedia(url)
  88. .catch(error => {
  89. logger.warn('unable to fetch media, sending plain text instead...');
  90. return altMessage;
  91. })
  92. .then(msg => { messageChain += msg; });
  93. };
  94. promise = promise.then(() => {
  95. if (item.video_versions) {
  96. return fetchBestCandidate(item.video_versions, type(item));
  97. } else if (item.image_versions2) {
  98. return fetchBestCandidate(item.image_versions2.candidates, type(item));
  99. }
  100. });
  101. promise.then(() => {
  102. logger.info(`done working on ${item.user.username}/${item.code}, message chain:`);
  103. logger.info(JSON.stringify(Message.ellipseBase64(messageChain)));
  104. callback(messageChain, xmlEntities.decode(item.caption), author);
  105. });
  106. });
  107. return promise;
  108. }
  109. }
  110. export default Webshot;