@warriorteam/redai-zalo-sdk 1.1.0 → 1.2.0

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.
@@ -0,0 +1,336 @@
1
+ # Article Management Service
2
+
3
+ The `ArticleService` and `VideoUploadService` provide comprehensive article management capabilities for Zalo Official Account (OA).
4
+
5
+ ## Features
6
+
7
+ ### ArticleService
8
+ - ✅ Create normal articles with rich content
9
+ - ✅ Create video articles
10
+ - ✅ Update existing articles
11
+ - ✅ Get article details and lists
12
+ - ✅ Remove articles
13
+ - ✅ Track article creation/update progress
14
+ - ✅ Comprehensive validation
15
+
16
+ ### VideoUploadService
17
+ - ✅ Upload video files for articles
18
+ - ✅ Check video processing status
19
+ - ✅ Wait for upload completion with polling
20
+ - ✅ Upload videos from URLs
21
+ - ✅ Get video information
22
+
23
+ ## Prerequisites
24
+
25
+ 1. **OA Requirements:**
26
+ - OA must have permission to create articles
27
+ - Access token must have "manage_article" scope
28
+ - OA must have active status and be verified
29
+
30
+ 2. **Content Limits:**
31
+ - Title: max 150 characters
32
+ - Author: max 50 characters (normal articles only)
33
+ - Description: max 300 characters
34
+ - Image size: max 1MB per image
35
+ - Video size: max 50MB per video
36
+ - Video formats: MP4, AVI only
37
+
38
+ ## Usage
39
+
40
+ ### Initialize SDK
41
+
42
+ ```typescript
43
+ import { ZaloSDK } from 'redai-zalo-sdk';
44
+
45
+ const sdk = new ZaloSDK({
46
+ appId: 'your_app_id',
47
+ appSecret: 'your_app_secret',
48
+ });
49
+
50
+ const article = sdk.article;
51
+ const videoUpload = sdk.videoUpload;
52
+ ```
53
+
54
+ ### 1. Create Normal Article
55
+
56
+ ```typescript
57
+ import { ArticleType, ArticleStatus, CommentStatus, CoverType, BodyItemType } from 'redai-zalo-sdk';
58
+
59
+ const normalArticle = {
60
+ type: ArticleType.NORMAL,
61
+ title: "Welcome to Our Service",
62
+ author: "RedAI Team",
63
+ description: "This is a comprehensive guide to our new features",
64
+ cover: {
65
+ cover_type: CoverType.PHOTO,
66
+ photo_url: "https://example.com/cover.jpg",
67
+ status: ArticleStatus.SHOW
68
+ },
69
+ body: [
70
+ {
71
+ type: BodyItemType.TEXT,
72
+ content: "Welcome to our new service! Here's what you need to know..."
73
+ },
74
+ {
75
+ type: BodyItemType.IMAGE,
76
+ url: "https://example.com/image1.jpg"
77
+ },
78
+ {
79
+ type: BodyItemType.TEXT,
80
+ content: "For more information, please contact us."
81
+ }
82
+ ],
83
+ tracking_link: "https://your-website.com/tracking",
84
+ status: ArticleStatus.SHOW,
85
+ comment: CommentStatus.SHOW
86
+ };
87
+
88
+ const result = await article.createNormalArticle(accessToken, normalArticle);
89
+ console.log(`Article creation token: ${result.token}`);
90
+ ```
91
+
92
+ ### 2. Create Video Article
93
+
94
+ ```typescript
95
+ const videoArticle = {
96
+ type: ArticleType.VIDEO,
97
+ title: "Product Demo Video",
98
+ description: "Watch our latest product demonstration",
99
+ video_id: "your_video_id", // Get from video upload
100
+ avatar: "https://example.com/thumbnail.jpg",
101
+ status: ArticleStatus.SHOW,
102
+ comment: CommentStatus.SHOW
103
+ };
104
+
105
+ const result = await article.createVideoArticle(accessToken, videoArticle);
106
+ console.log(`Video article creation token: ${result.token}`);
107
+ ```
108
+
109
+ ### 3. Upload Video for Articles
110
+
111
+ ```typescript
112
+ // Upload video file
113
+ const videoFile = new File([videoBuffer], 'demo.mp4', { type: 'video/mp4' });
114
+ const uploadResult = await videoUpload.uploadVideo(accessToken, videoFile);
115
+
116
+ // Wait for processing completion
117
+ const finalResult = await videoUpload.waitForUploadCompletion(
118
+ accessToken,
119
+ uploadResult.token,
120
+ 5 * 60 * 1000, // 5 minutes timeout
121
+ 5 * 1000 // 5 seconds polling interval
122
+ );
123
+
124
+ console.log(`Video ID: ${finalResult.video_id}`);
125
+ ```
126
+
127
+ ### 4. Upload Video from URL
128
+
129
+ ```typescript
130
+ const uploadResult = await videoUpload.uploadVideoFromUrl(
131
+ accessToken,
132
+ 'https://example.com/video.mp4'
133
+ );
134
+
135
+ const finalResult = await videoUpload.waitForUploadCompletion(
136
+ accessToken,
137
+ uploadResult.token
138
+ );
139
+ ```
140
+
141
+ ### 5. Check Article Creation Progress
142
+
143
+ ```typescript
144
+ const progress = await article.checkArticleProcess(accessToken, token);
145
+
146
+ switch (progress.status) {
147
+ case 'processing':
148
+ console.log('Article is still being processed...');
149
+ break;
150
+ case 'success':
151
+ console.log(`Article created with ID: ${progress.article_id}`);
152
+ break;
153
+ case 'failed':
154
+ console.error(`Article creation failed: ${progress.error_message}`);
155
+ break;
156
+ }
157
+ ```
158
+
159
+ ### 6. Get Article Details
160
+
161
+ ```typescript
162
+ const articleDetail = await article.getArticleDetail(accessToken, articleId);
163
+ console.log('Article details:', articleDetail);
164
+ ```
165
+
166
+ ### 7. Get Article List
167
+
168
+ ```typescript
169
+ const articleList = await article.getArticleList(accessToken, {
170
+ offset: 0,
171
+ limit: 20,
172
+ type: 'normal' // or 'video'
173
+ });
174
+
175
+ console.log(`Found ${articleList.data.total} articles`);
176
+ articleList.data.articles.forEach(article => {
177
+ console.log(`- ${article.title} (${article.total_view} views)`);
178
+ });
179
+ ```
180
+
181
+ ### 8. Update Article
182
+
183
+ ```typescript
184
+ const updateData = {
185
+ id: articleId,
186
+ type: ArticleType.NORMAL,
187
+ title: "Updated Article Title",
188
+ author: "Updated Author",
189
+ description: "Updated description",
190
+ cover: {
191
+ cover_type: CoverType.PHOTO,
192
+ photo_url: "https://example.com/new-cover.jpg",
193
+ status: ArticleStatus.SHOW
194
+ },
195
+ body: [
196
+ {
197
+ type: BodyItemType.TEXT,
198
+ content: "Updated content..."
199
+ }
200
+ ],
201
+ status: ArticleStatus.SHOW
202
+ };
203
+
204
+ const updateResult = await article.updateNormalArticle(accessToken, updateData);
205
+ console.log(`Update token: ${updateResult.token}`);
206
+ ```
207
+
208
+ ### 9. Remove Article
209
+
210
+ ```typescript
211
+ const removeResult = await article.removeArticle(accessToken, articleId);
212
+ console.log(removeResult.message);
213
+ ```
214
+
215
+ ### 10. Check Video Status
216
+
217
+ ```typescript
218
+ const videoStatus = await videoUpload.checkVideoStatus(accessToken, token);
219
+
220
+ console.log(`Status: ${videoStatus.status}`);
221
+ console.log(`Progress: ${videoStatus.convert_percent}%`);
222
+
223
+ if (videoStatus.video_id) {
224
+ console.log(`Video ID: ${videoStatus.video_id}`);
225
+ }
226
+ ```
227
+
228
+ ## Error Handling
229
+
230
+ ```typescript
231
+ import { ZaloSDKError } from 'redai-zalo-sdk';
232
+
233
+ try {
234
+ const result = await article.createNormalArticle(accessToken, articleData);
235
+ console.log('Success:', result);
236
+ } catch (error) {
237
+ if (error instanceof ZaloSDKError) {
238
+ console.error('Zalo API Error:', error.message, error.code);
239
+ console.error('Details:', error.details);
240
+ } else {
241
+ console.error('Unexpected error:', error);
242
+ }
243
+ }
244
+ ```
245
+
246
+ ## Complete Example
247
+
248
+ ```typescript
249
+ import { ZaloSDK, ArticleType, CoverType, BodyItemType, ArticleStatus } from 'redai-zalo-sdk';
250
+
251
+ class ArticleManager {
252
+ private sdk: ZaloSDK;
253
+
254
+ constructor() {
255
+ this.sdk = new ZaloSDK({
256
+ appId: process.env.ZALO_APP_ID!,
257
+ appSecret: process.env.ZALO_APP_SECRET!,
258
+ });
259
+ }
260
+
261
+ async createCompleteArticle(accessToken: string) {
262
+ try {
263
+ // 1. Upload video first (if needed)
264
+ const videoFile = new File([videoBuffer], 'demo.mp4');
265
+ const videoUpload = await this.sdk.videoUpload.uploadVideo(accessToken, videoFile);
266
+ const videoResult = await this.sdk.videoUpload.waitForUploadCompletion(
267
+ accessToken,
268
+ videoUpload.token
269
+ );
270
+
271
+ // 2. Create article with video
272
+ const article = await this.sdk.article.createNormalArticle(accessToken, {
273
+ type: ArticleType.NORMAL,
274
+ title: "Complete Guide",
275
+ author: "RedAI Team",
276
+ description: "A complete guide with video content",
277
+ cover: {
278
+ cover_type: CoverType.PHOTO,
279
+ photo_url: "https://example.com/cover.jpg",
280
+ status: ArticleStatus.SHOW
281
+ },
282
+ body: [
283
+ {
284
+ type: BodyItemType.TEXT,
285
+ content: "Introduction to our service..."
286
+ },
287
+ {
288
+ type: BodyItemType.VIDEO,
289
+ video_id: videoResult.video_id
290
+ },
291
+ {
292
+ type: BodyItemType.TEXT,
293
+ content: "Thank you for watching!"
294
+ }
295
+ ],
296
+ status: ArticleStatus.SHOW
297
+ });
298
+
299
+ // 3. Wait for article creation
300
+ const articleProgress = await this.sdk.article.checkArticleProcess(
301
+ accessToken,
302
+ article.token
303
+ );
304
+
305
+ return articleProgress;
306
+ } catch (error) {
307
+ console.error('Article creation error:', error);
308
+ throw error;
309
+ }
310
+ }
311
+ }
312
+ ```
313
+
314
+ ## Validation Rules
315
+
316
+ ### Normal Articles
317
+ - Title: required, max 150 characters
318
+ - Author: required, max 50 characters
319
+ - Description: required, max 300 characters
320
+ - Cover: required, must specify type and corresponding URL/ID
321
+ - Body: required, at least one item
322
+
323
+ ### Video Articles
324
+ - Title: required, max 150 characters
325
+ - Description: required, max 300 characters
326
+ - Video ID: required, must be valid uploaded video
327
+ - Avatar: required, thumbnail URL
328
+
329
+ ### Video Files
330
+ - Formats: MP4, AVI only
331
+ - Size: max 50MB
332
+ - Processing: asynchronous, use polling to check status
333
+
334
+ ## API Reference
335
+
336
+ For detailed API documentation, see the TypeScript definitions in the source code. All methods include comprehensive JSDoc comments with parameter descriptions and return types.
@@ -0,0 +1,330 @@
1
+ # Consultation Service - Hướng Dẫn Sử Dụng
2
+
3
+ ## Tổng Quan
4
+
5
+ `ConsultationService` là dịch vụ chuyên dụng để gửi tin nhắn tư vấn (Customer Service) qua Zalo Official Account. Tin nhắn tư vấn cho phép OA gửi tin nhắn chủ động đến người dùng trong khung thời gian nhất định để hỗ trợ và tư vấn khách hàng.
6
+
7
+ ## Điều Kiện Gửi Tin Nhắn Tư Vấn
8
+
9
+ ### 1. Thời Gian Gửi
10
+ - **Khung thời gian**: Chỉ được gửi trong vòng **48 giờ** kể từ khi người dùng tương tác cuối cùng với OA
11
+ - **Tương tác bao gồm**:
12
+ - Gửi tin nhắn đến OA
13
+ - Nhấn button/quick reply
14
+ - Gọi điện thoại từ OA
15
+ - Truy cập website từ OA
16
+
17
+ ### 2. Nội Dung Tin Nhắn
18
+ - **Mục đích**: Phải liên quan đến tư vấn, hỗ trợ khách hàng
19
+ - **Bao gồm**: Trả lời câu hỏi, hướng dẫn sử dụng, hỗ trợ kỹ thuật
20
+ - **Không được**: Chứa nội dung quảng cáo trực tiếp
21
+
22
+ ### 3. Điều Kiện Người Dùng
23
+ - Người dùng phải đã follow OA
24
+ - Người dùng không được block OA
25
+ - Người dùng phải có tương tác gần đây với OA
26
+
27
+ ### 4. Tần Suất Gửi
28
+ - Không giới hạn số lượng tin nhắn tư vấn trong ngày
29
+ - Cần tuân thủ nguyên tắc không spam
30
+
31
+ ## Khởi Tạo Service
32
+
33
+ ```typescript
34
+ import { ZaloSDK } from "@warriorteam/redai-zalo-sdk";
35
+
36
+ const zalo = new ZaloSDK({
37
+ appId: "your-app-id",
38
+ appSecret: "your-app-secret",
39
+ debug: true
40
+ });
41
+
42
+ // Lấy consultation service
43
+ const consultationService = zalo.consultation;
44
+ ```
45
+
46
+ ## Các Phương Thức Chính
47
+
48
+ ### 1. Gửi Tin Nhắn Văn Bản
49
+
50
+ ```typescript
51
+ // Gửi tin nhắn văn bản tư vấn
52
+ const response = await consultationService.sendTextMessage(
53
+ accessToken,
54
+ { user_id: "user-id-here" },
55
+ {
56
+ type: "text",
57
+ text: "Xin chào! Tôi có thể hỗ trợ gì cho bạn?"
58
+ }
59
+ );
60
+
61
+ console.log("Message ID:", response.message_id);
62
+ console.log("Quota remaining:", response.quota?.remain);
63
+ ```
64
+
65
+ **Giới hạn văn bản:**
66
+ - Nội dung không được để trống
67
+ - Tối đa 2000 ký tự
68
+
69
+ ### 2. Gửi Tin Nhắn Hình Ảnh
70
+
71
+ ```typescript
72
+ // Gửi hình ảnh tư vấn
73
+ const response = await consultationService.sendImageMessage(
74
+ accessToken,
75
+ { user_id: "user-id-here" },
76
+ {
77
+ type: "image",
78
+ attachment: {
79
+ type: "image",
80
+ payload: {
81
+ url: "https://example.com/support-image.jpg"
82
+ }
83
+ }
84
+ }
85
+ );
86
+ ```
87
+
88
+ ### 3. Gửi File Đính Kèm
89
+
90
+ ```typescript
91
+ // Gửi file hướng dẫn
92
+ const response = await consultationService.sendFileMessage(
93
+ accessToken,
94
+ { user_id: "user-id-here" },
95
+ {
96
+ type: "file",
97
+ url: "https://example.com/user-manual.pdf",
98
+ filename: "Hướng dẫn sử dụng.pdf",
99
+ attachment: {
100
+ type: "file",
101
+ payload: {
102
+ url: "https://example.com/user-manual.pdf"
103
+ }
104
+ }
105
+ }
106
+ );
107
+ ```
108
+
109
+ ### 4. Gửi Sticker
110
+
111
+ ```typescript
112
+ // Gửi sticker thân thiện
113
+ const response = await consultationService.sendStickerMessage(
114
+ accessToken,
115
+ { user_id: "user-id-here" },
116
+ {
117
+ type: "sticker",
118
+ sticker_id: "sticker-id",
119
+ attachment: {
120
+ type: "sticker",
121
+ payload: {
122
+ id: "sticker-id"
123
+ }
124
+ }
125
+ }
126
+ );
127
+ ```
128
+
129
+ ### 5. Gửi Tin Nhắn Tổng Quát
130
+
131
+ ```typescript
132
+ // Gửi bất kỳ loại tin nhắn nào
133
+ const response = await consultationService.sendMessage(
134
+ accessToken,
135
+ { user_id: "user-id-here" },
136
+ {
137
+ type: "text",
138
+ text: "Cảm ơn bạn đã liên hệ. Chúng tôi sẽ hỗ trợ bạn ngay!"
139
+ }
140
+ );
141
+ ```
142
+
143
+ ## Xử Lý Lỗi
144
+
145
+ ```typescript
146
+ import { ZaloSDKError } from "@warriorteam/redai-zalo-sdk";
147
+
148
+ try {
149
+ const response = await consultationService.sendTextMessage(
150
+ accessToken,
151
+ { user_id: "user-id" },
152
+ { type: "text", text: "Hello!" }
153
+ );
154
+ } catch (error) {
155
+ if (error instanceof ZaloSDKError) {
156
+ console.error("Zalo API Error:", error.message);
157
+ console.error("Error Code:", error.code);
158
+
159
+ // Xử lý các lỗi phổ biến
160
+ switch (error.code) {
161
+ case -216:
162
+ console.log("Access token không hợp lệ");
163
+ break;
164
+ case -201:
165
+ console.log("Tham số không hợp lệ");
166
+ break;
167
+ case -223:
168
+ console.log("Đã vượt quá quota");
169
+ break;
170
+ default:
171
+ console.log("Lỗi khác:", error.message);
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ ## Ví Dụ Thực Tế
178
+
179
+ ### Hệ Thống Hỗ Trợ Khách Hàng
180
+
181
+ ```typescript
182
+ class CustomerSupportBot {
183
+ constructor(private consultationService: ConsultationService) {}
184
+
185
+ async handleUserQuestion(accessToken: string, userId: string, question: string) {
186
+ try {
187
+ // Phân tích câu hỏi và tạo phản hồi
188
+ const response = this.generateResponse(question);
189
+
190
+ // Gửi tin nhắn tư vấn
191
+ await this.consultationService.sendTextMessage(
192
+ accessToken,
193
+ { user_id: userId },
194
+ {
195
+ type: "text",
196
+ text: response
197
+ }
198
+ );
199
+
200
+ // Gửi thêm hình ảnh hướng dẫn nếu cần
201
+ if (this.needsVisualGuide(question)) {
202
+ await this.consultationService.sendImageMessage(
203
+ accessToken,
204
+ { user_id: userId },
205
+ {
206
+ type: "image",
207
+ attachment: {
208
+ type: "image",
209
+ payload: {
210
+ url: "https://example.com/guide-image.jpg"
211
+ }
212
+ }
213
+ }
214
+ );
215
+ }
216
+
217
+ } catch (error) {
218
+ console.error("Failed to send consultation message:", error);
219
+ }
220
+ }
221
+
222
+ private generateResponse(question: string): string {
223
+ // Logic tạo phản hồi dựa trên câu hỏi
224
+ if (question.includes("đăng nhập")) {
225
+ return "Để đăng nhập, bạn vui lòng làm theo các bước sau...";
226
+ }
227
+
228
+ if (question.includes("thanh toán")) {
229
+ return "Về vấn đề thanh toán, chúng tôi hỗ trợ các phương thức...";
230
+ }
231
+
232
+ return "Cảm ơn bạn đã liên hệ. Chúng tôi sẽ hỗ trợ bạn ngay!";
233
+ }
234
+
235
+ private needsVisualGuide(question: string): boolean {
236
+ return question.includes("hướng dẫn") || question.includes("cách làm");
237
+ }
238
+ }
239
+ ```
240
+
241
+ ### Tích Hợp Với Webhook
242
+
243
+ ```typescript
244
+ // Xử lý tin nhắn từ webhook
245
+ app.post('/webhook', async (req, res) => {
246
+ const event = req.body;
247
+
248
+ if (event.event_name === 'user_send_text') {
249
+ const userId = event.sender.id;
250
+ const userMessage = event.message.text;
251
+
252
+ // Kiểm tra xem có phải câu hỏi cần hỗ trợ không
253
+ if (userMessage.includes('help') || userMessage.includes('hỗ trợ')) {
254
+
255
+ // Gửi tin nhắn tư vấn
256
+ await consultationService.sendTextMessage(
257
+ accessToken,
258
+ { user_id: userId },
259
+ {
260
+ type: "text",
261
+ text: "Tôi có thể hỗ trợ bạn về các vấn đề sau:\n1. Đăng nhập\n2. Thanh toán\n3. Sử dụng sản phẩm\nBạn cần hỗ trợ về vấn đề nào?"
262
+ }
263
+ );
264
+ }
265
+ }
266
+
267
+ res.status(200).send('OK');
268
+ });
269
+ ```
270
+
271
+ ## Best Practices
272
+
273
+ ### 1. Kiểm Tra Thời Gian Tương Tác
274
+ ```typescript
275
+ // Kiểm tra thời gian tương tác cuối cùng trước khi gửi
276
+ const lastInteraction = await getUserLastInteraction(userId);
277
+ const hoursSinceLastInteraction = (Date.now() - lastInteraction) / (1000 * 60 * 60);
278
+
279
+ if (hoursSinceLastInteraction > 48) {
280
+ console.log("Không thể gửi tin nhắn tư vấn - quá 48 giờ");
281
+ return;
282
+ }
283
+ ```
284
+
285
+ ### 2. Quản Lý Quota
286
+ ```typescript
287
+ // Kiểm tra quota trước khi gửi
288
+ const quota = await zalo.oa.getQuotaSummary(accessToken);
289
+ if (quota.consultation.remaining <= 0) {
290
+ console.log("Đã hết quota tin nhắn tư vấn");
291
+ return;
292
+ }
293
+ ```
294
+
295
+ ### 3. Retry Logic
296
+ ```typescript
297
+ async function sendWithRetry(
298
+ consultationService: ConsultationService,
299
+ accessToken: string,
300
+ recipient: MessageRecipient,
301
+ message: Message,
302
+ maxRetries = 3
303
+ ) {
304
+ for (let i = 0; i < maxRetries; i++) {
305
+ try {
306
+ return await consultationService.sendMessage(accessToken, recipient, message);
307
+ } catch (error) {
308
+ if (i === maxRetries - 1) throw error;
309
+
310
+ // Đợi trước khi retry
311
+ await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
317
+ ## Lưu Ý Quan Trọng
318
+
319
+ 1. **Tuân thủ chính sách**: Chỉ gửi tin nhắn tư vấn thực sự, không spam
320
+ 2. **Theo dõi quota**: Kiểm tra quota thường xuyên để tránh vượt giới hạn
321
+ 3. **Xử lý lỗi**: Luôn có cơ chế xử lý lỗi phù hợp
322
+ 4. **Logging**: Ghi log để theo dõi và debug
323
+ 5. **Rate limiting**: Tránh gửi quá nhiều tin nhắn trong thời gian ngắn
324
+
325
+ ## Tài Liệu Liên Quan
326
+
327
+ - [Message Types](./MESSAGE_TYPES.md) - Các loại tin nhắn được hỗ trợ
328
+ - [Webhook Events](./WEBHOOK_EVENTS.md) - Xử lý sự kiện webhook
329
+ - [Error Handling](./ERROR_HANDLING.md) - Xử lý lỗi chi tiết
330
+ - [Quota Management](./QUOTA_MANAGEMENT.md) - Quản lý quota tin nhắn