diff --git a/src/Connection/messages.ts b/src/Connection/messages.ts index f436478..82fcbfa 100644 --- a/src/Connection/messages.ts +++ b/src/Connection/messages.ts @@ -1,10 +1,11 @@ import BinaryNode from "../BinaryNode"; import { Boom } from '@hapi/boom' import { EventEmitter } from 'events' -import { Chat, Presence, WAMessageCursor, SocketConfig, WAMessage, WAMessageKey, ParticipantAction, WAMessageProto, WAMessageStatus, WAMessageStubType, GroupMetadata, AnyMessageContent, MiscMessageGenerationOptions, WAFlag, WAMetric, WAUrlInfo, MediaConnInfo, MessageUpdateType, MessageInfo, MessageInfoUpdate } from "../Types"; +import { Chat, Presence, WAMessageCursor, SocketConfig, WAMessage, WAMessageKey, ParticipantAction, WAMessageProto, WAMessageStatus, WAMessageStubType, GroupMetadata, AnyMessageContent, MiscMessageGenerationOptions, WAFlag, WAMetric, WAUrlInfo, MediaConnInfo, MessageUpdateType, MessageInfo, MessageInfoUpdate, WAMediaUploadFunction, MediaType } from "../Types"; import { isGroupID, toNumber, whatsappID, generateWAMessage, decryptMediaMessageBuffer } from "../Utils"; import makeChatsSocket from "./chats"; -import { WA_DEFAULT_EPHEMERAL } from "../Defaults"; +import { DEFAULT_ORIGIN, MEDIA_PATH_MAP, WA_DEFAULT_EPHEMERAL } from "../Defaults"; +import got from "got"; const STATUS_MAP = { read: WAMessageStatus.READ, @@ -211,6 +212,51 @@ const makeMessagesSocket = (config: SocketConfig) => { } } + const waUploadToServer: WAMediaUploadFunction = async(stream, { mediaType, fileEncSha256B64 }) => { + // send a query JSON to obtain the url & auth token to upload our media + let uploadInfo = await refreshMediaConn(false) + + let mediaUrl: string + for (let host of uploadInfo.hosts) { + const auth = encodeURIComponent(uploadInfo.auth) // the auth token + const url = `https://${host.hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}` + + try { + const {body: responseText} = await got.post( + url, + { + headers: { + 'Content-Type': 'application/octet-stream', + 'Origin': DEFAULT_ORIGIN + }, + agent: { + https: config.agent + }, + body: stream + } + ) + const result = JSON.parse(responseText) + mediaUrl = result?.url + + if (mediaUrl) break + else { + uploadInfo = await refreshMediaConn(true) + throw new Error(`upload failed, reason: ${JSON.stringify(result)}`) + } + } catch (error) { + const isLast = host.hostname === uploadInfo.hosts[uploadInfo.hosts.length-1].hostname + logger.debug(`Error in uploading to ${host.hostname} (${error}) ${isLast ? '' : ', retrying...'}`) + } + } + if (!mediaUrl) { + throw new Boom( + 'Media upload failed on all hosts', + { statusCode: 500 } + ) + } + return { mediaUrl } + } + /** Query a string to check if it has a url, if it does, return WAUrlInfo */ const generateUrlInfo = async(text: string) => { const response = await query({ @@ -484,7 +530,7 @@ const makeMessagesSocket = (config: SocketConfig) => { ...options, userJid: userJid, getUrlInfo: generateUrlInfo, - getMediaOptions: refreshMediaConn + upload: waUploadToServer } ) await relayWAMessage(msg, { waitForAck: options.waitForAck }) diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index caca01f..852e25d 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -1,5 +1,5 @@ import P from "pino" -import type { SocketConfig } from "../Types" +import type { MediaType, SocketConfig } from "../Types" import { Browsers } from "../Utils/generics" export const UNAUTHORIZED_CODES = [401, 403, 419] @@ -34,3 +34,14 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { maxQRCodes: Infinity, printQRInTerminal: false, } + + +export const MEDIA_PATH_MAP: { [T in MediaType]: string } = { + image: '/mms/image', + video: '/mms/video', + document: '/mms/document', + audio: '/mms/audio', + sticker: '/mms/image', +} + +export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[] \ No newline at end of file diff --git a/src/Tests/test.message-gen.ts b/src/Tests/test.message-gen.ts new file mode 100644 index 0000000..6b325b4 --- /dev/null +++ b/src/Tests/test.message-gen.ts @@ -0,0 +1,8 @@ + +describe('Message Generation', () => { + + it('should generate a text message', () => { + + }) + +}) \ No newline at end of file diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 29cbe44..da7e5a1 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -1,4 +1,4 @@ -import type { Agent } from "https" +import type { ReadStream } from "fs" import type { Logger } from "pino" import type { URL } from "url" import { proto } from '../../WAMessage' @@ -125,10 +125,12 @@ export type MiscMessageGenerationOptions = { export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions & { userJid: string } + +export type WAMediaUploadFunction = (readStream: ReadStream, opts: { fileEncSha256B64: string, mediaType: MediaType }) => Promise<{ mediaUrl: string }> + export type MediaGenerationOptions = { logger?: Logger - agent?: Agent - getMediaOptions: (refresh: boolean) => Promise + upload: WAMediaUploadFunction } export type MessageContentGenerationOptions = MediaGenerationOptions & { getUrlInfo?: (text: string) => Promise diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 95e6231..b15e689 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -1,8 +1,7 @@ import { Boom } from '@hapi/boom' import { createReadStream, promises as fs } from "fs" -import got from "got" import { proto } from '../../WAMessage' -import { DEFAULT_ORIGIN, URL_REGEX, WA_DEFAULT_EPHEMERAL } from "../Defaults" +import { MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from "../Defaults" import { AnyMediaMessageContent, AnyMessageContent, @@ -33,14 +32,6 @@ type MediaUploadData = { mimetype?: string } -const MEDIA_PATH_MAP: { [T in MediaType]: string } = { - image: '/mms/image', - video: '/mms/video', - document: '/mms/document', - audio: '/mms/audio', - sticker: '/mms/image', -} as const - const MIMETYPE_MAP: { [T in MediaType]: string } = { image: 'image/jpeg', video: 'video/mp4', @@ -57,8 +48,6 @@ const MessageTypeProto = { 'document': WAMessageProto.DocumentMessage, } as const -const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[] - export const prepareWAMessageMedia = async( message: AnyMediaMessageContent, options: MediaGenerationOptions @@ -110,47 +99,10 @@ export const prepareWAMessageMedia = async( } catch (error) { options.logger?.debug ({ error }, 'failed to obtain audio duration: ' + error.message) } - // send a query JSON to obtain the url & auth token to upload our media - let uploadInfo = await options.getMediaOptions(false) - - let mediaUrl: string - for (let host of uploadInfo.hosts) { - const auth = encodeURIComponent(uploadInfo.auth) // the auth token - const url = `https://${host.hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}` - - try { - const {body: responseText} = await got.post( - url, - { - headers: { - 'Content-Type': 'application/octet-stream', - 'Origin': DEFAULT_ORIGIN - }, - agent: { - https: options.agent - }, - body: createReadStream(encBodyPath) - } - ) - const result = JSON.parse(responseText) - mediaUrl = result?.url - - if (mediaUrl) break - else { - uploadInfo = await options.getMediaOptions(true) - throw new Error(`upload failed, reason: ${JSON.stringify(result)}`) - } - } catch (error) { - const isLast = host.hostname === uploadInfo.hosts[uploadInfo.hosts.length-1].hostname - options.logger?.debug(`Error in uploading to ${host.hostname} (${error}) ${isLast ? '' : ', retrying...'}`) - } - } - if (!mediaUrl) { - throw new Boom( - 'Media upload failed on all hosts', - { statusCode: 500 } - ) - } + const {mediaUrl} = await options.upload( + createReadStream(encBodyPath), + { fileEncSha256B64, mediaType } + ) // remove tmp files await Promise.all( [