webshot.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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. })(/\/.+\.(?:.*?(?<=[?&])stp=dst-(jpg)|(.+?)\?)/.exec(url).filter(g => g)[1])
  58. );
  59. public fetchBestCandidate = ({image_versions2, video_versions}: MediaItem) => {
  60. const candidates: (
  61. Partial<typeof video_versions[0]> & typeof image_versions2.candidates[0]
  62. )[] = video_versions || image_versions2.candidates;
  63. const url = candidates
  64. .sort((var1, var2) => var2.width + (var2?.type || 0) - var1.width - (var1?.type || 0))
  65. .map(variant => variant.url)[0]; // largest media
  66. const altMessage = `\n[失败的${typeInZH[video_versions ? 'video' : 'photo'].type}:${url}]`;
  67. return this.fetchMedia(url)
  68. .catch(error => {
  69. logger.warn('unable to fetch media, sending plain text instead...');
  70. return altMessage;
  71. });
  72. };
  73. public webshot(
  74. mediaItems: MediaItem[],
  75. callback: (msgs: string, text: string, author: string) => void,
  76. webshotDelay: number
  77. ): Promise<void> {
  78. const promises = mediaItems.map(item => {
  79. let promise = Promise.resolve();
  80. logger.info(`working on ${item.user.username}/${item.code}`);
  81. let messageChain = '';
  82. // text processing
  83. const author = `${item.user.full_name} (@${item.user.username}):\n`;
  84. const date = `${new Date(item.taken_at * 1000)}\n`;
  85. messageChain += author + date;
  86. // fetch extra entities
  87. promise = promise.then(() => this.fetchBestCandidate(item))
  88. .then(msg => { messageChain += msg; });
  89. return promise.then(() => {
  90. logger.info(`done working on ${item.user.username}/${item.code}, message chain:`);
  91. logger.info(JSON.stringify(Message.ellipseBase64(messageChain)));
  92. callback(messageChain, xmlEntities.decode(item.caption), author);
  93. });
  94. });
  95. return Promise.all(promises).then();
  96. }
  97. }
  98. export default Webshot;