itd-sdk-js 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/API_REFERENCE.md CHANGED
@@ -30,15 +30,17 @@ import { ITDClient } from 'itd-sdk-js';
30
30
 
31
31
  Для работы автоматического обновления токена создайте файл `.cookies` в корне проекта. Скопируйте содержимое заголовка `Cookie` из любого сетевого запроса к сайту в браузере и вставьте в этот файл.
32
32
 
33
+ **Пути к .env и .cookies:** по умолчанию SDK ищет и сохраняет эти файлы в **корне проекта** (`process.cwd()`). При refresh токена обновлённые значения пишутся в ваш проект, а не в папку пакета. При необходимости можно задать свой корень или явные пути через опции конструктора (см. ниже).
34
+
33
35
  ---
34
36
 
35
37
  ## Авторизация и сессии
36
38
 
37
39
  ### Инициализация клиента
38
40
 
39
- JavaScript
41
+ **Простой вариант (корень проекта = текущая рабочая директория):**
40
42
 
41
- ```
43
+ ```javascript
42
44
  import { ITDClient } from 'itd-sdk-js';
43
45
  import dotenv from 'dotenv';
44
46
 
@@ -49,6 +51,28 @@ client.setAccessToken(process.env.ITD_ACCESS_TOKEN);
49
51
  client.auth.isAuthenticated = true;
