itd-sdk-js 1.0.5 → 1.0.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/API_REFERENCE.md CHANGED
@@ -60,15 +60,18 @@ const client = new ITDClient({
60
60
  baseUrl: 'https://xn--d1ah4a.com',
61
61
  userAgent: '...',
62
62
  projectRoot: process.cwd(), // корень проекта (по умолчанию process.cwd())
63
- // либо явные пути:
64
63
  // envPath: '/path/to/project/.env',
65
64
  // cookiesPath: '/path/to/project/.cookies',
65
+ requestTimeout: 60000, // таймаут обычных запросов, мс (по умолчанию 60 с)
66
+ uploadTimeout: 120000, // таймаут загрузки файлов и создания поста, мс (по умолчанию 120 с)
66
67
  });
67
68
  client.setAccessToken(process.env.ITD_ACCESS_TOKEN);
68
69
  ```
69
70
 
70
71
  - `projectRoot` — директория, в которой ищутся `.env` и `.cookies` (по умолчанию `process.cwd()`).
71
72
  - `envPath` / `cookiesPath` — при указании переопределяют пути, собранные из `projectRoot`.
73
+ - `requestTimeout` — таймаут обычных запросов в мс (по умолчанию 60000). Предотвращает бесконечное ожидание при «тяжёлой» сети.
74
+ - `uploadTimeout` — таймаут для загрузки файлов и создания поста в мс (по умолчанию 120000). Используется в `uploadFile`, `createPost`, `createWallPost`.
72
75
 
73
76
  ### Автоматическое обновление (Refresh Token)
74
77
 
@@ -112,6 +115,10 @@ const post = await client.createPost('Текст поста', 'image.jpg');
112
115
 
113
116
  Создает новый пост. При указании `imagePath` файл предварительно загружается через `/api/files/upload`, после чего ID файла прикрепляется к посту через поле `attachmentIds`.
114
117
 
118
+ - **Возвращает:** объект поста при успехе; **`null`** при любой ошибке (сеть, 5xx, 429, не удалось загрузить файл, неверный ответ). Всегда проверяйте результат на `null`.
119
+ - **Таймаут:** для загрузки файла и создания поста используется `uploadTimeout` (по умолчанию 120 с), чтобы запрос не зависал при 504 или медленной сети.
120
+ - **Ретраи:** при 5xx/429 или «API вернул null» рекомендуется повторять запрос в приложении (например, до 3–8 попыток с экспоненциальной задержкой). Встроенных ретраев в SDK нет.
121
+
115
122
  **Важно:** API использует поле `attachmentIds` (массив ID файлов), а не `attachments`. SDK автоматически использует правильное поле.
116
123
 
117
124
  - **Параметры**: `text` (string), `imagePath` (string, опционально).
@@ -120,6 +127,8 @@ const post = await client.createPost('Текст поста', 'image.jpg');
120
127
 
121
128
  Создает пост **на стене другого пользователя** (wall post).
122
129
 
130
+ - **Возвращает:** объект поста при успехе; **`null`** при любой ошибке. Проверяйте результат на `null`; при 5xx/429 рекомендуется ретраи в приложении.
131
+ - **Таймаут:** используется `uploadTimeout` (по умолчанию 120 с).
123
132
  - Делается через `POST /api/posts` с телом `{ content, wallRecipientId, attachmentIds? }`.
124
133
  - `wallRecipientId` — это **ID пользователя-получателя**, поэтому метод сначала запрашивает профиль через `getUserProfile(username)` и берет `profile.id`.
125
134
  - При указании `imagePath` файл загружается и прикрепляется через `attachmentIds`.
@@ -165,11 +174,24 @@ const post = await client.createPost('Текст поста', 'image.jpg');
165
174
 
166
175
  Получает дерево комментариев к посту.
167
176
 
168
- - **Параметры**: `postId`, `limit`, `sort` (`popular`, `new`, `old`).
177
+ - **Параметры**: `postId`, `limit` (по умолчанию 20, в запросе ограничивается 1–100), `sort` — в SDK можно передавать `"popular"`, `"new"`, `"old"`; в API уходит `popular`, `newest`, `oldest` соответственно.
178
+ - **Ответ API:** `{ data: { comments: [], total, hasMore, nextCursor } }`. Комментарии могут содержать вложенные `replies`, у ответов — поле `replyTo`.
169
179
 
170
180
  ### addComment(postId, text, replyToCommentId?)
171
181
 
172
- Добавляет новый комментарий или ответ на существующий.
182
+ Добавляет новый комментарий или ответ на существующий (POST к посту: `/api/posts/:postId/comments`).
183
+
184
+ ### replyToComment(commentId, content, replyToUserId)
185
+
186
+ Ответ на комментарий через отдельный эндпоинт **POST** `/api/comments/:commentId/replies`.
187
+
188
+ - **Параметры**:
189
+ - `commentId` — ID комментария, на который отвечаем.
190
+ - `content` — текст ответа.
191
+ - `replyToUserId` — ID пользователя-автора комментария (обязательно для API).
192
+ - **Возвращает:** объект созданного комментария-ответа или `null` при ошибке.
193
+
194
+ Пример: `client.replyToComment('80a775df-811a-4b60-b2fd-24651c1e546e', 'кака', '220e565c-45b9-4634-bdba-a6ebe6e8c5d1')`.
173
195
 
174
196
  ### Управление комментариями
175
197
 
@@ -216,14 +238,14 @@ const post = await client.createPost('Текст поста', 'image.jpg');
216
238
  - `search(query, userLimit?, hashtagLimit?)` — универсальный поиск пользователей и хэштегов. Возвращает `{ users: [], hashtags: [] }`.
217
239
  - `searchUsers(query, limit?)` — поиск только пользователей.
218
240
  - `searchHashtags(query, limit?)` — поиск только хэштегов.
219
- - `getTopClans()` — рейтинг кланов по количеству участников. Возвращает `{ clans: [{ avatar, memberCount }] }`.
241
+ - `getTopClans()` — рейтинг кланов по количеству участников. **Возвращает массив** `Array<{ avatar, memberCount }>` или **`null`** при ошибке (не объект с полем `clans`).
220
242
  - `getWhoToFollow()` — рекомендованные пользователи.
221
243
  - `getTrendingHashtags(limit)` — список популярных тегов.
222
244
  - `getPostsByHashtag(tagName, limit, cursor)` — поиск постов по тегу. Возвращает `{ posts: [], hashtag: {}, pagination: {} }`.
223
245
 
224
246
  ### Файлы и репорты
225
247
 
226
- - `uploadFile(filePath)` — загрузка файла через `/api/files/upload`. Возвращает `{ id, url, filename, mimeType, size }`. Используется автоматически при создании поста с изображением.
248
+ - `uploadFile(filePath)` — загрузка файла через `/api/files/upload`. Возвращает `{ id, url, filename, mimeType, size }` или **`null`** при ошибке. Таймаут — `uploadTimeout` (по умолчанию 120 с). Используется автоматически при создании поста с изображением.
227
249
  - `report(targetType, targetId, reason?, description?)` — отправка репорта. `targetType`: `"post"`, `"comment"`, `"user"`. Возвращает `{ id, createdAt }`.
228
250
  - `reportPost(postId, reason?, description?)` — репорт поста.
229
251
  - `reportComment(commentId, reason?, description?)` — репорт комментария.
package/README.md CHANGED
@@ -91,6 +91,12 @@ console.log(`${stats.likes} лайков, ${stats.views} просмотров`);
91
91
  await client.createWallPost('ITD_API', 'Тестовый пост на чужой стене 🦫');
92
92
  ```
