@jiggai/kitchen-plugin-marketing 0.2.8 → 0.2.10

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.
@@ -198,6 +198,13 @@ function encryptCredentials(credentials) {
198
198
  encrypted += cipher.final("hex");
199
199
  return Buffer.from(encrypted, "hex");
200
200
  }
201
+ function decryptCredentials(encryptedData) {
202
+ const hash = (0, import_crypto.createHash)("sha256").update(ENCRYPTION_KEY).digest();
203
+ const decipher = (0, import_crypto.createDecipher)("aes-256-cbc", hash);
204
+ let decrypted = decipher.update(encryptedData.toString("hex"), "hex", "utf8");
205
+ decrypted += decipher.final("utf8");
206
+ return JSON.parse(decrypted);
207
+ }
201
208
  function initializeDatabase(teamId) {
202
209
  const { db, sqlite } = createDatabase(teamId);
203
210
  try {
@@ -208,6 +215,311 @@ function initializeDatabase(teamId) {
208
215
  return { db, sqlite };
209
216
  }
210
217
 
218
+ // src/drivers/postiz-backend.ts
219
+ async function postizFetch(config, path, options) {
220
+ return fetch(`${config.baseUrl}${path}`, {
221
+ ...options,
222
+ headers: {
223
+ "Authorization": config.apiKey,
224
+ "Content-Type": "application/json",
225
+ ...options?.headers || {}
226
+ }
227
+ });
228
+ }
229
+ async function getPostizIntegrations(config) {
230
+ const res = await postizFetch(config, "/integrations");
231
+ if (!res.ok) return [];
232
+ const data = await res.json();
233
+ return Array.isArray(data) ? data : data.integrations || [];
234
+ }
235
+ async function postizPublish(config, integrationId, content, options) {
236
+ const payload = {
237
+ content,
238
+ integrationIds: [integrationId]
239
+ };
240
+ if (options?.scheduledAt) payload.date = options.scheduledAt;
241
+ if (options?.settings) payload.settings = options.settings;
242
+ if (options?.mediaUrls?.length) {
243
+ payload.media = options.mediaUrls.map((url) => ({ url }));
244
+ }
245
+ const res = await postizFetch(config, "/posts", {
246
+ method: "POST",
247
+ body: JSON.stringify(payload)
248
+ });
249
+ const data = await res.json().catch(() => null);
250
+ if (!res.ok) {
251
+ return { success: false, error: data?.message || `Postiz error ${res.status}`, meta: data };
252
+ }
253
+ return { success: true, postId: data?.id, meta: data };
254
+ }
255
+
256
+ // src/drivers/base-driver.ts
257
+ var BaseDriver = class {
258
+ config;
259
+ _postizIntegrationId = null;
260
+ _statusCache = null;
261
+ constructor(config) {
262
+ this.config = config;
263
+ }
264
+ /** Platform-specific character limit */
265
+ getMaxLength() {
266
+ return void 0;
267
+ }
268
+ /** Platform-specific supported media types */
269
+ getSupportedMedia() {
270
+ return void 0;
271
+ }
272
+ getCapabilities() {
273
+ const hasPostiz = !!this.config.postiz;
274
+ const hasGateway = !!this.config.gateway;
275
+ const hasDirect = !!this.config.direct;
276
+ return {
277
+ canPost: hasPostiz || hasGateway || hasDirect,
278
+ canSchedule: hasPostiz,
279
+ // only Postiz supports native scheduling
280
+ canDelete: false,
281
+ canUploadMedia: hasPostiz || hasDirect,
282
+ maxLength: this.getMaxLength(),
283
+ supportedMedia: this.getSupportedMedia()
284
+ };
285
+ }
286
+ async getStatus() {
287
+ if (this._statusCache) return this._statusCache;
288
+ if (this.config.postiz) {
289
+ try {
290
+ const integrations = await getPostizIntegrations(this.config.postiz);
291
+ const match = integrations.find(
292
+ (i) => i.providerIdentifier === this.postizProvider && !i.disabled
293
+ );
294
+ if (match) {
295
+ this._postizIntegrationId = this.config.postiz.integrationId || match.id;
296
+ this._statusCache = {
297
+ connected: true,
298
+ backend: "postiz",
299
+ displayName: match.name || `${this.label} (Postiz)`,
300
+ username: match.username,
301
+ avatar: match.picture,
302
+ integrationId: match.id
303
+ };
304
+ return this._statusCache;
305
+ }
306
+ } catch {
307
+ }
308
+ }
309
+ if (this.config.gateway) {
310
+ this._statusCache = {
311
+ connected: true,
312
+ backend: "gateway",
313
+ displayName: `${this.label} (via OpenClaw)`
314
+ };
315
+ return this._statusCache;
316
+ }
317
+ if (this.config.direct?.accessToken) {
318
+ this._statusCache = {
319
+ connected: true,
320
+ backend: "direct",
321
+ displayName: `${this.label} (Direct API)`
322
+ };
323
+ return this._statusCache;
324
+ }
325
+ this._statusCache = {
326
+ connected: false,
327
+ backend: "none",
328
+ displayName: this.label
329
+ };
330
+ return this._statusCache;
331
+ }
332
+ async publish(content) {
333
+ const status = await this.getStatus();
334
+ switch (status.backend) {
335
+ case "postiz":
336
+ return this.publishViaPostiz(content);
337
+ case "gateway":
338
+ return this.publishViaGateway(content);
339
+ case "direct":
340
+ return this.publishDirect(content);
341
+ default:
342
+ return { success: false, error: `No backend configured for ${this.label}` };
343
+ }
344
+ }
345
+ /** Publish through Postiz */
346
+ async publishViaPostiz(content) {
347
+ const cfg = this.config.postiz;
348
+ if (!cfg) return { success: false, error: "Postiz not configured" };
349
+ const integrationId = this._postizIntegrationId || cfg.integrationId;
350
+ if (!integrationId) return { success: false, error: "No Postiz integration found for " + this.platform };
351
+ const result = await postizPublish(cfg, integrationId, content.text, {
352
+ scheduledAt: content.scheduledAt,
353
+ mediaUrls: content.mediaUrls,
354
+ settings: content.settings
355
+ });
356
+ return {
357
+ success: result.success,
358
+ postId: result.postId,
359
+ error: result.error,
360
+ scheduledAt: content.scheduledAt,
361
+ meta: result.meta
362
+ };
363
+ }
364
+ /** Publish through OpenClaw gateway messaging. Override for platform-specific formatting. */
365
+ async publishViaGateway(_content) {
366
+ return { success: false, error: `Gateway publishing not implemented for ${this.label}` };
367
+ }
368
+ /** Direct API publish. Override per platform. */
369
+ async publishDirect(_content) {
370
+ return { success: false, error: `Direct API publishing not implemented for ${this.label}` };
371
+ }
372
+ };
373
+
374
+ // src/drivers/x-driver.ts
375
+ var XDriver = class extends BaseDriver {
376
+ platform = "x";
377
+ label = "X (Twitter)";
378
+ icon = "\u{1D54F}";
379
+ postizProvider = "x";
380
+ getMaxLength() {
381
+ return 280;
382
+ }
383
+ getSupportedMedia() {
384
+ return ["image/jpeg", "image/png", "image/gif", "video/mp4"];
385
+ }
386
+ };
387
+
388
+ // src/drivers/instagram-driver.ts
389
+ var InstagramDriver = class extends BaseDriver {
390
+ platform = "instagram";
391
+ label = "Instagram";
392
+ icon = "\u{1F4F7}";
393
+ postizProvider = "instagram";
394
+ getMaxLength() {
395
+ return 2200;
396
+ }
397
+ getSupportedMedia() {
398
+ return ["image/jpeg", "image/png", "video/mp4"];
399
+ }
400
+ };
401
+
402
+ // src/drivers/facebook-driver.ts
403
+ var FacebookDriver = class extends BaseDriver {
404
+ platform = "facebook";
405
+ label = "Facebook";
406
+ icon = "\u{1F4D8}";
407
+ postizProvider = "facebook";
408
+ getMaxLength() {
409
+ return 63206;
410
+ }
411
+ getSupportedMedia() {
412
+ return ["image/jpeg", "image/png", "image/gif", "video/mp4"];
413
+ }
414
+ };
415
+
416
+ // src/drivers/linkedin-driver.ts
417
+ var LinkedInDriver = class extends BaseDriver {
418
+ platform = "linkedin";
419
+ label = "LinkedIn";
420
+ icon = "\u{1F4BC}";
421
+ postizProvider = "linkedin";
422
+ getMaxLength() {
423
+ return 3e3;
424
+ }
425
+ getSupportedMedia() {
426
+ return ["image/jpeg", "image/png", "image/gif", "video/mp4"];
427
+ }
428
+ };
429
+
430
+ // src/drivers/tiktok-driver.ts
431
+ var TikTokDriver = class extends BaseDriver {
432
+ platform = "tiktok";
433
+ label = "TikTok";
434
+ icon = "\u{1F3B5}";
435
+ postizProvider = "tiktok";
436
+ getMaxLength() {
437
+ return 2200;
438
+ }
439
+ getSupportedMedia() {
440
+ return ["video/mp4"];
441
+ }
442
+ };
443
+
444
+ // src/drivers/discord-driver.ts
445
+ var DiscordDriver = class extends BaseDriver {
446
+ platform = "discord";
447
+ label = "Discord";
448
+ icon = "\u{1F4AC}";
449
+ postizProvider = "discord";
450
+ getMaxLength() {
451
+ return 2e3;
452
+ }
453
+ /** Discord posting via OpenClaw gateway message tool */
454
+ async publishViaGateway(content) {
455
+ return {
456
+ success: false,
457
+ error: "Gateway publishing requires OpenClaw message routing \u2014 use the scheduler or workflow"
458
+ };
459
+ }
460
+ };
461
+
462
+ // src/drivers/telegram-driver.ts
463
+ var TelegramDriver = class extends BaseDriver {
464
+ platform = "telegram";
465
+ label = "Telegram";
466
+ icon = "\u2708\uFE0F";
467
+ postizProvider = "telegram";
468
+ getMaxLength() {
469
+ return 4096;
470
+ }
471
+ /** Telegram posting via OpenClaw gateway message tool */
472
+ async publishViaGateway(content) {
473
+ return {
474
+ success: false,
475
+ error: "Gateway publishing requires OpenClaw message routing \u2014 use the scheduler or workflow"
476
+ };
477
+ }
478
+ };
479
+
480
+ // src/drivers/index.ts
481
+ var DRIVER_MAP = {
482
+ x: XDriver,
483
+ twitter: XDriver,
484
+ instagram: InstagramDriver,
485
+ facebook: FacebookDriver,
486
+ linkedin: LinkedInDriver,
487
+ tiktok: TikTokDriver,
488
+ discord: DiscordDriver,
489
+ telegram: TelegramDriver
490
+ };
491
+ function getPlatforms() {
492
+ return ["x", "instagram", "facebook", "linkedin", "tiktok", "discord", "telegram"];
493
+ }
494
+ function createDriver(platform, config) {
495
+ const Cls = DRIVER_MAP[platform.toLowerCase()];
496
+ if (!Cls) return null;
497
+ return new Cls(config);
498
+ }
499
+ function createAllDrivers(sources) {
500
+ const platforms = getPlatforms();
501
+ const drivers = [];
502
+ for (const platform of platforms) {
503
+ const config = {};
504
+ if (sources.postiz) {
505
+ config.postiz = {
506
+ apiKey: sources.postiz.apiKey,
507
+ baseUrl: sources.postiz.baseUrl
508
+ };
509
+ }
510
+ if (sources.gatewayChannels?.includes(platform)) {
511
+ config.gateway = { channel: platform };
512
+ }
513
+ const stored = sources.storedAccounts?.find((a) => a.platform === platform);
514
+ if (stored) {
515
+ config.direct = stored.credentials;
516
+ }
517
+ const driver = createDriver(platform, config);
518
+ if (driver) drivers.push(driver);
519
+ }
520
+ return drivers;
521
+ }
522
+
211
523
  // src/api/handler.ts
212
524
  function apiError(status, error, message, details) {
213
525
  const payload = { error, message, details };
@@ -224,139 +536,151 @@ function getTeamId(req) {
224
536
  function getUserId(req) {
225
537
  return req.headers["x-user-id"] || "system";
226
538
  }
227
- function getPostizConfig(req) {
228
- const apiKey = req.query.postizApiKey || req.headers["x-postiz-api-key"];
229
- const baseUrl = req.query.postizBaseUrl || req.headers["x-postiz-base-url"] || "https://api.postiz.com/public/v1";
230
- if (!apiKey) return null;
231
- return { apiKey, baseUrl: baseUrl.replace(/\/+$/, "") };
232
- }
233
- async function postizFetch(config, path, options) {
234
- return fetch(`${config.baseUrl}${path}`, {
235
- ...options,
236
- headers: {
237
- "Authorization": config.apiKey,
238
- "Content-Type": "application/json",
239
- ...options?.headers || {}
240
- }
241
- });
242
- }
243
- async function detectProviders(req, teamId) {
244
- const providers = [];
245
- const postizCfg = getPostizConfig(req);
246
- if (postizCfg) {
247
- try {
248
- const res = await postizFetch(postizCfg, "/integrations");
249
- if (res.ok) {
250
- const data = await res.json();
251
- const integrations = Array.isArray(data) ? data : data.integrations || [];
252
- for (const integ of integrations) {
253
- providers.push({
254
- id: `postiz:${integ.id}`,
255
- type: "postiz",
256
- platform: integ.providerIdentifier || integ.provider || "unknown",
257
- displayName: integ.name || integ.providerIdentifier || "Postiz account",
258
- username: integ.username || void 0,
259
- avatar: integ.picture || integ.avatar || void 0,
260
- isActive: !integ.disabled,
261
- capabilities: ["post", "schedule"],
262
- meta: { postizId: integ.id, provider: integ.providerIdentifier }
263
- });
264
- }
265
- }
266
- } catch {
267
- }
539
+ function getBackendSources(req, teamId) {
540
+ const sources = {};
541
+ const postizKey = req.query.postizApiKey || req.headers["x-postiz-api-key"];
542
+ if (postizKey) {
543
+ const baseUrl = req.query.postizBaseUrl || req.headers["x-postiz-base-url"] || "https://api.postiz.com/public/v1";
544
+ sources.postiz = { apiKey: postizKey, baseUrl: baseUrl.replace(/\/+$/, "") };
268
545
  }
269
546
  try {
270
- const fs = await import("fs");
271
- const path = await import("path");
272
- const os = await import("os");
547
+ const fs = require("fs");
548
+ const path = require("path");
549
+ const os = require("os");
273
550
  const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
274
551
  if (fs.existsSync(configPath)) {
275
552
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
276
553
  const plugins = cfg?.plugins?.entries || {};
277
- if (plugins.discord?.enabled) {
278
- providers.push({
279
- id: "gateway:discord",
280
- type: "gateway",
281
- platform: "discord",
282
- displayName: "Discord (via OpenClaw)",
283
- isActive: true,
284
- capabilities: ["post"],
285
- meta: { channel: "discord" }
286
- });
287
- }
288
- if (plugins.telegram?.enabled) {
289
- providers.push({
290
- id: "gateway:telegram",
291
- type: "gateway",
292
- platform: "telegram",
293
- displayName: "Telegram (via OpenClaw)",
294
- isActive: true,
295
- capabilities: ["post"],
296
- meta: { channel: "telegram" }
297
- });
298
- }
554
+ const channels = [];
555
+ if (plugins.discord?.enabled) channels.push("discord");
556
+ if (plugins.telegram?.enabled) channels.push("telegram");
557
+ sources.gatewayChannels = channels;
299
558
  }
300
559
  } catch {
301
560
  }
302
- return providers;
303
- }
304
- async function postizPublish(config, body) {
305
- const payload = {
306
- content: body.content,
307
- integrationIds: body.integrationIds
308
- };
309
- if (body.scheduledAt) {
310
- payload.date = body.scheduledAt;
311
- }
312
- if (body.settings) {
313
- payload.settings = body.settings;
314
- }
315
- if (body.mediaUrls && body.mediaUrls.length > 0) {
316
- payload.media = body.mediaUrls.map((url) => ({ url }));
317
- }
318
- const res = await postizFetch(config, "/posts", {
319
- method: "POST",
320
- body: JSON.stringify(payload)
321
- });
322
- const data = await res.json().catch(() => null);
323
- if (!res.ok) {
324
- return apiError(res.status, "POSTIZ_ERROR", data?.message || `Postiz returned ${res.status}`, data);
561
+ try {
562
+ const { db } = initializeDatabase(teamId);
563
+ const accounts = db.select().from(socialAccounts).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(socialAccounts.teamId, teamId), (0, import_drizzle_orm2.eq)(socialAccounts.isActive, true))).all();
564
+ sources.storedAccounts = accounts.map((a) => ({
565
+ platform: a.platform,
566
+ credentials: decryptCredentials(a.credentials)
567
+ }));
568
+ } catch {
325
569
  }
326
- return { status: 201, data };
570
+ return sources;
327
571
  }
328
572
  async function handleRequest(req, ctx) {
329
573
  const teamId = getTeamId(req);
330
- if (req.path === "/providers" && req.method === "GET") {
574
+ if (req.path === "/drivers" && req.method === "GET") {
331
575
  try {
332
- const providers = await detectProviders(req, teamId);
333
- return { status: 200, data: { providers } };
576
+ const sources = getBackendSources(req, teamId);
577
+ const drivers = createAllDrivers(sources);
578
+ const results = await Promise.all(
579
+ drivers.map(async (d) => {
580
+ const status = await d.getStatus();
581
+ const caps = d.getCapabilities();
582
+ return {
583
+ platform: d.platform,
584
+ label: d.label,
585
+ icon: d.icon,
586
+ ...status,
587
+ capabilities: caps
588
+ };
589
+ })
590
+ );
591
+ return { status: 200, data: { drivers: results } };
334
592
  } catch (error) {
335
- return apiError(500, "DETECT_ERROR", error?.message || "Failed to detect providers");
593
+ return apiError(500, "DRIVER_ERROR", error?.message || "Failed to load drivers");
336
594
  }
337
595
  }
338
- if (req.path === "/providers/postiz/integrations" && req.method === "GET") {
339
- const postizCfg = getPostizConfig(req);
340
- if (!postizCfg) return apiError(400, "NO_POSTIZ", "Postiz API key not configured");
341
- try {
342
- const res = await postizFetch(postizCfg, "/integrations");
343
- const data = await res.json();
344
- return { status: res.status, data };
345
- } catch (error) {
346
- return apiError(502, "POSTIZ_UNREACHABLE", error?.message || "Cannot reach Postiz");
347
- }
596
+ if (req.path.match(/^\/drivers\/([^/]+)\/status$/) && req.method === "GET") {
597
+ const platform = req.path.split("/")[2];
598
+ const sources = getBackendSources(req, teamId);
599
+ const driver = createDriver(platform, {
600
+ postiz: sources.postiz,
601
+ gateway: sources.gatewayChannels?.includes(platform) ? { channel: platform } : void 0,
602
+ direct: sources.storedAccounts?.find((a) => a.platform === platform)?.credentials
603
+ });
604
+ if (!driver) return apiError(404, "NOT_FOUND", `No driver for platform: ${platform}`);
605
+ const status = await driver.getStatus();
606
+ const caps = driver.getCapabilities();
607
+ return { status: 200, data: { platform, ...status, capabilities: caps } };
348
608
  }
349
609
  if (req.path === "/publish" && req.method === "POST") {
350
- const postizCfg = getPostizConfig(req);
351
- if (!postizCfg) return apiError(400, "NO_POSTIZ", "Postiz API key not configured");
352
- const body = req.body || {};
353
- if (!body.content || !body.integrationIds?.length) {
354
- return apiError(400, "VALIDATION_ERROR", "content and integrationIds are required");
610
+ const body = req.body;
611
+ if (!body?.content || !body?.platforms?.length) {
612
+ return apiError(400, "VALIDATION_ERROR", "content and platforms[] are required");
613
+ }
614
+ const sources = getBackendSources(req, teamId);
615
+ const results = [];
616
+ for (const platform of body.platforms) {
617
+ const driver = createDriver(platform, {
618
+ postiz: sources.postiz,
619
+ gateway: sources.gatewayChannels?.includes(platform) ? { channel: platform } : void 0,
620
+ direct: sources.storedAccounts?.find((a) => a.platform === platform)?.credentials
621
+ });
622
+ if (!driver) {
623
+ results.push({ platform, success: false, error: `No driver for ${platform}` });
624
+ continue;
625
+ }
626
+ const status = await driver.getStatus();
627
+ if (!status.connected) {
628
+ results.push({ platform, success: false, error: `Not connected`, backend: status.backend });
629
+ continue;
630
+ }
631
+ const postContent = {
632
+ text: body.content,
633
+ mediaUrls: body.mediaUrls,
634
+ scheduledAt: body.scheduledAt,
635
+ settings: body.settings?.[platform]
636
+ };
637
+ const result = await driver.publish(postContent);
638
+ results.push({
639
+ platform,
640
+ success: result.success,
641
+ postId: result.postId,
642
+ error: result.error,
643
+ backend: status.backend
644
+ });
355
645
  }
646
+ const allOk = results.every((r) => r.success);
647
+ return { status: allOk ? 201 : 207, data: { results } };
648
+ }
649
+ if (req.path === "/platforms" && req.method === "GET") {
650
+ const sources = getBackendSources(req, teamId);
651
+ const drivers = createAllDrivers(sources);
652
+ const platforms = drivers.map((d) => ({
653
+ platform: d.platform,
654
+ label: d.label,
655
+ icon: d.icon,
656
+ capabilities: d.getCapabilities()
657
+ }));
658
+ return { status: 200, data: { platforms } };
659
+ }
660
+ if (req.path === "/providers" && req.method === "GET") {
356
661
  try {
357
- return await postizPublish(postizCfg, body);
662
+ const sources = getBackendSources(req, teamId);
663
+ const drivers = createAllDrivers(sources);
664
+ const providers = await Promise.all(
665
+ drivers.map(async (d) => {
666
+ const status = await d.getStatus();
667
+ if (!status.connected) return null;
668
+ return {
669
+ id: `${status.backend}:${d.platform}`,
670
+ type: status.backend,
671
+ platform: d.platform,
672
+ displayName: status.displayName,
673
+ username: status.username,
674
+ avatar: status.avatar,
675
+ isActive: status.connected,
676
+ capabilities: status.backend === "postiz" ? ["post", "schedule"] : ["post"],
677
+ meta: { integrationId: status.integrationId }
678
+ };
679
+ })
680
+ );
681
+ return { status: 200, data: { providers: providers.filter(Boolean) } };
358
682
  } catch (error) {
359
- return apiError(502, "POSTIZ_ERROR", error?.message || "Publish failed");
683
+ return apiError(500, "DETECT_ERROR", error?.message || "Failed to detect providers");
360
684
  }
361
685
  }
362
686
  if (req.path === "/posts" && req.method === "GET") {
@@ -364,12 +688,8 @@ async function handleRequest(req, ctx) {
364
688
  const { db } = initializeDatabase(teamId);
365
689
  const { limit, offset } = parsePagination(req.query);
366
690
  const conditions = [(0, import_drizzle_orm2.eq)(posts.teamId, teamId)];
367
- if (req.query.status) {
368
- conditions.push((0, import_drizzle_orm2.eq)(posts.status, String(req.query.status)));
369
- }
370
- if (req.query.platform) {
371
- conditions.push((0, import_drizzle_orm2.like)(posts.platforms, `%"${req.query.platform}"%`));
372
- }
691
+ if (req.query.status) conditions.push((0, import_drizzle_orm2.eq)(posts.status, String(req.query.status)));
692
+ if (req.query.platform) conditions.push((0, import_drizzle_orm2.like)(posts.platforms, `%"${req.query.platform}"%`));
373
693
  const totalResult = await db.select({ count: import_drizzle_orm2.sql`count(*)` }).from(posts).where((0, import_drizzle_orm2.and)(...conditions));
374
694
  const total = totalResult[0]?.count ?? 0;
375
695
  const posts2 = await db.select().from(posts).where((0, import_drizzle_orm2.and)(...conditions)).orderBy((0, import_drizzle_orm2.desc)(posts.createdAt)).limit(limit).offset(offset);