@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.3
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/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +928 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +8482 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +43 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +142 -27
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -49
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -83
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +4 -206
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -94
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +22138 -18770
- package/src/generic/ResolverBase.ts +171 -27
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +127 -50
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +6 -252
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Resolver, Mutation, Query, Subscription, Arg, Ctx, Root, ObjectType, Field, ResolverFilterData } from 'type-graphql';
|
|
2
|
+
import { IMetadataProvider, LogError, UserInfo } from '@memberjunction/core';
|
|
3
|
+
import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
|
|
4
|
+
import { NotificationEngine } from '@memberjunction/notifications';
|
|
5
|
+
import {
|
|
6
|
+
LiveKitSipService,
|
|
7
|
+
RoomHandoffEngine,
|
|
8
|
+
type HandoffOfferEvent,
|
|
9
|
+
type HandoffOfferView,
|
|
10
|
+
type IHandoffNotifier,
|
|
11
|
+
type IHandoffPublisher,
|
|
12
|
+
} from '@memberjunction/livekit-room-server';
|
|
13
|
+
import { AppContext } from '../types.js';
|
|
14
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
15
|
+
import { PubSubManager } from '../generic/PubSubManager.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GraphQL surface for the human side of a room handoff: a person at an Explorer console is OFFERED a conversation an AI
|
|
19
|
+
* agent is having (a phone call that arrived through LiveKit SIP, or a web room), and accepts or declines it. Thin by the
|
|
20
|
+
* Transport-Layer rule: the offer lifecycle, the room watching and the AI's leave step live in `RoomHandoffEngine`
|
|
21
|
+
* (`@memberjunction/livekit-room-server`); this file only authenticates, delegates and shapes the answer.
|
|
22
|
+
*
|
|
23
|
+
* Every operation is scoped to the signed-in user: a person lists, accepts and declines only their OWN offers, and the live
|
|
24
|
+
* subscription delivers only theirs. Accepting returns the room name; the console then joins the room through the same
|
|
25
|
+
* LiveKit token path the Meet room uses (`MintLiveKitClientToken`), under the same identity the engine watches for.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The pub/sub topic offer changes are published on. */
|
|
29
|
+
export const HANDOFF_OFFER_TOPIC = 'HANDOFF_OFFER_CHANGES';
|
|
30
|
+
|
|
31
|
+
/** One offer, as the console sees it. */
|
|
32
|
+
@ObjectType()
|
|
33
|
+
export class HandoffOffer {
|
|
34
|
+
@Field(() => String)
|
|
35
|
+
OfferID: string;
|
|
36
|
+
|
|
37
|
+
@Field(() => String)
|
|
38
|
+
RoomName: string;
|
|
39
|
+
|
|
40
|
+
/** `warm` (the AI introduces you, then leaves) or `blind` (the AI leaves as soon as you join). */
|
|
41
|
+
@Field(() => String)
|
|
42
|
+
Mode: string;
|
|
43
|
+
|
|
44
|
+
/** The AI's summary of the conversation so far. */
|
|
45
|
+
@Field(() => String)
|
|
46
|
+
Summary: string;
|
|
47
|
+
|
|
48
|
+
/** Who is on the other end (a masked number, "Web visitor"). */
|
|
49
|
+
@Field(() => String)
|
|
50
|
+
CallerLabel: string;
|
|
51
|
+
|
|
52
|
+
/** The AI agent that is in the room. */
|
|
53
|
+
@Field(() => String)
|
|
54
|
+
AgentName: string;
|
|
55
|
+
|
|
56
|
+
/** `Pending`, `Accepted`, `Declined`, `Expired` or `Cancelled`. */
|
|
57
|
+
@Field(() => String)
|
|
58
|
+
Status: string;
|
|
59
|
+
|
|
60
|
+
@Field(() => String)
|
|
61
|
+
CreatedAt: string;
|
|
62
|
+
|
|
63
|
+
/** After this the offer can no longer be accepted. */
|
|
64
|
+
@Field(() => String)
|
|
65
|
+
ExpiresAt: string;
|
|
66
|
+
|
|
67
|
+
/** The MJ interaction ID associated with this conversation/room, if known. */
|
|
68
|
+
@Field(() => String, { nullable: true })
|
|
69
|
+
InteractionID?: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One change to an offer, pushed to the person it belongs to. */
|
|
73
|
+
@ObjectType()
|
|
74
|
+
export class HandoffOfferChange {
|
|
75
|
+
/** `offered` for a new offer; `updated` when it was accepted, declined, expired or cancelled. */
|
|
76
|
+
@Field(() => String)
|
|
77
|
+
Kind: string;
|
|
78
|
+
|
|
79
|
+
@Field(() => HandoffOffer)
|
|
80
|
+
Offer: HandoffOffer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@ObjectType()
|
|
84
|
+
export class AcceptHandoffOfferResult {
|
|
85
|
+
@Field(() => Boolean)
|
|
86
|
+
Success: boolean;
|
|
87
|
+
|
|
88
|
+
@Field(() => String, { nullable: true })
|
|
89
|
+
ErrorMessage?: string;
|
|
90
|
+
|
|
91
|
+
/** The room to join (through `MintLiveKitClientToken`). Empty on failure. */
|
|
92
|
+
@Field(() => String)
|
|
93
|
+
RoomName: string;
|
|
94
|
+
|
|
95
|
+
@Field(() => HandoffOffer, { nullable: true })
|
|
96
|
+
Offer?: HandoffOffer;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@ObjectType()
|
|
100
|
+
export class DeclineHandoffOfferResult {
|
|
101
|
+
@Field(() => Boolean)
|
|
102
|
+
Success: boolean;
|
|
103
|
+
|
|
104
|
+
@Field(() => String, { nullable: true })
|
|
105
|
+
ErrorMessage?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What the subscription filter reads off the connection. */
|
|
109
|
+
interface HandoffSubscriptionContext {
|
|
110
|
+
userPayload?: { userRecord?: { ID?: string } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Delivery predicate for {@link HumanHandoffResolver.HandoffOfferChanges}: a change reaches only the person it belongs to.
|
|
115
|
+
* **Fails closed**: a missing identity on either side never matches.
|
|
116
|
+
*/
|
|
117
|
+
export function HandoffOfferChangeFilter(data: { payload: HandoffOfferEvent; context: HandoffSubscriptionContext | undefined }): boolean {
|
|
118
|
+
const connectionUserID = data.context?.userPayload?.userRecord?.ID;
|
|
119
|
+
if (!connectionUserID || !data.payload?.UserID) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return UUIDsEqual(data.payload.UserID, connectionUserID);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Redis channel carrying replicated handoff offer updates between server instances. */
|
|
126
|
+
export const HANDOFF_OFFER_FANOUT_CHANNEL = 'handoff-offer-changes';
|
|
127
|
+
|
|
128
|
+
export interface HandoffOfferFanOutPayload {
|
|
129
|
+
UserID: string;
|
|
130
|
+
Kind: 'offered' | 'updated';
|
|
131
|
+
Offer: HandoffOfferView;
|
|
132
|
+
SourceServerId?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type HandoffOfferPublishHook = (payload: HandoffOfferFanOutPayload) => void;
|
|
136
|
+
|
|
137
|
+
let _handoffPublishHook: HandoffOfferPublishHook | undefined;
|
|
138
|
+
|
|
139
|
+
export function SetHandoffOfferPublishHook(hook?: HandoffOfferPublishHook): void {
|
|
140
|
+
_handoffPublishHook = hook;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function ParseReplicatedHandoffOfferUpdate(raw: string, localServerId: string): HandoffOfferFanOutPayload | null {
|
|
144
|
+
let parsed: unknown;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(raw);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
LogError('Failed to parse replicated handoff offer update payload', undefined, err);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const candidate = parsed as Record<string, unknown>;
|
|
155
|
+
if (typeof candidate.SourceServerId === 'string' && candidate.SourceServerId === localServerId) {
|
|
156
|
+
return null; // echo suppression
|
|
157
|
+
}
|
|
158
|
+
if (typeof candidate.UserID !== 'string' || typeof candidate.Kind !== 'string' || !candidate.Offer) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const offer = candidate.Offer as Record<string, unknown>;
|
|
162
|
+
if (typeof offer.OfferID !== 'string' || typeof offer.RoomName !== 'string' || typeof offer.Status !== 'string') {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
UserID: candidate.UserID,
|
|
167
|
+
Kind: candidate.Kind as 'offered' | 'updated',
|
|
168
|
+
Offer: offer as unknown as HandoffOfferView,
|
|
169
|
+
SourceServerId: typeof candidate.SourceServerId === 'string' ? candidate.SourceServerId : undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Publishes offer changes onto the GraphQL subscription topic and forwards across instances. */
|
|
174
|
+
export class PubSubHandoffPublisher implements IHandoffPublisher {
|
|
175
|
+
public Publish(event: HandoffOfferEvent): void {
|
|
176
|
+
PubSubManager.Instance.Publish(HANDOFF_OFFER_TOPIC, { UserID: event.UserID, Kind: event.Kind, Offer: event.Offer });
|
|
177
|
+
_handoffPublishHook?.({
|
|
178
|
+
UserID: event.UserID,
|
|
179
|
+
Kind: event.Kind,
|
|
180
|
+
Offer: event.Offer,
|
|
181
|
+
SourceServerId: MJGlobal.Instance.ProcessUUID,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Tells the person about a new offer outside the live subscription, through the unified notification engine (in-app, and
|
|
188
|
+
* MJ Comms when the type is configured for it), using the dedicated "Conversation Handoff Offer" notification type.
|
|
189
|
+
* The `handoff-offer` resource configuration is what Explorer routes to the Conversation Console.
|
|
190
|
+
*/
|
|
191
|
+
export class NotificationHandoffNotifier implements IHandoffNotifier {
|
|
192
|
+
public async NotifyOffer(offer: HandoffOfferView, targetUserID: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<void> {
|
|
193
|
+
await NotificationEngine.Instance.Config(false, contextUser, provider);
|
|
194
|
+
const result = await NotificationEngine.Instance.SendNotification(
|
|
195
|
+
{
|
|
196
|
+
userId: targetUserID,
|
|
197
|
+
typeNameOrId: 'Conversation Handoff Offer',
|
|
198
|
+
title: `Incoming conversation: ${offer.CallerLabel}`,
|
|
199
|
+
message: `${offer.AgentName} is asking you to take over a conversation. ${offer.Summary}`,
|
|
200
|
+
resourceConfiguration: { type: 'handoff-offer', offerId: offer.OfferID, room: offer.RoomName },
|
|
201
|
+
},
|
|
202
|
+
contextUser,
|
|
203
|
+
);
|
|
204
|
+
if (!result.success) {
|
|
205
|
+
throw new Error(`the notification engine could not deliver it: ${(result.errors ?? []).join('; ') || 'unknown error'}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Binds the handoff engine's collaborators that only the server host can supply (same module-load rationale as the other
|
|
212
|
+
* realtime bindings): the pub/sub publisher, the notification sender, and a room-presence check over the LiveKit credentials
|
|
213
|
+
* the Meet room already uses. Idempotent (latest-wins). The telephony extension adds the collaborators that need a carrier
|
|
214
|
+
* (dialing a number into a room, starting another agent).
|
|
215
|
+
*/
|
|
216
|
+
RoomHandoffEngine.Instance.Configure({
|
|
217
|
+
Presence: new LiveKitSipService(),
|
|
218
|
+
Publisher: new PubSubHandoffPublisher(),
|
|
219
|
+
Notifier: new NotificationHandoffNotifier(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
@Resolver()
|
|
223
|
+
export class HumanHandoffResolver extends ResolverBase {
|
|
224
|
+
/** The signed-in user's offers: pending ones, plus ones that resolved recently enough to still be shown. */
|
|
225
|
+
@Query(() => [HandoffOffer])
|
|
226
|
+
async MyHandoffOffers(@Ctx() context: AppContext = {} as AppContext): Promise<HandoffOffer[]> {
|
|
227
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
228
|
+
if (!user) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
const offers = await RoomHandoffEngine.Instance.ListOffersForUser(user.ID, user);
|
|
232
|
+
return offers.map(toGraphQLOffer);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Accepts an offer. The engine authorises it against the offer's target; a stranger's attempt and a missing offer answer
|
|
237
|
+
* identically. On success the console should join the room, and the AI leaves once the person is actually there.
|
|
238
|
+
*/
|
|
239
|
+
@Mutation(() => AcceptHandoffOfferResult)
|
|
240
|
+
async AcceptHandoffOffer(
|
|
241
|
+
@Arg('offerID', () => String) offerID: string,
|
|
242
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
243
|
+
): Promise<AcceptHandoffOfferResult> {
|
|
244
|
+
const failure = (message: string): AcceptHandoffOfferResult => ({ Success: false, ErrorMessage: message, RoomName: '' });
|
|
245
|
+
try {
|
|
246
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
247
|
+
if (!user) {
|
|
248
|
+
return failure('Unable to determine current user.');
|
|
249
|
+
}
|
|
250
|
+
const result = await RoomHandoffEngine.Instance.AcceptOffer(offerID, user.ID, user);
|
|
251
|
+
if ('Reason' in result) {
|
|
252
|
+
return failure(result.Reason);
|
|
253
|
+
}
|
|
254
|
+
return { Success: true, RoomName: result.Offer.RoomName, Offer: toGraphQLOffer(result.Offer) };
|
|
255
|
+
} catch (error) {
|
|
256
|
+
LogError(`AcceptHandoffOffer failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
257
|
+
return failure('The offer could not be accepted.');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Declines an offer, so the AI can fall back (the person's phone, or carrying on). */
|
|
262
|
+
@Mutation(() => DeclineHandoffOfferResult)
|
|
263
|
+
async DeclineHandoffOffer(
|
|
264
|
+
@Arg('offerID', () => String) offerID: string,
|
|
265
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
266
|
+
): Promise<DeclineHandoffOfferResult> {
|
|
267
|
+
try {
|
|
268
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
269
|
+
if (!user) {
|
|
270
|
+
return { Success: false, ErrorMessage: 'Unable to determine current user.' };
|
|
271
|
+
}
|
|
272
|
+
const result = await RoomHandoffEngine.Instance.DeclineOffer(offerID, user.ID, user);
|
|
273
|
+
return 'Reason' in result ? { Success: false, ErrorMessage: result.Reason } : { Success: true };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
LogError(`DeclineHandoffOffer failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
276
|
+
return { Success: false, ErrorMessage: 'The offer could not be declined.' };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Live changes to the signed-in user's offers (a new offer, or one that was accepted, declined, expired or cancelled). */
|
|
281
|
+
@Subscription(() => HandoffOfferChange, {
|
|
282
|
+
topics: HANDOFF_OFFER_TOPIC,
|
|
283
|
+
filter: (data: ResolverFilterData<HandoffOfferEvent, Record<string, never>, HandoffSubscriptionContext>) => HandoffOfferChangeFilter(data),
|
|
284
|
+
})
|
|
285
|
+
HandoffOfferChanges(@Root() payload: HandoffOfferEvent): HandoffOfferChange {
|
|
286
|
+
// UserID is a server-side delivery key and is intentionally not returned.
|
|
287
|
+
return { Kind: payload.Kind, Offer: toGraphQLOffer(payload.Offer) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function toGraphQLOffer(offer: HandoffOfferView): HandoffOffer {
|
|
292
|
+
return {
|
|
293
|
+
OfferID: offer.OfferID,
|
|
294
|
+
RoomName: offer.RoomName,
|
|
295
|
+
Mode: offer.Mode,
|
|
296
|
+
Summary: offer.Summary,
|
|
297
|
+
CallerLabel: offer.CallerLabel,
|
|
298
|
+
AgentName: offer.AgentName,
|
|
299
|
+
Status: offer.Status,
|
|
300
|
+
CreatedAt: offer.CreatedAt,
|
|
301
|
+
ExpiresAt: offer.ExpiresAt,
|
|
302
|
+
InteractionID: offer.InteractionID ?? null,
|
|
303
|
+
};
|
|
304
|
+
}
|