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) { 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>