50
52
  ```
51
53
 
54
+ **С опциями (projectRoot / envPath / cookiesPath):**
55
+
56
+ Если скрипт запускается из подпапки или нужны явные пути:
57
+
58
+ ```javascript
59
+ const client = new ITDClient({
60
+ baseUrl: 'https://xn--d1ah4a.com',
61
+ userAgent: '...',
62
+ projectRoot: process.cwd(), // корень проекта (по умолчанию process.cwd())
63
+ // envPath: '/path/to/project/.env',
64
+ // cookiesPath: '/path/to/project/.cookies',
65
+ requestTimeout: 60000, // таймаут обычных запросов, мс (по умолчанию 60 с)
66
+ uploadTimeout: 120000, // таймаут загрузки файлов и создания поста, мс (по умолчанию 120 с)
67
+ });
68
+ client.setAccessToken(process.env.ITD_ACCESS_TOKEN);
69
+ ```
70
+
71
+ - `projectRoot` — директория, в которой ищутся `.env` и `.cookies` (по умолчанию `process.cwd()`).
72
+ - `envPath` / `cookiesPath` — при указании переопределяют пути, собранные из `projectRoot`.
73
+ - `requestTimeout` — таймаут обычных запросов в мс (по умолчанию 60000). Предотвращает бесконечное ожидание при «тяжёлой» сети.
74
+ - `uploadTimeout` — таймаут для загрузки файлов и создания поста в мс (по умолчанию 120000). Используется в `uploadFile`, `createPost`, `createWallPost`.
75
+
52
76
  ### Автоматическое обновление (Refresh Token)
53
77
 
54
78
  При получении ошибки `401 Unauthorized` клиент автоматически обращается к эндпоинту `/api/v1/auth/refresh`, используя данные из `.cookies`. В случае успеха новый токен сохраняется в `.env`, обновляются куки, и исходный запрос повторяется.
@@ -91,6 +115,10 @@ const post = await client.createPost('Текст поста', 'image.jpg');
91
115
 
92
116
  Создает новый пост. При указании `imagePath` файл предварительно загружается через `/api/files/upload`, после чего ID файла прикрепляется к посту через поле `attachmentIds`.
93
117
 
118
+ - **Возвращает:** объект поста при успехе; **`null`** при любой ошибке (сеть, 5xx, 429, не удалось загрузить файл, неверный ответ). Всегда проверяйте результат на `null`.
119
+ - **Таймаут:** для загрузки файла и создания поста используется `uploadTimeout` (по умолчанию 120 с), чтобы запрос не зависал при 504 или медленной сети.
120
+ - **Ретраи:** при 5xx/429 или «API вернул null» рекомендуется повторять запрос в приложении (например, до 3–8 попыток с экспоненциальной задержкой). Встроенных ретраев в SDK нет.
121
+
94
122
  **Важно:** API использует поле `attachmentIds` (массив ID файлов), а не `attachments`. SDK автоматически использует правильное поле.
95
123
 
96
124
  - **Параметры**: `text` (string), `imagePath` (string, опционально).
@@ -99,6 +127,8 @@ const post = await client.createPost('Текст поста', 'image.jpg');
99
127
 
100
128
  Создает пост **на стене другого пользователя** (wall post).
101
129
 
130
+ - **Возвращает:** объект поста при успехе; **`null`** при любой ошибке. Проверяйте результат на `null`; при 5xx/429 рекомендуется ретраи в приложении.
131
+ - **Таймаут:** используется `uploadTimeout` (по умолчанию 120 с).
102
132
  - Делается через `POST /api/posts` с телом `{ content, wallRecipientId, attachmentIds? }`.
103
133
  - `wallRecipientId` — это **ID пользователя-получателя**, поэтому метод сначала запрашивает профиль через `getUserProfile(username)` и берет `profile.id`.
104
134
  - При указании `imagePath` файл загружается и прикрепляется через `attachmentIds`.
@@ -195,14 +225,14 @@ const post = await client.createPost('Текст поста', 'image.jpg');
195
225
  - `search(query, userLimit?, hashtagLimit?)` — универсальный поиск пользователей и хэштегов. Возвращает `{ users: [], hashtags: [] }`.
196
226
  - `searchUsers(query, limit?)` — поиск только пользователей.
197
227
  - `searchHashtags(query, limit?)` — поиск только хэштегов.
198
- - `getTopClans()` — рейтинг кланов по количеству участников. Возвращает `{ clans: [{ avatar, memberCount }] }`.
228
+ - `getTopClans()` — рейтинг кланов по количеству участников. **Возвращает массив** `Array<{ avatar, memberCount }>` или **`null`** при ошибке (не объект с полем `clans`).
199
229
  - `getWhoToFollow()` — рекомендованные пользователи.
200
230
  - `getTrendingHashtags(limit)` — список популярных тегов.
201
231
  - `getPostsByHashtag(tagName, limit, cursor)` — поиск постов по тегу. Возвращает `{ posts: [], hashtag: {}, pagination: {} }`.
202
232
 
203
233
  ### Файлы и репорты
204
234
 
205
- - `uploadFile(filePath)` — загрузка файла через `/api/files/upload`. Возвращает `{ id, url, filename, mimeType, size }`. Используется автоматически при создании поста с изображением.
235
+ - `uploadFile(filePath)` — загрузка файла через `/api/files/upload`. Возвращает `{ id, url, filename, mimeType, size }` или **`null`** при ошибке. Таймаут — `uploadTimeout` (по умолчанию 120 с). Используется автоматически при создании поста с изображением.
206
236
  - `report(targetType, targetId, reason?, description?)` — отправка репорта. `targetType`: `"post"`, `"comment"`, `"user"`. Возвращает `{ id, createdAt }`.
207
237
  - `reportPost(postId, reason?, description?)` — репорт поста.
208
238
  - `reportComment(commentId, reason?, description?)` — репорт комментария.
package/README.md CHANGED
@@ -26,11 +26,11 @@ npm install
26
26
 
27
27
  ## Настройка
28
28
 
29
- 1. Создайте `.env` на основе `.env.example` (или используйте переменные окружения).
29
+ 1. Создайте `.env` в корне проекта на основе `.env.example` (или используйте переменные окружения).
30
30
  2. Вставьте свой `ITD_ACCESS_TOKEN` (его можно вытащить из Network в DevTools).
31
- 3. Для работы авто-обновления сессии создайте файл `.cookies` и вставьте туда строку `Cookie` из любого запроса к сайту в браузере.
31
+ 3. Для работы авто-обновления сессии создайте файл `.cookies` в корне проекта и вставьте туда строку `Cookie` из любого запроса к сайту в браузере.
32
32
 
33
- Подробный гайд по настройке лежит в [API_REFERENCE.md](API_REFERENCE.md).
33
+ SDK по умолчанию читает и пишет `.env` и `.cookies` в корне проекта (`process.cwd()`). При обновлении токена изменения сохраняются в ваш проект. При необходимости можно задать `projectRoot` или явные пути в конструкторе — см. [API_REFERENCE.md](API_REFERENCE.md).
34
34
 
35
35
  ## Примеры
36
36
 
@@ -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.4",
3
+ "version": "1.0.6",
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",
@@ -39,7 +39,7 @@
39
39
  "license": "MIT",
40
40
  "repository": {
41
41
  "type": "git",
42
- "url": "https://github.com/FriceKa/ITD-SDK-js.git"
42
+ "url": "git+https://github.com/FriceKa/ITD-SDK-js.git"
43
43
  },
44
44
  "bugs": {
45
45
  "url": "https://github.com/FriceKa/ITD-SDK-js/issues"
package/src/auth.js CHANGED
@@ -68,8 +68,8 @@ export class AuthManager {
68
68
  this.client.setAccessToken(newToken);
69
69
  this.isAuthenticated = true;
70
70
 
71
- // Сохраняем токен в .env файл
72
- await saveAccessToken(newToken);
71
+ // Сохраняем токен в .env файл (в корне проекта)
72
+ await saveAccessToken(newToken, this.client.envPath);
73
73
 
74
74
  // Обновляем cookies, если они пришли в ответе
75
75
  if (response.headers['set-cookie']) {
@@ -94,7 +94,7 @@ export class AuthManager {
94
94
  );
95
95
  if (importantCookies.length > 0) {
96
96
  const cookieHeader = importantCookies.map(c => `${c.key}=${c.value}`).join('; ');
97
- await saveCookieHeader(cookieHeader);
97
+ await saveCookieHeader(cookieHeader, this.client.cookiesPath);
98
98
  }
99
99
  } catch (e) {
100
100
  console.warn('⚠️ Не удалось сохранить обновленные cookies:', e.message);
package/src/client.js CHANGED
@@ -5,7 +5,6 @@ import axios from 'axios';
5
5
  import dotenv from 'dotenv';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
- import { fileURLToPath } from 'url';
9
8
  import { CookieJar } from 'tough-cookie';
10
9
  import { wrapper } from 'axios-cookiejar-support';
11
10
  import { AuthManager } from './auth.js';
@@ -20,22 +19,57 @@ import { SearchManager } from './search.js';
20
19
 
21
20
  dotenv.config();
22
21
 
23
- const __filename = fileURLToPath(import.meta.url);
24
- const __dirname = path.dirname(__filename);
25
-
26
22
  export class ITDClient {
27
23
  /**
28
24
  * Инициализация клиента
29
25
  *
30
- * @param {string} baseUrl - Базовый URL сайта (по умолчанию из .env)
31
- * @param {string} userAgent - User-Agent для запросов (по умолчанию из .env)
26
+ * @param {string|Object} baseUrlOrOptions - Базовый URL сайта или объект опций
27
+ * @param {string} [userAgent] - User-Agent (если первый аргумент baseUrl)
28
+ *
29
+ * Опции (если первый аргумент — объект):
30
+ * @param {string} [options.baseUrl] - Базовый URL сайта
31
+ * @param {string} [options.userAgent] - User-Agent
32
+ * @param {string} [options.projectRoot] - Корень проекта (по умолчанию process.cwd()); .env и .cookies ищутся здесь
33
+ * @param {string} [options.envPath] - Полный путь к .env (переопределяет projectRoot для .env)
34
+ * @param {string} [options.cookiesPath] - Полный путь к .cookies (переопределяет projectRoot для .cookies)
35
+ * @param {number} [options.requestTimeout] - Таймаут обычных запросов в мс (по умолчанию 60000)
36
+ * @param {number} [options.uploadTimeout] - Таймаут загрузки файлов и создания поста в мс (по умолчанию 120000)
32
37
  */
33
- constructor(baseUrl = null, userAgent = null) {
38
+ constructor(baseUrlOrOptions = null, userAgent = null) {
39
+ let baseUrl, projectRoot, envPath, cookiesPath, requestTimeout, uploadTimeout;
40
+
41
+ if (baseUrlOrOptions && typeof baseUrlOrOptions === 'object' && !(baseUrlOrOptions instanceof URL)) {
42
+ const opts = baseUrlOrOptions;
43
+ baseUrl = opts.baseUrl ?? process.env.ITD_BASE_URL ?? 'https://xn--d1ah4a.com';
44
+ userAgent = opts.userAgent ?? process.env.ITD_USER_AGENT ?? null;
45
+ projectRoot = opts.projectRoot ?? process.cwd();
46
+ envPath = opts.envPath ?? path.join(projectRoot, '.env');
47
+ cookiesPath = opts.cookiesPath ?? path.join(projectRoot, '.cookies');
48
+ requestTimeout = opts.requestTimeout ?? 60000;
49
+ uploadTimeout = opts.uploadTimeout ?? 120000;
50
+ } else {
51
+ projectRoot = process.cwd();
52
+ baseUrl = baseUrlOrOptions || process.env.ITD_BASE_URL || 'https://xn--d1ah4a.com';
53
+ envPath = path.join(projectRoot, '.env');
54
+ cookiesPath = path.join(projectRoot, '.cookies');
55
+ requestTimeout = 60000;
56
+ uploadTimeout = 120000;
57
+ }
58
+
34
59
  // Используем реальный домен (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 ||
60
+ this.baseUrl = baseUrl;
61
+ this.userAgent = userAgent || process.env.ITD_USER_AGENT ||
37
62
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
38
63
 
64
+ /** Пути к .env и .cookies (корень проекта по умолчанию) */
65
+ this.envPath = envPath;
66
+ this.cookiesPath = cookiesPath;
67
+
68
+ /** Таймаут обычных запросов (мс). Для загрузки и создания поста используется uploadTimeout. */
69
+ this.requestTimeout = requestTimeout;
70
+ /** Таймаут загрузки файлов и создания поста (мс), чтобы не зависать при 504/медленной сети. */
71
+ this.uploadTimeout = uploadTimeout;
72
+
39
73
  /** @type {string|null} */
40
74
  this.accessToken = null;
41
75
 
@@ -55,6 +89,7 @@ export class ITDClient {
55
89
  // Создание axios instance + cookie jar
56
90
  const axiosConfig = {
57
91
  baseURL: this.baseUrl,
92
+ timeout: requestTimeout,
58
93
  withCredentials: true,
59
94
  jar: this.cookieJar,
60
95
  headers: {
@@ -178,13 +213,12 @@ export class ITDClient {
178
213
  */
179
214
  _loadCookiesFromFile() {
180
215
  try {
181
- const cookiesPath = path.join(__dirname, '..', '.cookies');
182
- if (!fs.existsSync(cookiesPath)) {
216
+ if (!fs.existsSync(this.cookiesPath)) {
183
217
  // Файл не существует - это нормально, просто пропускаем
184
218
  return;
185
219
  }
186
220
 
187
- const cookieHeader = fs.readFileSync(cookiesPath, 'utf8').trim();
221
+ const cookieHeader = fs.readFileSync(this.cookiesPath, 'utf8').trim();
188
222
  if (!cookieHeader) {
189
223
  return;
190
224
  }
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 {
@@ -3,27 +3,24 @@
3
3
  */
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
6
 
11
7
  /**
12
8
  * Обновляет ITD_ACCESS_TOKEN в .env файле
13
- *
9
+ *
14
10
  * @param {string} newToken - Новый access token
11
+ * @param {string} [envPath] - Путь к .env (по умолчанию process.cwd() + '/.env')
15
12
  * @returns {Promise<boolean>} True если успешно
16
13
  */
17
- export async function saveAccessToken(newToken) {
14
+ export async function saveAccessToken(newToken, envPath = null) {
18
15
  try {
19
- const envPath = path.join(__dirname, '..', '.env');
20
-
21
- if (!fs.existsSync(envPath)) {
16
+ const targetPath = envPath ?? path.join(process.cwd(), '.env');
17
+
18
+ if (!fs.existsSync(targetPath)) {
22
19
  console.warn('⚠️ Файл .env не найден, токен не сохранен');
23
20
  return false;
24
21
  }
25
-
26
- let content = fs.readFileSync(envPath, 'utf8');
22
+
23
+ let content = fs.readFileSync(targetPath, 'utf8');
27
24
 
28
25
  // Ищем строку с ITD_ACCESS_TOKEN
29
26
  const tokenRegex = /^ITD_ACCESS_TOKEN=.*$/m;
@@ -36,7 +33,7 @@ export async function saveAccessToken(newToken) {
36
33
  content += `\nITD_ACCESS_TOKEN=${newToken}\n`;
37
34
  }
38
35
 
39
- fs.writeFileSync(envPath, content, 'utf8');
36
+ fs.writeFileSync(targetPath, content, 'utf8');
40
37
  console.log('✅ Токен сохранен в .env');
41
38
  return true;
42
39
  } catch (error) {
@@ -47,16 +44,17 @@ export async function saveAccessToken(newToken) {
47
44
 
48
45
  /**
49
46
  * Сохраняет cookies в файл .cookies
50
- *
47
+ *
51
48
  * @param {string} newCookieHeader - Новый cookie header
49
+ * @param {string} [cookiesPath] - Путь к .cookies (по умолчанию process.cwd() + '/.cookies')
52
50
  * @returns {Promise<boolean>} True если успешно
53
51
  */
54
- export async function saveCookieHeader(newCookieHeader) {
52
+ export async function saveCookieHeader(newCookieHeader, cookiesPath = null) {
55
53
  try {
56
- const cookiesPath = path.join(__dirname, '..', '.cookies');
57
-
54
+ const targetPath = cookiesPath ?? path.join(process.cwd(), '.cookies');
55
+
58
56
  // Просто записываем cookies в файл (одна строка)
59
- fs.writeFileSync(cookiesPath, newCookieHeader, 'utf8');
57
+ fs.writeFileSync(targetPath, newCookieHeader, 'utf8');
60
58
  console.log('✅ Cookies сохранены в .cookies');
61
59
  return true;
62
60
  } catch (error) {
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 {