Benjamin Harris 1 месяц назад
Родитель
Сommit
8f0c8a2e45
2 измененных файлов с 19 добавлено и 33 удалено
  1. 7 31
      .env.example
  2. 12 2
      services/scheduler/index.js

+ 7 - 31
.env.example

@@ -20,7 +20,7 @@ MONGODB_DB=socialmedia
 REDIS_URL=redis://redis:6379
 REDIS_URL=redis://redis:6379
 
 
 # ─── Twitter / X ───────────────────────────────────────────────────────────────
 # ─── Twitter / X ───────────────────────────────────────────────────────────────
-# Developer portalından alınır: developer.twitter.com
+# Taken from the Developer portal: developer.twitter.com
 TWITTER_API_KEY=
 TWITTER_API_KEY=
 TWITTER_API_SECRET=
 TWITTER_API_SECRET=
 TWITTER_ACCESS_TOKEN=
 TWITTER_ACCESS_TOKEN=
@@ -28,48 +28,24 @@ TWITTER_ACCESS_SECRET=
 TWITTER_BEARER_TOKEN=
 TWITTER_BEARER_TOKEN=
 
 
 # ─── LinkedIn ──────────────────────────────────────────────────────────────────
 # ─── LinkedIn ──────────────────────────────────────────────────────────────────
-# Developer portalından alınır: linkedin.com/developers
+# Taken from the Developer portal: linkedin.com/developers
 LINKEDIN_CLIENT_ID=
 LINKEDIN_CLIENT_ID=
 LINKEDIN_CLIENT_SECRET=
 LINKEDIN_CLIENT_SECRET=
 
 
 # ─── Mastodon ──────────────────────────────────────────────────────────────────
 # ─── Mastodon ──────────────────────────────────────────────────────────────────
-# Hesabın olduğu instance URL (örn: https://mastodon.social)
+# The instance URL where the account is located (e.g., https://mastodon.social)
 MASTODON_INSTANCE_URL=https://mastodon.social
 MASTODON_INSTANCE_URL=https://mastodon.social
-# Instance'ın Settings > Development > New application bölümünden alınır
+# It is obtained from the instance's Settings > Development > New application section.
 MASTODON_ACCESS_TOKEN=
 MASTODON_ACCESS_TOKEN=
 
 
 # ─── Bluesky ───────────────────────────────────────────────────────────────────
 # ─── Bluesky ───────────────────────────────────────────────────────────────────
-# Kullanıcı adın (örn: user.bsky.social)
+# Your username (e.g., user.bsky.social)
 BLUESKY_IDENTIFIER=
 BLUESKY_IDENTIFIER=
-# Settings > App Passwords bölümünden oluşturulan şifre (gerçek şifreni kullanma!)
+# Password generated from Settings > App Passwords (don't use your real password!)
 BLUESKY_APP_PASSWORD=
 BLUESKY_APP_PASSWORD=
 
 
-# ─── Facebook Developer App (shared by Instagram + Facebook services) ───────────
-# Create your app at: https://developers.facebook.com/apps/
-# Required permissions: pages_manage_posts, pages_read_engagement,
-#   instagram_basic, instagram_content_publish, instagram_manage_insights
-FACEBOOK_APP_ID=
-FACEBOOK_APP_SECRET=
-
-# ─── Instagram ─────────────────────────────────────────────────────────────────
-# Requires a Business or Creator Instagram account linked to a Facebook Page.
-# Get via OAuth callback (/auth/callback on the instagram service) or from
-# Graph API Explorer: https://developers.facebook.com/tools/explorer/
-# INSTAGRAM_ACCESS_TOKEN  — long-lived user access token (~60 days, use fb_exchange_token to refresh)
-# INSTAGRAM_BUSINESS_ACCOUNT_ID — numeric ID, found via GET /{page-id}?fields=instagram_business_account
-INSTAGRAM_ACCESS_TOKEN=
-INSTAGRAM_BUSINESS_ACCOUNT_ID=
-
-# ─── Facebook ──────────────────────────────────────────────────────────────────
-# Requires a Facebook Page (personal timelines are not supported by the Graph API).
-# FACEBOOK_PAGE_ID          — numeric Page ID (visible in Page settings or About section)
-# FACEBOOK_PAGE_ACCESS_TOKEN — never-expiring Page token obtained via GET /me/accounts
-#                              after exchanging a long-lived user token
-FACEBOOK_PAGE_ID=
-FACEBOOK_PAGE_ACCESS_TOKEN=
-
 # ─── Reddit ────────────────────────────────────────────────────────────────────
 # ─── Reddit ────────────────────────────────────────────────────────────────────
-# reddit.com/prefs/apps üzerinden oluşturulan uygulama
+# Application created via reddit.com/prefs/apps
 REDDIT_CLIENT_ID=
 REDDIT_CLIENT_ID=
 REDDIT_CLIENT_SECRET=
 REDDIT_CLIENT_SECRET=
 REDDIT_USERNAME=
 REDDIT_USERNAME=

+ 12 - 2
services/scheduler/index.js

@@ -30,14 +30,24 @@ async function processPostJob(job) {
   const { postId, content, destinations, platforms, media = [] } = job.data;
   const { postId, content, destinations, platforms, media = [] } = job.data;
 
 
   const destList = destinations || (platforms || []).map((p) => ({ platform: p }));
   const destList = destinations || (platforms || []).map((p) => ({ platform: p }));
-  log.info({ action: 'job_process', jobId: job.id, destinations: destList.map((d) => d.accountId ? `${d.platform}:${d.accountId}` : d.platform) });
+  log.info({ action: 'job_process', jobId: job.id, attempt: job.attemptsMade + 1, destinations: destList.map((d) => d.accountId ? `${d.platform}:${d.accountId}` : d.platform) });
 
 
   const db = await getDb();
   const db = await getDb();
-  const results = {};
+
+  // Load any results already recorded from previous attempts so we can skip
+  // destinations that already succeeded — preventing duplicate posts on retry.
+  const existingPost = postId ? await db.collection('posts').findOne({ _id: postId }, { projection: { platformResults: 1 } }) : null;
+  const results = { ...(existingPost?.platformResults || {}) };
 
 
   for (const dest of destList) {
   for (const dest of destList) {
     const { platform, accountId, imageUrl, videoUrl, link } = dest;
     const { platform, accountId, imageUrl, videoUrl, link } = dest;
     const resultKey = accountId ? `${platform}:${accountId}` : platform;
     const resultKey = accountId ? `${platform}:${accountId}` : platform;
+
+    if (results[resultKey]?.success) {
+      log.info({ action: 'job_skip_dest', jobId: job.id, destination: resultKey, reason: 'already_published' });
+      continue;
+    }
+
     const serviceUrl = PLATFORM_SERVICES[platform];
     const serviceUrl = PLATFORM_SERVICES[platform];
     if (!serviceUrl) {
     if (!serviceUrl) {
       results[resultKey] = { success: false, error: 'Unknown platform' };
       results[resultKey] = { success: false, error: 'Unknown platform' };