feat: implement webhook workflow engine for mention-to-audio pipeline
This commit is contained in:
150
src/lib/engine.js
Normal file
150
src/lib/engine.js
Normal file
@@ -0,0 +1,150 @@
|
||||
"use strict";
|
||||
|
||||
const { WalletStore } = require("./wallet");
|
||||
const { AudioAccessStore } = require("./access");
|
||||
const { calculateCredits } = require("./credits");
|
||||
const { extractArticleFromParent } = require("./article");
|
||||
|
||||
class XArtAudioEngine {
|
||||
constructor({ creditConfig }) {
|
||||
this.creditConfig = creditConfig;
|
||||
this.wallets = new WalletStore();
|
||||
this.access = new AudioAccessStore();
|
||||
|
||||
this.jobs = new Map();
|
||||
this.assets = new Map();
|
||||
this.mentions = new Map();
|
||||
|
||||
this.nextJobId = 1;
|
||||
this.nextAssetId = 1;
|
||||
}
|
||||
|
||||
topUpCredits(userId, amount, idempotencyKey) {
|
||||
return this.wallets.applyTransaction({
|
||||
userId,
|
||||
type: "credit",
|
||||
amount,
|
||||
reason: "topup",
|
||||
idempotencyKey,
|
||||
});
|
||||
}
|
||||
|
||||
processMention({ mentionPostId, callerUserId, parentPost }) {
|
||||
if (!mentionPostId) {
|
||||
throw new Error("mention_post_id_required");
|
||||
}
|
||||
|
||||
if (!callerUserId) {
|
||||
throw new Error("caller_user_id_required");
|
||||
}
|
||||
|
||||
const previousJobId = this.mentions.get(mentionPostId);
|
||||
if (previousJobId) {
|
||||
return {
|
||||
ok: true,
|
||||
deduped: true,
|
||||
job: this.jobs.get(previousJobId),
|
||||
};
|
||||
}
|
||||
|
||||
const articleResult = extractArticleFromParent(parentPost);
|
||||
if (!articleResult.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
status: "not_article",
|
||||
reason: articleResult.error,
|
||||
message: "This parent post is not an X Article.",
|
||||
};
|
||||
}
|
||||
|
||||
const creditsNeeded = calculateCredits(articleResult.article.charCount, this.creditConfig);
|
||||
|
||||
const jobId = String(this.nextJobId++);
|
||||
|
||||
this.wallets.applyTransaction({
|
||||
userId: callerUserId,
|
||||
type: "debit",
|
||||
amount: creditsNeeded,
|
||||
reason: "article_generation",
|
||||
idempotencyKey: `mention:${mentionPostId}:charge`,
|
||||
});
|
||||
|
||||
const assetId = String(this.nextAssetId++);
|
||||
const audioText = articleResult.article.content;
|
||||
const fakeAudioBytes = Buffer.byteLength(audioText, "utf8");
|
||||
|
||||
const asset = {
|
||||
id: assetId,
|
||||
ownerUserId: callerUserId,
|
||||
creditsCharged: creditsNeeded,
|
||||
articleTitle: articleResult.article.title,
|
||||
storageKey: `audio/${assetId}.mp3`,
|
||||
durationSec: Math.max(10, Math.ceil(audioText.length / 20)),
|
||||
sizeBytes: fakeAudioBytes,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const job = {
|
||||
id: jobId,
|
||||
mentionPostId,
|
||||
callerUserId,
|
||||
status: "completed",
|
||||
creditsCharged: creditsNeeded,
|
||||
article: articleResult.article,
|
||||
assetId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.assets.set(assetId, asset);
|
||||
this.jobs.set(jobId, job);
|
||||
this.mentions.set(mentionPostId, jobId);
|
||||
|
||||
this.access.grantAccess(assetId, callerUserId);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
deduped: false,
|
||||
job,
|
||||
reply: {
|
||||
message: `Your audiobook is ready: /audio/${assetId}`,
|
||||
publicLink: `/audio/${assetId}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getWalletBalance(userId) {
|
||||
return this.wallets.getBalance(userId);
|
||||
}
|
||||
|
||||
getJob(jobId) {
|
||||
return this.jobs.get(jobId) || null;
|
||||
}
|
||||
|
||||
getAsset(assetId) {
|
||||
return this.assets.get(String(assetId)) || null;
|
||||
}
|
||||
|
||||
checkAudioAccess(assetId, userId) {
|
||||
const audio = this.getAsset(assetId);
|
||||
if (!audio) {
|
||||
return { allowed: false, reason: "not_found" };
|
||||
}
|
||||
return this.access.canAccess({ audio, userId });
|
||||
}
|
||||
|
||||
unlockAudio(assetId, userId) {
|
||||
const audio = this.getAsset(assetId);
|
||||
if (!audio) {
|
||||
throw new Error("audio_not_found");
|
||||
}
|
||||
return this.access.unlockWithCredits({
|
||||
audio,
|
||||
userId,
|
||||
wallet: this.wallets,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
XArtAudioEngine,
|
||||
};
|
||||
Reference in New Issue
Block a user