Add consent-gated media exchange library
This commit is contained in:
1 parent
b8e4a1bb88
commit
bf474dfe10
42 files changed
+2533
-9
No files matched your search
@@ -20,6 +20,7 @@ import {
|
||||
type ConversationState,
|
||||
} from './conversation-state.js';
|
||||
import { containsMediaRequest } from './conversation-boundaries.js';
|
||||
import type { MediaLibraryExchangeService } from './media-library-exchange.js';
|
||||
|
||||
export type ConversationAutomationResultKind =
|
||||
| 'sent'
|
||||
@@ -94,6 +95,7 @@ export class ConversationAutomationService {
|
||||
private readonly now: () => number = Date.now,
|
||||
private readonly pendingReplyRetryDelayMs = PENDING_REPLY_RETRY_DELAY_MS,
|
||||
private readonly beforeSendLoader?: ConversationBeforeSendLoader,
|
||||
private readonly mediaExchange?: MediaLibraryExchangeService,
|
||||
) {}
|
||||
|
||||
async processPendingConversations(signal?: AbortSignal): Promise<ConversationAutomationResult[]> {
|
||||
@@ -260,6 +262,52 @@ export class ConversationAutomationService {
|
||||
return this.finish(conversation, conversationId, state, incomingKey, 'human-required', safetyReason);
|
||||
}
|
||||
}
|
||||
if (this.mediaExchange?.detect(conversationId, latest.text)) {
|
||||
const exchangeAttempt = await this.rateLimiter.throttle(
|
||||
async () => {
|
||||
const refreshed = await this.loadConversationBeforeSend(conversationId, conversation);
|
||||
const refreshedLatest = refreshed ? latestInboundMessage(refreshed.messages ?? []) : null;
|
||||
if (!refreshed || !refreshedLatest) return { kind: 'superseded' as const };
|
||||
const refreshedState = mergeConversationState(
|
||||
this.states.get(conversationId),
|
||||
buildConversationState((refreshed.messages ?? []).map(({ sender, text, isSelf }) => ({ sender, text, isSelf }))),
|
||||
);
|
||||
if (refreshedState.boundaries.length > 0 || hasReceivedImage(refreshed.messages ?? [])) {
|
||||
return { kind: 'human-required' as const, reason: 'The conversation became ineligible before the image could be sent.' };
|
||||
}
|
||||
const refreshedSellerSignal = findSellerSignal(
|
||||
refreshedLatest.text,
|
||||
[...this.sellerKeywords],
|
||||
[...this.sellerDomains],
|
||||
);
|
||||
if (refreshedSellerSignal) {
|
||||
return { kind: 'human-required' as const, reason: 'Potential upsell appeared before the image could be sent.' };
|
||||
}
|
||||
if (messageKey(refreshedLatest) !== incomingKey) return { kind: 'superseded' as const };
|
||||
return this.mediaExchange!.handle(conversationId, incomingKey, refreshedLatest.text);
|
||||
},
|
||||
(result) => result.kind === 'sent' || (result.kind === 'human-required' && result.remoteSent === true),
|
||||
signal,
|
||||
REPLY_SEND_PRIORITY,
|
||||
);
|
||||
const exchange = exchangeAttempt;
|
||||
if (exchange.kind === 'sent') {
|
||||
state.askedForMedia = true;
|
||||
state.automation.lastIncomingKey = incomingKey;
|
||||
this.states.save(conversationId, state);
|
||||
return {
|
||||
conversationId,
|
||||
username: conversation.username,
|
||||
kind: 'sent',
|
||||
remainingTurns: remainingAutomationTurns(state),
|
||||
};
|
||||
}
|
||||
if (exchange.kind === 'human-required') {
|
||||
return this.finish(conversation, conversationId, state, incomingKey, 'human-required', exchange.reason);
|
||||
}
|
||||
if (exchange.kind === 'retryable' || exchange.kind === 'dry-run') return null;
|
||||
if (exchange.kind === 'superseded') return null;
|
||||
}
|
||||
if (remainingAutomationTurns(state) === 0) {
|
||||
return this.finish(
|
||||
conversation,
|
||||
|
||||
Reference in new issue
Block a user