generic media uploads

This commit is contained in:
Adhiraj Singh
2021-08-06 18:41:07 +05:30
parent 0a746b9f53
commit 75c15e7767
5 changed files with 79 additions and 60 deletions

View File

@@ -1,10 +1,11 @@
import BinaryNode from "../BinaryNode"; import BinaryNode from "../BinaryNode";
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { EventEmitter } from 'events' 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 { isGroupID, toNumber, whatsappID, generateWAMessage, decryptMediaMessageBuffer } from "../Utils";
import makeChatsSocket from "./chats"; 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 = { const STATUS_MAP = {
read: WAMessageStatus.READ, 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 */ /** Query a string to check if it has a url, if it does, return WAUrlInfo */
const generateUrlInfo = async(text: string) => { const generateUrlInfo = async(text: string) => {
const response = await query({ const response = await query({
@@ -484,7 +530,7 @@ const makeMessagesSocket = (config: SocketConfig) => {
...options, ...options,
userJid: userJid, userJid: userJid,
getUrlInfo: generateUrlInfo, getUrlInfo: generateUrlInfo,
getMediaOptions: refreshMediaConn upload: waUploadToServer
} }
) )
await relayWAMessage(msg, { waitForAck: options.waitForAck }) await relayWAMessage(msg, { waitForAck: options.waitForAck })

View File

@@ -1,5 +1,5 @@
import P from "pino" import P from "pino"
import type { SocketConfig } from "../Types" import type { MediaType, SocketConfig } from "../Types"
import { Browsers } from "../Utils/generics" import { Browsers } from "../Utils/generics"
export const UNAUTHORIZED_CODES = [401, 403, 419] export const UNAUTHORIZED_CODES = [401, 403, 419]
@@ -34,3 +34,14 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
maxQRCodes: Infinity, maxQRCodes: Infinity,
printQRInTerminal: false, 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[]

View File

@@ -0,0 +1,8 @@
describe('Message Generation', () => {
it('should generate a text message', () => {
})
})

View File

@@ -1,4 +1,4 @@
import type { Agent } from "https" import type { ReadStream } from "fs"
import type { Logger } from "pino" import type { Logger } from "pino"
import type { URL } from "url" import type { URL } from "url"
import { proto } from '../../WAMessage' import { proto } from '../../WAMessage'
@@ -125,10 +125,12 @@ export type MiscMessageGenerationOptions = {
export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions & { export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions & {
userJid: string userJid: string
} }
export type WAMediaUploadFunction = (readStream: ReadStream, opts: { fileEncSha256B64: string, mediaType: MediaType }) => Promise<{ mediaUrl: string }>
export type MediaGenerationOptions = { export type MediaGenerationOptions = {
logger?: Logger logger?: Logger
agent?: Agent upload: WAMediaUploadFunction
getMediaOptions: (refresh: boolean) => Promise<MediaConnInfo>
} }
export type MessageContentGenerationOptions = MediaGenerationOptions & { export type MessageContentGenerationOptions = MediaGenerationOptions & {
getUrlInfo?: (text: string) => Promise<WAUrlInfo> getUrlInfo?: (text: string) => Promise<WAUrlInfo>

View File

@@ -1,8 +1,7 @@
import { Boom } from '@hapi/boom' import { Boom } from '@hapi/boom'
import { createReadStream, promises as fs } from "fs" import { createReadStream, promises as fs } from "fs"
import got from "got"
import { proto } from '../../WAMessage' 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 { import {
AnyMediaMessageContent, AnyMediaMessageContent,
AnyMessageContent, AnyMessageContent,
@@ -33,14 +32,6 @@ type MediaUploadData = {
mimetype?: string 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 } = { const MIMETYPE_MAP: { [T in MediaType]: string } = {
image: 'image/jpeg', image: 'image/jpeg',
video: 'video/mp4', video: 'video/mp4',
@@ -57,8 +48,6 @@ const MessageTypeProto = {
'document': WAMessageProto.DocumentMessage, 'document': WAMessageProto.DocumentMessage,
} as const } as const
const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
export const prepareWAMessageMedia = async( export const prepareWAMessageMedia = async(
message: AnyMediaMessageContent, message: AnyMediaMessageContent,
options: MediaGenerationOptions options: MediaGenerationOptions
@@ -110,47 +99,10 @@ export const prepareWAMessageMedia = async(
} catch (error) { } catch (error) {
options.logger?.debug ({ error }, 'failed to obtain audio duration: ' + error.message) 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 const {mediaUrl} = await options.upload(
let uploadInfo = await options.getMediaOptions(false) createReadStream(encBodyPath),
{ fileEncSha256B64, mediaType }
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 }
)
}
// remove tmp files // remove tmp files
await Promise.all( await Promise.all(
[ [