Files
memestream/src/attachments.ts
T
2024-04-13 23:41:25 +10:00

109 lines
3.4 KiB
TypeScript

import sharp from 'sharp'
import axios from 'axios'
import mime from 'mime-types'
import type { Logger } from 'pino'
import type Minio from 'minio'
import type { PrismaClient, Attachment, ChannelHistory } from '@prisma/client'
import type { MemeService } from './memes'
const STORAGE_BUCKET = process.env.STORAGE_BUCKET
export const IMAGE_TYPES = [
'image/png',
'image/jpeg',
'image/webp',
'image/gif',
]
export const VIDEO_TYPES = [
'video/mp4',
'video/quicktime'
]
export function isImage(a: Attachment) {
return IMAGE_TYPES.includes(a.contentType)
}
export function objectName(a: Attachment) {
if (isImage(a))
return `${a.memeId}.webp`
return `${a.memeId}.${mime.extension(a.contentType)}`
}
export default async function attachmentService(log: Logger, minio: Minio.Client, prisma: PrismaClient, memes: MemeService) {
async function ensureStorageBucketExists() {
const exists = await minio.bucketExists(STORAGE_BUCKET)
if (exists) return true
await minio.makeBucket(STORAGE_BUCKET)
}
async function storeAttachment(a: Attachment) {
const download = await axios.get(a.url, { responseType: 'stream' })
const stream = () => {
if (isImage(a)) {
const converter = sharp().webp()
download.data.pipe(converter)
return converter
}
return download.data
}
return minio.putObject(STORAGE_BUCKET, objectName(a), stream())
}
async function deleteAttachment(a: Attachment) {
return minio.removeObject(STORAGE_BUCKET, objectName(a))
}
async function process(a: Attachment, history: Partial<ChannelHistory>) {
try {
const results = await prisma.$transaction(async prisma => {
const meme = memes.fromAttachment(a)
const dbMeme = await prisma.meme.upsert({ create: meme, update: meme, where: { id: meme.id } })
const dbA = await prisma.attachment.upsert({ create: a, update: a, where: { id: a.id } })
log.debug({ msg: 'pending addition to db', dbMeme })
const upload = await storeAttachment(a)
log.debug({ msg: 'successfully uploaded', upload })
await memes.persist(dbMeme)
log.info(dbMeme, 'new meme')
return { meme: dbMeme, upload, attachment: dbA }
}, { timeout: 60000 })
} catch (e) {
// this happens pretty regularly it seems
// when ppl reply to an attachment it is included in their message
if (e.meta?.modelName !== 'Meme' && e.meta?.target[0] !== 'attachmentId') {
log.error(e, 'failed to process attachment')
log.error(a, 'failed to process attachment')
}
// deletes the upload if we got that far
// (probably not)
await deleteAttachment(a)
}
// we assume here that a failure in processing was caused by a duplicate
// so we update pg to say we've processed it
const newHistory = await prisma.channelHistory.update({ data: history, where: { id: history.id }})
log.debug({ newHistory }, 'updated channel history')
return true
}
await ensureStorageBucketExists()
return {
process,
}
}
export type AttachmentService = Awaited<ReturnType<typeof attachmentService>>