@ascegu/teamily 1.0.9 → 1.0.10
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/package.json +1 -1
- package/src/channel.ts +35 -55
- package/src/monitor.ts +102 -8
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -21,11 +21,10 @@ import {
|
|
|
21
21
|
} from "./accounts.js";
|
|
22
22
|
import { TeamilyConfigSchema } from "./config-schema.js";
|
|
23
23
|
import type { CoreConfig } from "./config-schema.js";
|
|
24
|
-
import { startTeamilyMonitoring, stopTeamilyMonitoring } from "./monitor.js";
|
|
24
|
+
import { getTeamilyMonitor, startTeamilyMonitoring, stopTeamilyMonitoring } from "./monitor.js";
|
|
25
25
|
import { normalizeTeamilyTarget, normalizeTeamilyAllowEntry } from "./normalize.js";
|
|
26
26
|
import { probeTeamily } from "./probe.js";
|
|
27
27
|
import { getTeamilyRuntime } from "./runtime.js";
|
|
28
|
-
import { sendMessageTeamily, sendMediaTeamily } from "./send.js";
|
|
29
28
|
import type { ResolvedTeamilyAccount } from "./types.js";
|
|
30
29
|
import { SESSION_TYPES } from "./types.js";
|
|
31
30
|
|
|
@@ -89,13 +88,12 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
89
88
|
notifyApproval: async ({ id, cfg }) => {
|
|
90
89
|
try {
|
|
91
90
|
const accountId = resolveDefaultTeamilyAccountId(cfg as CoreConfig);
|
|
92
|
-
const account = resolveTeamilyAccount(cfg as CoreConfig, accountId);
|
|
93
91
|
const target = normalizeTeamilyTarget(id);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
target,
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
const monitor = getTeamilyMonitor(accountId);
|
|
93
|
+
if (monitor) {
|
|
94
|
+
await monitor.sendText(target, PAIRING_APPROVED_MESSAGE);
|
|
95
|
+
}
|
|
96
|
+
// If monitor isn't running, skip silently — pairing was still approved
|
|
99
97
|
} catch {
|
|
100
98
|
// Silently fail on notification
|
|
101
99
|
}
|
|
@@ -152,71 +150,42 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
152
150
|
}
|
|
153
151
|
try {
|
|
154
152
|
const target = normalizeTeamilyTarget(to);
|
|
155
|
-
|
|
153
|
+
// Preserve the full target format so sendText/sendMedia can distinguish user vs group
|
|
154
|
+
const resolved = target.type === "group" ? `group:${target.id}` : target.id;
|
|
155
|
+
return { ok: true, to: resolved };
|
|
156
156
|
} catch (err) {
|
|
157
157
|
return { ok: false, error: err instanceof Error ? err : new Error(String(err)) };
|
|
158
158
|
}
|
|
159
159
|
},
|
|
160
160
|
sendText: async (ctx: ChannelOutboundContext) => {
|
|
161
161
|
const { to, text, accountId } = ctx;
|
|
162
|
-
const
|
|
162
|
+
const monitor = requireMonitor(accountId);
|
|
163
163
|
const target = normalizeTeamilyTarget(to);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
account,
|
|
167
|
-
target,
|
|
168
|
-
text,
|
|
169
|
-
replyToId: ctx.replyToId || undefined,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (!result.success) {
|
|
173
|
-
throw new Error(result.error || "Failed to send message");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { channel: "teamily" as const, messageId: result.messageId ?? "" };
|
|
164
|
+
const messageId = await monitor.sendText(target, text);
|
|
165
|
+
return { channel: "teamily" as const, messageId };
|
|
177
166
|
},
|
|
178
167
|
sendMedia: async (ctx: ChannelOutboundContext) => {
|
|
179
|
-
const { to,
|
|
168
|
+
const { to, accountId } = ctx;
|
|
180
169
|
const mediaUrl = ctx.mediaUrl;
|
|
181
170
|
if (!mediaUrl) {
|
|
182
171
|
throw new Error("Media URL is required");
|
|
183
172
|
}
|
|
184
|
-
const
|
|
173
|
+
const monitor = requireMonitor(accountId);
|
|
185
174
|
const target = normalizeTeamilyTarget(to);
|
|
186
175
|
|
|
187
|
-
|
|
188
|
-
let mediaType: "image" | "video" | "audio" | "file" = "image";
|
|
176
|
+
let messageId: string;
|
|
189
177
|
const urlLower = mediaUrl.toLowerCase();
|
|
190
178
|
if (urlLower.endsWith(".mp4") || urlLower.endsWith(".mov") || urlLower.endsWith(".webm")) {
|
|
191
|
-
|
|
192
|
-
} else if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} else if (
|
|
199
|
-
urlLower.endsWith(".pdf") ||
|
|
200
|
-
urlLower.endsWith(".doc") ||
|
|
201
|
-
urlLower.endsWith(".docx") ||
|
|
202
|
-
urlLower.endsWith(".zip")
|
|
203
|
-
) {
|
|
204
|
-
mediaType = "file";
|
|
179
|
+
messageId = await monitor.sendVideo(target, mediaUrl);
|
|
180
|
+
} else if (urlLower.endsWith(".mp3") || urlLower.endsWith(".m4a") || urlLower.endsWith(".wav")) {
|
|
181
|
+
messageId = await monitor.sendAudio(target, mediaUrl);
|
|
182
|
+
} else if (urlLower.endsWith(".pdf") || urlLower.endsWith(".doc") || urlLower.endsWith(".docx") || urlLower.endsWith(".zip")) {
|
|
183
|
+
messageId = await monitor.sendFile(target, mediaUrl);
|
|
184
|
+
} else {
|
|
185
|
+
messageId = await monitor.sendImage(target, mediaUrl);
|
|
205
186
|
}
|
|
206
187
|
|
|
207
|
-
|
|
208
|
-
account,
|
|
209
|
-
target,
|
|
210
|
-
mediaUrl,
|
|
211
|
-
mediaType,
|
|
212
|
-
caption: text,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (!result.success) {
|
|
216
|
-
throw new Error(result.error || "Failed to send media");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return { channel: "teamily" as const, messageId: result.messageId ?? "" };
|
|
188
|
+
return { channel: "teamily" as const, messageId };
|
|
220
189
|
},
|
|
221
190
|
},
|
|
222
191
|
status: {
|
|
@@ -305,8 +274,10 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
305
274
|
deliver: async (payload: { text?: string; body?: string }) => {
|
|
306
275
|
const replyText = payload?.text ?? payload?.body;
|
|
307
276
|
if (replyText) {
|
|
277
|
+
const monitor = getTeamilyMonitor(accountId);
|
|
278
|
+
if (!monitor) throw new Error(`Teamily monitor not running for account ${accountId}`);
|
|
308
279
|
const target = normalizeTeamilyTarget(from);
|
|
309
|
-
await
|
|
280
|
+
await monitor.sendText(target, replyText);
|
|
310
281
|
}
|
|
311
282
|
},
|
|
312
283
|
onReplyStart: () => {
|
|
@@ -369,3 +340,12 @@ function applyTeamilyAccountConfig(params: {
|
|
|
369
340
|
},
|
|
370
341
|
} as CoreConfig;
|
|
371
342
|
}
|
|
343
|
+
|
|
344
|
+
function requireMonitor(accountId?: string | null) {
|
|
345
|
+
const id = accountId || "default";
|
|
346
|
+
const monitor = getTeamilyMonitor(id);
|
|
347
|
+
if (!monitor) {
|
|
348
|
+
throw new Error(`Teamily gateway not running for account "${id}" — outbound requires an active gateway`);
|
|
349
|
+
}
|
|
350
|
+
return monitor;
|
|
351
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ResolvedTeamilyAccount,
|
|
3
3
|
TeamilyMessage,
|
|
4
|
+
TeamilyMessageTarget,
|
|
4
5
|
TeamilyPictureContent,
|
|
5
6
|
TeamilyVideoContent,
|
|
6
7
|
TeamilyAudioContent,
|
|
@@ -32,7 +33,8 @@ async function loadSDK() {
|
|
|
32
33
|
* Monitor for incoming Teamily messages using @openim/client-sdk.
|
|
33
34
|
*
|
|
34
35
|
* Delegates WebSocket connection, authentication, heartbeat, and
|
|
35
|
-
* reconnection to the official OpenIM SDK.
|
|
36
|
+
* reconnection to the official OpenIM SDK. Also exposes send methods
|
|
37
|
+
* so outbound replies flow through the same WebSocket connection.
|
|
36
38
|
*/
|
|
37
39
|
export class TeamilyMonitor {
|
|
38
40
|
private account: ResolvedTeamilyAccount;
|
|
@@ -56,7 +58,6 @@ export class TeamilyMonitor {
|
|
|
56
58
|
const sdk = getSDK();
|
|
57
59
|
this.sdk = sdk;
|
|
58
60
|
|
|
59
|
-
// Connection events
|
|
60
61
|
sdk.on(CbEvents.OnConnecting, () => {
|
|
61
62
|
if (!this.stopped) this.setState("connecting");
|
|
62
63
|
});
|
|
@@ -73,15 +74,12 @@ export class TeamilyMonitor {
|
|
|
73
74
|
if (!this.stopped) this.setState("error", "Token expired");
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
// Incoming messages
|
|
77
77
|
sdk.on(CbEvents.OnRecvNewMessages, ({ data }) => {
|
|
78
78
|
if (this.stopped || !data) return;
|
|
79
79
|
for (const msg of data) {
|
|
80
|
-
// Skip self-sent messages
|
|
81
80
|
if (msg.sendID === this.account.userID) continue;
|
|
82
81
|
const converted = convertSdkMessage(msg, this.account.userID);
|
|
83
82
|
if (converted) {
|
|
84
|
-
// Fire-and-forget; errors are logged by the gateway dispatcher
|
|
85
83
|
void this.onMessage(converted);
|
|
86
84
|
}
|
|
87
85
|
}
|
|
@@ -115,6 +113,105 @@ export class TeamilyMonitor {
|
|
|
115
113
|
return this.state;
|
|
116
114
|
}
|
|
117
115
|
|
|
116
|
+
/** Send a text message through the SDK WebSocket connection. */
|
|
117
|
+
async sendText(target: TeamilyMessageTarget, text: string): Promise<string> {
|
|
118
|
+
const sdk = this.requireSdk();
|
|
119
|
+
const created = await sdk.createTextMessage(text);
|
|
120
|
+
const message = created.data;
|
|
121
|
+
const result = await sdk.sendMessage({
|
|
122
|
+
recvID: target.type === "user" ? target.id : "",
|
|
123
|
+
groupID: target.type === "group" ? target.id : "",
|
|
124
|
+
message,
|
|
125
|
+
});
|
|
126
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Send an image message through the SDK WebSocket connection. */
|
|
130
|
+
async sendImage(target: TeamilyMessageTarget, url: string): Promise<string> {
|
|
131
|
+
const sdk = this.requireSdk();
|
|
132
|
+
const picInfo = { uuid: "", type: "", width: 0, height: 0, size: 0, url };
|
|
133
|
+
const created = await sdk.createImageMessageByURL({
|
|
134
|
+
sourcePicture: picInfo,
|
|
135
|
+
bigPicture: picInfo,
|
|
136
|
+
snapshotPicture: { ...picInfo, url: "" },
|
|
137
|
+
sourcePath: "",
|
|
138
|
+
});
|
|
139
|
+
const result = await sdk.sendMessage({
|
|
140
|
+
recvID: target.type === "user" ? target.id : "",
|
|
141
|
+
groupID: target.type === "group" ? target.id : "",
|
|
142
|
+
message: created.data,
|
|
143
|
+
});
|
|
144
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Send a video message through the SDK WebSocket connection. */
|
|
148
|
+
async sendVideo(target: TeamilyMessageTarget, url: string): Promise<string> {
|
|
149
|
+
const sdk = this.requireSdk();
|
|
150
|
+
const created = await sdk.createVideoMessageByURL({
|
|
151
|
+
videoPath: "",
|
|
152
|
+
duration: 0,
|
|
153
|
+
videoType: "mp4",
|
|
154
|
+
snapshotPath: "",
|
|
155
|
+
videoUUID: "",
|
|
156
|
+
videoUrl: url,
|
|
157
|
+
videoSize: 0,
|
|
158
|
+
snapshotUUID: "",
|
|
159
|
+
snapshotSize: 0,
|
|
160
|
+
snapshotUrl: "",
|
|
161
|
+
snapshotWidth: 0,
|
|
162
|
+
snapshotHeight: 0,
|
|
163
|
+
});
|
|
164
|
+
const result = await sdk.sendMessage({
|
|
165
|
+
recvID: target.type === "user" ? target.id : "",
|
|
166
|
+
groupID: target.type === "group" ? target.id : "",
|
|
167
|
+
message: created.data,
|
|
168
|
+
});
|
|
169
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Send a sound/audio message through the SDK WebSocket connection. */
|
|
173
|
+
async sendAudio(target: TeamilyMessageTarget, url: string): Promise<string> {
|
|
174
|
+
const sdk = this.requireSdk();
|
|
175
|
+
const created = await sdk.createSoundMessageByURL({
|
|
176
|
+
uuid: "",
|
|
177
|
+
soundPath: "",
|
|
178
|
+
sourceUrl: url,
|
|
179
|
+
dataSize: 0,
|
|
180
|
+
duration: 0,
|
|
181
|
+
});
|
|
182
|
+
const result = await sdk.sendMessage({
|
|
183
|
+
recvID: target.type === "user" ? target.id : "",
|
|
184
|
+
groupID: target.type === "group" ? target.id : "",
|
|
185
|
+
message: created.data,
|
|
186
|
+
});
|
|
187
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Send a file message through the SDK WebSocket connection. */
|
|
191
|
+
async sendFile(target: TeamilyMessageTarget, url: string, fileName?: string): Promise<string> {
|
|
192
|
+
const sdk = this.requireSdk();
|
|
193
|
+
const created = await sdk.createFileMessageByURL({
|
|
194
|
+
filePath: "",
|
|
195
|
+
fileName: fileName || url.split("/").pop() || "file",
|
|
196
|
+
uuid: "",
|
|
197
|
+
sourceUrl: url,
|
|
198
|
+
fileSize: 0,
|
|
199
|
+
});
|
|
200
|
+
const result = await sdk.sendMessage({
|
|
201
|
+
recvID: target.type === "user" ? target.id : "",
|
|
202
|
+
groupID: target.type === "group" ? target.id : "",
|
|
203
|
+
message: created.data,
|
|
204
|
+
});
|
|
205
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private requireSdk(): SdkInstance {
|
|
209
|
+
if (!this.sdk) {
|
|
210
|
+
throw new Error("Teamily SDK not connected");
|
|
211
|
+
}
|
|
212
|
+
return this.sdk;
|
|
213
|
+
}
|
|
214
|
+
|
|
118
215
|
private setState(state: TeamilyConnectionState, error?: string): void {
|
|
119
216
|
this.state = state;
|
|
120
217
|
this.onStateChange?.(state, error);
|
|
@@ -131,7 +228,6 @@ function convertSdkMessage(msg: MessageItem, selfUserID: string): TeamilyMessage
|
|
|
131
228
|
|
|
132
229
|
const content = parseSdkContent(msg, contentType);
|
|
133
230
|
|
|
134
|
-
// Skip messages with no usable content
|
|
135
231
|
if (!content.text && !content.picture && !content.video && !content.audio) {
|
|
136
232
|
return null;
|
|
137
233
|
}
|
|
@@ -150,7 +246,6 @@ function convertSdkMessage(msg: MessageItem, selfUserID: string): TeamilyMessage
|
|
|
150
246
|
function parseSdkContent(msg: MessageItem, contentType: number): TeamilyMessage["content"] {
|
|
151
247
|
switch (contentType) {
|
|
152
248
|
case CONTENT_TYPES.TEXT: {
|
|
153
|
-
// SDK puts text in textElem.content; fallback to raw content string
|
|
154
249
|
const text = msg.textElem?.content ?? tryParseTextContent(msg.content);
|
|
155
250
|
return text ? { text } : {};
|
|
156
251
|
}
|
|
@@ -174,7 +269,6 @@ function parseSdkContent(msg: MessageItem, contentType: number): TeamilyMessage[
|
|
|
174
269
|
}
|
|
175
270
|
}
|
|
176
271
|
|
|
177
|
-
/** Try to extract text from the raw JSON content string (OpenIM text format: `{"content":"..."}`) */
|
|
178
272
|
function tryParseTextContent(raw: string | undefined): string | undefined {
|
|
179
273
|
if (!raw) return undefined;
|
|
180
274
|
try {
|