Skip to content

Commit

Permalink
feat(discord): media
Browse files Browse the repository at this point in the history
  • Loading branch information
XxLittleCxX committed Jul 18, 2023
1 parent c9330ba commit 2cdbded
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 36 deletions.
28 changes: 25 additions & 3 deletions adapters/whatsapp/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,35 @@ export class WhatsAppBot extends Bot<WhatsAppBot.Config> {
}

async initialize() {
const { data } = await this.http('GET', `/v17.0/${this.config.id}/phone_numbers`)
const { data } = await this.http<{
data: {
verified_name: string
code_verification_status: string
display_phone_number: string
quality_rating: string
id: string
}[]
}>('GET', `/v17.0/${this.config.id}/phone_numbers`)
this.ctx.logger('whatsapp').debug(require('util').inspect(data, false, null, true))
if (data.length) {
console.log(data[0])
this.selfId = data[0].id
this.username = data[0].display_phone_number
this.username = data[0].verified_name
}
}

async createReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
// https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages#reaction-messages
await this.http.post(`/${this.selfId}/messages`, {
messaging_product: 'whatsapp',
to: channelId,
recipient_type: 'individual',
type: 'reaction',
reaction: {
message_id: messageId,
emoji,
},
})
}
}

export namespace WhatsAppBot {
Expand Down
50 changes: 30 additions & 20 deletions adapters/whatsapp/src/http.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Adapter, Context, Logger } from '@satorijs/satori'
import { WhatsAppBot } from './bot'
import { WebhookBody } from './types'
import { decodeMessage } from './utils'
import internal from 'stream'

