GuglielmoTor commited on
Commit
7f592b2
·
verified ·
1 Parent(s): fed4e5b

Create sync_logic.py

Browse files
Files changed (1) hide show
  1. sync_logic.py +353 -0
sync_logic.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # sync_logic.py
2
+ """
3
+ Handles the logic for syncing LinkedIn data: posts, mentions, and follower statistics.
4
+ Fetches data from LinkedIn APIs and uploads to Bubble.
5
+ """
6
+ import pandas as pd
7
+ import logging
8
+ import html
9
+
10
+ # Assuming Bubble_API_Calls contains bulk_upload_to_bubble
11
+ from Bubble_API_Calls import bulk_upload_to_bubble
12
+
13
+ # Assuming Linkedin_Data_API_Calls contains all necessary LinkedIn data fetching and processing functions
14
+ from Linkedin_Data_API_Calls import (
15
+ fetch_linkedin_posts_core,
16
+ fetch_comments,
17
+ analyze_sentiment, # For post comments
18
+ compile_detailed_posts,
19
+ prepare_data_for_bubble, # For posts, stats, comments
20
+ fetch_linkedin_mentions_core,
21
+ analyze_mentions_sentiment, # For individual mentions
22
+ compile_detailed_mentions, # Compiles to user-specified format
23
+ prepare_mentions_for_bubble # Prepares user-specified format for Bubble
24
+ )
25
+ # Assuming linkedin_follower_stats.py contains get_linkedin_follower_stats
26
+ from linkedin_follower_stats import get_linkedin_follower_stats
27
+
28
+ # Assuming config.py contains all necessary constants
29
+ from config import (
30
+ LINKEDIN_POST_URN_KEY, BUBBLE_POST_URN_COLUMN_NAME, BUBBLE_POSTS_TABLE_NAME,
31
+ BUBBLE_POST_STATS_TABLE_NAME, BUBBLE_POST_COMMENTS_TABLE_NAME,
32
+ BUBBLE_MENTIONS_TABLE_NAME, BUBBLE_MENTIONS_ID_COLUMN_NAME, BUBBLE_MENTIONS_DATE_COLUMN_NAME,
33
+ DEFAULT_MENTIONS_INITIAL_FETCH_COUNT, DEFAULT_MENTIONS_UPDATE_FETCH_COUNT,
34
+ BUBBLE_FOLLOWER_STATS_TABLE_NAME, FOLLOWER_STATS_TYPE_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN,
35
+ FOLLOWER_STATS_ORG_URN_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN,
36
+ LINKEDIN_CLIENT_ID_ENV_VAR
37
+ )
38
+
39
+
40
+ def _sync_linkedin_posts_internal(token_state, fetch_count_for_posts_api):
41
+ """Internal logic for syncing LinkedIn posts."""
42
+ logging.info(f"Posts sync: Starting fetch for {fetch_count_for_posts_api} posts.")
43
+ client_id = token_state.get("client_id")
44
+ token_dict = token_state.get("token")
45
+ org_urn = token_state.get('org_urn')
46
+ bubble_posts_df_orig = token_state.get("bubble_posts_df", pd.DataFrame()).copy()
47
+ posts_sync_message = ""
48
+
49
+ try:
50
+ processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn, count=fetch_count_for_posts_api)
51
+
52
+ if not processed_raw_posts:
53
+ posts_sync_message = "Posts: None found via API. "
54
+ logging.info("Posts sync: No raw posts returned from API.")
55
+ return posts_sync_message, token_state
56
+
57
+ existing_post_urns = set()
58
+ if not bubble_posts_df_orig.empty and BUBBLE_POST_URN_COLUMN_NAME in bubble_posts_df_orig.columns:
59
+ existing_post_urns = set(bubble_posts_df_orig[BUBBLE_POST_URN_COLUMN_NAME].dropna().astype(str))
60
+
61
+ new_raw_posts = [p for p in processed_raw_posts if str(p.get(LINKEDIN_POST_URN_KEY)) not in existing_post_urns]
62
+
63
+ if not new_raw_posts:
64
+ posts_sync_message = "Posts: All fetched already in Bubble. "
65
+ logging.info("Posts sync: All fetched posts were already found in Bubble.")
66
+ return posts_sync_message, token_state
67
+
68
+ logging.info(f"Posts sync: Processing {len(new_raw_posts)} new raw posts.")
69
+ post_urns_to_process = [p[LINKEDIN_POST_URN_KEY] for p in new_raw_posts if p.get(LINKEDIN_POST_URN_KEY)]
70
+
71
+ all_comments_data = fetch_comments(client_id, token_dict, post_urns_to_process, stats_map)
72
+ sentiments_per_post = analyze_sentiment(all_comments_data) # Assumes analysis of comments for posts
73
+ detailed_new_posts = compile_detailed_posts(new_raw_posts, stats_map, sentiments_per_post)
74
+
75
+ li_posts, li_post_stats, li_post_comments = prepare_data_for_bubble(detailed_new_posts, all_comments_data)
76
+
77
+ if li_posts:
78
+ bulk_upload_to_bubble(li_posts, BUBBLE_POSTS_TABLE_NAME)
79
+ updated_posts_df = pd.concat([bubble_posts_df_orig, pd.DataFrame(li_posts)], ignore_index=True)
80
+ token_state["bubble_posts_df"] = updated_posts_df.drop_duplicates(subset=[BUBBLE_POST_URN_COLUMN_NAME], keep='last')
81
+ logging.info(f"Posts sync: Uploaded {len(li_posts)} new posts to Bubble.")
82
+
83
+ if li_post_stats:
84
+ bulk_upload_to_bubble(li_post_stats, BUBBLE_POST_STATS_TABLE_NAME)
85
+ logging.info(f"Posts sync: Uploaded {len(li_post_stats)} post_stats entries.")
86
+ if li_post_comments:
87
+ bulk_upload_to_bubble(li_post_comments, BUBBLE_POST_COMMENTS_TABLE_NAME)
88
+ logging.info(f"Posts sync: Uploaded {len(li_post_comments)} post_comments entries.")
89
+ posts_sync_message = f"Posts: Synced {len(li_posts)} new. "
90
+ else:
91
+ posts_sync_message = "Posts: No new ones to upload after processing. "
92
+ logging.info("Posts sync: No new posts were prepared for Bubble upload.")
93
+
94
+ except ValueError as ve:
95
+ posts_sync_message = f"Posts Error: {html.escape(str(ve))}. "
96
+ logging.error(f"Posts sync: ValueError: {ve}", exc_info=True)
97
+ except Exception as e:
98
+ logging.exception("Posts sync: Unexpected error during processing.")
99
+ posts_sync_message = f"Posts: Unexpected error ({type(e).__name__}). "
100
+ return posts_sync_message, token_state
101
+
102
+
103
+ def sync_linkedin_mentions(token_state):
104
+ """Fetches new LinkedIn mentions and uploads them to Bubble."""
105
+ logging.info("Starting LinkedIn mentions sync process.")
106
+ if not token_state or not token_state.get("token"):
107
+ logging.error("Mentions sync: Access denied. No LinkedIn token.")
108
+ return "Mentions: No token. ", token_state
109
+
110
+ client_id = token_state.get("client_id")
111
+ token_dict = token_state.get("token")
112
+ org_urn = token_state.get('org_urn')
113
+ # Work with a copy, original df in token_state will be updated at the end
114
+ bubble_mentions_df_orig = token_state.get("bubble_mentions_df", pd.DataFrame()).copy()
115
+
116
+ if not org_urn or not client_id or client_id == "ENV VAR MISSING":
117
+ logging.error("Mentions sync: Configuration error (Org URN or Client ID missing).")
118
+ return "Mentions: Config error. ", token_state
119
+
120
+ fetch_count_for_mentions_api = 0
121
+ mentions_sync_is_needed_now = False
122
+ if bubble_mentions_df_orig.empty:
123
+ mentions_sync_is_needed_now = True
124
+ fetch_count_for_mentions_api = DEFAULT_MENTIONS_INITIAL_FETCH_COUNT
125
+ logging.info("Mentions sync needed: Bubble DF empty. Fetching initial count.")
126
+ else:
127
+ if BUBBLE_MENTIONS_DATE_COLUMN_NAME not in bubble_mentions_df_orig.columns or \
128
+ bubble_mentions_df_orig[BUBBLE_MENTIONS_DATE_COLUMN_NAME].isnull().all():
129
+ mentions_sync_is_needed_now = True
130
+ fetch_count_for_mentions_api = DEFAULT_MENTIONS_INITIAL_FETCH_COUNT
131
+ logging.info(f"Mentions sync needed: Date column '{BUBBLE_MENTIONS_DATE_COLUMN_NAME}' missing or all null. Fetching initial count.")
132
+ else:
133
+ # Use a copy for date checks to avoid SettingWithCopyWarning if any modification were made
134
+ mentions_df_check = bubble_mentions_df_orig.copy()
135
+ mentions_df_check[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(mentions_df_check[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce', utc=True)
136
+ last_mention_date_utc = mentions_df_check[BUBBLE_MENTIONS_DATE_COLUMN_NAME].dropna().max()
137
+ if pd.isna(last_mention_date_utc) or \
138
+ (pd.Timestamp('now', tz='UTC').normalize() - last_mention_date_utc.normalize()).days >= 7:
139
+ mentions_sync_is_needed_now = True
140
+ fetch_count_for_mentions_api = DEFAULT_MENTIONS_UPDATE_FETCH_COUNT
141
+ logging.info(f"Mentions sync needed: Last mention date {last_mention_date_utc} is old or invalid. Fetching update count.")
142
+
143
+ if not mentions_sync_is_needed_now:
144
+ logging.info("Mentions data is fresh based on current check. No API fetch needed for mentions.")
145
+ return "Mentions: Up-to-date. ", token_state
146
+
147
+ logging.info(f"Mentions sync proceeding. Fetch count: {fetch_count_for_mentions_api}")
148
+ try:
149
+ processed_raw_mentions = fetch_linkedin_mentions_core(client_id, token_dict, org_urn, count=fetch_count_for_mentions_api)
150
+ if not processed_raw_mentions:
151
+ logging.info("Mentions sync: No new mentions found via API.")
152
+ return "Mentions: None found via API. ", token_state
153
+
154
+ existing_mention_ids = set()
155
+ if not bubble_mentions_df_orig.empty and BUBBLE_MENTIONS_ID_COLUMN_NAME in bubble_mentions_df_orig.columns:
156
+ existing_mention_ids = set(bubble_mentions_df_orig[BUBBLE_MENTIONS_ID_COLUMN_NAME].dropna().astype(str))
157
+
158
+ sentiments_map = analyze_mentions_sentiment(processed_raw_mentions)
159
+ all_compiled_mentions = compile_detailed_mentions(processed_raw_mentions, sentiments_map)
160
+
161
+ new_compiled_mentions_to_upload = [
162
+ m for m in all_compiled_mentions if str(m.get("id")) not in existing_mention_ids
163
+ ]
164
+
165
+ if not new_compiled_mentions_to_upload:
166
+ logging.info("Mentions sync: All fetched mentions are already in Bubble.")
167
+ return "Mentions: All fetched already in Bubble. ", token_state
168
+
169
+ bubble_ready_mentions = prepare_mentions_for_bubble(new_compiled_mentions_to_upload)
170
+ if bubble_ready_mentions:
171
+ bulk_upload_to_bubble(bubble_ready_mentions, BUBBLE_MENTIONS_TABLE_NAME)
172
+ logging.info(f"Successfully uploaded {len(bubble_ready_mentions)} new mentions to Bubble.")
173
+ updated_mentions_df = pd.concat([bubble_mentions_df_orig, pd.DataFrame(bubble_ready_mentions)], ignore_index=True)
174
+ token_state["bubble_mentions_df"] = updated_mentions_df.drop_duplicates(subset=[BUBBLE_MENTIONS_ID_COLUMN_NAME], keep='last')
175
+ return f"Mentions: Synced {len(bubble_ready_mentions)} new. ", token_state
176
+ else:
177
+ logging.info("Mentions sync: No new mentions were prepared for Bubble upload.")
178
+ return "Mentions: No new ones to upload. ", token_state
179
+ except ValueError as ve:
180
+ logging.error(f"ValueError during mentions sync: {ve}", exc_info=True)
181
+ return f"Mentions Error: {html.escape(str(ve))}. ", token_state
182
+ except Exception as e:
183
+ logging.exception("Unexpected error in sync_linkedin_mentions.")
184
+ return f"Mentions: Unexpected error ({type(e).__name__}). ", token_state
185
+
186
+
187
+ def sync_linkedin_follower_stats(token_state):
188
+ """Fetches new LinkedIn follower statistics and uploads them to Bubble."""
189
+ logging.info("Starting LinkedIn follower stats sync process.")
190
+ if not token_state or not token_state.get("token"):
191
+ logging.error("Follower Stats sync: Access denied. No LinkedIn token.")
192
+ return "Follower Stats: No token. ", token_state
193
+
194
+ client_id = token_state.get("client_id")
195
+ token_dict = token_state.get("token")
196
+ org_urn = token_state.get('org_urn')
197
+ bubble_follower_stats_df_orig = token_state.get("bubble_follower_stats_df", pd.DataFrame()).copy()
198
+
199
+ if not org_urn or not client_id or client_id == "ENV VAR MISSING":
200
+ logging.error("Follower Stats sync: Configuration error (Org URN or Client ID missing).")
201
+ return "Follower Stats: Config error. ", token_state
202
+
203
+ follower_stats_sync_is_needed_now = False
204
+ if bubble_follower_stats_df_orig.empty:
205
+ follower_stats_sync_is_needed_now = True
206
+ logging.info("Follower stats sync needed: Bubble DF is empty.")
207
+ else:
208
+ monthly_gains_df_check = bubble_follower_stats_df_orig[bubble_follower_stats_df_orig[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_gains_monthly'].copy()
209
+ if monthly_gains_df_check.empty or FOLLOWER_STATS_CATEGORY_COLUMN not in monthly_gains_df_check.columns:
210
+ follower_stats_sync_is_needed_now = True
211
+ logging.info("Follower stats sync needed: Monthly gains data missing or date column absent.")
212
+ else:
213
+ monthly_gains_df_check.loc[:, FOLLOWER_STATS_CATEGORY_COLUMN] = pd.to_datetime(monthly_gains_df_check[FOLLOWER_STATS_CATEGORY_COLUMN], errors='coerce').dt.normalize()
214
+ last_gain_date = monthly_gains_df_check[FOLLOWER_STATS_CATEGORY_COLUMN].dropna().max()
215
+
216
+ if pd.isna(last_gain_date):
217
+ follower_stats_sync_is_needed_now = True
218
+ logging.info("Follower stats sync needed: No valid dates in monthly gains after conversion for check.")
219
+ else:
220
+ if last_gain_date.tzinfo is None or last_gain_date.tzinfo.utcoffset(last_gain_date) is None:
221
+ last_gain_date = last_gain_date.tz_localize('UTC')
222
+ else:
223
+ last_gain_date = last_gain_date.tz_convert('UTC')
224
+
225
+ start_of_current_month = pd.Timestamp('now', tz='UTC').normalize().replace(day=1)
226
+ if last_gain_date < start_of_current_month:
227
+ follower_stats_sync_is_needed_now = True
228
+ logging.info(f"Follower stats sync needed: Last gain date {last_gain_date} is old or invalid.")
229
+
230
+ if bubble_follower_stats_df_orig[bubble_follower_stats_df_orig[FOLLOWER_STATS_TYPE_COLUMN] != 'follower_gains_monthly'].empty:
231
+ follower_stats_sync_is_needed_now = True
232
+ logging.info("Follower stats sync needed: Demographic data (non-monthly) is missing.")
233
+
234
+ if not follower_stats_sync_is_needed_now:
235
+ logging.info("Follower stats data is fresh based on current check. No API fetch needed.")
236
+ return "Follower Stats: Data up-to-date. ", token_state
237
+
238
+ logging.info(f"Follower stats sync proceeding for org_urn: {org_urn}")
239
+ try:
240
+ api_follower_stats = get_linkedin_follower_stats(client_id, token_dict, org_urn)
241
+ if not api_follower_stats:
242
+ logging.info(f"Follower Stats sync: No stats found via API for org {org_urn}.")
243
+ return "Follower Stats: None found via API. ", token_state
244
+
245
+ new_stats_to_upload = []
246
+ api_monthly_gains = [s for s in api_follower_stats if s.get(FOLLOWER_STATS_TYPE_COLUMN) == 'follower_gains_monthly']
247
+ existing_monthly_gain_dates = set()
248
+ if not bubble_follower_stats_df_orig.empty:
249
+ bubble_monthly_df = bubble_follower_stats_df_orig[bubble_follower_stats_df_orig[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_gains_monthly']
250
+ if FOLLOWER_STATS_CATEGORY_COLUMN in bubble_monthly_df.columns:
251
+ existing_monthly_gain_dates = set(bubble_monthly_df[FOLLOWER_STATS_CATEGORY_COLUMN].astype(str).unique())
252
+
253
+ for gain_stat in api_monthly_gains:
254
+ if str(gain_stat.get(FOLLOWER_STATS_CATEGORY_COLUMN)) not in existing_monthly_gain_dates:
255
+ new_stats_to_upload.append(gain_stat)
256
+
257
+ api_demographics = [s for s in api_follower_stats if s.get(FOLLOWER_STATS_TYPE_COLUMN) != 'follower_gains_monthly']
258
+ existing_demographics_map = {}
259
+ if not bubble_follower_stats_df_orig.empty:
260
+ bubble_demographics_df = bubble_follower_stats_df_orig[bubble_follower_stats_df_orig[FOLLOWER_STATS_TYPE_COLUMN] != 'follower_gains_monthly']
261
+ if not bubble_demographics_df.empty and \
262
+ all(col in bubble_demographics_df.columns for col in [
263
+ FOLLOWER_STATS_ORG_URN_COLUMN, FOLLOWER_STATS_TYPE_COLUMN,
264
+ FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN,
265
+ FOLLOWER_STATS_PAID_COLUMN
266
+ ]):
267
+ for _, row in bubble_demographics_df.iterrows():
268
+ key = (
269
+ str(row[FOLLOWER_STATS_ORG_URN_COLUMN]),
270
+ str(row[FOLLOWER_STATS_TYPE_COLUMN]),
271
+ str(row[FOLLOWER_STATS_CATEGORY_COLUMN])
272
+ )
273
+ existing_demographics_map[key] = (
274
+ row[FOLLOWER_STATS_ORGANIC_COLUMN],
275
+ row[FOLLOWER_STATS_PAID_COLUMN]
276
+ )
277
+ for demo_stat in api_demographics:
278
+ key = (
279
+ str(demo_stat.get(FOLLOWER_STATS_ORG_URN_COLUMN)),
280
+ str(demo_stat.get(FOLLOWER_STATS_TYPE_COLUMN)),
281
+ str(demo_stat.get(FOLLOWER_STATS_CATEGORY_COLUMN))
282
+ )
283
+ api_counts = (
284
+ demo_stat.get(FOLLOWER_STATS_ORGANIC_COLUMN, 0),
285
+ demo_stat.get(FOLLOWER_STATS_PAID_COLUMN, 0)
286
+ )
287
+ if key not in existing_demographics_map or existing_demographics_map[key] != api_counts:
288
+ new_stats_to_upload.append(demo_stat)
289
+
290
+ if not new_stats_to_upload:
291
+ logging.info(f"Follower Stats sync: Data for org {org_urn} is up-to-date or no changes found.")
292
+ return "Follower Stats: Data up-to-date or no changes. ", token_state
293
+
294
+ bulk_upload_to_bubble(new_stats_to_upload, BUBBLE_FOLLOWER_STATS_TABLE_NAME)
295
+ logging.info(f"Successfully uploaded {len(new_stats_to_upload)} follower stat entries to Bubble for org {org_urn}.")
296
+
297
+ temp_df = pd.concat([bubble_follower_stats_df_orig, pd.DataFrame(new_stats_to_upload)], ignore_index=True)
298
+ monthly_part = temp_df[temp_df[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_gains_monthly'].drop_duplicates(
299
+ subset=[FOLLOWER_STATS_ORG_URN_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN],
300
+ keep='last'
301
+ )
302
+ demographics_part = temp_df[temp_df[FOLLOWER_STATS_TYPE_COLUMN] != 'follower_gains_monthly'].drop_duplicates(
303
+ subset=[FOLLOWER_STATS_ORG_URN_COLUMN, FOLLOWER_STATS_TYPE_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN],
304
+ keep='last'
305
+ )
306
+ token_state["bubble_follower_stats_df"] = pd.concat([monthly_part, demographics_part], ignore_index=True)
307
+ return f"Follower Stats: Synced {len(new_stats_to_upload)} entries. ", token_state
308
+ except ValueError as ve:
309
+ logging.error(f"ValueError during follower stats sync for {org_urn}: {ve}", exc_info=True)
310
+ return f"Follower Stats Error: {html.escape(str(ve))}. ", token_state
311
+ except Exception as e:
312
+ logging.exception(f"Unexpected error in sync_linkedin_follower_stats for {org_urn}.")
313
+ return f"Follower Stats: Unexpected error ({type(e).__name__}). ", token_state
314
+
315
+
316
+ def sync_all_linkedin_data_orchestrator(token_state):
317
+ """Orchestrates the syncing of all LinkedIn data types (Posts, Mentions, Follower Stats)."""
318
+ logging.info("Starting sync_all_linkedin_data_orchestrator process.")
319
+ if not token_state or not token_state.get("token"):
320
+ logging.error("Sync All: Access denied. LinkedIn token not available.")
321
+ return "<p style='color:red; text-align:center;'>❌ Access denied. LinkedIn token not available.</p>", token_state
322
+
323
+ org_urn = token_state.get('org_urn')
324
+ client_id = token_state.get("client_id") # Client ID should be in token_state from process_and_store_bubble_token
325
+
326
+ posts_sync_message = ""
327
+ mentions_sync_message = ""
328
+ follower_stats_sync_message = ""
329
+
330
+ if not org_urn:
331
+ logging.error("Sync All: Org URN missing in token_state.")
332
+ return "<p style='color:red;'>❌ Config error: Org URN missing.</p>", token_state
333
+ if not client_id or client_id == "ENV VAR MISSING": # Check client_id from token_state
334
+ logging.error("Sync All: Client ID missing or not set in token_state.")
335
+ return "<p style='color:red;'>❌ Config error: Client ID missing.</p>", token_state
336
+
337
+ # --- Sync Posts ---
338
+ fetch_count_for_posts_api = token_state.get('fetch_count_for_api', 0)
339
+ if fetch_count_for_posts_api == 0:
340
+ posts_sync_message = "Posts: Already up-to-date. "
341
+ logging.info("Posts sync: Skipped as fetch_count_for_posts_api is 0.")
342
+ else:
343
+ posts_sync_message, token_state = _sync_linkedin_posts_internal(token_state, fetch_count_for_posts_api)
344
+
345
+ # --- Sync Mentions ---
346
+ mentions_sync_message, token_state = sync_linkedin_mentions(token_state)
347
+
348
+ # --- Sync Follower Stats ---
349
+ follower_stats_sync_message, token_state = sync_linkedin_follower_stats(token_state)
350
+
351
+ logging.info(f"Sync process complete. Messages: Posts: [{posts_sync_message.strip()}], Mentions: [{mentions_sync_message.strip()}], Follower Stats: [{follower_stats_sync_message.strip()}]")
352
+ final_message = f"<p style='color:green; text-align:center;'>✅ Sync Attempted. {posts_sync_message} {mentions_sync_message} {follower_stats_sync_message}</p>"
353
+ return final_message, token_state