bozonx-social-media-posting 1.1.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/LICENSE +21 -0
- package/README.md +1003 -0
- package/dist/src/app.constants.d.ts +8 -0
- package/dist/src/app.constants.js +9 -0
- package/dist/src/app.constants.js.map +1 -0
- package/dist/src/common/enums/body-format.enum.d.ts +12 -0
- package/dist/src/common/enums/body-format.enum.js +14 -0
- package/dist/src/common/enums/body-format.enum.js.map +1 -0
- package/dist/src/common/enums/error-code.enum.d.ts +12 -0
- package/dist/src/common/enums/error-code.enum.js +14 -0
- package/dist/src/common/enums/error-code.enum.js.map +1 -0
- package/dist/src/common/enums/index.d.ts +3 -0
- package/dist/src/common/enums/index.js +4 -0
- package/dist/src/common/enums/index.js.map +1 -0
- package/dist/src/common/enums/post-type.enum.d.ts +28 -0
- package/dist/src/common/enums/post-type.enum.js +30 -0
- package/dist/src/common/enums/post-type.enum.js.map +1 -0
- package/dist/src/common/filters/all-exceptions.filter.d.ts +13 -0
- package/dist/src/common/filters/all-exceptions.filter.js +103 -0
- package/dist/src/common/filters/all-exceptions.filter.js.map +1 -0
- package/dist/src/common/helpers/media-input.helper.d.ts +73 -0
- package/dist/src/common/helpers/media-input.helper.js +122 -0
- package/dist/src/common/helpers/media-input.helper.js.map +1 -0
- package/dist/src/common/interceptors/shutdown.interceptor.d.ts +12 -0
- package/dist/src/common/interceptors/shutdown.interceptor.js +41 -0
- package/dist/src/common/interceptors/shutdown.interceptor.js.map +1 -0
- package/dist/src/common/interfaces/logger.interface.d.ts +44 -0
- package/dist/src/common/interfaces/logger.interface.js +44 -0
- package/dist/src/common/interfaces/logger.interface.js.map +1 -0
- package/dist/src/common/services/shutdown.module.d.ts +2 -0
- package/dist/src/common/services/shutdown.module.js +18 -0
- package/dist/src/common/services/shutdown.module.js.map +1 -0
- package/dist/src/common/services/shutdown.service.d.ts +40 -0
- package/dist/src/common/services/shutdown.service.js +122 -0
- package/dist/src/common/services/shutdown.service.js.map +1 -0
- package/dist/src/common/types/index.d.ts +1 -0
- package/dist/src/common/types/index.js +2 -0
- package/dist/src/common/types/index.js.map +1 -0
- package/dist/src/common/types/media-input.type.d.ts +29 -0
- package/dist/src/common/types/media-input.type.js +2 -0
- package/dist/src/common/types/media-input.type.js.map +1 -0
- package/dist/src/common/validators/body-length.validator.d.ts +24 -0
- package/dist/src/common/validators/body-length.validator.js +57 -0
- package/dist/src/common/validators/body-length.validator.js.map +1 -0
- package/dist/src/common/validators/channel-id.validator.d.ts +19 -0
- package/dist/src/common/validators/channel-id.validator.js +58 -0
- package/dist/src/common/validators/channel-id.validator.js.map +1 -0
- package/dist/src/common/validators/has-content.validator.d.ts +23 -0
- package/dist/src/common/validators/has-content.validator.js +57 -0
- package/dist/src/common/validators/has-content.validator.js.map +1 -0
- package/dist/src/common/validators/media-input.validator.d.ts +44 -0
- package/dist/src/common/validators/media-input.validator.js +112 -0
- package/dist/src/common/validators/media-input.validator.js.map +1 -0
- package/dist/src/common/validators/media-priority.validator.d.ts +19 -0
- package/dist/src/common/validators/media-priority.validator.js +38 -0
- package/dist/src/common/validators/media-priority.validator.js.map +1 -0
- package/dist/src/config/app.config.d.ts +33 -0
- package/dist/src/config/app.config.js +83 -0
- package/dist/src/config/app.config.js.map +1 -0
- package/dist/src/config/library.config.d.ts +51 -0
- package/dist/src/config/library.config.js +197 -0
- package/dist/src/config/library.config.js.map +1 -0
- package/dist/src/config/yaml-config.dto.d.ts +37 -0
- package/dist/src/config/yaml-config.dto.js +152 -0
- package/dist/src/config/yaml-config.dto.js.map +1 -0
- package/dist/src/config/yaml.config.d.ts +14 -0
- package/dist/src/config/yaml.config.js +72 -0
- package/dist/src/config/yaml.config.js.map +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +17 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/library.d.ts +57 -0
- package/dist/src/library.js +92 -0
- package/dist/src/library.js.map +1 -0
- package/dist/src/modules/app-config/app-config.module.d.ts +2 -0
- package/dist/src/modules/app-config/app-config.module.js +26 -0
- package/dist/src/modules/app-config/app-config.module.js.map +1 -0
- package/dist/src/modules/app-config/app-config.service.d.ts +14 -0
- package/dist/src/modules/app-config/app-config.service.js +18 -0
- package/dist/src/modules/app-config/app-config.service.js.map +1 -0
- package/dist/src/modules/app-config/interfaces/app-config.interface.d.ts +31 -0
- package/dist/src/modules/app-config/interfaces/app-config.interface.js +2 -0
- package/dist/src/modules/app-config/interfaces/app-config.interface.js.map +1 -0
- package/dist/src/modules/app-config/nest-config.service.d.ts +41 -0
- package/dist/src/modules/app-config/nest-config.service.js +91 -0
- package/dist/src/modules/app-config/nest-config.service.js.map +1 -0
- package/dist/src/modules/health/health.controller.d.ts +12 -0
- package/dist/src/modules/health/health.controller.js +33 -0
- package/dist/src/modules/health/health.controller.js.map +1 -0
- package/dist/src/modules/health/health.module.d.ts +2 -0
- package/dist/src/modules/health/health.module.js +17 -0
- package/dist/src/modules/health/health.module.js.map +1 -0
- package/dist/src/modules/media/media.module.d.ts +2 -0
- package/dist/src/modules/media/media.module.js +18 -0
- package/dist/src/modules/media/media.module.js.map +1 -0
- package/dist/src/modules/media/media.service.d.ts +15 -0
- package/dist/src/modules/media/media.service.js +49 -0
- package/dist/src/modules/media/media.service.js.map +1 -0
- package/dist/src/modules/platforms/base/auth-validator-registry.service.d.ts +25 -0
- package/dist/src/modules/platforms/base/auth-validator-registry.service.js +50 -0
- package/dist/src/modules/platforms/base/auth-validator-registry.service.js.map +1 -0
- package/dist/src/modules/platforms/base/auth-validator.interface.d.ts +16 -0
- package/dist/src/modules/platforms/base/auth-validator.interface.js +2 -0
- package/dist/src/modules/platforms/base/auth-validator.interface.js.map +1 -0
- package/dist/src/modules/platforms/base/index.d.ts +4 -0
- package/dist/src/modules/platforms/base/index.js +5 -0
- package/dist/src/modules/platforms/base/index.js.map +1 -0
- package/dist/src/modules/platforms/base/platform-registry.service.d.ts +31 -0
- package/dist/src/modules/platforms/base/platform-registry.service.js +54 -0
- package/dist/src/modules/platforms/base/platform-registry.service.js.map +1 -0
- package/dist/src/modules/platforms/base/platform.interface.d.ts +39 -0
- package/dist/src/modules/platforms/base/platform.interface.js +2 -0
- package/dist/src/modules/platforms/base/platform.interface.js.map +1 -0
- package/dist/src/modules/platforms/platforms.module.d.ts +13 -0
- package/dist/src/modules/platforms/platforms.module.js +59 -0
- package/dist/src/modules/platforms/platforms.module.js.map +1 -0
- package/dist/src/modules/platforms/telegram/telegram-auth.validator.d.ts +19 -0
- package/dist/src/modules/platforms/telegram/telegram-auth.validator.js +51 -0
- package/dist/src/modules/platforms/telegram/telegram-auth.validator.js.map +1 -0
- package/dist/src/modules/platforms/telegram/telegram-type-detector.service.d.ts +18 -0
- package/dist/src/modules/platforms/telegram/telegram-type-detector.service.js +47 -0
- package/dist/src/modules/platforms/telegram/telegram-type-detector.service.js.map +1 -0
- package/dist/src/modules/platforms/telegram/telegram.platform.d.ts +58 -0
- package/dist/src/modules/platforms/telegram/telegram.platform.js +434 -0
- package/dist/src/modules/platforms/telegram/telegram.platform.js.map +1 -0
- package/dist/src/modules/post/base-post.service.d.ts +64 -0
- package/dist/src/modules/post/base-post.service.js +99 -0
- package/dist/src/modules/post/base-post.service.js.map +1 -0
- package/dist/src/modules/post/dto/index.d.ts +3 -0
- package/dist/src/modules/post/dto/index.js +4 -0
- package/dist/src/modules/post/dto/index.js.map +1 -0
- package/dist/src/modules/post/dto/post-request.dto.d.ts +56 -0
- package/dist/src/modules/post/dto/post-request.dto.js +195 -0
- package/dist/src/modules/post/dto/post-request.dto.js.map +1 -0
- package/dist/src/modules/post/dto/post-response.dto.d.ts +41 -0
- package/dist/src/modules/post/dto/post-response.dto.js +2 -0
- package/dist/src/modules/post/dto/post-response.dto.js.map +1 -0
- package/dist/src/modules/post/dto/preview-response.dto.d.ts +33 -0
- package/dist/src/modules/post/dto/preview-response.dto.js +2 -0
- package/dist/src/modules/post/dto/preview-response.dto.js.map +1 -0
- package/dist/src/modules/post/idempotency.service.d.ts +95 -0
- package/dist/src/modules/post/idempotency.service.js +229 -0
- package/dist/src/modules/post/idempotency.service.js.map +1 -0
- package/dist/src/modules/post/post.controller.d.ts +13 -0
- package/dist/src/modules/post/post.controller.js +97 -0
- package/dist/src/modules/post/post.controller.js.map +1 -0
- package/dist/src/modules/post/post.module.d.ts +2 -0
- package/dist/src/modules/post/post.module.js +25 -0
- package/dist/src/modules/post/post.module.js.map +1 -0
- package/dist/src/modules/post/post.service.d.ts +62 -0
- package/dist/src/modules/post/post.service.js +325 -0
- package/dist/src/modules/post/post.service.js.map +1 -0
- package/dist/src/modules/post/preview.service.d.ts +23 -0
- package/dist/src/modules/post/preview.service.js +69 -0
- package/dist/src/modules/post/preview.service.js.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
# Social Media Posting Microservice
|
|
2
|
+
|
|
3
|
+
A stateless microservice for publishing content to social media platforms through a unified REST API. Can also be used as a standalone TypeScript library.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Unified API** — Single endpoint for all platforms
|
|
8
|
+
- **Telegram Support** — Posts, images, videos, albums, documents, audio
|
|
9
|
+
- **Auto Type Detection** — Automatically determines post type from media fields
|
|
10
|
+
- **Idempotency** — Prevents duplicate posts with `idempotencyKey`
|
|
11
|
+
- **Retry Logic** — Automatic retries with jitter for transient errors
|
|
12
|
+
- **YAML Config** — Environment variable substitution support
|
|
13
|
+
- **Library Mode** — Use as a standalone TypeScript library in your projects
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Usage Examples](#usage-examples)
|
|
19
|
+
- [API Reference](#api-reference)
|
|
20
|
+
- [Configuration](#configuration)
|
|
21
|
+
- [Library Mode](#library-mode)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [Docker](#docker)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Configure
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cp .env.production.example .env.production
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Edit `config.yaml` with your Telegram credentials:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
accounts:
|
|
44
|
+
my_account:
|
|
45
|
+
platform: telegram
|
|
46
|
+
auth:
|
|
47
|
+
apiKey: ${MY_TELEGRAM_TOKEN} # or direct value
|
|
48
|
+
channelId: "@my_channel"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Run
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm start:dev # Development
|
|
55
|
+
pnpm build && pnpm start:prod # Production
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
API available at `http://localhost:8080/api/v1`
|
|
59
|
+
|
|
60
|
+
## Usage Examples
|
|
61
|
+
|
|
62
|
+
### Text Post
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
66
|
+
-H "Content-Type: application/json" \
|
|
67
|
+
-d '{
|
|
68
|
+
"platform": "telegram",
|
|
69
|
+
"account": "my_channel",
|
|
70
|
+
"body": "<b>Hello!</b> This is a test post",
|
|
71
|
+
"bodyFormat": "html"
|
|
72
|
+
}'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Image Post
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-d '{
|
|
81
|
+
"platform": "telegram",
|
|
82
|
+
"account": "my_channel",
|
|
83
|
+
"body": "Check out this image!",
|
|
84
|
+
"cover": {
|
|
85
|
+
"src": "https://example.com/image.jpg"
|
|
86
|
+
}
|
|
87
|
+
}'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Album
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
94
|
+
-H "Content-Type: application/json" \
|
|
95
|
+
-d '{
|
|
96
|
+
"platform": "telegram",
|
|
97
|
+
"account": "my_channel",
|
|
98
|
+
"body": "Photo gallery",
|
|
99
|
+
"media": [
|
|
100
|
+
{ "src": "https://example.com/photo1.jpg" },
|
|
101
|
+
{ "src": "https://example.com/photo2.jpg" }
|
|
102
|
+
]
|
|
103
|
+
}'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Image with Spoiler
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
110
|
+
-H "Content-Type: application/json" \
|
|
111
|
+
-d '{
|
|
112
|
+
"platform": "telegram",
|
|
113
|
+
"account": "my_channel",
|
|
114
|
+
"body": "Sensitive content",
|
|
115
|
+
"cover": {
|
|
116
|
+
"src": "https://example.com/image.jpg",
|
|
117
|
+
"hasSpoiler": true
|
|
118
|
+
}
|
|
119
|
+
}'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
### Base URL
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
http://localhost:8080/{BASE_PATH}/api/v1
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
*By default, `BASE_PATH` is unset, making the base URL `http://localhost:8080/api/v1`.*
|
|
131
|
+
|
|
132
|
+
### Endpoints
|
|
133
|
+
|
|
134
|
+
| Method | Endpoint | Description |
|
|
135
|
+
|--------|----------|-------------|
|
|
136
|
+
| POST | `/post` | Publish content to a platform |
|
|
137
|
+
| POST | `/preview` | Validate and preview without publishing |
|
|
138
|
+
| GET | `/health` | Health check |
|
|
139
|
+
|
|
140
|
+
### POST /post
|
|
141
|
+
|
|
142
|
+
Publish content to a social media platform.
|
|
143
|
+
|
|
144
|
+
#### Request Body
|
|
145
|
+
|
|
146
|
+
| Field | Type | Required | Description |
|
|
147
|
+
|-------|------|----------|-------------|
|
|
148
|
+
| `platform` | string | Yes | Platform name (`telegram`) |
|
|
149
|
+
| `body` | string | No* | Post content (max length determined by `maxBody` or config default) |
|
|
150
|
+
| `account` | string | No* | Channel name from `config.yaml` |
|
|
151
|
+
| `channelId` | string \| number | No | Channel/chat ID (e.g., @mychannel, -100123456789, or 123456789 for Telegram). Accepts both string and number formats |
|
|
152
|
+
| `auth` | object | No* | Inline authentication credentials. See [Auth Field](#auth-field) below |
|
|
153
|
+
| `type` | string | No | Post type (default: `auto`). See [Post Types](#post-types) |
|
|
154
|
+
| `bodyFormat` | string | No | Body format: `text`, `html`, `md`, or platform-specific (e.g., `MarkdownV2`) (default: `text`) |
|
|
155
|
+
| `title` | string | No | Post title (platform-specific, max 1000 characters) |
|
|
156
|
+
| `description` | string | No | Post description (platform-specific, max 5000 characters) |
|
|
157
|
+
| `cover` | MediaInput | No | Cover image (object with `src` and optional `hasSpoiler`, max 500 characters for src) |
|
|
158
|
+
| `video` | MediaInput | No | Video file (object with `src` and optional `hasSpoiler`, max 500 characters for src) |
|
|
159
|
+
| `audio` | MediaInput | No | Audio file (object with `src`, max 500 characters) |
|
|
160
|
+
| `document` | MediaInput | No | Document file (object with `src`, max 500 characters) |
|
|
161
|
+
| `media` | MediaInput[] | No | Media array for albums (each object with `src` and optional `type`, `hasSpoiler`) |
|
|
162
|
+
| `options` | object | No | Platform-specific options (passed directly to platform API) |
|
|
163
|
+
| `disableNotification` | boolean | No | Send message silently (defaults to config value) |
|
|
164
|
+
| `tags` | string[] | No | Tags without # symbol. Passed as-is to supported platforms (max 200 items, each max 300 characters) |
|
|
165
|
+
| `scheduledAt` | string | No | Scheduled time (ISO 8601, max 50 characters) |
|
|
166
|
+
| `postLanguage` | string | No | Content language code. Passed as-is to supported platforms (max 50 characters) |
|
|
167
|
+
| `mode` | string | No | Mode: `publish`, `draft`. Only for supported platforms |
|
|
168
|
+
| `idempotencyKey` | string | No | Key to prevent duplicates (max 1000 characters) |
|
|
169
|
+
| `maxBody` | number | No | Override max body length from account config (max 500,000 characters). Takes priority over account's `maxBody` setting |
|
|
170
|
+
|
|
171
|
+
**Note:** Either `account` or `auth` must be provided. Either `body` or at least one media field (`cover`, `video`, `audio`, `document`, `media`) must be provided.
|
|
172
|
+
|
|
173
|
+
#### Auth Field
|
|
174
|
+
|
|
175
|
+
The `auth` field contains platform-specific authentication credentials. Its structure matches the `auth` section in `config.yaml`:
|
|
176
|
+
|
|
177
|
+
**For Telegram:**
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"auth": {
|
|
182
|
+
"apiKey": "123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
| Property | Type | Required | Description |
|
|
188
|
+
|----------|------|----------|-------------|
|
|
189
|
+
| `apiKey` | string | Yes | API key (for Telegram: bot token from @BotFather) |
|
|
190
|
+
|
|
191
|
+
In addition to the fields listed in the table, the `auth` object may contain **additional provider-specific fields**.
|
|
192
|
+
|
|
193
|
+
- All `auth` fields from config and request are **merged and passed to the provider as-is**
|
|
194
|
+
- Each provider is responsible for parsing and validating its own extra fields
|
|
195
|
+
|
|
196
|
+
**Auth Merging Behavior:**
|
|
197
|
+
|
|
198
|
+
- If `account` is provided, the base auth is taken from the account configuration in `config.yaml`
|
|
199
|
+
- If `auth` is also provided in the request, its fields **override** the channel's auth fields
|
|
200
|
+
- All auth fields in the request are optional - they only override specific fields from the channel config
|
|
201
|
+
- Final validation checks that all required fields for the platform are present after merging
|
|
202
|
+
|
|
203
|
+
**Examples:**
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
// Use channel config entirely
|
|
207
|
+
{
|
|
208
|
+
"platform": "telegram",
|
|
209
|
+
"account": "my_channel",
|
|
210
|
+
"body": "Hello"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Override only channelId (string format)
|
|
214
|
+
{
|
|
215
|
+
"platform": "telegram",
|
|
216
|
+
"account": "my_channel",
|
|
217
|
+
"channelId": "@different_channel",
|
|
218
|
+
"body": "Hello"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Provide complete inline auth (no channel)
|
|
222
|
+
{
|
|
223
|
+
"platform": "telegram",
|
|
224
|
+
"auth": {
|
|
225
|
+
"apiKey": "123456789:ABC-DEF..."
|
|
226
|
+
},
|
|
227
|
+
"channelId": "@my_channel",
|
|
228
|
+
"body": "Hello"
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Post Types
|
|
233
|
+
|
|
234
|
+
| Type | Description |
|
|
235
|
+
|------|-------------|
|
|
236
|
+
| `auto` | Auto-detect from media fields (default) |
|
|
237
|
+
| `post` | Text message |
|
|
238
|
+
| `image` | Single image with caption |
|
|
239
|
+
| `video` | Video with caption |
|
|
240
|
+
| `audio` | Audio file with caption |
|
|
241
|
+
| `album` | Media group |
|
|
242
|
+
| `document` | Document/file |
|
|
243
|
+
| `article` | Long-form article |
|
|
244
|
+
| `short` | Short-form video |
|
|
245
|
+
| `story` | Story/status |
|
|
246
|
+
| `poll` | Poll |
|
|
247
|
+
|
|
248
|
+
#### MediaInput Format
|
|
249
|
+
|
|
250
|
+
Media fields accept only object format with `src` property and additional options:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
// Object with URL src
|
|
254
|
+
"cover": {
|
|
255
|
+
"src": "https://example.com/image.jpg"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Object with file_id src (Telegram)
|
|
259
|
+
"cover": {
|
|
260
|
+
"src": "AgACAgIAAxkBAAIC..."
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Object with URL src and hasSpoiler
|
|
264
|
+
"cover": {
|
|
265
|
+
"src": "https://example.com/image.jpg",
|
|
266
|
+
"hasSpoiler": true
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Media array item with explicit type
|
|
270
|
+
"media": [
|
|
271
|
+
{
|
|
272
|
+
"src": "https://example.com/image.jpg",
|
|
273
|
+
"type": "image"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"src": "https://example.com/video.mp4",
|
|
277
|
+
"type": "video"
|
|
278
|
+
}
|
|
279
|
+
]
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Object properties:**
|
|
283
|
+
|
|
284
|
+
| Property | Type | Description |
|
|
285
|
+
|----------|------|-------------|
|
|
286
|
+
| `src` | string | Media source (URL or Telegram file_id, max 500 characters) - **required** |
|
|
287
|
+
| `hasSpoiler` | boolean | Hide under spoiler (optional) |
|
|
288
|
+
| `type` | string | Explicit media type: `image`, `video`, `audio`, `document` (optional, used in `media[]` arrays) |
|
|
289
|
+
|
|
290
|
+
**Notes:**
|
|
291
|
+
- `src` is **required** in all media fields
|
|
292
|
+
- `src` can be either a URL or a Telegram file_id
|
|
293
|
+
- URL and file_id strings have a maximum length of 500 characters
|
|
294
|
+
- The `type` property is only used in `media[]` arrays to override auto-detection by URL extension
|
|
295
|
+
- For single media fields (`cover`, `video`, `audio`, `document`) the `type` property is ignored
|
|
296
|
+
- For Telegram albums, `type` is mapped to supported types: `video` → `video`, others → `photo`
|
|
297
|
+
|
|
298
|
+
#### Success Response
|
|
299
|
+
|
|
300
|
+
**Status:** `200 OK`
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"success": true,
|
|
305
|
+
"data": {
|
|
306
|
+
"postId": "123456",
|
|
307
|
+
"url": "https://t.me/channel/123456",
|
|
308
|
+
"platform": "telegram",
|
|
309
|
+
"type": "post",
|
|
310
|
+
"publishedAt": "2025-11-30T20:00:00.000Z",
|
|
311
|
+
"raw": {
|
|
312
|
+
"ok": true,
|
|
313
|
+
"result": {
|
|
314
|
+
"message_id": 123456,
|
|
315
|
+
"chat": { "id": -1001234567890 },
|
|
316
|
+
"date": 1701369600,
|
|
317
|
+
"text": "Post content"
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
"requestId": "uuid-v4"
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
| Field | Type | Description |
|
|
326
|
+
|-------|------|-------------|
|
|
327
|
+
| `postId` | string | Platform-specific post ID |
|
|
328
|
+
| `url` | string | Public URL to the post (if available) |
|
|
329
|
+
| `platform` | string | Platform name |
|
|
330
|
+
| `type` | string | Actual post type used |
|
|
331
|
+
| `publishedAt` | string | Publication timestamp (ISO 8601) |
|
|
332
|
+
| `raw` | object | Raw response from platform API in format `{ok: true, result: {...}}` (matches Telegram Bot API structure) |
|
|
333
|
+
| `requestId` | string | Unique request identifier for tracking |
|
|
334
|
+
|
|
335
|
+
#### Error Response
|
|
336
|
+
|
|
337
|
+
**Status:** `200 OK`
|
|
338
|
+
|
|
339
|
+
```json
|
|
340
|
+
{
|
|
341
|
+
"success": false,
|
|
342
|
+
"error": {
|
|
343
|
+
"code": "VALIDATION_ERROR",
|
|
344
|
+
"message": "Error description",
|
|
345
|
+
"details": {},
|
|
346
|
+
"raw": {},
|
|
347
|
+
"requestId": "uuid-v4"
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### Error Codes
|
|
353
|
+
|
|
354
|
+
| Code | Description |
|
|
355
|
+
|------|-------------|
|
|
356
|
+
| `VALIDATION_ERROR` | Invalid request parameters |
|
|
357
|
+
| `AUTH_ERROR` | Authentication failed |
|
|
358
|
+
| `PLATFORM_ERROR` | Platform API error |
|
|
359
|
+
| `TIMEOUT_ERROR` | Request timeout |
|
|
360
|
+
| `RATE_LIMIT_ERROR` | Rate limit exceeded |
|
|
361
|
+
| `INTERNAL_ERROR` | Internal server error |
|
|
362
|
+
|
|
363
|
+
### POST /preview
|
|
364
|
+
|
|
365
|
+
Validate and preview content without publishing.
|
|
366
|
+
|
|
367
|
+
#### Request Body
|
|
368
|
+
|
|
369
|
+
Same as `/post`. The `idempotencyKey` field is ignored.
|
|
370
|
+
|
|
371
|
+
#### Success Response
|
|
372
|
+
|
|
373
|
+
**Status:** `200 OK`
|
|
374
|
+
|
|
375
|
+
```json
|
|
376
|
+
{
|
|
377
|
+
"success": true,
|
|
378
|
+
"data": {
|
|
379
|
+
"valid": true,
|
|
380
|
+
"detectedType": "post",
|
|
381
|
+
"convertedBody": "# Hello\n\nThis is **bold**",
|
|
382
|
+
"targetFormat": "md",
|
|
383
|
+
"convertedBodyLength": 22,
|
|
384
|
+
"warnings": []
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### Error Response
|
|
390
|
+
|
|
391
|
+
**Status:** `200 OK`
|
|
392
|
+
|
|
393
|
+
```json
|
|
394
|
+
{
|
|
395
|
+
"success": false,
|
|
396
|
+
"data": {
|
|
397
|
+
"valid": false,
|
|
398
|
+
"errors": ["Either 'account' or 'auth' must be provided"],
|
|
399
|
+
"warnings": []
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### GET /health
|
|
405
|
+
|
|
406
|
+
Returns service health status.
|
|
407
|
+
|
|
408
|
+
**Response:**
|
|
409
|
+
|
|
410
|
+
```json
|
|
411
|
+
{
|
|
412
|
+
"status": "ok"
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Telegram
|
|
417
|
+
|
|
418
|
+
#### Supported Types
|
|
419
|
+
|
|
420
|
+
| Type | API Method |
|
|
421
|
+
|------|------------|
|
|
422
|
+
| `post` | sendMessage |
|
|
423
|
+
| `image` | sendPhoto |
|
|
424
|
+
| `video` | sendVideo |
|
|
425
|
+
| `audio` | sendAudio |
|
|
426
|
+
| `document` | sendDocument |
|
|
427
|
+
| `album` | sendMediaGroup |
|
|
428
|
+
|
|
429
|
+
#### Auto Type Detection
|
|
430
|
+
|
|
431
|
+
When `type` is `auto` (default), the type is detected by priority:
|
|
432
|
+
|
|
433
|
+
| Priority | Field | Detected Type |
|
|
434
|
+
|----------|-------|---------------|
|
|
435
|
+
| 1 | `media[]` | `album` |
|
|
436
|
+
| 2 | `document` | `document` |
|
|
437
|
+
| 3 | `audio` | `audio` |
|
|
438
|
+
| 4 | `video` | `video` |
|
|
439
|
+
| 5 | `cover` | `image` |
|
|
440
|
+
| 6 | (none) | `post` |
|
|
441
|
+
|
|
442
|
+
**Note:** With `type: auto`, if multiple media fields are provided, the one with the highest priority is selected.
|
|
443
|
+
For Telegram, `cover` (priority 5) is considered low priority and cannot be combined with other media types (it will be ignored if higher priority media is present).
|
|
444
|
+
Other providers may allow combining `cover` with other media (e.g. video + cover).
|
|
445
|
+
|
|
446
|
+
#### Body Format Handling
|
|
447
|
+
|
|
448
|
+
The `bodyFormat` field specifies the format of the `body` content. For Telegram, the body is **sent as-is** without any conversion. The `bodyFormat` value is used to determine the appropriate `parse_mode` parameter for Telegram's API.
|
|
449
|
+
|
|
450
|
+
**Standard format mappings:**
|
|
451
|
+
|
|
452
|
+
| `bodyFormat` | Telegram `parse_mode` | Description |
|
|
453
|
+
|--------------|----------------------|-------------|
|
|
454
|
+
| `text` | *(not set)* | Plain text, no formatting |
|
|
455
|
+
| `html` | `HTML` | HTML formatting |
|
|
456
|
+
| `md` | `Markdown` | Markdown formatting |
|
|
457
|
+
|
|
458
|
+
**Custom format support:**
|
|
459
|
+
|
|
460
|
+
Any other `bodyFormat` value is passed directly as-is to the `parse_mode` parameter. This allows you to use platform-specific formats like Telegram's `MarkdownV2`:
|
|
461
|
+
|
|
462
|
+
| `bodyFormat` | Telegram `parse_mode` | Description |
|
|
463
|
+
|--------------|----------------------|-------------|
|
|
464
|
+
| `MarkdownV2` | `MarkdownV2` | Telegram's MarkdownV2 format (passed as-is) |
|
|
465
|
+
| *any other* | *passed as-is* | Custom value for the platform |
|
|
466
|
+
|
|
467
|
+
**Important Notes:**
|
|
468
|
+
|
|
469
|
+
- **No conversion is performed** - the body content you provide must already be in the format specified by `bodyFormat`
|
|
470
|
+
- **Options override** - If you specify `parse_mode` in the `options` field, it will **always override** the automatic mapping from `bodyFormat`
|
|
471
|
+
|
|
472
|
+
**Examples:**
|
|
473
|
+
|
|
474
|
+
```json
|
|
475
|
+
// Plain text (no formatting)
|
|
476
|
+
{
|
|
477
|
+
"body": "Hello world",
|
|
478
|
+
"bodyFormat": "text"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// HTML formatting
|
|
482
|
+
{
|
|
483
|
+
"body": "<b>Hello</b> <i>world</i>",
|
|
484
|
+
"bodyFormat": "html"
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Markdown formatting
|
|
488
|
+
{
|
|
489
|
+
"body": "**Hello** _world_",
|
|
490
|
+
"bodyFormat": "md"
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// MarkdownV2 formatting - now can be specified directly in bodyFormat
|
|
494
|
+
{
|
|
495
|
+
"body": "*Hello* _world_\\!",
|
|
496
|
+
"bodyFormat": "MarkdownV2"
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Override bodyFormat with options.parse_mode
|
|
500
|
+
{
|
|
501
|
+
"body": "*Hello* _world_\\!",
|
|
502
|
+
"bodyFormat": "html",
|
|
503
|
+
"options": {
|
|
504
|
+
"parse_mode": "MarkdownV2"
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
#### Platform Options
|
|
510
|
+
|
|
511
|
+
The `options` field accepts platform-specific parameters that are passed directly to the Telegram Bot API without transformation. Use the exact field names from the [Telegram Bot API documentation](https://core.telegram.org/bots/api).
|
|
512
|
+
|
|
513
|
+
Common options:
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
{
|
|
517
|
+
"parse_mode": "HTML" | "Markdown" | "MarkdownV2",
|
|
518
|
+
"disable_notification": boolean,
|
|
519
|
+
"disable_web_page_preview": boolean, // deprecated, use link_preview_options
|
|
520
|
+
"link_preview_options": {
|
|
521
|
+
"is_disabled": boolean,
|
|
522
|
+
"url": string,
|
|
523
|
+
"prefer_small_media": boolean,
|
|
524
|
+
"prefer_large_media": boolean,
|
|
525
|
+
"show_above_text": boolean
|
|
526
|
+
},
|
|
527
|
+
"protect_content": boolean,
|
|
528
|
+
"reply_to_message_id": number,
|
|
529
|
+
"reply_markup": {
|
|
530
|
+
"inline_keyboard": [[{
|
|
531
|
+
"text": string,
|
|
532
|
+
"url"?: string,
|
|
533
|
+
"callback_data"?: string,
|
|
534
|
+
"web_app"?: { "url": string },
|
|
535
|
+
// ... other button types
|
|
536
|
+
}]]
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Note:** If `parse_mode` or `disable_notification` are specified in `options`, they will override the values derived from `bodyFormat` or account configuration.
|
|
542
|
+
|
|
543
|
+
#### Telegram Limits
|
|
544
|
+
|
|
545
|
+
Telegram API enforces the following limits:
|
|
546
|
+
|
|
547
|
+
| Type | Limit |
|
|
548
|
+
|------|-------|
|
|
549
|
+
| Text message | 4096 characters |
|
|
550
|
+
| Caption | 1024 characters |
|
|
551
|
+
| Album items | Telegram limit: 2-10 files |
|
|
552
|
+
| File size (URL) | 50 MB |
|
|
553
|
+
|
|
554
|
+
**Note:** The microservice validates body length based on `maxBody` parameter. Priority order: request `maxBody` > account `maxBody`, with a hard service limit of 500,000 characters. Telegram's specific limits (4096 for text, 1024 for captions) are enforced by Telegram API itself.
|
|
555
|
+
|
|
556
|
+
#### Ignored Fields
|
|
557
|
+
|
|
558
|
+
These fields are not used by Telegram and will be ignored:
|
|
559
|
+
|
|
560
|
+
- `title`, `description`, `tags`, `postLanguage`, `mode`, `scheduledAt`
|
|
561
|
+
|
|
562
|
+
### More Examples
|
|
563
|
+
|
|
564
|
+
#### Audio
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
568
|
+
-H "Content-Type: application/json" \
|
|
569
|
+
-d '{
|
|
570
|
+
"platform": "telegram",
|
|
571
|
+
"account": "my_channel",
|
|
572
|
+
"body": "New podcast",
|
|
573
|
+
"audio": {
|
|
574
|
+
"src": "https://example.com/podcast.mp3"
|
|
575
|
+
}
|
|
576
|
+
}'
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
#### Document
|
|
580
|
+
|
|
581
|
+
```bash
|
|
582
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
583
|
+
-H "Content-Type: application/json" \
|
|
584
|
+
-d '{
|
|
585
|
+
"platform": "telegram",
|
|
586
|
+
"account": "my_channel",
|
|
587
|
+
"body": "Monthly report",
|
|
588
|
+
"document": {
|
|
589
|
+
"src": "https://example.com/report.pdf"
|
|
590
|
+
}
|
|
591
|
+
}'
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### Using file_id
|
|
595
|
+
|
|
596
|
+
```bash
|
|
597
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
598
|
+
-H "Content-Type: application/json" \
|
|
599
|
+
-d '{
|
|
600
|
+
"platform": "telegram",
|
|
601
|
+
"account": "my_channel",
|
|
602
|
+
"body": "Reposting video",
|
|
603
|
+
"video": {
|
|
604
|
+
"src": "BAACAgIAAxkBAAIC4mF9..."
|
|
605
|
+
}
|
|
606
|
+
}'
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
#### With Inline Keyboard
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
613
|
+
-H "Content-Type: application/json" \
|
|
614
|
+
-d '{
|
|
615
|
+
"platform": "telegram",
|
|
616
|
+
"account": "my_channel",
|
|
617
|
+
"body": "Check our website!",
|
|
618
|
+
"options": {
|
|
619
|
+
"reply_markup": {
|
|
620
|
+
"inline_keyboard": [
|
|
621
|
+
[{"text": "Visit", "url": "https://example.com"}],
|
|
622
|
+
[{"text": "Contact", "callback_data": "contact"}]
|
|
623
|
+
]
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}'
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
#### With Inline Auth
|
|
630
|
+
|
|
631
|
+
```bash
|
|
632
|
+
curl -X POST http://localhost:8080/api/v1/post \
|
|
633
|
+
-H "Content-Type: application/json" \
|
|
634
|
+
-d '{
|
|
635
|
+
"platform": "telegram",
|
|
636
|
+
"body": "Test post",
|
|
637
|
+
"auth": {
|
|
638
|
+
"apiKey": "123456:ABC..."
|
|
639
|
+
},
|
|
640
|
+
"channelId": "@my_channel"
|
|
641
|
+
}'
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
#### Preview
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
curl -X POST http://localhost:8080/api/v1/preview \
|
|
648
|
+
-H "Content-Type: application/json" \
|
|
649
|
+
-d '{
|
|
650
|
+
"platform": "telegram",
|
|
651
|
+
"account": "my_channel",
|
|
652
|
+
"body": "# Hello\n\nThis is **bold**",
|
|
653
|
+
"bodyFormat": "md"
|
|
654
|
+
}'
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Idempotency
|
|
658
|
+
|
|
659
|
+
When `idempotencyKey` is provided:
|
|
660
|
+
|
|
661
|
+
1. Key is combined with payload and hashed
|
|
662
|
+
2. Stored in in-memory cache with TTL (`idempotencyTtlMinutes`)
|
|
663
|
+
3. Duplicate requests within TTL:
|
|
664
|
+
- If processing → `409 Conflict`
|
|
665
|
+
- If completed → cached response returned
|
|
666
|
+
|
|
667
|
+
**Limitations:**
|
|
668
|
+
|
|
669
|
+
- Scoped to single instance
|
|
670
|
+
- Lost on restart
|
|
671
|
+
|
|
672
|
+
### Body Content Handling
|
|
673
|
+
|
|
674
|
+
**The microservice does not perform content conversion.** The `body` content is always sent as-is to the platform API.
|
|
675
|
+
|
|
676
|
+
The `bodyFormat` field is used only to specify the format of the content you're providing, which is then mapped to the appropriate platform-specific parameter (e.g., `parse_mode` for Telegram).
|
|
677
|
+
|
|
678
|
+
**Your responsibility:** Ensure that the `body` content is already formatted according to the `bodyFormat` you specify.
|
|
679
|
+
|
|
680
|
+
### Retry Logic
|
|
681
|
+
|
|
682
|
+
Automatic retries for transient errors:
|
|
683
|
+
|
|
684
|
+
- **Retryable:** Network timeouts, 5xx, rate limits
|
|
685
|
+
- **Non-retryable:** Validation errors, 4xx (except 429)
|
|
686
|
+
- **Formula:** `delay = retryDelayMs × random(0.8, 1.2) × attempt`
|
|
687
|
+
|
|
688
|
+
Configure in `config.yaml`:
|
|
689
|
+
|
|
690
|
+
```yaml
|
|
691
|
+
retryAttempts: 3
|
|
692
|
+
retryDelayMs: 1000
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
## Configuration
|
|
696
|
+
|
|
697
|
+
### Environment Variables
|
|
698
|
+
|
|
699
|
+
| Variable | Default | Description |
|
|
700
|
+
|----------|---------|-------------|
|
|
701
|
+
| `NODE_ENV` | `development` | Environment mode |
|
|
702
|
+
| `LISTEN_HOST` | `0.0.0.0` | Server bind address |
|
|
703
|
+
| `LISTEN_PORT` | `8080` | Server port |
|
|
704
|
+
| `BASE_PATH` | (none) | Base path for the application (API will be at `{BASE_PATH}/api/v1`) |
|
|
705
|
+
| `LOG_LEVEL` | `warn` | Logging level |
|
|
706
|
+
| `CONFIG_PATH` | `./config.yaml` | Path to config file |
|
|
707
|
+
|
|
708
|
+
### Config File (`config.yaml`)
|
|
709
|
+
|
|
710
|
+
```yaml
|
|
711
|
+
# Request timeout (seconds)
|
|
712
|
+
# Total time limit for processing a request, including all retries and platform delays
|
|
713
|
+
requestTimeoutSecs: 60
|
|
714
|
+
retryAttempts: 3
|
|
715
|
+
retryDelayMs: 1000
|
|
716
|
+
idempotencyTtlMinutes: 10
|
|
717
|
+
accounts:
|
|
718
|
+
my_account:
|
|
719
|
+
platform: telegram
|
|
720
|
+
auth:
|
|
721
|
+
apiKey: ${MY_TELEGRAM_TOKEN}
|
|
722
|
+
channelId: "@my_channel"
|
|
723
|
+
maxBody: 100000 # Optional: account-specific limit; request maxBody can override it (up to hard limit 500000)
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Limits
|
|
727
|
+
|
|
728
|
+
- **Request timeout**
|
|
729
|
+
- Config field: `requestTimeoutSecs`
|
|
730
|
+
- Range: **1–600 seconds** (up to 10 minutes)
|
|
731
|
+
- Applied to the whole processing of a request, including retries and platform API calls.
|
|
732
|
+
|
|
733
|
+
- **Body length**
|
|
734
|
+
- Hard service limit: **500,000 characters** (cannot be exceeded).
|
|
735
|
+
- Effective limit is calculated as:
|
|
736
|
+
- Request `maxBody` (if provided) →
|
|
737
|
+
- otherwise account `maxBody` from `config.yaml` (if provided) →
|
|
738
|
+
- otherwise the hard service limit `500000`.
|
|
739
|
+
- If the body is longer than the effective limit, validation fails with a `VALIDATION_ERROR`.
|
|
740
|
+
|
|
741
|
+
### Graceful Shutdown
|
|
742
|
+
|
|
743
|
+
The service implements graceful shutdown to ensure in-flight requests complete properly when receiving termination signals.
|
|
744
|
+
|
|
745
|
+
#### Behavior
|
|
746
|
+
|
|
747
|
+
When the service receives `SIGTERM` or `SIGINT`:
|
|
748
|
+
|
|
749
|
+
1. **New connections are rejected** — Returns `503 Service Unavailable` for new requests
|
|
750
|
+
2. **In-flight requests continue** — Active requests are allowed to complete
|
|
751
|
+
3. **Timeout enforcement** — Maximum wait time is **30 seconds** (hardcoded constant)
|
|
752
|
+
4. **Forced termination** — After timeout, the process exits even if requests are still active
|
|
753
|
+
|
|
754
|
+
#### Configuration
|
|
755
|
+
|
|
756
|
+
- **Shutdown timeout**: `30 seconds` (global constant in `src/app.constants.ts`)
|
|
757
|
+
- **Fastify settings**:
|
|
758
|
+
- `forceCloseConnections: 'idle'` — Closes idle connections during shutdown
|
|
759
|
+
- `connectionTimeout: 60s` — Maximum time for establishing connections
|
|
760
|
+
- `requestTimeout: 10m` — Maximum time for processing requests
|
|
761
|
+
|
|
762
|
+
#### Docker
|
|
763
|
+
|
|
764
|
+
When running in Docker, ensure `stop_grace_period` in `docker-compose.yml` is **greater than or equal to** the shutdown timeout:
|
|
765
|
+
|
|
766
|
+
```yaml
|
|
767
|
+
services:
|
|
768
|
+
microservice:
|
|
769
|
+
stop_grace_period: 30s # Must be >= GRACEFUL_SHUTDOWN_TIMEOUT_MS
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
If `stop_grace_period` is less than the shutdown timeout, Docker will forcefully kill the container before graceful shutdown completes.
|
|
773
|
+
|
|
774
|
+
## Library Mode
|
|
775
|
+
|
|
776
|
+
The project can be used as a standalone TypeScript library in other applications.
|
|
777
|
+
|
|
778
|
+
### Introduction
|
|
779
|
+
|
|
780
|
+
In library mode, the service runs without a NestJS HTTP server (Fastify). It provides a programmatic API to validate and publish content to various social media platforms.
|
|
781
|
+
|
|
782
|
+
Key features of library mode:
|
|
783
|
+
- **Full Isolation**: Uses only provided configuration. No environment variables are read.
|
|
784
|
+
- **Standalone DI**: Initialized manually without the overhead of a full NestJS application.
|
|
785
|
+
- **Standard API**: The same business logic as used in microservice mode.
|
|
786
|
+
- **Custom Logging**: Inject your own logger implementation.
|
|
787
|
+
|
|
788
|
+
### Installation
|
|
789
|
+
|
|
790
|
+
```bash
|
|
791
|
+
npm install social-media-posting-microservice
|
|
792
|
+
# or
|
|
793
|
+
pnpm add social-media-posting-microservice
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Quick Start
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
import { createPostingClient } from 'social-media-posting-microservice';
|
|
800
|
+
|
|
801
|
+
const client = createPostingClient({
|
|
802
|
+
accounts: {
|
|
803
|
+
marketing: {
|
|
804
|
+
platform: 'telegram',
|
|
805
|
+
auth: {
|
|
806
|
+
botToken: 'YOUR_BOT_TOKEN'
|
|
807
|
+
},
|
|
808
|
+
channelId: '@my_channel'
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const result = await client.post({
|
|
814
|
+
account: 'marketing',
|
|
815
|
+
platform: 'telegram',
|
|
816
|
+
body: 'Hello from library mode!'
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
console.log(result);
|
|
820
|
+
|
|
821
|
+
await client.destroy();
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### Library Configuration
|
|
825
|
+
|
|
826
|
+
The `createPostingClient` function accepts a `LibraryConfig` object.
|
|
827
|
+
|
|
828
|
+
#### Properties
|
|
829
|
+
|
|
830
|
+
| Property | Type | Default | Description |
|
|
831
|
+
|----------|------|---------|-------------|
|
|
832
|
+
| `accounts` | `Record<string, AccountConfig>` | **Required** | Dictionary of named accounts. |
|
|
833
|
+
| `requestTimeoutSecs` | `number` | `60` | Total timeout for a single post/preview operation. |
|
|
834
|
+
| `retryAttempts` | `number` | `3` | Number of automatic retry attempts for transient errors. |
|
|
835
|
+
| `retryDelayMs` | `number` | `1000` | Base delay between retries. |
|
|
836
|
+
| `idempotencyTtlMinutes` | `number` | `10` | How long to keep idempotency records in memory. |
|
|
837
|
+
| `logLevel` | `'debug' \| 'info' \| 'warn' \| 'error'` | `'warn'` | Logging verbosity for the default console logger. |
|
|
838
|
+
| `logger` | `ILogger` | `ConsoleLogger` | Custom logger implementation. |
|
|
839
|
+
|
|
840
|
+
#### Account Configuration
|
|
841
|
+
|
|
842
|
+
Each account must follow the `AccountConfig` interface:
|
|
843
|
+
- `platform`: `'telegram'` (more coming soon)
|
|
844
|
+
- `auth`: Platform-specific authentication (e.g., `{ botToken: string }` for Telegram)
|
|
845
|
+
- `channelId`: Target identifier (e.g., `@channel` or numeric ID)
|
|
846
|
+
- `maxBody`: (Optional) Limit for body length.
|
|
847
|
+
|
|
848
|
+
### Library API Reference
|
|
849
|
+
|
|
850
|
+
#### `client.post(request, abortSignal?)`
|
|
851
|
+
|
|
852
|
+
Publishes content to the specified account.
|
|
853
|
+
|
|
854
|
+
**Parameters:**
|
|
855
|
+
- `request`: PostRequestDto
|
|
856
|
+
- `abortSignal`: (Optional) Standard `AbortSignal` to cancel the request.
|
|
857
|
+
|
|
858
|
+
**Returns:**
|
|
859
|
+
A promise that resolves to either success data or an error object. It **does not throw** for business/platform errors.
|
|
860
|
+
|
|
861
|
+
#### `client.preview(request)`
|
|
862
|
+
|
|
863
|
+
Validates the request and performs content conversion (e.g., Markdown to HTML) without publishing.
|
|
864
|
+
|
|
865
|
+
**Parameters:**
|
|
866
|
+
- `request`: PostRequestDto
|
|
867
|
+
|
|
868
|
+
**Returns:**
|
|
869
|
+
A promise resolving to a preview result including `detectedType`, `convertedBody`, and any `warnings`.
|
|
870
|
+
|
|
871
|
+
#### `client.destroy()`
|
|
872
|
+
|
|
873
|
+
Gracefully shuts down the client, cleaning up any timers or internal resources. Always call this when finished or on application shutdown.
|
|
874
|
+
|
|
875
|
+
### Custom Logger
|
|
876
|
+
|
|
877
|
+
You can provide your own logger by implementing the `ILogger` interface:
|
|
878
|
+
|
|
879
|
+
```typescript
|
|
880
|
+
import { ILogger, createPostingClient } from 'social-media-posting-microservice';
|
|
881
|
+
|
|
882
|
+
class MyLogger implements ILogger {
|
|
883
|
+
debug(msg: string, ctx?: string) { /* ... */ }
|
|
884
|
+
log(msg: string, ctx?: string) { /* ... */ }
|
|
885
|
+
warn(msg: string, ctx?: string) { /* ... */ }
|
|
886
|
+
error(msg: string, trace?: string, ctx?: string) { /* ... */ }
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const client = createPostingClient({
|
|
890
|
+
accounts: { ... },
|
|
891
|
+
logger: new MyLogger()
|
|
892
|
+
});
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
### Isolation and Environment
|
|
896
|
+
|
|
897
|
+
When used as a library, the package is strictly isolated:
|
|
898
|
+
1. **No `process.env`**: It will NOT attempt to read `BOT_TOKEN` or any other environment variables. All secrets must be passed explicitly in the `accounts` configuration.
|
|
899
|
+
2. **No Config Files**: It will NOT try to load `config.yaml`.
|
|
900
|
+
3. **Instance Isolation**: You can create multiple clients with different configurations in the same process; they will not interfere with each other.
|
|
901
|
+
|
|
902
|
+
### Best Practices
|
|
903
|
+
|
|
904
|
+
1. **Re-use Client**: Create one client instance and reuse it for multiple posts.
|
|
905
|
+
2. **Idempotency**: Always provide an `idempotencyKey` in your post requests to prevent duplicate posts during retries or network issues.
|
|
906
|
+
3. **Graceful Shutdown**: Ensure `client.destroy()` is called to avoid memory leaks or hanging processes.
|
|
907
|
+
4. **Error Handling**: Check the `success` field in the response.
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
const result = await client.post(request);
|
|
911
|
+
if (result.success) {
|
|
912
|
+
// Use result.data
|
|
913
|
+
} else {
|
|
914
|
+
// Handle result.error
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
## Development
|
|
919
|
+
|
|
920
|
+
### Requirements
|
|
921
|
+
|
|
922
|
+
- Node.js 22+
|
|
923
|
+
- pnpm 10+
|
|
924
|
+
|
|
925
|
+
### Quick Start (Development)
|
|
926
|
+
|
|
927
|
+
```bash
|
|
928
|
+
# 1) Install dependencies
|
|
929
|
+
pnpm install
|
|
930
|
+
|
|
931
|
+
# 2) Environment (dev)
|
|
932
|
+
cp .env.development.example .env.development
|
|
933
|
+
|
|
934
|
+
# 3) Run in development mode (watch)
|
|
935
|
+
pnpm start:dev
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
- Default URL (dev): `http://localhost:8080/api/v1`
|
|
939
|
+
|
|
940
|
+
### Tests
|
|
941
|
+
|
|
942
|
+
Jest projects are separated into `unit` and `e2e`.
|
|
943
|
+
|
|
944
|
+
```bash
|
|
945
|
+
# All tests
|
|
946
|
+
pnpm test
|
|
947
|
+
|
|
948
|
+
# Unit tests
|
|
949
|
+
pnpm test:unit
|
|
950
|
+
|
|
951
|
+
# E2E tests
|
|
952
|
+
pnpm test:e2e
|
|
953
|
+
|
|
954
|
+
# Watch mode
|
|
955
|
+
pnpm test:watch
|
|
956
|
+
|
|
957
|
+
# Coverage
|
|
958
|
+
pnpm test:cov
|
|
959
|
+
|
|
960
|
+
# Debug
|
|
961
|
+
pnpm test:unit:debug
|
|
962
|
+
pnpm test:e2e:debug
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### Code Quality
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
# Lint
|
|
969
|
+
pnpm lint
|
|
970
|
+
|
|
971
|
+
# Format
|
|
972
|
+
pnpm format
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
### Build
|
|
976
|
+
|
|
977
|
+
```bash
|
|
978
|
+
# Build the application
|
|
979
|
+
pnpm build
|
|
980
|
+
|
|
981
|
+
# Build library mode
|
|
982
|
+
pnpm build:lib
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### Useful to Know
|
|
986
|
+
|
|
987
|
+
- Global `ValidationPipe` is enabled (whitelist, forbidNonWhitelisted, transform).
|
|
988
|
+
- In dev, `pino-pretty` is used with more detailed logs.
|
|
989
|
+
- In prod, auto-logging of `/health` is ignored; in dev — it's logged.
|
|
990
|
+
- Sensitive headers are redacted in logs (`authorization`, `x-api-key`).
|
|
991
|
+
- TypeScript/Jest path aliases: `@/*`, `@common/*`, `@modules/*`, `@config/*`, `@test/*`.
|
|
992
|
+
|
|
993
|
+
## Docker
|
|
994
|
+
|
|
995
|
+
```bash
|
|
996
|
+
pnpm build
|
|
997
|
+
docker build -t social-posting:latest -f docker/Dockerfile .
|
|
998
|
+
docker compose -f docker/docker-compose.yml up -d
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
## License
|
|
1002
|
+
|
|
1003
|
+
MIT
|