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/.cookies.example +1 -0
- package/.env.example +45 -0
- package/API_REFERENCE.md +237 -0
- package/README.md +85 -0
- package/examples/README.md +65 -0
- package/examples/auto-refresh.js +63 -0
- package/examples/basic-usage.js +54 -0
- package/examples/quick-start.js +326 -0
- package/examples/user-friendly.js +79 -0
- package/package.json +57 -0
- package/src/auth.js +126 -0
- package/src/client.js +920 -0
- package/src/comments.js +256 -0
- package/src/files.js +59 -0
- package/src/hashtags.js +101 -0
- package/src/notifications.js +215 -0
- package/src/posts.js +480 -0
- package/src/reports.js +97 -0
- package/src/search.js +86 -0
- package/src/token-storage.js +66 -0
- package/src/users.js +416 -0
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
|
+
}
|