export class HttpServer extends Adapter.Server<WhatsAppBot> {
logger = new Logger('whatsapp')
Expand All @@ -9,34 +11,19 @@ export class HttpServer extends Adapter.Server<WhatsAppBot> {
}

async start(bot: WhatsAppBot) {
// @TODO selfId
// https://developers.facebook.com/docs/graph-api/webhooks/getting-started
// https://developers.facebook.com/docs/graph-api/webhooks/getting-started/webhooks-for-whatsapp/
await bot.initialize()
bot.ctx.router.post('/whatsapp', async (ctx) => {
const parsed = ctx.request.body as WebhookBody
this.logger.debug(require('util').inspect(parsed, false, null, true))
ctx.body = 'ok'
ctx.status = 200
if (parsed.object !== 'whatsapp_business_account') return
for (const entry of parsed.entry) {
for (const change of entry.changes) {
if (change.field === 'messages' && change.value.messages?.length) {
const session = bot.session()
session.type = 'message'
session.isDirect = true
session.content = change.value.messages[0].text.body
session.channelId = change.value.messages[0].from
session.guildId = change.value.messages[0].from
session.messageId = change.value.messages[0].id
session.author = {
userId: change.value.messages[0].from,
username: change.value.contacts[0].profile.name,
}
session.userId = change.value.messages[0].from
session.timestamp = parseInt(change.value.messages[0].timestamp) * 1000

bot.dispatch(session)
}
}
const session = await decodeMessage(bot, entry)
if (session.length) session.forEach(bot.dispatch.bind(bot))
this.logger.debug(require('util').inspect(session, false, null, true))
}
})
bot.ctx.router.get('/whatsapp', async (ctx) => {
Expand All @@ -47,6 +34,29 @@ export class HttpServer extends Adapter.Server<WhatsAppBot> {
ctx.body = challenge
ctx.status = 200
})
bot.ctx.router.get('/whatsapp/assets/:self_id/:media_id', async (ctx) => {
const mediaId = ctx.params.media_id
const selfId = ctx.params.self_id
const localBot = this.bots.find((bot) => bot.selfId === selfId)
if (!localBot) return ctx.status = 404

const fetched = await localBot.http.get<{
url: string
}>('/' + mediaId)
this.logger.debug(fetched.url)
const resp = await localBot.ctx.http.axios<internal.Readable>({
url: fetched.url,
method: 'GET',
responseType: 'stream',
headers: {
Authorization: `Bearer ${localBot.config.systemToken}`,
},
})
ctx.type = resp.headers['content-type']
ctx.set('cache-control', resp.headers['cache-control'])
ctx.response.body = resp.data
ctx.status = 200
})
bot.online()
}
}
6 changes: 4 additions & 2 deletions adapters/whatsapp/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export class WhatsAppMessageEncoder extends MessageEncoder<WhatsAppBot> {
logger: Logger
prepare(): Promise<void> {

Check failure on line 12 in adapters/whatsapp/src/message.ts

View workflow job for this annotation

GitHub Actions / build

A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.
this.logger = this.bot.ctx.logger('whatsapp')
return
}

async flush(): Promise<void> {
Expand All @@ -29,7 +28,7 @@ export class WhatsAppMessageEncoder extends MessageEncoder<WhatsAppBot> {
const { messages } = await this.bot.http.post<{
messages: { id: string }[]
}>(`/${this.bot.selfId}/messages`, {
'messaging_product': 'whatsapp',
messaging_product: 'whatsapp',
to: this.channelId,
recipient_type: 'individual',
type,
Expand Down Expand Up @@ -93,6 +92,9 @@ export class WhatsAppMessageEncoder extends MessageEncoder<WhatsAppBot> {
if (!id) return
await this.flushTextMessage()
await this.sendMessage('document', { id })
} else if (type === 'face' && attrs.id) {
await this.flushTextMessage()
await this.sendMessage('sticker', { id: attrs.id })
} else if (type === 'message') {
await this.flush()
await this.render(children)
Expand Down
54 changes: 44 additions & 10 deletions adapters/whatsapp/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,40 +27,74 @@ export interface MessageValue {
}
wa_id: string
}[]
messages: {
messages: MessageBody[]
}

export interface MessageBodyBase {
from: string
id: string
timestamp: string
context?: {
from: string
id: string
timestamp: string
type: string
text: {
body: string
}
}[]
}
}

export interface ReceivedMedia {
filename?: string
caption?: string
mime_type: string
sha256: string
id: string
animated?: boolean
}

export interface MessageBodyText extends MessageBodyBase {
type: 'text'
text: {
body: string
}
}

export interface MessageBodyMedia extends MessageBodyBase {
type: 'image' | 'audio' | 'video' | 'document'
image?: ReceivedMedia
audio?: ReceivedMedia
video?: ReceivedMedia
document?: ReceivedMedia
}

export interface MessageBodySticker extends MessageBodyBase {
type: 'sticker'
sticker?: ReceivedMedia
}

export type MessageBody = MessageBodyText | MessageBodyMedia | MessageBodySticker

export interface SendMessageBase {
messaging_product: 'whatsapp'
recipient_type: 'individual'
to: string
}

export type SendMessage = SendTextMessage | SendMediaMessage<'image'> | SendMediaMessage<'audio'> | SendMediaMessage<'video'> | SendMediaMessage<'document'>
export type SendMessage = SendTextMessage | SendMediaMessage<'image'> | SendMediaMessage<'audio'> | SendMediaMessage<'video'> | SendMediaMessage<'document'> | SendMediaMessage<'sticker'>

Check warning on line 80 in adapters/whatsapp/src/types.ts

View workflow job for this annotation

GitHub Actions / lint

This line has a length of 186. Maximum allowed is 160

export interface SendTextMessage extends SendMessageBase {
type: 'text'
text: {
body: string
}
}
export type MediaType = 'image' | 'audio' | 'video' | 'document'
export type MediaType = 'image' | 'audio' | 'video' | 'document' | 'sticker'

type MediaDetail = { id: string; link: string }
type MediaDetail = { id?: string; link?: string }

interface Media {
image?: MediaDetail
audio?: MediaDetail
video?: MediaDetail
document?: MediaDetail
sticker?: MediaDetail
}

export interface SendMediaMessage<T extends MediaType> extends SendMessageBase, Media {
Expand Down
57 changes: 56 additions & 1 deletion adapters/whatsapp/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,57 @@
import { h } from '@satorijs/satori'
import { h, Session } from '@satorijs/satori'
import { WhatsAppBot } from './bot'
import { Entry } from './types'

export async function decodeMessage(bot: WhatsAppBot, entry: Entry) {
const result: Session[] = []
for (const change of entry.changes) {
if (change.field === 'messages' && change.value.messages?.length) {
const session = bot.session()
session.type = 'message'
session.isDirect = true
const message = change.value.messages[0]
session.channelId = message.from
session.guildId = message.from
session.messageId = message.id
session.author = {
userId: message.from,
username: change.value.contacts[0].profile.name,
}
session.userId = message.from
session.timestamp = parseInt(message.timestamp) * 1000

if (message.context) {
session.quote = {
messageId: message.context.id,
channelId: message.context.from,
userId: message.context.from,
content: '',
}
}

if (message.type === 'text') {
session.elements = [h.text(message.text.body)]
} else if (['video', 'audio', 'image', 'document'].includes(message.type)) {
const elements = []
let type = message.type as string
if (message.type === 'document') type = 'file'
const resource = message[message.type]
if (resource.caption) elements.push(h.text(message[message.type].caption))
elements.push(h[type](`${bot.ctx.root.config.selfUrl}/whatsapp/assets/${bot.selfId}/${resource.id}`))
session.elements = elements
} else if (message.type === 'sticker') {
session.elements = [h('face', {
id: /* (message.sticker.animated ? 'a:' : '') + */message.sticker.id,
platform: 'whatsapp',
}, [
h.image(`${bot.ctx.root.config.selfUrl}/whatsapp/assets/${bot.selfId}/${message.sticker.id}`),
])]
} else {
continue
}
session.content = session.elements.join('')
result.push(session)
}
}
return result
}

0 comments on commit 2cdbded

Please sign in to comment.