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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1003 -0
  3. package/dist/src/app.constants.d.ts +8 -0
  4. package/dist/src/app.constants.js +9 -0
  5. package/dist/src/app.constants.js.map +1 -0
  6. package/dist/src/common/enums/body-format.enum.d.ts +12 -0
  7. package/dist/src/common/enums/body-format.enum.js +14 -0
  8. package/dist/src/common/enums/body-format.enum.js.map +1 -0
  9. package/dist/src/common/enums/error-code.enum.d.ts +12 -0
  10. package/dist/src/common/enums/error-code.enum.js +14 -0
  11. package/dist/src/common/enums/error-code.enum.js.map +1 -0
  12. package/dist/src/common/enums/index.d.ts +3 -0
  13. package/dist/src/common/enums/index.js +4 -0
  14. package/dist/src/common/enums/index.js.map +1 -0
  15. package/dist/src/common/enums/post-type.enum.d.ts +28 -0
  16. package/dist/src/common/enums/post-type.enum.js +30 -0
  17. package/dist/src/common/enums/post-type.enum.js.map +1 -0
  18. package/dist/src/common/filters/all-exceptions.filter.d.ts +13 -0
  19. package/dist/src/common/filters/all-exceptions.filter.js +103 -0
  20. package/dist/src/common/filters/all-exceptions.filter.js.map +1 -0
  21. package/dist/src/common/helpers/media-input.helper.d.ts +73 -0
  22. package/dist/src/common/helpers/media-input.helper.js +122 -0
  23. package/dist/src/common/helpers/media-input.helper.js.map +1 -0
  24. package/dist/src/common/interceptors/shutdown.interceptor.d.ts +12 -0
  25. package/dist/src/common/interceptors/shutdown.interceptor.js +41 -0
  26. package/dist/src/common/interceptors/shutdown.interceptor.js.map +1 -0
  27. package/dist/src/common/interfaces/logger.interface.d.ts +44 -0
  28. package/dist/src/common/interfaces/logger.interface.js +44 -0
  29. package/dist/src/common/interfaces/logger.interface.js.map +1 -0
  30. package/dist/src/common/services/shutdown.module.d.ts +2 -0
  31. package/dist/src/common/services/shutdown.module.js +18 -0
  32. package/dist/src/common/services/shutdown.module.js.map +1 -0
  33. package/dist/src/common/services/shutdown.service.d.ts +40 -0
  34. package/dist/src/common/services/shutdown.service.js +122 -0
  35. package/dist/src/common/services/shutdown.service.js.map +1 -0
  36. package/dist/src/common/types/index.d.ts +1 -0
  37. package/dist/src/common/types/index.js +2 -0
  38. package/dist/src/common/types/index.js.map +1 -0
  39. package/dist/src/common/types/media-input.type.d.ts +29 -0
  40. package/dist/src/common/types/media-input.type.js +2 -0
  41. package/dist/src/common/types/media-input.type.js.map +1 -0
  42. package/dist/src/common/validators/body-length.validator.d.ts +24 -0
  43. package/dist/src/common/validators/body-length.validator.js +57 -0
  44. package/dist/src/common/validators/body-length.validator.js.map +1 -0
  45. package/dist/src/common/validators/channel-id.validator.d.ts +19 -0
  46. package/dist/src/common/validators/channel-id.validator.js +58 -0
  47. package/dist/src/common/validators/channel-id.validator.js.map +1 -0
  48. package/dist/src/common/validators/has-content.validator.d.ts +23 -0
  49. package/dist/src/common/validators/has-content.validator.js +57 -0
  50. package/dist/src/common/validators/has-content.validator.js.map +1 -0
  51. package/dist/src/common/validators/media-input.validator.d.ts +44 -0
  52. package/dist/src/common/validators/media-input.validator.js +112 -0
  53. package/dist/src/common/validators/media-input.validator.js.map +1 -0
  54. package/dist/src/common/validators/media-priority.validator.d.ts +19 -0
  55. package/dist/src/common/validators/media-priority.validator.js +38 -0
  56. package/dist/src/common/validators/media-priority.validator.js.map +1 -0
  57. package/dist/src/config/app.config.d.ts +33 -0
  58. package/dist/src/config/app.config.js +83 -0
  59. package/dist/src/config/app.config.js.map +1 -0
  60. package/dist/src/config/library.config.d.ts +51 -0
  61. package/dist/src/config/library.config.js +197 -0
  62. package/dist/src/config/library.config.js.map +1 -0
  63. package/dist/src/config/yaml-config.dto.d.ts +37 -0
  64. package/dist/src/config/yaml-config.dto.js +152 -0
  65. package/dist/src/config/yaml-config.dto.js.map +1 -0
  66. package/dist/src/config/yaml.config.d.ts +14 -0
  67. package/dist/src/config/yaml.config.js +72 -0
  68. package/dist/src/config/yaml.config.js.map +1 -0
  69. package/dist/src/index.d.ts +19 -0
  70. package/dist/src/index.js +17 -0
  71. package/dist/src/index.js.map +1 -0
  72. package/dist/src/library.d.ts +57 -0
  73. package/dist/src/library.js +92 -0
  74. package/dist/src/library.js.map +1 -0
  75. package/dist/src/modules/app-config/app-config.module.d.ts +2 -0
  76. package/dist/src/modules/app-config/app-config.module.js +26 -0
  77. package/dist/src/modules/app-config/app-config.module.js.map +1 -0
  78. package/dist/src/modules/app-config/app-config.service.d.ts +14 -0
  79. package/dist/src/modules/app-config/app-config.service.js +18 -0
  80. package/dist/src/modules/app-config/app-config.service.js.map +1 -0
  81. package/dist/src/modules/app-config/interfaces/app-config.interface.d.ts +31 -0
  82. package/dist/src/modules/app-config/interfaces/app-config.interface.js +2 -0
  83. package/dist/src/modules/app-config/interfaces/app-config.interface.js.map +1 -0
  84. package/dist/src/modules/app-config/nest-config.service.d.ts +41 -0
  85. package/dist/src/modules/app-config/nest-config.service.js +91 -0
  86. package/dist/src/modules/app-config/nest-config.service.js.map +1 -0
  87. package/dist/src/modules/health/health.controller.d.ts +12 -0
  88. package/dist/src/modules/health/health.controller.js +33 -0
  89. package/dist/src/modules/health/health.controller.js.map +1 -0
  90. package/dist/src/modules/health/health.module.d.ts +2 -0
  91. package/dist/src/modules/health/health.module.js +17 -0
  92. package/dist/src/modules/health/health.module.js.map +1 -0
  93. package/dist/src/modules/media/media.module.d.ts +2 -0
  94. package/dist/src/modules/media/media.module.js +18 -0
  95. package/dist/src/modules/media/media.module.js.map +1 -0
  96. package/dist/src/modules/media/media.service.d.ts +15 -0
  97. package/dist/src/modules/media/media.service.js +49 -0
  98. package/dist/src/modules/media/media.service.js.map +1 -0
  99. package/dist/src/modules/platforms/base/auth-validator-registry.service.d.ts +25 -0
  100. package/dist/src/modules/platforms/base/auth-validator-registry.service.js +50 -0
  101. package/dist/src/modules/platforms/base/auth-validator-registry.service.js.map +1 -0
  102. package/dist/src/modules/platforms/base/auth-validator.interface.d.ts +16 -0
  103. package/dist/src/modules/platforms/base/auth-validator.interface.js +2 -0
  104. package/dist/src/modules/platforms/base/auth-validator.interface.js.map +1 -0
  105. package/dist/src/modules/platforms/base/index.d.ts +4 -0
  106. package/dist/src/modules/platforms/base/index.js +5 -0
  107. package/dist/src/modules/platforms/base/index.js.map +1 -0
  108. package/dist/src/modules/platforms/base/platform-registry.service.d.ts +31 -0
  109. package/dist/src/modules/platforms/base/platform-registry.service.js +54 -0
  110. package/dist/src/modules/platforms/base/platform-registry.service.js.map +1 -0
  111. package/dist/src/modules/platforms/base/platform.interface.d.ts +39 -0
  112. package/dist/src/modules/platforms/base/platform.interface.js +2 -0
  113. package/dist/src/modules/platforms/base/platform.interface.js.map +1 -0
  114. package/dist/src/modules/platforms/platforms.module.d.ts +13 -0
  115. package/dist/src/modules/platforms/platforms.module.js +59 -0
  116. package/dist/src/modules/platforms/platforms.module.js.map +1 -0
  117. package/dist/src/modules/platforms/telegram/telegram-auth.validator.d.ts +19 -0
  118. package/dist/src/modules/platforms/telegram/telegram-auth.validator.js +51 -0
  119. package/dist/src/modules/platforms/telegram/telegram-auth.validator.js.map +1 -0
  120. package/dist/src/modules/platforms/telegram/telegram-type-detector.service.d.ts +18 -0
  121. package/dist/src/modules/platforms/telegram/telegram-type-detector.service.js +47 -0
  122. package/dist/src/modules/platforms/telegram/telegram-type-detector.service.js.map +1 -0
  123. package/dist/src/modules/platforms/telegram/telegram.platform.d.ts +58 -0
  124. package/dist/src/modules/platforms/telegram/telegram.platform.js +434 -0
  125. package/dist/src/modules/platforms/telegram/telegram.platform.js.map +1 -0
  126. package/dist/src/modules/post/base-post.service.d.ts +64 -0
  127. package/dist/src/modules/post/base-post.service.js +99 -0
  128. package/dist/src/modules/post/base-post.service.js.map +1 -0
  129. package/dist/src/modules/post/dto/index.d.ts +3 -0
  130. package/dist/src/modules/post/dto/index.js +4 -0
  131. package/dist/src/modules/post/dto/index.js.map +1 -0
  132. package/dist/src/modules/post/dto/post-request.dto.d.ts +56 -0
  133. package/dist/src/modules/post/dto/post-request.dto.js +195 -0
  134. package/dist/src/modules/post/dto/post-request.dto.js.map +1 -0
  135. package/dist/src/modules/post/dto/post-response.dto.d.ts +41 -0
  136. package/dist/src/modules/post/dto/post-response.dto.js +2 -0
  137. package/dist/src/modules/post/dto/post-response.dto.js.map +1 -0
  138. package/dist/src/modules/post/dto/preview-response.dto.d.ts +33 -0
  139. package/dist/src/modules/post/dto/preview-response.dto.js +2 -0
  140. package/dist/src/modules/post/dto/preview-response.dto.js.map +1 -0
  141. package/dist/src/modules/post/idempotency.service.d.ts +95 -0
  142. package/dist/src/modules/post/idempotency.service.js +229 -0
  143. package/dist/src/modules/post/idempotency.service.js.map +1 -0
  144. package/dist/src/modules/post/post.controller.d.ts +13 -0
  145. package/dist/src/modules/post/post.controller.js +97 -0
  146. package/dist/src/modules/post/post.controller.js.map +1 -0
  147. package/dist/src/modules/post/post.module.d.ts +2 -0
  148. package/dist/src/modules/post/post.module.js +25 -0
  149. package/dist/src/modules/post/post.module.js.map +1 -0
  150. package/dist/src/modules/post/post.service.d.ts +62 -0
  151. package/dist/src/modules/post/post.service.js +325 -0
  152. package/dist/src/modules/post/post.service.js.map +1 -0
  153. package/dist/src/modules/post/preview.service.d.ts +23 -0
  154. package/dist/src/modules/post/preview.service.js +69 -0
  155. package/dist/src/modules/post/preview.service.js.map +1 -0
  156. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  157. 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