Utils.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284
  1. const path = require('path')
  2. const mime = require('mime-types')
  3. const struct = require('python-struct')
  4. const { MarkdownParser, HTMLParser } = require('./extensions')
  5. const { types } = require('./tl')
  6. const USERNAME_RE = new RegExp('@|(?:https?:\\/\\/)?(?:www\\.)?' +
  7. '(?:telegram\\.(?:me|dog)|t\\.me)\\/(@|joinchat\\/)?')
  8. const TG_JOIN_RE = new RegExp('tg:\\/\\/(join)\\?invite=')
  9. const VALID_USERNAME_RE = new RegExp('^([a-z]((?!__)[\\w\\d]){3,30}[a-z\\d]|gif|vid|' +
  10. 'pic|bing|wiki|imdb|bold|vote|like|coub)$')
  11. function FileInfo(dcId, location, size) {
  12. this.dcId = dcId
  13. this.location = location
  14. this.size = size
  15. }
  16. /**
  17. Turns the given iterable into chunks of the specified size,
  18. which is 100 by default since that's what Telegram uses the most.
  19. * @param iter
  20. * @param size
  21. */
  22. function* chunk(iter, size = 100) {
  23. let items = []
  24. let index = 0
  25. for (const item of iter) {
  26. items[index++] = item
  27. if (index === size) {
  28. yield items
  29. items = []
  30. index = 0
  31. }
  32. }
  33. if (index) {
  34. yield items
  35. }
  36. }
  37. /**
  38. Gets the display name for the given User, Chat,
  39. or Channel. Otherwise returns an empty string.
  40. */
  41. function getDisplayName(entity) {
  42. if (entity instanceof types.User) {
  43. if (entity.lastName && entity.firstName) {
  44. return `${entity.firstName} ${entity.lastName}`
  45. } else if (entity.firstName) {
  46. return entity.firstName
  47. } else if (entity.lastName) {
  48. return entity.lastName
  49. }
  50. }
  51. if (entity instanceof types.Chat || entity instanceof types.Channel) {
  52. return entity.title
  53. }
  54. return ''
  55. }
  56. /**
  57. Gets the corresponding extension for any Telegram media.
  58. */
  59. function getExtension(media) {
  60. // Photos are always compressed as .jpg by Telegram
  61. try {
  62. getInputPhoto(media)
  63. return '.jpg'
  64. } catch (err) {
  65. if ((media instanceof types.UserProfilePhoto) ||
  66. (media instanceof types.ChatPhoto)) {
  67. return '.jpg'
  68. }
  69. }
  70. // Documents will come with a mime type
  71. if (media instanceof types.MessageMediaDocument) {
  72. media = media.document
  73. }
  74. if ((media instanceof types.Document) ||
  75. (media instanceof types.WebDocument) ||
  76. (media instanceof types.WebDocumentNoProxy)) {
  77. if (media.mimeType === 'application/octet-stream') {
  78. // Octet stream are just bytes, which have no default extension
  79. return ''
  80. } else {
  81. const ext = mime.extension(media.mimeType)
  82. return ext ? '.' + ext : ''
  83. }
  84. }
  85. return ''
  86. }
  87. function _raiseCastFail(entity, target) {
  88. throw new Error(`Cannot cast ${entity.constructor.name} to any kind of ${target}`)
  89. }
  90. /**
  91. Gets the input peer for the given "entity" (user, chat or channel).
  92. A ``TypeError`` is raised if the given entity isn't a supported type
  93. or if ``check_hash is True`` but the entity's ``access_hash is None``
  94. *or* the entity contains ``min`` information. In this case, the hash
  95. cannot be used for general purposes, and thus is not returned to avoid
  96. any issues which can derive from invalid access hashes.
  97. Note that ``check_hash`` **is ignored** if an input peer is already
  98. passed since in that case we assume the user knows what they're doing.
  99. This is key to getting entities by explicitly passing ``hash = 0``.
  100. * @param entity
  101. * @param allowSelf
  102. * @param checkHash
  103. */
  104. function getInputPeer(entity, allowSelf = true, checkHash = true) {
  105. if (entity.SUBCLASS_OF_ID === undefined) {
  106. // e.g. custom.Dialog (can't cyclic import).
  107. if (allowSelf && 'inputEntity' in entity) {
  108. return entity.inputEntity
  109. } else if ('entity' in entity) {
  110. return getInputPeer(entity.entity)
  111. } else {
  112. _raiseCastFail(entity, 'InputPeer')
  113. }
  114. }
  115. if (entity.SUBCLASS_OF_ID === 0xc91c90b6) { // crc32(b'InputPeer')
  116. return entity
  117. }
  118. if (entity instanceof types.User) {
  119. if (entity.isSelf && allowSelf) {
  120. return new types.InputPeerSelf()
  121. } else if ((entity.accessHash !== undefined && !entity.min) || !checkHash) {
  122. return new types.InputPeerUser({
  123. userId: entity.id,
  124. accessHash: entity.accessHash,
  125. })
  126. } else {
  127. throw new Error('User without access_hash or min info cannot be input')
  128. }
  129. }
  130. if (entity instanceof types.Chat || entity instanceof types.ChatEmpty ||
  131. entity instanceof types.ChatForbidden) {
  132. return new types.InputPeerChat({
  133. chatId: entity.id,
  134. })
  135. }
  136. if (entity instanceof types.Channel) {
  137. if ((entity.accessHash !== undefined && !entity.min) || !checkHash) {
  138. return new types.InputPeerChannel({
  139. channelId: entity.id,
  140. accessHash: entity.accessHash,
  141. })
  142. } else {
  143. throw new TypeError('Channel without access_hash or min info cannot be input')
  144. }
  145. }
  146. if (entity instanceof types.ChannelForbidden) {
  147. // "channelForbidden are never min", and since their hash is
  148. // also not optional, we assume that this truly is the case.
  149. return new types.InputPeerChannel({
  150. channelId: entity.id,
  151. accessHash: entity.accessHash,
  152. })
  153. }
  154. if (entity instanceof types.InputUser) {
  155. return new types.InputPeerUser({
  156. userId: entity.userId,
  157. accessHash: entity.accessHash,
  158. })
  159. }
  160. if (entity instanceof types.InputChannel) {
  161. return new types.InputPeerChannel({
  162. channelId: entity.channelId,
  163. accessHash: entity.accessHash,
  164. })
  165. }
  166. if (entity instanceof types.UserEmpty) {
  167. return new types.InputPeerEmpty()
  168. }
  169. if (entity instanceof types.UserFull) {
  170. return getInputPeer(entity.user)
  171. }
  172. if (entity instanceof types.ChatFull) {
  173. return new types.InputPeerChat({
  174. chatId: entity.id,
  175. })
  176. }
  177. if (entity instanceof types.PeerChat) {
  178. return new types.InputPeerChat(entity.chat_id)
  179. }
  180. _raiseCastFail(entity, 'InputPeer')
  181. }
  182. /**
  183. Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone.
  184. .. important::
  185. This method does not validate for invalid general-purpose access
  186. hashes, unlike `get_input_peer`. Consider using instead:
  187. ``get_input_channel(get_input_peer(channel))``.
  188. * @param entity
  189. * @returns {InputChannel|*}
  190. */
  191. function getInputChannel(entity) {
  192. if (entity.SUBCLASS_OF_ID === undefined) {
  193. _raiseCastFail(entity, 'InputChannel')
  194. }
  195. if (entity.SUBCLASS_OF_ID === 0x40f202fd) { // crc32(b'InputChannel')
  196. return entity
  197. }
  198. if (entity instanceof types.Channel || entity instanceof types.ChannelForbidden) {
  199. return new types.InputChannel({
  200. channelId: entity.id,
  201. accessHash: entity.accessHash || 0,
  202. })
  203. }
  204. if (entity instanceof types.InputPeerChannel) {
  205. return new types.InputChannel({
  206. channelId: entity.channelId,
  207. accessHash: entity.accessHash,
  208. })
  209. }
  210. _raiseCastFail(entity, 'InputChannel')
  211. }
  212. /**
  213. * Adds the JPG header and footer to a stripped image.
  214. Ported from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225
  215. * @param stripped{Buffer}
  216. * @returns {Buffer}
  217. */
  218. function strippedPhotoToJpg(stripped) {
  219. // Note: Changes here should update _stripped_real_length
  220. if (stripped.length < 3 || stripped[0] !== 1) {
  221. return stripped
  222. }
  223. const header = Buffer.from('ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e19282321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc00011080000000003012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffda000c03010002110311003f00', 'hex')
  224. const footer = Buffer.from('ffd9', 'hex')
  225. header[164] = stripped[1]
  226. header[166] = stripped[2]
  227. return Buffer.concat([header, stripped.slice(3), footer])
  228. }
  229. /**
  230. Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone.
  231. .. important::
  232. This method does not validate for invalid general-purpose access
  233. hashes, unlike `get_input_peer`. Consider using instead:
  234. ``get_input_channel(get_input_peer(channel))``.
  235. * @param entity
  236. */
  237. function getInputUser(entity) {
  238. if (entity.SUBCLASS_OF_ID === undefined) {
  239. _raiseCastFail(entity, 'InputUser')
  240. }
  241. if (entity.SUBCLASS_OF_ID === 0xe669bf46) { // crc32(b'InputUser')
  242. return entity
  243. }
  244. if (entity instanceof types.User) {
  245. if (entity.isSelf) {
  246. return new types.InputPeerSelf()
  247. } else {
  248. return new types.InputUser({
  249. userId: entity.id,
  250. accessHash: entity.accessHash || 0,
  251. })
  252. }
  253. }
  254. if (entity instanceof types.InputPeerSelf) {
  255. return new types.InputPeerSelf()
  256. }
  257. if (entity instanceof types.UserEmpty || entity instanceof types.InputPeerEmpty) {
  258. return new types.InputUserEmpty()
  259. }
  260. if (entity instanceof types.UserFull) {
  261. return getInputUser(entity.user)
  262. }
  263. if (entity instanceof types.InputPeerUser) {
  264. return new types.InputUser({
  265. userId: entity.userId,
  266. accessHash: entity.accessHash,
  267. })
  268. }
  269. _raiseCastFail(entity, 'InputUser')
  270. }
  271. function getInputLocation(location) {
  272. try {
  273. if (!location.SUBCLASS_OF_ID) {
  274. throw new Error()
  275. }
  276. if (location.SUBCLASS_OF_ID === 0x1523d462) {
  277. return {
  278. dcId: null,
  279. inputLocation: location,
  280. }
  281. }
  282. } catch (e) {
  283. _raiseCastFail(location, 'InputFileLocation')
  284. }
  285. if (location instanceof types.Message) {
  286. location = location.media
  287. }
  288. if (location instanceof types.MessageMediaDocument) {
  289. location = location.document
  290. } else if (location instanceof types.MessageMediaPhoto) {
  291. location = location.photo
  292. }
  293. if (location instanceof types.Document) {
  294. return {
  295. dcId: location.dcId,
  296. inputLocation: new types.InputDocumentFileLocation({
  297. id: location.id,
  298. accessHash: location.accessHash,
  299. fileReference: location.fileReference,
  300. thumbSize: '', // Presumably to download one of its thumbnails
  301. }),
  302. }
  303. } else if (location instanceof types.Photo) {
  304. return {
  305. dcId: location.dcId,
  306. inputLocation: new types.InputPhotoFileLocation({
  307. id: location.id,
  308. accessHash: location.accessHash,
  309. fileReference: location.fileReference,
  310. thumbSize: location.sizes[location.sizes.length - 1].type,
  311. }),
  312. }
  313. }
  314. if (location instanceof types.FileLocationToBeDeprecated) {
  315. throw new Error('Unavailable location cannot be used as input')
  316. }
  317. _raiseCastFail(location, 'InputFileLocation')
  318. }
  319. /**
  320. Similar to :meth:`get_input_peer`, but for dialogs
  321. * @param dialog
  322. */
  323. function getInputDialog(dialog) {
  324. try {
  325. if (dialog.SUBCLASS_OF_ID === 0xa21c9795) { // crc32(b'InputDialogPeer')
  326. return dialog
  327. }
  328. if (dialog.SUBCLASS_OF_ID === 0xc91c90b6) { // crc32(b'InputPeer')
  329. return new types.InputDialogPeer({
  330. peer: dialog,
  331. })
  332. }
  333. } catch (e) {
  334. _raiseCastFail(dialog, 'InputDialogPeer')
  335. }
  336. try {
  337. return new types.InputDialogPeer(getInputPeer(dialog))
  338. // eslint-disable-next-line no-empty
  339. } catch (e) {
  340. }
  341. _raiseCastFail(dialog, 'InputDialogPeer')
  342. }
  343. function getInputMessage(message) {
  344. try {
  345. if (typeof message == 'number') { // This case is really common too
  346. return new types.InputMessageID({
  347. id: message,
  348. })
  349. } else if (message.SUBCLASS_OF_ID === 0x54b6bcc5) { // crc32(b'InputMessage')
  350. return message
  351. } else if (message.SUBCLASS_OF_ID === 0x790009e3) { // crc32(b'Message')
  352. return new types.InputMessageID(message.id)
  353. }
  354. // eslint-disable-next-line no-empty
  355. } catch (e) {}
  356. _raiseCastFail(message, 'InputMessage')
  357. }
  358. function getInputDocument(document) {
  359. try {
  360. if (document.SUBCLASS_OF_ID === 0xf33fdb68) {
  361. return document
  362. }
  363. } catch (err) {
  364. _raiseCastFail(document, 'InputMediaDocument')
  365. }
  366. if (document instanceof types.Document) {
  367. return new types.InputDocument({
  368. id: document.id,
  369. accessHash: document.accessHash,
  370. fileReference: document.fileReference,
  371. })
  372. }
  373. if (document instanceof types.DocumentEmpty) {
  374. return new types.InputDocumentEmpty()
  375. }
  376. if (document instanceof types.MessageMediaDocument) {
  377. return getInputDocument(document.document)
  378. }
  379. if (document instanceof types.Message) {
  380. return getInputDocument(document.media)
  381. }
  382. _raiseCastFail(document, 'InputDocument')
  383. }
  384. /**
  385. Similar to `getInputPeer`, but for photos.
  386. */
  387. function getInputPhoto(photo) {
  388. try {
  389. if (photo.SUBCLASS_OF_ID === 0x846363e0) {
  390. return photo
  391. }
  392. } catch (err) {
  393. _raiseCastFail(photo, 'InputPhoto')
  394. }
  395. if (photo instanceof types.Message) {
  396. photo = photo.media
  397. }
  398. if ((photo instanceof types.photos.Photo) ||
  399. (photo instanceof types.MessageMediaPhoto)) {
  400. photo = photo.photo
  401. }
  402. if (photo instanceof types.Photo) {
  403. return new types.InputPhoto({
  404. id: photo.id,
  405. accessHash: photo.accessHash,
  406. fileReference: photo.fileReference,
  407. })
  408. }
  409. if (photo instanceof types.PhotoEmpty) {
  410. return new types.InputPhotoEmpty()
  411. }
  412. if (photo instanceof types.messages.ChatFull) {
  413. photo = photo.fullChat
  414. }
  415. if (photo instanceof types.ChannelFull) {
  416. return getInputPhoto(photo.chatPhoto)
  417. } else if (photo instanceof types.UserFull) {
  418. return getInputPhoto(photo.profilePhoto)
  419. } else if ((photo instanceof types.Photo) ||
  420. (photo instanceof types.Chat) ||
  421. (photo instanceof types.User)) {
  422. return getInputPhoto(photo.photo)
  423. }
  424. if ((photo instanceof types.UserEmpty) ||
  425. (photo instanceof types.ChatEmpty) ||
  426. (photo instanceof types.ChatForbidden) ||
  427. (photo instanceof types.ChannelForbidden)) {
  428. return new types.InputPhotoEmpty()
  429. }
  430. _raiseCastFail(photo, 'InputPhoto')
  431. }
  432. /**
  433. Similar to `getInputPeer`, but for chat photos.
  434. */
  435. function getInputChatPhoto(photo) {
  436. try {
  437. if (photo.SUBCLASS_OF_ID === 0xd4eb2d74) {
  438. return photo
  439. } else if (photo.SUBCLASS_OF_ID === 0xe7655f1f) {
  440. return new types.InputChatUploadedPhoto(photo)
  441. }
  442. } catch (err) {
  443. _raiseCastFail(photo, 'InputChatPhoto')
  444. }
  445. photo = getInputPhoto(photo)
  446. if (photo instanceof types.InputPhoto) {
  447. return new types.InputChatPhoto(photo)
  448. } else if (photo instanceof types.InputPhotoEmpty) {
  449. return new types.InputChatPhotoEmpty()
  450. }
  451. _raiseCastFail(photo, 'InputChatPhoto')
  452. }
  453. /**
  454. Similar to `getInputPeer`, but for geo points.
  455. */
  456. function getInputGeo(geo) {
  457. try {
  458. if (geo.SUBCLASS_OF_ID === 0x430d225) {
  459. return geo
  460. }
  461. } catch (err) {
  462. _raiseCastFail(geo, 'InputGeoPoint')
  463. }
  464. if (geo instanceof types.GeoPoint) {
  465. return new types.InputGeoPoint({
  466. lat: geo.lat,
  467. long: geo.long,
  468. })
  469. }
  470. if (geo instanceof types.GeoPointEmpty) {
  471. return new types.InputGeoPointEmpty()
  472. }
  473. if (geo instanceof types.MessageMediaGeo) {
  474. return getInputGeo(geo)
  475. }
  476. if (geo instanceof types.Message) {
  477. return getInputGeo(geo.media)
  478. }
  479. _raiseCastFail(geo, 'InputGeoPoint')
  480. }
  481. /**
  482. Similar to `getInputPeer`, but for media.
  483. If the media is `InputFile` and `is_photo` is known to be `True`,
  484. it will be treated as an `InputMediaUploadedPhoto`. Else, the rest
  485. of parameters will indicate how to treat it.
  486. */
  487. function getInputMedia(media, {
  488. isPhoto = false,
  489. attributes = null,
  490. forceDocument = false,
  491. voiceNote = false,
  492. videoNote = false,
  493. supportsStreaming = false,
  494. } = {}) {
  495. try {
  496. switch (media.SUBCLASS_OF_ID) {
  497. case 0xfaf846f4:
  498. return media
  499. case 0x846363e0:
  500. return new types.InputMediaPhoto(media)
  501. case 0xf33fdb68:
  502. return new types.InputMediaDocument(media)
  503. }
  504. } catch (err) {
  505. _raiseCastFail(media, 'InputMedia')
  506. }
  507. if (media instanceof types.MessageMediaPhoto) {
  508. return new types.InputMediaPhoto({
  509. id: getInputPhoto(media.photo),
  510. ttlSeconds: media.ttlSeconds,
  511. })
  512. }
  513. if ((media instanceof types.Photo) ||
  514. (media instanceof types.photos.Photo) ||
  515. (media instanceof types.PhotoEmpty)) {
  516. return new types.InputMediaPhoto({
  517. id: getInputPhoto(media),
  518. })
  519. }
  520. if (media instanceof types.MessageMediaDocument) {
  521. return new types.InputMediaDocument({
  522. id: getInputDocument(media.document),
  523. ttlSeconds: media.ttlSeconds,
  524. })
  525. }
  526. if ((media instanceof types.Document) ||
  527. (media instanceof types.DocumentEmpty)) {
  528. return new types.InputMediaDocument({
  529. id: getInputDocument(media),
  530. })
  531. }
  532. if ((media instanceof types.InputFile) ||
  533. (media instanceof types.InputFileBig)) {
  534. // eslint-disable-next-line one-var
  535. if (isPhoto) {
  536. return new types.InputMediaUploadedPhoto({
  537. file: media,
  538. })
  539. } else {
  540. // TODO: Get attributes from audio file
  541. // [attrs, mimeType] = getAttributes(media, {
  542. // attributes,
  543. // forceDocument,
  544. // voiceNote,
  545. // videoNote,
  546. // supportsStreaming,
  547. // })
  548. const mimeType = mime.lookup(media.name)
  549. return new types.InputMediaUploadedDocument({
  550. file: media,
  551. mimeType: mimeType,
  552. attributes: [],
  553. })
  554. }
  555. }
  556. if (media instanceof types.MessageMediaGame) {
  557. return new types.InputMediaGame({
  558. id: media.game.id,
  559. })
  560. }
  561. if (media instanceof types.MessageMediaContact) {
  562. return new types.InputMediaContact({
  563. phoneNumber: media.phoneNumber,
  564. firstName: media.firstName,
  565. lastName: media.lastName,
  566. vcard: '',
  567. })
  568. }
  569. if (media instanceof types.MessageMediaGeo) {
  570. return new types.InputMediaGeoPoint({
  571. geoPoint: getInputGeo(media.geo),
  572. })
  573. }
  574. if (media instanceof types.MessageMediaVenue) {
  575. return new types.InputMediaVenue({
  576. geoPoint: getInputGeo(media.geo),
  577. title: media.title,
  578. address: media.address,
  579. provider: media.provider,
  580. venueId: media.venueId,
  581. venueType: '',
  582. })
  583. }
  584. if ((media instanceof types.MessageMediaEmpty) ||
  585. (media instanceof types.MessageMediaUnsupported) ||
  586. (media instanceof types.ChatPhotoEmpty) ||
  587. (media instanceof types.UserProfilePhoto) ||
  588. (media instanceof types.FileLocationToBeDeprecated)) {
  589. return new types.InputMediaEmpty()
  590. }
  591. if (media instanceof types.Message) {
  592. return getInputMedia(media.media, {
  593. isPhoto,
  594. })
  595. }
  596. _raiseCastFail(media, 'InputMedia')
  597. }
  598. function getPeer(peer) {
  599. try {
  600. if (typeof peer === 'number') {
  601. const res = resolveId(peer)
  602. if (res[1] === types.PeerChannel) {
  603. return new res[1]({
  604. channelId: res[0],
  605. })
  606. } else if (res[1] === types.PeerChat) {
  607. return new res[1]({
  608. chatId: res[0],
  609. })
  610. } else {
  611. return new res[1]({
  612. userId: res[0],
  613. })
  614. }
  615. }
  616. if (peer.SUBCLASS_OF_ID === undefined) {
  617. throw new Error()
  618. }
  619. if (peer.SUBCLASS_OF_ID === 0x2d45687) {
  620. return peer
  621. } else if (peer instanceof types.contacts.ResolvedPeer ||
  622. peer instanceof types.InputNotifyPeer || peer instanceof types.TopPeer ||
  623. peer instanceof types.Dialog || peer instanceof types.DialogPeer) {
  624. return peer.peer
  625. } else if (peer instanceof types.ChannelFull) {
  626. return new types.PeerChannel({
  627. channelId: peer.id,
  628. })
  629. }
  630. if (peer.SUBCLASS_OF_ID === 0x7d7c6f86 || peer.SUBCLASS_OF_ID === 0xd9c7fc18) {
  631. // ChatParticipant, ChannelParticipant
  632. return new types.PeerUser({
  633. userId: peer.userId,
  634. })
  635. }
  636. peer = getInputPeer(peer, false, false)
  637. if (peer instanceof types.InputPeerUser) {
  638. return new types.PeerUser({
  639. userId: peer.userId,
  640. })
  641. } else if (peer instanceof types.InputPeerChat) {
  642. return new types.PeerChat({
  643. chatId: peer.chatId,
  644. })
  645. } else if (peer instanceof types.InputPeerChannel) {
  646. return new types.PeerChannel({
  647. channelId: peer.channelId,
  648. })
  649. }
  650. // eslint-disable-next-line no-empty
  651. } catch (e) {
  652. console.log(e)
  653. }
  654. _raiseCastFail(peer, 'peer')
  655. }
  656. /**
  657. Convert the given peer into its marked ID by default.
  658. This "mark" comes from the "bot api" format, and with it the peer type
  659. can be identified back. User ID is left unmodified, chat ID is negated,
  660. and channel ID is prefixed with -100:
  661. * ``user_id``
  662. * ``-chat_id``
  663. * ``-100channel_id``
  664. The original ID and the peer type class can be returned with
  665. a call to :meth:`resolve_id(marked_id)`.
  666. * @param peer
  667. * @param addMark
  668. */
  669. function getPeerId(peer, addMark = true) {
  670. // First we assert it's a Peer TLObject, or early return for integers
  671. if (typeof peer == 'number') {
  672. return addMark ? peer : resolveId(peer)[0]
  673. }
  674. // Tell the user to use their client to resolve InputPeerSelf if we got one
  675. if (peer instanceof types.InputPeerSelf) {
  676. _raiseCastFail(peer, 'int (you might want to use client.get_peer_id)')
  677. }
  678. try {
  679. peer = getPeer(peer)
  680. } catch (e) {
  681. console.log(e)
  682. _raiseCastFail(peer, 'int')
  683. }
  684. if (peer instanceof types.PeerUser) {
  685. return peer.userId
  686. } else if (peer instanceof types.PeerChat) {
  687. // Check in case the user mixed things up to avoid blowing up
  688. if (!(0 < peer.chatId <= 0x7fffffff)) {
  689. peer.chatId = resolveId(peer.chatId)[0]
  690. }
  691. return addMark ? -(peer.chatId) : peer.chatId
  692. } else { // if (peer instanceof types.PeerChannel)
  693. // Check in case the user mixed things up to avoid blowing up
  694. if (!(0 < peer.channelId <= 0x7fffffff)) {
  695. peer.channelId = resolveId(peer.channelId)[0]
  696. }
  697. if (!addMark) {
  698. return peer.channelId
  699. }
  700. // Concat -100 through math tricks, .to_supergroup() on
  701. // Madeline IDs will be strictly positive -> log works.
  702. try {
  703. return -(peer.channelId + Math.pow(10, Math.floor(Math.log10(peer.channelId) + 3)))
  704. } catch (e) {
  705. throw new Error('Cannot get marked ID of a channel unless its ID is strictly positive')
  706. }
  707. }
  708. }
  709. /**
  710. * Given a marked ID, returns the original ID and its :tl:`Peer` type.
  711. * @param markedId
  712. */
  713. function resolveId(markedId) {
  714. if (markedId >= 0) {
  715. return [markedId, types.PeerUser]
  716. }
  717. // There have been report of chat IDs being 10000xyz, which means their
  718. // marked version is -10000xyz, which in turn looks like a channel but
  719. // it becomes 00xyz (= xyz). Hence, we must assert that there are only
  720. // two zeroes.
  721. const m = markedId.toString().match(/-100([^0]\d*)/)
  722. if (m) {
  723. return [parseInt(m[1]), types.PeerChannel]
  724. }
  725. return [-markedId, types.PeerChat]
  726. }
  727. /**
  728. * returns an entity pair
  729. * @param entityId
  730. * @param entities
  731. * @param cache
  732. * @param getInputPeer
  733. * @returns {{inputEntity: *, entity: *}}
  734. * @private
  735. */
  736. function _getEntityPair(entityId, entities, cache, getInputPeer = getInputPeer) {
  737. const entity = entities.get(entityId)
  738. let inputEntity = cache[entityId]
  739. if (inputEntity === undefined) {
  740. try {
  741. inputEntity = getInputPeer(inputEntity)
  742. } catch (e) {
  743. inputEntity = null
  744. }
  745. }
  746. return {
  747. entity,
  748. inputEntity,
  749. }
  750. }
  751. function getMessageId(message) {
  752. if (message === null || message === undefined) {
  753. return null
  754. }
  755. if (typeof message == 'number') {
  756. return message
  757. }
  758. if (message.SUBCLASS_OF_ID === 0x790009e3) { // crc32(b'Message')
  759. return message.id
  760. }
  761. throw new Error(`Invalid message type: ${message.constructor.name}`)
  762. }
  763. /**
  764. Converts the given parse mode into a matching parser.
  765. */
  766. function sanitizeParseMode(mode) {
  767. if (!mode) return null
  768. if (mode instanceof Function) {
  769. class CustomMode {
  770. static unparse(text, entities) {
  771. throw new Error('Not implemented')
  772. }
  773. }
  774. CustomMode.prototype.parse = mode
  775. return CustomMode
  776. } else if (mode.parse && mode.unparse) {
  777. return mode
  778. } else if (mode instanceof String) {
  779. switch (mode.toLowerCase()) {
  780. case 'md':
  781. case 'markdown':
  782. return MarkdownParser
  783. case 'htm':
  784. case 'html':
  785. return HTMLParser
  786. default:
  787. throw new Error(`Unknown parse mode ${mode}`)
  788. }
  789. } else {
  790. throw new TypeError(`Invalid parse mode type ${mode}`)
  791. }
  792. }
  793. function _getFileInfo(location) {
  794. try {
  795. if (location.SUBCLASS_OF_ID === 0x1523d462) {
  796. return new FileInfo(null, location, null)
  797. }
  798. } catch (err) {
  799. _raiseCastFail(location, 'InputFileLocation')
  800. }
  801. if (location instanceof types.Message) {
  802. location = location.media
  803. }
  804. if (location instanceof types.MessageMediaDocument) {
  805. location = location.document
  806. } else if (location instanceof types.MessageMediaPhoto) {
  807. location = location.photo
  808. }
  809. if (location instanceof types.Document) {
  810. return new FileInfo(location.dcId, new types.InputDocumentFileLocation({
  811. id: location.id,
  812. accessHash: location.accessHash,
  813. fileReference: location.fileReference,
  814. thumbSize: '',
  815. }), location.size)
  816. } else if (location instanceof types.Photo) {
  817. return new FileInfo(location.dcId, new types.InputPhotoFileLocation({
  818. id: location.id,
  819. accessHash: location.accessHash,
  820. fileReference: location.fileReference,
  821. thumbSize: location.sizes.slice(-1)[0].type,
  822. }))
  823. }
  824. if (location instanceof types.FileLocationToBeDeprecated) {
  825. throw new TypeError('Unavailable location can\'t be used as input')
  826. }
  827. _raiseCastFail(location, 'InputFileLocation')
  828. }
  829. /**
  830. * Parses the given phone, or returns `None` if it's invalid.
  831. * @param phone
  832. */
  833. function parsePhone(phone) {
  834. if (typeof phone === 'number') {
  835. return phone.toString()
  836. } else {
  837. phone = phone.toString().replace(/[+()\s-]/gm, '')
  838. if (!isNaN(phone)) {
  839. return phone
  840. }
  841. }
  842. }
  843. function isImage(file) {
  844. if (path.extname(file).match(/\.(png|jpe?g)/i)) {
  845. return true
  846. } else {
  847. return resolveBotFileId(file) instanceof types.Photo
  848. }
  849. }
  850. function isGif(file) {
  851. return !!(path.extname(file).match(/\.gif/i))
  852. }
  853. function isAudio(file) {
  854. return (mime.lookup(file) || '').startsWith('audio/')
  855. }
  856. function isVideo(file) {
  857. return (mime.lookup(file) || '').startsWith('video/')
  858. }
  859. function isIterable(obj) {
  860. if (obj == null) {
  861. return false
  862. }
  863. return typeof obj[Symbol.iterator] === 'function'
  864. }
  865. /**
  866. Parses the given username or channel access hash, given
  867. a string, username or URL. Returns a tuple consisting of
  868. both the stripped, lowercase username and whether it is
  869. a joinchat/ hash (in which case is not lowercase'd).
  870. Returns ``(None, False)`` if the ``username`` or link is not valid.
  871. * @param username {string}
  872. */
  873. function parseUsername(username) {
  874. username = username.trim()
  875. const m = username.match(USERNAME_RE) || username.match(TG_JOIN_RE)
  876. if (m) {
  877. username = username.replace(m[0], '')
  878. if (m[1]) {
  879. return {
  880. username: username,
  881. isInvite: true,
  882. }
  883. } else {
  884. username = rtrim(username, '/')
  885. }
  886. }
  887. if (username.match(VALID_USERNAME_RE)) {
  888. return {
  889. username: username.toLowerCase(),
  890. isInvite: false,
  891. }
  892. } else {
  893. return {
  894. username: null,
  895. isInvite: false,
  896. }
  897. }
  898. }
  899. /**
  900. Gets the inner text that's surrounded by the given entites.
  901. For instance: `text = 'Hey!', entity = new MessageEntityBold(2, 2) // -> 'y!'`
  902. @param text the original text
  903. @param entities the entity or entities that must be matched
  904. */
  905. function getInnerText(text, entities) {
  906. entities = Array.isArray(entities) ? entities : [entities]
  907. return entities.reduce((acc, e) => {
  908. const start = e.offset
  909. const stop = e.offset + e.length
  910. acc.push(text.substring(start, stop))
  911. return acc
  912. }, [])
  913. }
  914. function rtrim(s, mask) {
  915. while (~mask.indexOf(s[s.length - 1])) {
  916. s = s.slice(0, -1)
  917. }
  918. return s
  919. }
  920. /**
  921. Decoded run-length-encoded data
  922. */
  923. function _rleDecode(data) {
  924. return data.replace(/(\d+)([A-z\s])/g, (_, runLength, char) => char.repeat(runLength))
  925. }
  926. /**
  927. Run-length encodes data
  928. */
  929. function _rleEncode(data) {
  930. return data.replace(/([A-z])\1+/g, (run, char) => (run.length + char))
  931. }
  932. /**
  933. Decodes a url-safe base64 encoded string into its bytes
  934. by first adding the stripped necessary padding characters.
  935. This is the way Telegram shares binary data as strings, such
  936. as the Bot API style file IDs or invite links.
  937. Returns `null` if the input string was not valid.
  938. */
  939. function _decodeTelegramBase64(string) {
  940. string += '='.repeat(string.length % 4)
  941. return new Buffer(string).toString('utf8')
  942. }
  943. /**
  944. Inverse of `_decodeTelegramBase64`
  945. */
  946. function _encodeTelegramBase64(string) {
  947. return new Buffer(string).toString('base64').replace(/=+$/, '')
  948. }
  949. /**
  950. Given a Bot API style `fileId`, returns the media it
  951. represents. If the `fileId` is not valid, `null` is
  952. returned instead.
  953. Note that the `fileId` does not have information such as
  954. dimensions, or file size, so these will be zero if
  955. present.
  956. For thumbnails, the photo ID hash will always be zero.
  957. */
  958. function resolveBotFileId(fileId) {
  959. let data = _rleDecode(_decodeTelegramBase64(fileId))
  960. if (!data) return null
  961. // Not officially documented anywhere, but we
  962. // assume the last byte is some kind of "version".
  963. let version
  964. [data, version] = data.slice(0, data.length - 1), data.slice(-1)
  965. if (![2, 4].includes(version)) return null
  966. if ((version === 2 && data.size === 24) ||
  967. (version === 4 && data.size === 25)) {
  968. // eslint-disable-next-line one-var
  969. let fileType, dcId, mediaId, accessHash
  970. if (version === 2) {
  971. [fileType, dcId, mediaId, accessHash] = struct.unpack('<iiqq', Buffer.from(data))
  972. } else {
  973. // TODO: Figure out what the extra byte means
  974. // eslint-disable-next-line comma-dangle, comma-spacing
  975. [fileType, dcId, mediaId, accessHash,] = struct.unpack('<iiqqb', Buffer.from(data))
  976. }
  977. if (!((1 <= dcId) && (dcId <= 5))) {
  978. // Valid `fileId`'s must have valid DC IDs. Since this method is
  979. // called when sending a file and the user may have entered a path
  980. // they believe is correct but the file doesn't exist, this method
  981. // may detect a path as "valid" bot `fileId` even when it's not.
  982. // By checking the `dcId`, we greatly reduce the chances of this
  983. // happening.
  984. return null
  985. }
  986. const attributes = []
  987. switch (fileType) {
  988. case 3:
  989. case 9:
  990. attributes.push(new types.DocumentAttributeAudio({
  991. duration: 0,
  992. voice: fileType === 3,
  993. }))
  994. break
  995. case 4:
  996. case 13:
  997. attributes.push(new types.DocumentAttributeVideo({
  998. duration: 0,
  999. w: 0,
  1000. h: 0,
  1001. roundMessage: fileType === 13,
  1002. }))
  1003. break
  1004. case 5:
  1005. // No idea what this is
  1006. break
  1007. case 8:
  1008. attributes.push(new types.DocumentAttributeSticker({
  1009. alt: '',
  1010. stickerSet: new types.InputStickerSetEmpty(),
  1011. }))
  1012. break
  1013. case 10:
  1014. attributes.push(new types.DocumentAttributeAnimated())
  1015. }
  1016. return new types.Document({
  1017. id: mediaId,
  1018. accessHash: accessHash,
  1019. date: null,
  1020. mimeType: '',
  1021. size: 0,
  1022. thumbs: null,
  1023. dcId: dcId,
  1024. attributes: attributes,
  1025. file_reference: '',
  1026. })
  1027. } else if ((version === 2 && data.size === 44) ||
  1028. (version === 4 && data.size === 49)) {
  1029. // eslint-disable-next-line one-var
  1030. let dcId, mediaId, accessHash, volumeId, localId
  1031. if (version === 2) {
  1032. [, dcId, mediaId, accessHash, volumeId, , localId] = struct.unpack('<iiqqqqi', Buffer.from(data))
  1033. } else {
  1034. // TODO: Figure out what the extra five bytes mean
  1035. // eslint-disable-next-line comma-dangle, comma-spacing
  1036. [, dcId, mediaId, accessHash, volumeId, , localId,] = struct.unpack('<iiqqqqi5s', Buffer.from(data))
  1037. }
  1038. if (!((1 <= dcId) && (dcId <= 5))) {
  1039. return null
  1040. }
  1041. // Thumbnails (small) always have ID 0; otherwise size 'x'
  1042. const photoSize = mediaId || accessHash ? 's' : 'x'
  1043. return new types.Photo({
  1044. id: mediaId,
  1045. accessHash: accessHash,
  1046. fileReference: '',
  1047. data: null,
  1048. sizes: [new types.PhotoSize({
  1049. type: photoSize,
  1050. location: new types.FileLocationToBeDeprecated({
  1051. volumeId: volumeId,
  1052. localId: localId,
  1053. }),
  1054. w: 0,
  1055. h: 0,
  1056. size: 0,
  1057. })],
  1058. dcId: dcId,
  1059. hasStickers: null,
  1060. })
  1061. }
  1062. }
  1063. /**
  1064. * Gets the appropriated part size when uploading or downloading files,
  1065. * given an initial file size.
  1066. * @param fileSize
  1067. * @returns {Number}
  1068. */
  1069. function getAppropriatedPartSize(fileSize) {
  1070. if (fileSize <= 104857600) { // 100MB
  1071. return 128
  1072. }
  1073. if (fileSize <= 786432000) { // 750MB
  1074. return 256
  1075. }
  1076. if (fileSize <= 1572864000) { // 1500MB
  1077. return 512
  1078. }
  1079. throw new Error('File size too large')
  1080. }
  1081. /**
  1082. * check if a given item is an array like or not
  1083. * @param item
  1084. * @returns {boolean}
  1085. */
  1086. function isListLike(item) {
  1087. return (
  1088. Array.isArray(item) ||
  1089. (!!item &&
  1090. typeof item === 'object' &&
  1091. typeof(item.length) === 'number' &&
  1092. (item.length === 0 ||
  1093. (item.length > 0 &&
  1094. (item.length - 1) in item)
  1095. )
  1096. )
  1097. )
  1098. }
  1099. module.exports = {
  1100. _getEntityPair,
  1101. _decodeTelegramBase64,
  1102. _encodeTelegramBase64,
  1103. _rleDecode,
  1104. _rleEncode,
  1105. _getFileInfo,
  1106. chunk,
  1107. getMessageId,
  1108. getExtension,
  1109. getInputChatPhoto,
  1110. getInputMedia,
  1111. getInputMessage,
  1112. getInputDialog,
  1113. getInputDocument,
  1114. getInputUser,
  1115. getInputChannel,
  1116. getInputPeer,
  1117. getInputPhoto,
  1118. parsePhone,
  1119. parseUsername,
  1120. getPeer,
  1121. getPeerId,
  1122. getDisplayName,
  1123. resolveId,
  1124. isListLike,
  1125. getAppropriatedPartSize,
  1126. getInputLocation,
  1127. strippedPhotoToJpg,
  1128. resolveBotFileId,
  1129. getInnerText,
  1130. isImage,
  1131. isGif,
  1132. isAudio,
  1133. isVideo,
  1134. isIterable,
  1135. sanitizeParseMode,
  1136. }