@ascegu/teamily 1.0.4 → 1.0.6

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/src/send.ts ADDED
@@ -0,0 +1,273 @@
1
+ import type { ResolvedTeamilyAccount, TeamilyMessageTarget } from "./types.js";
2
+ import { CONTENT_TYPES, SESSION_TYPES } from "./types.js";
3
+ import { generateOperationID } from "./probe.js";
4
+
5
+ export interface SendTeamilyMessageParams {
6
+ account: ResolvedTeamilyAccount;
7
+ target: TeamilyMessageTarget;
8
+ text: string;
9
+ replyToId?: string;
10
+ fetchImpl?: typeof fetch;
11
+ }
12
+
13
+ export interface SendTeamilyMediaParams {
14
+ account: ResolvedTeamilyAccount;
15
+ target: TeamilyMessageTarget;
16
+ mediaUrl: string;
17
+ mediaType: "image" | "video" | "audio" | "file";
18
+ caption?: string;
19
+ fetchImpl?: typeof fetch;
20
+ }
21
+
22
+ export interface TeamilySendResult {
23
+ success: boolean;
24
+ messageId?: string;
25
+ serverMsgID?: string;
26
+ clientMsgID?: string;
27
+ error?: string;
28
+ }
29
+
30
+ /**
31
+ * Send a text message via Teamily REST API.
32
+ *
33
+ * @param params - Send parameters
34
+ * @returns Send result with message ID or error
35
+ */
36
+ export async function sendMessageTeamily(
37
+ params: SendTeamilyMessageParams
38
+ ): Promise<TeamilySendResult> {
39
+ const { account, target, text, replyToId, fetchImpl = fetch } = params;
40
+
41
+ const url = `${account.apiURL}/msg/send_msg`;
42
+
43
+ const payload = buildSendMessagePayload({
44
+ sendID: account.userID,
45
+ target,
46
+ content: { content: text },
47
+ contentType: CONTENT_TYPES.TEXT,
48
+ replyToId,
49
+ });
50
+
51
+ try {
52
+ const response = await fetchImpl(url, {
53
+ method: "POST",
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ "operationID": generateOperationID(),
57
+ "token": account.token,
58
+ },
59
+ body: JSON.stringify(payload),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const errorText = await response.text();
64
+ return {
65
+ success: false,
66
+ error: `HTTP ${response.status}: ${errorText}`,
67
+ };
68
+ }
69
+
70
+ const data = await response.json() as {
71
+ errCode: number;
72
+ errMsg: string;
73
+ data?: {
74
+ serverMsgID: string;
75
+ clientMsgID: string;
76
+ sendTime: number;
77
+ };
78
+ };
79
+
80
+ if (data.errCode !== 0) {
81
+ return {
82
+ success: false,
83
+ error: data.errMsg || "Send failed",
84
+ };
85
+ }
86
+
87
+ return {
88
+ success: true,
89
+ messageId: data.data?.serverMsgID,
90
+ serverMsgID: data.data?.serverMsgID,
91
+ clientMsgID: data.data?.clientMsgID,
92
+ };
93
+ } catch (error) {
94
+ return {
95
+ success: false,
96
+ error: error instanceof Error ? error.message : String(error),
97
+ };
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Send a media message via Teamily REST API.
103
+ *
104
+ * @param params - Send parameters
105
+ * @returns Send result with message ID or error
106
+ */
107
+ export async function sendMediaTeamily(
108
+ params: SendTeamilyMediaParams
109
+ ): Promise<TeamilySendResult> {
110
+ const { account, target, mediaUrl, mediaType, caption, fetchImpl = fetch } = params;
111
+
112
+ const url = `${account.apiURL}/msg/send_msg`;
113
+
114
+ let content: Record<string, unknown>;
115
+ let contentType: number;
116
+
117
+ switch (mediaType) {
118
+ case "image":
119
+ contentType = CONTENT_TYPES.PICTURE;
120
+ content = {
121
+ sourcePicture: {
122
+ uuid: generateOperationID(),
123
+ type: "public",
124
+ width: 0,
125
+ height: 0,
126
+ url: mediaUrl,
127
+ },
128
+ };
129
+ break;
130
+
131
+ case "video":
132
+ contentType = CONTENT_TYPES.VIDEO;
133
+ content = {
134
+ videoPath: mediaUrl,
135
+ videoUUID: generateOperationID(),
136
+ videoUrl: mediaUrl,
137
+ videoType: "mp4",
138
+ videoSize: 0,
139
+ duration: 0,
140
+ snapshotUUID: `${generateOperationID()}_snap`,
141
+ snapshotUrl: "",
142
+ snapshotSize: 0,
143
+ snapshotWidth: 0,
144
+ snapshotHeight: 0,
145
+ };
146
+ break;
147
+
148
+ case "audio":
149
+ contentType = CONTENT_TYPES.VOICE;
150
+ content = {
151
+ uuid: generateOperationID(),
152
+ soundPath: mediaUrl,
153
+ sourceUrl: mediaUrl,
154
+ dataSize: 0,
155
+ duration: 0,
156
+ soundType: "mp3",
157
+ };
158
+ break;
159
+
160
+ case "file":
161
+ contentType = CONTENT_TYPES.FILE;
162
+ content = {
163
+ uuid: generateOperationID(),
164
+ fileName: mediaUrl.split("/").pop() || "file",
165
+ fileSize: 0,
166
+ sourceUrl: mediaUrl,
167
+ fileType: "application/octet-stream",
168
+ };
169
+ break;
170
+
171
+ default:
172
+ return {
173
+ success: false,
174
+ error: `Unsupported media type: ${mediaType}`,
175
+ };
176
+ }
177
+
178
+ // Add caption to text content for media messages
179
+ if (caption) {
180
+ content.text = caption;
181
+ }
182
+
183
+ const payload = buildSendMessagePayload({
184
+ sendID: account.userID,
185
+ target,
186
+ content,
187
+ contentType,
188
+ });
189
+
190
+ try {
191
+ const response = await fetchImpl(url, {
192
+ method: "POST",
193
+ headers: {
194
+ "Content-Type": "application/json",
195
+ "operationID": generateOperationID(),
196
+ "token": account.token,
197
+ },
198
+ body: JSON.stringify(payload),
199
+ });
200
+
201
+ if (!response.ok) {
202
+ const errorText = await response.text();
203
+ return {
204
+ success: false,
205
+ error: `HTTP ${response.status}: ${errorText}`,
206
+ };
207
+ }
208
+
209
+ const data = await response.json() as {
210
+ errCode: number;
211
+ errMsg: string;
212
+ data?: {
213
+ serverMsgID: string;
214
+ clientMsgID: string;
215
+ sendTime: number;
216
+ };
217
+ };
218
+
219
+ if (data.errCode !== 0) {
220
+ return {
221
+ success: false,
222
+ error: data.errMsg || "Send failed",
223
+ };
224
+ }
225
+
226
+ return {
227
+ success: true,
228
+ messageId: data.data?.serverMsgID,
229
+ serverMsgID: data.data?.serverMsgID,
230
+ clientMsgID: data.data?.clientMsgID,
231
+ };
232
+ } catch (error) {
233
+ return {
234
+ success: false,
235
+ error: error instanceof Error ? error.message : String(error),
236
+ };
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Build the send message payload for Teamily API.
242
+ */
243
+ function buildSendMessagePayload(params: {
244
+ sendID: string;
245
+ target: TeamilyMessageTarget;
246
+ content: Record<string, unknown>;
247
+ contentType: number;
248
+ replyToId?: string;
249
+ }): Record<string, unknown> {
250
+ const { sendID, target, content, contentType, replyToId } = params;
251
+
252
+ const payload: Record<string, unknown> = {
253
+ sendID,
254
+ recvID: target.type === "user" ? target.id : "",
255
+ groupID: target.type === "group" ? target.id : "",
256
+ content,
257
+ contentType,
258
+ sessionType: target.type === "group" ? SESSION_TYPES.GROUP : SESSION_TYPES.SINGLE,
259
+ isOnlineOnly: false,
260
+ };
261
+
262
+ if (replyToId) {
263
+ payload.quote = {
264
+ text: "",
265
+ content: {},
266
+ isReact: false,
267
+ userID: "",
268
+ msgID: replyToId,
269
+ };
270
+ }
271
+
272
+ return payload;
273
+ }
package/src/types.ts ADDED
@@ -0,0 +1,116 @@
1
+ // Core configuration types for Teamily channel
2
+
3
+ export interface TeamilyServerConfig {
4
+ platformUrl: string;
5
+ apiURL: string;
6
+ wsURL: string;
7
+ }
8
+
9
+ export interface TeamilyUserAccount {
10
+ userID: string;
11
+ token: string;
12
+ nickname?: string;
13
+ faceURL?: string;
14
+ }
15
+
16
+ export interface TeamilyConfig {
17
+ enabled: boolean;
18
+ server: TeamilyServerConfig;
19
+ accounts: Record<string, TeamilyUserAccount>;
20
+ }
21
+
22
+ export interface ResolvedTeamilyAccount {
23
+ accountId: string;
24
+ enabled: boolean;
25
+ platformUrl: string;
26
+ apiURL: string;
27
+ wsURL: string;
28
+ userID: string;
29
+ token: string;
30
+ nickname?: string;
31
+ faceURL?: string;
32
+ }
33
+
34
+ export interface TeamilyMessageTarget {
35
+ type: "user" | "group";
36
+ id: string;
37
+ }
38
+
39
+ export interface TeamilyMessage {
40
+ serverMsgID: string;
41
+ sendID: string;
42
+ recvID: string;
43
+ content: {
44
+ text?: string;
45
+ picture?: TeamilyPictureContent;
46
+ video?: TeamilyVideoContent;
47
+ audio?: TeamilyAudioContent;
48
+ };
49
+ contentType: number;
50
+ sessionType: number;
51
+ sendTime: number;
52
+ }
53
+
54
+ export interface TeamilyPictureContent {
55
+ sourcePicture: {
56
+ uuid: string;
57
+ type: string;
58
+ width: number;
59
+ height: number;
60
+ url: string;
61
+ };
62
+ }
63
+
64
+ export interface TeamilyVideoContent {
65
+ videoPath: string;
66
+ videoUUID: string;
67
+ videoUrl: string;
68
+ videoType: string;
69
+ videoSize: number;
70
+ duration: number;
71
+ snapshotUUID: string;
72
+ snapshotUrl: string;
73
+ snapshotSize: number;
74
+ snapshotWidth: number;
75
+ snapshotHeight: number;
76
+ }
77
+
78
+ export interface TeamilyAudioContent {
79
+ uuid: string;
80
+ soundPath: string;
81
+ sourceUrl: string;
82
+ dataSize: number;
83
+ duration: number;
84
+ soundType: string;
85
+ }
86
+
87
+ export type TeamilyProbeResult = {
88
+ connected: boolean;
89
+ error?: string;
90
+ userExists?: boolean;
91
+ };
92
+
93
+ // Content type constants
94
+ export const CONTENT_TYPES = {
95
+ TEXT: 101,
96
+ PICTURE: 102,
97
+ VOICE: 103,
98
+ VIDEO: 104,
99
+ FILE: 105,
100
+ AT_TEXT: 106,
101
+ MERGER: 107,
102
+ CARD: 108,
103
+ LOCATION: 109,
104
+ CUSTOM: 110,
105
+ TYPING: 113,
106
+ QUOTE: 114,
107
+ FACE: 115,
108
+ ADVANCED_TEXT: 117,
109
+ } as const;
110
+
111
+ // Session type constants
112
+ export const SESSION_TYPES = {
113
+ SINGLE: 1,
114
+ GROUP: 3,
115
+ NOTIFICATION: 4,
116
+ } as const;