93
93
 
94
+ ## Рекомендации при создании постов
95
+
96
+ - **createPost** и **createWallPost** при любой ошибке возвращают **`null`** — всегда проверяйте результат.
97
+ - Для загрузки файла и создания поста используется таймаут **120 с** по умолчанию (`uploadTimeout` в опциях клиента), чтобы запрос не зависал при 504 или медленной сети.
98
+ - При 5xx/429 или «API вернул null» рекомендуется повторять запрос в приложении (ретраи с задержкой). Подробнее — в [API_REFERENCE.md](API_REFERENCE.md).
99
+
94
100
  ## Важно
95
101
 
96
102
  Это неофициальный проект. Если разработчики сайта изменят структуру API или введут новую защиту, методы могут временно перестать работать. Используйте аккуратно и не спамьте запросами.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itd-sdk-js",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Unofficial SDK for итд.com - Node.js library for working with API. Automatic token refresh, session management, and convenient methods for posts, comments, users, and notifications.",
5
5
  "main": "src/client.js",
6
6
  "type": "module",
package/src/client.js CHANGED
@@ -32,9 +32,11 @@ export class ITDClient {
32
32
  * @param {string} [options.projectRoot] - Корень проекта (по умолчанию process.cwd()); .env и .cookies ищутся здесь
33
33
  * @param {string} [options.envPath] - Полный путь к .env (переопределяет projectRoot для .env)
34
34
  * @param {string} [options.cookiesPath] - Полный путь к .cookies (переопределяет projectRoot для .cookies)
35
+ * @param {number} [options.requestTimeout] - Таймаут обычных запросов в мс (по умолчанию 60000)
36
+ * @param {number} [options.uploadTimeout] - Таймаут загрузки файлов и создания поста в мс (по умолчанию 120000)
35
37
  */
36
38
  constructor(baseUrlOrOptions = null, userAgent = null) {
37
- let baseUrl, projectRoot, envPath, cookiesPath;
39
+ let baseUrl, projectRoot, envPath, cookiesPath, requestTimeout, uploadTimeout;
38
40
 
39
41
  if (baseUrlOrOptions && typeof baseUrlOrOptions === 'object' && !(baseUrlOrOptions instanceof URL)) {
40
42
  const opts = baseUrlOrOptions;
@@ -43,11 +45,15 @@ export class ITDClient {
43
45
  projectRoot = opts.projectRoot ?? process.cwd();
44
46
  envPath = opts.envPath ?? path.join(projectRoot, '.env');
45
47
  cookiesPath = opts.cookiesPath ?? path.join(projectRoot, '.cookies');
48
+ requestTimeout = opts.requestTimeout ?? 60000;
49
+ uploadTimeout = opts.uploadTimeout ?? 120000;
46
50
  } else {
47
51
  projectRoot = process.cwd();
48
52
  baseUrl = baseUrlOrOptions || process.env.ITD_BASE_URL || 'https://xn--d1ah4a.com';
49
53
  envPath = path.join(projectRoot, '.env');
50
54
  cookiesPath = path.join(projectRoot, '.cookies');
55
+ requestTimeout = 60000;
56
+ uploadTimeout = 120000;
51
57
  }
52
58
 
53
59
  // Используем реальный домен (IDN: итд.com = xn--d1ah4a.com)
@@ -59,6 +65,11 @@ export class ITDClient {
59
65
  this.envPath = envPath;
60
66
  this.cookiesPath = cookiesPath;
61
67
 
68
+ /** Таймаут обычных запросов (мс). Для загрузки и создания поста используется uploadTimeout. */
69
+ this.requestTimeout = requestTimeout;
70
+ /** Таймаут загрузки файлов и создания поста (мс), чтобы не зависать при 504/медленной сети. */
71
+ this.uploadTimeout = uploadTimeout;
72
+
62
73
  /** @type {string|null} */
63
74
  this.accessToken = null;
64
75
 
@@ -78,6 +89,7 @@ export class ITDClient {
78
89
  // Создание axios instance + cookie jar
79
90
  const axiosConfig = {
80
91
  baseURL: this.baseUrl,
92
+ timeout: requestTimeout,
81
93
  withCredentials: true,
82
94
  jar: this.cookieJar,
83
95
  headers: {
@@ -469,6 +481,18 @@ export class ITDClient {
469
481
  async addComment(postId, text, replyToCommentId = null) {
470
482
  return await this.comments.addComment(postId, text, replyToCommentId);
471
483
  }
484
+
485
+ /**
486
+ * Ответ на комментарий (POST /api/comments/:id/replies).
487
+ *
488
+ * @param {string} commentId - ID комментария, на который отвечаем
489
+ * @param {string} content - Текст ответа
490
+ * @param {string} replyToUserId - ID пользователя-автора комментария (обязательно для API)
491
+ * @returns {Promise<Object|null>} Данные созданного комментария-ответа или null при ошибке
492
+ */
493
+ async replyToComment(commentId, content, replyToUserId) {
494
+ return await this.comments.replyToComment(commentId, content, replyToUserId);
495
+ }
472
496
 
473
497
  /**
474
498
  * Ставит лайк на комментарий
package/src/comments.js CHANGED
@@ -54,58 +54,107 @@ export class CommentsManager {
54
54
  return null;
55
55
  }
56
56
  }
57
+
58
+ /**
59
+ * Ответ на комментарий (отдельный эндпоинт /api/comments/:id/replies).
60
+ *
61
+ * @param {string} commentId - ID комментария, на который отвечаем
62
+ * @param {string} content - Текст ответа
63
+ * @param {string} replyToUserId - ID пользователя-автора комментария (обязательно для API)
64
+ * @returns {Promise<Object|null>} Данные созданного комментария-ответа или null при ошибке
65
+ */
66
+ async replyToComment(commentId, content, replyToUserId) {
67
+ if (!await this.client.auth.checkAuth()) {
68
+ console.error('Ошибка: необходимо войти в аккаунт');
69
+ return null;
70
+ }
71
+ if (!replyToUserId) {
72
+ console.error('Ошибка: replyToUserId обязателен для ответа на комментарий');
73
+ return null;
74
+ }
75
+ try {
76
+ const url = `${this.client.baseUrl}/api/comments/${commentId}/replies`;
77
+ const response = await this.axios.post(url, {
78
+ content,
79
+ replyToUserId,
80
+ });
81
+ if (response.status === 200 || response.status === 201) {
82
+ return response.data;
83
+ }
84
+ console.error(`Ошибка ответа на комментарий: ${response.status} - ${JSON.stringify(response.data)}`);
85
+ return null;
86
+ } catch (error) {
87
+ console.error('Исключение при ответе на комментарий:', error.message);
88
+ if (error.response) {
89
+ console.error('Response status:', error.response.status);
90
+ console.error('Response data:', error.response.data);
91
+ }
92
+ return null;
93
+ }
94
+ }
57
95
 
58
96
  /**
59
- * Получает комментарии к посту
60
- *
97
+ * Получает комментарии к посту.
98
+ * API ожидает sort: "newest" | "oldest" | "popular". SDK принимает "new"/"old"/"popular" и маппит в newest/oldest/popular.
99
+ *
61
100
  * @param {string} postId - ID поста
62
- * @param {number} limit - Количество комментариев
63
- * @param {string} sort - Сортировка: "popular", "new", "old" (по умолчанию "popular")
101
+ * @param {number} limit - Количество комментариев (по умолчанию 20)
102
+ * @param {string} sort - Сортировка: "popular", "new", "old" (в API уходит как popular, newest, oldest)
64
103
  * @returns {Promise<Object>} { comments: [], total, hasMore, nextCursor } или { comments: [] } при ошибке
65
- *
66
- * Примечание: Авторизация не требуется для просмотра комментариев
67
104
  */
68
105
  async getComments(postId, limit = 20, sort = 'popular') {
106
+ const commentsUrl = `${this.client.baseUrl}/api/posts/${postId}/comments`;
107
+ const reqLimit = Math.min(Math.max(1, Number(limit) || 20), 100);
108
+ const sortMap = { new: 'newest', old: 'oldest', popular: 'popular', newest: 'newest', oldest: 'oldest' };
109
+ const reqSort = sortMap[sort] || 'popular';
110
+
111
+ const parseResponse = (response) => {
112
+ const data = response.data?.data ?? response.data;
113
+ if (data?.comments) {
114
+ return {
115
+ comments: data.comments,
116
+ total: data.total ?? data.comments.length,
117
+ hasMore: data.hasMore ?? false,
118
+ nextCursor: data.nextCursor ?? null
119
+ };
120
+ }
121
+ if (Array.isArray(data)) {
122
+ return {
123
+ comments: data,
124
+ total: data.length,
125
+ hasMore: false,
126
+ nextCursor: null
127
+ };
128
+ }
129
+ return { comments: [], total: 0, hasMore: false, nextCursor: null };
130
+ };
131
+
69
132
  try {
70
- const commentsUrl = `${this.client.baseUrl}/api/posts/${postId}/comments`;
71
- const params = {
72
- limit: limit,
73
- sort: sort,
74
- };
75
-
76
- const response = await this.axios.get(commentsUrl, { params });
77
-
133
+ const response = await this.axios.get(commentsUrl, {
134
+ params: { limit: reqLimit, sort: reqSort },
135
+ });
136
+
78
137
  if (response.status === 200) {
79
- const data = response.data;
80
- // Структура: { data: { comments: [...], total, hasMore, nextCursor } }
81
- if (data.data && data.data.comments) {
82
- return {
83
- comments: data.data.comments,
84
- total: data.data.total || 0,
85
- hasMore: data.data.hasMore || false,
86
- nextCursor: data.data.nextCursor || null
87
- };
88
- } else if (data.comments) {
89
- return {
90
- comments: data.comments,
91
- total: data.total || 0,
92
- hasMore: data.hasMore || false,
93
- nextCursor: data.nextCursor || null
94
- };
95
- } else if (Array.isArray(data)) {
96
- return {
97
- comments: data,
98
- total: data.length,
99
- hasMore: false,
100
- nextCursor: null
101
- };
138
+ return parseResponse(response);
139
+ }
140
+ if (response.status === 422) {
141
+ const fallback = await this.axios.get(commentsUrl, { params: { limit: reqLimit, sort: 'popular' } });
142
+ if (fallback.status === 200) {
143
+ return parseResponse(fallback);
102
144
  }
103
- return { comments: [], total: 0, hasMore: false, nextCursor: null };
104
- } else {
105
- console.error(`Ошибка получения комментариев: ${response.status}`);
106
- return { comments: [], total: 0, hasMore: false, nextCursor: null };
145
+ console.warn('⚠️ GET /api/posts/:postId/comments: 422. API ожидает sort: newest | oldest | popular.');
107
146
  }
147
+ console.error(`Ошибка получения комментариев: ${response.status}`);
148
+ return { comments: [], total: 0, hasMore: false, nextCursor: null };
108
149
  } catch (error) {
150
+ if (error.response?.status === 422) {
151
+ try {
152
+ const retry = await this.axios.get(commentsUrl, { params: { limit: 20, sort: 'popular' } });
153
+ if (retry.status === 200) {
154
+ return parseResponse(retry);
155
+ }
156
+ } catch (_) {}
157
+ }
109
158
  console.error('Исключение при получении комментариев:', error.message);
110
159
  if (error.response) {
111
160
  console.error('Response status:', error.response.status);
package/src/files.js CHANGED
@@ -11,8 +11,9 @@ export class FilesManager {
11
11
  }
12
12
 
13
13
  /**
14
- * Загружает файл (изображение) на сервер
15
- *
14
+ * Загружает файл (изображение) на сервер.
15
+ * Таймаут — client.uploadTimeout (по умолчанию 120 с). При ошибке возвращает null.
16
+ *
16
17
  * @param {string} filePath - Путь к файлу
17
18
  * @returns {Promise<Object|null>} { id, url, filename, mimeType, size } или null при ошибке
18
19
  */
@@ -36,6 +37,7 @@ export class FilesManager {
36
37
  formData.append('file', fs.createReadStream(filePath));
37
38
 
38
39
  const response = await this.axios.post(uploadUrl, formData, {
40
+ timeout: this.client.uploadTimeout ?? 120000,
39
41
  headers: {
40
42
  ...formData.getHeaders(),
41
43
  }
package/src/posts.js CHANGED
@@ -16,8 +16,10 @@ export class PostsManager {
16
16
  }
17
17
 
18
18
  /**
19
- * Создает новый пост
20
- *
19
+ * Создает новый пост.
20
+ * При любой ошибке (сеть, 5xx, 429, не удалось загрузить файл) возвращает null.
21
+ * Таймаут загрузки и создания — client.uploadTimeout (по умолчанию 120 с).
22
+ *
21
23
  * @param {string} text - Текст поста
22
24
  * @param {string|null} imagePath - Путь к изображению (опционально)
23
25
  * @returns {Promise<Object|null>} Данные созданного поста или null при ошибке
@@ -51,9 +53,11 @@ export class PostsManager {
51
53
  postData.attachmentIds = [uploadedFile.id];
52
54
  }
53
55
 
54
- // Создаем пост (с изображением или без)
55
- const response = await this.axios.post(postUrl, postData);
56
-
56
+ // Создаем пост (с изображением или без); увеличенный таймаут для тяжёлых запросов
57
+ const response = await this.axios.post(postUrl, postData, {
58
+ timeout: this.client.uploadTimeout ?? 120000,
59
+ });
60
+
57
61
  if (response.status === 200 || response.status === 201) {
58
62
  return response.data;
59
63
  } else {
@@ -71,8 +75,9 @@ export class PostsManager {
71
75
  }
72
76
 
73
77
  /**
74
- * Создает пост на стене другого пользователя (wall post)
75
- *
78
+ * Создает пост на стене другого пользователя (wall post).
79
+ * При любой ошибке возвращает null. Таймаут — client.uploadTimeout (по умолчанию 120 с).
80
+ *
76
81
  * @param {string} username - Имя пользователя, на чью стену нужно написать
77
82
  * @param {string} text - Текст поста
78
83
  * @param {string|null} imagePath - Путь к изображению (опционально)
@@ -117,9 +122,11 @@ export class PostsManager {
117
122
  postData.attachmentIds = [uploadedFile.id];
118
123
  }
119
124
 
120
- // Создаем пост на стене
121
- const response = await this.axios.post(postUrl, postData);
122
-
125
+ // Создаем пост на стене; увеличенный таймаут для тяжёлых запросов
126
+ const response = await this.axios.post(postUrl, postData, {
127
+ timeout: this.client.uploadTimeout ?? 120000,
128
+ });
129
+
123
130
  if (response.status === 200 || response.status === 201) {
124
131
  return response.data;
125
132
  } else {
package/src/users.js CHANGED
@@ -295,9 +295,10 @@ export class UsersManager {
295
295
  }
296
296
 
297
297
  /**
298
- * Получает топ кланов по количеству участников
299
- *
300
- * @returns {Promise<Array|null>} Массив кланов [{ avatar: "🦎", memberCount: 3794 }, ...] или null при ошибке
298
+ * Получает топ кланов по количеству участников.
299
+ * Возвращает массив (Array), не объект с полем clans.
300
+ *
301
+ * @returns {Promise<Array|null>} Массив кланов [{ avatar, memberCount }, ...] или null при ошибке
301
302
  */
302
303
  async getTopClans() {
303
304
  try {