fp/packages/scout/src/temporal/activities.ts
CJ_Clippy a60d0f0821
Some checks failed
ci / build (push) Has been cancelled
assert a vod CUID when updated
2024-07-06 00:49:51 -08:00

371 lines
14 KiB
TypeScript

import fetch from "node-fetch"
import { NotificationData, processEmail } from "./workflows.js"
import qs from 'qs'
import { IPlatformNotificationResponse, IVtuberResponse, IStreamResponse } from 'next'
import { getImage } from '../vtuber.js'
import { fpSlugify } from '../utils.js'
import { getProminentColor } from '../image.js'
import { uploadFile } from '../s3.js'
import { addMinutes, subMinutes } from 'date-fns'
export type ChargeResult = {
status: string;
errorMessage?: string;
};
if (!process.env.SCOUT_STRAPI_API_KEY) throw new Error('SCOUT_STRAPI_API_KEY is missing from env');
if (!process.env.STRAPI_URL) throw new Error('STRAPI_URL is missing from env');
if (!process.env.CDN_BUCKET_URL) throw new Error('CDN_BUCKET_URL is missing from env');
if (!process.env.SCOUT_NITTER_URL) throw new Error('SCOUT_NITTER_URL is missing from env');
if (!process.env.SCOUT_NITTER_ACCESS_KEY) throw new Error('SCOUT_NITTER_ACCESS_KEY is missing from env');
/**
* find or create vtuber in Strapi
*/
export async function upsertVtuber({ platform, userId, url, channel }: NotificationData): Promise<number> {
let vtuberId
console.log('>> # Step 1, upsertVtuber')
// # Step 1.
// First we find or create the vtuber
// The vtuber may already be in the db, so we look for that record. All we need is the Vtuber ID.
// If the vtuber is not in the db, we create the vtuber record.
// GET /api/:pluralApiId?filters[field][operator]=value
const findVtubersFilters = (() => {
if (platform === 'chaturbate') {
return { chaturbate: { $eq: url } }
} else if (platform === 'fansly') {
if (!userId) throw new Error('Fansly userId was undefined, but it is required.')
return { fanslyId: { $eq: userId } }
}
})()
console.log('>>>>> the following is findVtubersFilters.')
console.log(findVtubersFilters)
const findVtubersQueryString = qs.stringify({
filters: findVtubersFilters
}, { encode: false })
console.log(`>>>>> platform=${platform}, url=${url}, userId=${userId}`)
console.log('>> findVtuber')
const findVtuberRes = await fetch(`${process.env.STRAPI_URL}/api/vtubers?${findVtubersQueryString}`, {
method: 'GET',
headers: {
'content-type': 'application/json'
}
})
const findVtuberJson = await findVtuberRes.json() as IVtuberResponse
console.log('>> here is the vtuber json')
console.log(findVtuberJson)
if (findVtuberJson?.data && findVtuberJson.data.length > 0) {
console.log('>> a vtuber was FOUND')
if (findVtuberJson.data.length > 1) throw new Error('There was more than one vtuber match. There must only be one.')
vtuberId = findVtuberJson.data[0].id
console.log('here is the findVtuberJson (as follows)')
console.log(findVtuberJson)
console.log(`the matching vtuber has ID=${vtuberId} (${findVtuberJson.data[0].attributes.displayName})`)
}
if (!vtuberId) {
console.log('>> vtuberId was not found so we create')
/**
* We are creating a vtuber record.
* We need a few things.
* * image URL
* * themeColor
*
* To get an image, we have to do a few things.
* * [x] download image from platform
* * [x] get themeColor from image
* * [x] upload image to b2
* * [x] get B2 cdn link to image
*
* To get themeColor, we need the image locally where we can then run
*/
// download image from platform
// vtuber.getImage expects a vtuber object, which we don't have yet, so we create a dummy one
const dummyVtuber = {
attributes: {
slug: fpSlugify(channel),
fanslyId: (platform === 'fansly') ? userId : null
}
}
const imageFile = await getImage(dummyVtuber)
// get themeColor from image
const themeColor = await getProminentColor(imageFile)
// upload image to b2
const b2FileData = await uploadFile(imageFile)
// get b2 cdn link to image
const imageCdnLink = `${process.env.CDN_BUCKET_URL}/${b2FileData.Key}`
console.log(`>>> createVtuberRes here we go 3-2-1, POST!`)
const createVtuberRes = await fetch(`${process.env.STRAPI_URL}/api/vtubers`, {
method: 'POST',
headers: {
'authorization': `Bearer ${process.env.SCOUT_STRAPI_API_KEY}`,
'content-type': 'application/json'
},
body: JSON.stringify({
data: {
displayName: channel,
fansly: (platform === 'fansly') ? url : null,
fanslyId: (platform === 'fansly') ? userId : null,
chaturbate: (platform === 'chaturbate') ? url : null,
slug: fpSlugify(channel),
description1: ' ',
image: imageCdnLink,
themeColor: themeColor || '#dde1ec'
}
})
})
const createVtuberJson = await createVtuberRes.json() as IVtuberResponse
console.log('>> createVtuberJson as follows')
console.log(JSON.stringify(createVtuberJson, null, 2))
if (createVtuberJson.data) {
vtuberId = createVtuberJson.data.id
console.log(`>>> vtuber created with id=${vtuberId}`)
}
}
if (!vtuberId) throw new Error(`upsertVtuber failed to produce a vtuberId! This should not happen under normal circumstances.`);
return vtuberId
}
export async function upsertPlatformNotification({ source, date, platform, vtuberId }: { source: string, date: string, platform: string, vtuberId: number }): Promise<number> {
if (!source) throw new Error(`upsertPlatformNotification requires source arg, but it was undefined`);
if (!date) throw new Error(`upsertPlatformNotification requires date arg, but it was undefined`);
if (!platform) throw new Error(`upsertPlatformNotification requires platform arg, but it was undefined`);
if (!vtuberId) throw new Error(`upsertPlatformNotification requires vtuberId arg, but it was undefined`);
let pNotifId
// # Step 2.
// Next we create the platform-notification record.
// This probably doesn't already exist, so we don't check for a pre-existing platform-notification.
const pNotifPayload = {
data: {
source: source,
date: date,
date2: date,
platform: platform,
vtuber: vtuberId,
}
}
console.log('pNotifPayload as follows')
console.log(pNotifPayload)
const pNotifCreateRes = await fetch(`${process.env.STRAPI_URL}/api/platform-notifications`, {
method: 'POST',
headers: {
'authorization': `Bearer ${process.env.SCOUT_STRAPI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(pNotifPayload)
})
const pNotifData = await pNotifCreateRes.json() as IPlatformNotificationResponse
if (pNotifData.error) {
console.error('>> we failed to create platform-notification, there was an error in the response')
console.error(JSON.stringify(pNotifData.error, null, 2))
throw new Error(pNotifData.error)
}
console.log(`>> pNotifData (json response) is as follows`)
console.log(pNotifData)
if (!pNotifData.data?.id) throw new Error('failed to created pNotifData! The response was missing an id');
pNotifId = pNotifData.data.id
if (!pNotifId) throw new Error('failed to get Platform Notification ID');
return pNotifId
}
export async function upsertStream({
date,
vtuberId,
platform,
pNotifId
}: {
date: string,
vtuberId: number,
platform: string,
pNotifId: number
}): Promise<number> {
if (!date) throw new Error(`upsertStream requires date in the arg object, but it was undefined`);
if (!vtuberId) throw new Error(`upsertStream requires vtuberId in the arg object, but it was undefined`);
if (!platform) throw new Error(`upsertStream requires platform in the arg object, but it was undefined`);
if (!pNotifId) throw new Error(`upsertStream requires pNotifId in the arg object, but it was undefined`);
let streamId
// # Step 3.
// Finally we find or create the stream record
// The stream may already be in the db (the streamer is multi-platform streaming), so we look for that record.
// This gets a bit tricky. How do we determine one stream from another?
// For now, the rule is 30 minutes of separation.
// Anything <=30m is interpreted as the same stream. Anything >30m is interpreted as a different stream.
// If the stream is not in the db, we create the stream record
const dateSinceRange = subMinutes(new Date(date), 30)
const dateUntilRange = addMinutes(new Date(date), 30)
console.log(`Find a stream within + or - 30 mins of the notif date=${new Date(date).toISOString()}. dateSinceRange=${dateSinceRange.toISOString()}, dateUntilRange=${dateUntilRange.toISOString()}`)
const findStreamQueryString = qs.stringify({
populate: 'platform-notifications',
filters: {
date: {
$gte: dateSinceRange,
$lte: dateUntilRange
},
vtuber: {
id: {
'$eq': vtuberId
}
}
}
}, { encode: false })
console.log('>> findStream')
const findStreamRes = await fetch(`${process.env.STRAPI_URL}/api/streams?${findStreamQueryString}`, {
method: 'GET',
headers: {
'authorization': `Bearer ${process.env.SCOUT_STRAPI_API_KEY}`,
'Content-Type': 'application/json'
}
})
const findStreamData = await findStreamRes.json() as IStreamResponse
if (findStreamData?.data && findStreamData.data.length > 0) {
console.log('>> we found a findStreamData json. (there is an existing stream for this e-mail/notification)')
console.log(JSON.stringify(findStreamData, null, 2))
streamId = findStreamData.data[0].id
// Before we're done here, we need to do something extra. We need to populate isChaturbateStream and/or isFanslyStream.
// We know which of these booleans to set based on the stream's related platformNotifications
// We go through each pNotif and look at it's platform
let isFanslyStream = false
let isChaturbateStream = false
if (findStreamData.data[0].attributes.platformNotifications) {
for (const pn of findStreamData.data[0].attributes.platformNotifications) {
if (pn.platform === 'fansly') {
isFanslyStream = true
} else if (pn.platform === 'chaturbate') {
isChaturbateStream = true
}
}
}
console.log(`>>> updating stream ${streamId}. isFanslyStream=${isFanslyStream}, isChaturbateStream=${isChaturbateStream}`)
const updateStreamRes = await fetch(`${process.env.STRAPI_URL}/api/streams/${streamId}`, {
method: 'PUT',
headers: {
'authorization': `Bearer ${process.env.SCOUT_STRAPI_API_KEY}`,
'content-type': 'application/json'
},
body: JSON.stringify({
data: {
isFanslyStream: isFanslyStream,
isChaturbateStream: isChaturbateStream,
platformNotifications: [
pNotifId
]
}
})
})
const updateStreamJson = await updateStreamRes.json() as IStreamResponse
if (updateStreamJson?.error) throw new Error(JSON.stringify(updateStreamJson, null, 2));
console.log(`>> assuming a successful update to the stream record. response as follows.`)
console.log(JSON.stringify(updateStreamJson, null, 2))
}
if (!streamId) {
console.log('>> did not find a streamId, so we go ahead and create a stream record in the db.')
const createStreamPayload = {
data: {
isFanslyStream: (platform === 'fansly') ? true : false,
isChaturbateStream: (platform === 'chaturbate') ? true : false,
archiveStatus: 'missing',
date: date,
date2: date,
date_str: date,
vtuber: vtuberId,
platformNotifications: [
pNotifId
]
}
}
console.log('>> createStreamPayload as follows')
console.log(createStreamPayload)
const createStreamRes = await fetch(`${process.env.STRAPI_URL}/api/streams`, {
method: 'POST',
headers: {
'authorization': `Bearer ${process.env.SCOUT_STRAPI_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(createStreamPayload)
})
const createStreamJson = await createStreamRes.json() as IStreamResponse
console.log('>> we got the createStreamJson')
console.log(createStreamJson)
if (createStreamJson.error) {
console.error(JSON.stringify(createStreamJson.error, null, 2))
throw new Error('Failed to create stream in DB due to an error. (see above)')
}
streamId = createStreamJson.id
}
if (!streamId) throw new Error('failed to get streamId')
return streamId
}
export async function chargeUser(
userId: string,
itemId: string,
quantity: number,
): Promise<ChargeResult> {
// TODO send request to the payments service that looks up the user's saved
// payment info and the cost of the item and attempts to charge their payment
// method.
console.log(`Charging user ${userId} for ${quantity} of item ${itemId}`);
try {
const response = await fetch("http://httpbin.org/get?status=success");
const body: any = await response.json();
return { status: body.args.status };
} catch (e: any) {
return { status: "failure", errorMessage: e.message };
}
}
export async function checkAndDecrementInventory(
itemId: string,
quantity: number,
): Promise<boolean> {
// TODO a database request that—in a single operation or transaction—checks
// whether there are `quantity` items remaining, and if so, decreases the
// total. Something like:
// const result = await db.collection('items').updateOne(
// { _id: itemId, numAvailable: { $gte: quantity } },
// { $inc: { numAvailable: -quantity } }
// )
// return result.modifiedCount === 1
console.log(`Reserving ${quantity} of item ${itemId}`);
return true;
}
export async function incrementInventory(
itemId: string,
quantity: number,
): Promise<boolean> {
// TODO increment inventory:
// const result = await db.collection('items').updateOne(
// { _id: itemId },
// { $inc: { numAvailable: quantity } }
// )
// return result.modifiedCount === 1
console.log(`Incrementing ${itemId} inventory by ${quantity}`);
return true;
}