crewly 1.2.6 → 1.2.7
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/backend/backend/src/constants.d.ts +24 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +24 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.d.ts +7 -0
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.js +64 -3
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +2 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.d.ts +95 -22
- package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.js +284 -60
- package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/response-router.service.d.ts +8 -0
- package/dist/backend/backend/src/services/messaging/response-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/response-router.service.js +42 -0
- package/dist/backend/backend/src/services/messaging/response-router.service.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +24 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +24 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/frontend/dist/assets/{index-3558d1a2.js → index-5cf8820f.js} +71 -71
- package/frontend/dist/assets/{index-4c4dcc31.css → index-dee3a3a9.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.d.ts
CHANGED
|
@@ -1,47 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Google Chat Messenger Adapter
|
|
3
3
|
*
|
|
4
|
-
* Messenger adapter for Google Chat
|
|
5
|
-
*
|
|
6
|
-
* the Chat API
|
|
4
|
+
* Messenger adapter for Google Chat supporting three connection modes:
|
|
5
|
+
* 1. **Webhook mode** — post messages via an incoming webhook URL (send-only)
|
|
6
|
+
* 2. **Service account mode** — send messages via the Chat API (send-only)
|
|
7
|
+
* 3. **Pub/Sub mode** — pull incoming messages from a Cloud Pub/Sub subscription
|
|
8
|
+
* and reply via the Chat API (bidirectional, thread-aware)
|
|
9
|
+
*
|
|
10
|
+
* In Pub/Sub mode, a Google Chat App is configured to publish events to a
|
|
11
|
+
* Pub/Sub topic. Crewly pulls from the subscription, processes MESSAGE events,
|
|
12
|
+
* and replies in the same thread via the Chat API.
|
|
7
13
|
*
|
|
8
14
|
* @module services/messaging/adapters/google-chat-messenger.adapter
|
|
9
15
|
*/
|
|
10
|
-
import { MessengerAdapter, MessengerPlatform } from '../messenger-adapter.interface.js';
|
|
16
|
+
import { MessengerAdapter, MessengerPlatform, IncomingMessage } from '../messenger-adapter.interface.js';
|
|
11
17
|
/**
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*
|
|
18
|
+
* Callback type for incoming messages from Pub/Sub pull.
|
|
19
|
+
*/
|
|
20
|
+
export type GoogleChatIncomingCallback = (msg: IncomingMessage) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Messenger adapter for Google Chat with Pub/Sub support.
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
24
|
+
* Supports webhook, service-account, and pubsub modes.
|
|
25
|
+
* Pub/Sub mode enables bidirectional communication with thread tracking.
|
|
20
26
|
*/
|
|
21
27
|
export declare class GoogleChatMessengerAdapter implements MessengerAdapter {
|
|
22
28
|
readonly platform: MessengerPlatform;
|
|
29
|
+
/** Current connection mode */
|
|
30
|
+
private mode;
|
|
23
31
|
/** Webhook URL for posting messages (webhook mode) */
|
|
24
32
|
private webhookUrl;
|
|
25
|
-
/** Service account key JSON
|
|
33
|
+
/** Service account key JSON string */
|
|
26
34
|
private serviceAccountKey;
|
|
27
35
|
/** Access token obtained from service account (cached) */
|
|
28
36
|
private accessToken;
|
|
29
37
|
/** Access token expiry timestamp */
|
|
30
38
|
private tokenExpiresAt;
|
|
39
|
+
/** OAuth2 scopes for the current mode */
|
|
40
|
+
private tokenScopes;
|
|
41
|
+
/** Full Pub/Sub subscription resource name (e.g. projects/PROJECT/subscriptions/SUB) */
|
|
42
|
+
private subscriptionName;
|
|
43
|
+
/** GCP project ID (for Pub/Sub mode) */
|
|
44
|
+
private projectId;
|
|
45
|
+
/** Pub/Sub pull interval timer */
|
|
46
|
+
private pullIntervalTimer;
|
|
47
|
+
/** Callback for incoming messages */
|
|
48
|
+
private onIncomingMessage;
|
|
49
|
+
/** Consecutive pull failure count */
|
|
50
|
+
private consecutiveFailures;
|
|
51
|
+
/** Whether the pull loop is paused due to failures */
|
|
52
|
+
private pullPaused;
|
|
31
53
|
/**
|
|
32
|
-
* Initialize the adapter
|
|
54
|
+
* Initialize the adapter with the provided credentials.
|
|
33
55
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
56
|
+
* Detects mode based on provided config fields:
|
|
57
|
+
* - `webhookUrl` → webhook mode
|
|
58
|
+
* - `serviceAccountKey` + `projectId` + `subscriptionName` → pubsub mode
|
|
59
|
+
* - `serviceAccountKey` alone → service-account mode
|
|
60
|
+
*
|
|
61
|
+
* @param config - Configuration object with credentials
|
|
62
|
+
* @throws Error if credentials are invalid or missing
|
|
36
63
|
*/
|
|
37
64
|
initialize(config: Record<string, unknown>): Promise<void>;
|
|
38
65
|
/**
|
|
39
66
|
* Send a text message to a Google Chat space.
|
|
40
67
|
*
|
|
41
68
|
* In webhook mode, the `channel` parameter is ignored (webhook URL determines the space).
|
|
42
|
-
* In service
|
|
69
|
+
* In service-account/pubsub mode, `channel` is the space name (e.g., "spaces/AAAA...").
|
|
43
70
|
*
|
|
44
|
-
* @param channel - Google Chat space name (used in service
|
|
71
|
+
* @param channel - Google Chat space name (used in service-account/pubsub mode)
|
|
45
72
|
* @param text - Message content
|
|
46
73
|
* @param options - Optional send options (threadId for threaded replies)
|
|
47
74
|
* @throws Error if adapter is not initialized or send fails
|
|
@@ -52,7 +79,7 @@ export declare class GoogleChatMessengerAdapter implements MessengerAdapter {
|
|
|
52
79
|
/**
|
|
53
80
|
* Get the current connection status.
|
|
54
81
|
*
|
|
55
|
-
* @returns Status object with connected flag and
|
|
82
|
+
* @returns Status object with connected flag, platform, and mode details
|
|
56
83
|
*/
|
|
57
84
|
getStatus(): {
|
|
58
85
|
connected: boolean;
|
|
@@ -60,9 +87,40 @@ export declare class GoogleChatMessengerAdapter implements MessengerAdapter {
|
|
|
60
87
|
details?: Record<string, unknown>;
|
|
61
88
|
};
|
|
62
89
|
/**
|
|
63
|
-
* Disconnect by clearing stored credentials.
|
|
90
|
+
* Disconnect by clearing stored credentials and stopping the pull loop.
|
|
64
91
|
*/
|
|
65
92
|
disconnect(): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Start the Pub/Sub pull loop that periodically fetches messages.
|
|
95
|
+
*/
|
|
96
|
+
private startPubSubPull;
|
|
97
|
+
/**
|
|
98
|
+
* Stop the Pub/Sub pull loop.
|
|
99
|
+
*/
|
|
100
|
+
private stopPubSubPull;
|
|
101
|
+
/**
|
|
102
|
+
* Pull messages from the Pub/Sub subscription, process them, and acknowledge.
|
|
103
|
+
*
|
|
104
|
+
* Each message is a base64-encoded Google Chat event JSON. Only MESSAGE-type
|
|
105
|
+
* events are forwarded to the incoming message callback. All messages are
|
|
106
|
+
* acknowledged regardless of type to prevent redelivery.
|
|
107
|
+
*/
|
|
108
|
+
pullMessages(): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Process a Google Chat event and forward MESSAGE events to the callback.
|
|
111
|
+
*
|
|
112
|
+
* Thread tracking: extracts the thread name from the event so that replies
|
|
113
|
+
* can be posted back to the same thread.
|
|
114
|
+
*
|
|
115
|
+
* @param event - Parsed Google Chat event payload
|
|
116
|
+
*/
|
|
117
|
+
private processChatEvent;
|
|
118
|
+
/**
|
|
119
|
+
* Acknowledge processed messages to prevent redelivery.
|
|
120
|
+
*
|
|
121
|
+
* @param ackIds - Array of ack IDs from the pull response
|
|
122
|
+
*/
|
|
123
|
+
private acknowledgeMessages;
|
|
66
124
|
/**
|
|
67
125
|
* Send a message via incoming webhook URL.
|
|
68
126
|
*
|
|
@@ -73,18 +131,33 @@ export declare class GoogleChatMessengerAdapter implements MessengerAdapter {
|
|
|
73
131
|
/**
|
|
74
132
|
* Send a message via the Google Chat REST API using service account credentials.
|
|
75
133
|
*
|
|
134
|
+
* When a threadId (thread name) is provided, the reply is posted to the same
|
|
135
|
+
* thread. This enables conversational thread tracking for Pub/Sub mode.
|
|
136
|
+
*
|
|
76
137
|
* @param space - Space name (e.g., "spaces/AAAA...")
|
|
77
138
|
* @param text - Message text
|
|
78
|
-
* @param
|
|
139
|
+
* @param threadName - Optional full thread name (e.g., "spaces/SPACE/threads/THREAD")
|
|
79
140
|
*/
|
|
80
141
|
private sendViaApi;
|
|
81
142
|
/**
|
|
82
143
|
* Get a valid access token, refreshing if needed.
|
|
83
144
|
*
|
|
84
|
-
* Uses a
|
|
145
|
+
* Uses a JWT-based OAuth2 flow for service accounts.
|
|
146
|
+
* The scope includes Pub/Sub when in pubsub mode.
|
|
85
147
|
*
|
|
86
148
|
* @returns Valid access token string
|
|
87
149
|
*/
|
|
88
|
-
|
|
150
|
+
getAccessToken(): Promise<string>;
|
|
151
|
+
/**
|
|
152
|
+
* Validate a service account key JSON string.
|
|
153
|
+
*
|
|
154
|
+
* @param key - JSON string of the service account key
|
|
155
|
+
* @throws Error if the key is invalid
|
|
156
|
+
*/
|
|
157
|
+
private validateServiceAccountKey;
|
|
158
|
+
/**
|
|
159
|
+
* Reset all internal state to defaults.
|
|
160
|
+
*/
|
|
161
|
+
private resetState;
|
|
89
162
|
}
|
|
90
163
|
//# sourceMappingURL=google-chat-messenger.adapter.d.ts.map
|
package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-chat-messenger.adapter.d.ts","sourceRoot":"","sources":["../../../../../../../backend/src/services/messaging/adapters/google-chat-messenger.adapter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"google-chat-messenger.adapter.d.ts","sourceRoot":"","sources":["../../../../../../../backend/src/services/messaging/adapters/google-chat-messenger.adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAoCzG;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,CAAC;AAExE;;;;;GAKG;AACH,qBAAa,0BAA2B,YAAW,gBAAgB;IACjE,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAiB;IAErD,8BAA8B;IAC9B,OAAO,CAAC,IAAI,CAA0B;IAEtC,sDAAsD;IACtD,OAAO,CAAC,UAAU,CAAuB;IAEzC,sCAAsC;IACtC,OAAO,CAAC,iBAAiB,CAAuB;IAEhD,0DAA0D;IAC1D,OAAO,CAAC,WAAW,CAAuB;IAE1C,oCAAoC;IACpC,OAAO,CAAC,cAAc,CAAK;IAE3B,yCAAyC;IACzC,OAAO,CAAC,WAAW,CAAmD;IAEtE,wFAAwF;IACxF,OAAO,CAAC,gBAAgB,CAAuB;IAE/C,wCAAwC;IACxC,OAAO,CAAC,SAAS,CAAuB;IAExC,kCAAkC;IAClC,OAAO,CAAC,iBAAiB,CAA+C;IAExE,qCAAqC;IACrC,OAAO,CAAC,iBAAiB,CAA2C;IAEpE,qCAAqC;IACrC,OAAO,CAAC,mBAAmB,CAAK;IAEhC,sDAAsD;IACtD,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;;;;;;;OAUG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkDhE;;;;;;;;;;OAUG;IACG,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAchG;;;;OAIG;IACH,SAAS,IAAI;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,iBAAiB,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE;IAiBnG;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IASjC;;OAEG;IACH,OAAO,CAAC,eAAe;IAmBvB;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;;;;;OAMG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAmDnC;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IA6BxB;;;;OAIG;YACW,mBAAmB;IA0BjC;;;;;OAKG;YACW,cAAc;IA8B5B;;;;;;;;;OASG;YACW,UAAU;IA2CxB;;;;;;;OAOG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAqDvC;;;;;OAKG;IACH,OAAO,CAAC,yBAAyB;IAcjC;;OAEG;IACH,OAAO,CAAC,UAAU;CAcnB"}
|
package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.js
CHANGED
|
@@ -1,72 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Google Chat Messenger Adapter
|
|
3
3
|
*
|
|
4
|
-
* Messenger adapter for Google Chat
|
|
5
|
-
*
|
|
6
|
-
* the Chat API
|
|
4
|
+
* Messenger adapter for Google Chat supporting three connection modes:
|
|
5
|
+
* 1. **Webhook mode** — post messages via an incoming webhook URL (send-only)
|
|
6
|
+
* 2. **Service account mode** — send messages via the Chat API (send-only)
|
|
7
|
+
* 3. **Pub/Sub mode** — pull incoming messages from a Cloud Pub/Sub subscription
|
|
8
|
+
* and reply via the Chat API (bidirectional, thread-aware)
|
|
9
|
+
*
|
|
10
|
+
* In Pub/Sub mode, a Google Chat App is configured to publish events to a
|
|
11
|
+
* Pub/Sub topic. Crewly pulls from the subscription, processes MESSAGE events,
|
|
12
|
+
* and replies in the same thread via the Chat API.
|
|
7
13
|
*
|
|
8
14
|
* @module services/messaging/adapters/google-chat-messenger.adapter
|
|
9
15
|
*/
|
|
10
|
-
|
|
11
|
-
const FETCH_TIMEOUT_MS = 15_000;
|
|
16
|
+
import { GOOGLE_CHAT_PUBSUB_CONSTANTS } from '../../../constants.js';
|
|
12
17
|
/**
|
|
13
|
-
* Messenger adapter for Google Chat.
|
|
14
|
-
*
|
|
15
|
-
* Supports two modes:
|
|
16
|
-
* 1. **Webhook mode** (simpler): uses an incoming webhook URL to post messages
|
|
17
|
-
* 2. **Service account mode**: uses a service account key to call the Chat API
|
|
18
|
+
* Messenger adapter for Google Chat with Pub/Sub support.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* Supports webhook, service-account, and pubsub modes.
|
|
21
|
+
* Pub/Sub mode enables bidirectional communication with thread tracking.
|
|
21
22
|
*/
|
|
22
23
|
export class GoogleChatMessengerAdapter {
|
|
23
24
|
platform = 'google-chat';
|
|
25
|
+
/** Current connection mode */
|
|
26
|
+
mode = 'none';
|
|
24
27
|
/** Webhook URL for posting messages (webhook mode) */
|
|
25
28
|
webhookUrl = null;
|
|
26
|
-
/** Service account key JSON
|
|
29
|
+
/** Service account key JSON string */
|
|
27
30
|
serviceAccountKey = null;
|
|
28
31
|
/** Access token obtained from service account (cached) */
|
|
29
32
|
accessToken = null;
|
|
30
33
|
/** Access token expiry timestamp */
|
|
31
34
|
tokenExpiresAt = 0;
|
|
35
|
+
/** OAuth2 scopes for the current mode */
|
|
36
|
+
tokenScopes = GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE;
|
|
37
|
+
/** Full Pub/Sub subscription resource name (e.g. projects/PROJECT/subscriptions/SUB) */
|
|
38
|
+
subscriptionName = null;
|
|
39
|
+
/** GCP project ID (for Pub/Sub mode) */
|
|
40
|
+
projectId = null;
|
|
41
|
+
/** Pub/Sub pull interval timer */
|
|
42
|
+
pullIntervalTimer = null;
|
|
43
|
+
/** Callback for incoming messages */
|
|
44
|
+
onIncomingMessage = null;
|
|
45
|
+
/** Consecutive pull failure count */
|
|
46
|
+
consecutiveFailures = 0;
|
|
47
|
+
/** Whether the pull loop is paused due to failures */
|
|
48
|
+
pullPaused = false;
|
|
32
49
|
/**
|
|
33
|
-
* Initialize the adapter
|
|
50
|
+
* Initialize the adapter with the provided credentials.
|
|
51
|
+
*
|
|
52
|
+
* Detects mode based on provided config fields:
|
|
53
|
+
* - `webhookUrl` → webhook mode
|
|
54
|
+
* - `serviceAccountKey` + `projectId` + `subscriptionName` → pubsub mode
|
|
55
|
+
* - `serviceAccountKey` alone → service-account mode
|
|
34
56
|
*
|
|
35
|
-
* @param config -
|
|
36
|
-
* @throws Error if
|
|
57
|
+
* @param config - Configuration object with credentials
|
|
58
|
+
* @throws Error if credentials are invalid or missing
|
|
37
59
|
*/
|
|
38
60
|
async initialize(config) {
|
|
39
61
|
const webhookUrl = config.webhookUrl;
|
|
40
62
|
const serviceAccountKey = config.serviceAccountKey;
|
|
63
|
+
const projectId = config.projectId;
|
|
64
|
+
const subscriptionName = config.subscriptionName;
|
|
65
|
+
const onIncomingMessage = config.onIncomingMessage;
|
|
66
|
+
// Webhook mode
|
|
41
67
|
if (typeof webhookUrl === 'string' && webhookUrl) {
|
|
42
|
-
// Webhook mode: validate the URL format
|
|
43
68
|
if (!webhookUrl.startsWith('https://chat.googleapis.com/')) {
|
|
44
69
|
throw new Error('Invalid Google Chat webhook URL. Must start with https://chat.googleapis.com/');
|
|
45
70
|
}
|
|
46
|
-
|
|
47
|
-
// so we just validate the URL format and store it
|
|
71
|
+
this.resetState();
|
|
48
72
|
this.webhookUrl = webhookUrl;
|
|
49
|
-
this.
|
|
50
|
-
this.accessToken = null;
|
|
73
|
+
this.mode = 'webhook';
|
|
51
74
|
return;
|
|
52
75
|
}
|
|
76
|
+
// Pub/Sub mode or Service Account mode — both require a service account key
|
|
53
77
|
if (typeof serviceAccountKey === 'string' && serviceAccountKey) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
this.validateServiceAccountKey(serviceAccountKey);
|
|
79
|
+
this.resetState();
|
|
80
|
+
this.serviceAccountKey = serviceAccountKey;
|
|
81
|
+
// Pub/Sub mode: requires projectId + subscriptionName
|
|
82
|
+
if (typeof projectId === 'string' && projectId &&
|
|
83
|
+
typeof subscriptionName === 'string' && subscriptionName) {
|
|
84
|
+
this.projectId = projectId;
|
|
85
|
+
this.subscriptionName = `projects/${projectId}/subscriptions/${subscriptionName}`;
|
|
86
|
+
this.tokenScopes = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE} ${GOOGLE_CHAT_PUBSUB_CONSTANTS.PUBSUB_SCOPE}`;
|
|
87
|
+
this.mode = 'pubsub';
|
|
88
|
+
if (typeof onIncomingMessage === 'function') {
|
|
89
|
+
this.onIncomingMessage = onIncomingMessage;
|
|
64
90
|
}
|
|
65
|
-
|
|
91
|
+
this.startPubSubPull();
|
|
92
|
+
return;
|
|
66
93
|
}
|
|
67
|
-
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
94
|
+
// Service account mode (send-only)
|
|
95
|
+
this.mode = 'service-account';
|
|
96
|
+
this.tokenScopes = GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE;
|
|
70
97
|
return;
|
|
71
98
|
}
|
|
72
99
|
throw new Error('Google Chat requires either a webhookUrl or serviceAccountKey');
|
|
@@ -75,19 +102,19 @@ export class GoogleChatMessengerAdapter {
|
|
|
75
102
|
* Send a text message to a Google Chat space.
|
|
76
103
|
*
|
|
77
104
|
* In webhook mode, the `channel` parameter is ignored (webhook URL determines the space).
|
|
78
|
-
* In service
|
|
105
|
+
* In service-account/pubsub mode, `channel` is the space name (e.g., "spaces/AAAA...").
|
|
79
106
|
*
|
|
80
|
-
* @param channel - Google Chat space name (used in service
|
|
107
|
+
* @param channel - Google Chat space name (used in service-account/pubsub mode)
|
|
81
108
|
* @param text - Message content
|
|
82
109
|
* @param options - Optional send options (threadId for threaded replies)
|
|
83
110
|
* @throws Error if adapter is not initialized or send fails
|
|
84
111
|
*/
|
|
85
112
|
async sendMessage(channel, text, options) {
|
|
86
|
-
if (this.webhookUrl) {
|
|
113
|
+
if (this.mode === 'webhook' && this.webhookUrl) {
|
|
87
114
|
await this.sendViaWebhook(text, options?.threadId);
|
|
88
115
|
return;
|
|
89
116
|
}
|
|
90
|
-
if (this.serviceAccountKey) {
|
|
117
|
+
if ((this.mode === 'service-account' || this.mode === 'pubsub') && this.serviceAccountKey) {
|
|
91
118
|
await this.sendViaApi(channel, text, options?.threadId);
|
|
92
119
|
return;
|
|
93
120
|
}
|
|
@@ -96,27 +123,173 @@ export class GoogleChatMessengerAdapter {
|
|
|
96
123
|
/**
|
|
97
124
|
* Get the current connection status.
|
|
98
125
|
*
|
|
99
|
-
* @returns Status object with connected flag and
|
|
126
|
+
* @returns Status object with connected flag, platform, and mode details
|
|
100
127
|
*/
|
|
101
128
|
getStatus() {
|
|
102
|
-
const connected = Boolean(this.webhookUrl || this.serviceAccountKey);
|
|
103
129
|
return {
|
|
104
|
-
connected,
|
|
130
|
+
connected: this.mode !== 'none',
|
|
105
131
|
platform: this.platform,
|
|
106
132
|
details: {
|
|
107
|
-
mode: this.
|
|
133
|
+
mode: this.mode,
|
|
134
|
+
...(this.mode === 'pubsub' ? {
|
|
135
|
+
subscriptionName: this.subscriptionName,
|
|
136
|
+
projectId: this.projectId,
|
|
137
|
+
pullActive: Boolean(this.pullIntervalTimer) && !this.pullPaused,
|
|
138
|
+
pullPaused: this.pullPaused,
|
|
139
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
140
|
+
} : {}),
|
|
108
141
|
},
|
|
109
142
|
};
|
|
110
143
|
}
|
|
111
144
|
/**
|
|
112
|
-
* Disconnect by clearing stored credentials.
|
|
145
|
+
* Disconnect by clearing stored credentials and stopping the pull loop.
|
|
113
146
|
*/
|
|
114
147
|
async disconnect() {
|
|
115
|
-
this.
|
|
116
|
-
this.
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
this.stopPubSubPull();
|
|
149
|
+
this.resetState();
|
|
150
|
+
}
|
|
151
|
+
// ===========================================================================
|
|
152
|
+
// Pub/Sub Pull Loop
|
|
153
|
+
// ===========================================================================
|
|
154
|
+
/**
|
|
155
|
+
* Start the Pub/Sub pull loop that periodically fetches messages.
|
|
156
|
+
*/
|
|
157
|
+
startPubSubPull() {
|
|
158
|
+
this.stopPubSubPull();
|
|
159
|
+
this.consecutiveFailures = 0;
|
|
160
|
+
this.pullPaused = false;
|
|
161
|
+
this.pullIntervalTimer = setInterval(async () => {
|
|
162
|
+
if (this.pullPaused)
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
await this.pullMessages();
|
|
166
|
+
this.consecutiveFailures = 0;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
this.consecutiveFailures++;
|
|
170
|
+
if (this.consecutiveFailures >= GOOGLE_CHAT_PUBSUB_CONSTANTS.MAX_CONSECUTIVE_FAILURES) {
|
|
171
|
+
this.pullPaused = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, GOOGLE_CHAT_PUBSUB_CONSTANTS.PULL_INTERVAL_MS);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Stop the Pub/Sub pull loop.
|
|
178
|
+
*/
|
|
179
|
+
stopPubSubPull() {
|
|
180
|
+
if (this.pullIntervalTimer) {
|
|
181
|
+
clearInterval(this.pullIntervalTimer);
|
|
182
|
+
this.pullIntervalTimer = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Pull messages from the Pub/Sub subscription, process them, and acknowledge.
|
|
187
|
+
*
|
|
188
|
+
* Each message is a base64-encoded Google Chat event JSON. Only MESSAGE-type
|
|
189
|
+
* events are forwarded to the incoming message callback. All messages are
|
|
190
|
+
* acknowledged regardless of type to prevent redelivery.
|
|
191
|
+
*/
|
|
192
|
+
async pullMessages() {
|
|
193
|
+
if (!this.subscriptionName) {
|
|
194
|
+
throw new Error('Pub/Sub subscription not configured');
|
|
195
|
+
}
|
|
196
|
+
const token = await this.getAccessToken();
|
|
197
|
+
const pullUrl = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.PUBSUB_API_BASE}/${this.subscriptionName}:pull`;
|
|
198
|
+
const resp = await fetch(pullUrl, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: {
|
|
201
|
+
Authorization: `Bearer ${token}`,
|
|
202
|
+
'Content-Type': 'application/json',
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({ maxMessages: GOOGLE_CHAT_PUBSUB_CONSTANTS.MAX_MESSAGES_PER_PULL }),
|
|
205
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
206
|
+
});
|
|
207
|
+
if (!resp.ok) {
|
|
208
|
+
const details = await resp.text();
|
|
209
|
+
throw new Error(`Pub/Sub pull failed (${resp.status}): ${details}`);
|
|
210
|
+
}
|
|
211
|
+
const data = await resp.json();
|
|
212
|
+
if (!data.receivedMessages || data.receivedMessages.length === 0) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const ackIds = [];
|
|
216
|
+
for (const received of data.receivedMessages) {
|
|
217
|
+
ackIds.push(received.ackId);
|
|
218
|
+
if (!received.message.data)
|
|
219
|
+
continue;
|
|
220
|
+
try {
|
|
221
|
+
const decoded = Buffer.from(received.message.data, 'base64').toString('utf-8');
|
|
222
|
+
const event = JSON.parse(decoded);
|
|
223
|
+
this.processChatEvent(event);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Skip malformed messages — they will still be acked
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Acknowledge all messages (even non-MESSAGE types) to prevent redelivery
|
|
230
|
+
if (ackIds.length > 0) {
|
|
231
|
+
await this.acknowledgeMessages(ackIds);
|
|
232
|
+
}
|
|
119
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Process a Google Chat event and forward MESSAGE events to the callback.
|
|
236
|
+
*
|
|
237
|
+
* Thread tracking: extracts the thread name from the event so that replies
|
|
238
|
+
* can be posted back to the same thread.
|
|
239
|
+
*
|
|
240
|
+
* @param event - Parsed Google Chat event payload
|
|
241
|
+
*/
|
|
242
|
+
processChatEvent(event) {
|
|
243
|
+
// Only process MESSAGE events (ignore ADDED_TO_SPACE, REMOVED_FROM_SPACE, etc.)
|
|
244
|
+
if (event.type !== 'MESSAGE' || !event.message?.text) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!this.onIncomingMessage) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const spaceName = event.space?.name || '';
|
|
251
|
+
const threadName = event.message.thread?.name || '';
|
|
252
|
+
const senderName = event.message.sender?.displayName || event.message.sender?.name || '';
|
|
253
|
+
const incomingMessage = {
|
|
254
|
+
platform: 'google-chat',
|
|
255
|
+
conversationId: spaceName,
|
|
256
|
+
channelId: spaceName,
|
|
257
|
+
userId: senderName,
|
|
258
|
+
text: event.message.text,
|
|
259
|
+
// Thread name is the full resource path (e.g. spaces/SPACE/threads/THREAD)
|
|
260
|
+
// This is used to reply in the same thread via the Chat API
|
|
261
|
+
threadId: threadName,
|
|
262
|
+
timestamp: event.message.createTime || event.eventTime || new Date().toISOString(),
|
|
263
|
+
};
|
|
264
|
+
this.onIncomingMessage(incomingMessage);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Acknowledge processed messages to prevent redelivery.
|
|
268
|
+
*
|
|
269
|
+
* @param ackIds - Array of ack IDs from the pull response
|
|
270
|
+
*/
|
|
271
|
+
async acknowledgeMessages(ackIds) {
|
|
272
|
+
if (!this.subscriptionName)
|
|
273
|
+
return;
|
|
274
|
+
const token = await this.getAccessToken();
|
|
275
|
+
const ackUrl = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.PUBSUB_API_BASE}/${this.subscriptionName}:acknowledge`;
|
|
276
|
+
const resp = await fetch(ackUrl, {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: {
|
|
279
|
+
Authorization: `Bearer ${token}`,
|
|
280
|
+
'Content-Type': 'application/json',
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({ ackIds }),
|
|
283
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
284
|
+
});
|
|
285
|
+
if (!resp.ok) {
|
|
286
|
+
const details = await resp.text();
|
|
287
|
+
throw new Error(`Pub/Sub acknowledge failed (${resp.status}): ${details}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// ===========================================================================
|
|
291
|
+
// Send Methods
|
|
292
|
+
// ===========================================================================
|
|
120
293
|
/**
|
|
121
294
|
* Send a message via incoming webhook URL.
|
|
122
295
|
*
|
|
@@ -141,7 +314,7 @@ export class GoogleChatMessengerAdapter {
|
|
|
141
314
|
method: 'POST',
|
|
142
315
|
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
|
143
316
|
body: JSON.stringify(body),
|
|
144
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
317
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
145
318
|
});
|
|
146
319
|
if (!resp.ok) {
|
|
147
320
|
const details = await resp.text();
|
|
@@ -151,11 +324,14 @@ export class GoogleChatMessengerAdapter {
|
|
|
151
324
|
/**
|
|
152
325
|
* Send a message via the Google Chat REST API using service account credentials.
|
|
153
326
|
*
|
|
327
|
+
* When a threadId (thread name) is provided, the reply is posted to the same
|
|
328
|
+
* thread. This enables conversational thread tracking for Pub/Sub mode.
|
|
329
|
+
*
|
|
154
330
|
* @param space - Space name (e.g., "spaces/AAAA...")
|
|
155
331
|
* @param text - Message text
|
|
156
|
-
* @param
|
|
332
|
+
* @param threadName - Optional full thread name (e.g., "spaces/SPACE/threads/THREAD")
|
|
157
333
|
*/
|
|
158
|
-
async sendViaApi(space, text,
|
|
334
|
+
async sendViaApi(space, text, threadName) {
|
|
159
335
|
if (!this.serviceAccountKey) {
|
|
160
336
|
throw new Error('Service account key not configured');
|
|
161
337
|
}
|
|
@@ -164,27 +340,37 @@ export class GoogleChatMessengerAdapter {
|
|
|
164
340
|
}
|
|
165
341
|
const token = await this.getAccessToken();
|
|
166
342
|
const body = { text };
|
|
167
|
-
if (
|
|
168
|
-
|
|
343
|
+
if (threadName) {
|
|
344
|
+
// Use the full thread resource name for API-based threading
|
|
345
|
+
body.thread = { name: threadName };
|
|
346
|
+
}
|
|
347
|
+
// Build URL with messageReplyOption to enable thread replies
|
|
348
|
+
let url = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_API_BASE}/${space}/messages`;
|
|
349
|
+
if (threadName) {
|
|
350
|
+
url += '?messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD';
|
|
169
351
|
}
|
|
170
|
-
const resp = await fetch(
|
|
352
|
+
const resp = await fetch(url, {
|
|
171
353
|
method: 'POST',
|
|
172
354
|
headers: {
|
|
173
355
|
Authorization: `Bearer ${token}`,
|
|
174
356
|
'Content-Type': 'application/json',
|
|
175
357
|
},
|
|
176
358
|
body: JSON.stringify(body),
|
|
177
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
359
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
178
360
|
});
|
|
179
361
|
if (!resp.ok) {
|
|
180
362
|
const details = await resp.text();
|
|
181
363
|
throw new Error(`Google Chat API send failed (${resp.status}): ${details}`);
|
|
182
364
|
}
|
|
183
365
|
}
|
|
366
|
+
// ===========================================================================
|
|
367
|
+
// Auth
|
|
368
|
+
// ===========================================================================
|
|
184
369
|
/**
|
|
185
370
|
* Get a valid access token, refreshing if needed.
|
|
186
371
|
*
|
|
187
|
-
* Uses a
|
|
372
|
+
* Uses a JWT-based OAuth2 flow for service accounts.
|
|
373
|
+
* The scope includes Pub/Sub when in pubsub mode.
|
|
188
374
|
*
|
|
189
375
|
* @returns Valid access token string
|
|
190
376
|
*/
|
|
@@ -196,14 +382,12 @@ export class GoogleChatMessengerAdapter {
|
|
|
196
382
|
if (!this.serviceAccountKey) {
|
|
197
383
|
throw new Error('Service account key not configured');
|
|
198
384
|
}
|
|
199
|
-
// For service account auth, we need to create a JWT and exchange it for an access token.
|
|
200
|
-
// This is a simplified implementation — production use should use google-auth-library.
|
|
201
385
|
const key = JSON.parse(this.serviceAccountKey);
|
|
202
386
|
const now = Math.floor(Date.now() / 1000);
|
|
203
387
|
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
|
204
388
|
const payload = Buffer.from(JSON.stringify({
|
|
205
389
|
iss: key.client_email,
|
|
206
|
-
scope:
|
|
390
|
+
scope: this.tokenScopes,
|
|
207
391
|
aud: 'https://oauth2.googleapis.com/token',
|
|
208
392
|
iat: now,
|
|
209
393
|
exp: now + 3600,
|
|
@@ -218,7 +402,7 @@ export class GoogleChatMessengerAdapter {
|
|
|
218
402
|
method: 'POST',
|
|
219
403
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
220
404
|
body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,
|
|
221
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
405
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
222
406
|
});
|
|
223
407
|
if (!resp.ok) {
|
|
224
408
|
const details = await resp.text();
|
|
@@ -229,5 +413,45 @@ export class GoogleChatMessengerAdapter {
|
|
|
229
413
|
this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
|
|
230
414
|
return this.accessToken;
|
|
231
415
|
}
|
|
416
|
+
// ===========================================================================
|
|
417
|
+
// Helpers
|
|
418
|
+
// ===========================================================================
|
|
419
|
+
/**
|
|
420
|
+
* Validate a service account key JSON string.
|
|
421
|
+
*
|
|
422
|
+
* @param key - JSON string of the service account key
|
|
423
|
+
* @throws Error if the key is invalid
|
|
424
|
+
*/
|
|
425
|
+
validateServiceAccountKey(key) {
|
|
426
|
+
try {
|
|
427
|
+
const parsed = JSON.parse(key);
|
|
428
|
+
if (!parsed.client_email || !parsed.private_key) {
|
|
429
|
+
throw new Error('Service account key must contain client_email and private_key');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
if (err instanceof SyntaxError) {
|
|
434
|
+
throw new Error('Service account key must be valid JSON');
|
|
435
|
+
}
|
|
436
|
+
throw err;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Reset all internal state to defaults.
|
|
441
|
+
*/
|
|
442
|
+
resetState() {
|
|
443
|
+
this.stopPubSubPull();
|
|
444
|
+
this.webhookUrl = null;
|
|
445
|
+
this.serviceAccountKey = null;
|
|
446
|
+
this.accessToken = null;
|
|
447
|
+
this.tokenExpiresAt = 0;
|
|
448
|
+
this.tokenScopes = GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE;
|
|
449
|
+
this.subscriptionName = null;
|
|
450
|
+
this.projectId = null;
|
|
451
|
+
this.onIncomingMessage = null;
|
|
452
|
+
this.consecutiveFailures = 0;
|
|
453
|
+
this.pullPaused = false;
|
|
454
|
+
this.mode = 'none';
|
|
455
|
+
}
|
|
232
456
|
}
|
|
233
457
|
//# sourceMappingURL=google-chat-messenger.adapter.js.map
|