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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PostType } from '../../../common/enums/index.js';
|
|
2
|
+
import type { MediaInput } from '../../../common/types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Post request DTO
|
|
5
|
+
* Contains all data needed to publish a post to a social media platform
|
|
6
|
+
*/
|
|
7
|
+
export declare class PostRequestDto {
|
|
8
|
+
/** Target social media platform (e.g., 'telegram') */
|
|
9
|
+
platform: string;
|
|
10
|
+
/** Post content/text body (optional if media is provided) */
|
|
11
|
+
body?: string;
|
|
12
|
+
/** Post type (auto-detected if not specified) */
|
|
13
|
+
type?: PostType;
|
|
14
|
+
/**
|
|
15
|
+
* Format of the body content.
|
|
16
|
+
* Standard values: 'text', 'html', 'md'
|
|
17
|
+
* Platform-specific values (e.g., 'MarkdownV2' for Telegram) are also supported
|
|
18
|
+
*/
|
|
19
|
+
bodyFormat?: string;
|
|
20
|
+
/** Post title (used by platforms that support it, max 1000 characters) */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** Post description/summary (used by platforms that support it, max 5000 characters) */
|
|
23
|
+
description?: string;
|
|
24
|
+
/** Cover image (for image posts or article thumbnails) */
|
|
25
|
+
cover?: MediaInput;
|
|
26
|
+
/** Video file (for video posts) */
|
|
27
|
+
video?: MediaInput;
|
|
28
|
+
/** Audio file (for audio posts) */
|
|
29
|
+
audio?: MediaInput;
|
|
30
|
+
/** Document file (for document posts) */
|
|
31
|
+
document?: MediaInput;
|
|
32
|
+
/** Multiple media files (for album/gallery posts) */
|
|
33
|
+
media?: MediaInput[];
|
|
34
|
+
/** Named account from configuration */
|
|
35
|
+
account?: string;
|
|
36
|
+
/** Platform-agnostic channel/chat identifier (e.g., @mychannel, -100123456789, or 123456789) */
|
|
37
|
+
channelId?: string | number;
|
|
38
|
+
/** Inline authentication credentials (alternative to account) */
|
|
39
|
+
auth?: Record<string, any>;
|
|
40
|
+
/** Platform-specific options */
|
|
41
|
+
options?: Record<string, any>;
|
|
42
|
+
/** Disable notification (silent message) */
|
|
43
|
+
disableNotification?: boolean;
|
|
44
|
+
/** Post tags/hashtags (max 200 items, each max 300 characters) */
|
|
45
|
+
tags?: string[];
|
|
46
|
+
/** Scheduled publication time (ISO 8601 format, max 50 characters) */
|
|
47
|
+
scheduledAt?: string;
|
|
48
|
+
/** Post language code (e.g., 'en', 'ru', max 50 characters) */
|
|
49
|
+
postLanguage?: string;
|
|
50
|
+
/** Publication mode: publish immediately or save as draft */
|
|
51
|
+
mode?: 'publish' | 'draft';
|
|
52
|
+
/** Idempotency key to prevent duplicate posts (max 1000 characters) */
|
|
53
|
+
idempotencyKey?: string;
|
|
54
|
+
/** Maximum body length override (max 500,000 characters) */
|
|
55
|
+
maxBody?: number;
|
|
56
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { ArrayMaxSize, IsArray, IsBoolean, IsEnum, IsNumber, IsObject, IsOptional, IsString, Max, MaxLength, Min, } from 'class-validator';
|
|
11
|
+
import { PostType } from '../../../common/enums/index.js';
|
|
12
|
+
import { IsMediaInput, IsMediaInputArray, } from '../../../common/validators/media-input.validator.js';
|
|
13
|
+
import { IsValidBodyLength, MAX_BODY_LIMIT, } from '../../../common/validators/body-length.validator.js';
|
|
14
|
+
import { IsChannelId } from '../../../common/validators/channel-id.validator.js';
|
|
15
|
+
import { HasContent } from '../../../common/validators/has-content.validator.js';
|
|
16
|
+
/**
|
|
17
|
+
* Post request DTO
|
|
18
|
+
* Contains all data needed to publish a post to a social media platform
|
|
19
|
+
*/
|
|
20
|
+
let PostRequestDto = class PostRequestDto {
|
|
21
|
+
/** Target social media platform (e.g., 'telegram') */
|
|
22
|
+
platform;
|
|
23
|
+
/** Post content/text body (optional if media is provided) */
|
|
24
|
+
body;
|
|
25
|
+
/** Post type (auto-detected if not specified) */
|
|
26
|
+
type = PostType.AUTO;
|
|
27
|
+
/**
|
|
28
|
+
* Format of the body content.
|
|
29
|
+
* Standard values: 'text', 'html', 'md'
|
|
30
|
+
* Platform-specific values (e.g., 'MarkdownV2' for Telegram) are also supported
|
|
31
|
+
*/
|
|
32
|
+
bodyFormat = 'text';
|
|
33
|
+
/** Post title (used by platforms that support it, max 1000 characters) */
|
|
34
|
+
title;
|
|
35
|
+
/** Post description/summary (used by platforms that support it, max 5000 characters) */
|
|
36
|
+
description;
|
|
37
|
+
/** Cover image (for image posts or article thumbnails) */
|
|
38
|
+
cover;
|
|
39
|
+
/** Video file (for video posts) */
|
|
40
|
+
video;
|
|
41
|
+
/** Audio file (for audio posts) */
|
|
42
|
+
audio;
|
|
43
|
+
/** Document file (for document posts) */
|
|
44
|
+
document;
|
|
45
|
+
/** Multiple media files (for album/gallery posts) */
|
|
46
|
+
media;
|
|
47
|
+
/** Named account from configuration */
|
|
48
|
+
account;
|
|
49
|
+
/** Platform-agnostic channel/chat identifier (e.g., @mychannel, -100123456789, or 123456789) */
|
|
50
|
+
channelId;
|
|
51
|
+
/** Inline authentication credentials (alternative to account) */
|
|
52
|
+
auth;
|
|
53
|
+
/** Platform-specific options */
|
|
54
|
+
options;
|
|
55
|
+
/** Disable notification (silent message) */
|
|
56
|
+
disableNotification;
|
|
57
|
+
/** Post tags/hashtags (max 200 items, each max 300 characters) */
|
|
58
|
+
tags;
|
|
59
|
+
/** Scheduled publication time (ISO 8601 format, max 50 characters) */
|
|
60
|
+
scheduledAt;
|
|
61
|
+
/** Post language code (e.g., 'en', 'ru', max 50 characters) */
|
|
62
|
+
postLanguage;
|
|
63
|
+
/** Publication mode: publish immediately or save as draft */
|
|
64
|
+
mode;
|
|
65
|
+
/** Idempotency key to prevent duplicate posts (max 1000 characters) */
|
|
66
|
+
idempotencyKey;
|
|
67
|
+
/** Maximum body length override (max 500,000 characters) */
|
|
68
|
+
maxBody;
|
|
69
|
+
};
|
|
70
|
+
__decorate([
|
|
71
|
+
IsString(),
|
|
72
|
+
__metadata("design:type", String)
|
|
73
|
+
], PostRequestDto.prototype, "platform", void 0);
|
|
74
|
+
__decorate([
|
|
75
|
+
IsOptional(),
|
|
76
|
+
IsString(),
|
|
77
|
+
IsValidBodyLength(),
|
|
78
|
+
__metadata("design:type", String)
|
|
79
|
+
], PostRequestDto.prototype, "body", void 0);
|
|
80
|
+
__decorate([
|
|
81
|
+
IsOptional(),
|
|
82
|
+
IsEnum(PostType),
|
|
83
|
+
__metadata("design:type", String)
|
|
84
|
+
], PostRequestDto.prototype, "type", void 0);
|
|
85
|
+
__decorate([
|
|
86
|
+
IsOptional(),
|
|
87
|
+
IsString(),
|
|
88
|
+
MaxLength(50),
|
|
89
|
+
__metadata("design:type", String)
|
|
90
|
+
], PostRequestDto.prototype, "bodyFormat", void 0);
|
|
91
|
+
__decorate([
|
|
92
|
+
IsOptional(),
|
|
93
|
+
IsString(),
|
|
94
|
+
MaxLength(1000),
|
|
95
|
+
__metadata("design:type", String)
|
|
96
|
+
], PostRequestDto.prototype, "title", void 0);
|
|
97
|
+
__decorate([
|
|
98
|
+
IsOptional(),
|
|
99
|
+
IsString(),
|
|
100
|
+
MaxLength(5000),
|
|
101
|
+
__metadata("design:type", String)
|
|
102
|
+
], PostRequestDto.prototype, "description", void 0);
|
|
103
|
+
__decorate([
|
|
104
|
+
IsOptional(),
|
|
105
|
+
IsMediaInput(),
|
|
106
|
+
__metadata("design:type", Object)
|
|
107
|
+
], PostRequestDto.prototype, "cover", void 0);
|
|
108
|
+
__decorate([
|
|
109
|
+
IsOptional(),
|
|
110
|
+
IsMediaInput(),
|
|
111
|
+
__metadata("design:type", Object)
|
|
112
|
+
], PostRequestDto.prototype, "video", void 0);
|
|
113
|
+
__decorate([
|
|
114
|
+
IsOptional(),
|
|
115
|
+
IsMediaInput(),
|
|
116
|
+
__metadata("design:type", Object)
|
|
117
|
+
], PostRequestDto.prototype, "audio", void 0);
|
|
118
|
+
__decorate([
|
|
119
|
+
IsOptional(),
|
|
120
|
+
IsMediaInput(),
|
|
121
|
+
__metadata("design:type", Object)
|
|
122
|
+
], PostRequestDto.prototype, "document", void 0);
|
|
123
|
+
__decorate([
|
|
124
|
+
IsOptional(),
|
|
125
|
+
IsMediaInputArray(),
|
|
126
|
+
__metadata("design:type", Array)
|
|
127
|
+
], PostRequestDto.prototype, "media", void 0);
|
|
128
|
+
__decorate([
|
|
129
|
+
IsOptional(),
|
|
130
|
+
IsString(),
|
|
131
|
+
__metadata("design:type", String)
|
|
132
|
+
], PostRequestDto.prototype, "account", void 0);
|
|
133
|
+
__decorate([
|
|
134
|
+
IsOptional(),
|
|
135
|
+
IsChannelId(),
|
|
136
|
+
__metadata("design:type", Object)
|
|
137
|
+
], PostRequestDto.prototype, "channelId", void 0);
|
|
138
|
+
__decorate([
|
|
139
|
+
IsOptional(),
|
|
140
|
+
IsObject(),
|
|
141
|
+
__metadata("design:type", Object)
|
|
142
|
+
], PostRequestDto.prototype, "auth", void 0);
|
|
143
|
+
__decorate([
|
|
144
|
+
IsOptional(),
|
|
145
|
+
IsObject(),
|
|
146
|
+
__metadata("design:type", Object)
|
|
147
|
+
], PostRequestDto.prototype, "options", void 0);
|
|
148
|
+
__decorate([
|
|
149
|
+
IsOptional(),
|
|
150
|
+
IsBoolean(),
|
|
151
|
+
__metadata("design:type", Boolean)
|
|
152
|
+
], PostRequestDto.prototype, "disableNotification", void 0);
|
|
153
|
+
__decorate([
|
|
154
|
+
IsOptional(),
|
|
155
|
+
IsArray(),
|
|
156
|
+
ArrayMaxSize(200),
|
|
157
|
+
IsString({ each: true }),
|
|
158
|
+
MaxLength(300, { each: true }),
|
|
159
|
+
__metadata("design:type", Array)
|
|
160
|
+
], PostRequestDto.prototype, "tags", void 0);
|
|
161
|
+
__decorate([
|
|
162
|
+
IsOptional(),
|
|
163
|
+
IsString(),
|
|
164
|
+
MaxLength(50),
|
|
165
|
+
__metadata("design:type", String)
|
|
166
|
+
], PostRequestDto.prototype, "scheduledAt", void 0);
|
|
167
|
+
__decorate([
|
|
168
|
+
IsOptional(),
|
|
169
|
+
IsString(),
|
|
170
|
+
MaxLength(50),
|
|
171
|
+
__metadata("design:type", String)
|
|
172
|
+
], PostRequestDto.prototype, "postLanguage", void 0);
|
|
173
|
+
__decorate([
|
|
174
|
+
IsOptional(),
|
|
175
|
+
IsEnum(['publish', 'draft']),
|
|
176
|
+
__metadata("design:type", String)
|
|
177
|
+
], PostRequestDto.prototype, "mode", void 0);
|
|
178
|
+
__decorate([
|
|
179
|
+
IsOptional(),
|
|
180
|
+
IsString(),
|
|
181
|
+
MaxLength(1000),
|
|
182
|
+
__metadata("design:type", String)
|
|
183
|
+
], PostRequestDto.prototype, "idempotencyKey", void 0);
|
|
184
|
+
__decorate([
|
|
185
|
+
IsOptional(),
|
|
186
|
+
IsNumber(),
|
|
187
|
+
Min(1),
|
|
188
|
+
Max(MAX_BODY_LIMIT),
|
|
189
|
+
__metadata("design:type", Number)
|
|
190
|
+
], PostRequestDto.prototype, "maxBody", void 0);
|
|
191
|
+
PostRequestDto = __decorate([
|
|
192
|
+
HasContent()
|
|
193
|
+
], PostRequestDto);
|
|
194
|
+
export { PostRequestDto };
|
|
195
|
+
//# sourceMappingURL=post-request.dto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-request.dto.js","sourceRoot":"","sources":["../../../../../src/modules/post/dto/post-request.dto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PostType } from '../../../common/enums/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Successful post publication response
|
|
4
|
+
*/
|
|
5
|
+
export interface PostResponseDto {
|
|
6
|
+
success: true;
|
|
7
|
+
data: {
|
|
8
|
+
/** Platform-specific post ID */
|
|
9
|
+
postId: string;
|
|
10
|
+
/** Public URL to the post (if available) */
|
|
11
|
+
url?: string;
|
|
12
|
+
/** Platform name */
|
|
13
|
+
platform: string;
|
|
14
|
+
/** Actual post type used */
|
|
15
|
+
type: PostType;
|
|
16
|
+
/** Publication timestamp (ISO 8601) */
|
|
17
|
+
publishedAt: string;
|
|
18
|
+
/** Raw response from platform API */
|
|
19
|
+
raw?: Record<string, any>;
|
|
20
|
+
/** Unique request identifier for tracking */
|
|
21
|
+
requestId: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Error response for failed post publication
|
|
26
|
+
*/
|
|
27
|
+
export interface ErrorResponseDto {
|
|
28
|
+
success: false;
|
|
29
|
+
error: {
|
|
30
|
+
/** Error code for categorization */
|
|
31
|
+
code: string;
|
|
32
|
+
/** Human-readable error message */
|
|
33
|
+
message: string;
|
|
34
|
+
/** Additional error details */
|
|
35
|
+
details?: Record<string, any>;
|
|
36
|
+
/** Raw error response from platform API */
|
|
37
|
+
raw?: Record<string, any>;
|
|
38
|
+
/** Unique request identifier for tracking */
|
|
39
|
+
requestId: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-response.dto.js","sourceRoot":"","sources":["../../../../../src/modules/post/dto/post-response.dto.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PostType } from '../../../common/enums/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Successful preview response
|
|
4
|
+
*/
|
|
5
|
+
export interface PreviewResponseDto {
|
|
6
|
+
success: true;
|
|
7
|
+
data: {
|
|
8
|
+
valid: true;
|
|
9
|
+
/** Detected post type based on content */
|
|
10
|
+
detectedType: PostType;
|
|
11
|
+
/** Body content after conversion */
|
|
12
|
+
convertedBody?: string;
|
|
13
|
+
/** Target format after conversion */
|
|
14
|
+
targetFormat: string;
|
|
15
|
+
/** Length of converted body */
|
|
16
|
+
convertedBodyLength?: number;
|
|
17
|
+
/** Validation warnings (non-blocking) */
|
|
18
|
+
warnings: string[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Preview error response for invalid requests
|
|
23
|
+
*/
|
|
24
|
+
export interface PreviewErrorResponseDto {
|
|
25
|
+
success: false;
|
|
26
|
+
data: {
|
|
27
|
+
valid: false;
|
|
28
|
+
/** Validation errors (blocking) */
|
|
29
|
+
errors: string[];
|
|
30
|
+
/** Validation warnings (non-blocking) */
|
|
31
|
+
warnings: string[];
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-response.dto.js","sourceRoot":"","sources":["../../../../../src/modules/post/dto/preview-response.dto.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { Cache } from '@nestjs/cache-manager';
|
|
3
|
+
import { AppConfigService } from '../app-config/app-config.service.js';
|
|
4
|
+
import { PostRequestDto, PostResponseDto, ErrorResponseDto } from './dto/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Idempotency record stored in cache
|
|
7
|
+
* Tracks request processing status and cached response
|
|
8
|
+
*/
|
|
9
|
+
interface IdempotencyRecord {
|
|
10
|
+
status: 'processing' | 'completed';
|
|
11
|
+
response?: PostResponseDto | ErrorResponseDto;
|
|
12
|
+
}
|
|
13
|
+
export declare class IdempotencyService implements OnModuleDestroy {
|
|
14
|
+
private readonly cache;
|
|
15
|
+
private readonly appConfig;
|
|
16
|
+
/** Default TTL for idempotency records in cache (10 minutes) */
|
|
17
|
+
private static readonly DEFAULT_IDEMPOTENCY_TTL_MINUTES;
|
|
18
|
+
/** Cleanup interval in milliseconds (5 minutes) */
|
|
19
|
+
private static readonly CLEANUP_INTERVAL_MS;
|
|
20
|
+
private readonly logger;
|
|
21
|
+
/** In-memory map used for atomic idempotency lock management within a single process */
|
|
22
|
+
private readonly records;
|
|
23
|
+
/** Interval handle for periodic cleanup */
|
|
24
|
+
private cleanupInterval?;
|
|
25
|
+
constructor(cache: Cache, appConfig: AppConfigService);
|
|
26
|
+
/**
|
|
27
|
+
* Try to acquire processing lock for the given key or return existing record state.
|
|
28
|
+
* This method is fully in-memory and synchronous, so it is safe from race conditions
|
|
29
|
+
* inside a single Node.js process.
|
|
30
|
+
*
|
|
31
|
+
* @param key - Idempotency cache key
|
|
32
|
+
* @returns
|
|
33
|
+
* - { acquired: true, status: 'processing' } when current request becomes the owner
|
|
34
|
+
* - { acquired: false, status: 'processing' } when another request is already processing
|
|
35
|
+
* - { acquired: false, status: 'completed', response } when a completed response is cached
|
|
36
|
+
*/
|
|
37
|
+
acquireLock(key: string): {
|
|
38
|
+
acquired: true;
|
|
39
|
+
status: 'processing';
|
|
40
|
+
} | {
|
|
41
|
+
acquired: false;
|
|
42
|
+
status: 'processing';
|
|
43
|
+
} | {
|
|
44
|
+
acquired: false;
|
|
45
|
+
status: 'completed';
|
|
46
|
+
response: PostResponseDto | ErrorResponseDto;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Build idempotency cache key from request
|
|
50
|
+
* Combines user-provided key with content hash for uniqueness
|
|
51
|
+
* @param request - Post request
|
|
52
|
+
* @returns Cache key or null if no idempotencyKey provided
|
|
53
|
+
*/
|
|
54
|
+
buildKey(request: PostRequestDto): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Get idempotency record from cache
|
|
57
|
+
* @param key - Idempotency cache key
|
|
58
|
+
* @returns Cached record or undefined if not found
|
|
59
|
+
*/
|
|
60
|
+
getRecord(key: string): Promise<IdempotencyRecord | undefined>;
|
|
61
|
+
/**
|
|
62
|
+
* Mark request as processing in cache
|
|
63
|
+
* Prevents duplicate processing of the same request
|
|
64
|
+
* @param key - Idempotency cache key
|
|
65
|
+
*/
|
|
66
|
+
setProcessing(key: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Store completed request response in cache
|
|
69
|
+
* @param key - Idempotency cache key
|
|
70
|
+
* @param response - Response to cache (success or error)
|
|
71
|
+
*/
|
|
72
|
+
setCompleted(key: string, response: PostResponseDto | ErrorResponseDto): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Get TTL for idempotency records in milliseconds
|
|
75
|
+
* Uses configured value or default if not set
|
|
76
|
+
* @returns TTL in milliseconds
|
|
77
|
+
*/
|
|
78
|
+
private getTtlMs;
|
|
79
|
+
/**
|
|
80
|
+
* Start periodic cleanup of expired records
|
|
81
|
+
* Runs every 5 minutes to prevent memory leaks
|
|
82
|
+
*/
|
|
83
|
+
private startPeriodicCleanup;
|
|
84
|
+
/**
|
|
85
|
+
* Cleanup expired records from in-memory map
|
|
86
|
+
* Called periodically to prevent memory leaks
|
|
87
|
+
*/
|
|
88
|
+
private cleanupExpiredRecords;
|
|
89
|
+
/**
|
|
90
|
+
* Called by NestJS when module is being destroyed
|
|
91
|
+
* Cleanup interval and clear all records
|
|
92
|
+
*/
|
|
93
|
+
onModuleDestroy(): void;
|
|
94
|
+
}
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
var IdempotencyService_1;
|
|
14
|
+
import { Injectable, Inject, Logger } from '@nestjs/common';
|
|
15
|
+
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
|
|
16
|
+
import { createHash } from 'crypto';
|
|
17
|
+
import { AppConfigService } from '../app-config/app-config.service.js';
|
|
18
|
+
let IdempotencyService = class IdempotencyService {
|
|
19
|
+
static { IdempotencyService_1 = this; }
|
|
20
|
+
cache;
|
|
21
|
+
appConfig;
|
|
22
|
+
/** Default TTL for idempotency records in cache (10 minutes) */
|
|
23
|
+
static DEFAULT_IDEMPOTENCY_TTL_MINUTES = 10;
|
|
24
|
+
/** Cleanup interval in milliseconds (5 minutes) */
|
|
25
|
+
static CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
|
26
|
+
logger = new Logger(IdempotencyService_1.name);
|
|
27
|
+
/** In-memory map used for atomic idempotency lock management within a single process */
|
|
28
|
+
records = new Map();
|
|
29
|
+
/** Interval handle for periodic cleanup */
|
|
30
|
+
cleanupInterval;
|
|
31
|
+
constructor(cache, appConfig) {
|
|
32
|
+
this.cache = cache;
|
|
33
|
+
this.appConfig = appConfig;
|
|
34
|
+
// Start periodic cleanup of expired records
|
|
35
|
+
this.startPeriodicCleanup();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Try to acquire processing lock for the given key or return existing record state.
|
|
39
|
+
* This method is fully in-memory and synchronous, so it is safe from race conditions
|
|
40
|
+
* inside a single Node.js process.
|
|
41
|
+
*
|
|
42
|
+
* @param key - Idempotency cache key
|
|
43
|
+
* @returns
|
|
44
|
+
* - { acquired: true, status: 'processing' } when current request becomes the owner
|
|
45
|
+
* - { acquired: false, status: 'processing' } when another request is already processing
|
|
46
|
+
* - { acquired: false, status: 'completed', response } when a completed response is cached
|
|
47
|
+
*/
|
|
48
|
+
acquireLock(key) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const ttlMs = this.getTtlMs();
|
|
51
|
+
const existing = this.records.get(key);
|
|
52
|
+
if (existing) {
|
|
53
|
+
if (existing.expiresAt <= now) {
|
|
54
|
+
this.records.delete(key);
|
|
55
|
+
}
|
|
56
|
+
else if (existing.status === 'processing') {
|
|
57
|
+
return { acquired: false, status: 'processing' };
|
|
58
|
+
}
|
|
59
|
+
else if (existing.status === 'completed' && existing.response) {
|
|
60
|
+
return {
|
|
61
|
+
acquired: false,
|
|
62
|
+
status: 'completed',
|
|
63
|
+
response: existing.response,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const record = {
|
|
68
|
+
status: 'processing',
|
|
69
|
+
expiresAt: now + ttlMs,
|
|
70
|
+
};
|
|
71
|
+
this.records.set(key, record);
|
|
72
|
+
return { acquired: true, status: 'processing' };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build idempotency cache key from request
|
|
76
|
+
* Combines user-provided key with content hash for uniqueness
|
|
77
|
+
* @param request - Post request
|
|
78
|
+
* @returns Cache key or null if no idempotencyKey provided
|
|
79
|
+
*/
|
|
80
|
+
buildKey(request) {
|
|
81
|
+
if (!request.idempotencyKey) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const hash = createHash('sha256')
|
|
85
|
+
.update(JSON.stringify({
|
|
86
|
+
platform: request.platform,
|
|
87
|
+
account: request.account,
|
|
88
|
+
auth: request.auth,
|
|
89
|
+
body: request.body,
|
|
90
|
+
type: request.type,
|
|
91
|
+
media: request.media,
|
|
92
|
+
video: request.video,
|
|
93
|
+
audio: request.audio,
|
|
94
|
+
document: request.document,
|
|
95
|
+
cover: request.cover,
|
|
96
|
+
options: request.options,
|
|
97
|
+
}))
|
|
98
|
+
.digest('hex');
|
|
99
|
+
return `idempotency:${request.idempotencyKey}:${hash}`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get idempotency record from cache
|
|
103
|
+
* @param key - Idempotency cache key
|
|
104
|
+
* @returns Cached record or undefined if not found
|
|
105
|
+
*/
|
|
106
|
+
async getRecord(key) {
|
|
107
|
+
try {
|
|
108
|
+
const record = await this.cache.get(key);
|
|
109
|
+
if (record !== undefined && record !== null) {
|
|
110
|
+
return record;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Swallow cache errors and fall back to in-memory records
|
|
115
|
+
}
|
|
116
|
+
const internal = this.records.get(key);
|
|
117
|
+
if (!internal) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
if (internal.expiresAt <= Date.now()) {
|
|
121
|
+
this.records.delete(key);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
status: internal.status,
|
|
126
|
+
response: internal.response,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Mark request as processing in cache
|
|
131
|
+
* Prevents duplicate processing of the same request
|
|
132
|
+
* @param key - Idempotency cache key
|
|
133
|
+
*/
|
|
134
|
+
async setProcessing(key) {
|
|
135
|
+
const ttlMs = this.getTtlMs();
|
|
136
|
+
const record = {
|
|
137
|
+
status: 'processing',
|
|
138
|
+
expiresAt: Date.now() + ttlMs,
|
|
139
|
+
};
|
|
140
|
+
this.records.set(key, record);
|
|
141
|
+
try {
|
|
142
|
+
await this.cache.set(key, { status: 'processing' }, ttlMs);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Best-effort idempotency: ignore cache backend errors
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Store completed request response in cache
|
|
150
|
+
* @param key - Idempotency cache key
|
|
151
|
+
* @param response - Response to cache (success or error)
|
|
152
|
+
*/
|
|
153
|
+
async setCompleted(key, response) {
|
|
154
|
+
const ttlMs = this.getTtlMs();
|
|
155
|
+
const record = {
|
|
156
|
+
status: 'completed',
|
|
157
|
+
response,
|
|
158
|
+
expiresAt: Date.now() + ttlMs,
|
|
159
|
+
};
|
|
160
|
+
this.records.set(key, record);
|
|
161
|
+
try {
|
|
162
|
+
await this.cache.set(key, { status: 'completed', response }, ttlMs);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Best-effort idempotency: ignore cache backend errors
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get TTL for idempotency records in milliseconds
|
|
170
|
+
* Uses configured value or default if not set
|
|
171
|
+
* @returns TTL in milliseconds
|
|
172
|
+
*/
|
|
173
|
+
getTtlMs() {
|
|
174
|
+
const minutes = typeof this.appConfig.idempotencyTtlMinutes === 'number'
|
|
175
|
+
? this.appConfig.idempotencyTtlMinutes
|
|
176
|
+
: IdempotencyService_1.DEFAULT_IDEMPOTENCY_TTL_MINUTES;
|
|
177
|
+
return minutes * 60_000;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Start periodic cleanup of expired records
|
|
181
|
+
* Runs every 5 minutes to prevent memory leaks
|
|
182
|
+
*/
|
|
183
|
+
startPeriodicCleanup() {
|
|
184
|
+
this.cleanupInterval = setInterval(() => {
|
|
185
|
+
this.cleanupExpiredRecords();
|
|
186
|
+
}, IdempotencyService_1.CLEANUP_INTERVAL_MS);
|
|
187
|
+
// Don't prevent Node.js from exiting
|
|
188
|
+
this.cleanupInterval.unref();
|
|
189
|
+
this.logger.log(`Started periodic cleanup (interval: ${IdempotencyService_1.CLEANUP_INTERVAL_MS}ms)`, 'IdempotencyService');
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Cleanup expired records from in-memory map
|
|
193
|
+
* Called periodically to prevent memory leaks
|
|
194
|
+
*/
|
|
195
|
+
cleanupExpiredRecords() {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
let cleanedCount = 0;
|
|
198
|
+
for (const [key, record] of this.records.entries()) {
|
|
199
|
+
if (record.expiresAt <= now) {
|
|
200
|
+
this.records.delete(key);
|
|
201
|
+
cleanedCount++;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (cleanedCount > 0) {
|
|
205
|
+
this.logger.debug(`Cleaned up ${cleanedCount} expired idempotency records. Remaining: ${this.records.size}`, 'IdempotencyService');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Called by NestJS when module is being destroyed
|
|
210
|
+
* Cleanup interval and clear all records
|
|
211
|
+
*/
|
|
212
|
+
onModuleDestroy() {
|
|
213
|
+
if (this.cleanupInterval) {
|
|
214
|
+
clearInterval(this.cleanupInterval);
|
|
215
|
+
this.cleanupInterval = undefined;
|
|
216
|
+
}
|
|
217
|
+
const recordCount = this.records.size;
|
|
218
|
+
this.records.clear();
|
|
219
|
+
this.logger.log(`IdempotencyService destroyed. Cleared ${recordCount} records.`, 'IdempotencyService');
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
IdempotencyService = IdempotencyService_1 = __decorate([
|
|
223
|
+
Injectable(),
|
|
224
|
+
__param(0, Inject(CACHE_MANAGER)),
|
|
225
|
+
__metadata("design:paramtypes", [Cache,
|
|
226
|
+
AppConfigService])
|
|
227
|
+
], IdempotencyService);
|
|
228
|
+
export { IdempotencyService };
|
|
229
|
+
//# sourceMappingURL=idempotency.service.js.map
|