返回 AiToEarn
publish-content.utils.ts
根目录 / project / aitoearn-backend / apps / aitoearn-server / src / core / channels / utils / publish-content.utils.ts
1 import type { PlatformContentLimits, TopicCapability } from '../platforms/platforms.interface'
2 import type { PublishContentInput } from '../publish/schemas/publish-content.schema'
3 import { PublishContentMode, PublishMediaType } from '../platforms/platforms.interface'
4 import { hasUrlPathExtension } from '../platforms/platforms.utils'
5 import {
6 parseTopicsFromBody,
7 PublishValidationCombination,
8 PublishValidationField,
9 PublishValidationIssue,
10 PublishValidationIssueCode,
11 stripTopicsFromBody,
12 } from '../platforms/publish.schema'
13
14 function isImageExtension(url: string): boolean {
15 return hasUrlPathExtension(url, ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff'])
16 }
17
18 function isVideoExtension(url: string): boolean {
19 return hasUrlPathExtension(url, ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.flv', '.wmv', '.rmvb', '.3gp'])
20 }
21
22 function getMediaType(media: { url: string, metadata?: { type?: unknown } }): PublishMediaType | undefined {
23 const metadataType = media.metadata?.['type']
24 if (metadataType === PublishMediaType.Image || metadataType === PublishMediaType.Video) {
25 return metadataType
26 }
27 if (isImageExtension(media.url)) {
28 return PublishMediaType.Image
29 }
30 if (isVideoExtension(media.url)) {
31 return PublishMediaType.Video
32 }
33 return undefined
34 }
35
36 export function validatePublishContent(
37 content: PublishContentInput,
38 limits: PlatformContentLimits,
39 topic?: TopicCapability,
40 ): PublishValidationIssue[] {
41 const issues: PublishValidationIssue[] = []
42 const mediaTypes = content.media.map(getMediaType)
43 const imageCount = mediaTypes.filter(type => type === PublishMediaType.Image).length
44 const videoCount = mediaTypes.filter(type => type === PublishMediaType.Video).length
45 const bodyForLength = topic?.nativeField ? stripTopicsFromBody(content.body) : content.body
46
47 // Required check
48 if (!content.body && !content.title && !content.media.length) {
49 issues.push({
50 code: PublishValidationIssueCode.Required,
51 path: ['content'],
52 params: { field: PublishValidationField.Post },
53 })
54 }
55
56 if (!(imageCount > 0 && videoCount > 0)) {
57 const mode = resolveContentMode(content, videoCount)
58 if (mode && !limits.modes.includes(mode)) {
59 issues.push({
60 code: PublishValidationIssueCode.UnsupportedContentMode,
61 path: ['content'],
62 params: { field: PublishValidationField.Post, mode },
63 })
64 }
65 }
66
67 // Title length
68 if (limits.maxTitleLength !== undefined && content.title && content.title.length > limits.maxTitleLength) {
69 issues.push({
70 code: PublishValidationIssueCode.TooBig,
71 path: ['content', 'title'],
72 params: { field: PublishValidationField.Title, current: content.title.length, maximum: limits.maxTitleLength, unit: 'characters' },
73 })
74 }
75
76 // Body length
77 if (limits.maxBodyLength !== undefined && bodyForLength && bodyForLength.length > limits.maxBodyLength) {
78 issues.push({
79 code: PublishValidationIssueCode.TooBig,
80 path: ['content', 'body'],
81 params: { field: PublishValidationField.Text, current: bodyForLength.length, maximum: limits.maxBodyLength, unit: 'characters' },
82 })
83 }
84
85 // Total text length
86 if (limits.maxTotalTextLength !== undefined) {
87 const totalTextLength = (content.title?.length ?? 0) + (bodyForLength?.length ?? 0)
88 if (totalTextLength > limits.maxTotalTextLength) {
89 issues.push({
90 code: PublishValidationIssueCode.TooBig,
91 path: ['content'],
92 params: { field: PublishValidationField.Text, current: totalTextLength, maximum: limits.maxTotalTextLength, unit: 'characters' },
93 })
94 }
95 }
96
97 // Media count checks
98 if (imageCount > 0 && videoCount > 0) {
99 issues.push({
100 code: PublishValidationIssueCode.InvalidCombination,
101 path: ['content', 'media'],
102 params: { combination: PublishValidationCombination.ImageVideo },
103 })
104 }
105
106 if (limits.maxImages !== undefined && imageCount > limits.maxImages) {
107 issues.push({
108 code: PublishValidationIssueCode.TooBig,
109 path: ['content', 'media'],
110 params: { field: PublishValidationField.Image, current: imageCount, maximum: limits.maxImages, unit: 'items' },
111 })
112 }
113
114 if (limits.maxVideos !== undefined && videoCount > limits.maxVideos) {
115 issues.push({
116 code: PublishValidationIssueCode.TooBig,
117 path: ['content', 'media'],
118 params: { field: PublishValidationField.Video, current: videoCount, maximum: limits.maxVideos, unit: 'items' },
119 })
120 }
121
122 if (limits.maxMediaCount !== undefined && content.media.length > limits.maxMediaCount) {
123 issues.push({
124 code: PublishValidationIssueCode.TooBig,
125 path: ['content', 'media'],
126 params: { field: PublishValidationField.Media, current: content.media.length, maximum: limits.maxMediaCount, unit: 'items' },
127 })
128 }
129
130 if (topic?.supported && topic.maxCount !== undefined) {
131 const topics = parseTopicsFromBody(content.body)
132 if (topics.length > topic.maxCount) {
133 issues.push({
134 code: PublishValidationIssueCode.TooBig,
135 path: ['content', 'topics'],
136 params: { field: PublishValidationField.Topic, current: topics.length, maximum: topic.maxCount, unit: 'items' },
137 })
138 }
139 }
140
141 // Cover format
142 if (content.cover?.url && getMediaType(content.cover) !== PublishMediaType.Image && !isImageExtension(content.cover.url)) {
143 issues.push({
144 code: PublishValidationIssueCode.UnsupportedFormat,
145 path: ['content', 'cover'],
146 params: { field: PublishValidationField.Cover, format: getExtension(content.cover.url) },
147 })
148 }
149
150 return issues
151 }
152
153 function resolveContentMode(content: PublishContentInput, videoCount: number): PublishContentMode | undefined {
154 if (videoCount > 0) {
155 return PublishContentMode.Video
156 }
157 if (content.media.length > 0 || content.cover?.url) {
158 return PublishContentMode.ImageText
159 }
160 if (content.title || content.body) {
161 return PublishContentMode.Text
162 }
163 return undefined
164 }
165
166 function getExtension(url: string): string {
167 try {
168 const pathname = new URL(url).pathname
169 const dot = pathname.lastIndexOf('.')
170 return dot >= 0 ? pathname.slice(dot) : 'unknown'
171 }
172 catch {
173 return 'unknown'
174 }
175 }
176
176 lines TYPESCRIPT