@happyvertical/smrt-social 0.37.1 → 0.37.3
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/dist/index.js +679 -797
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/server.js +125 -97
- package/dist/server.js.map +1 -1
- package/dist/smrt-knowledge.json +10 -10
- package/package.json +19 -19
package/dist/index.js
CHANGED
|
@@ -1,824 +1,706 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getSocialPlatformSetup } from "./server.js";
|
|
2
|
+
import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
3
|
+
import { TenantScoped, getCurrentTenant, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { SecretService } from "@happyvertical/smrt-secrets";
|
|
5
|
-
import { getSocialPlatformSetup } from "./server.js";
|
|
6
6
|
import { VideoContent } from "@happyvertical/smrt-video";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
//#region src/__smrt-register__.ts
|
|
8
|
+
ObjectRegistry.registerPackageManifest(new URL("./manifest.json", "" + import.meta.url));
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/oauth-state.ts
|
|
10
11
|
var __defProp$3 = Object.defineProperty;
|
|
11
12
|
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
|
|
12
13
|
var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (kind && result) __defProp$3(target, key, result);
|
|
18
|
-
return result;
|
|
14
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
|
|
15
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
16
|
+
if (kind && result) __defProp$3(target, key, result);
|
|
17
|
+
return result;
|
|
19
18
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const encoder = new TextEncoder();
|
|
103
|
-
const data = encoder.encode(verifier);
|
|
104
|
-
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
105
|
-
const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)));
|
|
106
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
107
|
-
}
|
|
19
|
+
var OAuthState = class extends SmrtObject {
|
|
20
|
+
tenantId = null;
|
|
21
|
+
/**
|
|
22
|
+
* Platform being connected
|
|
23
|
+
*/
|
|
24
|
+
platform = "youtube";
|
|
25
|
+
/**
|
|
26
|
+
* CSRF state token
|
|
27
|
+
* This is sent to the OAuth provider and verified on callback
|
|
28
|
+
*/
|
|
29
|
+
state = "";
|
|
30
|
+
/**
|
|
31
|
+
* PKCE code verifier
|
|
32
|
+
* Required for platforms using PKCE (YouTube, etc.)
|
|
33
|
+
*/
|
|
34
|
+
codeVerifier = null;
|
|
35
|
+
/**
|
|
36
|
+
* Redirect URI used in the OAuth request
|
|
37
|
+
* Must match on callback for verification
|
|
38
|
+
*/
|
|
39
|
+
redirectUri = "";
|
|
40
|
+
/**
|
|
41
|
+
* Requested OAuth scopes
|
|
42
|
+
*/
|
|
43
|
+
scopes = [];
|
|
44
|
+
/**
|
|
45
|
+
* When this state expires
|
|
46
|
+
* States should be short-lived (10 minutes typical)
|
|
47
|
+
*/
|
|
48
|
+
expiresAt = new Date(Date.now() + 600 * 1e3);
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
super(options);
|
|
51
|
+
if (options.platform !== void 0) this.platform = options.platform;
|
|
52
|
+
if (options.state !== void 0) this.state = options.state;
|
|
53
|
+
if (options.codeVerifier !== void 0) this.codeVerifier = options.codeVerifier;
|
|
54
|
+
if (options.redirectUri !== void 0) this.redirectUri = options.redirectUri;
|
|
55
|
+
if (options.scopes !== void 0) this.scopes = options.scopes;
|
|
56
|
+
if (options.expiresAt !== void 0) this.expiresAt = options.expiresAt;
|
|
57
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if the state has expired
|
|
61
|
+
*/
|
|
62
|
+
get isExpired() {
|
|
63
|
+
return Date.now() >= this.expiresAt.getTime();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if the state is still valid
|
|
67
|
+
*/
|
|
68
|
+
get isValid() {
|
|
69
|
+
return !this.isExpired && this.state.length > 0;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Verify a callback state matches this record
|
|
73
|
+
*/
|
|
74
|
+
verifyState(callbackState) {
|
|
75
|
+
return this.isValid && this.state === callbackState;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Generate a new random state token
|
|
79
|
+
*/
|
|
80
|
+
static generateState() {
|
|
81
|
+
return crypto.randomUUID();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Generate a PKCE code verifier
|
|
85
|
+
* Returns a 43-128 character random string
|
|
86
|
+
*/
|
|
87
|
+
static generateCodeVerifier() {
|
|
88
|
+
const array = /* @__PURE__ */ new Uint8Array(32);
|
|
89
|
+
crypto.getRandomValues(array);
|
|
90
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Generate PKCE code challenge from verifier (S256 method)
|
|
94
|
+
* Note: This requires async crypto operations
|
|
95
|
+
*/
|
|
96
|
+
static async generateCodeChallenge(verifier) {
|
|
97
|
+
const data = new TextEncoder().encode(verifier);
|
|
98
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
99
|
+
return btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
100
|
+
}
|
|
108
101
|
};
|
|
109
|
-
__decorateClass$3([
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
102
|
+
__decorateClass$3([tenantId({ nullable: true })], OAuthState.prototype, "tenantId", 2);
|
|
103
|
+
OAuthState = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
|
|
104
|
+
tableStrategy: "sti",
|
|
105
|
+
api: { include: [
|
|
106
|
+
"list",
|
|
107
|
+
"get",
|
|
108
|
+
"create",
|
|
109
|
+
"delete"
|
|
110
|
+
] },
|
|
111
|
+
mcp: false,
|
|
112
|
+
cli: true
|
|
113
|
+
})], OAuthState);
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/collections/oauth-state-collection.ts
|
|
116
|
+
var OAuthStateCollection = class extends SmrtCollection {
|
|
117
|
+
static _itemClass = OAuthState;
|
|
118
|
+
async findByState(state) {
|
|
119
|
+
return this.get({ state });
|
|
120
|
+
}
|
|
121
|
+
async findExpired(now = /* @__PURE__ */ new Date()) {
|
|
122
|
+
return this.list({
|
|
123
|
+
where: { "expiresAt <=": now },
|
|
124
|
+
orderBy: "expiresAt ASC"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async deleteExpired(now = /* @__PURE__ */ new Date()) {
|
|
128
|
+
const expired = await this.findExpired(now);
|
|
129
|
+
await Promise.all(expired.map((state) => state.delete()));
|
|
130
|
+
return expired.length;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/social-account.ts
|
|
140
135
|
var __defProp$2 = Object.defineProperty;
|
|
141
136
|
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
142
137
|
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (kind && result) __defProp$2(target, key, result);
|
|
148
|
-
return result;
|
|
138
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
139
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
140
|
+
if (kind && result) __defProp$2(target, key, result);
|
|
141
|
+
return result;
|
|
149
142
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
async getCredentials() {
|
|
373
|
-
if (!this.credentialSecretId) {
|
|
374
|
-
if (this.accessTokenSecretName || this.refreshTokenSecretName) {
|
|
375
|
-
const tenantId3 = this.requireCredentialTenantId(
|
|
376
|
-
"retrieve social account credentials"
|
|
377
|
-
);
|
|
378
|
-
const secretService2 = await SecretService.create({ db: this.db });
|
|
379
|
-
const credentials = {};
|
|
380
|
-
if (this.accessTokenSecretName) {
|
|
381
|
-
const secret2 = await secretService2.retrieveForTenant(
|
|
382
|
-
tenantId3,
|
|
383
|
-
this.accessTokenSecretName
|
|
384
|
-
);
|
|
385
|
-
credentials.accessToken = secret2.value;
|
|
386
|
-
}
|
|
387
|
-
if (this.refreshTokenSecretName) {
|
|
388
|
-
const secret2 = await secretService2.retrieveForTenant(
|
|
389
|
-
tenantId3,
|
|
390
|
-
this.refreshTokenSecretName
|
|
391
|
-
);
|
|
392
|
-
credentials.refreshToken = secret2.value;
|
|
393
|
-
}
|
|
394
|
-
return credentials;
|
|
395
|
-
}
|
|
396
|
-
if (!this.accessToken && !this.refreshToken) return null;
|
|
397
|
-
return {
|
|
398
|
-
accessToken: this.accessToken,
|
|
399
|
-
refreshToken: this.refreshToken
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
const credentialSecretId = this.credentialSecretId;
|
|
403
|
-
const tenantId2 = this.requireCredentialTenantId(
|
|
404
|
-
"retrieve social account credentials"
|
|
405
|
-
);
|
|
406
|
-
const secretService = await SecretService.create({ db: this.db });
|
|
407
|
-
const secret = await secretService.retrieveForTenant(
|
|
408
|
-
tenantId2,
|
|
409
|
-
credentialSecretId
|
|
410
|
-
);
|
|
411
|
-
return JSON.parse(secret.value);
|
|
412
|
-
}
|
|
413
|
-
async withCredentialTenantContext(fn) {
|
|
414
|
-
const tenantId2 = this.getCredentialTenantId();
|
|
415
|
-
const currentTenantId = getCurrentTenant()?.tenantId;
|
|
416
|
-
if (!tenantId2 || currentTenantId === tenantId2) {
|
|
417
|
-
return fn();
|
|
418
|
-
}
|
|
419
|
-
return withTenant({ tenantId: tenantId2 }, fn);
|
|
420
|
-
}
|
|
421
|
-
getCredentialTenantId() {
|
|
422
|
-
if (this.tenantId) {
|
|
423
|
-
return this.tenantId;
|
|
424
|
-
}
|
|
425
|
-
return getCurrentTenant()?.tenantId ?? null;
|
|
426
|
-
}
|
|
427
|
-
requireCredentialTenantId(action) {
|
|
428
|
-
const tenantId2 = this.getCredentialTenantId();
|
|
429
|
-
if (!tenantId2) {
|
|
430
|
-
throw new Error(`Tenant context required to ${action}.`);
|
|
431
|
-
}
|
|
432
|
-
return tenantId2;
|
|
433
|
-
}
|
|
143
|
+
var SocialAccount = class extends SmrtObject {
|
|
144
|
+
tenantId = null;
|
|
145
|
+
/**
|
|
146
|
+
* Human-readable name for the account
|
|
147
|
+
*/
|
|
148
|
+
name = "";
|
|
149
|
+
/**
|
|
150
|
+
* Social platform type
|
|
151
|
+
*/
|
|
152
|
+
platform = "youtube";
|
|
153
|
+
/**
|
|
154
|
+
* Platform-specific user ID
|
|
155
|
+
*/
|
|
156
|
+
platformUserId = null;
|
|
157
|
+
/**
|
|
158
|
+
* Platform username/handle
|
|
159
|
+
*/
|
|
160
|
+
platformUsername = null;
|
|
161
|
+
/**
|
|
162
|
+
* Profile URL on the platform
|
|
163
|
+
*/
|
|
164
|
+
platformUrl = null;
|
|
165
|
+
/**
|
|
166
|
+
* Deprecated raw OAuth access token.
|
|
167
|
+
* Prefer credentialSecretId/accessTokenSecretName.
|
|
168
|
+
*/
|
|
169
|
+
accessToken = null;
|
|
170
|
+
/**
|
|
171
|
+
* Deprecated raw OAuth refresh token.
|
|
172
|
+
* Prefer credentialSecretId/refreshTokenSecretName.
|
|
173
|
+
*/
|
|
174
|
+
refreshToken = null;
|
|
175
|
+
/**
|
|
176
|
+
* Secret name containing the complete platform credential payload.
|
|
177
|
+
*/
|
|
178
|
+
credentialSecretId = null;
|
|
179
|
+
/**
|
|
180
|
+
* Secret name containing only the access token.
|
|
181
|
+
*/
|
|
182
|
+
accessTokenSecretName = null;
|
|
183
|
+
/**
|
|
184
|
+
* Secret name containing only the refresh token.
|
|
185
|
+
*/
|
|
186
|
+
refreshTokenSecretName = null;
|
|
187
|
+
/**
|
|
188
|
+
* Token expiration time
|
|
189
|
+
*/
|
|
190
|
+
tokenExpiresAt = null;
|
|
191
|
+
/**
|
|
192
|
+
* Whether the account is active
|
|
193
|
+
*/
|
|
194
|
+
isActive = true;
|
|
195
|
+
/**
|
|
196
|
+
* Default hashtags to add to posts
|
|
197
|
+
*/
|
|
198
|
+
defaultHashtags = [];
|
|
199
|
+
/**
|
|
200
|
+
* Granted OAuth scopes or platform permissions.
|
|
201
|
+
*/
|
|
202
|
+
scopes = [];
|
|
203
|
+
/**
|
|
204
|
+
* Required permissions that still need app review or user grant.
|
|
205
|
+
*/
|
|
206
|
+
missingPermissions = [];
|
|
207
|
+
/**
|
|
208
|
+
* How to handle links in posts
|
|
209
|
+
* - description: Include link in post body/description
|
|
210
|
+
* - reply: Post link as a reply (better for X algorithm)
|
|
211
|
+
* - none: Don't include link
|
|
212
|
+
*/
|
|
213
|
+
linkBehavior = "description";
|
|
214
|
+
/**
|
|
215
|
+
* Safety mode for publish operations.
|
|
216
|
+
*/
|
|
217
|
+
publishMode = "dry_run";
|
|
218
|
+
/**
|
|
219
|
+
* Separate latch required before public publishing is allowed.
|
|
220
|
+
*/
|
|
221
|
+
publicPublishingAllowed = false;
|
|
222
|
+
/**
|
|
223
|
+
* Account connection status
|
|
224
|
+
*/
|
|
225
|
+
status = "connected";
|
|
226
|
+
/**
|
|
227
|
+
* Error message if status is 'error'
|
|
228
|
+
*/
|
|
229
|
+
errorMessage = null;
|
|
230
|
+
constructor(options = {}) {
|
|
231
|
+
super(options);
|
|
232
|
+
if (options.name !== void 0) this.name = options.name;
|
|
233
|
+
if (options.platform !== void 0) this.platform = options.platform;
|
|
234
|
+
if (options.platformUserId !== void 0) this.platformUserId = options.platformUserId;
|
|
235
|
+
if (options.platformUsername !== void 0) this.platformUsername = options.platformUsername;
|
|
236
|
+
if (options.platformUrl !== void 0) this.platformUrl = options.platformUrl;
|
|
237
|
+
if (options.accessToken !== void 0) this.accessToken = options.accessToken;
|
|
238
|
+
if (options.refreshToken !== void 0) this.refreshToken = options.refreshToken;
|
|
239
|
+
if (options.credentialSecretId !== void 0) this.credentialSecretId = options.credentialSecretId;
|
|
240
|
+
if (options.accessTokenSecretName !== void 0) this.accessTokenSecretName = options.accessTokenSecretName;
|
|
241
|
+
if (options.refreshTokenSecretName !== void 0) this.refreshTokenSecretName = options.refreshTokenSecretName;
|
|
242
|
+
if (options.tokenExpiresAt !== void 0) this.tokenExpiresAt = options.tokenExpiresAt;
|
|
243
|
+
if (options.isActive !== void 0) this.isActive = options.isActive;
|
|
244
|
+
if (options.defaultHashtags !== void 0) this.defaultHashtags = options.defaultHashtags;
|
|
245
|
+
if (options.scopes !== void 0) this.scopes = options.scopes;
|
|
246
|
+
if (options.missingPermissions !== void 0) this.missingPermissions = options.missingPermissions;
|
|
247
|
+
if (options.linkBehavior !== void 0) this.linkBehavior = options.linkBehavior;
|
|
248
|
+
if (options.publishMode !== void 0) this.publishMode = options.publishMode;
|
|
249
|
+
if (options.publicPublishingAllowed !== void 0) this.publicPublishingAllowed = options.publicPublishingAllowed;
|
|
250
|
+
if (options.status !== void 0) this.status = options.status;
|
|
251
|
+
if (options.errorMessage !== void 0) this.errorMessage = options.errorMessage;
|
|
252
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
253
|
+
if (options.platform !== void 0) {
|
|
254
|
+
const setup = getSocialPlatformSetup(this.platform);
|
|
255
|
+
if (options.linkBehavior === void 0) this.linkBehavior = setup.defaultLinkBehavior;
|
|
256
|
+
if (options.publishMode === void 0) this.publishMode = setup.defaultPublishMode;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Social accounts need a slug identity that is scoped by tenant and platform.
|
|
261
|
+
* A newsroom may connect `@localnews` on X, YouTube, Threads, and Facebook;
|
|
262
|
+
* the generic name-derived slug would make those accounts overwrite each
|
|
263
|
+
* other through SMRT's slug/context upsert identity.
|
|
264
|
+
*/
|
|
265
|
+
async getSlug() {
|
|
266
|
+
if (!this.slug) {
|
|
267
|
+
const identity = this.platformUserId || this.platformUsername || this.name || this.id;
|
|
268
|
+
if (!identity) return this.slug;
|
|
269
|
+
const source = [
|
|
270
|
+
this.tenantId ? `tenant-${this.tenantId}` : "no-tenant",
|
|
271
|
+
this.platform,
|
|
272
|
+
identity
|
|
273
|
+
].filter(Boolean).join("-");
|
|
274
|
+
this.slug = source.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
275
|
+
}
|
|
276
|
+
return this.slug;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if the token is expired or will expire soon
|
|
280
|
+
*/
|
|
281
|
+
get isTokenExpired() {
|
|
282
|
+
if (!this.tokenExpiresAt) return false;
|
|
283
|
+
return Date.now() >= this.tokenExpiresAt.getTime() - 300 * 1e3;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Check if the account needs attention (expired or error)
|
|
287
|
+
*/
|
|
288
|
+
get needsAttention() {
|
|
289
|
+
return this.status !== "connected" || this.isTokenExpired || this.missingPermissions.length > 0 || this.publishMode === "public" && !this.publicPublishingAllowed;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Check if the account is ready for publishing
|
|
293
|
+
*/
|
|
294
|
+
get isReady() {
|
|
295
|
+
return this.isActive && this.status === "connected" && this.hasCredentials && this.missingPermissions.length === 0 && !this.isTokenExpired && (this.publishMode !== "public" || this.publicPublishingAllowed);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Effective publish mode after applying the public-publishing latch.
|
|
299
|
+
*/
|
|
300
|
+
get effectivePublishMode() {
|
|
301
|
+
if (this.publishMode === "public" && !this.publicPublishingAllowed) return "dry_run";
|
|
302
|
+
return this.publishMode;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Check whether any usable credential reference exists.
|
|
306
|
+
*/
|
|
307
|
+
get hasCredentials() {
|
|
308
|
+
return Boolean(this.credentialSecretId || this.accessTokenSecretName || this.accessToken);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Store all platform credentials in smrt-secrets as a single JSON payload.
|
|
312
|
+
*/
|
|
313
|
+
async setCredentials(credentials, options = {}) {
|
|
314
|
+
if (!this.id) this.id = randomUUID();
|
|
315
|
+
const secretName = this.credentialSecretId ?? `social-account-${this.id}`;
|
|
316
|
+
const tenantId2 = this.requireCredentialTenantId("store social account credentials");
|
|
317
|
+
await (await SecretService.create({ db: this.db })).storeForTenant(tenantId2, secretName, JSON.stringify(credentials), {
|
|
318
|
+
description: options.description ?? `Credentials for ${this.name}`,
|
|
319
|
+
category: options.category ?? "social"
|
|
320
|
+
});
|
|
321
|
+
this.credentialSecretId = secretName;
|
|
322
|
+
await this.withCredentialTenantContext(async () => {
|
|
323
|
+
await this.save();
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Retrieve platform credentials from smrt-secrets, falling back to deprecated fields.
|
|
328
|
+
*/
|
|
329
|
+
async getCredentials() {
|
|
330
|
+
if (!this.credentialSecretId) {
|
|
331
|
+
if (this.accessTokenSecretName || this.refreshTokenSecretName) {
|
|
332
|
+
const tenantId3 = this.requireCredentialTenantId("retrieve social account credentials");
|
|
333
|
+
const secretService2 = await SecretService.create({ db: this.db });
|
|
334
|
+
const credentials = {};
|
|
335
|
+
if (this.accessTokenSecretName) credentials.accessToken = (await secretService2.retrieveForTenant(tenantId3, this.accessTokenSecretName)).value;
|
|
336
|
+
if (this.refreshTokenSecretName) credentials.refreshToken = (await secretService2.retrieveForTenant(tenantId3, this.refreshTokenSecretName)).value;
|
|
337
|
+
return credentials;
|
|
338
|
+
}
|
|
339
|
+
if (!this.accessToken && !this.refreshToken) return null;
|
|
340
|
+
return {
|
|
341
|
+
accessToken: this.accessToken,
|
|
342
|
+
refreshToken: this.refreshToken
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const credentialSecretId = this.credentialSecretId;
|
|
346
|
+
const tenantId2 = this.requireCredentialTenantId("retrieve social account credentials");
|
|
347
|
+
const secret = await (await SecretService.create({ db: this.db })).retrieveForTenant(tenantId2, credentialSecretId);
|
|
348
|
+
return JSON.parse(secret.value);
|
|
349
|
+
}
|
|
350
|
+
async withCredentialTenantContext(fn) {
|
|
351
|
+
const tenantId2 = this.getCredentialTenantId();
|
|
352
|
+
const currentTenantId = getCurrentTenant()?.tenantId;
|
|
353
|
+
if (!tenantId2 || currentTenantId === tenantId2) return fn();
|
|
354
|
+
return withTenant({ tenantId: tenantId2 }, fn);
|
|
355
|
+
}
|
|
356
|
+
getCredentialTenantId() {
|
|
357
|
+
if (this.tenantId) return this.tenantId;
|
|
358
|
+
return getCurrentTenant()?.tenantId ?? null;
|
|
359
|
+
}
|
|
360
|
+
requireCredentialTenantId(action) {
|
|
361
|
+
const tenantId2 = this.getCredentialTenantId();
|
|
362
|
+
if (!tenantId2) throw new Error(`Tenant context required to ${action}.`);
|
|
363
|
+
return tenantId2;
|
|
364
|
+
}
|
|
434
365
|
};
|
|
435
|
-
__decorateClass$2([
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
}
|
|
366
|
+
__decorateClass$2([tenantId({ nullable: true })], SocialAccount.prototype, "tenantId", 2);
|
|
367
|
+
SocialAccount = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
|
|
368
|
+
tableStrategy: "sti",
|
|
369
|
+
api: { include: [
|
|
370
|
+
"list",
|
|
371
|
+
"get",
|
|
372
|
+
"create",
|
|
373
|
+
"update",
|
|
374
|
+
"delete"
|
|
375
|
+
] },
|
|
376
|
+
mcp: { include: ["list", "get"] },
|
|
377
|
+
cli: true
|
|
378
|
+
})], SocialAccount);
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/collections/social-account-collection.ts
|
|
381
|
+
var SocialAccountCollection = class extends SmrtCollection {
|
|
382
|
+
static _itemClass = SocialAccount;
|
|
383
|
+
async findActive(platform) {
|
|
384
|
+
return await this.list({
|
|
385
|
+
where: {
|
|
386
|
+
isActive: true,
|
|
387
|
+
...platform ? { platform } : {}
|
|
388
|
+
},
|
|
389
|
+
orderBy: "name ASC"
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async findReady(platform) {
|
|
393
|
+
return (await this.findActive(platform)).filter((account) => account.isReady);
|
|
394
|
+
}
|
|
395
|
+
async findNeedsAttention() {
|
|
396
|
+
return (await this.list({
|
|
397
|
+
where: { isActive: true },
|
|
398
|
+
orderBy: "name ASC"
|
|
399
|
+
})).filter((account) => account.needsAttention);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/social-post.ts
|
|
475
404
|
var __defProp$1 = Object.defineProperty;
|
|
476
405
|
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
477
406
|
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (kind && result) __defProp$1(target, key, result);
|
|
483
|
-
return result;
|
|
407
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
408
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
409
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
410
|
+
return result;
|
|
484
411
|
};
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
get hashtagString() {
|
|
618
|
-
return this.hashtags.map((tag) => tag.startsWith("#") ? tag : `#${tag}`).join(" ");
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Get the full post text with hashtags
|
|
622
|
-
*/
|
|
623
|
-
get fullText() {
|
|
624
|
-
const parts = [this.description];
|
|
625
|
-
if (this.hashtags.length > 0) {
|
|
626
|
-
parts.push(this.hashtagString);
|
|
627
|
-
}
|
|
628
|
-
return parts.join("\n\n");
|
|
629
|
-
}
|
|
412
|
+
var SocialPost = class extends SmrtObject {
|
|
413
|
+
tenantId = null;
|
|
414
|
+
socialAccountId = null;
|
|
415
|
+
videoContentId = null;
|
|
416
|
+
contentId = null;
|
|
417
|
+
/**
|
|
418
|
+
* High-level post type
|
|
419
|
+
*/
|
|
420
|
+
postType = "text";
|
|
421
|
+
/**
|
|
422
|
+
* Public media URL for platforms that require URL media publishing
|
|
423
|
+
*/
|
|
424
|
+
mediaUrl = null;
|
|
425
|
+
/**
|
|
426
|
+
* Post title (for platforms that support it)
|
|
427
|
+
*/
|
|
428
|
+
title = null;
|
|
429
|
+
/**
|
|
430
|
+
* Post description/caption
|
|
431
|
+
*/
|
|
432
|
+
description = "";
|
|
433
|
+
/**
|
|
434
|
+
* Hashtags to include
|
|
435
|
+
*/
|
|
436
|
+
hashtags = [];
|
|
437
|
+
/**
|
|
438
|
+
* Link URL to include in the post
|
|
439
|
+
*/
|
|
440
|
+
linkUrl = null;
|
|
441
|
+
/**
|
|
442
|
+
* Platform-specific post ID (set after publishing)
|
|
443
|
+
*/
|
|
444
|
+
platformPostId = null;
|
|
445
|
+
/**
|
|
446
|
+
* Public post URL (set after publishing)
|
|
447
|
+
*/
|
|
448
|
+
platformUrl = null;
|
|
449
|
+
/**
|
|
450
|
+
* Scheduled publish time
|
|
451
|
+
* If set, post will be scheduled instead of published immediately
|
|
452
|
+
*/
|
|
453
|
+
scheduledAt = null;
|
|
454
|
+
/**
|
|
455
|
+
* Actual publish time
|
|
456
|
+
*/
|
|
457
|
+
publishedAt = null;
|
|
458
|
+
/**
|
|
459
|
+
* Post status
|
|
460
|
+
* - draft: Not yet submitted for publishing
|
|
461
|
+
* - pending_approval: Awaiting editorial approval
|
|
462
|
+
* - approved: Approved and ready to publish
|
|
463
|
+
* - scheduled: Queued for future publishing
|
|
464
|
+
* - publishing: Currently being published
|
|
465
|
+
* - dry_run: Payload was validated without remote write
|
|
466
|
+
* - staged: Non-public platform object/container was created
|
|
467
|
+
* - published: Successfully published
|
|
468
|
+
* - failed: Publishing failed
|
|
469
|
+
* - cancelled: Publishing was cancelled
|
|
470
|
+
*/
|
|
471
|
+
status = "draft";
|
|
472
|
+
/**
|
|
473
|
+
* Error message if status is 'failed'
|
|
474
|
+
*/
|
|
475
|
+
errorMessage = null;
|
|
476
|
+
/**
|
|
477
|
+
* Engagement analytics
|
|
478
|
+
*/
|
|
479
|
+
analytics = {};
|
|
480
|
+
/**
|
|
481
|
+
* When analytics were last synced
|
|
482
|
+
*/
|
|
483
|
+
analyticsLastSyncedAt = null;
|
|
484
|
+
constructor(options = {}) {
|
|
485
|
+
super(options);
|
|
486
|
+
if (options.socialAccountId !== void 0) this.socialAccountId = options.socialAccountId;
|
|
487
|
+
if (options.videoContentId !== void 0) this.videoContentId = options.videoContentId;
|
|
488
|
+
if (options.contentId !== void 0) this.contentId = options.contentId;
|
|
489
|
+
if (options.postType !== void 0) this.postType = options.postType;
|
|
490
|
+
if (options.mediaUrl !== void 0) this.mediaUrl = options.mediaUrl;
|
|
491
|
+
if (options.title !== void 0) this.title = options.title;
|
|
492
|
+
if (options.description !== void 0) this.description = options.description;
|
|
493
|
+
if (options.hashtags !== void 0) this.hashtags = options.hashtags;
|
|
494
|
+
if (options.linkUrl !== void 0) this.linkUrl = options.linkUrl;
|
|
495
|
+
if (options.platformPostId !== void 0) this.platformPostId = options.platformPostId;
|
|
496
|
+
if (options.platformUrl !== void 0) this.platformUrl = options.platformUrl;
|
|
497
|
+
if (options.scheduledAt !== void 0) this.scheduledAt = options.scheduledAt;
|
|
498
|
+
if (options.publishedAt !== void 0) this.publishedAt = options.publishedAt;
|
|
499
|
+
if (options.status !== void 0) this.status = options.status;
|
|
500
|
+
if (options.errorMessage !== void 0) this.errorMessage = options.errorMessage;
|
|
501
|
+
if (options.analytics !== void 0) this.analytics = options.analytics;
|
|
502
|
+
if (options.analyticsLastSyncedAt !== void 0) this.analyticsLastSyncedAt = options.analyticsLastSyncedAt;
|
|
503
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Check if the post is scheduled for future publishing
|
|
507
|
+
*/
|
|
508
|
+
get isScheduled() {
|
|
509
|
+
return this.status === "scheduled" && this.scheduledAt !== null;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Check if the post has been published
|
|
513
|
+
*/
|
|
514
|
+
get isPublished() {
|
|
515
|
+
return this.status === "published";
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Check if the post can be edited (draft or failed)
|
|
519
|
+
*/
|
|
520
|
+
get isEditable() {
|
|
521
|
+
return this.status === "draft" || this.status === "pending_approval" || this.status === "failed";
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Check if the post is due for publishing now.
|
|
525
|
+
*/
|
|
526
|
+
get isDueForPublish() {
|
|
527
|
+
if (this.status !== "approved" && this.status !== "scheduled") return false;
|
|
528
|
+
return !this.scheduledAt || this.scheduledAt.getTime() <= Date.now();
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Get formatted hashtag string
|
|
532
|
+
*/
|
|
533
|
+
get hashtagString() {
|
|
534
|
+
return this.hashtags.map((tag) => tag.startsWith("#") ? tag : `#${tag}`).join(" ");
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get the full post text with hashtags
|
|
538
|
+
*/
|
|
539
|
+
get fullText() {
|
|
540
|
+
const parts = [this.description];
|
|
541
|
+
if (this.hashtags.length > 0) parts.push(this.hashtagString);
|
|
542
|
+
return parts.join("\n\n");
|
|
543
|
+
}
|
|
630
544
|
};
|
|
631
|
-
__decorateClass$1([
|
|
632
|
-
|
|
633
|
-
], SocialPost.prototype, "
|
|
634
|
-
__decorateClass$1([
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
},
|
|
650
|
-
mcp: {
|
|
651
|
-
include: ["list", "get"]
|
|
652
|
-
},
|
|
653
|
-
cli: true
|
|
654
|
-
})
|
|
655
|
-
], SocialPost);
|
|
545
|
+
__decorateClass$1([tenantId({ nullable: true })], SocialPost.prototype, "tenantId", 2);
|
|
546
|
+
__decorateClass$1([foreignKey(() => SocialAccount)], SocialPost.prototype, "socialAccountId", 2);
|
|
547
|
+
__decorateClass$1([foreignKey(() => VideoContent)], SocialPost.prototype, "videoContentId", 2);
|
|
548
|
+
__decorateClass$1([crossPackageRef("@happyvertical/smrt-content:Content")], SocialPost.prototype, "contentId", 2);
|
|
549
|
+
SocialPost = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
|
|
550
|
+
tableStrategy: "sti",
|
|
551
|
+
api: { include: [
|
|
552
|
+
"list",
|
|
553
|
+
"get",
|
|
554
|
+
"create",
|
|
555
|
+
"update",
|
|
556
|
+
"delete"
|
|
557
|
+
] },
|
|
558
|
+
mcp: { include: ["list", "get"] },
|
|
559
|
+
cli: true
|
|
560
|
+
})], SocialPost);
|
|
561
|
+
//#endregion
|
|
562
|
+
//#region src/social-post-analytics-snapshot.ts
|
|
656
563
|
var __defProp = Object.defineProperty;
|
|
657
564
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
658
565
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (kind && result) __defProp(target, key, result);
|
|
664
|
-
return result;
|
|
566
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
567
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
568
|
+
if (kind && result) __defProp(target, key, result);
|
|
569
|
+
return result;
|
|
665
570
|
};
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
571
|
+
var SocialPostAnalyticsSnapshot = class extends SmrtObject {
|
|
572
|
+
tenantId = null;
|
|
573
|
+
socialPostId = null;
|
|
574
|
+
platform = null;
|
|
575
|
+
analytics = {};
|
|
576
|
+
raw = null;
|
|
577
|
+
capturedAt = /* @__PURE__ */ new Date();
|
|
578
|
+
constructor(options = {}) {
|
|
579
|
+
super(options);
|
|
580
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
581
|
+
if (options.socialPostId !== void 0) this.socialPostId = options.socialPostId;
|
|
582
|
+
if (options.platform !== void 0) this.platform = options.platform;
|
|
583
|
+
if (options.analytics !== void 0) this.analytics = options.analytics;
|
|
584
|
+
if (options.raw !== void 0) this.raw = options.raw;
|
|
585
|
+
if (options.capturedAt !== void 0) this.capturedAt = options.capturedAt;
|
|
586
|
+
}
|
|
683
587
|
};
|
|
684
|
-
__decorateClass([
|
|
685
|
-
|
|
686
|
-
], SocialPostAnalyticsSnapshot.prototype, "
|
|
687
|
-
__decorateClass([
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
include: ["list", "get"]
|
|
705
|
-
},
|
|
706
|
-
cli: true
|
|
707
|
-
})
|
|
708
|
-
], SocialPostAnalyticsSnapshot);
|
|
588
|
+
__decorateClass([tenantId({ nullable: true })], SocialPostAnalyticsSnapshot.prototype, "tenantId", 2);
|
|
589
|
+
__decorateClass([foreignKey(() => SocialPost)], SocialPostAnalyticsSnapshot.prototype, "socialPostId", 2);
|
|
590
|
+
__decorateClass([field({ required: true })], SocialPostAnalyticsSnapshot.prototype, "platform", 2);
|
|
591
|
+
__decorateClass([field({
|
|
592
|
+
type: "json",
|
|
593
|
+
nullable: true
|
|
594
|
+
})], SocialPostAnalyticsSnapshot.prototype, "raw", 2);
|
|
595
|
+
SocialPostAnalyticsSnapshot = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
|
|
596
|
+
tableStrategy: "sti",
|
|
597
|
+
api: { include: [
|
|
598
|
+
"list",
|
|
599
|
+
"get",
|
|
600
|
+
"create",
|
|
601
|
+
"delete"
|
|
602
|
+
] },
|
|
603
|
+
mcp: { include: ["list", "get"] },
|
|
604
|
+
cli: true
|
|
605
|
+
})], SocialPostAnalyticsSnapshot);
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region src/collections/social-post-analytics-snapshot-collection.ts
|
|
709
608
|
function isRawAnalyticsPayload(value) {
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
class SocialPostAnalyticsSnapshotCollection extends SmrtCollection {
|
|
713
|
-
static _itemClass = SocialPostAnalyticsSnapshot;
|
|
714
|
-
async recordSnapshot(options) {
|
|
715
|
-
const snapshot = await this.create({
|
|
716
|
-
socialPostId: options.socialPostId,
|
|
717
|
-
platform: options.platform,
|
|
718
|
-
analytics: options.metrics,
|
|
719
|
-
raw: options.raw ?? (isRawAnalyticsPayload(options.metrics.raw) ? options.metrics.raw : null),
|
|
720
|
-
capturedAt: options.capturedAt ?? /* @__PURE__ */ new Date(),
|
|
721
|
-
tenantId: options.tenantId
|
|
722
|
-
});
|
|
723
|
-
return snapshot;
|
|
724
|
-
}
|
|
725
|
-
async findForPost(socialPostId) {
|
|
726
|
-
return this.list({
|
|
727
|
-
where: { socialPostId },
|
|
728
|
-
orderBy: "capturedAt DESC"
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
async findLatestForPost(socialPostId) {
|
|
732
|
-
const snapshots = await this.findForPost(socialPostId);
|
|
733
|
-
return snapshots[0] ?? null;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
class SocialPostCollection extends SmrtCollection {
|
|
737
|
-
static _itemClass = SocialPost;
|
|
738
|
-
async createDraft(options) {
|
|
739
|
-
return this.create({
|
|
740
|
-
...options,
|
|
741
|
-
postType: options.postType ?? (options.videoContentId || options.mediaUrl ? "video" : options.linkUrl ? "link" : "text"),
|
|
742
|
-
status: options.scheduledAt ? "scheduled" : "draft"
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
async findByStatus(status) {
|
|
746
|
-
return this.list({ where: { status }, orderBy: "scheduledAt ASC" });
|
|
747
|
-
}
|
|
748
|
-
async findForAccount(socialAccountId) {
|
|
749
|
-
return this.list({
|
|
750
|
-
where: { socialAccountId },
|
|
751
|
-
orderBy: "created_at DESC"
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
async findDueForPublish(now = /* @__PURE__ */ new Date()) {
|
|
755
|
-
const [approved, scheduled] = await Promise.all([
|
|
756
|
-
this.list({
|
|
757
|
-
where: { status: "approved" },
|
|
758
|
-
orderBy: "scheduledAt ASC"
|
|
759
|
-
}),
|
|
760
|
-
this.list({
|
|
761
|
-
where: { status: "scheduled", "scheduledAt <=": now },
|
|
762
|
-
orderBy: "scheduledAt ASC"
|
|
763
|
-
})
|
|
764
|
-
]);
|
|
765
|
-
return [
|
|
766
|
-
...approved.filter((post) => post.isDueForPublish),
|
|
767
|
-
...scheduled
|
|
768
|
-
].sort(
|
|
769
|
-
(a, b) => (a.scheduledAt?.getTime() ?? 0) - (b.scheduledAt?.getTime() ?? 0)
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
async recordPublishSuccess(post, data) {
|
|
773
|
-
const status = data.status ?? "published";
|
|
774
|
-
post.platformPostId = data.platformPostId;
|
|
775
|
-
post.platformUrl = data.platformUrl;
|
|
776
|
-
if (status === "published") {
|
|
777
|
-
post.publishedAt = data.publishedAt ?? post.publishedAt ?? /* @__PURE__ */ new Date();
|
|
778
|
-
}
|
|
779
|
-
post.status = status;
|
|
780
|
-
post.errorMessage = null;
|
|
781
|
-
await post.save();
|
|
782
|
-
return post;
|
|
783
|
-
}
|
|
784
|
-
async recordPublishFailure(post, error) {
|
|
785
|
-
post.status = "failed";
|
|
786
|
-
post.errorMessage = formatPublishError(error);
|
|
787
|
-
await post.save();
|
|
788
|
-
return post;
|
|
789
|
-
}
|
|
790
|
-
async updateLatestAnalytics(post, analytics, syncedAt = /* @__PURE__ */ new Date()) {
|
|
791
|
-
post.analytics = {
|
|
792
|
-
...analytics,
|
|
793
|
-
lastUpdated: analytics.lastUpdated ?? syncedAt
|
|
794
|
-
};
|
|
795
|
-
post.analyticsLastSyncedAt = syncedAt;
|
|
796
|
-
await post.save();
|
|
797
|
-
return post;
|
|
798
|
-
}
|
|
609
|
+
return value === null || Array.isArray(value) || typeof value === "object" && value !== null;
|
|
799
610
|
}
|
|
611
|
+
var SocialPostAnalyticsSnapshotCollection = class extends SmrtCollection {
|
|
612
|
+
static _itemClass = SocialPostAnalyticsSnapshot;
|
|
613
|
+
async recordSnapshot(options) {
|
|
614
|
+
return await this.create({
|
|
615
|
+
socialPostId: options.socialPostId,
|
|
616
|
+
platform: options.platform,
|
|
617
|
+
analytics: options.metrics,
|
|
618
|
+
raw: options.raw ?? (isRawAnalyticsPayload(options.metrics.raw) ? options.metrics.raw : null),
|
|
619
|
+
capturedAt: options.capturedAt ?? /* @__PURE__ */ new Date(),
|
|
620
|
+
tenantId: options.tenantId
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async findForPost(socialPostId) {
|
|
624
|
+
return this.list({
|
|
625
|
+
where: { socialPostId },
|
|
626
|
+
orderBy: "capturedAt DESC"
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
async findLatestForPost(socialPostId) {
|
|
630
|
+
return (await this.findForPost(socialPostId))[0] ?? null;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/collections/social-post-collection.ts
|
|
635
|
+
var SocialPostCollection = class extends SmrtCollection {
|
|
636
|
+
static _itemClass = SocialPost;
|
|
637
|
+
async createDraft(options) {
|
|
638
|
+
return this.create({
|
|
639
|
+
...options,
|
|
640
|
+
postType: options.postType ?? (options.videoContentId || options.mediaUrl ? "video" : options.linkUrl ? "link" : "text"),
|
|
641
|
+
status: options.scheduledAt ? "scheduled" : "draft"
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
async findByStatus(status) {
|
|
645
|
+
return this.list({
|
|
646
|
+
where: { status },
|
|
647
|
+
orderBy: "scheduledAt ASC"
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
async findForAccount(socialAccountId) {
|
|
651
|
+
return this.list({
|
|
652
|
+
where: { socialAccountId },
|
|
653
|
+
orderBy: "created_at DESC"
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
async findDueForPublish(now = /* @__PURE__ */ new Date()) {
|
|
657
|
+
const [approved, scheduled] = await Promise.all([this.list({
|
|
658
|
+
where: { status: "approved" },
|
|
659
|
+
orderBy: "scheduledAt ASC"
|
|
660
|
+
}), this.list({
|
|
661
|
+
where: {
|
|
662
|
+
status: "scheduled",
|
|
663
|
+
"scheduledAt <=": now
|
|
664
|
+
},
|
|
665
|
+
orderBy: "scheduledAt ASC"
|
|
666
|
+
})]);
|
|
667
|
+
return [...approved.filter((post) => post.isDueForPublish), ...scheduled].sort((a, b) => (a.scheduledAt?.getTime() ?? 0) - (b.scheduledAt?.getTime() ?? 0));
|
|
668
|
+
}
|
|
669
|
+
async recordPublishSuccess(post, data) {
|
|
670
|
+
const status = data.status ?? "published";
|
|
671
|
+
post.platformPostId = data.platformPostId;
|
|
672
|
+
post.platformUrl = data.platformUrl;
|
|
673
|
+
if (status === "published") post.publishedAt = data.publishedAt ?? post.publishedAt ?? /* @__PURE__ */ new Date();
|
|
674
|
+
post.status = status;
|
|
675
|
+
post.errorMessage = null;
|
|
676
|
+
await post.save();
|
|
677
|
+
return post;
|
|
678
|
+
}
|
|
679
|
+
async recordPublishFailure(post, error) {
|
|
680
|
+
post.status = "failed";
|
|
681
|
+
post.errorMessage = formatPublishError(error);
|
|
682
|
+
await post.save();
|
|
683
|
+
return post;
|
|
684
|
+
}
|
|
685
|
+
async updateLatestAnalytics(post, analytics, syncedAt = /* @__PURE__ */ new Date()) {
|
|
686
|
+
post.analytics = {
|
|
687
|
+
...analytics,
|
|
688
|
+
lastUpdated: analytics.lastUpdated ?? syncedAt
|
|
689
|
+
};
|
|
690
|
+
post.analyticsLastSyncedAt = syncedAt;
|
|
691
|
+
await post.save();
|
|
692
|
+
return post;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
800
695
|
function formatPublishError(error) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
details.push(`code=${String(code)}`);
|
|
808
|
-
}
|
|
809
|
-
if (error.stack) {
|
|
810
|
-
details.push(error.stack);
|
|
811
|
-
}
|
|
812
|
-
return details.join("\n");
|
|
696
|
+
if (typeof error === "string") return error;
|
|
697
|
+
const details = [error.message];
|
|
698
|
+
const code = error.code;
|
|
699
|
+
if (code !== void 0) details.push(`code=${String(code)}`);
|
|
700
|
+
if (error.stack) details.push(error.stack);
|
|
701
|
+
return details.join("\n");
|
|
813
702
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
SocialAccountCollection,
|
|
819
|
-
SocialPost,
|
|
820
|
-
SocialPostAnalyticsSnapshot,
|
|
821
|
-
SocialPostAnalyticsSnapshotCollection,
|
|
822
|
-
SocialPostCollection
|
|
823
|
-
};
|
|
824
|
-
//# sourceMappingURL=index.js.map
|
|
703
|
+
//#endregion
|
|
704
|
+
export { OAuthState, OAuthStateCollection, SocialAccount, SocialAccountCollection, SocialPost, SocialPostAnalyticsSnapshot, SocialPostAnalyticsSnapshotCollection, SocialPostCollection };
|
|
705
|
+
|
|
706
|
+
//# sourceMappingURL=index.js.map
|