@automagik/omni 2.260922.13 → 2.260922.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/server/index.js +632 -582
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -136088,8 +136088,12 @@ var init_types10 = __esm(() => {
|
|
|
136088
136088
|
|
|
136089
136089
|
// ../channel-slack/src/connection/bolt-client.ts
|
|
136090
136090
|
function buildActingClients(options, botClient) {
|
|
136091
|
-
if (options.authMode !== "user")
|
|
136091
|
+
if (options.authMode !== "user") {
|
|
136092
|
+
if (!botClient) {
|
|
136093
|
+
throw new SlackError(SlackErrorCode.INVALID_TOKEN, "botToken (xoxb-...) is required unless authMode is 'user'");
|
|
136094
|
+
}
|
|
136092
136095
|
return { actingClient: botClient };
|
|
136096
|
+
}
|
|
136093
136097
|
if (!options.userToken) {
|
|
136094
136098
|
throw new SlackError(SlackErrorCode.CONNECTION_FAILED, "userToken is required when authMode is 'user'");
|
|
136095
136099
|
}
|
|
@@ -136276,6 +136280,9 @@ var init_bolt_client = __esm(() => {
|
|
|
136276
136280
|
|
|
136277
136281
|
// ../channel-slack/src/connection/app-receiver.ts
|
|
136278
136282
|
import { createHash as createHash6 } from "crypto";
|
|
136283
|
+
function hasBotToken(attachment) {
|
|
136284
|
+
return Boolean(attachment.botToken);
|
|
136285
|
+
}
|
|
136279
136286
|
function receiverKeyFor(opts) {
|
|
136280
136287
|
const mode = opts.mode ?? "socket";
|
|
136281
136288
|
if (mode === "http") {
|
|
@@ -136425,8 +136432,7 @@ class SlackAppReceiver {
|
|
|
136425
136432
|
}
|
|
136426
136433
|
this.attachmentMap.delete(attachment.instanceId);
|
|
136427
136434
|
this.attachmentMap.set(attachment.instanceId, attachment);
|
|
136428
|
-
|
|
136429
|
-
this.botClients.set(newest.teamId, this.clientForBotToken(newest.botToken));
|
|
136435
|
+
this.refreshBotClient(attachment.teamId);
|
|
136430
136436
|
this.logger.debug("Instance attached to Slack receiver", {
|
|
136431
136437
|
receiver: this.key,
|
|
136432
136438
|
instanceId: attachment.instanceId,
|
|
@@ -136439,21 +136445,13 @@ class SlackAppReceiver {
|
|
|
136439
136445
|
if (!removed)
|
|
136440
136446
|
return false;
|
|
136441
136447
|
this.attachmentMap.delete(instanceId);
|
|
136442
|
-
|
|
136443
|
-
if (
|
|
136444
|
-
|
|
136445
|
-
|
|
136446
|
-
|
|
136447
|
-
|
|
136448
|
-
let tokenStillUsed = false;
|
|
136449
|
-
for (const attachment of this.attachmentMap.values()) {
|
|
136450
|
-
if (attachment.botToken === removed.botToken) {
|
|
136451
|
-
tokenStillUsed = true;
|
|
136452
|
-
break;
|
|
136453
|
-
}
|
|
136448
|
+
this.refreshBotClient(removed.teamId);
|
|
136449
|
+
if (hasBotToken(removed)) {
|
|
136450
|
+
const token = removed.botToken;
|
|
136451
|
+
const tokenStillUsed = [...this.attachmentMap.values()].some((attachment) => attachment.botToken === token);
|
|
136452
|
+
if (!tokenStillUsed)
|
|
136453
|
+
this.clientsByToken.delete(token);
|
|
136454
136454
|
}
|
|
136455
|
-
if (!tokenStillUsed)
|
|
136456
|
-
this.clientsByToken.delete(removed.botToken);
|
|
136457
136455
|
this.logger.debug("Instance detached from Slack receiver", {
|
|
136458
136456
|
receiver: this.key,
|
|
136459
136457
|
instanceId,
|
|
@@ -136471,16 +136469,25 @@ class SlackAppReceiver {
|
|
|
136471
136469
|
return [];
|
|
136472
136470
|
const teamAttachments = this.teamAttachmentsFor(teamId);
|
|
136473
136471
|
if (teamAttachments.length <= 1)
|
|
136474
|
-
return teamAttachments;
|
|
136472
|
+
return this.soleTargets(teamAttachments, body);
|
|
136475
136473
|
this.announceNarrowing(teamId, teamAttachments.length);
|
|
136476
136474
|
return selectAuthorizedTargets(teamAttachments, await this.authorizationsFor(body));
|
|
136477
136475
|
}
|
|
136476
|
+
async soleTargets(teamAttachments, body) {
|
|
136477
|
+
const [only] = teamAttachments;
|
|
136478
|
+
if (!only || hasBotToken(only))
|
|
136479
|
+
return teamAttachments;
|
|
136480
|
+
if (selectAuthorizedTargets(teamAttachments, body.authorizations ?? []).length > 0)
|
|
136481
|
+
return teamAttachments;
|
|
136482
|
+
return selectAuthorizedTargets(teamAttachments, await this.authorizationsFor(body));
|
|
136483
|
+
}
|
|
136478
136484
|
async targetsForActor(teamId, actorUserId) {
|
|
136479
136485
|
if (!teamId)
|
|
136480
136486
|
return [];
|
|
136481
136487
|
const teamAttachments = this.teamAttachmentsFor(teamId);
|
|
136482
|
-
if (teamAttachments.length <= 1)
|
|
136483
|
-
return teamAttachments;
|
|
136488
|
+
if (teamAttachments.length <= 1) {
|
|
136489
|
+
return teamAttachments.filter((attachment) => hasBotToken(attachment) || actorUserId !== undefined && attachment.actingUserId === actorUserId);
|
|
136490
|
+
}
|
|
136484
136491
|
const own = actorUserId ? teamAttachments.find((attachment) => attachment.authMode === "user" && attachment.actingUserId === actorUserId) : undefined;
|
|
136485
136492
|
if (own) {
|
|
136486
136493
|
this.announceNarrowing(teamId, teamAttachments.length);
|
|
@@ -136570,27 +136577,31 @@ class SlackAppReceiver {
|
|
|
136570
136577
|
}
|
|
136571
136578
|
async authorize(source) {
|
|
136572
136579
|
const teamId = source.teamId;
|
|
136573
|
-
|
|
136574
|
-
if (!attachment) {
|
|
136580
|
+
if (!teamId || this.teamAttachmentsFor(teamId).length === 0) {
|
|
136575
136581
|
throw new SlackError(SlackErrorCode.NOT_CONNECTED, `No Slack instance attached for team ${teamId ?? "(none)"} on receiver ${this.key}`, true);
|
|
136576
136582
|
}
|
|
136577
|
-
|
|
136578
|
-
|
|
136579
|
-
|
|
136580
|
-
|
|
136581
|
-
teamId: attachment.teamId
|
|
136582
|
-
};
|
|
136583
|
+
const bot = this.latestBotAttachmentFor(teamId);
|
|
136584
|
+
if (!bot)
|
|
136585
|
+
return { teamId };
|
|
136586
|
+
return { botToken: bot.botToken, botId: bot.botId, botUserId: bot.botUserId, teamId: bot.teamId };
|
|
136583
136587
|
}
|
|
136584
|
-
|
|
136588
|
+
latestBotAttachmentFor(teamId) {
|
|
136585
136589
|
let latest;
|
|
136586
136590
|
for (const attachment of this.attachmentMap.values()) {
|
|
136587
|
-
if (attachment.teamId !== teamId)
|
|
136591
|
+
if (attachment.teamId !== teamId || !hasBotToken(attachment))
|
|
136588
136592
|
continue;
|
|
136589
136593
|
if (!latest || attachment.attachedAt >= latest.attachedAt)
|
|
136590
136594
|
latest = attachment;
|
|
136591
136595
|
}
|
|
136592
136596
|
return latest;
|
|
136593
136597
|
}
|
|
136598
|
+
refreshBotClient(teamId) {
|
|
136599
|
+
const newest = this.latestBotAttachmentFor(teamId);
|
|
136600
|
+
if (newest)
|
|
136601
|
+
this.botClients.set(teamId, this.clientForBotToken(newest.botToken));
|
|
136602
|
+
else
|
|
136603
|
+
this.botClients.delete(teamId);
|
|
136604
|
+
}
|
|
136594
136605
|
botAttachmentFor(teamId) {
|
|
136595
136606
|
for (const attachment of this.attachmentMap.values()) {
|
|
136596
136607
|
if (attachment.teamId === teamId && attachment.authMode !== "user")
|
|
@@ -136707,7 +136718,7 @@ class SlackAppReceiver {
|
|
|
136707
136718
|
affected.push(attachment);
|
|
136708
136719
|
continue;
|
|
136709
136720
|
}
|
|
136710
|
-
const revoked = attachment.authMode === "user" ? attachment.actingUserId !== undefined && (tokens.oauth?.includes(attachment.actingUserId) ?? false) : tokens.bot?.includes(attachment.botUserId) ?? false;
|
|
136721
|
+
const revoked = attachment.authMode === "user" ? attachment.actingUserId !== undefined && (tokens.oauth?.includes(attachment.actingUserId) ?? false) : attachment.botUserId !== undefined && (tokens.bot?.includes(attachment.botUserId) ?? false);
|
|
136711
136722
|
if (revoked)
|
|
136712
136723
|
affected.push(attachment);
|
|
136713
136724
|
}
|
|
@@ -136810,7 +136821,7 @@ function headersWithOptionalAuthorization(botToken, preserveAuthorization) {
|
|
|
136810
136821
|
}
|
|
136811
136822
|
async function fetchSlackPrivateUrl(url, botToken) {
|
|
136812
136823
|
let currentUrl = new URL(url);
|
|
136813
|
-
let preserveAuthorization =
|
|
136824
|
+
let preserveAuthorization = isSlackHost(currentUrl);
|
|
136814
136825
|
for (let redirects = 0;redirects <= 5; redirects++) {
|
|
136815
136826
|
const response = await fetch(currentUrl.toString(), {
|
|
136816
136827
|
headers: headersWithOptionalAuthorization(botToken, preserveAuthorization),
|
|
@@ -137211,7 +137222,8 @@ function setupMessageHandlers2(app, target, botUserId, callbacks, dmPolicyConfig
|
|
|
137211
137222
|
const meta = extractMessageMeta(msg);
|
|
137212
137223
|
if (await enforceDmPolicy(meta, userId, dmPolicyConfig, instanceId, callbacks, logger5))
|
|
137213
137224
|
return;
|
|
137214
|
-
|
|
137225
|
+
const mentionUserId = resolveBotUserId() ?? resolveActingUserId();
|
|
137226
|
+
await processMessage2(instanceId, msg, mentionUserId, callbacks, logger5, reliability, filterConfig);
|
|
137215
137227
|
};
|
|
137216
137228
|
if (app) {
|
|
137217
137229
|
app.message(async ({ message: message2 }) => handle(message2));
|
|
@@ -149754,7 +149766,7 @@ function resolveSlackTokens(slackConfig, rawOptions, rawCredentials) {
|
|
|
149754
149766
|
const appToken = slackConfig.appToken ?? rawCredentials.appToken;
|
|
149755
149767
|
const signingSecret = slackConfig.signingSecret ?? rawCredentials.signingSecret;
|
|
149756
149768
|
const mode = slackConfig.mode ?? "socket";
|
|
149757
|
-
if (!botToken) {
|
|
149769
|
+
if (!botToken && authMode !== "user") {
|
|
149758
149770
|
throw new SlackError(SlackErrorCode.INVALID_TOKEN, "botToken (xoxb-...) is required");
|
|
149759
149771
|
}
|
|
149760
149772
|
if (authMode === "user" && !userToken) {
|
|
@@ -149774,6 +149786,16 @@ function resolveSlackTokens(slackConfig, rawOptions, rawCredentials) {
|
|
|
149774
149786
|
function readConnectOverrides(rawOptions) {
|
|
149775
149787
|
return { force: rawOptions.force === true };
|
|
149776
149788
|
}
|
|
149789
|
+
function resolveAttachmentTeamId(botTeamId, userTeamId) {
|
|
149790
|
+
if (botTeamId && userTeamId && botTeamId !== userTeamId) {
|
|
149791
|
+
throw new SlackError(SlackErrorCode.CONNECTION_FAILED, `The bot token belongs to workspace ${botTeamId} but the user token to ${userTeamId}; both must come from the same workspace`);
|
|
149792
|
+
}
|
|
149793
|
+
const teamId = botTeamId ?? userTeamId;
|
|
149794
|
+
if (!teamId) {
|
|
149795
|
+
throw new SlackError(SlackErrorCode.CONNECTION_FAILED, "auth.test returned no team_id for the user token; cannot tell which workspace this instance belongs to");
|
|
149796
|
+
}
|
|
149797
|
+
return teamId;
|
|
149798
|
+
}
|
|
149777
149799
|
function resolvePresentedIdentity(attachment) {
|
|
149778
149800
|
if (attachment.authMode === "user" && attachment.actingUserId) {
|
|
149779
149801
|
return {
|
|
@@ -149962,29 +149984,30 @@ var init_plugin = __esm(() => {
|
|
|
149962
149984
|
return { key, receiver };
|
|
149963
149985
|
}
|
|
149964
149986
|
async buildAttachment(instanceId, receiver, options, config2, caches) {
|
|
149965
|
-
const botClient = receiver.clientForBotToken(options.botToken);
|
|
149966
|
-
const identity = await resolveWorkspaceIdentity(botClient);
|
|
149987
|
+
const botClient = options.botToken ? receiver.clientForBotToken(options.botToken) : undefined;
|
|
149988
|
+
const identity = botClient ? await resolveWorkspaceIdentity(botClient) : undefined;
|
|
149967
149989
|
const { actingClient, userClient } = buildActingClients(options, botClient);
|
|
149968
149990
|
let actingUserId;
|
|
149969
149991
|
let actingUserName;
|
|
149992
|
+
let actingTeamId;
|
|
149970
149993
|
if (userClient) {
|
|
149971
|
-
({ actingUserId, actingUserName } = await this.resolveActingUserId(userClient));
|
|
149994
|
+
({ actingUserId, actingUserName, teamId: actingTeamId } = await this.resolveActingUserId(userClient));
|
|
149972
149995
|
if (!actingUserId) {
|
|
149973
149996
|
throw new SlackError(SlackErrorCode.CONNECTION_FAILED, "User mode requires a resolved acting user id, but it could not be determined from the user token. Refusing to connect.");
|
|
149974
149997
|
}
|
|
149975
149998
|
}
|
|
149976
149999
|
return {
|
|
149977
150000
|
instanceId,
|
|
149978
|
-
teamId: identity
|
|
150001
|
+
teamId: resolveAttachmentTeamId(identity?.teamId, actingTeamId),
|
|
149979
150002
|
authMode: options.authMode ?? "bot",
|
|
149980
150003
|
actingClient,
|
|
149981
150004
|
userClient,
|
|
149982
150005
|
actingUserId,
|
|
149983
150006
|
actingUserName,
|
|
149984
|
-
botUserId: identity
|
|
149985
|
-
botId: identity
|
|
149986
|
-
botToken: options.botToken,
|
|
149987
|
-
botName: identity
|
|
150007
|
+
botUserId: identity?.botUserId,
|
|
150008
|
+
botId: identity?.botId,
|
|
150009
|
+
botToken: botClient ? options.botToken : undefined,
|
|
150010
|
+
botName: identity?.botName,
|
|
149988
150011
|
config: config2,
|
|
149989
150012
|
dedupeCache: caches.dedupeCache,
|
|
149990
150013
|
debouncer: caches.debouncer,
|
|
@@ -150002,7 +150025,7 @@ var init_plugin = __esm(() => {
|
|
|
150002
150025
|
const username = auth.user ?? undefined;
|
|
150003
150026
|
const actingUserName = actingUserId ? await resolveActingUserName(userClient, actingUserId, username, this.logger) : username;
|
|
150004
150027
|
this.logger.info("Acting user identity resolved", { actingUserId, actingUser: actingUserName });
|
|
150005
|
-
return { actingUserId, actingUserName };
|
|
150028
|
+
return { actingUserId, actingUserName, teamId: auth.team_id ?? undefined };
|
|
150006
150029
|
} catch (error) {
|
|
150007
150030
|
this.logger.warn("Failed to resolve the acting user identity from the user token", {
|
|
150008
150031
|
error: String(error)
|
|
@@ -150578,12 +150601,12 @@ var init_plugin = __esm(() => {
|
|
|
150578
150601
|
if (!channelId || !threadTs)
|
|
150579
150602
|
return { totalFetched: 0, messages: [] };
|
|
150580
150603
|
const botUserId = attachment.botUserId;
|
|
150581
|
-
const
|
|
150604
|
+
const downloadToken = attachment.authMode === "user" && attachment.userClient?.token || attachment.botToken;
|
|
150582
150605
|
const limit = options.limit ?? 200;
|
|
150583
|
-
const messages = await this.paginateThreadHistory(attachment, channelId, threadTs, botUserId,
|
|
150606
|
+
const messages = await this.paginateThreadHistory(attachment, channelId, threadTs, botUserId, downloadToken, limit);
|
|
150584
150607
|
return { totalFetched: messages.length, messages };
|
|
150585
150608
|
}
|
|
150586
|
-
async paginateThreadHistory(attachment, channelId, threadTs, botUserId,
|
|
150609
|
+
async paginateThreadHistory(attachment, channelId, threadTs, botUserId, downloadToken, maxMessages) {
|
|
150587
150610
|
const messages = [];
|
|
150588
150611
|
let cursor;
|
|
150589
150612
|
do {
|
|
@@ -150594,7 +150617,7 @@ var init_plugin = __esm(() => {
|
|
|
150594
150617
|
cursor
|
|
150595
150618
|
});
|
|
150596
150619
|
for (const msg of response.messages ?? []) {
|
|
150597
|
-
const result = await this.buildHistorySyncMessage(msg, channelId, botUserId,
|
|
150620
|
+
const result = await this.buildHistorySyncMessage(msg, channelId, botUserId, downloadToken, attachment.botId);
|
|
150598
150621
|
if (result)
|
|
150599
150622
|
messages.push(result);
|
|
150600
150623
|
if (messages.length >= maxMessages)
|
|
@@ -150604,13 +150627,13 @@ var init_plugin = __esm(() => {
|
|
|
150604
150627
|
} while (cursor && messages.length < maxMessages);
|
|
150605
150628
|
return messages;
|
|
150606
150629
|
}
|
|
150607
|
-
async downloadSlackMediaToTemp(file,
|
|
150630
|
+
async downloadSlackMediaToTemp(file, downloadToken, ts) {
|
|
150608
150631
|
const mimeType = file.mimetype ?? "application/octet-stream";
|
|
150609
150632
|
const urlPrivate = file.url_private_download ?? file.url_private;
|
|
150610
|
-
if (!urlPrivate)
|
|
150633
|
+
if (!urlPrivate || !downloadToken)
|
|
150611
150634
|
return { mimeType, localPath: undefined };
|
|
150612
150635
|
try {
|
|
150613
|
-
const { buffer: buffer2 } = await downloadSlackFile(urlPrivate,
|
|
150636
|
+
const { buffer: buffer2 } = await downloadSlackFile(urlPrivate, downloadToken, this.logger);
|
|
150614
150637
|
const ext = (mimeType.split("/")[1]?.split(";")[0] ?? "bin").replace(/[^a-z0-9]/gi, "");
|
|
150615
150638
|
const tmpPath = join4(tmpdir(), `omni-slack-hist-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
|
|
150616
150639
|
await writeFile2(tmpPath, buffer2);
|
|
@@ -150620,7 +150643,7 @@ var init_plugin = __esm(() => {
|
|
|
150620
150643
|
return { mimeType, localPath: undefined };
|
|
150621
150644
|
}
|
|
150622
150645
|
}
|
|
150623
|
-
async buildHistorySyncMessage(msg, channelId, botUserId,
|
|
150646
|
+
async buildHistorySyncMessage(msg, channelId, botUserId, downloadToken, ownBotId) {
|
|
150624
150647
|
const userId = msg.user;
|
|
150625
150648
|
if (!userId || shouldSkipMessage(msg, [botUserId], ownBotId))
|
|
150626
150649
|
return null;
|
|
@@ -150629,7 +150652,7 @@ var init_plugin = __esm(() => {
|
|
|
150629
150652
|
const files = msg.files;
|
|
150630
150653
|
const timestamp = new Date(Number.parseFloat(ts) * 1000);
|
|
150631
150654
|
if (files && files.length > 0) {
|
|
150632
|
-
const { mimeType, localPath } = await this.downloadSlackMediaToTemp(files[0],
|
|
150655
|
+
const { mimeType, localPath } = await this.downloadSlackMediaToTemp(files[0], downloadToken, ts);
|
|
150633
150656
|
return {
|
|
150634
150657
|
externalId: ts,
|
|
150635
150658
|
chatId: channelId,
|
|
@@ -266412,7 +266435,7 @@ var init_sentry_scrub = __esm(() => {
|
|
|
266412
266435
|
var require_package7 = __commonJS((exports, module) => {
|
|
266413
266436
|
module.exports = {
|
|
266414
266437
|
name: "@omni/api",
|
|
266415
|
-
version: "2.260922.
|
|
266438
|
+
version: "2.260922.15",
|
|
266416
266439
|
private: true,
|
|
266417
266440
|
type: "module",
|
|
266418
266441
|
exports: {
|
|
@@ -408999,6 +409022,484 @@ var init_media3 = __esm(() => {
|
|
|
408999
409022
|
});
|
|
409000
409023
|
});
|
|
409001
409024
|
|
|
409025
|
+
// ../api/src/plugins/media-processor.ts
|
|
409026
|
+
function getContentFieldForType(processingType, contentType) {
|
|
409027
|
+
switch (processingType) {
|
|
409028
|
+
case "transcription":
|
|
409029
|
+
return "transcription";
|
|
409030
|
+
case "description":
|
|
409031
|
+
return contentType === "video" ? "videoDescription" : "imageDescription";
|
|
409032
|
+
case "extraction":
|
|
409033
|
+
return "documentExtraction";
|
|
409034
|
+
default:
|
|
409035
|
+
return;
|
|
409036
|
+
}
|
|
409037
|
+
}
|
|
409038
|
+
function inferProcessingType(contentType) {
|
|
409039
|
+
switch (contentType) {
|
|
409040
|
+
case "audio":
|
|
409041
|
+
return "transcription";
|
|
409042
|
+
case "image":
|
|
409043
|
+
case "video":
|
|
409044
|
+
return "description";
|
|
409045
|
+
case "document":
|
|
409046
|
+
return "extraction";
|
|
409047
|
+
default:
|
|
409048
|
+
return "transcription";
|
|
409049
|
+
}
|
|
409050
|
+
}
|
|
409051
|
+
function shouldProcess(contentType) {
|
|
409052
|
+
if (!contentType)
|
|
409053
|
+
return false;
|
|
409054
|
+
return PROCESSABLE_MEDIA_TYPES.has(contentType);
|
|
409055
|
+
}
|
|
409056
|
+
function isUuid(value) {
|
|
409057
|
+
return typeof value === "string" && isValidUuid(value);
|
|
409058
|
+
}
|
|
409059
|
+
function envelopeTenantId(envelope2) {
|
|
409060
|
+
const classification = classifyEnvelope(envelope2.metadata);
|
|
409061
|
+
return classification.world === "tenant" ? classification.tenantId : undefined;
|
|
409062
|
+
}
|
|
409063
|
+
function runMediaDb(ctx, trustedTenantId, fn) {
|
|
409064
|
+
return trustedTenantId === undefined ? fn() : runInWorkerTenantScope(ctx.db, trustedTenantId, fn);
|
|
409065
|
+
}
|
|
409066
|
+
function getMimeType2(content) {
|
|
409067
|
+
if (content.mimeType)
|
|
409068
|
+
return content.mimeType;
|
|
409069
|
+
switch (content.type) {
|
|
409070
|
+
case "audio":
|
|
409071
|
+
return "audio/ogg";
|
|
409072
|
+
case "image":
|
|
409073
|
+
return "image/jpeg";
|
|
409074
|
+
case "video":
|
|
409075
|
+
return "video/mp4";
|
|
409076
|
+
case "document":
|
|
409077
|
+
return "application/octet-stream";
|
|
409078
|
+
default:
|
|
409079
|
+
return;
|
|
409080
|
+
}
|
|
409081
|
+
}
|
|
409082
|
+
function isSlackFileUrl(mediaUrl) {
|
|
409083
|
+
let url;
|
|
409084
|
+
try {
|
|
409085
|
+
url = new URL(mediaUrl);
|
|
409086
|
+
} catch {
|
|
409087
|
+
return false;
|
|
409088
|
+
}
|
|
409089
|
+
return url.protocol === "https:" && hostMatchesSuffix4(url.hostname, "slack.com");
|
|
409090
|
+
}
|
|
409091
|
+
function selectSlackDownloadToken(instance4, mediaUrl) {
|
|
409092
|
+
if (!isSlackFileUrl(mediaUrl))
|
|
409093
|
+
return;
|
|
409094
|
+
const slackAuthMode = instance4.slackAuthMode;
|
|
409095
|
+
const slackUserToken = instance4.slackUserToken;
|
|
409096
|
+
if (slackAuthMode === "user" && typeof slackUserToken === "string" && slackUserToken.length > 0) {
|
|
409097
|
+
return slackUserToken;
|
|
409098
|
+
}
|
|
409099
|
+
return instance4.slackBotToken;
|
|
409100
|
+
}
|
|
409101
|
+
async function buildFetchOptions2(ctx, instanceId, mediaUrl, channelType, trustedTenantId) {
|
|
409102
|
+
if (channelType !== "slack")
|
|
409103
|
+
return;
|
|
409104
|
+
try {
|
|
409105
|
+
const instance4 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.instances.getById(instanceId));
|
|
409106
|
+
const token = selectSlackDownloadToken(instance4, mediaUrl);
|
|
409107
|
+
if (token) {
|
|
409108
|
+
return { headers: { Authorization: `Bearer ${token}` } };
|
|
409109
|
+
}
|
|
409110
|
+
} catch {}
|
|
409111
|
+
return;
|
|
409112
|
+
}
|
|
409113
|
+
async function downloadMediaFromUrl(ctx, instanceId, messageId, mediaUrl, mimeType, platformTimestamp, channelType, trustedTenantId) {
|
|
409114
|
+
const fetchOptions = await buildFetchOptions2(ctx, instanceId, mediaUrl, channelType, trustedTenantId);
|
|
409115
|
+
try {
|
|
409116
|
+
const result = await ctx.mediaStorage.storeFromUrl(instanceId, messageId, mediaUrl, mimeType, platformTimestamp, fetchOptions, trustedTenantId);
|
|
409117
|
+
await runMediaDb(ctx, trustedTenantId, () => ctx.mediaStorage.updateMessageLocalPath(messageId, result.localPath));
|
|
409118
|
+
log125.debug("Downloaded media from URL", { messageId, filePath: result.localPath });
|
|
409119
|
+
return result.localPath;
|
|
409120
|
+
} catch (error3) {
|
|
409121
|
+
log125.error("Failed to download media", { error: String(error3), mediaUrl });
|
|
409122
|
+
return null;
|
|
409123
|
+
}
|
|
409124
|
+
}
|
|
409125
|
+
async function downloadMediaViaPlugin(ctx, instanceId, messageId, channelType, mediaRef, fallbackMimeType, platformTimestamp, trustedTenantId) {
|
|
409126
|
+
const plugin13 = await (ctx.resolveChannelPlugin ?? getPlugin)(channelType);
|
|
409127
|
+
if (!plugin13?.downloadInboundMedia) {
|
|
409128
|
+
log125.warn("Channel plugin cannot materialize deferred media", { channelType, messageId });
|
|
409129
|
+
return null;
|
|
409130
|
+
}
|
|
409131
|
+
try {
|
|
409132
|
+
const { buffer: buffer3, mimeType } = pluginMediaSchema.parse(await plugin13.downloadInboundMedia(instanceId, mediaRef));
|
|
409133
|
+
const result = await ctx.mediaStorage.storeFromBuffer(instanceId, messageId, buffer3, mimeType || fallbackMimeType, platformTimestamp, trustedTenantId);
|
|
409134
|
+
await runMediaDb(ctx, trustedTenantId, () => ctx.mediaStorage.updateMessageLocalPath(messageId, result.localPath));
|
|
409135
|
+
log125.debug("Downloaded inbound media via plugin", { messageId, channelType, filePath: result.localPath });
|
|
409136
|
+
return result.localPath;
|
|
409137
|
+
} catch (error3) {
|
|
409138
|
+
log125.error("Failed to download inbound media via plugin", {
|
|
409139
|
+
error: String(error3),
|
|
409140
|
+
channelType,
|
|
409141
|
+
messageId
|
|
409142
|
+
});
|
|
409143
|
+
return null;
|
|
409144
|
+
}
|
|
409145
|
+
}
|
|
409146
|
+
async function resolveMediaPath2(ctx, instanceId, chatId, externalId, content, mimeType, channelType, trustedTenantId) {
|
|
409147
|
+
const maxWaitMs = 5000;
|
|
409148
|
+
const pollMs = 250;
|
|
409149
|
+
const deadline = Date.now() + maxWaitMs;
|
|
409150
|
+
let chat2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.chats.findByExternalIdSmart(instanceId, chatId));
|
|
409151
|
+
while (!chat2 && Date.now() < deadline) {
|
|
409152
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
409153
|
+
chat2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.chats.findByExternalIdSmart(instanceId, chatId));
|
|
409154
|
+
}
|
|
409155
|
+
if (!chat2) {
|
|
409156
|
+
log125.debug("Chat not found, cannot process media", { chatId, externalId });
|
|
409157
|
+
return null;
|
|
409158
|
+
}
|
|
409159
|
+
const chatDbId = chat2.id;
|
|
409160
|
+
let message2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.messages.getByExternalId(chatDbId, externalId));
|
|
409161
|
+
while (!message2 && Date.now() < deadline) {
|
|
409162
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
409163
|
+
message2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.messages.getByExternalId(chatDbId, externalId));
|
|
409164
|
+
}
|
|
409165
|
+
if (!message2) {
|
|
409166
|
+
log125.debug("Message not found after waiting, cannot process media", { externalId });
|
|
409167
|
+
return null;
|
|
409168
|
+
}
|
|
409169
|
+
let filePath = message2.mediaLocalPath;
|
|
409170
|
+
if (!filePath && content.mediaUrl) {
|
|
409171
|
+
filePath = await downloadMediaFromUrl(ctx, instanceId, message2.id, content.mediaUrl, mimeType, message2.platformTimestamp ?? undefined, channelType, trustedTenantId);
|
|
409172
|
+
if (!filePath)
|
|
409173
|
+
return null;
|
|
409174
|
+
}
|
|
409175
|
+
if (!filePath && content.mediaId && channelType) {
|
|
409176
|
+
filePath = await downloadMediaViaPlugin(ctx, instanceId, message2.id, channelType, content.mediaId, mimeType, message2.platformTimestamp ?? undefined, trustedTenantId);
|
|
409177
|
+
if (!filePath)
|
|
409178
|
+
return null;
|
|
409179
|
+
}
|
|
409180
|
+
if (!filePath) {
|
|
409181
|
+
log125.debug("No media file path available", { externalId });
|
|
409182
|
+
return null;
|
|
409183
|
+
}
|
|
409184
|
+
return {
|
|
409185
|
+
messageId: message2.id,
|
|
409186
|
+
filePath
|
|
409187
|
+
};
|
|
409188
|
+
}
|
|
409189
|
+
async function resolveSafeMediaContentEventId(ctx, envelope2, eventId) {
|
|
409190
|
+
if (!isUuid(eventId))
|
|
409191
|
+
return null;
|
|
409192
|
+
const id = eventId;
|
|
409193
|
+
const maxWaitMs = 250;
|
|
409194
|
+
const pollMs = 50;
|
|
409195
|
+
const deadline = Date.now() + maxWaitMs;
|
|
409196
|
+
while (true) {
|
|
409197
|
+
let found;
|
|
409198
|
+
try {
|
|
409199
|
+
found = await runConsumerInTenantContext(ctx.db, envelope2, async () => {
|
|
409200
|
+
const [event] = await scopedHandle(ctx.db).select({ id: omniEvents.id }).from(omniEvents).where(eq(omniEvents.id, id)).limit(1);
|
|
409201
|
+
return event?.id ?? null;
|
|
409202
|
+
});
|
|
409203
|
+
} catch (error3) {
|
|
409204
|
+
log125.debug("Failed to validate media_content event FK", { eventId: id, error: String(error3) });
|
|
409205
|
+
return null;
|
|
409206
|
+
}
|
|
409207
|
+
if (found)
|
|
409208
|
+
return found;
|
|
409209
|
+
if (Date.now() >= deadline) {
|
|
409210
|
+
log125.debug("Skipping media_content event FK; omni_event not found", { eventId: id });
|
|
409211
|
+
return null;
|
|
409212
|
+
}
|
|
409213
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
409214
|
+
}
|
|
409215
|
+
}
|
|
409216
|
+
async function persistProcessingResult(ctx, messageId, safeEventId, result, contentType) {
|
|
409217
|
+
if (result.content) {
|
|
409218
|
+
const updateField = getContentFieldForType(result.processingType, contentType);
|
|
409219
|
+
if (updateField) {
|
|
409220
|
+
await scopedHandle(ctx.db).update(messages2).set({ [updateField]: result.content }).where(eq(messages2.id, messageId));
|
|
409221
|
+
}
|
|
409222
|
+
}
|
|
409223
|
+
try {
|
|
409224
|
+
const sdb = scopedHandle(ctx.db);
|
|
409225
|
+
const row = {
|
|
409226
|
+
eventId: safeEventId ?? null,
|
|
409227
|
+
mediaId: messageId,
|
|
409228
|
+
processingType: result.processingType,
|
|
409229
|
+
content: result.content ?? "",
|
|
409230
|
+
model: result.model,
|
|
409231
|
+
provider: result.provider,
|
|
409232
|
+
language: result.language,
|
|
409233
|
+
duration: result.duration,
|
|
409234
|
+
tokensUsed: result.inputTokens ? result.inputTokens + (result.outputTokens ?? 0) : undefined,
|
|
409235
|
+
costUsd: result.costCents != null ? String(Math.round(result.costCents)) : null,
|
|
409236
|
+
processingTimeMs: result.processingTimeMs
|
|
409237
|
+
};
|
|
409238
|
+
if (currentTenantScope()) {
|
|
409239
|
+
await sdb.transaction(async (sp) => {
|
|
409240
|
+
await sp.insert(mediaContent).values(row);
|
|
409241
|
+
});
|
|
409242
|
+
} else {
|
|
409243
|
+
await sdb.insert(mediaContent).values(row);
|
|
409244
|
+
}
|
|
409245
|
+
} catch (error3) {
|
|
409246
|
+
log125.warn("Failed to insert media_content record (non-critical)", {
|
|
409247
|
+
messageId,
|
|
409248
|
+
error: String(error3)
|
|
409249
|
+
});
|
|
409250
|
+
}
|
|
409251
|
+
}
|
|
409252
|
+
function getPromptOverride(ctx, contentType) {
|
|
409253
|
+
if (contentType === "audio")
|
|
409254
|
+
return ctx.promptOverrides.audio;
|
|
409255
|
+
if (contentType === "image")
|
|
409256
|
+
return ctx.promptOverrides.image;
|
|
409257
|
+
if (contentType === "video")
|
|
409258
|
+
return ctx.promptOverrides.video;
|
|
409259
|
+
if (contentType === "document")
|
|
409260
|
+
return ctx.promptOverrides.document;
|
|
409261
|
+
return;
|
|
409262
|
+
}
|
|
409263
|
+
async function processMessageMedia(ctx, payload, metadata, envelope2) {
|
|
409264
|
+
const { instanceId, eventId } = metadata;
|
|
409265
|
+
const { content, externalId } = payload;
|
|
409266
|
+
const mimeType = getMimeType2(content);
|
|
409267
|
+
const trustedTenantId = envelopeTenantId(envelope2);
|
|
409268
|
+
if (!mimeType || !ctx.mediaService.canProcess(mimeType)) {
|
|
409269
|
+
log125.debug("MIME type not processable or missing", { mimeType, externalId });
|
|
409270
|
+
return;
|
|
409271
|
+
}
|
|
409272
|
+
const media = await resolveMediaPath2(ctx, instanceId, payload.chatId, externalId, content, mimeType, metadata.channelType, trustedTenantId);
|
|
409273
|
+
if (!media)
|
|
409274
|
+
return;
|
|
409275
|
+
const processable = await ctx.mediaStorage.materializeForProcessing(media.filePath);
|
|
409276
|
+
log125.info("Processing media", { messageId: media.messageId, mimeType, filePath: processable.path });
|
|
409277
|
+
let result;
|
|
409278
|
+
try {
|
|
409279
|
+
result = await ctx.mediaService.process(processable.path, mimeType, {
|
|
409280
|
+
language: ctx.defaultLanguage,
|
|
409281
|
+
caption: content.text,
|
|
409282
|
+
prompt: getPromptOverride(ctx, content.type)
|
|
409283
|
+
});
|
|
409284
|
+
} finally {
|
|
409285
|
+
await processable.cleanup();
|
|
409286
|
+
}
|
|
409287
|
+
if (!result.success) {
|
|
409288
|
+
const reason = result.errorMessage ?? "unknown";
|
|
409289
|
+
log125.warn("Media processing failed", { messageId: media.messageId, error: reason });
|
|
409290
|
+
const processingType = result.processingType ?? inferProcessingType(content.type);
|
|
409291
|
+
const errorColumn = getContentFieldForType(processingType, content.type);
|
|
409292
|
+
if (errorColumn) {
|
|
409293
|
+
const marker = `[error: media processing failed \u2014 ${reason}]`;
|
|
409294
|
+
await runConsumerInTenantContext(ctx.db, envelope2, async () => {
|
|
409295
|
+
await scopedHandle(ctx.db).update(messages2).set({ [errorColumn]: marker }).where(eq(messages2.id, media.messageId));
|
|
409296
|
+
});
|
|
409297
|
+
}
|
|
409298
|
+
await ctx.eventBus.publish("media.processing.failed", {
|
|
409299
|
+
eventId: eventId ?? media.messageId,
|
|
409300
|
+
mediaId: media.messageId,
|
|
409301
|
+
processingType,
|
|
409302
|
+
error: reason,
|
|
409303
|
+
provider: result.provider,
|
|
409304
|
+
model: result.model
|
|
409305
|
+
}, { instanceId, channelType: metadata.channelType });
|
|
409306
|
+
await ctx.eventBus.publish("media.processed", {
|
|
409307
|
+
eventId: eventId ?? media.messageId,
|
|
409308
|
+
mediaId: media.messageId,
|
|
409309
|
+
processingType,
|
|
409310
|
+
content: "",
|
|
409311
|
+
error: reason
|
|
409312
|
+
}, { instanceId, channelType: metadata.channelType });
|
|
409313
|
+
return;
|
|
409314
|
+
}
|
|
409315
|
+
const safeEventId = await resolveSafeMediaContentEventId(ctx, envelope2, eventId);
|
|
409316
|
+
await runConsumerInTenantContext(ctx.db, envelope2, () => persistProcessingResult(ctx, media.messageId, safeEventId, result, content.type));
|
|
409317
|
+
log125.info("Media processing complete", {
|
|
409318
|
+
messageId: media.messageId,
|
|
409319
|
+
processingType: result.processingType,
|
|
409320
|
+
provider: result.provider,
|
|
409321
|
+
model: result.model,
|
|
409322
|
+
costCents: result.costCents,
|
|
409323
|
+
processingTimeMs: result.processingTimeMs
|
|
409324
|
+
});
|
|
409325
|
+
await ctx.eventBus.publish("media.processed", {
|
|
409326
|
+
eventId: eventId ?? media.messageId,
|
|
409327
|
+
mediaId: media.messageId,
|
|
409328
|
+
processingType: result.processingType,
|
|
409329
|
+
content: result.content ?? "",
|
|
409330
|
+
model: result.model,
|
|
409331
|
+
provider: result.provider,
|
|
409332
|
+
tokensUsed: result.inputTokens ? result.inputTokens + (result.outputTokens ?? 0) : undefined
|
|
409333
|
+
}, { instanceId, channelType: metadata.channelType });
|
|
409334
|
+
}
|
|
409335
|
+
async function publishMediaCrashEvents(ctx, eventId, payload, metadata, error3, trustedTenantId) {
|
|
409336
|
+
try {
|
|
409337
|
+
const processingType = inferProcessingType(payload.content.type);
|
|
409338
|
+
const reason = `unexpected: ${String(error3)}`;
|
|
409339
|
+
const mediaId = await resolveMediaIdForCrash(ctx, metadata.instanceId ?? "", payload, trustedTenantId);
|
|
409340
|
+
const meta = { instanceId: metadata.instanceId, channelType: metadata.channelType };
|
|
409341
|
+
await ctx.eventBus.publish("media.processing.failed", {
|
|
409342
|
+
eventId,
|
|
409343
|
+
mediaId,
|
|
409344
|
+
processingType,
|
|
409345
|
+
error: reason,
|
|
409346
|
+
provider: "unknown",
|
|
409347
|
+
model: "unknown"
|
|
409348
|
+
}, meta);
|
|
409349
|
+
await ctx.eventBus.publish("media.processed", {
|
|
409350
|
+
eventId,
|
|
409351
|
+
mediaId,
|
|
409352
|
+
processingType,
|
|
409353
|
+
content: "",
|
|
409354
|
+
error: reason
|
|
409355
|
+
}, meta);
|
|
409356
|
+
} catch (publishError) {
|
|
409357
|
+
log125.error("Failed to publish media failure event", { error: String(publishError) });
|
|
409358
|
+
}
|
|
409359
|
+
}
|
|
409360
|
+
async function resolveMediaIdForCrash(ctx, instanceId, payload, trustedTenantId) {
|
|
409361
|
+
try {
|
|
409362
|
+
const msg = await runMediaDb(ctx, trustedTenantId, async () => {
|
|
409363
|
+
const chat2 = await ctx.services.chats.findByExternalIdSmart(instanceId, payload.chatId);
|
|
409364
|
+
if (!chat2)
|
|
409365
|
+
return null;
|
|
409366
|
+
return ctx.services.messages.getByExternalId(chat2.id, payload.externalId);
|
|
409367
|
+
});
|
|
409368
|
+
if (msg)
|
|
409369
|
+
return msg.id;
|
|
409370
|
+
} catch (lookupError) {
|
|
409371
|
+
log125.debug("Failed to resolve DB message UUID for crash handler, falling back to externalId", {
|
|
409372
|
+
error: String(lookupError),
|
|
409373
|
+
instanceId,
|
|
409374
|
+
chatId: payload.chatId
|
|
409375
|
+
});
|
|
409376
|
+
}
|
|
409377
|
+
return payload.externalId;
|
|
409378
|
+
}
|
|
409379
|
+
async function setupMediaProcessor(eventBus, db2, services) {
|
|
409380
|
+
const [
|
|
409381
|
+
groqApiKey,
|
|
409382
|
+
openaiApiKey,
|
|
409383
|
+
geminiApiKey,
|
|
409384
|
+
defaultLanguage,
|
|
409385
|
+
audioProvider,
|
|
409386
|
+
audioModel,
|
|
409387
|
+
geminiAudioModel,
|
|
409388
|
+
audioPrompt,
|
|
409389
|
+
imagePrompt,
|
|
409390
|
+
videoPrompt,
|
|
409391
|
+
documentPrompt,
|
|
409392
|
+
geminiVisionModel,
|
|
409393
|
+
openaiVisionModel
|
|
409394
|
+
] = await Promise.all([
|
|
409395
|
+
services.settings.getSecret("groq.api_key", "GROQ_API_KEY"),
|
|
409396
|
+
services.settings.getSecret("openai.api_key", "OPENAI_API_KEY"),
|
|
409397
|
+
services.settings.getSecret("gemini.api_key", "GEMINI_API_KEY"),
|
|
409398
|
+
services.settings.getString("media.default_language", "DEFAULT_LANGUAGE", "pt"),
|
|
409399
|
+
services.settings.getString("stt.provider", "STT_PROVIDER", "openai"),
|
|
409400
|
+
services.settings.getString("stt.openai.model", "OPENAI_STT_MODEL", OPENAI_TRANSCRIBE_MODEL),
|
|
409401
|
+
services.settings.getString("stt.gemini.model", "GEMINI_STT_MODEL", GEMINI_AUDIO_MODEL),
|
|
409402
|
+
services.settings.getString("prompt.audio_transcription"),
|
|
409403
|
+
services.settings.getString("prompt.image_description"),
|
|
409404
|
+
services.settings.getString("prompt.video_description"),
|
|
409405
|
+
services.settings.getString("prompt.document_ocr"),
|
|
409406
|
+
services.settings.getString("media.gemini.model", "GEMINI_MEDIA_MODEL", GEMINI_MODEL),
|
|
409407
|
+
services.settings.getString("media.openai.vision_model", "OPENAI_VISION_MODEL", OPENAI_VISION_MODEL)
|
|
409408
|
+
]);
|
|
409409
|
+
const mediaService = createMediaProcessingService({
|
|
409410
|
+
groqApiKey,
|
|
409411
|
+
openaiApiKey,
|
|
409412
|
+
geminiApiKey,
|
|
409413
|
+
defaultLanguage,
|
|
409414
|
+
audioProvider: audioProvider ?? "openai",
|
|
409415
|
+
audioModel: audioModel ?? OPENAI_TRANSCRIBE_MODEL,
|
|
409416
|
+
geminiAudioModel: geminiAudioModel ?? GEMINI_AUDIO_MODEL,
|
|
409417
|
+
geminiVisionModel: geminiVisionModel ?? GEMINI_MODEL,
|
|
409418
|
+
openaiVisionModel: openaiVisionModel ?? OPENAI_VISION_MODEL,
|
|
409419
|
+
audioPrompt: audioPrompt ?? undefined
|
|
409420
|
+
});
|
|
409421
|
+
const mediaStorage2 = new MediaStorageService(db2);
|
|
409422
|
+
const ctx = {
|
|
409423
|
+
db: db2,
|
|
409424
|
+
eventBus,
|
|
409425
|
+
services,
|
|
409426
|
+
mediaService,
|
|
409427
|
+
mediaStorage: mediaStorage2,
|
|
409428
|
+
defaultLanguage: defaultLanguage ?? "pt",
|
|
409429
|
+
promptOverrides: {
|
|
409430
|
+
audio: audioPrompt,
|
|
409431
|
+
image: imagePrompt,
|
|
409432
|
+
video: videoPrompt,
|
|
409433
|
+
document: documentPrompt
|
|
409434
|
+
}
|
|
409435
|
+
};
|
|
409436
|
+
await eventBus.subscribe("message.received", async (event) => {
|
|
409437
|
+
const payload = event.payload;
|
|
409438
|
+
const metadata = event.metadata;
|
|
409439
|
+
if (!metadata.instanceId)
|
|
409440
|
+
return;
|
|
409441
|
+
if (!shouldProcess(payload.content.type))
|
|
409442
|
+
return;
|
|
409443
|
+
try {
|
|
409444
|
+
await processMessageMedia(ctx, payload, {
|
|
409445
|
+
instanceId: metadata.instanceId,
|
|
409446
|
+
eventId: event.id,
|
|
409447
|
+
channelType: metadata.channelType
|
|
409448
|
+
}, event);
|
|
409449
|
+
} catch (error3) {
|
|
409450
|
+
log125.error("Failed to process media", {
|
|
409451
|
+
externalId: payload.externalId,
|
|
409452
|
+
error: String(error3)
|
|
409453
|
+
});
|
|
409454
|
+
await publishMediaCrashEvents(ctx, event.id, payload, metadata, error3, envelopeTenantId(event));
|
|
409455
|
+
}
|
|
409456
|
+
}, {
|
|
409457
|
+
durable: "media-processor",
|
|
409458
|
+
queue: "media-processor",
|
|
409459
|
+
maxRetries: 2,
|
|
409460
|
+
retryDelayMs: 1000,
|
|
409461
|
+
startFrom: "first",
|
|
409462
|
+
concurrency: 5
|
|
409463
|
+
});
|
|
409464
|
+
setGlobalCircuitBreakerStateChangeCallback((name, from, to) => {
|
|
409465
|
+
log125.warn("Circuit breaker state change", { provider: name, from, to });
|
|
409466
|
+
});
|
|
409467
|
+
await eventBus.subscribePattern("media.health.request", async (event) => {
|
|
409468
|
+
const tracker = getMediaHealthTracker();
|
|
409469
|
+
const report = tracker.getReport();
|
|
409470
|
+
const correlationId = event.metadata?.correlationId ?? event.id;
|
|
409471
|
+
await eventBus.publishGeneric("system.media.health.response", { correlationId, report }, { source: "media-processor" });
|
|
409472
|
+
log125.debug("Published media health report", {
|
|
409473
|
+
correlationId,
|
|
409474
|
+
totalRequests: report.overall.totalRequests,
|
|
409475
|
+
successRate: report.overall.successRate
|
|
409476
|
+
});
|
|
409477
|
+
}, {
|
|
409478
|
+
durable: "media-health-responder",
|
|
409479
|
+
queue: "media-health-responder"
|
|
409480
|
+
});
|
|
409481
|
+
log125.info("Media processor initialized");
|
|
409482
|
+
}
|
|
409483
|
+
var log125, PROCESSABLE_MEDIA_TYPES, pluginMediaSchema;
|
|
409484
|
+
var init_media_processor = __esm(() => {
|
|
409485
|
+
init_src();
|
|
409486
|
+
init_src6();
|
|
409487
|
+
init_src9();
|
|
409488
|
+
init_drizzle_orm();
|
|
409489
|
+
init_zod();
|
|
409490
|
+
init_media_storage();
|
|
409491
|
+
init_tenant_scope();
|
|
409492
|
+
init_worker_tenant_context();
|
|
409493
|
+
init_safe_media_fetch();
|
|
409494
|
+
init_loader2();
|
|
409495
|
+
log125 = createLogger("media-processor");
|
|
409496
|
+
PROCESSABLE_MEDIA_TYPES = new Set(["audio", "image", "document", "video"]);
|
|
409497
|
+
pluginMediaSchema = exports_external.object({
|
|
409498
|
+
buffer: exports_external.instanceof(Buffer),
|
|
409499
|
+
mimeType: exports_external.string().min(1).optional()
|
|
409500
|
+
});
|
|
409501
|
+
});
|
|
409502
|
+
|
|
409002
409503
|
// ../api/src/routes/v2/_close-contact-config.ts
|
|
409003
409504
|
function resolveCloseContactConfig(outcome, instanceSettings) {
|
|
409004
409505
|
const override = instanceSettings?.closeContactConfig;
|
|
@@ -409062,7 +409563,7 @@ async function resolveReactionTarget(services, instanceId, resolvedTo, messageId
|
|
|
409062
409563
|
recoverable: false
|
|
409063
409564
|
});
|
|
409064
409565
|
}
|
|
409065
|
-
|
|
409566
|
+
log126.warn("Reaction target chat not found in DB; deferring fromMe to channel plugin fallback (#386)", {
|
|
409066
409567
|
instanceId,
|
|
409067
409568
|
resolvedTo,
|
|
409068
409569
|
messageId,
|
|
@@ -409072,7 +409573,7 @@ async function resolveReactionTarget(services, instanceId, resolvedTo, messageId
|
|
|
409072
409573
|
}
|
|
409073
409574
|
const target = isUUID(messageId) ? await getReactionTargetByOmniId(services, instanceId, chat2.id, messageId) : await services.messages.getByExternalId(chat2.id, messageId);
|
|
409074
409575
|
if (!target) {
|
|
409075
|
-
|
|
409576
|
+
log126.warn("Reaction target message not found in DB; deferring fromMe to channel plugin fallback (#386)", {
|
|
409076
409577
|
instanceId,
|
|
409077
409578
|
chatId: chat2.id,
|
|
409078
409579
|
messageId,
|
|
@@ -409223,7 +409724,7 @@ function resolveSentByAgentId(sentBy, instance4) {
|
|
|
409223
409724
|
if (sentBy !== "agent")
|
|
409224
409725
|
return;
|
|
409225
409726
|
if (!instance4.agentId) {
|
|
409226
|
-
|
|
409727
|
+
log126.warn("sentBy=agent could not be attributed: instance has no configured agent", { instanceId: instance4.id });
|
|
409227
409728
|
return;
|
|
409228
409729
|
}
|
|
409229
409730
|
return instance4.agentId;
|
|
@@ -409279,14 +409780,14 @@ async function resolveMessageFromRef(services, ref) {
|
|
|
409279
409780
|
}
|
|
409280
409781
|
return found;
|
|
409281
409782
|
}
|
|
409282
|
-
function buildMediaDownloadFetchOptions(instance4) {
|
|
409783
|
+
function buildMediaDownloadFetchOptions(instance4, mediaUrl) {
|
|
409283
409784
|
if (instance4.channel !== "slack")
|
|
409284
409785
|
return;
|
|
409285
|
-
const
|
|
409286
|
-
if (!
|
|
409786
|
+
const token = selectSlackDownloadToken(instance4, mediaUrl);
|
|
409787
|
+
if (!token)
|
|
409287
409788
|
return;
|
|
409288
409789
|
return {
|
|
409289
|
-
headers: { Authorization: `Bearer ${
|
|
409790
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
409290
409791
|
preserveAuthRedirectHostSuffixes: ["slack.com"]
|
|
409291
409792
|
};
|
|
409292
409793
|
}
|
|
@@ -409339,10 +409840,10 @@ async function resolveChannelMessageId(services, messageId, instanceId) {
|
|
|
409339
409840
|
return messageId;
|
|
409340
409841
|
const message2 = await services.messages.getById(messageId);
|
|
409341
409842
|
await verifyMessageInstanceOwnership(services, message2, instanceId);
|
|
409342
|
-
|
|
409843
|
+
log126.debug("Resolved internal UUID to external ID", { messageId, externalId: message2.externalId });
|
|
409343
409844
|
return message2.externalId;
|
|
409344
409845
|
}
|
|
409345
|
-
var
|
|
409846
|
+
var log126, mediaDownloadLog, messagesRoutes, MIME_BY_EXTENSION, DEFAULT_MIME_BY_MEDIA_TYPE, UUID_REGEX2, permalinkQuerySchema, MessageSourceSchema, MessageTypeSchema, MessageStatusSchema, DeliveryStatusSchema, listQuerySchema15, createMessageSchema, updateMessageSchema, recordEditSchema, addReactionSchema, removeReactionSchema, updateDeliveryStatusSchema, sentByField, MentionSchema, sendTextSchema, sendMediaSchema, sendReactionSchema, sendStickerSchema, sendContactSchema, sendLocationSchema, messageRefSchema, _mediaStorageForDownload = null, sendTtsSchema, forwardMessageSchema, sendPresenceSchema, markMessageReadSchema, markBatchReadSchema, sendPollSchema, sendEmbedSchema, editMessageChannelSchema, deleteMessageChannelSchema, starMessageSchema;
|
|
409346
409847
|
var init_messages6 = __esm(() => {
|
|
409347
409848
|
init_dist6();
|
|
409348
409849
|
init_src2();
|
|
@@ -409353,13 +409854,14 @@ var init_messages6 = __esm(() => {
|
|
|
409353
409854
|
init_dist2();
|
|
409354
409855
|
init_zod();
|
|
409355
409856
|
init_sentry_scrub();
|
|
409857
|
+
init_media_processor();
|
|
409356
409858
|
init_date_query();
|
|
409357
409859
|
init_messages5();
|
|
409358
409860
|
init_api_keys();
|
|
409359
409861
|
init_media_storage();
|
|
409360
409862
|
init_tenant_scope();
|
|
409361
409863
|
init__close_contact_config();
|
|
409362
|
-
|
|
409864
|
+
log126 = createLogger("routes:messages");
|
|
409363
409865
|
mediaDownloadLog = createLogger("routes:messages:media-download");
|
|
409364
409866
|
messagesRoutes = new Hono2;
|
|
409365
409867
|
MIME_BY_EXTENSION = {
|
|
@@ -409668,7 +410170,7 @@ var init_messages6 = __esm(() => {
|
|
|
409668
410170
|
}
|
|
409669
410171
|
if (!cached) {
|
|
409670
410172
|
try {
|
|
409671
|
-
const result = await mediaStorage2.storeFromUrl(instanceId, message2.id, mediaUrl, message2.mediaMimeType ?? undefined, message2.platformTimestamp ?? undefined, buildMediaDownloadFetchOptions(instance4), currentTenantScope()?.tenantId ?? undefined);
|
|
410173
|
+
const result = await mediaStorage2.storeFromUrl(instanceId, message2.id, mediaUrl, message2.mediaMimeType ?? undefined, message2.platformTimestamp ?? undefined, buildMediaDownloadFetchOptions(instance4, mediaUrl), currentTenantScope()?.tenantId ?? undefined);
|
|
409672
410174
|
mediaLocalPath = result.localPath;
|
|
409673
410175
|
await mediaStorage2.updateMessageLocalPath(message2.id, result.localPath);
|
|
409674
410176
|
mediaDownloadLog.info("Downloaded and cached media", {
|
|
@@ -410297,7 +410799,7 @@ var init_messages6 = __esm(() => {
|
|
|
410297
410799
|
channelHandoffSupported: hasNativeHandoff,
|
|
410298
410800
|
...data.motivoHandoff ? { motivoHandoff: data.motivoHandoff } : {}
|
|
410299
410801
|
}
|
|
410300
|
-
}).catch((err2) =>
|
|
410802
|
+
}).catch((err2) => log126.warn("Failed to persist handoff log", { error: String(err2) }));
|
|
410301
410803
|
return c.json({
|
|
410302
410804
|
data: {
|
|
410303
410805
|
messageId: channelSendResult?.messageId ?? null,
|
|
@@ -410400,7 +410902,7 @@ var init_messages6 = __esm(() => {
|
|
|
410400
410902
|
closedFields: data.closeFields ?? null,
|
|
410401
410903
|
closedAt: closedAt.toISOString()
|
|
410402
410904
|
};
|
|
410403
|
-
services.eventBus.publish("chat.closed", payload, { instanceId: data.instanceId }).catch((err2) =>
|
|
410905
|
+
services.eventBus.publish("chat.closed", payload, { instanceId: data.instanceId }).catch((err2) => log126.debug("Failed to publish chat.closed", { error: String(err2) }));
|
|
410404
410906
|
}
|
|
410405
410907
|
return c.json({
|
|
410406
410908
|
data: {
|
|
@@ -412103,9 +412605,10 @@ function buildSlackAuthorizeUrl(input) {
|
|
|
412103
412605
|
const scopes = slackAuthorizeScopes();
|
|
412104
412606
|
const url = new URL(SLACK_AUTHORIZE_URL);
|
|
412105
412607
|
url.searchParams.set("client_id", input.clientId);
|
|
412106
|
-
url.searchParams.set("scope", scopes.scope);
|
|
412107
412608
|
if (input.mode === "user")
|
|
412108
412609
|
url.searchParams.set("user_scope", scopes.user_scope);
|
|
412610
|
+
else
|
|
412611
|
+
url.searchParams.set("scope", scopes.scope);
|
|
412109
412612
|
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
412110
412613
|
url.searchParams.set("state", input.state);
|
|
412111
412614
|
return url.toString();
|
|
@@ -412302,7 +412805,7 @@ function matchesIdentity(row, input) {
|
|
|
412302
412805
|
async function findExisting(instances2, input) {
|
|
412303
412806
|
const page = await instances2.list({ channel: ["slack"], limit: SLACK_LOOKUP_LIMIT });
|
|
412304
412807
|
if (page.hasMore) {
|
|
412305
|
-
|
|
412808
|
+
log127.warn("Slack OAuth identity lookup truncated; add a service finder", { limit: SLACK_LOOKUP_LIMIT });
|
|
412306
412809
|
}
|
|
412307
412810
|
return page.items.find((row) => matchesIdentity(row, input));
|
|
412308
412811
|
}
|
|
@@ -412320,7 +412823,7 @@ function isUniqueViolation(error3) {
|
|
|
412320
412823
|
}
|
|
412321
412824
|
function credentialColumns(input) {
|
|
412322
412825
|
return {
|
|
412323
|
-
slackBotToken: input.botToken,
|
|
412826
|
+
slackBotToken: input.mode === "bot" ? input.botToken ?? null : null,
|
|
412324
412827
|
slackUserToken: input.mode === "user" ? input.userToken ?? null : null,
|
|
412325
412828
|
slackAuthMode: input.mode,
|
|
412326
412829
|
slackAppToken: input.appToken,
|
|
@@ -412368,9 +412871,12 @@ function applyProfileMetadata(options, metadata) {
|
|
|
412368
412871
|
options.dmAllowlist = metadata.dmAllowlist;
|
|
412369
412872
|
}
|
|
412370
412873
|
function buildConnectOptions(input, row) {
|
|
412371
|
-
const options = { forceNewQr: false
|
|
412874
|
+
const options = { forceNewQr: false };
|
|
412372
412875
|
applyProfileMetadata(options, row.profileMetadata);
|
|
412373
|
-
|
|
412876
|
+
if (input.mode === "bot" && input.botToken) {
|
|
412877
|
+
options.token = input.botToken;
|
|
412878
|
+
options.botToken = input.botToken;
|
|
412879
|
+
}
|
|
412374
412880
|
if (input.mode === "user" && input.userToken)
|
|
412375
412881
|
options.userToken = input.userToken;
|
|
412376
412882
|
options.authMode = input.mode;
|
|
@@ -412389,7 +412895,7 @@ async function upsertSlackOAuthInstance(deps, input) {
|
|
|
412389
412895
|
const existing = await findExisting(deps.instances, input);
|
|
412390
412896
|
const row = existing ? await deps.instances.update(existing.id, credentialColumns(input)) : await createWithUniqueName(deps.instances, input);
|
|
412391
412897
|
const created = !existing;
|
|
412392
|
-
|
|
412898
|
+
log127.info(created ? "Slack OAuth instance created" : "Slack OAuth instance re-authorized", {
|
|
412393
412899
|
instanceId: row.id,
|
|
412394
412900
|
teamId: input.teamId,
|
|
412395
412901
|
mode: input.mode
|
|
@@ -412398,18 +412904,18 @@ async function upsertSlackOAuthInstance(deps, input) {
|
|
|
412398
412904
|
await plugin13.connect(row.id, { instanceId: row.id, credentials: {}, options: buildConnectOptions(input, row) });
|
|
412399
412905
|
} catch (error3) {
|
|
412400
412906
|
const connectError = redactSlackTokens(error3 instanceof Error ? error3.message : "Unknown error");
|
|
412401
|
-
|
|
412907
|
+
log127.error("Slack OAuth instance failed to connect", { instanceId: row.id, error: connectError });
|
|
412402
412908
|
return { instanceId: row.id, created, connectError };
|
|
412403
412909
|
}
|
|
412404
412910
|
return { instanceId: row.id, created };
|
|
412405
412911
|
}
|
|
412406
|
-
var
|
|
412912
|
+
var log127, SLACK_LOOKUP_LIMIT = 1000, NAME_MAX = 200, NAME_SUFFIX_ATTEMPTS = 20;
|
|
412407
412913
|
var init_slack_oauth2 = __esm(() => {
|
|
412408
412914
|
init_src4();
|
|
412409
412915
|
init_src();
|
|
412410
412916
|
init_slack_app();
|
|
412411
412917
|
init_slack_oauth();
|
|
412412
|
-
|
|
412918
|
+
log127 = createLogger("api:slack-oauth");
|
|
412413
412919
|
});
|
|
412414
412920
|
|
|
412415
412921
|
// ../api/src/routes/v2/slack.ts
|
|
@@ -412501,6 +413007,17 @@ async function resolveSlackAppForCallback(c, state) {
|
|
|
412501
413007
|
function slackError(code, message2) {
|
|
412502
413008
|
return `${code}: ${message2}`;
|
|
412503
413009
|
}
|
|
413010
|
+
function grantTokens(mode, access) {
|
|
413011
|
+
if (mode === "bot") {
|
|
413012
|
+
return access.access_token ? { ok: true, botToken: access.access_token } : { ok: false, code: "SLACK_OAUTH_EXCHANGE_FAILED", message: "Slack returned no workspace bot token" };
|
|
413013
|
+
}
|
|
413014
|
+
const userToken = access.authed_user.access_token;
|
|
413015
|
+
return userToken ? { ok: true, userToken } : {
|
|
413016
|
+
ok: false,
|
|
413017
|
+
code: "SLACK_USER_TOKEN_MISSING",
|
|
413018
|
+
message: "Slack granted no user token; the user scopes were not authorized"
|
|
413019
|
+
};
|
|
413020
|
+
}
|
|
412504
413021
|
async function completeSlackInstall(deps, pending2, code, credentials) {
|
|
412505
413022
|
const tenantId = pending2.tenantContext?.tenantId ?? null;
|
|
412506
413023
|
let access;
|
|
@@ -412519,22 +413036,21 @@ async function completeSlackInstall(deps, pending2, code, credentials) {
|
|
|
412519
413036
|
return errorOutcome(tenantId, "SLACK_ENTERPRISE_INSTALL_UNSUPPORTED", "Enterprise Grid (org-wide) installs are not supported; install into a single workspace");
|
|
412520
413037
|
}
|
|
412521
413038
|
const teamId = access.team?.id;
|
|
412522
|
-
if (!teamId
|
|
412523
|
-
return errorOutcome(tenantId, "SLACK_OAUTH_EXCHANGE_FAILED", "Slack returned no workspace
|
|
412524
|
-
}
|
|
412525
|
-
const userToken = access.authed_user.access_token;
|
|
412526
|
-
if (pending2.mode === "user" && !userToken) {
|
|
412527
|
-
return errorOutcome(tenantId, "SLACK_USER_TOKEN_MISSING", "Slack granted no user token; the user scopes were not authorized");
|
|
413039
|
+
if (!teamId) {
|
|
413040
|
+
return errorOutcome(tenantId, "SLACK_OAUTH_EXCHANGE_FAILED", "Slack returned no workspace id");
|
|
412528
413041
|
}
|
|
412529
|
-
const
|
|
413042
|
+
const tokens = grantTokens(pending2.mode, access);
|
|
413043
|
+
if (!tokens.ok)
|
|
413044
|
+
return errorOutcome(tenantId, tokens.code, tokens.message);
|
|
413045
|
+
const displayName = tokens.userToken ? await fetchSlackUserDisplayName(tokens.userToken, access.authed_user.id) : undefined;
|
|
412530
413046
|
const input = {
|
|
412531
413047
|
mode: pending2.mode,
|
|
412532
413048
|
teamId,
|
|
412533
413049
|
teamName: access.team?.name,
|
|
412534
413050
|
userId: access.authed_user.id,
|
|
412535
413051
|
displayName,
|
|
412536
|
-
botToken:
|
|
412537
|
-
userToken,
|
|
413052
|
+
botToken: tokens.botToken,
|
|
413053
|
+
userToken: tokens.userToken,
|
|
412538
413054
|
appToken: credentials.appToken,
|
|
412539
413055
|
signingSecret: credentials.signingSecret
|
|
412540
413056
|
};
|
|
@@ -412790,7 +413306,7 @@ function makeClient(cfg) {
|
|
|
412790
413306
|
function variablesToBodyParameters(variables) {
|
|
412791
413307
|
return Object.entries(variables).map(([k2, v2]) => [Number.parseInt(k2, 10), v2]).filter(([n2]) => Number.isFinite(n2) && n2 >= 1).sort((a, b3) => a[0] - b3[0]).map(([, v2]) => v2);
|
|
412792
413308
|
}
|
|
412793
|
-
var
|
|
413309
|
+
var log128, templatesRoutes, instanceAccess4, idParamSchema6, templateIdParamSchema, templateNameParamSchema, listQuerySchema20, createBodySchema, sendTestBodySchema, sendByNameBodySchema;
|
|
412794
413310
|
var init_templates3 = __esm(() => {
|
|
412795
413311
|
init_dist6();
|
|
412796
413312
|
init_src7();
|
|
@@ -412801,7 +413317,7 @@ var init_templates3 = __esm(() => {
|
|
|
412801
413317
|
init_dist2();
|
|
412802
413318
|
init_zod();
|
|
412803
413319
|
init_auth();
|
|
412804
|
-
|
|
413320
|
+
log128 = createLogger("routes:whatsapp-templates");
|
|
412805
413321
|
templatesRoutes = new Hono2;
|
|
412806
413322
|
instanceAccess4 = requireInstanceAccess((c) => c.req.param("id") ?? "");
|
|
412807
413323
|
idParamSchema6 = exports_external.object({
|
|
@@ -412928,7 +413444,7 @@ var init_templates3 = __esm(() => {
|
|
|
412928
413444
|
});
|
|
412929
413445
|
return c.json({ data: persisted.row }, persisted.created ? 201 : 200);
|
|
412930
413446
|
} catch (dbErr) {
|
|
412931
|
-
|
|
413447
|
+
log128.error("[whatsapp-business] local template mirror failed after Meta create", {
|
|
412932
413448
|
instanceId,
|
|
412933
413449
|
metaTemplateId: result.id,
|
|
412934
413450
|
name: body.name,
|
|
@@ -413630,7 +414146,7 @@ async function readProfilePhotoUpload(c) {
|
|
|
413630
414146
|
fileType: blob.type || "image/jpeg"
|
|
413631
414147
|
};
|
|
413632
414148
|
}
|
|
413633
|
-
var
|
|
414149
|
+
var log129, whatsappBusinessRoutes, instanceAccess5, analyticsQuerySchema2, updateProfileSchema;
|
|
413634
414150
|
var init_whatsapp_business3 = __esm(() => {
|
|
413635
414151
|
init_dist6();
|
|
413636
414152
|
init_src7();
|
|
@@ -413641,7 +414157,7 @@ var init_whatsapp_business3 = __esm(() => {
|
|
|
413641
414157
|
init_oauth_token_cache();
|
|
413642
414158
|
init_whatsapp_business_connection();
|
|
413643
414159
|
init_auth();
|
|
413644
|
-
|
|
414160
|
+
log129 = createLogger("api:whatsapp-business");
|
|
413645
414161
|
whatsappBusinessRoutes = new Hono2;
|
|
413646
414162
|
instanceAccess5 = requireInstanceAccess((c) => c.req.param("id") ?? "");
|
|
413647
414163
|
whatsappBusinessRoutes.use("/:id/whatsapp-business/*", instanceAccess5);
|
|
@@ -413667,7 +414183,7 @@ var init_whatsapp_business3 = __esm(() => {
|
|
|
413667
414183
|
});
|
|
413668
414184
|
} catch (err2) {
|
|
413669
414185
|
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
413670
|
-
|
|
414186
|
+
log129.error("OAuth exchange failed", { error: message2 });
|
|
413671
414187
|
return c.json({ error: { code: "OAUTH_EXCHANGE_FAILED", message: message2 } }, 400);
|
|
413672
414188
|
}
|
|
413673
414189
|
});
|
|
@@ -413727,7 +414243,7 @@ var init_whatsapp_business3 = __esm(() => {
|
|
|
413727
414243
|
await services.instances.update(id, { metaDisplayPhoneNumber: displayPhoneNumber });
|
|
413728
414244
|
}
|
|
413729
414245
|
} catch (err2) {
|
|
413730
|
-
|
|
414246
|
+
log129.warn("Failed to fetch phone_number info during connect \u2014 continuing", {
|
|
413731
414247
|
instanceId: id,
|
|
413732
414248
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
413733
414249
|
});
|
|
@@ -413753,7 +414269,7 @@ var init_whatsapp_business3 = __esm(() => {
|
|
|
413753
414269
|
});
|
|
413754
414270
|
} catch (err2) {
|
|
413755
414271
|
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
413756
|
-
|
|
414272
|
+
log129.error("plugin.connect failed for whatsapp-business", { instanceId: id, error: message2 });
|
|
413757
414273
|
return c.json({ error: { code: "CONNECT_FAILED", message: message2 } }, 500);
|
|
413758
414274
|
}
|
|
413759
414275
|
return c.json({
|
|
@@ -413781,7 +414297,7 @@ var init_whatsapp_business3 = __esm(() => {
|
|
|
413781
414297
|
return c.json({ ok: true });
|
|
413782
414298
|
} catch (err2) {
|
|
413783
414299
|
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
413784
|
-
|
|
414300
|
+
log129.error("register phone number failed", { instanceId: id, error: message2 });
|
|
413785
414301
|
return c.json({ error: { code: "REGISTER_FAILED", message: message2 } }, 500);
|
|
413786
414302
|
}
|
|
413787
414303
|
});
|
|
@@ -413800,7 +414316,7 @@ var init_whatsapp_business3 = __esm(() => {
|
|
|
413800
414316
|
return c.json({ subscribed: result.success });
|
|
413801
414317
|
} catch (err2) {
|
|
413802
414318
|
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
413803
|
-
|
|
414319
|
+
log129.error("subscribe app failed", { instanceId: id, error: message2 });
|
|
413804
414320
|
return c.json({ error: { code: "SUBSCRIBE_FAILED", message: message2 } }, 500);
|
|
413805
414321
|
}
|
|
413806
414322
|
});
|
|
@@ -413838,7 +414354,7 @@ var init_whatsapp_business3 = __esm(() => {
|
|
|
413838
414354
|
try {
|
|
413839
414355
|
await plugin13.disconnect(id);
|
|
413840
414356
|
} catch (err2) {
|
|
413841
|
-
|
|
414357
|
+
log129.warn("plugin.disconnect failed during connection delete", {
|
|
413842
414358
|
instanceId: id,
|
|
413843
414359
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
413844
414360
|
});
|
|
@@ -414051,7 +414567,7 @@ function flowJsonInvalid(issues) {
|
|
|
414051
414567
|
issues
|
|
414052
414568
|
};
|
|
414053
414569
|
}
|
|
414054
|
-
var
|
|
414570
|
+
var log130, whatsappFlowsRoutes, instanceAccess6, idParamSchema8, flowIdParamSchema, FlowCategorySchema2, createBodySchema2, updateBodySchema, sendBodySchema;
|
|
414055
414571
|
var init_whatsapp_flows3 = __esm(() => {
|
|
414056
414572
|
init_dist6();
|
|
414057
414573
|
init_src7();
|
|
@@ -414064,7 +414580,7 @@ var init_whatsapp_flows3 = __esm(() => {
|
|
|
414064
414580
|
init_auth();
|
|
414065
414581
|
init_sealed_credentials();
|
|
414066
414582
|
init_whatsapp_business3();
|
|
414067
|
-
|
|
414583
|
+
log130 = createLogger("routes:whatsapp-flows");
|
|
414068
414584
|
whatsappFlowsRoutes = new Hono2;
|
|
414069
414585
|
instanceAccess6 = requireInstanceAccess((c) => c.req.param("id") ?? "");
|
|
414070
414586
|
idParamSchema8 = exports_external.object({
|
|
@@ -414175,7 +414691,7 @@ var init_whatsapp_flows3 = __esm(() => {
|
|
|
414175
414691
|
}
|
|
414176
414692
|
});
|
|
414177
414693
|
const status = await resolved.client.getBusinessPublicKey().catch(() => null);
|
|
414178
|
-
|
|
414694
|
+
log130.info("flow encryption key registered", { instanceId });
|
|
414179
414695
|
return c.json({
|
|
414180
414696
|
data: {
|
|
414181
414697
|
uploaded: true,
|
|
@@ -414307,7 +414823,7 @@ var init_whatsapp_flows3 = __esm(() => {
|
|
|
414307
414823
|
metadata: { flow: { ...flow, flowToken } }
|
|
414308
414824
|
});
|
|
414309
414825
|
if (!result.success) {
|
|
414310
|
-
|
|
414826
|
+
log130.error("flow send failed", { instanceId, to, error: result.error, errorCode: result.errorCode });
|
|
414311
414827
|
return c.json(jsonError2(result.error ?? "Flow send failed", result.errorCode ?? "SEND_FAILED"), 500);
|
|
414312
414828
|
}
|
|
414313
414829
|
return c.json({ messageId: result.messageId, flowToken }, 201);
|
|
@@ -415379,12 +415895,12 @@ async function setupEventPersistence(eventBus, db2) {
|
|
|
415379
415895
|
const tracker = getJourneyTracker();
|
|
415380
415896
|
tracker.recordCheckpoint(metadata.correlationId, "T4", JOURNEY_STAGES.T4);
|
|
415381
415897
|
}
|
|
415382
|
-
|
|
415898
|
+
log131.debug("Persisted message.received", {
|
|
415383
415899
|
externalId: payload.externalId,
|
|
415384
415900
|
instanceId: metadata.instanceId
|
|
415385
415901
|
});
|
|
415386
415902
|
} catch (error3) {
|
|
415387
|
-
|
|
415903
|
+
log131.error("Failed to persist message.received", {
|
|
415388
415904
|
externalId: payload.externalId,
|
|
415389
415905
|
error: String(error3)
|
|
415390
415906
|
});
|
|
@@ -415430,12 +415946,12 @@ async function setupEventPersistence(eventBus, db2) {
|
|
|
415430
415946
|
};
|
|
415431
415947
|
await sdb.insert(omniEvents).values({ ...eventIdInsert(event.id), ...columns2 }).onConflictDoUpdate({ target: omniEvents.id, set: columns2 });
|
|
415432
415948
|
});
|
|
415433
|
-
|
|
415949
|
+
log131.debug("Persisted message.sent", {
|
|
415434
415950
|
externalId: payload.externalId,
|
|
415435
415951
|
instanceId: metadata.instanceId
|
|
415436
415952
|
});
|
|
415437
415953
|
} catch (error3) {
|
|
415438
|
-
|
|
415954
|
+
log131.error("Failed to persist message.sent", {
|
|
415439
415955
|
externalId: payload.externalId,
|
|
415440
415956
|
error: String(error3)
|
|
415441
415957
|
});
|
|
@@ -415472,11 +415988,11 @@ async function setupEventPersistence(eventBus, db2) {
|
|
|
415472
415988
|
await sdb.insert(omniEvents).values(newEvent).onConflictDoNothing({ target: omniEvents.id });
|
|
415473
415989
|
}
|
|
415474
415990
|
});
|
|
415475
|
-
|
|
415991
|
+
log131.debug("Persisted message.delivered", {
|
|
415476
415992
|
externalId: payload.externalId
|
|
415477
415993
|
});
|
|
415478
415994
|
} catch (error3) {
|
|
415479
|
-
|
|
415995
|
+
log131.error("Failed to persist message.delivered", {
|
|
415480
415996
|
externalId: payload.externalId,
|
|
415481
415997
|
error: String(error3)
|
|
415482
415998
|
});
|
|
@@ -415512,11 +416028,11 @@ async function setupEventPersistence(eventBus, db2) {
|
|
|
415512
416028
|
await sdb.insert(omniEvents).values(newEvent).onConflictDoNothing({ target: omniEvents.id });
|
|
415513
416029
|
}
|
|
415514
416030
|
});
|
|
415515
|
-
|
|
416031
|
+
log131.debug("Persisted message.read", {
|
|
415516
416032
|
externalId: payload.externalId
|
|
415517
416033
|
});
|
|
415518
416034
|
} catch (error3) {
|
|
415519
|
-
|
|
416035
|
+
log131.error("Failed to persist message.read", {
|
|
415520
416036
|
externalId: payload.externalId,
|
|
415521
416037
|
error: String(error3)
|
|
415522
416038
|
});
|
|
@@ -415568,14 +416084,14 @@ async function setupEventPersistence(eventBus, db2) {
|
|
|
415568
416084
|
}
|
|
415569
416085
|
return updated.length;
|
|
415570
416086
|
});
|
|
415571
|
-
|
|
416087
|
+
log131.debug("Persisted message.failed", {
|
|
415572
416088
|
chatId: payload.chatId,
|
|
415573
416089
|
externalId: payload.externalId,
|
|
415574
416090
|
updatedRows,
|
|
415575
416091
|
error: payload.error
|
|
415576
416092
|
});
|
|
415577
416093
|
} catch (error3) {
|
|
415578
|
-
|
|
416094
|
+
log131.error("Failed to persist message.failed", {
|
|
415579
416095
|
chatId: payload.chatId,
|
|
415580
416096
|
error: String(error3)
|
|
415581
416097
|
});
|
|
@@ -415610,9 +416126,9 @@ async function setupEventPersistence(eventBus, db2) {
|
|
|
415610
416126
|
};
|
|
415611
416127
|
await sdb.insert(omniEvents).values({ ...eventIdInsert(event.id), ...columns2 }).onConflictDoUpdate({ target: omniEvents.id, set: columns2 });
|
|
415612
416128
|
});
|
|
415613
|
-
|
|
416129
|
+
log131.debug(`Persisted ${type}`, { messageId: payload.messageId, emoji: payload.emoji });
|
|
415614
416130
|
} catch (error3) {
|
|
415615
|
-
|
|
416131
|
+
log131.error(`Failed to persist ${type}`, { messageId: payload.messageId, error: String(error3) });
|
|
415616
416132
|
}
|
|
415617
416133
|
}, { ...CONSUMER_OPTIONS, durable: `event-persistence-${type.replace(".", "-")}` });
|
|
415618
416134
|
}
|
|
@@ -415636,9 +416152,9 @@ async function setupEventPersistence(eventBus, db2) {
|
|
|
415636
416152
|
durable: "event-persistence-agent-run-completed",
|
|
415637
416153
|
startFrom: "new"
|
|
415638
416154
|
});
|
|
415639
|
-
|
|
416155
|
+
log131.info("Event persistence initialized - listening for message events");
|
|
415640
416156
|
} catch (error3) {
|
|
415641
|
-
|
|
416157
|
+
log131.error("Failed to set up event persistence", { error: String(error3) });
|
|
415642
416158
|
throw error3;
|
|
415643
416159
|
}
|
|
415644
416160
|
}
|
|
@@ -415676,14 +416192,14 @@ async function journalInternalEvent(db2, event) {
|
|
|
415676
416192
|
await sdb.insert(omniEvents).values(newEvent).onConflictDoNothing({ target: omniEvents.id });
|
|
415677
416193
|
});
|
|
415678
416194
|
} catch (error3) {
|
|
415679
|
-
|
|
416195
|
+
log131.error("Failed to persist custom event", {
|
|
415680
416196
|
eventType: event.type,
|
|
415681
416197
|
eventId: event.id,
|
|
415682
416198
|
error: String(error3)
|
|
415683
416199
|
});
|
|
415684
416200
|
}
|
|
415685
416201
|
}
|
|
415686
|
-
var
|
|
416202
|
+
var log131, NO_CHAT_LINK, CONSUMER_OPTIONS;
|
|
415687
416203
|
var init_event_persistence = __esm(() => {
|
|
415688
416204
|
init_src();
|
|
415689
416205
|
init_src6();
|
|
@@ -415691,7 +416207,7 @@ var init_event_persistence = __esm(() => {
|
|
|
415691
416207
|
init_chats();
|
|
415692
416208
|
init_tenant_scope();
|
|
415693
416209
|
init_worker_tenant_context();
|
|
415694
|
-
|
|
416210
|
+
log131 = createLogger("event-persistence");
|
|
415695
416211
|
NO_CHAT_LINK = { chatUuid: null, canonicalChatId: null };
|
|
415696
416212
|
CONSUMER_OPTIONS = {
|
|
415697
416213
|
queue: "event-persistence",
|
|
@@ -415702,472 +416218,6 @@ var init_event_persistence = __esm(() => {
|
|
|
415702
416218
|
};
|
|
415703
416219
|
});
|
|
415704
416220
|
|
|
415705
|
-
// ../api/src/plugins/media-processor.ts
|
|
415706
|
-
function getContentFieldForType(processingType, contentType) {
|
|
415707
|
-
switch (processingType) {
|
|
415708
|
-
case "transcription":
|
|
415709
|
-
return "transcription";
|
|
415710
|
-
case "description":
|
|
415711
|
-
return contentType === "video" ? "videoDescription" : "imageDescription";
|
|
415712
|
-
case "extraction":
|
|
415713
|
-
return "documentExtraction";
|
|
415714
|
-
default:
|
|
415715
|
-
return;
|
|
415716
|
-
}
|
|
415717
|
-
}
|
|
415718
|
-
function inferProcessingType(contentType) {
|
|
415719
|
-
switch (contentType) {
|
|
415720
|
-
case "audio":
|
|
415721
|
-
return "transcription";
|
|
415722
|
-
case "image":
|
|
415723
|
-
case "video":
|
|
415724
|
-
return "description";
|
|
415725
|
-
case "document":
|
|
415726
|
-
return "extraction";
|
|
415727
|
-
default:
|
|
415728
|
-
return "transcription";
|
|
415729
|
-
}
|
|
415730
|
-
}
|
|
415731
|
-
function shouldProcess(contentType) {
|
|
415732
|
-
if (!contentType)
|
|
415733
|
-
return false;
|
|
415734
|
-
return PROCESSABLE_MEDIA_TYPES.has(contentType);
|
|
415735
|
-
}
|
|
415736
|
-
function isUuid(value) {
|
|
415737
|
-
return typeof value === "string" && isValidUuid(value);
|
|
415738
|
-
}
|
|
415739
|
-
function envelopeTenantId(envelope2) {
|
|
415740
|
-
const classification = classifyEnvelope(envelope2.metadata);
|
|
415741
|
-
return classification.world === "tenant" ? classification.tenantId : undefined;
|
|
415742
|
-
}
|
|
415743
|
-
function runMediaDb(ctx, trustedTenantId, fn) {
|
|
415744
|
-
return trustedTenantId === undefined ? fn() : runInWorkerTenantScope(ctx.db, trustedTenantId, fn);
|
|
415745
|
-
}
|
|
415746
|
-
function getMimeType2(content) {
|
|
415747
|
-
if (content.mimeType)
|
|
415748
|
-
return content.mimeType;
|
|
415749
|
-
switch (content.type) {
|
|
415750
|
-
case "audio":
|
|
415751
|
-
return "audio/ogg";
|
|
415752
|
-
case "image":
|
|
415753
|
-
return "image/jpeg";
|
|
415754
|
-
case "video":
|
|
415755
|
-
return "video/mp4";
|
|
415756
|
-
case "document":
|
|
415757
|
-
return "application/octet-stream";
|
|
415758
|
-
default:
|
|
415759
|
-
return;
|
|
415760
|
-
}
|
|
415761
|
-
}
|
|
415762
|
-
function selectSlackDownloadToken(instance4) {
|
|
415763
|
-
const slackAuthMode = instance4.slackAuthMode;
|
|
415764
|
-
const slackUserToken = instance4.slackUserToken;
|
|
415765
|
-
if (slackAuthMode === "user" && typeof slackUserToken === "string" && slackUserToken.length > 0) {
|
|
415766
|
-
return slackUserToken;
|
|
415767
|
-
}
|
|
415768
|
-
return instance4.slackBotToken;
|
|
415769
|
-
}
|
|
415770
|
-
async function buildFetchOptions2(ctx, instanceId, channelType, trustedTenantId) {
|
|
415771
|
-
if (channelType !== "slack")
|
|
415772
|
-
return;
|
|
415773
|
-
try {
|
|
415774
|
-
const instance4 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.instances.getById(instanceId));
|
|
415775
|
-
const token = selectSlackDownloadToken(instance4);
|
|
415776
|
-
if (token) {
|
|
415777
|
-
return { headers: { Authorization: `Bearer ${token}` } };
|
|
415778
|
-
}
|
|
415779
|
-
} catch {}
|
|
415780
|
-
return;
|
|
415781
|
-
}
|
|
415782
|
-
async function downloadMediaFromUrl(ctx, instanceId, messageId, mediaUrl, mimeType, platformTimestamp, channelType, trustedTenantId) {
|
|
415783
|
-
const fetchOptions = await buildFetchOptions2(ctx, instanceId, channelType, trustedTenantId);
|
|
415784
|
-
try {
|
|
415785
|
-
const result = await ctx.mediaStorage.storeFromUrl(instanceId, messageId, mediaUrl, mimeType, platformTimestamp, fetchOptions, trustedTenantId);
|
|
415786
|
-
await runMediaDb(ctx, trustedTenantId, () => ctx.mediaStorage.updateMessageLocalPath(messageId, result.localPath));
|
|
415787
|
-
log131.debug("Downloaded media from URL", { messageId, filePath: result.localPath });
|
|
415788
|
-
return result.localPath;
|
|
415789
|
-
} catch (error3) {
|
|
415790
|
-
log131.error("Failed to download media", { error: String(error3), mediaUrl });
|
|
415791
|
-
return null;
|
|
415792
|
-
}
|
|
415793
|
-
}
|
|
415794
|
-
async function downloadMediaViaPlugin(ctx, instanceId, messageId, channelType, mediaRef, fallbackMimeType, platformTimestamp, trustedTenantId) {
|
|
415795
|
-
const plugin13 = await (ctx.resolveChannelPlugin ?? getPlugin)(channelType);
|
|
415796
|
-
if (!plugin13?.downloadInboundMedia) {
|
|
415797
|
-
log131.warn("Channel plugin cannot materialize deferred media", { channelType, messageId });
|
|
415798
|
-
return null;
|
|
415799
|
-
}
|
|
415800
|
-
try {
|
|
415801
|
-
const { buffer: buffer3, mimeType } = pluginMediaSchema.parse(await plugin13.downloadInboundMedia(instanceId, mediaRef));
|
|
415802
|
-
const result = await ctx.mediaStorage.storeFromBuffer(instanceId, messageId, buffer3, mimeType || fallbackMimeType, platformTimestamp, trustedTenantId);
|
|
415803
|
-
await runMediaDb(ctx, trustedTenantId, () => ctx.mediaStorage.updateMessageLocalPath(messageId, result.localPath));
|
|
415804
|
-
log131.debug("Downloaded inbound media via plugin", { messageId, channelType, filePath: result.localPath });
|
|
415805
|
-
return result.localPath;
|
|
415806
|
-
} catch (error3) {
|
|
415807
|
-
log131.error("Failed to download inbound media via plugin", {
|
|
415808
|
-
error: String(error3),
|
|
415809
|
-
channelType,
|
|
415810
|
-
messageId
|
|
415811
|
-
});
|
|
415812
|
-
return null;
|
|
415813
|
-
}
|
|
415814
|
-
}
|
|
415815
|
-
async function resolveMediaPath2(ctx, instanceId, chatId, externalId, content, mimeType, channelType, trustedTenantId) {
|
|
415816
|
-
const maxWaitMs = 5000;
|
|
415817
|
-
const pollMs = 250;
|
|
415818
|
-
const deadline = Date.now() + maxWaitMs;
|
|
415819
|
-
let chat2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.chats.findByExternalIdSmart(instanceId, chatId));
|
|
415820
|
-
while (!chat2 && Date.now() < deadline) {
|
|
415821
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
415822
|
-
chat2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.chats.findByExternalIdSmart(instanceId, chatId));
|
|
415823
|
-
}
|
|
415824
|
-
if (!chat2) {
|
|
415825
|
-
log131.debug("Chat not found, cannot process media", { chatId, externalId });
|
|
415826
|
-
return null;
|
|
415827
|
-
}
|
|
415828
|
-
const chatDbId = chat2.id;
|
|
415829
|
-
let message2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.messages.getByExternalId(chatDbId, externalId));
|
|
415830
|
-
while (!message2 && Date.now() < deadline) {
|
|
415831
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
415832
|
-
message2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.messages.getByExternalId(chatDbId, externalId));
|
|
415833
|
-
}
|
|
415834
|
-
if (!message2) {
|
|
415835
|
-
log131.debug("Message not found after waiting, cannot process media", { externalId });
|
|
415836
|
-
return null;
|
|
415837
|
-
}
|
|
415838
|
-
let filePath = message2.mediaLocalPath;
|
|
415839
|
-
if (!filePath && content.mediaUrl) {
|
|
415840
|
-
filePath = await downloadMediaFromUrl(ctx, instanceId, message2.id, content.mediaUrl, mimeType, message2.platformTimestamp ?? undefined, channelType, trustedTenantId);
|
|
415841
|
-
if (!filePath)
|
|
415842
|
-
return null;
|
|
415843
|
-
}
|
|
415844
|
-
if (!filePath && content.mediaId && channelType) {
|
|
415845
|
-
filePath = await downloadMediaViaPlugin(ctx, instanceId, message2.id, channelType, content.mediaId, mimeType, message2.platformTimestamp ?? undefined, trustedTenantId);
|
|
415846
|
-
if (!filePath)
|
|
415847
|
-
return null;
|
|
415848
|
-
}
|
|
415849
|
-
if (!filePath) {
|
|
415850
|
-
log131.debug("No media file path available", { externalId });
|
|
415851
|
-
return null;
|
|
415852
|
-
}
|
|
415853
|
-
return {
|
|
415854
|
-
messageId: message2.id,
|
|
415855
|
-
filePath
|
|
415856
|
-
};
|
|
415857
|
-
}
|
|
415858
|
-
async function resolveSafeMediaContentEventId(ctx, envelope2, eventId) {
|
|
415859
|
-
if (!isUuid(eventId))
|
|
415860
|
-
return null;
|
|
415861
|
-
const id = eventId;
|
|
415862
|
-
const maxWaitMs = 250;
|
|
415863
|
-
const pollMs = 50;
|
|
415864
|
-
const deadline = Date.now() + maxWaitMs;
|
|
415865
|
-
while (true) {
|
|
415866
|
-
let found;
|
|
415867
|
-
try {
|
|
415868
|
-
found = await runConsumerInTenantContext(ctx.db, envelope2, async () => {
|
|
415869
|
-
const [event] = await scopedHandle(ctx.db).select({ id: omniEvents.id }).from(omniEvents).where(eq(omniEvents.id, id)).limit(1);
|
|
415870
|
-
return event?.id ?? null;
|
|
415871
|
-
});
|
|
415872
|
-
} catch (error3) {
|
|
415873
|
-
log131.debug("Failed to validate media_content event FK", { eventId: id, error: String(error3) });
|
|
415874
|
-
return null;
|
|
415875
|
-
}
|
|
415876
|
-
if (found)
|
|
415877
|
-
return found;
|
|
415878
|
-
if (Date.now() >= deadline) {
|
|
415879
|
-
log131.debug("Skipping media_content event FK; omni_event not found", { eventId: id });
|
|
415880
|
-
return null;
|
|
415881
|
-
}
|
|
415882
|
-
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
415883
|
-
}
|
|
415884
|
-
}
|
|
415885
|
-
async function persistProcessingResult(ctx, messageId, safeEventId, result, contentType) {
|
|
415886
|
-
if (result.content) {
|
|
415887
|
-
const updateField = getContentFieldForType(result.processingType, contentType);
|
|
415888
|
-
if (updateField) {
|
|
415889
|
-
await scopedHandle(ctx.db).update(messages2).set({ [updateField]: result.content }).where(eq(messages2.id, messageId));
|
|
415890
|
-
}
|
|
415891
|
-
}
|
|
415892
|
-
try {
|
|
415893
|
-
const sdb = scopedHandle(ctx.db);
|
|
415894
|
-
const row = {
|
|
415895
|
-
eventId: safeEventId ?? null,
|
|
415896
|
-
mediaId: messageId,
|
|
415897
|
-
processingType: result.processingType,
|
|
415898
|
-
content: result.content ?? "",
|
|
415899
|
-
model: result.model,
|
|
415900
|
-
provider: result.provider,
|
|
415901
|
-
language: result.language,
|
|
415902
|
-
duration: result.duration,
|
|
415903
|
-
tokensUsed: result.inputTokens ? result.inputTokens + (result.outputTokens ?? 0) : undefined,
|
|
415904
|
-
costUsd: result.costCents != null ? String(Math.round(result.costCents)) : null,
|
|
415905
|
-
processingTimeMs: result.processingTimeMs
|
|
415906
|
-
};
|
|
415907
|
-
if (currentTenantScope()) {
|
|
415908
|
-
await sdb.transaction(async (sp) => {
|
|
415909
|
-
await sp.insert(mediaContent).values(row);
|
|
415910
|
-
});
|
|
415911
|
-
} else {
|
|
415912
|
-
await sdb.insert(mediaContent).values(row);
|
|
415913
|
-
}
|
|
415914
|
-
} catch (error3) {
|
|
415915
|
-
log131.warn("Failed to insert media_content record (non-critical)", {
|
|
415916
|
-
messageId,
|
|
415917
|
-
error: String(error3)
|
|
415918
|
-
});
|
|
415919
|
-
}
|
|
415920
|
-
}
|
|
415921
|
-
function getPromptOverride(ctx, contentType) {
|
|
415922
|
-
if (contentType === "audio")
|
|
415923
|
-
return ctx.promptOverrides.audio;
|
|
415924
|
-
if (contentType === "image")
|
|
415925
|
-
return ctx.promptOverrides.image;
|
|
415926
|
-
if (contentType === "video")
|
|
415927
|
-
return ctx.promptOverrides.video;
|
|
415928
|
-
if (contentType === "document")
|
|
415929
|
-
return ctx.promptOverrides.document;
|
|
415930
|
-
return;
|
|
415931
|
-
}
|
|
415932
|
-
async function processMessageMedia(ctx, payload, metadata, envelope2) {
|
|
415933
|
-
const { instanceId, eventId } = metadata;
|
|
415934
|
-
const { content, externalId } = payload;
|
|
415935
|
-
const mimeType = getMimeType2(content);
|
|
415936
|
-
const trustedTenantId = envelopeTenantId(envelope2);
|
|
415937
|
-
if (!mimeType || !ctx.mediaService.canProcess(mimeType)) {
|
|
415938
|
-
log131.debug("MIME type not processable or missing", { mimeType, externalId });
|
|
415939
|
-
return;
|
|
415940
|
-
}
|
|
415941
|
-
const media = await resolveMediaPath2(ctx, instanceId, payload.chatId, externalId, content, mimeType, metadata.channelType, trustedTenantId);
|
|
415942
|
-
if (!media)
|
|
415943
|
-
return;
|
|
415944
|
-
const processable = await ctx.mediaStorage.materializeForProcessing(media.filePath);
|
|
415945
|
-
log131.info("Processing media", { messageId: media.messageId, mimeType, filePath: processable.path });
|
|
415946
|
-
let result;
|
|
415947
|
-
try {
|
|
415948
|
-
result = await ctx.mediaService.process(processable.path, mimeType, {
|
|
415949
|
-
language: ctx.defaultLanguage,
|
|
415950
|
-
caption: content.text,
|
|
415951
|
-
prompt: getPromptOverride(ctx, content.type)
|
|
415952
|
-
});
|
|
415953
|
-
} finally {
|
|
415954
|
-
await processable.cleanup();
|
|
415955
|
-
}
|
|
415956
|
-
if (!result.success) {
|
|
415957
|
-
const reason2 = result.errorMessage ?? "unknown";
|
|
415958
|
-
log131.warn("Media processing failed", { messageId: media.messageId, error: reason2 });
|
|
415959
|
-
const processingType = result.processingType ?? inferProcessingType(content.type);
|
|
415960
|
-
const errorColumn = getContentFieldForType(processingType, content.type);
|
|
415961
|
-
if (errorColumn) {
|
|
415962
|
-
const marker = `[error: media processing failed \u2014 ${reason2}]`;
|
|
415963
|
-
await runConsumerInTenantContext(ctx.db, envelope2, async () => {
|
|
415964
|
-
await scopedHandle(ctx.db).update(messages2).set({ [errorColumn]: marker }).where(eq(messages2.id, media.messageId));
|
|
415965
|
-
});
|
|
415966
|
-
}
|
|
415967
|
-
await ctx.eventBus.publish("media.processing.failed", {
|
|
415968
|
-
eventId: eventId ?? media.messageId,
|
|
415969
|
-
mediaId: media.messageId,
|
|
415970
|
-
processingType,
|
|
415971
|
-
error: reason2,
|
|
415972
|
-
provider: result.provider,
|
|
415973
|
-
model: result.model
|
|
415974
|
-
}, { instanceId, channelType: metadata.channelType });
|
|
415975
|
-
await ctx.eventBus.publish("media.processed", {
|
|
415976
|
-
eventId: eventId ?? media.messageId,
|
|
415977
|
-
mediaId: media.messageId,
|
|
415978
|
-
processingType,
|
|
415979
|
-
content: "",
|
|
415980
|
-
error: reason2
|
|
415981
|
-
}, { instanceId, channelType: metadata.channelType });
|
|
415982
|
-
return;
|
|
415983
|
-
}
|
|
415984
|
-
const safeEventId = await resolveSafeMediaContentEventId(ctx, envelope2, eventId);
|
|
415985
|
-
await runConsumerInTenantContext(ctx.db, envelope2, () => persistProcessingResult(ctx, media.messageId, safeEventId, result, content.type));
|
|
415986
|
-
log131.info("Media processing complete", {
|
|
415987
|
-
messageId: media.messageId,
|
|
415988
|
-
processingType: result.processingType,
|
|
415989
|
-
provider: result.provider,
|
|
415990
|
-
model: result.model,
|
|
415991
|
-
costCents: result.costCents,
|
|
415992
|
-
processingTimeMs: result.processingTimeMs
|
|
415993
|
-
});
|
|
415994
|
-
await ctx.eventBus.publish("media.processed", {
|
|
415995
|
-
eventId: eventId ?? media.messageId,
|
|
415996
|
-
mediaId: media.messageId,
|
|
415997
|
-
processingType: result.processingType,
|
|
415998
|
-
content: result.content ?? "",
|
|
415999
|
-
model: result.model,
|
|
416000
|
-
provider: result.provider,
|
|
416001
|
-
tokensUsed: result.inputTokens ? result.inputTokens + (result.outputTokens ?? 0) : undefined
|
|
416002
|
-
}, { instanceId, channelType: metadata.channelType });
|
|
416003
|
-
}
|
|
416004
|
-
async function publishMediaCrashEvents(ctx, eventId, payload, metadata, error3, trustedTenantId) {
|
|
416005
|
-
try {
|
|
416006
|
-
const processingType = inferProcessingType(payload.content.type);
|
|
416007
|
-
const reason2 = `unexpected: ${String(error3)}`;
|
|
416008
|
-
const mediaId = await resolveMediaIdForCrash(ctx, metadata.instanceId ?? "", payload, trustedTenantId);
|
|
416009
|
-
const meta = { instanceId: metadata.instanceId, channelType: metadata.channelType };
|
|
416010
|
-
await ctx.eventBus.publish("media.processing.failed", {
|
|
416011
|
-
eventId,
|
|
416012
|
-
mediaId,
|
|
416013
|
-
processingType,
|
|
416014
|
-
error: reason2,
|
|
416015
|
-
provider: "unknown",
|
|
416016
|
-
model: "unknown"
|
|
416017
|
-
}, meta);
|
|
416018
|
-
await ctx.eventBus.publish("media.processed", {
|
|
416019
|
-
eventId,
|
|
416020
|
-
mediaId,
|
|
416021
|
-
processingType,
|
|
416022
|
-
content: "",
|
|
416023
|
-
error: reason2
|
|
416024
|
-
}, meta);
|
|
416025
|
-
} catch (publishError) {
|
|
416026
|
-
log131.error("Failed to publish media failure event", { error: String(publishError) });
|
|
416027
|
-
}
|
|
416028
|
-
}
|
|
416029
|
-
async function resolveMediaIdForCrash(ctx, instanceId, payload, trustedTenantId) {
|
|
416030
|
-
try {
|
|
416031
|
-
const msg = await runMediaDb(ctx, trustedTenantId, async () => {
|
|
416032
|
-
const chat2 = await ctx.services.chats.findByExternalIdSmart(instanceId, payload.chatId);
|
|
416033
|
-
if (!chat2)
|
|
416034
|
-
return null;
|
|
416035
|
-
return ctx.services.messages.getByExternalId(chat2.id, payload.externalId);
|
|
416036
|
-
});
|
|
416037
|
-
if (msg)
|
|
416038
|
-
return msg.id;
|
|
416039
|
-
} catch (lookupError) {
|
|
416040
|
-
log131.debug("Failed to resolve DB message UUID for crash handler, falling back to externalId", {
|
|
416041
|
-
error: String(lookupError),
|
|
416042
|
-
instanceId,
|
|
416043
|
-
chatId: payload.chatId
|
|
416044
|
-
});
|
|
416045
|
-
}
|
|
416046
|
-
return payload.externalId;
|
|
416047
|
-
}
|
|
416048
|
-
async function setupMediaProcessor(eventBus, db2, services) {
|
|
416049
|
-
const [
|
|
416050
|
-
groqApiKey,
|
|
416051
|
-
openaiApiKey,
|
|
416052
|
-
geminiApiKey,
|
|
416053
|
-
defaultLanguage,
|
|
416054
|
-
audioProvider,
|
|
416055
|
-
audioModel,
|
|
416056
|
-
geminiAudioModel,
|
|
416057
|
-
audioPrompt,
|
|
416058
|
-
imagePrompt,
|
|
416059
|
-
videoPrompt,
|
|
416060
|
-
documentPrompt,
|
|
416061
|
-
geminiVisionModel,
|
|
416062
|
-
openaiVisionModel
|
|
416063
|
-
] = await Promise.all([
|
|
416064
|
-
services.settings.getSecret("groq.api_key", "GROQ_API_KEY"),
|
|
416065
|
-
services.settings.getSecret("openai.api_key", "OPENAI_API_KEY"),
|
|
416066
|
-
services.settings.getSecret("gemini.api_key", "GEMINI_API_KEY"),
|
|
416067
|
-
services.settings.getString("media.default_language", "DEFAULT_LANGUAGE", "pt"),
|
|
416068
|
-
services.settings.getString("stt.provider", "STT_PROVIDER", "openai"),
|
|
416069
|
-
services.settings.getString("stt.openai.model", "OPENAI_STT_MODEL", OPENAI_TRANSCRIBE_MODEL),
|
|
416070
|
-
services.settings.getString("stt.gemini.model", "GEMINI_STT_MODEL", GEMINI_AUDIO_MODEL),
|
|
416071
|
-
services.settings.getString("prompt.audio_transcription"),
|
|
416072
|
-
services.settings.getString("prompt.image_description"),
|
|
416073
|
-
services.settings.getString("prompt.video_description"),
|
|
416074
|
-
services.settings.getString("prompt.document_ocr"),
|
|
416075
|
-
services.settings.getString("media.gemini.model", "GEMINI_MEDIA_MODEL", GEMINI_MODEL),
|
|
416076
|
-
services.settings.getString("media.openai.vision_model", "OPENAI_VISION_MODEL", OPENAI_VISION_MODEL)
|
|
416077
|
-
]);
|
|
416078
|
-
const mediaService = createMediaProcessingService({
|
|
416079
|
-
groqApiKey,
|
|
416080
|
-
openaiApiKey,
|
|
416081
|
-
geminiApiKey,
|
|
416082
|
-
defaultLanguage,
|
|
416083
|
-
audioProvider: audioProvider ?? "openai",
|
|
416084
|
-
audioModel: audioModel ?? OPENAI_TRANSCRIBE_MODEL,
|
|
416085
|
-
geminiAudioModel: geminiAudioModel ?? GEMINI_AUDIO_MODEL,
|
|
416086
|
-
geminiVisionModel: geminiVisionModel ?? GEMINI_MODEL,
|
|
416087
|
-
openaiVisionModel: openaiVisionModel ?? OPENAI_VISION_MODEL,
|
|
416088
|
-
audioPrompt: audioPrompt ?? undefined
|
|
416089
|
-
});
|
|
416090
|
-
const mediaStorage2 = new MediaStorageService(db2);
|
|
416091
|
-
const ctx = {
|
|
416092
|
-
db: db2,
|
|
416093
|
-
eventBus,
|
|
416094
|
-
services,
|
|
416095
|
-
mediaService,
|
|
416096
|
-
mediaStorage: mediaStorage2,
|
|
416097
|
-
defaultLanguage: defaultLanguage ?? "pt",
|
|
416098
|
-
promptOverrides: {
|
|
416099
|
-
audio: audioPrompt,
|
|
416100
|
-
image: imagePrompt,
|
|
416101
|
-
video: videoPrompt,
|
|
416102
|
-
document: documentPrompt
|
|
416103
|
-
}
|
|
416104
|
-
};
|
|
416105
|
-
await eventBus.subscribe("message.received", async (event) => {
|
|
416106
|
-
const payload = event.payload;
|
|
416107
|
-
const metadata = event.metadata;
|
|
416108
|
-
if (!metadata.instanceId)
|
|
416109
|
-
return;
|
|
416110
|
-
if (!shouldProcess(payload.content.type))
|
|
416111
|
-
return;
|
|
416112
|
-
try {
|
|
416113
|
-
await processMessageMedia(ctx, payload, {
|
|
416114
|
-
instanceId: metadata.instanceId,
|
|
416115
|
-
eventId: event.id,
|
|
416116
|
-
channelType: metadata.channelType
|
|
416117
|
-
}, event);
|
|
416118
|
-
} catch (error3) {
|
|
416119
|
-
log131.error("Failed to process media", {
|
|
416120
|
-
externalId: payload.externalId,
|
|
416121
|
-
error: String(error3)
|
|
416122
|
-
});
|
|
416123
|
-
await publishMediaCrashEvents(ctx, event.id, payload, metadata, error3, envelopeTenantId(event));
|
|
416124
|
-
}
|
|
416125
|
-
}, {
|
|
416126
|
-
durable: "media-processor",
|
|
416127
|
-
queue: "media-processor",
|
|
416128
|
-
maxRetries: 2,
|
|
416129
|
-
retryDelayMs: 1000,
|
|
416130
|
-
startFrom: "first",
|
|
416131
|
-
concurrency: 5
|
|
416132
|
-
});
|
|
416133
|
-
setGlobalCircuitBreakerStateChangeCallback((name, from, to) => {
|
|
416134
|
-
log131.warn("Circuit breaker state change", { provider: name, from, to });
|
|
416135
|
-
});
|
|
416136
|
-
await eventBus.subscribePattern("media.health.request", async (event) => {
|
|
416137
|
-
const tracker = getMediaHealthTracker();
|
|
416138
|
-
const report = tracker.getReport();
|
|
416139
|
-
const correlationId = event.metadata?.correlationId ?? event.id;
|
|
416140
|
-
await eventBus.publishGeneric("system.media.health.response", { correlationId, report }, { source: "media-processor" });
|
|
416141
|
-
log131.debug("Published media health report", {
|
|
416142
|
-
correlationId,
|
|
416143
|
-
totalRequests: report.overall.totalRequests,
|
|
416144
|
-
successRate: report.overall.successRate
|
|
416145
|
-
});
|
|
416146
|
-
}, {
|
|
416147
|
-
durable: "media-health-responder",
|
|
416148
|
-
queue: "media-health-responder"
|
|
416149
|
-
});
|
|
416150
|
-
log131.info("Media processor initialized");
|
|
416151
|
-
}
|
|
416152
|
-
var log131, PROCESSABLE_MEDIA_TYPES, pluginMediaSchema;
|
|
416153
|
-
var init_media_processor = __esm(() => {
|
|
416154
|
-
init_src();
|
|
416155
|
-
init_src6();
|
|
416156
|
-
init_src9();
|
|
416157
|
-
init_drizzle_orm();
|
|
416158
|
-
init_zod();
|
|
416159
|
-
init_media_storage();
|
|
416160
|
-
init_tenant_scope();
|
|
416161
|
-
init_worker_tenant_context();
|
|
416162
|
-
init_loader2();
|
|
416163
|
-
log131 = createLogger("media-processor");
|
|
416164
|
-
PROCESSABLE_MEDIA_TYPES = new Set(["audio", "image", "document", "video"]);
|
|
416165
|
-
pluginMediaSchema = exports_external.object({
|
|
416166
|
-
buffer: exports_external.instanceof(Buffer),
|
|
416167
|
-
mimeType: exports_external.string().min(1).optional()
|
|
416168
|
-
});
|
|
416169
|
-
});
|
|
416170
|
-
|
|
416171
416221
|
// ../api/src/tenancy/inflight-revocation.ts
|
|
416172
416222
|
function resolveInflightRecheckIntervalMs() {
|
|
416173
416223
|
return Math.floor(INFLIGHT_REVOCATION_CEILING_SECONDS * 1000 / 2);
|