webshot.ts 4.0 KB

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