109 lines
3.4 KiB
TypeScript
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>>
|