itd-sdk-js 1.0.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.
package/src/client.js ADDED
@@ -0,0 +1,920 @@
1
+ /**
2
+ * Главный клиент для работы с неофициальным API итд.com
3
+ */
4
+ import axios from 'axios';
5
+ import dotenv from 'dotenv';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { CookieJar } from 'tough-cookie';
10
+ import { wrapper } from 'axios-cookiejar-support';
11
+ import { AuthManager } from './auth.js';
12
+ import { PostsManager } from './posts.js';
13
+ import { CommentsManager } from './comments.js';
14
+ import { UsersManager } from './users.js';
15
+ import { NotificationsManager } from './notifications.js';
16
+ import { HashtagsManager } from './hashtags.js';
17
+ import { FilesManager } from './files.js';
18
+ import { ReportsManager } from './reports.js';
19
+ import { SearchManager } from './search.js';
20
+
21
+ dotenv.config();
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = path.dirname(__filename);
25
+
26
+ export class ITDClient {
27
+ /**
28
+ * Инициализация клиента
29
+ *
30
+ * @param {string} baseUrl - Базовый URL сайта (по умолчанию из .env)
31
+ * @param {string} userAgent - User-Agent для запросов (по умолчанию из .env)
32
+ */
33
+ constructor(baseUrl = null, userAgent = null) {
34
+ // Используем реальный домен (IDN: итд.com = xn--d1ah4a.com)
35
+ this.baseUrl = baseUrl || process.env.ITD_BASE_URL || 'https://xn--d1ah4a.com';
36
+ this.userAgent = userAgent || process.env.ITD_USER_AGENT ||
37
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
38
+
39
+ /** @type {string|null} */
40
+ this.accessToken = null;
41
+
42
+ // Прокси (важно, если браузер ходит через 127.0.0.1:10808)
43
+ // Можно задать: ITD_PROXY=http://127.0.0.1:10808
44
+ // Или стандартные: HTTPS_PROXY / HTTP_PROXY
45
+ this.proxyUrl = process.env.ITD_PROXY || process.env.HTTPS_PROXY || process.env.HTTP_PROXY || null;
46
+
47
+ // В Node.js axios НЕ хранит cookies сам по себе.
48
+ // Поэтому используем CookieJar, чтобы сессия сохранялась как в браузере.
49
+ this.cookieJar = new CookieJar();
50
+
51
+ // Cookies загружаются из отдельного файла .cookies (чтобы избежать проблем с ; в .env)
52
+ // ВАЖНО: это чувствительные данные — не коммитьте .cookies
53
+ this._loadCookiesFromFile();
54
+
55
+ // Создание axios instance + cookie jar
56
+ const axiosConfig = {
57
+ baseURL: this.baseUrl,
58
+ withCredentials: true,
59
+ jar: this.cookieJar,
60
+ headers: {
61
+ 'User-Agent': this.userAgent,
62
+ 'Accept': 'application/json, text/plain, */*',
63
+ 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
64
+ 'Content-Type': 'application/json',
65
+ // Возможно понадобятся дополнительные заголовки:
66
+ // 'Referer': this.baseUrl,
67
+ // 'Origin': this.baseUrl,
68
+ }
69
+ };
70
+
71
+ if (this.proxyUrl) {
72
+ // axios-cookiejar-support не работает с кастомными http(s).Agent,
73
+ // поэтому используем встроенную поддержку proxy у axios.
74
+ //
75
+ // Формат: ITD_PROXY=http://127.0.0.1:10808
76
+ // ВАЖНО: это должен быть HTTP CONNECT proxy, не SOCKS.
77
+ const parsed = new URL(this.proxyUrl);
78
+ axiosConfig.proxy = {
79
+ protocol: parsed.protocol.replace(':', ''),
80
+ host: parsed.hostname,
81
+ port: parsed.port ? Number(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80),
82
+ };
83
+ }
84
+
85
+ this.axios = wrapper(axios.create(axiosConfig));
86
+
87
+ // Анти-дребезг для refresh (чтобы 10 параллельных 401 не делали 10 refresh)
88
+ /** @type {Promise<string|null> | null} */
89
+ this._refreshPromise = null;
90
+
91
+ // Автоматически подставляем Authorization, если есть accessToken
92
+ this.axios.interceptors.request.use((config) => {
93
+ if (this.accessToken && !config.headers?.Authorization) {
94
+ config.headers = config.headers || {};
95
+ config.headers.Authorization = `Bearer ${this.accessToken}`;
96
+ }
97
+ return config;
98
+ });
99
+
100
+ // Авто-рефреш токена на 401 + повтор запроса
101
+ this.axios.interceptors.response.use(
102
+ (response) => response,
103
+ async (error) => {
104
+ const status = error?.response?.status;
105
+ const originalRequest = error?.config;
106
+
107
+ // Если нет конфига запроса — просто пробрасываем ошибку
108
+ if (!originalRequest) {
109
+ throw error;
110
+ }
111
+
112
+ // Не пытаемся рефрешить при ошибках не-401
113
+ // 429 (Rate Limit) тоже не рефрешим - это другая проблема
114
+ if (status !== 401) {
115
+ throw error;
116
+ }
117
+
118
+ // Не зацикливаемся
119
+ if (originalRequest.__itdRetried) {
120
+ throw error;
121
+ }
122
+
123
+ // Не пытаемся рефрешить, если это сам refresh
124
+ const url = String(originalRequest.url || '');
125
+ if (url.includes('/api/v1/auth/refresh')) {
126
+ throw error;
127
+ }
128
+
129
+ originalRequest.__itdRetried = true;
130
+
131
+ // Пытаемся обновить токен (требует refresh_token cookie в cookie jar)
132
+ if (!this._refreshPromise) {
133
+ this._refreshPromise = this.refreshAccessToken().finally(() => {
134
+ this._refreshPromise = null;
135
+ });
136
+ }
137
+
138
+ const newToken = await this._refreshPromise;
139
+
140
+ if (!newToken) {
141
+ // Не смогли обновить — пробрасываем исходную 401
142
+ throw error;
143
+ }
144
+
145
+ // Повторяем исходный запрос с новым токеном
146
+ originalRequest.headers = originalRequest.headers || {};
147
+ originalRequest.headers.Authorization = `Bearer ${newToken}`;
148
+ // Убираем флаг retry для следующей попытки
149
+ delete originalRequest.__itdRetried;
150
+ const retryResponse = await this.axios.request(originalRequest);
151
+ return retryResponse;
152
+ }
153
+ );
154
+
155
+ // Инициализация менеджеров
156
+ this.auth = new AuthManager(this);
157
+ this.posts = new PostsManager(this);
158
+ this.comments = new CommentsManager(this);
159
+ this.users = new UsersManager(this);
160
+ this.notifications = new NotificationsManager(this);
161
+ this.hashtags = new HashtagsManager(this);
162
+ this.files = new FilesManager(this);
163
+ this.reports = new ReportsManager(this);
164
+ this.search = new SearchManager(this);
165
+ }
166
+
167
+ /**
168
+ * Установить accessToken (JWT) для Authorization header
169
+ * @param {string|null} token
170
+ */
171
+ setAccessToken(token) {
172
+ this.accessToken = token || null;
173
+ }
174
+
175
+ /**
176
+ * Загружает cookies из файла .cookies
177
+ * @private
178
+ */
179
+ _loadCookiesFromFile() {
180
+ try {
181
+ const cookiesPath = path.join(__dirname, '..', '.cookies');
182
+ if (!fs.existsSync(cookiesPath)) {
183
+ // Файл не существует - это нормально, просто пропускаем
184
+ return;
185
+ }
186
+
187
+ const cookieHeader = fs.readFileSync(cookiesPath, 'utf8').trim();
188
+ if (!cookieHeader) {
189
+ return;
190
+ }
191
+
192
+ // Парсим cookies
193
+ const parts = cookieHeader.split(';').map((p) => p.trim()).filter(Boolean);
194
+ const domain = new URL(this.baseUrl).hostname;
195
+
196
+ for (const part of parts) {
197
+ // part вида "name=value"
198
+ const [name, ...valueParts] = part.split('=');
199
+ if (name && valueParts.length > 0) {
200
+ const value = valueParts.join('='); // На случай если в value есть =
201
+ // Создаем cookie с правильным форматом для tough-cookie
202
+ const cookieString = `${name}=${value}; Domain=${domain}; Path=/`;
203
+ this.cookieJar.setCookieSync(cookieString, this.baseUrl);
204
+ }
205
+ }
206
+ } catch (e) {
207
+ // Не валим процесс — просто предупреждаем в консоль
208
+ console.warn('⚠️ Не удалось загрузить cookies из .cookies:', e?.message || e);
209
+ }
210
+ }
211
+
212
+
213
+ /**
214
+ * Обновить accessToken через refresh endpoint.
215
+ * Обычно работает, если в cookie jar уже есть refresh-cookie от сайта.
216
+ * @returns {Promise<string|null>} accessToken или null
217
+ */
218
+ async refreshAccessToken() {
219
+ return await this.auth.refreshAccessToken();
220
+ }
221
+
222
+ /**
223
+ * Выход из аккаунта
224
+ *
225
+ * @returns {Promise<boolean>} True если успешно
226
+ */
227
+ async logout() {
228
+ return await this.auth.logout();
229
+ }
230
+
231
+ /**
232
+ * Создает пост (удобный метод)
233
+ *
234
+ * @param {string} text - Текст поста
235
+ * @param {string|null} imagePath - Путь к изображению (опционально)
236
+ * @returns {Promise<Object|null>} Данные поста или null
237
+ */
238
+ async createPost(text, imagePath = null) {
239
+ return await this.posts.createPost(text, imagePath);
240
+ }
241
+
242
+ /**
243
+ * Редактирует пост (удобный метод)
244
+ *
245
+ * @param {string} postId - ID поста
246
+ * @param {string} newContent - Новый текст поста
247
+ * @returns {Promise<Object|null>} Обновленные данные поста или null
248
+ */
249
+ async editPost(postId, newContent) {
250
+ return await this.posts.editPost(postId, newContent);
251
+ }
252
+
253
+ /**
254
+ * Получает список постов пользователя или ленту
255
+ *
256
+ * @param {string|null} username - Имя пользователя (null = лента/свои посты)
257
+ * @param {number} limit - Количество постов
258
+ * @param {string} sort - Сортировка: "new", "old", "popular"
259
+ * @param {string|null} cursor - Курсор для пагинации
260
+ * @param {string|null} tab - Тип ленты: "popular" (популярные), "following" (из подписок), null (обычная лента)
261
+ * @returns {Promise<Object>} { posts: [], pagination: {} }
262
+ */
263
+ async getPosts(username = null, limit = 20, sort = 'new', cursor = null, tab = null) {
264
+ return await this.posts.getPosts(username, limit, sort, cursor, tab);
265
+ }
266
+
267
+ /**
268
+ * Получает популярные посты (лента популярного)
269
+ *
270
+ * @param {number} limit - Количество постов
271
+ * @param {string|null} cursor - Курсор для пагинации
272
+ * @returns {Promise<Object>} { posts: [], pagination: {} }
273
+ */
274
+ async getFeedPopular(limit = 20, cursor = null) {
275
+ return await this.posts.getFeedPopular(limit, cursor);
276
+ }
277
+
278
+ /**
279
+ * Получает посты из подписок (лента подписок)
280
+ *
281
+ * @param {number} limit - Количество постов
282
+ * @param {string|null} cursor - Курсор для пагинации
283
+ * @returns {Promise<Object>} { posts: [], pagination: {} }
284
+ */
285
+ async getFeedFollowing(limit = 20, cursor = null) {
286
+ return await this.posts.getFeedFollowing(limit, cursor);
287
+ }
288
+
289
+ /**
290
+ * Получает список постов (простой вариант - только массив)
291
+ *
292
+ * @param {string|null} username - Имя пользователя
293
+ * @param {number} limit - Количество постов
294
+ * @returns {Promise<Array>} Список постов
295
+ */
296
+ async getPostsList(username = null, limit = 20) {
297
+ const result = await this.posts.getPosts(username, limit, 'new', null);
298
+ return result.posts;
299
+ }
300
+
301
+ /**
302
+ * Получает конкретный пост по ID
303
+ *
304
+ * @param {string} postId - ID поста
305
+ * @returns {Promise<Object|null>} Данные поста или null
306
+ */
307
+ async getPost(postId) {
308
+ return await this.posts.getPost(postId);
309
+ }
310
+
311
+ /**
312
+ * Удаляет пост (удобный метод)
313
+ *
314
+ * @param {string} postId - ID поста
315
+ * @returns {Promise<boolean>} True если успешно
316
+ */
317
+ async deletePost(postId) {
318
+ return await this.posts.deletePost(postId);
319
+ }
320
+
321
+ /**
322
+ * Закрепляет пост (удобный метод)
323
+ *
324
+ * @param {string} postId - ID поста
325
+ * @returns {Promise<boolean>} True если успешно
326
+ */
327
+ async pinPost(postId) {
328
+ return await this.posts.pinPost(postId);
329
+ }
330
+
331
+ /**
332
+ * Делает репост (удобный метод)
333
+ *
334
+ * @param {string} postId - ID поста для репоста
335
+ * @param {string|null} comment - Комментарий к репосту (опционально)
336
+ * @returns {Promise<Object|null>} Данные созданного репоста или null
337
+ */
338
+ async repost(postId, comment = null) {
339
+ return await this.posts.repost(postId, comment);
340
+ }
341
+
342
+ /**
343
+ * Ставит лайк на пост
344
+ *
345
+ * @param {string} postId - ID поста
346
+ * @returns {Promise<Object|null>} { liked: true, likesCount: number } или null при ошибке
347
+ */
348
+ async likePost(postId) {
349
+ if (!await this.auth.checkAuth()) {
350
+ console.error('Ошибка: необходимо войти в аккаунт');
351
+ return null;
352
+ }
353
+
354
+ try {
355
+ const likeUrl = `${this.baseUrl}/api/posts/${postId}/like`;
356
+ const response = await this.axios.post(likeUrl);
357
+
358
+ if (response.status === 200 || response.status === 201) {
359
+ return response.data; // { liked: true, likesCount: number }
360
+ } else {
361
+ console.error(`Ошибка лайка: ${response.status} - ${JSON.stringify(response.data)}`);
362
+ return null;
363
+ }
364
+ } catch (error) {
365
+ console.error('Исключение при лайке:', error.message);
366
+ if (error.response) {
367
+ console.error('Response:', error.response.status, error.response.data);
368
+ }
369
+ return null;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Убирает лайк с поста
375
+ *
376
+ * @param {string} postId - ID поста
377
+ * @returns {Promise<Object|null>} { liked: false, likesCount: number } или null при ошибке
378
+ */
379
+ async unlikePost(postId) {
380
+ if (!await this.auth.checkAuth()) {
381
+ console.error('Ошибка: необходимо войти в аккаунт');
382
+ return null;
383
+ }
384
+
385
+ try {
386
+ const unlikeUrl = `${this.baseUrl}/api/posts/${postId}/like`;
387
+ const response = await this.axios.delete(unlikeUrl);
388
+
389
+ if (response.status === 200 || response.status === 204) {
390
+ return response.data || { liked: false, likesCount: 0 };
391
+ } else {
392
+ console.error(`Ошибка убирания лайка: ${response.status}`);
393
+ if (response.data) {
394
+ console.error('Response data:', response.data);
395
+ }
396
+ return null;
397
+ }
398
+ } catch (error) {
399
+ console.error('Исключение при убирании лайка:', error.message);
400
+ if (error.response) {
401
+ console.error('Response status:', error.response.status);
402
+ console.error('Response data:', error.response.data);
403
+ }
404
+ return null;
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Добавляет комментарий к посту
410
+ *
411
+ * @param {string} postId - ID поста
412
+ * @param {string} text - Текст комментария
413
+ * @param {string|null} replyToCommentId - ID комментария для ответа (опционально)
414
+ * @returns {Promise<Object|null>} Данные комментария
415
+ */
416
+ async addComment(postId, text, replyToCommentId = null) {
417
+ return await this.comments.addComment(postId, text, replyToCommentId);
418
+ }
419
+
420
+ /**
421
+ * Ставит лайк на комментарий
422
+ *
423
+ * @param {string} commentId - ID комментария
424
+ * @returns {Promise<Object|null>} { liked: true, likesCount: number } или null при ошибке
425
+ */
426
+ async likeComment(commentId) {
427
+ return await this.comments.likeComment(commentId);
428
+ }
429
+
430
+ /**
431
+ * Убирает лайк с комментария
432
+ *
433
+ * @param {string} commentId - ID комментария
434
+ * @returns {Promise<Object|null>} { liked: false, likesCount: number } или null при ошибке
435
+ */
436
+ async unlikeComment(commentId) {
437
+ return await this.comments.unlikeComment(commentId);
438
+ }
439
+
440
+ /**
441
+ * Удаляет комментарий
442
+ *
443
+ * @param {string} commentId - ID комментария
444
+ * @returns {Promise<boolean>} True если успешно
445
+ */
446
+ async deleteComment(commentId) {
447
+ return await this.comments.deleteComment(commentId);
448
+ }
449
+
450
+ /**
451
+ * Получает комментарии к посту
452
+ *
453
+ * @param {string} postId - ID поста
454
+ * @param {number} limit - Количество комментариев
455
+ * @param {string} sort - Сортировка: "popular", "new", "old"
456
+ * @returns {Promise<Object>} { comments: [], total, hasMore, nextCursor }
457
+ */
458
+ async getComments(postId, limit = 20, sort = 'popular') {
459
+ return await this.comments.getComments(postId, limit, sort);
460
+ }
461
+
462
+ /**
463
+ * Обновляет описание профиля текущего пользователя
464
+ *
465
+ * @param {string} bio - Новое описание профиля
466
+ * @param {string|null} displayName - Новое отображаемое имя (опционально)
467
+ * @returns {Promise<Object|null>} Обновленные данные профиля или null при ошибке
468
+ */
469
+ async updateProfile(bio, displayName = null) {
470
+ return await this.users.updateProfile(bio, displayName);
471
+ }
472
+
473
+ /**
474
+ * Получает данные текущего пользователя
475
+ *
476
+ * @returns {Promise<Object|null>} Данные профиля или null при ошибке
477
+ */
478
+ async getMyProfile() {
479
+ return await this.users.getMyProfile();
480
+ }
481
+
482
+ /**
483
+ * Получает профиль пользователя по username
484
+ *
485
+ * @param {string} username - Имя пользователя
486
+ * @returns {Promise<Object|null>} Данные профиля или null при ошибке
487
+ */
488
+ async getUserProfile(username) {
489
+ return await this.users.getUserProfile(username);
490
+ }
491
+
492
+ /**
493
+ * Подписывается на пользователя
494
+ *
495
+ * @param {string} username - Имя пользователя
496
+ * @returns {Promise<Object|null>} { following: true, followersCount: number } или null при ошибке
497
+ */
498
+ async followUser(username) {
499
+ return await this.users.followUser(username);
500
+ }
501
+
502
+ /**
503
+ * Отписывается от пользователя
504
+ *
505
+ * @param {string} username - Имя пользователя
506
+ * @returns {Promise<Object|null>} { following: false, followersCount: number } или null при ошибке
507
+ */
508
+ async unfollowUser(username) {
509
+ return await this.users.unfollowUser(username);
510
+ }
511
+
512
+ /**
513
+ * Получает список подписчиков пользователя
514
+ *
515
+ * @param {string} username - Имя пользователя
516
+ * @param {number} page - Номер страницы (начиная с 1)
517
+ * @param {number} limit - Количество на странице
518
+ * @returns {Promise<Object|null>} { users: [], pagination: {} } или null
519
+ */
520
+ async getFollowers(username, page = 1, limit = 30) {
521
+ return await this.users.getFollowers(username, page, limit);
522
+ }
523
+
524
+ /**
525
+ * Получает список подписок пользователя
526
+ *
527
+ * @param {string} username - Имя пользователя
528
+ * @param {number} page - Номер страницы (начиная с 1)
529
+ * @param {number} limit - Количество на странице
530
+ * @returns {Promise<Object|null>} { users: [], pagination: {} } или null
531
+ */
532
+ async getFollowing(username, page = 1, limit = 30) {
533
+ return await this.users.getFollowing(username, page, limit);
534
+ }
535
+
536
+ /**
537
+ * Получает клан пользователя (эмодзи из avatar)
538
+ *
539
+ * @param {string} username - Имя пользователя
540
+ * @returns {Promise<string|null>} Эмодзи клана или null
541
+ */
542
+ async getUserClan(username) {
543
+ return await this.users.getUserClan(username);
544
+ }
545
+
546
+ /**
547
+ * Получает список уведомлений
548
+ *
549
+ * @param {number} limit - Количество уведомлений
550
+ * @param {string|null} cursor - Курсор для пагинации
551
+ * @param {string|null} type - Фильтр по типу: 'reply', 'like', 'wall_post', 'follow', 'comment' (опционально)
552
+ * @returns {Promise<Object|null>} { notifications: [], pagination: {} } или null
553
+ */
554
+ async getNotifications(limit = 20, cursor = null, type = null) {
555
+ return await this.notifications.getNotifications(limit, cursor, type);
556
+ }
557
+
558
+ /**
559
+ * Получает уведомления определенного типа
560
+ *
561
+ * @param {string} type - Тип уведомления: 'reply', 'like', 'wall_post', 'follow', 'comment'
562
+ * @param {number} limit - Количество уведомлений (по умолчанию 20)
563
+ * @param {string|null} cursor - Курсор для пагинации
564
+ * @returns {Promise<Object|null>} { notifications: [], pagination: {} } или null
565
+ */
566
+ async getNotificationsByType(type, limit = 20, cursor = null) {
567
+ return await this.notifications.getNotifications(limit, cursor, type);
568
+ }
569
+
570
+ /**
571
+ * Отмечает уведомление как прочитанное
572
+ *
573
+ * @param {string} notificationId - ID уведомления
574
+ * @returns {Promise<Object|null>} { success: true } или null при ошибке
575
+ */
576
+ async markNotificationAsRead(notificationId) {
577
+ return await this.notifications.markAsRead(notificationId);
578
+ }
579
+
580
+ /**
581
+ * Отмечает все уведомления как прочитанные
582
+ *
583
+ * @returns {Promise<boolean>} True если успешно
584
+ */
585
+ async markAllNotificationsAsRead() {
586
+ return await this.notifications.markAllAsRead();
587
+ }
588
+
589
+ /**
590
+ * Получает количество непрочитанных уведомлений
591
+ *
592
+ * @returns {Promise<number|null>} Количество уведомлений или null при ошибке
593
+ */
594
+ async getNotificationCount() {
595
+ return await this.notifications.getUnreadCount();
596
+ }
597
+
598
+ /**
599
+ * Получает трендовые хэштеги
600
+ *
601
+ * @param {number} limit - Количество хэштегов (по умолчанию 10)
602
+ * @returns {Promise<Object|null>} { hashtags: [] } или null при ошибке
603
+ */
604
+ async getTrendingHashtags(limit = 10) {
605
+ return await this.hashtags.getTrending(limit);
606
+ }
607
+
608
+ /**
609
+ * Получает посты по хэштегу
610
+ *
611
+ * @param {string} hashtagName - Имя хэштега (без #)
612
+ * @param {number} limit - Количество постов (по умолчанию 20)
613
+ * @param {string|null} cursor - Курсор для пагинации
614
+ * @returns {Promise<Object|null>} { posts: [], hashtag: {}, pagination: {} } или null при ошибке
615
+ */
616
+ async getPostsByHashtag(hashtagName, limit = 20, cursor = null) {
617
+ return await this.hashtags.getPostsByHashtag(hashtagName, limit, cursor);
618
+ }
619
+
620
+ /**
621
+ * Получает топ кланов по количеству участников
622
+ *
623
+ * @returns {Promise<Array|null>} Массив кланов [{ avatar: "🦎", memberCount: 3794 }, ...] или null при ошибке
624
+ */
625
+ async getTopClans() {
626
+ return await this.users.getTopClans();
627
+ }
628
+
629
+ /**
630
+ * Получает рекомендации кого подписаться
631
+ *
632
+ * @returns {Promise<Array|null>} Массив пользователей или null при ошибке
633
+ */
634
+ async getWhoToFollow() {
635
+ return await this.users.getWhoToFollow();
636
+ }
637
+
638
+ /**
639
+ * Загружает файл (изображение) на сервер
640
+ *
641
+ * @param {string} filePath - Путь к файлу
642
+ * @returns {Promise<Object|null>} { id, url, filename, mimeType, size } или null при ошибке
643
+ */
644
+ async uploadFile(filePath) {
645
+ return await this.files.uploadFile(filePath);
646
+ }
647
+
648
+ /**
649
+ * Отправляет репорт на пост, комментарий или пользователя
650
+ *
651
+ * @param {string} targetType - Тип цели: "post", "comment", "user"
652
+ * @param {string} targetId - ID цели
653
+ * @param {string} reason - Причина репорта (по умолчанию "other")
654
+ * @param {string} description - Описание проблемы
655
+ * @returns {Promise<Object|null>} { id, createdAt } или null при ошибке
656
+ */
657
+ async report(targetType, targetId, reason = 'other', description = '') {
658
+ return await this.reports.report(targetType, targetId, reason, description);
659
+ }
660
+
661
+ /**
662
+ * Отправляет репорт на пост
663
+ *
664
+ * @param {string} postId - ID поста
665
+ * @param {string} reason - Причина репорта (по умолчанию "other")
666
+ * @param {string} description - Описание проблемы
667
+ * @returns {Promise<Object|null>} { id, createdAt } или null при ошибке
668
+ */
669
+ async reportPost(postId, reason = 'other', description = '') {
670
+ return await this.reports.reportPost(postId, reason, description);
671
+ }
672
+
673
+ /**
674
+ * Отправляет репорт на комментарий
675
+ *
676
+ * @param {string} commentId - ID комментария
677
+ * @param {string} reason - Причина репорта (по умолчанию "other")
678
+ * @param {string} description - Описание проблемы
679
+ * @returns {Promise<Object|null>} { id, createdAt } или null при ошибке
680
+ */
681
+ async reportComment(commentId, reason = 'other', description = '') {
682
+ return await this.reports.reportComment(commentId, reason, description);
683
+ }
684
+
685
+ /**
686
+ * Отправляет репорт на пользователя
687
+ *
688
+ * @param {string} userId - ID пользователя
689
+ * @param {string} reason - Причина репорта (по умолчанию "other")
690
+ * @param {string} description - Описание проблемы
691
+ * @returns {Promise<Object|null>} { id, createdAt } или null при ошибке
692
+ */
693
+ async reportUser(userId, reason = 'other', description = '') {
694
+ return await this.reports.reportUser(userId, reason, description);
695
+ }
696
+
697
+ /**
698
+ * Выполняет поиск пользователей и хэштегов
699
+ *
700
+ * @param {string} query - Поисковый запрос
701
+ * @param {number} userLimit - Максимальное количество пользователей (по умолчанию 5)
702
+ * @param {number} hashtagLimit - Максимальное количество хэштегов (по умолчанию 5)
703
+ * @returns {Promise<Object|null>} { users: [], hashtags: [] } или null при ошибке
704
+ */
705
+ async search(query, userLimit = 5, hashtagLimit = 5) {
706
+ return await this.search.search(query, userLimit, hashtagLimit);
707
+ }
708
+
709
+ /**
710
+ * Ищет пользователей
711
+ *
712
+ * @param {string} query - Поисковый запрос
713
+ * @param {number} limit - Максимальное количество пользователей (по умолчанию 5)
714
+ * @returns {Promise<Array|null>} Массив пользователей или null при ошибке
715
+ */
716
+ async searchUsers(query, limit = 5) {
717
+ return await this.search.searchUsers(query, limit);
718
+ }
719
+
720
+ /**
721
+ * Ищет хэштеги
722
+ *
723
+ * @param {string} query - Поисковый запрос
724
+ * @param {number} limit - Максимальное количество хэштегов (по умолчанию 5)
725
+ * @returns {Promise<Array|null>} Массив хэштегов или null при ошибке
726
+ */
727
+ async searchHashtags(query, limit = 5) {
728
+ return await this.search.searchHashtags(query, limit);
729
+ }
730
+
731
+ // ========== USER-FRIENDLY МЕТОДЫ ==========
732
+
733
+ // === Посты ===
734
+
735
+ /**
736
+ * Получает трендовые посты (удобный метод)
737
+ *
738
+ * @param {number} limit - Количество постов (по умолчанию 20)
739
+ * @param {string|null} cursor - Курсор для пагинации
740
+ * @returns {Promise<Object>} { posts: [], pagination: {} }
741
+ */
742
+ async getTrendingPosts(limit = 20, cursor = null) {
743
+ return await this.posts.getTrendingPosts(limit, cursor);
744
+ }
745
+
746
+ /**
747
+ * Получает недавние посты (удобный метод)
748
+ *
749
+ * @param {number} limit - Количество постов (по умолчанию 20)
750
+ * @param {string|null} cursor - Курсор для пагинации
751
+ * @returns {Promise<Object>} { posts: [], pagination: {} }
752
+ */
753
+ async getRecentPosts(limit = 20, cursor = null) {
754
+ return await this.posts.getRecentPosts(limit, cursor);
755
+ }
756
+
757
+ /**
758
+ * Получает свои посты (удобный метод)
759
+ *
760
+ * @param {number} limit - Количество постов (по умолчанию 20)
761
+ * @param {string} sort - Сортировка: 'new', 'old', 'popular' (по умолчанию 'new')
762
+ * @param {string|null} cursor - Курсор для пагинации
763
+ * @returns {Promise<Object>} { posts: [], pagination: {} }
764
+ */
765
+ async getMyPosts(limit = 20, sort = 'new', cursor = null) {
766
+ return await this.posts.getMyPosts(limit, sort, cursor);
767
+ }
768
+
769
+ /**
770
+ * Получает последний пост пользователя (удобный метод)
771
+ *
772
+ * @param {string} username - Имя пользователя
773
+ * @returns {Promise<Object|null>} Последний пост или null
774
+ */
775
+ async getUserLatestPost(username) {
776
+ return await this.posts.getUserLatestPost(username);
777
+ }
778
+
779
+ /**
780
+ * Получает количество лайков поста (удобный метод)
781
+ *
782
+ * @param {string} postId - ID поста
783
+ * @returns {Promise<number>} Количество лайков
784
+ */
785
+ async getPostLikesCount(postId) {
786
+ return await this.posts.getPostLikesCount(postId);
787
+ }
788
+
789
+ /**
790
+ * Получает количество просмотров поста (удобный метод)
791
+ *
792
+ * @param {string} postId - ID поста
793
+ * @returns {Promise<number>} Количество просмотров
794
+ */
795
+ async getPostViewsCount(postId) {
796
+ return await this.posts.getPostViewsCount(postId);
797
+ }
798
+
799
+ /**
800
+ * Получает количество комментариев поста (удобный метод)
801
+ *
802
+ * @param {string} postId - ID поста
803
+ * @returns {Promise<number>} Количество комментариев
804
+ */
805
+ async getPostCommentsCount(postId) {
806
+ return await this.posts.getPostCommentsCount(postId);
807
+ }
808
+
809
+ /**
810
+ * Получает статистику поста (удобный метод)
811
+ *
812
+ * @param {string} postId - ID поста
813
+ * @returns {Promise<Object|null>} { likes: number, views: number, comments: number, reposts: number } или null
814
+ */
815
+ async getPostStats(postId) {
816
+ return await this.posts.getPostStats(postId);
817
+ }
818
+
819
+ // === Пользователи ===
820
+
821
+ /**
822
+ * Получает свой профиль (удобный метод)
823
+ *
824
+ * @returns {Promise<Object|null>} Данные профиля или null
825
+ */
826
+ async getMyProfile() {
827
+ return await this.users.getMyProfile();
828
+ }
829
+
830
+ /**
831
+ * Проверяет, подписан ли текущий пользователь на указанного (удобный метод)
832
+ *
833
+ * @param {string} username - Имя пользователя для проверки
834
+ * @returns {Promise<boolean>} True если подписан, false если нет или ошибка
835
+ */
836
+ async isFollowing(username) {
837
+ return await this.users.isFollowing(username);
838
+ }
839
+
840
+ /**
841
+ * Получает количество своих подписчиков (удобный метод)
842
+ *
843
+ * @returns {Promise<number>} Количество подписчиков
844
+ */
845
+ async getMyFollowersCount() {
846
+ return await this.users.getMyFollowersCount();
847
+ }
848
+
849
+ /**
850
+ * Получает количество своих подписок (удобный метод)
851
+ *
852
+ * @returns {Promise<number>} Количество подписок
853
+ */
854
+ async getMyFollowingCount() {
855
+ return await this.users.getMyFollowingCount();
856
+ }
857
+
858
+ /**
859
+ * Получает свой клан (эмодзи аватара) (удобный метод)
860
+ *
861
+ * @returns {Promise<string|null>} Эмодзи клана или null
862
+ */
863
+ async getMyClan() {
864
+ return await this.users.getMyClan();
865
+ }
866
+
867
+ /**
868
+ * Получает клан пользователя (эмодзи аватара) (удобный метод)
869
+ *
870
+ * @param {string} username - Имя пользователя
871
+ * @returns {Promise<string|null>} Эмодзи клана или null
872
+ */
873
+ async getUserClan(username) {
874
+ return await this.users.getUserClan(username);
875
+ }
876
+
877
+ // === Комментарии ===
878
+
879
+ /**
880
+ * Получает топ-комментарий поста (с наибольшим количеством лайков) (удобный метод)
881
+ *
882
+ * @param {string} postId - ID поста
883
+ * @returns {Promise<Object|null>} Топ-комментарий или null
884
+ */
885
+ async getTopComment(postId) {
886
+ return await this.comments.getTopComment(postId);
887
+ }
888
+
889
+ /**
890
+ * Проверяет, есть ли комментарии у поста (удобный метод)
891
+ *
892
+ * @param {string} postId - ID поста
893
+ * @returns {Promise<boolean>} True если есть комментарии
894
+ */
895
+ async hasComments(postId) {
896
+ return await this.comments.hasComments(postId);
897
+ }
898
+
899
+ // === Уведомления ===
900
+
901
+ /**
902
+ * Проверяет, есть ли непрочитанные уведомления (удобный метод)
903
+ *
904
+ * @returns {Promise<boolean>} True если есть непрочитанные
905
+ */
906
+ async hasUnreadNotifications() {
907
+ return await this.notifications.hasUnreadNotifications();
908
+ }
909
+
910
+ /**
911
+ * Получает только непрочитанные уведомления (удобный метод)
912
+ *
913
+ * @param {number} limit - Количество уведомлений
914
+ * @param {string|null} cursor - Курсор для пагинации
915
+ * @returns {Promise<Object|null>} { notifications: [], pagination: {} } или null
916
+ */
917
+ async getUnreadNotifications(limit = 20, cursor = null) {
918
+ return await this.notifications.getUnreadNotifications(limit, cursor);
919
+ }
920
+ }