import os import logging import importlib import inspect import re logging.basicConfig( level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(message)s" ) log = logging.getLogger('werkzeug') log.setLevel(logging.DEBUG) os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1" import time from flask import send_from_directory from flask import Flask,request,redirect,make_response,jsonify,render_template_string import sqlite3,uuid,datetime,os,json import secrets from flask_cors import CORS import zoneinfo from user_agents import parse import pandas as pd from tika import parser import xmltodict import requests import datetime import sqlite3 from urllib.parse import urlencode from werkzeug.middleware.proxy_fix import ProxyFix from backend.destinations.destination_router import push_to_destination # Google OAuth from dotenv import load_dotenv from google_auth_oauthlib.flow import Flow # Scheduler from backend.scheduler.scheduler import start_scheduler PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) REAL_DB = os.getenv("DB_PATH", "/tmp/identity.db") _original_sqlite_connect = sqlite3.connect def fixed_connect(db_path, *args, **kwargs): if isinstance(db_path, str) and db_path.endswith("identity.db"): db_path = REAL_DB return _original_sqlite_connect(db_path, *args, **kwargs) sqlite3.connect = fixed_connect # AI from backend.ai.intent_engine import detect_intent from backend.ai.llm_engine import call_llm from backend.ai.executor import execute_intent from backend.ai.orchestrator import orchestrate #credentials security from backend.security.secure_db import encrypt_payload from backend.security.secure_db import decrypt_payload from backend.security.crypto import encrypt_value from backend.security.secure_fetch import auto_decrypt_row from backend.security.secure_fetch import ( fetchone_secure, fetchall_secure ) from backend.security.auth_routes import auth from backend.security.auth_middleware import load_logged_user def get_base_url(): """ Returns the base URL for the application. Prioritizes BASE_URL environment variable, falls back to request.host_url. """ return os.getenv("BASE_URL", request.host_url.rstrip("/")) def resolve_intent(message: str) -> dict: try: llm_intent = call_llm(message) if llm_intent and llm_intent.get("action") not in (None, "unknown"): connector = llm_intent.get("connector") # Normalize any LLM-suggested connector through the registry first. from backend.ai.registry import ALIAS_INDEX canonical = None if connector: canonical = ALIAS_INDEX.get(str(connector).lower()) return { "action": llm_intent.get("action"), "connectors": [canonical] if canonical else [], "raw": message, } except Exception as e: print(f"[LLM] Failed: {e}", flush=True) return detect_intent(message) # Connectors from backend.connectors.pinterest import ( pinterest_get_auth_url, pinterest_exchange_code, pinterest_save_token, sync_pinterest ) from backend.connectors.nvd import sync_nvd from backend.connectors.openstreetmap import sync_openstreetmap from backend.connectors.lemmy import sync_lemmy from backend.connectors.discourse import sync_discourse from backend.connectors.mastodon import sync_mastodon from backend.connectors.peertube import sync_peertube from backend.connectors.wikipedia import sync_wikipedia from backend.connectors.producthunt import sync_producthunt from backend.connectors.hackernews import sync_hackernews from backend.connectors.google_youtube import sync_youtube from backend.connectors.google_webfonts import sync_webfonts from backend.connectors.google_gcs import sync_gcs from backend.connectors.google_contacts import sync_contacts from backend.connectors.google_tasks import sync_tasks from backend.connectors.classroom import sync_classroom from backend.connectors.google_gmail import sync_gmail from backend.connectors.google_calendar import sync_calendar_files from backend.connectors.google_pagespeed import sync_pagespeed from backend.connectors.google_search_console import sync_search_console from backend.connectors.google_drive import sync_drive_files from backend.connectors.google_forms import sync_forms from backend.connectors.google_sheets import sync_sheets_files from backend.connectors.google_ga4 import sync_ga4 from backend.connectors.facebook_pages import sync_facebook_pages from backend.connectors.tiktok import ( get_tiktok_auth_url, handle_tiktok_oauth_callback, sync_tiktok, disconnect_tiktok ) from backend.connectors.taboola import ( connect_taboola, sync_taboola, disconnect_taboola, ) from backend.connectors.outbrain import ( connect_outbrain, sync_outbrain, disconnect_outbrain, ) from backend.connectors.similarweb import ( connect_similarweb, sync_similarweb, disconnect_similarweb, ) from backend.connectors.bigquery import ( connect_bigquery, sync_bigquery, disconnect_bigquery, ) from backend.connectors.x import ( get_x_auth_url, handle_x_oauth_callback, sync_x, disconnect_x ) from backend.connectors.linkedin import ( get_linkedin_auth_url, handle_linkedin_oauth_callback, sync_linkedin, disconnect_linkedin, ) from backend.connectors.chartbeat import ( connect_chartbeat, sync_chartbeat, disconnect_chartbeat, ) from backend.connectors.stripe import ( save_credentials as save_stripe_credentials, connect_stripe, sync_stripe, disconnect_stripe, ) from backend.connectors.socialinsider import ( connect_socialinsider, sync_socialinsider, disconnect_socialinsider, ) from backend.connectors.aws_rds import ( connect_rds, sync_rds, disconnect_rds, save_config as save_rds_config, ) from backend.connectors.dynamodb import ( connect_dynamodb, sync_dynamodb, disconnect_dynamodb, save_config as save_dynamodb_config, ) from backend.connectors.slack import ( connect_slack, sync_slack, disconnect_slack, save_config as save_slack_config, ) from backend.connectors.looker import connect_looker, sync_looker, disconnect_looker, save_config as save_looker_config from backend.connectors.superset import connect_superset, sync_superset, disconnect_superset, save_config as save_superset_config from backend.connectors.azure_blob import connect_azure_blob, sync_azure_blob, disconnect_azure_blob, save_config as save_azure_blob_config from backend.connectors.datadog import connect_datadog, sync_datadog, disconnect_datadog, save_config as save_datadog_config from backend.connectors.notion import ( connect_notion, sync_notion, disconnect_notion, save_config as save_notion_config, ) from backend.connectors.hubspot import ( connect_hubspot, sync_hubspot, disconnect_hubspot, save_config as save_hubspot_config, ) from backend.connectors.airtable import ( connect_airtable, sync_airtable, disconnect_airtable, save_config as save_airtable_config, ) from backend.connectors.shopify import ( connect_shopify, sync_shopify, disconnect_shopify, save_config as save_shopify_config, ) from backend.connectors.zendesk import ( connect_zendesk, sync_zendesk, disconnect_zendesk, save_config as save_zendesk_config, ) from backend.connectors.intercom import ( connect_intercom, sync_intercom, disconnect_intercom, save_config as save_intercom_config, ) from backend.connectors.mailchimp import ( connect_mailchimp, sync_mailchimp, disconnect_mailchimp, save_config as save_mailchimp_config, ) from backend.connectors.twilio import ( connect_twilio, sync_twilio, disconnect_twilio, save_config as save_twilio_config, ) from backend.connectors.pipedrive import ( connect_pipedrive, sync_pipedrive, disconnect_pipedrive, save_config as save_pipedrive_config, ) from backend.connectors.freshdesk import ( connect_freshdesk, sync_freshdesk, disconnect_freshdesk, save_config as save_freshdesk_config, ) from backend.connectors.klaviyo import ( connect_klaviyo, sync_klaviyo, disconnect_klaviyo, save_config as save_klaviyo_config, ) from backend.connectors.amplitude import ( connect_amplitude, sync_amplitude, disconnect_amplitude, save_config as save_amplitude_config, ) from backend.connectors.salesforce import ( save_config as save_salesforce_config, connect_salesforce, sync_salesforce, disconnect_salesforce, ) from backend.connectors.jira import ( save_config as save_jira_config, connect_jira, sync_jira, disconnect_jira, ) from backend.connectors.zoho_crm import ( save_config as save_zoho_crm_config, connect_zoho_crm, sync_zoho_crm, disconnect_zoho_crm, ) from backend.connectors.paypal import ( save_config as save_paypal_config, connect_paypal, sync_paypal, disconnect_paypal, ) from backend.connectors.asana import ( save_config as save_asana_config, connect_asana, sync_asana, disconnect_asana, ) from backend.connectors.sendgrid import ( save_config as save_sendgrid_config, connect_sendgrid, sync_sendgrid, disconnect_sendgrid, ) from backend.connectors.tableau import ( save_config as save_tableau_config, connect_tableau, sync_tableau, disconnect_tableau, ) from backend.connectors.power_bi import ( save_config as save_power_bi_config, connect_power_bi, sync_power_bi, disconnect_power_bi, ) from backend.connectors.workday import ( save_config as save_workday_config, connect_workday, sync_workday, disconnect_workday, ) from backend.connectors.ebay import ( save_config as save_ebay_config, connect_ebay, sync_ebay, disconnect_ebay, ) from backend.connectors.mixpanel import ( save_config as save_mixpanel_config, connect_mixpanel, sync_mixpanel, disconnect_mixpanel, ) from backend.connectors.monday import ( save_config as save_monday_config, connect_monday, sync_monday, disconnect_monday, ) from backend.connectors.clickup import ( save_config as save_clickup_config, connect_clickup, sync_clickup, disconnect_clickup, ) from backend.connectors.helpscout import ( save_config as save_helpscout_config, connect_helpscout, sync_helpscout, disconnect_helpscout, ) from backend.connectors.okta import ( save_config as save_okta_config, connect_okta, sync_okta, disconnect_okta, ) from backend.connectors.auth0 import ( save_config as save_auth0_config, connect_auth0, sync_auth0, disconnect_auth0, ) from backend.connectors.cloudflare import ( save_config as save_cloudflare_config, connect_cloudflare, sync_cloudflare, disconnect_cloudflare, ) from backend.connectors.sentry import ( save_config as save_sentry_config, connect_sentry, sync_sentry, disconnect_sentry, ) from backend.connectors.openai import ( save_config as save_openai_config, connect_openai, sync_openai, disconnect_openai, ) from backend.connectors.huggingface import ( save_config as save_huggingface_config, connect_huggingface, sync_huggingface, disconnect_huggingface, ) from backend.connectors.airflow import ( save_config as save_airflow_config, connect_airflow, sync_airflow, disconnect_airflow, ) from backend.connectors.kafka import ( save_config as save_kafka_config, connect_kafka, sync_kafka, disconnect_kafka, ) from backend.connectors.dbt import ( connect_dbt, sync_dbt, disconnect_dbt, save_config as save_dbt_config, ) from backend.connectors.typeform import ( connect_typeform, sync_typeform, disconnect_typeform, save_config as save_typeform_config, ) from backend.connectors.surveymonkey import ( connect_surveymonkey, sync_surveymonkey, disconnect_surveymonkey, save_config as save_surveymonkey_config, ) from backend.connectors.pinecone import ( connect_pinecone, sync_pinecone, disconnect_pinecone, save_config as save_pinecone_config, ) from backend.connectors.bitbucket import ( connect_bitbucket, sync_bitbucket, disconnect_bitbucket, save_config as save_bitbucket_config, ) from backend.connectors.vercel import ( connect_vercel, sync_vercel, disconnect_vercel, save_config as save_vercel_config, ) from backend.connectors.netlify import ( connect_netlify, sync_netlify, disconnect_netlify, save_config as save_netlify_config, ) from backend.connectors.linear import ( connect_linear, sync_linear, disconnect_linear, save_config as save_linear_config, ) from backend.connectors import quickbooks from backend.connectors import xero from backend.connectors import amazon_seller from backend.connectors import newrelic # ---------------- CONFIG ---------------- load_dotenv() IST = zoneinfo.ZoneInfo("Asia/Kolkata") app = Flask(__name__) app.config.update( SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_PATH="/", ) app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) CORS( app, supports_credentials=True, origins="*" ) # Start scheduler once when API server starts start_scheduler() from flask import g @app.before_request def load_logged_user(): g.user_id = None # Scheduler / internal UID internal_uid = request.headers.get("X-Internal-UID") if internal_uid: g.user_id = internal_uid return # Normal logged-in user via session session_id = request.cookies.get("segmento_session") if not session_id: return con = sqlite3.connect(REAL_DB) cur = con.cursor() cur.execute(""" SELECT user_id FROM user_sessions WHERE session_id=? """, (session_id,)) row = cur.fetchone() con.close() if row: g.user_id = row[0] # ---------------- USAGE AUTO SYNC DETECTOR ---------------- @app.before_request def usage_sync_start(): path = request.path if "/sync/" not in path: return uid = get_uid() if not uid: return try: source = path.split("/sync/")[-1] mode = request.args.get("mode", "manual") if mode not in ["manual", "scheduled"]: mode = "manual" g.sync_run_id = log_sync_start( uid, source, mode ) print(f"[USAGE] Sync START → {source} ({mode})", flush=True) except Exception as e: print("[USAGE START ERROR]", e, flush=True) # ---------------- API USAGE LOGGER ---------------- @app.before_request def log_api_usage(): path = request.path # ignore static + health endpoints if ( path.startswith("/static") or path.startswith("/__") or "favicon" in path ): return uid = getattr(g, "user_id", None) if not uid: return try: con = get_db() cur = con.cursor() cur.execute(""" INSERT INTO api_usage_logs (uid, endpoint, method, created_at) VALUES (?, ?, ?, ?) """, ( uid, path, request.method, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() except Exception as e: print("[API LOG ERROR]", e, flush=True) @app.after_request def usage_sync_finish(response): if not hasattr(g, "sync_run_id"): return response try: status = "success" if response.status_code == 200 else "failed" log_sync_finish( g.sync_run_id, 0, status ) print("[USAGE] Sync FINISHED", flush=True) except Exception as e: print("[USAGE FINISH ERROR]", e, flush=True) return response @app.route("/__ping") def ping(): return "IDENTITY OK" @auth.route("/auth/me") def me(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify({ "user_id": uid }) app.register_blueprint(auth) UPLOAD_FOLDER = os.getenv("UPLOAD_DIR", "/tmp/uploads") os.makedirs(UPLOAD_FOLDER, exist_ok=True) # ✓ CHANGED: Use absolute path to root database, not relative "identity.db" DB = REAL_DB # ---------------- GOOGLE CONFIG ---------------- GOOGLE_SCOPES = [ # Contacts "https://www.googleapis.com/auth/contacts.readonly", # Cloud Storage "https://www.googleapis.com/auth/devstorage.read_only", # Tasks "https://www.googleapis.com/auth/tasks.readonly", # Classroom "https://www.googleapis.com/auth/classroom.courses.readonly", "https://www.googleapis.com/auth/classroom.rosters.readonly", "https://www.googleapis.com/auth/classroom.coursework.students.readonly", "https://www.googleapis.com/auth/classroom.student-submissions.students.readonly", "https://www.googleapis.com/auth/classroom.announcements.readonly", # Youtube "https://www.googleapis.com/auth/youtube.readonly", # Gmail "https://www.googleapis.com/auth/gmail.readonly", # Drive & Sheets "https://www.googleapis.com/auth/drive.readonly", "https://www.googleapis.com/auth/spreadsheets.readonly", # Search Console "https://www.googleapis.com/auth/webmasters.readonly", # Analytics "https://www.googleapis.com/auth/analytics.readonly", # Forms "https://www.googleapis.com/auth/forms.responses.readonly", "https://www.googleapis.com/auth/forms.body.readonly", # Calendar "https://www.googleapis.com/auth/calendar.readonly" ] GOOGLE_CLIENT_CONFIG = { "web": { "client_id": os.getenv("GOOGLE_CLIENT_ID"), "client_secret": os.getenv("GOOGLE_CLIENT_SECRET"), "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "redirect_uris": [os.getenv("GOOGLE_REDIRECT_URI")] } } #-------------Helper Fucntion--------------- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.getenv("DB_PATH", "/tmp/identity.db") def get_db(): con = sqlite3.connect( DB, timeout=60, isolation_level=None, # autocommit check_same_thread=False ) con.execute("PRAGMA journal_mode=WAL;") con.execute("PRAGMA busy_timeout=60000;") con.execute("PRAGMA synchronous=NORMAL;") return con from flask import request, g def get_uid(): # Priority 1 — Logged web user if getattr(g, "user_id", None): return g.user_id # Priority 2 — Scheduler / internal calls internal_uid = request.headers.get("X-Internal-UID") if internal_uid: return internal_uid # Priority 3 — legacy cookie fallback uid_cookie = request.cookies.get("uid") if uid_cookie: return uid_cookie return None # CONNECTOR INITIALIZATION def ensure_connector_initialized(uid, source): con = get_db() cur = con.cursor() cur.execute(""" INSERT OR IGNORE INTO google_connections (uid, source, enabled) VALUES (?, ?, 0) """, (uid, source)) con.commit() con.close() GOOGLE_OAUTH_SOURCES = { "gmail", "drive", "calendar", "sheets", "forms", "classroom", "contacts", "tasks", "ga4", "search-console", "youtube", } OAUTH_SOURCES = GOOGLE_OAUTH_SOURCES | { "pinterest", "github", "instagram", "tiktok", "x", "linkedin", } CONNECT_MODULE_OVERRIDES = { "search-console": "google_search_console", "books": "googlebooks", "factcheck": "googlefactcheck", "news": "googlenews", "webfonts": "google_webfonts", } CONNECT_FUNCTION_OVERRIDES = { "aws_rds": "connect_rds", } OAUTH_REDIRECTS = { "github": "/github/connect", "instagram": "/instagram/connect", "tiktok": "/connectors/tiktok/connect", "x": "/connectors/x/connect", "linkedin": "/connectors/linkedin/connect", } def _is_internal_ai_request(): return bool(request.headers.get("X-Internal-UID")) def _has_connector_config(uid, source): con = get_db() cur = con.cursor() cur.execute( """ SELECT 1 FROM connector_configs WHERE uid=? AND connector=? LIMIT 1 """, (uid, source), ) row = fetchone_secure(cur) con.close() return bool(row) def _is_connector_enabled(uid, source): con = get_db() cur = con.cursor() cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source=? LIMIT 1 """, (uid, source), ) row = fetchone_secure(cur) con.close() return bool(row and row.get("enabled") == 1) def _oauth_redirect_for(source): if source in GOOGLE_OAUTH_SOURCES: return f"/google/connect?source={source}" return OAUTH_REDIRECTS.get(source) def _has_google_oauth_completion(uid, source): con = get_db() cur = con.cursor() cur.execute( """ SELECT 1 FROM google_accounts WHERE uid=? AND source=? LIMIT 1 """, (uid, source), ) row = fetchone_secure(cur) con.close() return bool(row) def _has_oauth_completion(uid, source): con = get_db() cur = con.cursor() try: if source in GOOGLE_OAUTH_SOURCES: cur.execute( """ SELECT access_token FROM google_accounts WHERE uid=? AND source=? LIMIT 1 """, (uid, source), ) elif source == "github": cur.execute("SELECT access_token FROM github_tokens WHERE uid=? LIMIT 1", (uid,)) elif source == "instagram": cur.execute( """ SELECT access_token FROM instagram_connections WHERE uid=? LIMIT 1 """, (uid,), ) elif source == "tiktok": cur.execute( """ SELECT access_token FROM tiktok_connections WHERE uid=? LIMIT 1 """, (uid,), ) elif source == "x": cur.execute( """ SELECT access_token FROM x_connections WHERE uid=? LIMIT 1 """, (uid,), ) elif source == "linkedin": cur.execute( """ SELECT access_token FROM linkedin_connections WHERE uid=? LIMIT 1 """, (uid,), ) else: return False row = fetchone_secure(cur) if not row: return False value = next(iter(row.values())) if isinstance(row, dict) else row[0] return bool(value and str(value).strip()) finally: con.close() def _load_connector_module(source): module_name = CONNECT_MODULE_OVERRIDES.get(source, source.replace("-", "_")) return importlib.import_module(f"backend.connectors.{module_name}") def _get_connector_connect_callable(source): module = _load_connector_module(source) function_name = CONNECT_FUNCTION_OVERRIDES.get( source, f"connect_{source.replace('-', '_')}", ) return getattr(module, function_name, None) def _run_connector_connect_probe(uid, source): try: connect_fn = _get_connector_connect_callable(source) except Exception: return None if not callable(connect_fn): return None try: return connect_fn(uid) except TypeError: return None except Exception as exc: return {"status": "error", "error": str(exc)} def _normalize_probe_error(result): if not isinstance(result, dict): return "connection failed" error = result.get("error") or result.get("message") or "connection failed" error_text = str(error).strip().lower() if "missing" in error_text and "credential" in error_text: return "missing credentials" if "not configured" in error_text: return "missing credentials" return str(error) def _credential_template_path(source): try: from backend.ai.registry import get_connector_url template_name = get_connector_url(source).rstrip("/").split("/")[-1] except Exception: template_name = source.replace("-", "_") connectors_dir = os.path.join(PROJECT_ROOT, "frontend", "templates", "connectors") candidates = [ template_name, source, source.replace("-", "_"), source.replace("_", ""), source.replace("-", ""), ] seen = set() for candidate in candidates: if not candidate or candidate in seen: continue seen.add(candidate) path = os.path.join(connectors_dir, f"{candidate}.html") if os.path.exists(path): return path return None def _to_snake_case(name): text = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", str(name)) return text.replace("-", "_").strip().lower() def _extract_template_field_labels(source): path = _credential_template_path(source) if not path: return {}, [] try: html = open(path, "r", encoding="utf-8").read() except Exception: return {}, [] pattern = re.compile( r"]*>(.*?)\s*<(?:input|select|textarea)[^>]*id=\"([^\"]+)\"", re.IGNORECASE | re.DOTALL, ) by_key = {} ordered = [] for raw_label, raw_id in pattern.findall(html): label = re.sub(r"<[^>]+>", "", raw_label) label = " ".join(label.split()).strip() field_key = _to_snake_case(raw_id) if not label or not field_key: continue if field_key not in by_key: by_key[field_key] = label ordered.append(label) return by_key, ordered def get_required_fields(source): route_path = f"/connectors/{source}/save_app" endpoint = None for rule in app.url_map.iter_rules(): if rule.rule == route_path: endpoint = rule.endpoint break if not endpoint: return {} view_fn = app.view_functions.get(endpoint) if not view_fn: return {} try: src = inspect.getsource(view_fn) except Exception: return {} fields = [] for field in re.findall(r"data\.get\(\s*[\"']([a-zA-Z0-9_]+)[\"']", src): if field not in fields: fields.append(field) if not fields: return {} labels_by_key, ordered_labels = _extract_template_field_labels(source) required = {} for idx, field in enumerate(fields): label = labels_by_key.get(field) if not label and idx < len(ordered_labels): label = ordered_labels[idx] if not label: label = field.replace("_", " ").title() required[field] = label return required def _resolve_connector_contract(uid, source): has_config = _has_connector_config(uid, source) if source in OAUTH_SOURCES: if not has_config: return { "connected": False, "auth_required": False, "credentials_required": True, } oauth_done = _has_oauth_completion(uid, source) if not oauth_done: return { "connected": False, "auth_required": True, "credentials_required": False, } return { "connected": True, "auth_required": False, "credentials_required": False, } if not has_config: return { "connected": False, "auth_required": False, "credentials_required": True, } return { "connected": True, "auth_required": False, "credentials_required": False, } def _normalize_connect_response_payload(data, status_code, location=None): source = None path = request.path.rstrip("/") if path.startswith("/connectors/") and path.endswith("/connect"): parts = path.split("/") if len(parts) >= 4: source = parts[2] uid = getattr(g, "user_id", None) if uid and source: state = _resolve_connector_contract(uid, source) print("[CONNECTOR CONTRACT]", { "source": source, "state": state }, flush=True) if state["connected"]: return {"connected": True} if state["credentials_required"]: payload = {"connected": False, "error": "missing credentials"} required_fields = get_required_fields(source) if required_fields: payload["required_fields"] = required_fields return payload if state["auth_required"]: return { "connected": False, "auth_required": True, "redirect": _oauth_redirect_for(source) or location, } return {"connected": False, "error": "connection failed"} if location: return { "connected": False, "auth_required": True, "redirect": location, } if not isinstance(data, dict): return {"connected": False, "error": "Invalid connector response"} if data.get("connected") is True: return {"connected": True} if data.get("auth_required") and data.get("redirect"): return { "connected": False, "auth_required": True, "redirect": data["redirect"], } if data.get("status") in ("success", "connected"): return {"connected": True} error = data.get("error") or data.get("message") if status_code >= 400 and error: return {"connected": False, "error": str(error)} if error: return {"connected": False, "error": str(error)} normalized_error = _normalize_probe_error(data) if normalized_error == "missing credentials": payload = {"connected": False, "error": "missing credentials"} required_fields = get_required_fields(source) if required_fields: payload["required_fields"] = required_fields return payload return {"connected": False, "error": normalized_error} @app.after_request def normalize_connector_contracts(response): path = request.path.rstrip("/") if path.startswith("/api/status/") and response.status_code < 400: uid = getattr(g, "user_id", None) source = path.split("/", 3)[-1] if uid and source: state = _resolve_connector_contract(uid, source) normalized = jsonify(state) normalized.status_code = response.status_code return normalized if ( _is_internal_ai_request() and path.startswith("/connectors/") and path.endswith("/connect") ): location = response.headers.get("Location") if 300 <= response.status_code < 400 else None data = response.get_json(silent=True) or {} payload = _normalize_connect_response_payload(data, response.status_code, location=location) status_code = response.status_code if payload.get("connected") is True: status_code = 200 elif payload.get("auth_required"): status_code = 200 elif payload.get("error") == "missing credentials": status_code = 400 normalized = jsonify(payload) normalized.status_code = status_code return normalized return response # ---------------- USAGE: SYNC RUN LOGGER ---------------- def log_sync_start(uid, source, sync_type): con = get_db() cur = con.cursor() cur.execute(""" INSERT INTO sync_runs (uid, source, sync_type, started_at, status) VALUES (?, ?, ?, ?, ?) """, ( uid, source, sync_type, datetime.datetime.utcnow().isoformat(), "running" )) run_id = cur.lastrowid con.commit() con.close() return run_id def log_sync_finish(run_id, rows_synced, status, error=None): con = get_db() cur = con.cursor() cur.execute(""" UPDATE sync_runs SET rows_synced=?, finished_at=?, status=?, error=? WHERE id=? """, ( rows_synced, datetime.datetime.utcnow().isoformat(), status, error, run_id )) con.commit() con.close() # ---------------- DB INIT ---------------- def init_db(): con = get_db() cur = con.cursor() # AI Companion cur.execute(""" CREATE TABLE IF NOT EXISTS ai_chats( id TEXT PRIMARY KEY, user_id TEXT, title TEXT, created_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS ai_messages( id TEXT PRIMARY KEY, chat_id TEXT, role TEXT, content TEXT, created_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS ai_state( chat_id TEXT PRIMARY KEY, uid TEXT, state_json TEXT, updated_at TEXT ) """) # Visits cur.execute(""" CREATE TABLE IF NOT EXISTS visits( id INTEGER PRIMARY KEY, uid TEXT,domain TEXT,browser TEXT,os TEXT,device TEXT,ip TEXT, screen TEXT,language TEXT,timezone TEXT, referrer TEXT,page_url TEXT,user_agent TEXT, name TEXT,age INTEGER,gender TEXT,city TEXT,country TEXT,profession TEXT, ts TEXT) """) # Identity cur.execute(""" CREATE TABLE IF NOT EXISTS identity_map( id INTEGER PRIMARY KEY, uid TEXT,email TEXT,device_id TEXT, session_id TEXT,external_id TEXT,created_at TEXT) """) # Events cur.execute(""" CREATE TABLE IF NOT EXISTS web_events( id INTEGER PRIMARY KEY, uid TEXT,domain TEXT,event TEXT, device_id TEXT,session_id TEXT, meta TEXT,ts TEXT) """) # Files cur.execute(""" CREATE TABLE IF NOT EXISTS file_data( id INTEGER PRIMARY KEY, uid TEXT,filename TEXT,filetype TEXT,content TEXT,ts TEXT) """) # Forms cur.execute(""" CREATE TABLE IF NOT EXISTS form_data( id INTEGER PRIMARY KEY, uid TEXT,form_name TEXT,data TEXT,ts TEXT) """) # ---------------- PLATFORM USERS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS users( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, password TEXT, first_name TEXT, last_name TEXT, company_name TEXT, company_size TEXT, country TEXT, phone TEXT, company_logo TEXT, is_individual INTEGER DEFAULT 0, created_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS user_sessions( session_id TEXT PRIMARY KEY, user_id TEXT, created_at TEXT, expires_at TEXT ) """) cur.execute("PRAGMA table_info(user_sessions)") columns = [col[1] for col in cur.fetchall()] if "expires_at" not in columns: cur.execute("ALTER TABLE user_sessions ADD COLUMN expires_at TEXT") # Google Accounts cur.execute(""" CREATE TABLE IF NOT EXISTS google_accounts( id INTEGER PRIMARY KEY, uid TEXT, source TEXT, access_token TEXT, refresh_token TEXT, scopes TEXT, created_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS google_connections ( uid TEXT, source TEXT, enabled INTEGER DEFAULT 1, PRIMARY KEY (uid, source) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS connector_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, source TEXT, sync_type TEXT, schedule_time TEXT, enabled INTEGER DEFAULT 1, created_at TEXT, last_run_at TEXT, UNIQUE (uid, source) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS connector_state ( uid TEXT, source TEXT, state_json TEXT, updated_at TEXT, PRIMARY KEY (uid, source) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS destination_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, source TEXT, dest_type TEXT, host TEXT, port INTEGER, username TEXT, password TEXT, database_name TEXT, created_at TEXT, is_active INTEGER DEFAULT 1, format TEXT DEFAULT 'parquet' ) """) # Drive cur.execute(""" CREATE TABLE IF NOT EXISTS drive_files( id INTEGER PRIMARY KEY, uid TEXT, file_id TEXT, name TEXT, mime_type TEXT, size INTEGER, created_time TEXT, fetched_at TEXT ) """) # Sheets cur.execute(""" CREATE TABLE IF NOT EXISTS sheets_data( id INTEGER PRIMARY KEY, uid TEXT, sheet_id TEXT, name TEXT, created_time TEXT, modified_time TEXT, fetched_at TEXT ) """) # Search Console cur.execute(""" CREATE TABLE IF NOT EXISTS gsc_queries( id INTEGER PRIMARY KEY, site TEXT, query TEXT, clicks INTEGER, impressions INTEGER, position REAL, fetched_at TEXT ) """) # GA4 Website Overview cur.execute(""" CREATE TABLE IF NOT EXISTS ga4_website_overview( id INTEGER PRIMARY KEY, uid TEXT, property_id TEXT, date TEXT, total_users INTEGER, new_users INTEGER, sessions INTEGER, views INTEGER, bounce_rate REAL, avg_session_duration REAL, fetched_at TEXT ) """) # GA4 Devices cur.execute(""" CREATE TABLE IF NOT EXISTS ga4_devices( id INTEGER PRIMARY KEY, uid TEXT, property_id TEXT, date TEXT, device TEXT, os TEXT, browser TEXT, users INTEGER, sessions INTEGER ) """) # GA4 Locations cur.execute(""" CREATE TABLE IF NOT EXISTS ga4_locations( id INTEGER PRIMARY KEY, uid TEXT, property_id TEXT, date TEXT, country TEXT, region TEXT, city TEXT, users INTEGER, sessions INTEGER ) """) # GA4 Traffic cur.execute(""" CREATE TABLE IF NOT EXISTS ga4_traffic_sources( id INTEGER PRIMARY KEY, uid TEXT, property_id TEXT, date TEXT, source TEXT, medium TEXT, users INTEGER, sessions INTEGER ) """) # GA4 Events cur.execute(""" CREATE TABLE IF NOT EXISTS ga4_events( id INTEGER PRIMARY KEY, uid TEXT, property_id TEXT, date TEXT, event TEXT, count INTEGER, users INTEGER, fetched_at TEXT ) """) # Google Search Console cur.execute(""" CREATE TABLE IF NOT EXISTS google_search_console( id INTEGER PRIMARY KEY, uid TEXT, site_url TEXT, query TEXT, page TEXT, country TEXT, device TEXT, clicks INTEGER, impressions INTEGER, ctr REAL, position REAL, fetched_at TEXT ) """) # PageSpeed cur.execute(""" CREATE TABLE IF NOT EXISTS google_pagespeed( id INTEGER PRIMARY KEY, uid TEXT, url TEXT, strategy TEXT, categories TEXT, performance_score REAL, seo_score REAL, accessibility_score REAL, best_practices_score REAL, pwa_score REAL, raw_response TEXT, fetched_at TEXT ) """) #google_forms cur.execute(""" CREATE TABLE IF NOT EXISTS google_forms ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, form_id TEXT, title TEXT, description TEXT, responder_uri TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_responses cur.execute(""" CREATE TABLE IF NOT EXISTS google_form_responses ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, form_id TEXT, response_id TEXT, create_time TEXT, last_submitted_time TEXT, answers_json TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_calendar_colors cur.execute(""" CREATE TABLE IF NOT EXISTS google_calendar_colors ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_calendar_settings cur.execute(""" CREATE TABLE IF NOT EXISTS google_calendar_settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, setting_id TEXT, value TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_calendar_list cur.execute(""" CREATE TABLE IF NOT EXISTS google_calendar_list ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, calendar_id TEXT, summary TEXT, time_zone TEXT, access_role TEXT, raw_json TEXT, fetched_at TEXT ) """) # socialinsider cur.execute(""" CREATE TABLE IF NOT EXISTS socialinsider_connections ( uid TEXT PRIMARY KEY, api_key TEXT, platform TEXT, handle TEXT, created_at TEXT ) """) # slack cur.execute(""" CREATE TABLE IF NOT EXISTS slack_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, channel_id TEXT, name TEXT, is_private INTEGER, is_archived INTEGER, member_count INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS slack_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, user_id TEXT, name TEXT, real_name TEXT, is_bot INTEGER, deleted INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS slack_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, message_id TEXT, channel_id TEXT, user_id TEXT, ts TEXT, text TEXT, type TEXT, subtype TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS socialinsider_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, platform TEXT, handle TEXT, post_id TEXT, publish_date TEXT, content_type TEXT, engagement INTEGER, reach INTEGER, impressions INTEGER, saves INTEGER, video_views INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS socialinsider_profile_insights ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, platform TEXT, handle TEXT, follower_count INTEGER, follower_growth INTEGER, gender_distribution TEXT, age_distribution TEXT, geo_distribution TEXT, industry TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_calendar_events cur.execute(""" CREATE TABLE IF NOT EXISTS google_calendar_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, calendar_id TEXT, event_id TEXT, summary TEXT, start_time TEXT, end_time TEXT, status TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_gmail_profile cur.execute(""" CREATE TABLE IF NOT EXISTS google_gmail_profile ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, email_address TEXT, messages_total INTEGER, threads_total INTEGER, raw_json TEXT, fetched_at TEXT ) """) #google_gmail_labels cur.execute(""" CREATE TABLE IF NOT EXISTS google_gmail_labels ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, label_id TEXT, name TEXT, type TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_gmail_messages cur.execute(""" CREATE TABLE IF NOT EXISTS google_gmail_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, message_id TEXT, thread_id TEXT, snippet TEXT, internal_date TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_gmail_message_details cur.execute(""" CREATE TABLE IF NOT EXISTS google_gmail_message_details ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, message_id TEXT, thread_id TEXT, subject TEXT, sender TEXT, to_email TEXT, snippet TEXT, internal_date TEXT, payload_json TEXT, raw_json TEXT, fetched_at TEXT ) """) # ---------------- GOOGLE CLASSROOM ---------------- # # Courses cur.execute(""" CREATE TABLE IF NOT EXISTS google_classroom_courses ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, course_id TEXT UNIQUE, name TEXT, state TEXT, owner_id TEXT, raw_json TEXT, fetched_at TEXT ) """) # Teachers cur.execute(""" CREATE TABLE IF NOT EXISTS google_classroom_teachers ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, course_id TEXT, user_id TEXT, full_name TEXT, email TEXT, raw_json TEXT, fetched_at TEXT ) """) # Students cur.execute(""" CREATE TABLE IF NOT EXISTS google_classroom_students ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, course_id TEXT, user_id TEXT, full_name TEXT, email TEXT, raw_json TEXT, fetched_at TEXT ) """) # Announcements cur.execute(""" CREATE TABLE IF NOT EXISTS google_classroom_announcements ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, course_id TEXT, announcement_id TEXT, text TEXT, state TEXT, created_at TEXT, raw_json TEXT, fetched_at TEXT ) """) # Coursework cur.execute(""" CREATE TABLE IF NOT EXISTS google_classroom_coursework ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, course_id TEXT, work_id TEXT, title TEXT, work_type TEXT, state TEXT, max_points REAL, raw_json TEXT, fetched_at TEXT ) """) # Submissions cur.execute(""" CREATE TABLE IF NOT EXISTS google_classroom_submissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, course_id TEXT, work_id TEXT, submission_id TEXT, user_id TEXT, state TEXT, assigned_grade REAL, raw_json TEXT, fetched_at TEXT ) """) # google_tasks_lists cur.execute(""" CREATE TABLE IF NOT EXISTS google_tasks_lists ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, list_id TEXT UNIQUE, title TEXT, updated TEXT, raw_json TEXT, fetched_at TEXT ) """) # google_tasks_items cur.execute(""" CREATE TABLE IF NOT EXISTS google_tasks_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, task_id TEXT UNIQUE, list_id TEXT, title TEXT, status TEXT, due TEXT, completed TEXT, updated TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_contacts_persons cur.execute(""" CREATE TABLE IF NOT EXISTS google_contacts_persons ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, resource_name TEXT UNIQUE, etag TEXT, names TEXT, emails TEXT, phone_numbers TEXT, organizations TEXT, addresses TEXT, metadata_json TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_gcs_buckets cur.execute(""" CREATE TABLE IF NOT EXISTS google_gcs_buckets ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, project_id TEXT, bucket_name TEXT, location TEXT, storage_class TEXT, raw_json TEXT, fetched_at TEXT ) """) #google_gcs_objects cur.execute(""" CREATE TABLE IF NOT EXISTS google_gcs_objects ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, bucket_name TEXT, object_name TEXT, size INTEGER, content_type TEXT, updated TEXT, md5_hash TEXT, raw_json TEXT, fetched_at TEXT ) """) # google_webfonts cur.execute(""" CREATE TABLE IF NOT EXISTS google_webfonts ( id INTEGER PRIMARY KEY AUTOINCREMENT, family TEXT, category TEXT, version TEXT, kind TEXT, last_modified TEXT, variants TEXT, subsets TEXT, files_json TEXT, raw_json TEXT, fetched_at TEXT ) """) # Google YouTube Channels cur.execute(""" CREATE TABLE IF NOT EXISTS google_youtube_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, channel_id TEXT, title TEXT, description TEXT, subscribers INTEGER, views INTEGER, videos INTEGER, raw_json TEXT, fetched_at TEXT ) """) # Google YouTube Videos cur.execute(""" CREATE TABLE IF NOT EXISTS google_youtube_videos ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, channel_id TEXT, video_id TEXT, title TEXT, description TEXT, published_at TEXT, views INTEGER, likes INTEGER, comments INTEGER, raw_json TEXT, fetched_at TEXT ) """) # ---------------- REDDIT TABLES ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS reddit_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, access_token TEXT, refresh_token TEXT, expires_at TEXT, scopes TEXT, created_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS reddit_profiles ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, username TEXT, karma INTEGER, created_utc TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS reddit_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, post_id TEXT UNIQUE, subreddit TEXT, title TEXT, author TEXT, score INTEGER, num_comments INTEGER, created_utc TEXT, url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS reddit_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, message_id TEXT UNIQUE, author TEXT, subject TEXT, body TEXT, created_utc TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS reddit_tokens ( uid TEXT PRIMARY KEY, client_id TEXT, client_secret TEXT, username TEXT, password TEXT, access_token TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS reddit_state ( uid TEXT PRIMARY KEY, last_created_utc INTEGER ) """) # ---------------- TELEGRAM TABLES ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS telegram_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, bot_token TEXT, created_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS telegram_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, channel_id TEXT, username TEXT, title TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS telegram_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, channel_id TEXT, message_id TEXT, text TEXT, author TEXT, date INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS telegram_state ( uid TEXT PRIMARY KEY, last_update_id INTEGER ) """) # ---------------- MEDIUM ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS medium_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, title TEXT, link TEXT, author TEXT, published TEXT, summary TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS medium_accounts ( uid TEXT PRIMARY KEY, username TEXT, created_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS medium_state ( uid TEXT PRIMARY KEY, last_published TEXT ) """) # ---------------- QUORA ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS quora_answers ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, question TEXT, answer_url TEXT, author TEXT, upvotes INTEGER, content TEXT, raw_html TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS quora_profile ( uid TEXT PRIMARY KEY, username TEXT, full_name TEXT, bio TEXT, followers INTEGER, raw_html TEXT, fetched_at TEXT ) """) # ---------------- TUMBLR ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS tumblr_blogs ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, blog_name TEXT, title TEXT, description TEXT, posts_count INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS tumblr_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, blog_name TEXT, post_id TEXT, post_type TEXT, title TEXT, body TEXT, url TEXT, timestamp INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS tumblr_state ( uid TEXT, blog_name TEXT, last_post_id INTEGER, PRIMARY KEY(uid, blog_name) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS tumblr_accounts ( uid TEXT PRIMARY KEY, api_key TEXT, created_at TEXT ) """) # ---------------- TWITCH ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS twitch_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, twitch_id TEXT, login TEXT, display_name TEXT, description TEXT, followers INTEGER, view_count INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS twitch_streams ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, twitch_id TEXT, title TEXT, game_name TEXT, viewer_count INTEGER, started_at TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS twitch_videos ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, twitch_id TEXT, video_id TEXT, title TEXT, views INTEGER, duration TEXT, created_at TEXT, raw_json TEXT, fetched_at TEXT ) """) # ---------------- DISCORD ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS discord_guilds ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, guild_id TEXT, name TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS discord_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, channel_id TEXT, guild_id TEXT, name TEXT, type INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS discord_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, channel_id TEXT, message_id TEXT, author TEXT, content TEXT, timestamp TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS discord_connections ( uid TEXT PRIMARY KEY, bot_token TEXT, created_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS discord_state ( uid TEXT, channel_id TEXT, last_message_id TEXT, PRIMARY KEY(uid, channel_id) ) """) # ---------------- GOOGLE BOOKS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS google_books_volumes ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, volume_id TEXT UNIQUE, title TEXT, authors TEXT, publisher TEXT, published_date TEXT, description TEXT, page_count INTEGER, categories TEXT, language TEXT, preview_link TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS google_books_state ( uid TEXT, query TEXT, last_index INTEGER, PRIMARY KEY (uid, query) ) """) # ---------------- GOOGLE FACT CHECK ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS google_factcheck_claims ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, claim_id TEXT UNIQUE, text TEXT, claimant TEXT, claim_date TEXT, rating TEXT, review_publisher TEXT, review_url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS google_factcheck_state ( uid TEXT, query TEXT, next_page_token TEXT, PRIMARY KEY (uid, query) ) """) # ---------------- GOOGLE NEWS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS google_news_articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, article_id TEXT UNIQUE, query TEXT, title TEXT, link TEXT, source TEXT, published TEXT, summary TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS google_news_state ( uid TEXT, query TEXT, last_published TEXT, PRIMARY KEY (uid, query) ) """) # ---------------- GOOGLE TRENDS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS google_trends_interest ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, keyword TEXT, date TEXT, value INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS google_trends_related ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, keyword TEXT, type TEXT, query TEXT, value INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS google_trends_state ( uid TEXT, keyword TEXT, last_date TEXT, PRIMARY KEY (uid, keyword) ) """) # ---------------- DEV.TO ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS devto_articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, article_id INTEGER UNIQUE, title TEXT, url TEXT, author TEXT, published_at TEXT, tags TEXT, reactions INTEGER, comments INTEGER, raw_json TEXT, fetched_at TEXT ) """) # ---------------- WHATSAPP ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS whatsapp_connections ( uid TEXT PRIMARY KEY, access_token_encrypted TEXT, waba_id TEXT, phone_number_id TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS whatsapp_business_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, waba_id TEXT, name TEXT, currency TEXT, timezone_id TEXT, messaging_limit TEXT, verification_status TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, waba_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS whatsapp_phone_numbers ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, waba_id TEXT, phone_number_id TEXT, display_phone_number TEXT, verified_name TEXT, quality_rating TEXT, status TEXT, platform_type TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, phone_number_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS whatsapp_message_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, waba_id TEXT, template_name TEXT, namespace TEXT, category TEXT, language TEXT, status TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, waba_id, template_name, language) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS whatsapp_conversation_analytics ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, phone_number_id TEXT, conversation_id TEXT, category TEXT, origin_type TEXT, start_time TEXT, end_time TEXT, messages_sent INTEGER, messages_received INTEGER, date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, conversation_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS whatsapp_message_insights ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, phone_number_id TEXT, sent INTEGER, delivered INTEGER, read INTEGER, failed INTEGER, date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, phone_number_id, date) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS devto_tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, name TEXT UNIQUE, popularity INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS devto_state ( uid TEXT, endpoint TEXT, last_page INTEGER, PRIMARY KEY (uid, endpoint) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS github_tokens ( uid TEXT PRIMARY KEY, access_token TEXT, scope TEXT, token_type TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS github_repos ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, repo_id INTEGER UNIQUE, name TEXT, full_name TEXT, private INTEGER, url TEXT, stars INTEGER, forks INTEGER, language TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS github_commits ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, repo_full TEXT, sha TEXT UNIQUE, author TEXT, message TEXT, date TEXT, url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS github_issues ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, issue_id INTEGER UNIQUE, repo_full TEXT, title TEXT, state TEXT, author TEXT, created_at TEXT, url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS github_state ( uid TEXT, repo_full TEXT, last_commit_sha TEXT, last_issue_updated TEXT, PRIMARY KEY (uid, repo_full) ) """) # ---------------- GITLAB ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS gitlab_tokens ( uid TEXT PRIMARY KEY, access_token TEXT, refresh_token TEXT, expires_at TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS gitlab_projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, project_id INTEGER UNIQUE, name TEXT, path TEXT, namespace TEXT, visibility TEXT, web_url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS gitlab_commits ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, project_id INTEGER, sha TEXT UNIQUE, author TEXT, message TEXT, date TEXT, web_url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS gitlab_issues ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, issue_id INTEGER UNIQUE, project_id INTEGER, title TEXT, state TEXT, author TEXT, created_at TEXT, web_url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS gitlab_merge_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, mr_id INTEGER UNIQUE, project_id INTEGER, title TEXT, state TEXT, author TEXT, created_at TEXT, web_url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS gitlab_state ( uid TEXT, project_id INTEGER, last_commit_sha TEXT, last_issue_updated TEXT, PRIMARY KEY (uid, project_id) ) """) # ---------------- STACKOVERFLOW ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS stack_tokens ( uid TEXT PRIMARY KEY, access_token TEXT, expires_at TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS stack_questions ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, question_id INTEGER UNIQUE, title TEXT, tags TEXT, score INTEGER, owner TEXT, created_at TEXT, link TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS stack_answers ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, answer_id INTEGER UNIQUE, question_id INTEGER, score INTEGER, owner TEXT, created_at TEXT, link TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS stack_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, user_id INTEGER UNIQUE, name TEXT, reputation INTEGER, profile_url TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS stack_state ( uid TEXT, endpoint TEXT, last_date INTEGER, PRIMARY KEY (uid, endpoint) ) """) # ---------------- HACKERNEWS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS hackernews_stories ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, story_id INTEGER, title TEXT, author TEXT, url TEXT, score INTEGER, descendants INTEGER, type TEXT, time INTEGER, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, story_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS hackernews_state ( uid TEXT PRIMARY KEY, last_story_id INTEGER ) """) # ---------------- PRODUCTHUNT ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS producthunt_tokens ( uid TEXT PRIMARY KEY, access_token TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS producthunt_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, post_id TEXT, name TEXT, tagline TEXT, votes INTEGER, comments INTEGER, created_at TEXT, url TEXT, topics TEXT, makers TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, post_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS producthunt_topics ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, topic_id TEXT, name TEXT, slug TEXT, followers INTEGER, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, topic_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS producthunt_state ( uid TEXT PRIMARY KEY, last_post_time TEXT ) """) # ---------------- WIKIPEDIA ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS wikipedia_recent_changes ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, rcid INTEGER, title TEXT, user TEXT, comment TEXT, timestamp TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, rcid) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS wikipedia_new_pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, page_id INTEGER, title TEXT, creator TEXT, created_at TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, page_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS wikipedia_most_viewed ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, article TEXT, views INTEGER, rank INTEGER, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, article) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS wikipedia_state ( uid TEXT PRIMARY KEY, last_rc_timestamp TEXT ) """) # ---------------- PEERTUBE ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS peertube_videos ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, instance TEXT, video_id TEXT, name TEXT, description TEXT, duration INTEGER, views INTEGER, likes INTEGER, dislikes INTEGER, published_at TEXT, channel_name TEXT, url TEXT, category TEXT, language TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, instance, video_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS peertube_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, instance TEXT, channel_id TEXT, name TEXT, display_name TEXT, followers INTEGER, url TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, instance, channel_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS peertube_state ( uid TEXT PRIMARY KEY, instance TEXT, last_published_at TEXT ) """) # ---------------- MASTODON ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS mastodon_statuses( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, instance TEXT, status_id TEXT, content TEXT, author TEXT, url TEXT, replies INTEGER, reblogs INTEGER, favourites INTEGER, created_at TEXT, visibility TEXT, language TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,instance,status_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS mastodon_tags( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, instance TEXT, tag TEXT, url TEXT, history TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,instance,tag) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS mastodon_state( uid TEXT PRIMARY KEY, instance TEXT, last_status_id TEXT ) """) # ---------------- DISCOURSE ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS discourse_topics( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, forum TEXT, topic_id INTEGER, title TEXT, posts_count INTEGER, views INTEGER, created_at TEXT, last_posted_at TEXT, slug TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,forum,topic_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS discourse_categories( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, forum TEXT, category_id INTEGER, name TEXT, description TEXT, topic_count INTEGER, post_count INTEGER, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,forum,category_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS discourse_users( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, forum TEXT, user_id INTEGER, username TEXT, name TEXT, trust_level INTEGER, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,forum,user_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS discourse_state( uid TEXT PRIMARY KEY, forum TEXT, last_topic_id INTEGER ) """) # ---------------- LEMMY ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS lemmy_posts( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, instance TEXT, post_id INTEGER, name TEXT, url TEXT, creator TEXT, community TEXT, score INTEGER, comments INTEGER, published TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,instance,post_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS lemmy_communities( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, instance TEXT, community_id INTEGER, name TEXT, title TEXT, subscribers INTEGER, posts INTEGER, comments INTEGER, published TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,instance,community_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS lemmy_users( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, instance TEXT, user_id INTEGER, username TEXT, display_name TEXT, posts INTEGER, comments INTEGER, published TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,instance,user_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS lemmy_state( uid TEXT PRIMARY KEY, instance TEXT, last_post_id INTEGER ) """) # ---------------- OPENSTREETMAP ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS osm_changesets( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, changeset_id INTEGER, user TEXT, uid_osm INTEGER, created_at TEXT, closed_at TEXT, min_lat REAL, min_lon REAL, max_lat REAL, max_lon REAL, raw_xml TEXT, fetched_at TEXT, UNIQUE(uid,changeset_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS osm_notes( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, note_id INTEGER, status TEXT, lat REAL, lon REAL, created_at TEXT, closed_at TEXT, comments INTEGER, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,note_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS osm_state( uid TEXT PRIMARY KEY, last_changeset_id INTEGER, last_note_id INTEGER ) """) # ---------------- NVD ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS nvd_cves( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, cve_id TEXT, source_identifier TEXT, published TEXT, last_modified TEXT, vuln_status TEXT, description TEXT, severity TEXT, cvss_score REAL, reference_url TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,cve_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS nvd_state( uid TEXT PRIMARY KEY, last_modified TEXT ) """) # ---------------- PINTEREST ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS pinterest_tokens( uid TEXT PRIMARY KEY, access_token TEXT, refresh_token TEXT, expires_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS pinterest_boards( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, board_id TEXT, name TEXT, description TEXT, privacy TEXT, url TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,board_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS pinterest_pins( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, pin_id TEXT, board_id TEXT, title TEXT, description TEXT, link TEXT, media_url TEXT, created_at TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid,pin_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS pinterest_state( uid TEXT PRIMARY KEY, last_pin_time TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS connector_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, connector TEXT, client_id TEXT, client_secret TEXT, api_key TEXT, access_token TEXT, refresh_token TEXT, scopes TEXT, status TEXT, created_at TEXT ) """) cur.execute("PRAGMA table_info(connector_configs)") columns = [col[1] for col in cur.fetchall()] required_columns = { "client_id": "TEXT", "client_secret": "TEXT", "config_json": "TEXT", } for col, col_type in required_columns.items(): if col not in columns: cur.execute(f"ALTER TABLE connector_configs ADD COLUMN {col} {col_type}") # ---------------- Meta ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, page_id TEXT, page_name TEXT, page_access_token TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS instagram_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, ig_account_id TEXT, access_token TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_pages_metadata ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, page_id TEXT, name TEXT, category TEXT, link TEXT, tasks TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_page_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, post_id TEXT UNIQUE, page_id TEXT, message TEXT, story TEXT, created_time TEXT, privacy TEXT, attachments TEXT, message_tags TEXT, reactions_count INTEGER, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_post_comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, comment_id TEXT UNIQUE, post_id TEXT, from_id TEXT, from_name TEXT, message TEXT, created_time TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_page_insights ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, page_id TEXT, metric_name TEXT, period TEXT, value TEXT, end_time TEXT, title TEXT, description TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_reactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, reaction_id TEXT UNIQUE, post_id TEXT, comment_id TEXT, type TEXT, user_psid TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_app_credentials ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, app_id TEXT, app_secret TEXT, redirect_uri TEXT, created_at TEXT ) """) # ---------------- FACEBOOK ADS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_ads_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, ad_account_id TEXT, ad_account_name TEXT, access_token TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_ad_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT UNIQUE, name TEXT, account_status TEXT, currency TEXT, timezone_name TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_ad_campaigns ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, campaign_id TEXT UNIQUE, account_id TEXT, name TEXT, status TEXT, objective TEXT, daily_budget TEXT, start_time TEXT, stop_time TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_ad_sets ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, adset_id TEXT UNIQUE, campaign_id TEXT, name TEXT, status TEXT, daily_budget TEXT, optimization_goal TEXT, start_time TEXT, end_time TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_ads ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, ad_id TEXT UNIQUE, adset_id TEXT, name TEXT, status TEXT, creative_id TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_ad_creatives ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, creative_id TEXT UNIQUE, name TEXT, object_story_id TEXT, raw_json TEXT, fetched_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS facebook_ads_insights ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT, campaign_id TEXT, adset_id TEXT, ad_id TEXT, date_start TEXT, date_stop TEXT, impressions TEXT, clicks TEXT, spend TEXT, ctr TEXT, cpc TEXT, cpm TEXT, reach TEXT, raw_json TEXT, fetched_at TEXT ) """) # ---------------- TIKTOK BUSINESS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS tiktok_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, advertiser_id TEXT, access_token TEXT, refresh_token TEXT, expires_at TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS tiktok_campaigns ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, advertiser_id TEXT, campaign_id TEXT, campaign_name TEXT, objective TEXT, status TEXT, budget TEXT, budget_type TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, campaign_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS tiktok_ads ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, advertiser_id TEXT, ad_id TEXT, campaign_id TEXT, adgroup_id TEXT, ad_name TEXT, status TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, ad_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS tiktok_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, advertiser_id TEXT, ad_id TEXT, campaign_id TEXT, adgroup_id TEXT, stat_time_day TEXT, impressions TEXT, clicks TEXT, spend TEXT, ctr TEXT, cpc TEXT, cpm TEXT, conversions TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, ad_id, stat_time_day) ) """) # ---------------- TABOOLA BACKSTAGE ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS taboola_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, account_id TEXT, access_token TEXT, expires_at TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS taboola_campaign_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT, dimension TEXT, dimension_value TEXT, campaign_id TEXT, campaign_name TEXT, impressions TEXT, clicks TEXT, ctr TEXT, spent TEXT, cpc TEXT, cpm TEXT, conversions TEXT, conversion_rate TEXT, roas TEXT, date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, account_id, dimension, dimension_value, campaign_id, date) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS taboola_ads ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT, campaign_id TEXT, campaign_name TEXT, item_id TEXT, item_name TEXT, thumbnail_url TEXT, url TEXT, impressions TEXT, clicks TEXT, ctr TEXT, cpc TEXT, spent TEXT, conversions TEXT, date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, account_id, campaign_id, item_id, date) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS taboola_publisher_revenue ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT, dimension TEXT, dimension_value TEXT, page_views TEXT, page_views_with_ads_pct TEXT, ad_revenue TEXT, ad_rpm TEXT, ad_cpc TEXT, date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, account_id, dimension, dimension_value, date) ) """) # ---------------- OUTBRAIN AMPLIFY ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS outbrain_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, marketer_id TEXT, access_token TEXT, expires_at TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS outbrain_marketers ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, marketer_id TEXT, name TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, marketer_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS outbrain_campaign_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, marketer_id TEXT, campaign_id TEXT, campaign_name TEXT, breakdown TEXT, impressions TEXT, clicks TEXT, ctr TEXT, spend TEXT, conversions TEXT, date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, marketer_id, campaign_id, breakdown, date) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS outbrain_ads ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, marketer_id TEXT, campaign_id TEXT, promoted_link_id TEXT, promoted_link_text TEXT, impressions TEXT, clicks TEXT, ctr TEXT, spend TEXT, conversions TEXT, date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, marketer_id, campaign_id, promoted_link_id, date) ) """) # ---------------- SIMILARWEB ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS similarweb_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, domain TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS similarweb_domain_overview ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, domain TEXT, date TEXT, visits TEXT, desktop_share TEXT, mobile_share TEXT, pages_per_visit TEXT, visit_duration TEXT, bounce_rate TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, domain, date) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS similarweb_traffic_sources ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, domain TEXT, date TEXT, direct_share TEXT, referral_share TEXT, organic_search_share TEXT, paid_search_share TEXT, social_share TEXT, mail_share TEXT, display_share TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, domain, date) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS similarweb_referrals ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, domain TEXT, date TEXT, referring_domain TEXT, referral_share TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, domain, date, referring_domain) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS similarweb_search_keywords ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, domain TEXT, date TEXT, keyword TEXT, search_volume TEXT, traffic_share TEXT, cpc TEXT, organic_vs_paid TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, domain, date, keyword) ) """) # ---------------- X (TWITTER) ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS x_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, x_user_id TEXT, username TEXT, access_token TEXT, refresh_token TEXT, expires_at TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS x_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, user_id TEXT, username TEXT, display_name TEXT, bio TEXT, location TEXT, followers_count INTEGER, following_count INTEGER, tweet_count INTEGER, profile_image_url TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, user_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS x_tweets ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, tweet_id TEXT, author_id TEXT, text TEXT, like_count INTEGER, retweet_count INTEGER, reply_count INTEGER, media_ids TEXT, created_at TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, tweet_id) ) """) # ---------------- LINKEDIN MARKETING ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS linkedin_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, linkedin_member_id TEXT, access_token TEXT, refresh_token TEXT, expires_at TEXT, linkedin_version TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS linkedin_ad_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT, name TEXT, status TEXT, type TEXT, currency TEXT, test TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, account_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS linkedin_campaigns ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT, campaign_id TEXT, name TEXT, status TEXT, daily_budget TEXT, objective TEXT, start_date TEXT, end_date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, campaign_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS linkedin_creatives ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT, creative_id TEXT, campaign_id TEXT, intended_status TEXT, is_serving TEXT, review_status TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, creative_id) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS linkedin_ad_analytics ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, account_id TEXT, pivot_type TEXT, pivot_value TEXT, impressions TEXT, clicks TEXT, cost_in_local_currency TEXT, date_start TEXT, date_end TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, account_id, pivot_type, pivot_value, date_start, date_end) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS chartbeat_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, host TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS stripe_connections ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE, account_id TEXT, display_name TEXT, connected_at TEXT ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS chartbeat_top_pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, path TEXT, title TEXT, concurrents INTEGER, engaged_time REAL, page_views INTEGER, visits INTEGER, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, path, fetched_at) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS chartbeat_page_engagement ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, path TEXT, title TEXT, author TEXT, section TEXT, device TEXT, referrer_type TEXT, page_views INTEGER, page_uniques INTEGER, page_avg_time REAL, page_total_time REAL, page_avg_scroll REAL, page_scroll_starts INTEGER, page_views_quality REAL, date TEXT, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, path, device, referrer_type, date) ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS chartbeat_video_engagement ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, video_title TEXT, video_path TEXT, play_state TEXT, video_plays INTEGER, video_loads INTEGER, video_play_rate REAL, video_avg_time REAL, raw_json TEXT, fetched_at TEXT, UNIQUE(uid, video_path, play_state, fetched_at) ) """) # ---------------- USAGE : SYNC RUN HISTORY ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS sync_runs( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, source TEXT, sync_type TEXT, rows_synced INTEGER DEFAULT 0, started_at TEXT, finished_at TEXT, status TEXT, error TEXT ) """) # ---------------- USAGE : DESTINATION PUSH LOGS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS destination_push_logs( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, source TEXT, destination_type TEXT, rows_pushed INTEGER, pushed_at TEXT, status TEXT, error TEXT ) """) # ---------------- USAGE : API CALL TRACKING ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS api_usage_logs( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, endpoint TEXT, method TEXT, created_at TEXT ) """) # ---------------- QUICKBOOKS ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS quickbooks_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id TEXT, client_secret TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS quickbooks_auth ( id INTEGER PRIMARY KEY AUTOINCREMENT, access_token TEXT, refresh_token TEXT, realm_id TEXT, expires_at REAL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) # ---------------- XERO ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS xero_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id TEXT, client_secret TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS xero_auth ( id INTEGER PRIMARY KEY AUTOINCREMENT, access_token TEXT, refresh_token TEXT, tenant_id TEXT, tenant_name TEXT, expires_at REAL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) # ---------------- AMAZON SELLER ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS amazon_seller_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id TEXT, client_secret TEXT, seller_id TEXT, region TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS amazon_seller_auth ( id INTEGER PRIMARY KEY AUTOINCREMENT, refresh_token TEXT, seller_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) # ---------------- NEW RELIC ---------------- cur.execute(""" CREATE TABLE IF NOT EXISTS newrelic_auth ( id INTEGER PRIMARY KEY AUTOINCREMENT, api_key TEXT, account_id TEXT, region TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """) con.commit() con.close() init_db() # ---------------- IDENTITY ---------------- @app.route("/sync") def sync(): r = request.args.get("return_url") uid = request.cookies.get("uid") or str(uuid.uuid4()) resp = make_response(redirect(f"{r}?uid={uid}")) resp.set_cookie("uid", uid, max_age=30*24*3600) return resp IFRAME = """""" @app.route("/iframe_sync") def iframe_sync(): uid = request.cookies.get("uid") or str(uuid.uuid4()) resp = make_response(render_template_string(IFRAME, uid=uid)) resp.set_cookie("uid", uid, max_age=30*24*3600) return resp # ---------------- WEB TRACKING ---------------- @app.route("/record",methods=["POST"]) def record(): d = request.get_json() or {} uid = d.get("uid") domain = d.get("domain") email = d.get("email") did = d.get("device_id") sid = d.get("session_id") event = d.get("event_type","page") meta = d.get("meta",{}) if not uid or not domain: return jsonify({"error":"missing"}),400 ua = request.headers.get("User-Agent") ip = request.remote_addr p = parse(ua) browser = p.browser.family os_name = p.os.family device = "Mobile" if p.is_mobile else "Tablet" if p.is_tablet else "Desktop" ts = datetime.datetime.now(IST).isoformat() con = get_db() cur = con.cursor() cur.execute(""" INSERT INTO visits (uid,domain,browser,os,device,ip, screen,language,timezone, referrer,page_url,user_agent,ts) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?) """,(uid,domain,browser,os_name,device,ip, meta.get("screen"),meta.get("language"),meta.get("timezone"), meta.get("referrer"),meta.get("page_url"),ua,ts)) cur.execute(""" INSERT INTO identity_map (uid,email,device_id,session_id,external_id,created_at) VALUES(?,?,?,?,?,?) """,(uid,email,did,sid,None,ts)) cur.execute(""" INSERT INTO web_events (uid,domain,event,device_id,session_id,meta,ts) VALUES(?,?,?,?,?,?,?) """,(uid,domain,event,did,sid,json.dumps(meta),ts)) con.commit() con.close() return jsonify({"status":"stored"}) # ---------------- PROFILE ---------------- @app.route("/profile",methods=["POST"]) def profile(): d = request.get_json() or {} uid = d.get("uid") email = d.get("email") if not uid: return jsonify({"error":"uid"}),400 con = get_db() cur = con.cursor() cur.execute(""" UPDATE visits SET name=?,age=?,gender=?,city=?,country=?,profession=? WHERE uid=? """,(d.get("name"),d.get("age"),d.get("gender"), d.get("city"),d.get("country"),d.get("profession"),uid)) if email: cur.execute("UPDATE identity_map SET email=? WHERE uid=?",(email,uid)) con.commit() con.close() return jsonify({"status":"saved"}) # ---------------- FILE UPLOAD ---------------- @app.route("/upload",methods=["POST"]) def upload(): uid = request.form.get("uid") f = request.files.get("file") if not f: return "No file",400 path = os.path.join(UPLOAD_FOLDER,f.filename) f.save(path) c = "" if f.filename.endswith((".csv",".xlsx")): df = pd.read_csv(path) if f.filename.endswith(".csv") else pd.read_excel(path) c = df.to_json() elif f.filename.endswith(".json"): c = json.dumps(json.load(open(path))) elif f.filename.endswith(".xml"): c = json.dumps(xmltodict.parse(open(path).read())) else: c = parser.from_file(path).get("content","") ts = datetime.datetime.now(IST).isoformat() con = get_db() cur = con.cursor() cur.execute(""" INSERT INTO file_data VALUES(NULL,?,?,?,?,?) """,(uid,f.filename,f.filename.split(".")[-1],c,ts)) con.commit() con.close() return jsonify({"status":"uploaded"}) # ---------------- API COLLECT ---------------- @app.route("/api/collect",methods=["POST"]) def api(): d = request.get_json() ts = datetime.datetime.now(IST).isoformat() con = get_db() cur = con.cursor() cur.execute(""" INSERT INTO api_data VALUES(NULL,?,?,?,?) """,(d.get("source"),d.get("endpoint"), json.dumps(d.get("data")),ts)) con.commit() con.close() return jsonify({"status":"ok"}) # ---------------- FORM ---------------- @app.route("/form/submit",methods=["POST"]) def form(): d = request.get_json() ts = datetime.datetime.now(IST).isoformat() con = get_db() cur = con.cursor() cur.execute(""" INSERT INTO form_data VALUES(NULL,?,?,?,?) """,(d.get("uid"),d.get("form"),json.dumps(d),ts)) con.commit() con.close() return jsonify({"status":"saved"}) @app.route("/api/status/") def connector_status(source): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 return jsonify(_resolve_connector_contract(uid, source)) # ---------------- GOOGLE OAUTH ---------------- @app.route("/google/connect") def google_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 source = request.args.get("source") if not source: return "Missing source parameter", 400 # Fetch Google App Credentials from DB con = get_db() cur = con.cursor() cur.execute(""" SELECT client_id, client_secret FROM connector_configs WHERE uid=? AND connector=? """, (uid, source)) row = fetchone_secure(cur) con.close() if not row: return "Google App credentials not saved", 400 client_id = row["client_id"] client_secret = row["client_secret"] # Define scopes dynamically (for now only gmail) if source == "gmail": scopes = ["https://www.googleapis.com/auth/gmail.readonly"] elif source == "drive": scopes = ["https://www.googleapis.com/auth/drive.readonly"] elif source == "calendar": scopes = ["https://www.googleapis.com/auth/calendar.readonly"] elif source == "sheets": scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"] elif source == "forms": scopes = [ "https://www.googleapis.com/auth/forms.responses.readonly", "https://www.googleapis.com/auth/forms.body.readonly" ] elif source == "classroom": scopes = [ "https://www.googleapis.com/auth/classroom.courses.readonly", "https://www.googleapis.com/auth/classroom.rosters.readonly", "https://www.googleapis.com/auth/classroom.coursework.students.readonly", "https://www.googleapis.com/auth/classroom.student-submissions.students.readonly", "https://www.googleapis.com/auth/classroom.announcements.readonly", ] elif source == "contacts": scopes = ["https://www.googleapis.com/auth/contacts.readonly"] elif source == "tasks": scopes = ["https://www.googleapis.com/auth/tasks.readonly"] elif source == "ga4": scopes = ["https://www.googleapis.com/auth/analytics.readonly"] elif source == "search-console": scopes = ["https://www.googleapis.com/auth/webmasters.readonly"] elif source == "youtube": scopes = ["https://www.googleapis.com/auth/youtube.readonly"] else: return "Unsupported Google connector", 400 flow = Flow.from_client_config( { "web": { "client_id": client_id, "client_secret": client_secret, "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token" } }, scopes=scopes, redirect_uri=get_base_url() + "/oauth/callback" ) auth_url, state = flow.authorization_url( access_type="offline", prompt="consent", include_granted_scopes="true", state=source ) return redirect(auth_url) @app.route("/oauth/callback") def unified_oauth_callback(): code = request.args.get("code") state = request.args.get("state") # state usually contains the source uid = getattr(g, "user_id", None) if not code: return "No code", 400 if not uid: return jsonify({"error": "Unauthorized"}), 401 source = state or "gmail" redirect_uri = get_base_url() + "/oauth/callback" # Handle Pinterest if source == "pinterest": from backend.connectors import pinterest res = pinterest.pinterest_exchange_code(uid, code, redirect_uri=redirect_uri) return redirect("/connectors/pinterest?connected=1") # Handle TikTok if source == "tiktok": from backend.connectors import tiktok res = tiktok.handle_tiktok_oauth_callback(uid, code, redirect_uri=redirect_uri) return redirect("/connectors/tiktok?connected=1") # Handle LinkedIn if source == "linkedin": from backend.connectors import linkedin res = linkedin.handle_linkedin_oauth_callback(uid, code, redirect_uri=redirect_uri) return redirect("/connectors/linkedin?connected=1") # Handle Instagram if source == "instagram": from backend.connectors import instagram res = instagram.handle_oauth_callback(uid, code, redirect_uri=redirect_uri) return redirect("/connectors/instagram?connected=1") # Handle X (Twitter) if source == "x": from backend.connectors import x res = x.handle_x_oauth_callback(uid, code, redirect_uri=redirect_uri) return redirect("/connectors/x?connected=1") # Handle Xero if source == "xero": from backend.connectors import xero return xero.callback_xero(uid=uid, redirect_uri=redirect_uri) # Handle Quickbooks if source == "quickbooks": from backend.connectors import quickbooks return quickbooks.callback_quickbooks(uid=uid, redirect_uri=redirect_uri) # Handle Amazon Seller if source == "amazon_seller": from backend.connectors import amazon_seller return amazon_seller.callback_amazon_seller(uid=uid, redirect_uri=redirect_uri) # Handle Github if source == "github": from backend.connectors import github res = github.callback_github(uid, code, redirect_uri=redirect_uri) return redirect("/connectors/github?connected=1") # Handle Gitlab if source == "gitlab": from backend.connectors import gitlab res = gitlab.exchange_code(uid, code, redirect_uri=redirect_uri) return redirect("/connectors/gitlab?connected=1") # Handle Google Connectors # Fetch Google App Credentials from DB con = get_db() cur = con.cursor() cur.execute(""" SELECT client_id, client_secret FROM connector_configs WHERE uid=? AND connector=? """, (uid, source)) row = fetchone_secure(cur) if not row: return "Google App credentials not saved", 400 client_id = row["client_id"] client_secret = row["client_secret"] # Define scopes if source == "gmail": scopes = ["https://www.googleapis.com/auth/gmail.readonly"] elif source == "drive": scopes = ["https://www.googleapis.com/auth/drive.readonly"] elif source == "calendar": scopes = ["https://www.googleapis.com/auth/calendar.readonly"] elif source == "sheets": scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"] elif source == "forms": scopes = [ "https://www.googleapis.com/auth/forms.responses.readonly", "https://www.googleapis.com/auth/forms.body.readonly" ] elif source == "classroom": scopes = [ "https://www.googleapis.com/auth/classroom.courses.readonly", "https://www.googleapis.com/auth/classroom.rosters.readonly", "https://www.googleapis.com/auth/classroom.coursework.students.readonly", "https://www.googleapis.com/auth/classroom.student-submissions.students.readonly", "https://www.googleapis.com/auth/classroom.announcements.readonly", ] elif source == "contacts": scopes = ["https://www.googleapis.com/auth/contacts.readonly"] elif source == "tasks": scopes = ["https://www.googleapis.com/auth/tasks.readonly"] elif source == "ga4": scopes = ["https://www.googleapis.com/auth/analytics.readonly"] elif source == "search-console": scopes = ["https://www.googleapis.com/auth/webmasters.readonly"] elif source == "youtube": scopes = ["https://www.googleapis.com/auth/youtube.readonly"] else: con.close() return "Unsupported Google connector", 400 flow = Flow.from_client_config( { "web": { "client_id": client_id, "client_secret": client_secret, "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token" } }, scopes=scopes, redirect_uri=get_base_url() + "/oauth/callback" ) flow.fetch_token( code=code, include_client_id=True ) creds = flow.credentials try: # Remove old token cur.execute(""" DELETE FROM google_accounts WHERE uid=? AND source=? """, (uid, source)) # Save new token cur.execute(""" INSERT INTO google_accounts (uid, source, access_token, refresh_token, scopes, created_at) VALUES (?, ?, ?, ?, ?, ?) """, ( uid, source, creds.token, creds.refresh_token, ",".join(creds.scopes), datetime.datetime.utcnow().isoformat() )) # Enable connector cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, ?, 1) """, (uid, source)) con.commit() con.close() finally: con.close() return redirect( f"/connectors/{source}" ) @app.route("/ai/connector//", methods=["GET", "POST"]) def ai_connector_router(connector, action): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 try: # -------- GOOGLE SPECIAL CASE -------- if connector == "google_gmail": if action == "sync": return sync_gmail_route() elif action == "connect": # Gmail uses OAuth → redirect flow return redirect("/google/connect/gmail") if connector == "google_drive": if action == "sync": return jsonify(sync_drive_files(uid)) if connector == "google_calendar": if action == "sync": return jsonify(sync_calendar_files(uid)) # -------- GENERIC CONNECTORS -------- module_name = f"backend.connectors.{connector}" module = __import__(module_name, fromlist=["*"]) func_name = f"{action}_{connector}" if hasattr(module, func_name): func = getattr(module, func_name) try: result = func(uid) except TypeError: result = func() return jsonify(result) return jsonify({ "error": f"{action} not supported for {connector}" }), 400 except Exception as e: return jsonify({ "error": str(e) }), 500 @app.route("/connectors/gmail/connect") def gmail_connect_alt(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 if not _has_connector_config(uid, "gmail"): return jsonify({"connected": False, "error": "credentials_required"}), 400 auth_url = "/google/connect?source=gmail" if _is_internal_ai_request(): return jsonify({ "connected": False, "auth_required": True, "redirect": auth_url, }) return redirect(auth_url) @app.route("/connectors/gmail/status") def gmail_status_alt(): # Call the original gmail status logic return gmail_status() @app.route("/ai/sync/") def ai_sync(source): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 # call existing sync route internally res = call_existing_sync(source, uid) # AFTER sync, read rows from DB or router rows_pushed = get_last_sync_rows(uid, source) return jsonify({ "status": "ok", "rows_pushed": rows_pushed }) # ---------------- DRIVE ---------------- @app.route("/google/sync/drive") def sync_drive(): from backend.connectors.google_drive import sync_drive_files return jsonify(sync_drive_files()) # ---------------- DRIVE SAVE APP CREDENTIALS ---------------- @app.route("/connectors/drive/save_app", methods=["POST"]) def drive_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'drive', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "drive") return jsonify({"status": "saved"}) # ---------------- SHEETS ---------------- @app.route("/google/sync/sheets") def sync_sheets(): return jsonify(sync_sheets_files()) # ---------------- SHEETS SAVE APP ---------------- @app.route("/connectors/sheets/save_app", methods=["POST"]) def sheets_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'sheets', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "sheets") return jsonify({"status": "saved"}) # ---------------- SHEETS DISCONNECT ---------------- @app.route("/google/disconnect/sheets") def disconnect_sheets(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='sheets' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- SHEETS JOB GET ---------------- @app.route("/connectors/sheets/job/get") def sheets_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='sheets' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- SHEETS JOB SAVE ---------------- @app.route("/connectors/sheets/job/save", methods=["POST"]) def sheets_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time, enabled) VALUES (?, 'sheets', ?, ?, 1) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------- GA4 Sync ---------- from backend.connectors.google_ga4 import sync_ga4 as run_ga4_sync @app.route("/google/sync/ga4") def google_ga4_sync(): try: result = sync_ga4() # Always return JSON return jsonify(result), 200 except Exception as e: print("[GA4 SYNC ERROR]", str(e), flush=True) return jsonify({ "status": "error", "message": str(e) }), 500 # ---------------- GA4 SAVE APP ---------------- @app.route("/connectors/ga4/save_app", methods=["POST"]) def ga4_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") property_id = data.get("property_id") if not client_id or not client_secret or not property_id: return jsonify({"error": "Client ID, Secret and Property ID required"}), 400 config = { "property_id": property_id } con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, config_json, created_at) VALUES (?, 'ga4', ?, ?, ?, ?) """, ( uid, client_id, client_secret, json.dumps(config), datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "ga4") return jsonify({"status": "saved"}) # ---------------- GA4 JOB GET ---------------- @app.route("/connectors/ga4/job/get") def ga4_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='ga4' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- GA4 JOB SAVE ---------------- @app.route("/connectors/ga4/job/save", methods=["POST"]) def ga4_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time, enabled) VALUES (?, 'ga4', ?, ?, 1) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------- SEARCH CONSOLE ---------- @app.route("/connectors/search-console/sync") def gsc_sync(): site = request.args.get("site") sync_type = request.args.get("sync_type", "incremental") if not site: return jsonify({"status": "error", "message": "Missing site URL"}) result = sync_search_console(site, sync_type) return jsonify(result) # ---------------- SEARCH CONSOLE SAVE APP ---------------- @app.route("/connectors/search-console/save_app", methods=["POST"]) def search_console_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'search-console', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "search-console") return jsonify({"status": "saved"}) # ---------------- SEARCH CONSOLE JOB GET ---------------- @app.route("/connectors/search-console/job/get") def gsc_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='search-console' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- SEARCH CONSOLE JOB SAVE ---------------- @app.route("/connectors/search-console/job/save", methods=["POST"]) def gsc_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time, enabled) VALUES (?, 'search-console', ?, ?, 1) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "ok"}) @app.route("/api/status/search-console") def search_console_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='search-console' """, (uid,)) row = fetchone_secure(cur) con.close() return jsonify({ "connected": True if row and row[0] == 1 else False }) @app.route("/google/disconnect/search-console") def disconnect_search_console(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='search-console' """, (uid,)) cur.execute(""" DELETE FROM google_accounts WHERE uid=? AND source='search-console' """, (uid,)) cur.execute(""" UPDATE connector_jobs SET enabled=0 WHERE uid=? AND source='search-console' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) @app.route("/connectors/pagespeed/sync") def pagespeed_sync(): url = request.args.get("url") sync_type = request.args.get("sync_type", "incremental") if not url: return jsonify({"error": "url required"}), 400 result = sync_pagespeed(url, sync_type) return jsonify(result) # ---------------- GOOGLE PAGESPEED CONNECT ---------------- @app.route("/connectors/pagespeed/connect") def pagespeed_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # ensure API key exists first cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='pagespeed' LIMIT 1 """, (uid,)) key = cur.fetchone() if not key: con.close() return jsonify({ "status": "error", "message": "Save API key first" }), 400 # enable only after credential exists cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'pagespeed', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) @app.route("/connectors/pagespeed/disconnect") def pagespeed_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='pagespeed' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/pagespeed/save_config", methods=["POST"]) def pagespeed_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) api_key = data.get("api_key") if not api_key: return jsonify({"error": "API key required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, api_key, created_at) VALUES (?, 'pagespeed', ?, datetime('now')) """, (uid, api_key)) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/api/status/pagespeed") def pagespeed_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # ---------- connection ---------- cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='pagespeed' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) connected = bool(row and row[0] == 1) # ---------- api key ---------- cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='pagespeed' LIMIT 1 """, (uid,)) key_row = fetchone_secure(cur) api_key_saved = key_row is not None con.close() return jsonify({ "connected": connected, "api_key_saved": api_key_saved }) @app.route("/connectors/pagespeed/job/get") def pagespeed_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='pagespeed' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/pagespeed/job/save", methods=["POST"]) def pagespeed_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'pagespeed', ?, ?) """, ( uid, data.get("sync_type"), data.get("schedule_time") )) con.commit() con.close() return jsonify({"status": "job_saved"}) @app.route("/google/sync/forms") def sync_forms_api(): try: return jsonify(sync_forms()) except Exception as e: return jsonify({ "status": "failed", "error": str(e) }), 500 # ---------------- FORMS SAVE APP ---------------- @app.route("/connectors/forms/save_app", methods=["POST"]) def forms_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, config_json, created_at) VALUES (?, 'forms', ?, ?, ?, ?) """, ( uid, client_id, client_secret, "{}", datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "forms") return jsonify({"status": "saved"}) # ---------------- FORMS DISCONNECT ---------------- @app.route("/google/disconnect/forms") def disconnect_forms(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='forms' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- FORMS JOB GET ---------------- @app.route("/connectors/forms/job/get") def forms_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='forms' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- FORMS JOB SAVE ---------------- @app.route("/connectors/forms/job/save", methods=["POST"]) def forms_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time, enabled) VALUES (?, 'forms', ?, ?, 1) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- CALENDAR ---------------- @app.route("/google/sync/calendar") def sync_calendar_files(): from backend.connectors.google_calendar import sync_calendar_files return jsonify(sync_calendar_files()) @app.route("/google/sync/gmail") def sync_gmail_route(): try: result = sync_gmail() return jsonify({ "status": "ok", "messages": result["messages"] }) except Exception as e: return jsonify({ "status": "error", "message": str(e) }), 500 @app.route("/google/disconnect/gmail") def google_disconnect_gmail(): uid = getattr(g, "user_id", None) source = "gmail" print("DISCONNECT USER:", g.user_id, flush=True) con = get_db() cur = con.cursor() try: # Disable connection cur.execute(""" UPDATE google_connections SET enabled = 0 WHERE uid=? AND source=? """, (uid, source)) # Disable scheduled job cur.execute(""" UPDATE connector_jobs SET enabled = 0 WHERE uid=? AND source=? """, (uid, source)) # Remove OAuth tokens cur.execute(""" DELETE FROM google_accounts WHERE uid=? AND source=? """, (uid, source)) con.commit() except Exception as e: con.rollback() return jsonify({ "status": "error", "message": str(e) }), 500 finally: con.close() return jsonify({ "status": "ok", "message": "Gmail disconnected successfully" }) # ---------------- GMAIL SAVE APP CREDENTIALS ---------------- @app.route("/connectors/gmail/save_app", methods=["POST"]) def gmail_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'gmail', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "gmail") return jsonify({"status": "saved"}) # ---------------- CALENDAR SAVE APP ---------------- @app.route("/connectors/calendar/save_app", methods=["POST"]) def calendar_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'calendar', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "calendar") return jsonify({"status": "saved"}) @app.route("/google/disconnect/drive") def disconnect_drive(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Disable connection cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='drive' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- GA4 DISCONNECT ---------------- @app.route("/google/disconnect/ga4") def google_disconnect_ga4(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Disable connection cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='ga4' """, (uid,)) # Remove token cur.execute(""" DELETE FROM google_accounts WHERE uid=? AND source='ga4' """, (uid,)) # Disable scheduled job cur.execute(""" UPDATE connector_jobs SET enabled=0 WHERE uid=? AND source='ga4' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- CALENDAR DISCONNECT ---------------- @app.route("/google/disconnect/calendar") def disconnect_calendar(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='calendar' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- TASKS DISCONNECT ---------------- @app.route("/google/disconnect/tasks") def disconnect_tasks(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='tasks' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) @app.route("/google/disconnect/") def google_disconnect(source): print("DISCONNECT CALLED:", source, flush=True) con = sqlite3.connect(DB) cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE source=? """, (source,)) con.commit() con.close() return jsonify({"status": "disconnected"}) #============== connectors ============== @app.route("/connectors/classroom/sync") def classroom_sync(): sync_type = request.args.get("sync_type", "incremental") result = sync_classroom(sync_type) return jsonify(result) @app.route("/connectors/classroom/save_app", methods=["POST"]) def classroom_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Missing credentials"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'classroom', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "classroom") return jsonify({"status": "saved"}) @app.route("/google/disconnect/classroom") def disconnect_classroom(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='classroom' """, (uid,)) cur.execute(""" DELETE FROM google_accounts WHERE uid=? AND source='classroom' """, (uid,)) cur.execute(""" UPDATE connector_jobs SET enabled=0 WHERE uid=? AND source='classroom' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) @app.route("/connectors/classroom/job/get") def classroom_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='classroom' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/classroom/job/save", methods=["POST"]) def classroom_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'classroom', ?, ?) """, ( uid, data.get("sync_type","incremental"), data.get("schedule_time") )) con.commit() con.close() return jsonify({"status":"job_saved"}) @app.route("/google/sync/tasks") def google_sync_tasks(): try: result = sync_tasks() return jsonify(result) except Exception as e: return jsonify({ "status": "error", "message": str(e) }), 500 # ---------------- TASKS SAVE APP ---------------- @app.route("/connectors/tasks/save_app", methods=["POST"]) def tasks_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'tasks', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "tasks") return jsonify({"status": "saved"}) # ---------------- TASKS JOB GET ---------------- @app.route("/connectors/tasks/job/get") def tasks_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='tasks' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- TASKS JOB SAVE ---------------- @app.route("/connectors/tasks/job/save", methods=["POST"]) def tasks_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time, enabled) VALUES (?, 'tasks', ?, ?, 1) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "ok"}) @app.route("/google/sync/contacts") def sync_contacts_api(): print("[SERVER] Triggering contacts sync", flush=True) return jsonify(sync_contacts()) # ---------------- CONTACTS SAVE APP ---------------- @app.route("/connectors/contacts/save_app", methods=["POST"]) def contacts_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'contacts', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "contacts") return jsonify({"status": "saved"}) # ---------------- CONTACTS DISCONNECT ---------------- @app.route("/google/disconnect/contacts") def disconnect_contacts(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='contacts' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- CONTACTS JOB GET ---------------- @app.route("/connectors/contacts/job/get") def contacts_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='contacts' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- CONTACTS JOB SAVE ---------------- @app.route("/connectors/contacts/job/save", methods=["POST"]) def contacts_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time, enabled) VALUES (?, 'contacts', ?, ?, 1) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- GOOGLE GCS SYNC ---------------- @app.route("/google/sync/gcs") def google_sync_gcs(): try: from backend.connectors.google_gcs import sync_gcs result = sync_gcs() return jsonify(result) except Exception as e: return jsonify({ "status": "error", "message": str(e) }), 500 # ---------------- GCS DISCONNECT ---------------- @app.route("/google/disconnect/gcs") def disconnect_gcs(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='gcs' """, (uid,)) cur.execute(""" DELETE FROM google_accounts WHERE uid=? AND source='gcs' """, (uid,)) cur.execute(""" UPDATE connector_jobs SET enabled=0 WHERE uid=? AND source='gcs' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) @app.route("/connectors/gcs/job/get") def gcs_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='gcs' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/gcs/job/save", methods=["POST"]) def gcs_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'gcs', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) @app.route("/connectors/gcs/save_app", methods=["POST"]) def gcs_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error":"missing"}),400 con=get_db() cur=con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid,connector,client_id,client_secret,created_at) VALUES (?,?,?, ?,?) """,( uid, "gcs", client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "gcs") return jsonify({"status":"saved"}) @app.route("/google/sync/webfonts", methods=["GET", "POST"]) def google_sync_webfonts(): try: result = sync_webfonts() return jsonify({ "status": "ok", "data": result }) except Exception as e: return jsonify({ "status": "error", "message": str(e) }), 500 from backend.connectors.google_webfonts import sync_webfonts @app.route("/connectors/webfonts/sync") def webfonts_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='webfonts' """, (uid,)) row = fetchone_secure(cur) con.close() if not row or row[0] != 1: return jsonify({"error": "WebFonts not connected"}), 400 result = sync_webfonts() return jsonify(result) # ---------------- GOOGLE WEBFONTS CONNECT ---------------- @app.route("/connectors/webfonts/connect") def webfonts_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'webfonts', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) # ---------------- GOOGLE WEBFONTS DISCONNECT ---------------- @app.route("/connectors/webfonts/disconnect") def webfonts_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled = 0 WHERE uid=? AND source='webfonts' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/webfonts") def webfonts_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # ---------- connection ---------- cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='webfonts' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) connected = bool(row and row[0] == 1) # ---------- API KEY ---------- cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='webfonts' LIMIT 1 """, (uid,)) key_row = fetchone_secure(cur) api_key_saved = bool( key_row and key_row[0] ) con.close() return jsonify({ "connected": connected, "api_key_saved": api_key_saved }) @app.route("/connectors/webfonts/job/get") def webfonts_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='webfonts' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/webfonts/job/save", methods=["POST"]) def webfonts_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'webfonts', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- GOOGLE WEBFONTS SAVE CONFIG ---------------- @app.route("/connectors/webfonts/save_config", methods=["POST"]) def webfonts_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) api_key = data.get("api_key") if not api_key: return jsonify({"error": "API key required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, api_key, created_at) VALUES (?, 'webfonts', ?, datetime('now')) """, (uid, api_key)) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/connectors/youtube/sync") def youtube_sync(): sync_type = request.args.get("sync_type", "incremental") result = sync_youtube(sync_type) return jsonify(result) # ---------------- YOUTUBE SAVE APP ---------------- @app.route("/connectors/youtube/save_app", methods=["POST"]) def youtube_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID and Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'youtube', ?, ?, ?) """, ( uid, client_id, client_secret, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "youtube") return jsonify({"status": "saved"}) @app.route("/google/disconnect/youtube") def disconnect_youtube(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='youtube' """, (uid,)) cur.execute(""" DELETE FROM google_accounts WHERE uid=? AND source='youtube' """, (uid,)) cur.execute(""" UPDATE connector_jobs SET enabled=0 WHERE uid=? AND source='youtube' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) # ---------------- YOUTUBE JOB GET ---------------- @app.route("/connectors/youtube/job/get") def youtube_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='youtube' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- YOUTUBE JOB SAVE ---------------- @app.route("/connectors/youtube/job/save", methods=["POST"]) def youtube_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'youtube', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) #----------------Reddit---------------- @app.route("/connectors/reddit/connect") def reddit_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT client_id, client_secret, config_json FROM connector_configs WHERE uid=? AND connector='reddit' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) if not row: return jsonify({"error":"Config missing"}),400 client_id, client_secret, cfg = row cfg=json.loads(cfg) from backend.connectors.reddit import connect_reddit connect_reddit( uid, client_id, client_secret, cfg["username"], cfg["password"] ) cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES (?, 'reddit',1) """,(uid,)) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/reddit/disconnect") def reddit_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='reddit' """, (uid,)) cur.execute("DELETE FROM reddit_tokens WHERE uid=?", (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/reddit") def reddit_status(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='reddit' """,(uid,)) conn=cur.fetchone() cur.execute(""" SELECT client_id FROM connector_configs WHERE uid=? AND connector='reddit' """,(uid,)) cfg=cur.fetchone() con.close() return jsonify({ "connected": bool(conn and conn[0]==1), "has_credentials": bool(cfg) }) @app.route("/connectors/reddit/save_config", methods=["POST"]) def reddit_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") username = data.get("username") password = data.get("password") if not all([client_id, client_secret, username, password]): return jsonify({"error": "Missing fields"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, config_json, created_at) VALUES (?, 'reddit', ?, ?, ?, datetime('now')) """, ( uid, client_id, client_secret, json.dumps({ "username": username, "password": password }) )) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/connectors/reddit/job/get") def reddit_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='reddit' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({}) return jsonify({ "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/reddit/job/save", methods=["POST"]) def reddit_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'reddit', ?, ?) """, ( uid, data.get("sync_type"), data.get("schedule_time") )) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/connectors/reddit/sync") def reddit_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # ---------------------------- # CHECK CONNECTION # ---------------------------- cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='reddit' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 # ---------------------------- # GET SYNC TYPE # ---------------------------- cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='reddit' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "historical" con.close() # ---------------------------- # IMPORT REAL FUNCTIONS # ---------------------------- from backend.connectors.reddit import ( sync_posts, sync_profile, sync_messages ) # ---------------------------- # RUN SYNC # ---------------------------- profile_data = sync_profile(uid) posts_data = sync_posts(uid, query="python", sync_type=sync_type) messages_data = sync_messages(uid) post_rows = posts_data.get("rows", []) # ---------------------------- # DESTINATION # ---------------------------- con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source=? AND is_active=1 LIMIT 1 """, (uid, "reddit")) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "posts": len(post_rows), "rows_pushed": 0, "sync_type": sync_type, "message": "No active destination" }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if post_rows: pushed += push_to_destination(dest, "reddit_posts", post_rows) print(f"[REDDIT] Sync type: {sync_type}", flush=True) print(f"[REDDIT] Posts fetched: {len(post_rows)}", flush=True) print(f"[REDDIT] Rows pushed: {pushed}", flush=True) return jsonify({ "posts": len(post_rows), "rows_pushed": pushed, "sync_type": sync_type }) @app.route("/reddit/sync/profile") def reddit_profile(): uid = request.args.get("uid") from backend.connectors.reddit import sync_profile return sync_profile(uid) @app.route("/reddit/sync/posts") def reddit_posts(): uid = request.args.get("uid") q = request.args.get("q", "python") from backend.connectors.reddit import sync_posts return sync_posts(uid, q) @app.route("/reddit/sync/messages") def reddit_messages(): uid = request.args.get("uid") from backend.connectors.reddit import sync_messages return sync_messages(uid) # ---------------- TELEGRAM ---------------- @app.route("/connectors/telegram/sync") def telegram_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT COUNT(*) FROM telegram_accounts WHERE uid=? """, (uid,)) if cur.fetchone()[0] == 0: con.close() return jsonify({"error": "not connected"}), 400 cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='telegram' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "historical" con.close() from backend.connectors.telegram import sync_messages res = sync_messages(uid, sync_type) total_messages = res["messages"] rows = res["rows"] con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source='telegram' AND is_active=1 LIMIT 1 """, (uid,)) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "messages": total_messages, "rows_pushed": 0, "sync_type": sync_type }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if rows: pushed = push_to_destination(dest, "telegram_messages", rows) return jsonify({ "messages": total_messages, "rows_pushed": pushed, "sync_type": sync_type }) @app.route("/connectors/telegram/connect") def telegram_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # read saved token cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='telegram' LIMIT 1 """,(uid,)) row = fetchone_secure(cur) if not row: return jsonify({"error":"config missing"}),400 bot_token = row[0] # store runtime connection cur.execute(""" INSERT OR REPLACE INTO telegram_accounts (uid, bot_token, created_at) VALUES (?,?,?) """,( uid, bot_token, datetime.datetime.utcnow().isoformat() )) # enable connector cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES (?, 'telegram',1) """,(uid,)) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/telegram/disconnect") def telegram_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("DELETE FROM telegram_accounts WHERE uid=?", (uid,)) cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='telegram' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/telegram") def telegram_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='telegram' """,(uid,)) conn = cur.fetchone() # credentials saved cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='telegram' """,(uid,)) cfg = cur.fetchone() con.close() return jsonify({ "connected": bool(conn and conn[0]==1), "has_credentials": bool(cfg and cfg[0]) }) # ---------------- TELEGRAM SAVE CONFIG ---------------- @app.route("/connectors/telegram/save_config", methods=["POST"]) def telegram_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) bot_token = data.get("bot_token") if not bot_token: return jsonify({"error": "bot_token required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, api_key, created_at) VALUES (?, 'telegram', ?, datetime('now')) """, (uid, bot_token)) con.commit() con.close() return jsonify({"status": "saved"}) # ---------------- MEDIUM ---------------- @app.route("/medium/sync") def medium_sync(): uid = getattr(g, "user_id", None) username = request.args.get("username") from backend.connectors.medium import sync_user return jsonify(sync_user(uid, username)) @app.route("/connectors/medium/connect") def medium_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT config_json FROM connector_configs WHERE uid=? AND connector='medium' """,(uid,)) if not cur.fetchone(): con.close() return jsonify({"error":"config missing"}),400 cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'medium', 1) """,(uid,)) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/medium/save_config", methods=["POST"]) def medium_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) username = data.get("username") if not username: return jsonify({"error": "username required"}), 400 con = get_db() cur = con.cursor() # check existing cur.execute(""" SELECT id FROM connector_configs WHERE uid=? AND connector='medium' LIMIT 1 """,(uid,)) row = fetchone_secure(cur) if row: # UPDATE cur.execute(""" UPDATE connector_configs SET config_json=?, created_at=datetime('now') WHERE uid=? AND connector='medium' """,( json.dumps({"username": username}), uid )) else: # INSERT cur.execute(""" INSERT INTO connector_configs (uid, connector, config_json, created_at) VALUES (?, 'medium', ?, datetime('now')) """,( uid, json.dumps({"username": username}) )) con.commit() con.close() return jsonify({"status":"saved"}) @app.route("/connectors/medium/disconnect") def medium_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("DELETE FROM medium_accounts WHERE uid=?", (uid,)) cur.execute("DELETE FROM medium_state WHERE uid=?", (uid,)) cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='medium' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/medium") def medium_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='medium' """,(uid,)) conn = cur.fetchone() # credentials cur.execute(""" SELECT config_json FROM connector_configs WHERE uid=? AND connector='medium' """,(uid,)) cfg = cur.fetchone() con.close() return jsonify({ "connected": bool(conn and conn[0]==1), "has_credentials": bool(cfg) }) @app.route("/connectors/medium/sync") def medium_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='medium' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "historical" con.close() from backend.connectors.medium import sync_medium res = sync_medium(uid, sync_type) total = res["posts"] rows = res["rows"] # Destination con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source='medium' AND is_active=1 LIMIT 1 """, (uid,)) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "posts": total, "rows_pushed": 0, "sync_type": sync_type }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if rows: pushed = push_to_destination(dest, "medium_posts", rows) return jsonify({ "posts": total, "rows_pushed": pushed, "sync_type": sync_type }) # ---------------- QUORA (DISABLED) ---------------- @app.route("/quora/sync/profile") def quora_sync_profile(): return jsonify({ "status": "blocked", "platform": "quora", "reason": "Quora blocks automated access (HTTP 403). No public API." }) @app.route("/quora/sync/answers") def quora_sync_answers(): return jsonify({ "status": "blocked", "platform": "quora", "reason": "Quora blocks automated access (HTTP 403). No public API." }) # ---------------- TUMBLR ---------------- @app.route("/tumblr/sync/blog") def tumblr_sync_blog(): uid = request.cookies.get("uid") blog = request.args.get("blog") from backend.connectors.tumblr import sync_blog return jsonify(sync_blog(uid, blog)) @app.route("/tumblr/sync/posts") def tumblr_sync_posts(): uid = request.cookies.get("uid") blog = request.args.get("blog") from backend.connectors.tumblr import sync_posts return jsonify(sync_posts(uid, blog)) # ---------------- TWITCH ---------------- @app.route("/connectors/twitch/connect") def twitch_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='twitch' """,(uid,)) if not cur.fetchone(): con.close() return jsonify({"error":"config missing"}),400 cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES (?,?,1) """,(uid,"twitch")) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/api/status/twitch") def twitch_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='twitch' """,(uid,)) connected = bool( (row := cur.fetchone()) and row[0]==1 ) cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='twitch' """,(uid,)) has_credentials = bool(cur.fetchone()) con.close() return jsonify({ "connected":connected, "has_credentials":has_credentials }) @app.route("/connectors/twitch/disconnect") def twitch_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Only disable connector cur.execute(""" UPDATE google_connections SET enabled = 0 WHERE uid=? AND source='twitch' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/twitch/sync") def twitch_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check if enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='twitch' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "Twitch not connected"}), 400 # Get username from connector_state cur.execute(""" SELECT config_json FROM connector_configs WHERE uid=? AND source='twitch' """, (uid,)) row = fetchone_secure(cur) if not row: con.close() return jsonify({"error": "Username missing"}), 400 cfg = json.loads(row[0]) username = cfg.get("username") if not username: con.close() return jsonify({"error": "Username missing"}), 400 # Get sync type from job sync_type = get_connector_sync_type(uid, "twitch") con.close() # Run connector from backend.connectors.twitch import sync_videos result = sync_videos( uid=uid, username=username, sync_type=sync_type ) rows = result.get("rows", []) # Check destination con = get_db() con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source='twitch' ORDER BY id DESC LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() dest = None if row: dest = dict(row) # Normalize key name for router if "dest_type" in dest: dest["type"] = dest["dest_type"] if dest and rows: inserted = push_to_destination( dest, "twitch_videos", rows ) return jsonify({ "status": "pushed_to_destination", "rows": inserted }) return jsonify({ "status": "stored_locally", "videos": result.get("videos", 0) }) @app.route("/connectors/twitch/save_config", methods=["POST"]) def twitch_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) or {} username = data.get("username") if not username: return jsonify({"error":"username required"}),400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, config_json, created_at) VALUES (?,?,?,datetime('now')) """,( uid, "twitch", json.dumps({"username": username}) )) con.commit() con.close() return jsonify({"status":"saved"}) #------------------- Tumblr -------------------------- @app.route("/connectors/tumblr/save_config", methods=["POST"]) def tumblr_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) api_key = data.get("api_key") if not api_key: return jsonify({"error":"API key required"}),400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, api_key, created_at) VALUES (?, 'tumblr', ?, datetime('now')) """,(uid, api_key)) con.commit() con.close() return jsonify({"status":"saved"}) @app.route("/connectors/tumblr/connect") def tumblr_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # check config exists cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='tumblr' """,(uid,)) row = fetchone_secure(cur) if not row: con.close() return jsonify({"error":"config missing"}),400 cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'tumblr', 1) """,(uid,)) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/tumblr/disconnect") def tumblr_disconnect(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='tumblr' """,(uid,)) con.commit() con.close() return jsonify({"status":"disconnected"}) @app.route("/connectors/tumblr/sync") def tumblr_sync_universal(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='tumblr' """,(uid,)) if not cur.fetchone(): con.close() return jsonify({"error":"not connected"}),400 cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='tumblr' LIMIT 1 """,(uid,)) job=cur.fetchone() sync_type=job[0] if job else "historical" con.close() from backend.connectors.tumblr import sync_posts result=sync_posts(uid,sync_type) rows=result.get("rows",[]) return jsonify({ "posts":len(rows), "sync_type":sync_type }) @app.route("/api/status/tumblr") def tumblr_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # ---------- connection ---------- cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='tumblr' LIMIT 1 """,(uid,)) conn = cur.fetchone() connected = bool(conn and conn[0] == 1) # ---------- credentials ---------- cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='tumblr' LIMIT 1 """,(uid,)) cfg = cur.fetchone() has_credentials = bool(cfg and cfg[0]) con.close() return jsonify({ "connected": connected, "has_credentials": has_credentials }) # ---------------- DISCORD (BOT MODE) ---------------- @app.route("/connectors/discord/connect") def discord_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='discord' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) if not row: return jsonify({"error":"token missing"}),400 bot_token = row[0] cur.execute(""" INSERT OR REPLACE INTO discord_connections (uid, bot_token, created_at) VALUES (?,?,?) """, ( uid, bot_token, datetime.datetime.utcnow().isoformat() )) cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'discord', 1) """, (uid,)) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/discord/disconnect") def discord_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("DELETE FROM discord_connections WHERE uid=?", (uid,)) cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='discord' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/discord/sync") def discord_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 print("SYNC UID:", uid, flush=True) con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='discord' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='discord' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "historical" from backend.connectors.discord import sync_guilds, sync_channels, sync_messages sync_guilds(uid) cur.execute("SELECT guild_id FROM discord_guilds WHERE uid=?", (uid,)) guilds = cur.fetchall() total_messages = 0 all_rows = [] # Reduced safety limits (faster sync) max_global_messages = 200 max_channels_per_guild = 3 for g in guilds: guild_id = g[0] sync_channels(guild_id, uid) cur.execute(""" SELECT channel_id FROM discord_channels WHERE uid=? AND guild_id=? AND type=0 LIMIT ? """, (uid, guild_id, max_channels_per_guild)) channels = cur.fetchall() for c in channels: if total_messages >= max_global_messages: break channel_id = c[0] try: res = sync_messages(uid, channel_id, sync_type) total_messages += res.get("messages", 0) all_rows.extend(res.get("rows", [])) except Exception as e: print("Channel error:", channel_id, str(e), flush=True) continue if total_messages >= max_global_messages: break cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source=? AND is_active=1 LIMIT 1 """, (uid, "discord")) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "messages": total_messages, "rows_pushed": 0, "sync_type": sync_type }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if all_rows: pushed = push_to_destination(dest, "discord_messages", all_rows) print(f"[DISCORD] Sync type: {sync_type}", flush=True) print(f"[DISCORD] Messages inserted: {total_messages}", flush=True) print(f"[DISCORD] Rows pushed: {pushed}", flush=True) return jsonify({ "messages": total_messages, "rows_pushed": pushed, "sync_type": sync_type }) @app.route("/api/status/discord") def discord_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='discord' """,(uid,)) conn = cur.fetchone() # token saved cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='discord' """,(uid,)) cfg = cur.fetchone() con.close() return jsonify({ "connected": bool(conn and conn[0]==1), "has_credentials": bool(cfg and cfg[0]) }) # ---------------- DISCORD SAVE CONFIG ---------------- @app.route("/connectors/discord/save_config", methods=["POST"]) def discord_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) bot_token = data.get("bot_token") if not bot_token: return jsonify({"error": "bot_token required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, api_key, created_at) VALUES (?, 'discord', ?, datetime('now')) """, (uid, bot_token)) con.commit() con.close() return jsonify({"status": "saved"}) # ---------------- GOOGLE BOOKS ---------------- from backend.connectors.googlebooks import sync_books @app.route("/connectors/books/connect") def connect_books(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections(uid, source, enabled) VALUES (?, 'books', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) @app.route("/connectors/books/disconnect") def disconnect_books(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='books' """, (uid,)) cur.execute(""" UPDATE connector_jobs SET enabled=0 WHERE uid=? AND source='books' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/books/sync") def books_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 query = request.args.get("query") sync_type = request.args.get("sync_type", "incremental") if not query: return jsonify({"status": "error", "message": "query required"}) return jsonify(sync_books(query, sync_type)) @app.route("/connectors/books/job/get") def get_books_job(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='books' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({ "sync_type": "incremental", "schedule_time": "00:00" }) return jsonify({ "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/books/job/save", methods=["POST"]) def save_books_job(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.json sync_type = data.get("sync_type") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT INTO connector_jobs (uid, source, sync_type, schedule_time, enabled) VALUES (?, 'books', ?, ?, 1) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/api/status/books") def books_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='books' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() return jsonify({ "connected": bool(row and row[0] == 1) }) # ---------------- GOOGLE FACT CHECK ---------------- from backend.connectors.googlefactcheck import sync_factcheck @app.route("/connectors/factcheck/sync") def factcheck_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 query = request.args.get("query") sync_type = request.args.get("sync_type", "incremental") if not query: return jsonify({ "status": "error", "message": "query required" }) result = sync_factcheck(uid, query, sync_type) return jsonify(result) @app.route("/connectors/factcheck/save_config", methods=["POST"]) def factcheck_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) api_key = data.get("api_key") if not api_key: return jsonify({"error": "API key required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, api_key, created_at) VALUES (?, 'factcheck', ?, datetime('now')) """, (uid, api_key)) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/connectors/factcheck/connect") def factcheck_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'factcheck', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) @app.route("/connectors/factcheck/disconnect") def factcheck_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='factcheck' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/factcheck") def factcheck_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='factcheck' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) connected = bool(row and row[0] == 1) # api key cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='factcheck' LIMIT 1 """, (uid,)) key = cur.fetchone() con.close() return jsonify({ "connected": connected, "api_key_saved": key is not None }) @app.route("/connectors/factcheck/job/get") def factcheck_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='factcheck' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/factcheck/job/save", methods=["POST"]) def factcheck_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'factcheck', ?, ?) """, ( uid, sync_type, schedule_time )) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- GOOGLE NEWS ---------------- @app.route("/googlenews/sync/articles") def googlenews_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 query = request.args.get("q") limit = int(request.args.get("limit", 100)) from backend.connectors.googlenews import sync_articles return jsonify(sync_articles(uid, query, limit)) from backend.connectors.googlenews import sync_news @app.route("/connectors/news/sync", methods=["GET"]) def news_sync(): keyword = request.args.get("keyword") sync_type = request.args.get("sync_type", "incremental") if not keyword: return jsonify({"error": "keyword required"}), 400 result = sync_news(keyword, sync_type) return jsonify(result) # ---------------- GOOGLE NEWS CONNECT ---------------- @app.route("/connectors/news/connect", methods=["POST"]) def connect_news(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'news', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) # ---------------- GOOGLE NEWS DISCONNECT ---------------- @app.route("/connectors/news/disconnect", methods=["POST"]) def disconnect_news(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='news' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) # ---------------- GOOGLE NEWS STATUS ---------------- @app.route("/api/status/news") def news_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='news' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() return jsonify({ "connected": bool(row and row[0] == 1) }) # ---------------- GOOGLE NEWS JOB GET ---------------- @app.route("/connectors/news/job/get") def news_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='news' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({ "exists": False, "sync_type": "incremental", "schedule_time": "00:00" }) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- GOOGLE NEWS JOB SAVE ---------------- @app.route("/connectors/news/job/save", methods=["POST"]) def news_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'news', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "saved"}) # ---------------- GOOGLE TRENDS ---------------- @app.route("/googletrends/sync/interest") def googletrends_sync_interest(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 keyword = request.args.get("q") from backend.connectors.googletrends import sync_interest return jsonify(sync_interest(uid, keyword)) @app.route("/googletrends/sync/related") def googletrends_sync_related(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 keyword = request.args.get("q") from backend.connectors.googletrends import sync_related return jsonify(sync_related(uid, keyword)) from backend.connectors.googletrends import sync_trends @app.route("/connectors/trends/sync", methods=["GET"]) def trends_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 keyword = request.args.get("keyword") sync_type = request.args.get("sync_type", "incremental") if not keyword: return jsonify({"status": "error", "message": "Keyword required"}) print("CALLING SYNC TRENDS NOW", flush=True) result = sync_trends(uid, keyword, sync_type) return jsonify(result) @app.route("/connectors/trends/connect", methods=["POST"]) def trends_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections(uid, source, enabled) VALUES (?, 'trends', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) # ---------------- TRENDS STATUS ---------------- @app.route("/api/status/trends") def trends_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='trends' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() return jsonify({ "connected": bool(row and row[0] == 1) }) # ---------------- TRENDS DISCONNECT ---------------- @app.route("/google/disconnect/trends") def disconnect_trends(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='trends' """, (uid,)) cur.execute(""" UPDATE connector_jobs SET enabled=0 WHERE uid=? AND source='trends' """, (uid,)) con.commit() con.close() return jsonify({"status": "ok"}) @app.route("/connectors/trends/job/get") def trends_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='trends' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/trends/job/save", methods=["POST"]) def trends_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'trends', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- DEV.TO ---------------- @app.route("/connectors/devto/connect") def devto_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'devto', 1) """, (uid,)) con.commit() con.close() return redirect("/connectors/devto") # ---------------- DEVTO STATUS ---------------- @app.route("/api/status/devto") def devto_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # API connectors have implicit credentials has_credentials = True cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='devto' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": has_credentials, "connected": bool(row and row[0] == 1) }) @app.route("/connectors/devto/disconnect") def devto_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='devto' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/devto/job/get") def devto_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='devto' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({}) return jsonify({ "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/devto/job/save", methods=["POST"]) def devto_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'devto', ?, ?) """, ( uid, data.get("sync_type"), data.get("schedule_time") )) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/connectors/devto/sync") def devto_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # ---- Check connection ---- cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='devto' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 # ---- Get sync type ---- cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='devto' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "historical" con.close() # ---- Run connector ---- from backend.connectors.devto import sync_articles, sync_tags articles_data = sync_articles(uid, sync_type=sync_type) tags_data = sync_tags(uid) article_rows = articles_data.get("rows", []) tag_rows = tags_data.get("rows", []) # ---- Get destination ---- con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source=? AND is_active=1 LIMIT 1 """, (uid, "devto")) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "articles": articles_data.get("articles", 0), "tags": tags_data.get("tags", 0), "rows_pushed": 0, "sync_type": sync_type, "message": "No active destination" }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } # ---- Push ONLY NEW ROWS ---- from backend.destinations.destination_router import push_to_destination pushed = 0 if article_rows: pushed += push_to_destination(dest, "devto_articles", article_rows) if tag_rows: pushed += push_to_destination(dest, "devto_tags", tag_rows) print(f"[DEVTO] Sync type: {sync_type}", flush=True) print(f"[DEVTO] Articles found: {len(article_rows)}", flush=True) print(f"[DEVTO] Tags found: {len(tag_rows)}", flush=True) print(f"[DEVTO] Rows pushed: {pushed}", flush=True) return jsonify({ "articles": len(article_rows), "tags": len(tag_rows), "rows_pushed": pushed, "sync_type": sync_type }) # ---------------- GITHUB ---------------- from backend.connectors.github import ( get_auth_url, exchange_code, save_token, sync_github, disable_connection ) @app.route("/github/connect") def github_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 # Use unified redirect URI targeting the frontend proxy redirect_uri = get_base_url() + "/oauth/callback" return redirect(get_auth_url(uid, redirect_uri=redirect_uri)) @app.route("/github/callback") def github_callback(): code = request.args.get("code") if not code: return "Authorization failed", 400 uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 token = exchange_code(uid, code) if not token.get("access_token"): return "Token exchange failed", 400 save_token(uid, token) return redirect("/connectors/github") @app.route("/connectors/github/disconnect") def github_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Disable connection cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='github' """, (uid,)) # DELETE stored GitHub token cur.execute(""" DELETE FROM github_tokens WHERE uid=? """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/github/sync") def github_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 return jsonify(sync_github(uid)) @app.route("/connectors/github/save_app", methods=["POST"]) def github_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({"error": "Client ID & Secret required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'github', ?, ?, datetime('now')) """, ( uid, client_id, client_secret )) con.commit() con.close() ensure_connector_initialized(uid, "github") return jsonify({"status": "saved"}) @app.route("/api/status/github") def github_status(): uid = getattr(g, "user_id", None) conn = sqlite3.connect(DB_PATH) cur = conn.cursor() # credentials exist? cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='github' LIMIT 1 """, (uid,)) creds = cur.fetchone() # connection enabled? cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='github' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) conn.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(row and row["enabled"] == 1) }) @app.route("/connectors/github/job/get") def github_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='github' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row[1] }) @app.route("/connectors/github/job/save", methods=["POST"]) def github_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'github', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- INSTAGRAM ---------------- from backend.connectors.instagram import ( get_instagram_auth_url, handle_oauth_callback, sync_instagram, disconnect_instagram ) @app.route("/connectors/instagram/save_app", methods=["POST"]) def instagram_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} app_id = data.get("app_id") app_secret = data.get("app_secret") redirect_uri = data.get("redirect_uri") or (request.host_url.rstrip("/") + "/instagram/callback") if not app_id or not app_secret: return jsonify({"error": "App ID & App Secret required"}), 400 con = get_db() cur = con.cursor() # Store encrypted values in connector_configs (while preserving existing schema). cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, scopes, created_at) VALUES (?, 'instagram', ?, ?, ?, datetime('now')) """, ( uid, encrypt_value(app_id), encrypt_value(app_secret), encrypt_value(redirect_uri) )) con.commit() con.close() ensure_connector_initialized(uid, "instagram") return jsonify({"status": "saved"}) @app.route("/instagram/connect") def instagram_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 try: # Use unified redirect URI targeting the frontend proxy redirect_uri = get_base_url() + "/oauth/callback" return redirect(get_instagram_auth_url(uid, redirect_uri=redirect_uri)) except Exception as e: return jsonify({"error": str(e)}), 400 @app.route("/instagram/callback") def instagram_callback(): uid = getattr(g, "user_id", None) code = request.args.get("code") if not uid: return jsonify({"error": "Unauthorized"}), 401 if not code: return "Authorization failed", 400 # Use unified redirect URI targeting the frontend proxy redirect_uri = get_base_url() + "/oauth/callback" result = handle_oauth_callback(uid, code, redirect_uri=redirect_uri) if result.get("status") != "success": return jsonify(result), 400 return redirect("/connectors/instagram") @app.route("/connectors/instagram/disconnect") def instagram_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_instagram(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/instagram/sync") def instagram_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='instagram' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_instagram(uid, sync_type=sync_type)) @app.route("/api/status/instagram") def instagram_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='instagram' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='instagram' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT ig_account_id FROM instagram_connections WHERE uid=? LIMIT 1 """, (uid,)) ig_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "ig_account_id": ig_row["ig_account_id"] if ig_row else None }) @app.route("/connectors/instagram/job/get") def instagram_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='instagram' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/instagram/job/save", methods=["POST"]) def instagram_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'instagram', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- TIKTOK BUSINESS ---------------- @app.route("/connectors/tiktok/save_app", methods=["POST"]) def tiktok_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} client_key = data.get("client_key") client_secret = data.get("client_secret") advertiser_id = data.get("advertiser_id") scopes = data.get("scopes") or "user.info.basic,video.list,ads.read" redirect_uri = data.get("redirect_uri") or (request.host_url.rstrip("/") + "/connectors/tiktok/callback") if not client_key or not client_secret or not advertiser_id: return jsonify({"error": "client_key, client_secret and advertiser_id are required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, api_key, scopes, status, created_at) VALUES (?, 'tiktok', ?, ?, ?, ?, 'configured', datetime('now')) """, ( uid, encrypt_value(client_key), encrypt_value(client_secret), encrypt_value(advertiser_id), encrypt_value(redirect_uri) )) con.commit() con.close() ensure_connector_initialized(uid, "tiktok") return jsonify({"status": "saved"}) @app.route("/connectors/tiktok/connect") def tiktok_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 try: # Use unified redirect URI targeting the frontend proxy redirect_uri = get_base_url() + "/oauth/callback" return redirect(get_tiktok_auth_url(uid, redirect_uri=redirect_uri)) except Exception as e: return jsonify({"error": str(e)}), 400 @app.route("/connectors/tiktok/callback") def tiktok_callback(): uid = getattr(g, "user_id", None) code = request.args.get("code") if not uid: return jsonify({"error": "Unauthorized"}), 401 if not code: return "Authorization failed", 400 result = handle_tiktok_oauth_callback(uid, code) if result.get("status") != "success": return jsonify(result), 400 return redirect("/connectors/tiktok") @app.route("/connectors/tiktok/disconnect") def tiktok_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_tiktok(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/tiktok/sync") def tiktok_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='tiktok' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_tiktok(uid, sync_type=sync_type)) @app.route("/api/status/tiktok") def tiktok_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='tiktok' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='tiktok' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT advertiser_id, expires_at FROM tiktok_connections WHERE uid=? LIMIT 1 """, (uid,)) tk = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "advertiser_id": tk["advertiser_id"] if tk else None, "expires_at": tk["expires_at"] if tk else None }) @app.route("/connectors/tiktok/job/get") def tiktok_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='tiktok' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/tiktok/job/save", methods=["POST"]) def tiktok_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'tiktok', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- TABOOLA BACKSTAGE ---------------- @app.route("/connectors/taboola/save_app", methods=["POST"]) def taboola_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} client_id = data.get("client_id") client_secret = data.get("client_secret") account_id = data.get("account_id") if not client_id or not client_secret or not account_id: return jsonify({"error": "client_id, client_secret and account_id are required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, api_key, status, created_at) VALUES (?, 'taboola', ?, ?, ?, 'configured', datetime('now')) """, ( uid, encrypt_value(client_id), encrypt_value(client_secret), encrypt_value(account_id) )) con.commit() con.close() ensure_connector_initialized(uid, "taboola") return jsonify({"status": "saved"}) @app.route("/connectors/taboola/connect") def taboola_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_taboola(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/taboola/disconnect") def taboola_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_taboola(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/taboola/sync") def taboola_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='taboola' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_taboola(uid, sync_type=sync_type)) @app.route("/api/status/taboola") def taboola_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='taboola' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='taboola' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT account_id, expires_at FROM taboola_connections WHERE uid=? LIMIT 1 """, (uid,)) tb = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "account_id": tb["account_id"] if tb else None, "expires_at": tb["expires_at"] if tb else None }) @app.route("/connectors/taboola/job/get") def taboola_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='taboola' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/taboola/job/save", methods=["POST"]) def taboola_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'taboola', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- OUTBRAIN AMPLIFY ---------------- @app.route("/connectors/outbrain/save_app", methods=["POST"]) def outbrain_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} username = data.get("username") password = data.get("password") marketer_id = data.get("marketer_id") if not username or not password or not marketer_id: return jsonify({"error": "username, password and marketer_id are required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, api_key, status, created_at) VALUES (?, 'outbrain', ?, ?, ?, 'configured', datetime('now')) """, ( uid, encrypt_value(username), encrypt_value(password), encrypt_value(marketer_id) )) con.commit() con.close() ensure_connector_initialized(uid, "outbrain") return jsonify({"status": "saved"}) @app.route("/connectors/outbrain/connect") def outbrain_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_outbrain(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/outbrain/disconnect") def outbrain_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_outbrain(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/outbrain/sync") def outbrain_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='outbrain' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_outbrain(uid, sync_type=sync_type)) @app.route("/api/status/outbrain") def outbrain_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='outbrain' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='outbrain' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT marketer_id, expires_at FROM outbrain_connections WHERE uid=? LIMIT 1 """, (uid,)) ob = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "marketer_id": ob["marketer_id"] if ob else None, "expires_at": ob["expires_at"] if ob else None }) @app.route("/connectors/outbrain/job/get") def outbrain_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='outbrain' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/outbrain/job/save", methods=["POST"]) def outbrain_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'outbrain', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- SIMILARWEB ---------------- @app.route("/connectors/similarweb/save_app", methods=["POST"]) def similarweb_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} api_key = data.get("api_key") domain = data.get("domain") if not api_key or not domain: return jsonify({"error": "api_key and domain are required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, api_key, scopes, status, created_at) VALUES (?, 'similarweb', ?, ?, 'configured', datetime('now')) """, ( uid, encrypt_value(api_key), encrypt_value(domain) )) con.commit() con.close() ensure_connector_initialized(uid, "similarweb") return jsonify({"status": "saved"}) @app.route("/connectors/similarweb/connect") def similarweb_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_similarweb(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/similarweb/disconnect") def similarweb_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_similarweb(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/similarweb/sync") def similarweb_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='similarweb' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_similarweb(uid, sync_type=sync_type)) @app.route("/api/status/similarweb") def similarweb_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='similarweb' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='similarweb' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT domain FROM similarweb_connections WHERE uid=? LIMIT 1 """, (uid,)) sw = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "domain": sw["domain"] if sw else None }) @app.route("/connectors/similarweb/job/get") def similarweb_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='similarweb' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/similarweb/job/save", methods=["POST"]) def similarweb_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'similarweb', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- BIGQUERY DESTINATION ---------------- @app.route("/connectors/bigquery/save_app", methods=["POST"]) def bigquery_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 payload = request.get_json() or {} project_id = (payload.get("project_id") or "").strip() dataset_id = (payload.get("dataset_id") or "").strip() sa_json_raw = (payload.get("service_account_json") or "").strip() if not project_id or not dataset_id or not sa_json_raw: return jsonify({ "error": "project_id, dataset_id and service_account_json are required" }), 400 try: sa_info = json.loads(sa_json_raw) except Exception: return jsonify({"error": "Invalid service_account_json"}), 400 required_keys = ["type", "project_id", "private_key", "client_email", "token_uri"] missing = [k for k in required_keys if k not in sa_info] if missing: return jsonify({ "error": f"service_account_json missing keys: {', '.join(missing)}" }), 400 # Normalize configuration – store everything in encrypted config_json config = { "project_id": project_id or sa_info.get("project_id"), "dataset_id": dataset_id, "service_account": sa_info, } secured = encrypt_payload({ "config_json": json.dumps(config) }) con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, config_json, status, created_at) VALUES (?, 'bigquery', ?, 'configured', datetime('now')) """, ( uid, secured.get("config_json"), )) con.commit() con.close() ensure_connector_initialized(uid, "bigquery") return jsonify({"status": "saved"}) @app.route("/connectors/bigquery/connect") def bigquery_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_bigquery(uid) status = result.get("status") if status != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/bigquery/disconnect") def bigquery_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_bigquery(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/bigquery/sync") def bigquery_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 # BigQuery is a destination connector – sync here is a validation/schema refresh return jsonify(sync_bigquery(uid, sync_type="incremental")) @app.route("/api/status/bigquery") def bigquery_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT config_json, status FROM connector_configs WHERE uid=? AND connector='bigquery' LIMIT 1 """, (uid,)) cfg_row = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='bigquery' LIMIT 1 """, (uid,)) conn_row = fetchone_secure(cur) con.close() project_id = None dataset_id = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) project_id = cfg.get("project_id") dataset_id = cfg.get("dataset_id") except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), "project_id": project_id, "dataset_id": dataset_id, "status": (cfg_row.get("status") if cfg_row else None) }) @app.route("/connectors/bigquery/job/get") def bigquery_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='bigquery' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row.get("sync_type"), "schedule_time": row.get("schedule_time") }) @app.route("/connectors/bigquery/job/save", methods=["POST"]) def bigquery_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'bigquery', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- X (TWITTER) ---------------- @app.route("/connectors/x/save_app", methods=["POST"]) def x_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} client_id = data.get("client_id") client_secret = data.get("client_secret") redirect_uri = data.get("redirect_uri") or (get_base_url() + "/oauth/callback") if not client_id or not client_secret: return jsonify({"error": "client_id and client_secret are required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, scopes, status, created_at) VALUES (?, 'x', ?, ?, ?, 'configured', datetime('now')) """, ( uid, encrypt_value(client_id), encrypt_value(client_secret), encrypt_value(redirect_uri) )) con.commit() con.close() ensure_connector_initialized(uid, "x") return jsonify({"status": "saved"}) @app.route("/connectors/x/connect") def x_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 try: # Use unified redirect URI targeting the frontend proxy redirect_uri = get_base_url() + "/oauth/callback" return redirect(get_x_auth_url(uid, redirect_uri=redirect_uri)) except Exception as e: return jsonify({"error": str(e)}), 400 @app.route("/connectors/x/callback") def x_callback(): uid = getattr(g, "user_id", None) code = request.args.get("code") if not uid: return jsonify({"error": "Unauthorized"}), 401 if not code: return "Authorization failed", 400 result = handle_x_oauth_callback(uid, code) if result.get("status") != "success": return jsonify(result), 400 return redirect("/connectors/x") @app.route("/connectors/x/disconnect") def x_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_x(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/x/sync") def x_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='x' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_x(uid, sync_type=sync_type)) @app.route("/api/status/x") def x_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='x' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='x' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT username, x_user_id, expires_at FROM x_connections WHERE uid=? LIMIT 1 """, (uid,)) xr = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "username": xr["username"] if xr else None, "x_user_id": xr["x_user_id"] if xr else None, "expires_at": xr["expires_at"] if xr else None }) @app.route("/connectors/x/job/get") def x_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='x' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/x/job/save", methods=["POST"]) def x_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'x', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- LINKEDIN MARKETING ---------------- @app.route("/connectors/linkedin/save_app", methods=["POST"]) def linkedin_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} client_id = data.get("client_id") client_secret = data.get("client_secret") redirect_uri = data.get("redirect_uri") or (request.host_url.rstrip("/") + "/connectors/linkedin/callback") linkedin_version = data.get("linkedin_version") or "202503" if not client_id or not client_secret: return jsonify({"error": "client_id and client_secret are required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, api_key, scopes, status, created_at) VALUES (?, 'linkedin', ?, ?, ?, ?, 'configured', datetime('now')) """, ( uid, encrypt_value(client_id), encrypt_value(client_secret), encrypt_value(linkedin_version), encrypt_value(redirect_uri), )) con.commit() con.close() ensure_connector_initialized(uid, "linkedin") return jsonify({"status": "saved"}) @app.route("/connectors/linkedin/connect") def linkedin_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 try: oauth_state = secrets.token_urlsafe(24) # Append connector name for unified routing state_with_connector = f"{oauth_state}|linkedin" state = get_connector_state(uid, "linkedin") or {} state["oauth_state"] = oauth_state # Store the base state for CSRF validation state["oauth_state_expires_at"] = ( datetime.datetime.utcnow() + datetime.timedelta(minutes=10) ).isoformat() save_connector_state(uid, "linkedin", state) # Use unified redirect URI targeting the frontend proxy redirect_uri = get_base_url() + "/oauth/callback" return redirect(get_linkedin_auth_url(uid, state_with_connector, redirect_uri=redirect_uri)) except Exception as e: return jsonify({"error": str(e)}), 400 @app.route("/connectors/linkedin/callback") def linkedin_callback(): uid = getattr(g, "user_id", None) code = request.args.get("code") returned_state = request.args.get("state") if not uid: return jsonify({"error": "Unauthorized"}), 401 if not code: return "Authorization failed", 400 stored_state = get_connector_state(uid, "linkedin") or {} expected_state = stored_state.get("oauth_state") expires_at = stored_state.get("oauth_state_expires_at") expires_dt = None if expires_at: try: expires_dt = datetime.datetime.fromisoformat(expires_at.replace("Z", "+00:00")) if expires_dt.tzinfo is None: expires_dt = expires_dt.replace(tzinfo=datetime.timezone.utc) except Exception: expires_dt = None if not expected_state or not returned_state or returned_state != expected_state: return jsonify({"error": "Invalid OAuth state"}), 400 if not expires_dt or expires_dt < datetime.datetime.utcnow(): return jsonify({"error": "OAuth state expired"}), 400 result = handle_linkedin_oauth_callback(uid, code) if result.get("status") != "success": return jsonify(result), 400 stored_state.pop("oauth_state", None) stored_state.pop("oauth_state_expires_at", None) save_connector_state(uid, "linkedin", stored_state) return redirect("/connectors/linkedin") @app.route("/connectors/linkedin/disconnect") def linkedin_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_linkedin(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/linkedin/sync") def linkedin_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='linkedin' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_linkedin(uid, sync_type=sync_type)) @app.route("/api/status/linkedin") def linkedin_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='linkedin' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='linkedin' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT linkedin_member_id, expires_at FROM linkedin_connections WHERE uid=? LIMIT 1 """, (uid,)) lk = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "linkedin_member_id": lk["linkedin_member_id"] if lk else None, "expires_at": lk["expires_at"] if lk else None, }) @app.route("/connectors/linkedin/job/get") def linkedin_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='linkedin' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], }) @app.route("/connectors/linkedin/job/save", methods=["POST"]) def linkedin_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'linkedin', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- GITLAB ---------------- @app.route("/gitlab/connect") def gitlab_connect(): from backend.connectors.gitlab import get_auth_url uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 return redirect(get_auth_url(uid)) # ---------------- GITLAB SAVE CONFIG ---------------- @app.route("/connectors/gitlab/save_app", methods=["POST"]) def gitlab_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) client_id = data.get("client_id") client_secret = data.get("client_secret") if not client_id or not client_secret: return jsonify({ "error": "Client ID & Secret required" }), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, created_at) VALUES (?, 'gitlab', ?, ?, datetime('now')) """, ( uid, client_id, client_secret )) con.commit() con.close() ensure_connector_initialized(uid, "gitlab") return jsonify({"status": "saved"}) @app.route("/connectors/gitlab/disconnect") def gitlab_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # 1. Disable connection cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='gitlab' """, (uid,)) # 2. Delete token cur.execute(""" DELETE FROM gitlab_tokens WHERE uid=? """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/gitlab/callback") def gitlab_callback(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 code = request.args.get("code") from backend.connectors.gitlab import exchange_code, save_token data = exchange_code(uid, code) save_token(uid, data) # Enable connection con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'gitlab', 1) """, (uid,)) con.commit() con.close() return redirect("/connectors/gitlab") @app.route("/api/status/gitlab") def gitlab_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # credentials exist? cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='gitlab' LIMIT 1 """, (uid,)) creds = cur.fetchone() # connection enabled? cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='gitlab' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(row and row[0] == 1) }) @app.route("/connectors/gitlab/job/get") def gitlab_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='gitlab' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/gitlab/job/save", methods=["POST"]) def gitlab_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'gitlab', ?, ?) """, ( uid, sync_type, schedule_time )) con.commit() con.close() return jsonify({"status": "job_saved"}) @app.route("/connectors/gitlab/sync") def gitlab_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='gitlab' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='gitlab' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "historical" con.close() from backend.connectors.gitlab import ( sync_projects, sync_commits, sync_issues, sync_mrs ) projects_data = sync_projects(uid) con = get_db() cur = con.cursor() cur.execute(""" SELECT project_id FROM gitlab_projects WHERE uid=? """, (uid,)) project_ids = [r[0] for r in cur.fetchall()] con.close() new_rows = [] for pid in project_ids: commits = sync_commits(uid, pid, sync_type) issues = sync_issues(uid, pid, sync_type) mrs = sync_mrs(uid, pid) new_rows += commits.get("rows", []) new_rows += issues.get("rows", []) new_rows += mrs.get("rows", []) from backend.destinations.destination_router import push_to_destination con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source='gitlab' AND is_active=1 LIMIT 1 """, (uid,)) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "projects": len(project_ids), "rows_pushed": 0, "sync_type": sync_type }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } pushed = 0 if new_rows: pushed = push_to_destination(dest, "gitlab_data", new_rows) else: pushed = 0 print(f"[GITLAB] Sync type: {sync_type}", flush=True) print(f"[GITLAB] New rows found: {len(new_rows)}", flush=True) print(f"[GITLAB] Rows pushed: {pushed}", flush=True) return jsonify({ "projects": len(project_ids), "rows_pushed": pushed, "rows_found": len(new_rows), "sync_type": sync_type }) # ---------------- STACKOVERFLOW ---------------- @app.route("/connectors/stackoverflow/connect") def stackoverflow_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'stackoverflow', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) @app.route("/api/status/stackoverflow") def stackoverflow_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='stackoverflow' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) connected = bool(row and row[0] == 1) # api key cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='stackoverflow' LIMIT 1 """, (uid,)) key_row = fetchone_secure(cur) api_key_saved = bool(key_row and key_row[0]) con.close() return jsonify({ "has_credentials": api_key_saved, "connected": connected }) @app.route("/connectors/stackoverflow/disconnect") def stackoverflow_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='stackoverflow' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/stackoverflow/job/get") def stackoverflow_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='stackoverflow' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({}) return jsonify({ "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/stackoverflow/job/save", methods=["POST"]) def stackoverflow_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'stackoverflow', ?, ?) """, ( uid, data.get("sync_type"), data.get("schedule_time") )) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/connectors/stackoverflow/sync") def stackoverflow_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='stackoverflow' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 # Get sync type cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='stackoverflow' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "historical" con.close() from backend.connectors.stackoverflow import ( sync_questions, sync_answers, sync_users ) q_data = sync_questions(uid, sync_type) a_data = sync_answers(uid, sync_type) u_data = sync_users(uid) q_rows = q_data.get("rows", []) a_rows = a_data.get("rows", []) u_rows = u_data.get("rows", []) # Destination con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source=? AND is_active=1 LIMIT 1 """, (uid, "stackoverflow")) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "rows_pushed": 0, "sync_type": sync_type }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if q_rows: pushed += push_to_destination(dest, "stack_questions", q_rows) if a_rows: pushed += push_to_destination(dest, "stack_answers", a_rows) if u_rows: pushed += push_to_destination(dest, "stack_users", u_rows) print(f"[STACK] Sync type: {sync_type}", flush=True) print(f"[STACK] Questions: {len(q_rows)}", flush=True) print(f"[STACK] Answers: {len(a_rows)}", flush=True) print(f"[STACK] Users: {len(u_rows)}", flush=True) print(f"[STACK] Rows pushed: {pushed}", flush=True) return jsonify({ "questions": len(q_rows), "answers": len(a_rows), "users": len(u_rows), "rows_pushed": pushed, "sync_type": sync_type }) # ---------------- STACKOVERFLOW SAVE CONFIG ---------------- @app.route("/connectors/stackoverflow/save_config", methods=["POST"]) def stackoverflow_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) api_key = data.get("api_key") if not api_key: return jsonify({"error": "API key required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, api_key, created_at) VALUES (?, 'stackoverflow', ?, datetime('now')) """, (uid, api_key)) con.commit() con.close() return jsonify({"status": "saved"}) # ---------------- HACKERNEWS ---------------- @app.route("/hackernews/sync") def hackernews_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = sync_hackernews(uid) return jsonify(result) @app.route("/connectors/hackernews/sync") def hackernews_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='hackernews' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 # Get sync type cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='hackernews' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "incremental" con.close() from backend.connectors.hackernews import sync_hackernews data = sync_hackernews(uid, sync_type) rows = data.get("rows", []) # Destination con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source=? AND is_active=1 LIMIT 1 """, (uid, "hackernews")) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "stories": len(rows), "rows_pushed": 0, "sync_type": sync_type }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if rows: pushed = push_to_destination(dest, "hackernews_stories", rows) print(f"[HN] Sync type: {sync_type}", flush=True) print(f"[HN] Stories: {len(rows)}", flush=True) print(f"[HN] Rows pushed: {pushed}", flush=True) return jsonify({ "stories": len(rows), "rows_pushed": pushed, "sync_type": sync_type }) @app.route("/connectors/hackernews/connect") def hackernews_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'hackernews', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) @app.route("/connectors/hackernews/disconnect") def hackernews_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='hackernews' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/hackernews/job/get") def hackernews_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='hackernews' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({}) return jsonify({ "sync_type": row[0], "schedule_time": row[1] }) @app.route("/connectors/hackernews/job/save", methods=["POST"]) def hackernews_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'hackernews', ?, ?) """, ( uid, data.get("sync_type"), data.get("schedule_time") )) con.commit() con.close() return jsonify({"status": "saved"}) # ---------------- PRODUCTHUNT ---------------- @app.route("/connectors/producthunt/connect") def producthunt_connect(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='producthunt' """,(uid,)) if not cur.fetchone(): return jsonify({"error":"config missing"}),400 cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES (?,?,1) """,(uid,"producthunt")) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/producthunt/save_config", methods=["POST"]) def producthunt_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) or {} token = data.get("api_token") if not token: return jsonify({"error":"token required"}),400 con=get_db() cur=con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid,connector,config_json,created_at) VALUES (?,?,?,datetime('now')) """,( uid, "producthunt", json.dumps({"api_token":token}) )) con.commit() con.close() return jsonify({"status":"saved"}) @app.route("/connectors/producthunt/disconnect") def producthunt_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='producthunt' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/producthunt") def producthunt_status(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='producthunt' """,(uid,)) connected=bool( (r:=cur.fetchone()) and r[0]==1 ) cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='producthunt' """,(uid,)) has_credentials=bool(cur.fetchone()) con.close() return jsonify({ "connected":connected, "has_credentials":has_credentials }) @app.route("/connectors/producthunt/sync") def producthunt_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='producthunt' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "Product Hunt not connected"}), 400 # Get job config cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='producthunt' """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "incremental" con.close() # Run connector result = sync_producthunt( uid=uid, sync_type=sync_type ) rows = result.get("rows", []) # Destination con = get_db() con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source='producthunt' ORDER BY id DESC LIMIT 1 """, (uid,)) dest = cur.fetchone() con.close() if dest and rows: dest_cfg = dict(dest) dest_cfg["type"] = dest_cfg["dest_type"] inserted = push_to_destination( dest_cfg, "producthunt_data", rows ) return jsonify({ "status": "pushed_to_destination", "rows": inserted }) return jsonify({ "status": "stored_locally", "posts": result.get("posts", 0), "topics": result.get("topics", 0) }) # ---------------- PRODUCTHUNT DATA ---------------- @app.route("/producthunt/data/posts") def producthunt_data_posts(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = sqlite3.connect(DB) con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM producthunt_posts WHERE uid=? ORDER BY created_at DESC """, (uid,)) data = [dict(r) for r in cur.fetchall()] con.close() return jsonify(data) @app.route("/producthunt/data/topics") def producthunt_data_topics(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = sqlite3.connect(DB) con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM producthunt_topics WHERE uid=? ORDER BY followers DESC """, (uid,)) data = [dict(r) for r in cur.fetchall()] con.close() return jsonify(data) # ---------------- WIKIPEDIA ---------------- @app.route("/connectors/wikipedia/connect") def wikipedia_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES (?,?,1) """,(uid,"wikipedia")) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/wikipedia/disconnect") def wikipedia_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='wikipedia' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/wikipedia") def wikipedia_status(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='wikipedia' """,(uid,)) connected=bool( (r:=cur.fetchone()) and r[0]==1 ) con.close() return jsonify({ "connected":connected, "has_credentials":True }) @app.route("/connectors/wikipedia/sync") def wikipedia_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='wikipedia' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "Wikipedia not connected"}), 400 # Get job config cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='wikipedia' """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "incremental" con.close() # Run connector result = sync_wikipedia( uid=uid, sync_type=sync_type ) rows = result.get("rows", []) # Destination con = get_db() con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source='wikipedia' ORDER BY id DESC LIMIT 1 """, (uid,)) dest = cur.fetchone() con.close() if dest and rows: dest_cfg = dict(dest) dest_cfg["type"] = dest_cfg["dest_type"] inserted = push_to_destination( dest_cfg, "wikipedia_data", rows ) return jsonify({ "status": "pushed_to_destination", "rows": inserted }) return jsonify({ "status": "stored_locally", "recent_changes": result.get("recent_changes"), "new_pages": result.get("new_pages"), "most_viewed": result.get("most_viewed") }) # ---------------- PEERTUBE ---------------- @app.route("/connectors/peertube/sync") def peertube_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='peertube' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "PeerTube not connected"}), 400 sync_type = get_connector_sync_type(uid, "peertube") con.close() result = sync_peertube(uid, sync_type=sync_type) rows = result.get("rows", []) # ---- Destination ---- con = get_db() con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source='peertube' ORDER BY id DESC LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() dest = None if row: dest = dict(row) if "dest_type" in dest: dest["type"] = dest["dest_type"] if dest and rows: inserted = push_to_destination(dest, "peertube_videos", rows) return jsonify({ "status": "pushed_to_destination", "rows": inserted }) return jsonify({ "status": "stored_locally", "rows": len(rows) }) @app.route("/connectors/peertube/connect") def peertube_connect(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='peertube' """,(uid,)) if not cur.fetchone(): return jsonify({"error":"config missing"}),400 cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES (?,?,1) """,(uid,"peertube")) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/peertube/disconnect") def peertube_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='peertube' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/peertube/save_config", methods=["POST"]) def peertube_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) or {} instance = data.get("instance") if not instance: return jsonify({"error":"instance required"}),400 instance = instance.strip().rstrip("/") con=get_db() cur=con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid,connector,config_json,created_at) VALUES (?,?,?,datetime('now')) """,( uid, "peertube", json.dumps({"instance":instance}) )) con.commit() con.close() return jsonify({"status":"saved"}) @app.route("/api/status/peertube") def peertube_status(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='peertube' """,(uid,)) connected=bool( (r:=cur.fetchone()) and r[0]==1 ) cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='peertube' """,(uid,)) has_credentials=bool(cur.fetchone()) con.close() return jsonify({ "connected":connected, "has_credentials":has_credentials }) # ---------------- MASTODON ---------------- @app.route("/connectors/mastodon/sync") def mastodon_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='mastodon' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 # Get sync type cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='mastodon' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "historical" # Get instance cur.execute(""" SELECT state_json FROM connector_state WHERE uid=? AND source='mastodon' """, (uid,)) state_row = fetchone_secure(cur) instance = "https://mastodon.social" if state_row: state = json.loads(state_row[0]) instance = state.get("instance", instance) con.close() from backend.connectors.mastodon import sync_mastodon result = sync_mastodon(uid, instance, sync_type) rows = result.get("rows", []) # Destination lookup con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source=? AND is_active=1 LIMIT 1 """, (uid, "mastodon")) dest_row = fetchone_secure(cur) con.close() pushed = 0 if dest_row and rows: dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = push_to_destination(dest, "mastodon_statuses_data", rows) return jsonify({ "statuses": result.get("count", 0), "rows_pushed": pushed, "sync_type": sync_type }) @app.route("/connectors/mastodon/connect") def mastodon_connect(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT config_json FROM connector_configs WHERE uid=? AND connector='mastodon' LIMIT 1 """,(uid,)) row=cur.fetchone() if not row: con.close() return jsonify({"error":"config missing"}),400 state=json.loads(row[0]) instance=state.get("instance") cur.execute(""" INSERT OR REPLACE INTO connector_state (uid,source,state_json,updated_at) VALUES(?,?,?,?) """,( uid, "mastodon", json.dumps({"instance":instance}), datetime.datetime.utcnow().isoformat() )) cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES(?, 'mastodon',1) """,(uid,)) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/api/status/mastodon") def mastodon_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='mastodon' LIMIT 1 """,(uid,)) row = fetchone_secure(cur) connected = bool(row and row[0] == 1) # credentials cur.execute(""" SELECT config_json FROM connector_configs WHERE uid=? AND connector='mastodon' LIMIT 1 """,(uid,)) cfg = cur.fetchone() has_credentials = bool(cfg) con.close() return jsonify({ "connected": connected, "has_credentials": has_credentials }) @app.route("/connectors/mastodon/disconnect") def mastodon_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='mastodon' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) # ---------------- MASTODON SAVE CONFIG ---------------- @app.route("/connectors/mastodon/save_config", methods=["POST"]) def mastodon_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) or {} instance = data.get("instance") if not instance: return jsonify({"error":"instance required"}),400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, config_json, created_at) VALUES (?, 'mastodon', ?, datetime('now')) """,( uid, json.dumps({"instance":instance}) )) con.commit() con.close() return jsonify({"status":"saved"}) # ---------------- DISCOURSE ---------------- @app.route("/connectors/discourse/connect") def discourse_connect(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='discourse' """,(uid,)) if not cur.fetchone(): return jsonify({"error":"config missing"}),400 cur.execute(""" INSERT OR REPLACE INTO google_connections VALUES (?,?,1) """,(uid,"discourse")) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/discourse/save_config",methods=["POST"]) def discourse_save(): uid=get_uid() data = encrypt_payload(request.get_json()) or {} forum=data.get("forum") api_key=data.get("api_key") api_user=data.get("api_user","system") if not forum: return jsonify({"error":"forum required"}),400 con=get_db() cur=con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid,connector,config_json,created_at) VALUES (?,?,?,datetime('now')) """,( uid, "discourse", json.dumps({ "forum":forum, "api_key":api_key, "api_user":api_user }) )) con.commit() con.close() return jsonify({"status":"saved"}) @app.route("/connectors/discourse/disconnect") def discourse_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='discourse' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/discourse") def discourse_status(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='discourse' """,(uid,)) connected=bool( (r:=cur.fetchone()) and r[0]==1 ) cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='discourse' """,(uid,)) has_credentials=bool(cur.fetchone()) con.close() return jsonify({ "connected":connected, "has_credentials":has_credentials }) @app.route("/connectors/discourse/sync") def discourse_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='discourse' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "Discourse not connected"}), 400 # Get job config cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='discourse' """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "incremental" con.close() # Run connector result = sync_discourse( uid=uid, sync_type=sync_type ) rows = result.get("rows", []) # Destination con = get_db() con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source='discourse' ORDER BY id DESC LIMIT 1 """, (uid,)) dest = cur.fetchone() con.close() if dest and rows: dest_cfg = dict(dest) dest_cfg["type"] = dest_cfg["dest_type"] inserted = push_to_destination( dest_cfg, "discourse_topics_data", rows ) return jsonify({ "status": "pushed_to_destination", "rows": inserted }) return jsonify({ "status": "stored_locally", "new_topics": result.get("new_topics", 0), "categories": result.get("categories", 0), "users": result.get("users", 0) }) # ---------------- DISCOURSE DATA ---------------- @app.route("/discourse/data/topics") def discourse_topics(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = sqlite3.connect(REAL_DB) con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM discourse_topics WHERE uid=? ORDER BY created_at DESC LIMIT 200 """, (uid,)) rows = fetchall_secure(cur) con.close() return jsonify([dict(r) for r in rows]) @app.route("/discourse/data/categories") def discourse_categories(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = sqlite3.connect(REAL_DB) con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM discourse_categories WHERE uid=? """, (uid,)) rows = fetchall_secure(cur) con.close() return jsonify([dict(r) for r in rows]) # ---------------- LEMMY ---------------- @app.route("/connectors/lemmy/connect") def lemmy_connect(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT config_json FROM connector_configs WHERE uid=? AND connector='lemmy' LIMIT 1 """,(uid,)) row=cur.fetchone() if not row: con.close() return jsonify({"error":"config missing"}),400 state=json.loads(row[0]) cur.execute(""" INSERT OR REPLACE INTO connector_state (uid,source,state_json,updated_at) VALUES(?,?,?,?) """,( uid, "lemmy", json.dumps(state), datetime.datetime.utcnow().isoformat() )) cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES(?, 'lemmy',1) """,(uid,)) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/lemmy/disconnect") def lemmy_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='lemmy' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/lemmy/sync") def lemmy_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='lemmy' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 # Get sync type cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='lemmy' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "incremental" from backend.connectors.lemmy import sync_lemmy result = sync_lemmy(uid) # Fetch newly inserted posts con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM lemmy_posts WHERE uid=? ORDER BY fetched_at DESC LIMIT ? """, (uid, result.get("new_posts", 0))) rows = [] for r in cur.fetchall(): d = dict(r) d.pop("id", None) d.pop("uid", None) d.pop("raw_json", None) d.pop("fetched_at", None) rows.append(d) # Destination lookup cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source='lemmy' AND is_active=1 LIMIT 1 """, (uid,)) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "posts": result.get("new_posts", 0), "rows_pushed": 0, "sync_type": sync_type }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if rows: pushed = push_to_destination(dest, "lemmy_posts_data", rows) return jsonify({ "posts": result.get("new_posts", 0), "rows_pushed": pushed, "sync_type": sync_type }) # ---------------- LEMMY SAVE CONFIG ---------------- @app.route("/connectors/lemmy/save_config", methods=["POST"]) def lemmy_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) or {} instance = data.get("instance") if not instance: return jsonify({"error":"instance required"}),400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, config_json, created_at) VALUES (?, 'lemmy', ?, datetime('now')) """,( uid, json.dumps({"instance":instance}) )) con.commit() con.close() return jsonify({"status":"saved"}) @app.route("/api/status/lemmy") def lemmy_status(): uid=get_uid() con=get_db() cur=con.cursor() # connected cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='lemmy' LIMIT 1 """,(uid,)) row=cur.fetchone() connected = bool(row and row[0]==1) # credentials cur.execute(""" SELECT config_json FROM connector_configs WHERE uid=? AND connector='lemmy' LIMIT 1 """,(uid,)) cfg=cur.fetchone() has_credentials = bool(cfg) con.close() return jsonify({ "connected":connected, "has_credentials":has_credentials }) # ---------------- OPENSTREETMAP ---------------- @app.route("/connectors/openstreetmap/connect") def osm_connect(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid,source,enabled) VALUES (?,?,1) """,(uid,"openstreetmap")) con.commit() con.close() return jsonify({"status":"connected"}) @app.route("/connectors/openstreetmap/disconnect") def osm_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='openstreetmap' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/connectors/openstreetmap/sync") def osm_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='openstreetmap' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "OpenStreetMap not connected"}), 400 # Get job config cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='openstreetmap' """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "incremental" con.close() # Run connector result = sync_openstreetmap( uid=uid, sync_type=sync_type ) rows = result.get("rows", []) # Destination lookup con = get_db() con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source='openstreetmap' ORDER BY id DESC LIMIT 1 """, (uid,)) dest = cur.fetchone() con.close() if dest and rows: dest_cfg = dict(dest) dest_cfg["type"] = dest_cfg["dest_type"] inserted = push_to_destination( dest_cfg, "openstreetmap_data", rows ) return jsonify({ "status": "pushed_to_destination", "rows": inserted }) return jsonify({ "status": "stored_locally", "new_changesets": result.get("new_changesets", 0), "new_notes": result.get("new_notes", 0) }) @app.route("/api/status/openstreetmap") def osm_status(): uid=get_uid() con=get_db() cur=con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='openstreetmap' """,(uid,)) connected=bool( (r:=cur.fetchone()) and r[0]==1 ) con.close() return jsonify({ "connected":connected, "has_credentials":True }) # ---------------- NVD ---------------- @app.route("/connectors/nvd/sync") def nvd_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='nvd' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 # Get sync type cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='nvd' LIMIT 1 """, (uid,)) job = cur.fetchone() sync_type = job[0] if job else "incremental" con.close() from backend.connectors.nvd import sync_nvd result = sync_nvd(uid, sync_type) rows = result.get("rows", []) # -------- Destination ---------- con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source=? AND is_active=1 LIMIT 1 """, (uid, "nvd")) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "cves": len(rows), "rows_pushed": 0, "sync_type": sync_type }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if rows: pushed += push_to_destination(dest, "nvd_cves", rows) print(f"[NVD] Sync type: {sync_type}", flush=True) print(f"[NVD] CVEs: {len(rows)}", flush=True) print(f"[NVD] Rows pushed: {pushed}", flush=True) return jsonify({ "cves": len(rows), "rows_pushed": pushed, "sync_type": sync_type }) @app.route("/connectors/nvd/connect") def nvd_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'nvd', 1) """, (uid,)) con.commit() con.close() return jsonify({"status": "connected"}) @app.route("/connectors/nvd/disconnect") def nvd_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='nvd' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/api/status/nvd") def nvd_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # credentials cur.execute(""" SELECT api_key FROM connector_configs WHERE uid=? AND connector='nvd' LIMIT 1 """, (uid,)) creds = cur.fetchone() # connection cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='nvd' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds and creds[0]), "connected": bool(row and row[0] == 1) }) @app.route("/connectors/nvd/job/save", methods=["POST"]) def nvd_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'nvd', ?, ?) """, ( uid, data.get("sync_type"), data.get("schedule_time") )) con.commit() con.close() return jsonify({"status": "saved"}) # ---------------- NVD SAVE CONFIG ---------------- @app.route("/connectors/nvd/save_config", methods=["POST"]) def nvd_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = encrypt_payload(request.get_json()) api_key = data.get("api_key") keywords = data.get("keywords", []) if not api_key: return jsonify({"error": "API key required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs ( uid, connector, api_key, config_json, created_at ) VALUES (?, 'nvd', ?, ?, datetime('now')) """, ( uid, api_key, json.dumps({ "keywords": keywords }) )) con.commit() con.close() return jsonify({"status": "saved"}) # ---------------- PINTEREST ---------------- @app.route("/connectors/pinterest/connect") def pinterest_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 auth_url = pinterest_get_auth_url(uid) if not auth_url: return jsonify({"error":"credentials missing"}),400 return redirect(auth_url) @app.route("/connectors/pinterest/disconnect") def pinterest_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Remove token cur.execute("DELETE FROM pinterest_tokens WHERE uid=?", (uid,)) # Disable connector cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source='pinterest' """, (uid,)) con.commit() con.close() return jsonify({"status": "disconnected"}) @app.route("/pinterest/callback") def pinterest_callback(): uid = get_uid() code = request.args.get("code") print(f"[PINTEREST CALLBACK] uid={uid} code_present={bool(code)}", flush=True) if not uid: return jsonify({"error": "Unauthorized"}), 401 if not code: return jsonify({"error": "missing code"}), 400 try: token = pinterest_exchange_code(uid, code) if not token: print("[PINTEREST CALLBACK] Token exchange returned None", flush=True) return jsonify({"error": "token exchange failed"}), 400 access_token = token.get("access_token") refresh_token = token.get("refresh_token") expires_in = token.get("expires_in") if not access_token: print(f"[PINTEREST CALLBACK] No access_token in response: {token}", flush=True) return jsonify({"error": "missing access_token", "response": token}), 400 pinterest_save_token(uid, access_token, refresh_token, expires_in) con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'pinterest', 1) """, (uid,)) con.commit() con.close() print(f"[PINTEREST CALLBACK] Success — token saved for uid={uid}", flush=True) return redirect("/connectors/pinterest?connected=1") except Exception as e: print(f"[PINTEREST CALLBACK ERROR] {str(e)}", flush=True) return jsonify({"error": str(e)}), 500 @app.route("/connectors/pinterest/sync") def pinterest_sync_universal(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check enabled cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='pinterest' """, (uid,)) row = fetchone_secure(cur) if not row or row[0] != 1: con.close() return jsonify({"error": "not connected"}), 400 # Run sync result = sync_pinterest(uid) # Fetch newly inserted pins con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(""" SELECT * FROM pinterest_pins WHERE uid=? ORDER BY fetched_at DESC LIMIT ? """, (uid, result.get("pins", 0))) rows = [] for r in cur.fetchall(): d = dict(r) d.pop("id", None) d.pop("uid", None) d.pop("raw_json", None) d.pop("fetched_at", None) rows.append(d) # Destination lookup cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source='pinterest' AND is_active=1 LIMIT 1 """, (uid,)) dest_row = fetchone_secure(cur) con.close() if not dest_row: return jsonify({ "pins": result.get("pins", 0), "rows_pushed": 0 }) dest = { "type": dest_row[0], "host": dest_row[1], "port": dest_row[2], "username": dest_row[3], "password": dest_row[4], "database_name": dest_row[5] } from backend.destinations.destination_router import push_to_destination pushed = 0 if rows: pushed = push_to_destination(dest, "pinterest_pins_data", rows) return jsonify({ "pins": result.get("pins", 0), "rows_pushed": pushed }) # ---------------- PINTEREST SAVE CONFIG ---------------- @app.route("/connectors/pinterest/save_app", methods=["POST"]) def save_pinterest_app(): uid = get_uid() if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json(silent=True) or {} client_id = (data.get("client_id") or data.get("app_id") or "").strip() client_secret = (data.get("client_secret") or data.get("app_secret") or "").strip() if not client_id or not client_secret: return jsonify({"error": "missing credentials"}), 400 encrypted = encrypt_payload({ "client_id": client_id, "client_secret": client_secret }) con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_configs (uid, connector, client_id, client_secret, config_json, created_at) VALUES (?, ?, ?, ?, ?, ?) """, ( uid, "pinterest", encrypted["client_id"], encrypted["client_secret"], json.dumps(encrypted), datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "pinterest") return jsonify({"status": "saved"}) @app.route("/api/status/pinterest") def pinterest_status(): uid = get_uid() if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT access_token FROM pinterest_tokens WHERE uid=? LIMIT 1 """,(uid,)) connected = bool( (row := cur.fetchone()) and row[0] ) cur.execute(""" SELECT config_json FROM connector_configs WHERE uid=? AND connector='pinterest' LIMIT 1 """,(uid,)) row = cur.fetchone() has_credentials = bool( row and row[0] and row[0] != "{}" ) print("[PINTEREST STATUS]", { "uid": uid, "has_credentials": has_credentials, "connected": connected }, flush=True) con.close() return jsonify({ "connected": connected, "has_credentials": has_credentials }) # ---------------- FACEBOOK PAGES SAVE APP CREDENTIALS ---------------- @app.route("/connectors/facebook/save_app", methods=["POST"]) def facebook_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 print("SAVE UID:", uid, flush=True) data = request.get_json() app_id = data.get("app_id") app_secret = data.get("app_secret") if not app_id or not app_secret: return jsonify({"error": "App ID and App Secret required"}), 400 # Use unified redirect URI targeting the frontend proxy redirect_uri = get_base_url() + "/oauth/callback" con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO facebook_app_credentials (uid, app_id, app_secret, redirect_uri, created_at) VALUES (?, ?, ?, ?, ?) """, ( uid, app_id, app_secret, redirect_uri, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "facebook") return jsonify({"status": "saved"}) #-------------- Temporary route to test saving Facebook credentials without going through the UI -------------- @app.route("/connectors/facebook/test_save", methods=["GET"]) def facebook_test_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO facebook_app_credentials (uid, app_id, app_secret, redirect_uri, created_at) VALUES (?, ?, ?, ?, ?) """, ( uid, "TEST_APP_ID", "TEST_SECRET", get_base_url() + "/oauth/callback", datetime.datetime.utcnow().isoformat() )) con.commit() con.close() return "Test credentials saved" # ---------------- FACEBOOK PAGES CONNECT ---------------- @app.route("/connectors/facebook/connect", methods=["GET"]) def facebook_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT app_id FROM facebook_app_credentials WHERE uid=? """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return "App credentials not saved", 400 app_id = row[0] # Use unified redirect URI targeting the frontend proxy redirect_uri = get_base_url() + "/oauth/callback" params = { "client_id": app_id, "redirect_uri": redirect_uri, "scope": "pages_show_list,pages_read_engagement,pages_read_user_content,read_insights", "response_type": "code", "state": "facebook" # Pass connector name for unified routing } auth_url = "https://www.facebook.com/v19.0/dialog/oauth?" + urlencode(params) return redirect(auth_url) # ---------------- FACEBOOK PAGES CALLBACK ---------------- @app.route("/connectors/facebook/callback", methods=["GET"]) def facebook_callback(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 code = request.args.get("code") if not code: return "Authorization failed: No code received", 400 # Get user app credentials con = get_db() cur = con.cursor() cur.execute(""" SELECT app_id, app_secret, redirect_uri FROM facebook_app_credentials WHERE uid=? """, (uid,)) row = fetchone_secure(cur) if not row: con.close() return "App credentials not found", 400 app_id, app_secret, redirect_uri = row con.close() # Exchange code for user access token token_res = requests.get( "https://graph.facebook.com/v19.0/oauth/access_token", params={ "client_id": app_id, "redirect_uri": redirect_uri, "client_secret": app_secret, "code": code }, timeout=30 ) token_data = token_res.json() user_token = token_data.get("access_token") if not user_token: return jsonify({"error": "Token exchange failed", "details": token_data}), 400 # Get managed pages pages_res = requests.get( "https://graph.facebook.com/v19.0/me/accounts", params={"access_token": user_token}, timeout=30 ) pages_data = pages_res.json() pages = pages_data.get("data", []) if not pages: return jsonify({"error": "No pages found"}), 400 # For now select first page (we can improve later) page = pages[0] # Store page connection con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO facebook_connections (uid, page_id, page_name, page_access_token, connected_at) VALUES (?, ?, ?, ?, ?) """, ( uid, page.get("id"), page.get("name"), page.get("access_token"), datetime.datetime.utcnow().isoformat() )) # Enable connector cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'facebook', 1) """, (uid,)) con.commit() con.close() return "Facebook Page Connected Successfully" # ---------------- FACEBOOK PAGES DISCONNECT ---------------- @app.route("/connectors/facebook/disconnect", methods=["GET"]) def facebook_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Remove stored page token cur.execute("DELETE FROM facebook_connections WHERE uid=?", (uid,)) # Disable connector cur.execute(""" UPDATE google_connections SET enabled = 0 WHERE uid=? AND source='facebook' """, (uid,)) con.commit() con.close() return jsonify({"status": "facebook disconnected"}) # ---------------- FACEBOOK PAGES SYNC ---------------- @app.route("/connectors/facebook/sync") def facebook_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 # Check enabled con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='facebook' """, (uid,)) row = fetchone_secure(cur) con.close() if not row or row[0] != 1: return jsonify({"error": "Facebook not connected"}), 400 sync_type = "historical" job_res = get_connector_job("facebook") try: job_data = job_res.get_json() if job_data.get("exists"): sync_type = job_data.get("sync_type", "historical") except: import traceback; traceback.print_exc() print('Exception caught', flush=True) result = sync_facebook_pages(uid, sync_type) # Destination push con = get_db() cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source='facebook' ORDER BY id DESC LIMIT 1 """, (uid,)) dest = cur.fetchone() con.close() if dest and result.get("rows"): inserted = push_to_destination(dest, "facebook_data", result["rows"]) return jsonify({ "status": "pushed_to_destination", "rows": inserted }) return jsonify({ "status": "stored_locally", "posts": result.get("posts", 0), "insights": result.get("insights", 0) }) @app.route("/connectors/facebook/job/get") def facebook_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='facebook' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- FACEBOOK PAGES STATUS ---------------- @app.route("/api/status/facebook") def facebook_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check saved app credentials cur.execute(""" SELECT 1 FROM facebook_app_credentials WHERE uid=? LIMIT 1 """, (uid,)) creds = cur.fetchone() # Check page connection cur.execute(""" SELECT 1 FROM facebook_connections WHERE uid=? LIMIT 1 """, (uid,)) conn = cur.fetchone() con.close() return jsonify({ "connected": bool(conn), "has_credentials": bool(creds) }) @app.route("/connectors//disconnect") def connector_disconnect(source): print("DISCONNECT CALLED:", source, flush=True) con = sqlite3.connect(DB) cur = con.cursor() cur.execute(""" UPDATE google_connections SET enabled=0 WHERE source=? """, (source,)) con.commit() con.close() return jsonify({"status": "disconnected"}) # ---------------- FACEBOOK ADS CONNECT ---------------- @app.route("/connectors/facebook_ads/connect", methods=["GET"]) def facebook_ads_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 print("ADS CONNECT UID:", uid, flush=True) con = get_db() cur = con.cursor() cur.execute(""" SELECT app_id, redirect_uri FROM facebook_app_credentials WHERE uid=? """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return "App credentials not saved", 400 app_id, redirect_uri = row # IMPORTANT: different scope for Ads params = { "client_id": app_id, "redirect_uri": redirect_uri, "scope": "ads_read,ads_management", "response_type": "code" } auth_url = "https://www.facebook.com/v19.0/dialog/oauth?" + urlencode(params) return redirect(auth_url) # ---------------- FACEBOOK ADS CALLBACK ---------------- @app.route("/connectors/facebook_ads/callback", methods=["GET"]) def facebook_ads_callback(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 code = request.args.get("code") if not code: return "Authorization failed: No code received", 400 # Get app credentials con = get_db() cur = con.cursor() cur.execute(""" SELECT app_id, app_secret, redirect_uri FROM facebook_app_credentials WHERE uid=? """, (uid,)) row = fetchone_secure(cur) if not row: con.close() return "App credentials not found", 400 app_id, app_secret, redirect_uri = row con.close() # Exchange code for token token_res = requests.get( "https://graph.facebook.com/v19.0/oauth/access_token", params={ "client_id": app_id, "redirect_uri": redirect_uri, "client_secret": app_secret, "code": code }, timeout=30 ) token_data = token_res.json() user_token = token_data.get("access_token") if not user_token: return jsonify({"error": "Token exchange failed", "details": token_data}), 400 # Fetch Ad Accounts accounts_res = requests.get( "https://graph.facebook.com/v19.0/me/adaccounts", params={ "access_token": user_token, "fields": "id,name,account_status,currency,timezone_name" }, timeout=30 ) accounts_data = accounts_res.json() accounts = accounts_data.get("data", []) if not accounts: return jsonify({"error": "No Ad Accounts found"}), 400 # Select first ad account for now account = accounts[0] con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO facebook_ads_connections (uid, ad_account_id, ad_account_name, access_token, connected_at) VALUES (?, ?, ?, ?, ?) """, ( uid, account.get("id"), account.get("name"), user_token, datetime.datetime.utcnow().isoformat() )) # Enable connector cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'facebook_ads', 1) """, (uid,)) con.commit() con.close() return "Facebook Ads Connected Successfully" # ---------------- FACEBOOK ADS STATUS ---------------- @app.route("/api/status/facebook_ads") def facebook_ads_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Check saved app credentials cur.execute(""" SELECT 1 FROM facebook_app_credentials WHERE uid=? LIMIT 1 """, (uid,)) creds = cur.fetchone() # Check ads connection cur.execute(""" SELECT 1 FROM facebook_ads_connections WHERE uid=? LIMIT 1 """, (uid,)) conn = cur.fetchone() con.close() return jsonify({ "connected": bool(conn), "has_credentials": bool(creds) }) # ---------------- FACEBOOK ADS DISCONNECT ---------------- @app.route("/connectors/facebook_ads/disconnect", methods=["GET"]) def facebook_ads_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Remove stored ad account connection cur.execute("DELETE FROM facebook_ads_connections WHERE uid=?", (uid,)) # Disable connector cur.execute(""" UPDATE google_connections SET enabled = 0 WHERE uid=? AND source='facebook_ads' """, (uid,)) con.commit() con.close() return jsonify({"status": "facebook_ads disconnected"}) # ---------------- FACEBOOK ADS JOB GET ---------------- @app.route("/connectors/facebook_ads/job/get") def facebook_ads_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='facebook_ads' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row[0], "schedule_time": row[1] }) # ---------------- FACEBOOK ADS JOB SAVE ---------------- @app.route("/connectors/facebook_ads/job/save", methods=["POST"]) def facebook_ads_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'facebook_ads', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- FACEBOOK ADS SYNC ---------------- from backend.connectors.facebook_ads import sync_facebook_ads @app.route("/connectors/facebook_ads/sync") def facebook_ads_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 # Check enabled con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='facebook_ads' """, (uid,)) row = fetchone_secure(cur) con.close() if not row or row[0] != 1: return jsonify({"error": "Facebook Ads not connected"}), 400 sync_type = "historical" job_res = facebook_ads_job_get() try: job_data = job_res.get_json() if job_data.get("exists"): sync_type = job_data.get("sync_type", "historical") except: import traceback; traceback.print_exc() print('Exception caught', flush=True) result = sync_facebook_ads(uid, sync_type) # Destination push con = get_db() cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source='facebook_ads' ORDER BY id DESC LIMIT 1 """, (uid,)) dest = cur.fetchone() con.close() if dest and result.get("rows"): inserted = push_to_destination(dest, "facebook_ads_data", result["rows"]) return jsonify({ "status": "pushed_to_destination", "rows": inserted }) return jsonify({ "status": "stored_locally", "campaigns": result.get("campaigns", 0), "adsets": result.get("adsets", 0), "ads": result.get("ads", 0), "insights": result.get("insights", 0) }) # ---------------- FACEBOOK ADS SAVE APP CREDENTIALS ---------------- @app.route("/connectors/facebook_ads/save_app", methods=["POST"]) def facebook_ads_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() app_id = data.get("app_id") app_secret = data.get("app_secret") if not app_id or not app_secret: return jsonify({"error": "App ID and App Secret required"}), 400 redirect_uri = request.host_url.rstrip("/") + "/connectors/facebook_ads/callback" con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO facebook_app_credentials (uid, app_id, app_secret, redirect_uri, created_at) VALUES (?, ?, ?, ?, ?) """, ( uid, app_id, app_secret, redirect_uri, datetime.datetime.utcnow().isoformat() )) con.commit() con.close() ensure_connector_initialized(uid, "facebook_ads") return jsonify({"status": "saved"}) @app.route("/connectors//connect") def connector_connect(source): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 state = _resolve_connector_contract(uid, source) if state["connected"]: return jsonify({"connected": True}) if source in OAUTH_SOURCES: if not _has_connector_config(uid, source): return jsonify({"connected": False, "error": "missing credentials"}), 400 auth_url = _oauth_redirect_for(source) if not auth_url: return jsonify({"connected": False, "error": "missing credentials"}), 400 if _is_internal_ai_request(): return jsonify({ "connected": False, "auth_required": True, "redirect": auth_url, }) return redirect(auth_url) if not _has_connector_config(uid, source): return jsonify({"connected": False, "error": "missing credentials"}), 400 result = _run_connector_connect_probe(uid, source) if isinstance(result, dict) and ( result.get("connected") is True or result.get("status") in ("success", "connected") ): return jsonify({"connected": True}) error = _normalize_probe_error(result or {}) return jsonify({"connected": False, "error": error}), 400 @app.route("/connectors//disconnect") def disconnect_connector(source): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # Disable connection cur.execute(""" UPDATE google_connections SET enabled=0 WHERE uid=? AND source=? """, (uid, source)) # Disable scheduled jobs cur.execute(""" UPDATE connector_jobs SET enabled=0 WHERE uid=? AND source=? """, (uid, source)) con.commit() con.close() return jsonify({ "status": "success", "message": f"{source} disconnected" }) #sdk_route @app.route("/static/sdk/") def sdk_file(filename): return send_from_directory("static/sdk", filename) # ---------------- DOMAINS API ---------------- @app.route("/api/domains") def get_domains(): con = get_db() cur = con.cursor() cur.execute(""" SELECT DISTINCT domain FROM visits WHERE domain IS NOT NULL AND domain != '' ORDER BY domain """) rows = fetchall_secure(cur) # List of dicts domains = [r["domain"] for r in rows] # Use key instead of r[0] con.close() return jsonify(domains) @app.route("/api/dashboard") def api_dashboard(): domain = request.args.get("domain") if not domain: return jsonify({"error": "domain required"}), 400 con = get_db() cur = con.cursor() # ---------------- VISITS (FULL) ---------------- cur.execute(""" SELECT uid,domain,browser,os,device,ip, screen,language,timezone, referrer,page_url,user_agent, name,age,gender,city,country,profession, ts FROM visits WHERE domain=? ORDER BY ts DESC LIMIT 300 """,(domain,)) visits = cur.fetchall() # ---------------- EVENTS (FULL) ---------------- cur.execute(""" SELECT uid,domain,event, device_id,session_id, meta,ts FROM web_events WHERE domain=? ORDER BY ts DESC LIMIT 500 """,(domain,)) events = cur.fetchall() # ---------------- IDENTITIES ---------------- uids = list(set([v[0] for v in visits])) identities = [] if uids: placeholders = ",".join("?" for _ in uids) cur.execute(f""" SELECT uid,email,device_id, session_id,external_id, created_at FROM identity_map WHERE uid IN ({placeholders}) ORDER BY created_at DESC """, uids) identities = cur.fetchall() con.close() return jsonify({ "domain": domain, "visits": visits, "events": events, "identities": identities }) @app.route("/google/job/save/", methods=["POST"]) def save_connector_job(source): data = request.json sync_type = data.get("sync_type") schedule_time = data.get("schedule_time") uid = getattr(g, "user_id", None) if not sync_type or not schedule_time: return jsonify({ "status": "error", "message": "Missing fields" }), 400 con = get_db() cur = con.cursor() try: # Remove old job cur.execute(""" DELETE FROM connector_jobs WHERE uid=? AND source=? """, (uid, source)) # Insert new job cur.execute(""" INSERT INTO connector_jobs (uid, source, sync_type, schedule_time, enabled, created_at) VALUES (?, ?, ?, ?, 1, ?) """, ( uid, source, sync_type, schedule_time, datetime.datetime.now(IST).isoformat() )) con.commit() finally: con.close() return jsonify({"status": "ok"}) @app.route("/google/job/get/") def get_connector_job(source): uid = getattr(g, "user_id", None) con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time, enabled FROM connector_jobs WHERE uid=? AND source=? LIMIT 1 """, (uid, source)) row = fetchone_secure(cur) # Dict con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], # Use key "schedule_time": row["schedule_time"], "enabled": row["enabled"] }) def get_connector_sync_type(uid, source): con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source=? AND enabled=1 LIMIT 1 """, (uid, source)) row = fetchone_secure(cur) # Dict con.close() if not row: return "historical" return row["sync_type"] # Use key @app.route("/destination/save", methods=["POST"]) def save_destination(): data = encrypt_payload(request.json) uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 source = data.get("source") dest_type = data.get("type") host = data.get("host") port = data.get("port") username = data.get("username") password = data.get("password") database = data.get("database") # -------- FORMAT CONTROL -------- format_value = data.get("format") # allow format ONLY for object-storage destinations if dest_type not in ["s3", "bigquery", "azure_datalake"]: format_value = None # ---------------- Validation ---------------- # if not source or not dest_type: return jsonify({ "status": "error", "msg": "Missing source or destination type" }), 400 # ---------- MySQL / Postgres Validation ---------- if dest_type in ["mysql", "postgres"]: if not host or not username or not database: return jsonify({ "status": "error", "msg": "Missing database credentials" }), 400 # ---------- Redshift Validation ---------- if dest_type == "redshift": if not host or not username or not password or not database: return jsonify({ "status": "error", "msg": "Missing Redshift credentials" }), 400 # ---------- BigQuery Validation ---------- if dest_type == "bigquery": if not host or not password or not database: return jsonify({ "status": "error", "msg": "Missing BigQuery credentials" }), 400 # ---------- Azure Data Lake Validation ---------- if dest_type == "azure_datalake": if not host or not port or not password: return jsonify({ "status": "error", "msg": "Missing Azure Data Lake credentials" }), 400 # ---------- Databricks Validation ---------- if dest_type == "databricks": if not host or not port or not password: return jsonify({ "status": "error", "msg": "Missing Databricks credentials" }), 400 con = get_db() cur = con.cursor() try: # ---------------- Deactivate Old ---------------- cur.execute(""" UPDATE destination_configs SET is_active = 0 WHERE uid=? AND source=? """, (uid, source)) # ---------------- Insert New ---------------- cur.execute(""" INSERT INTO destination_configs ( uid, source, dest_type, host, port, username, password, database_name, is_active, created_at, format ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( uid, source, dest_type, host, port, username, password, database, 1, # active datetime.datetime.utcnow().isoformat(), format_value )) con.commit() except Exception as e: con.rollback() con.close() return jsonify({ "status": "error", "msg": str(e) }), 500 finally: con.close() return jsonify({ "status": "ok", "message": "Destination saved and activated" }) @app.route("/destination/list/") def list_destinations(source): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT id, dest_type, host, port, username, database_name, is_active, created_at, format FROM destination_configs WHERE uid=? AND source=? ORDER BY created_at DESC """, (uid, source)) rows = fetchall_secure(cur) con.close() result = [] for r in rows: result.append({ "id": r["id"], "type": r["dest_type"], "host": r["host"], "port": r["port"], "username": r["username"], "database": r["database_name"], "active": bool(r["is_active"]), "created_at": r["created_at"] }) return jsonify({ "status": "ok", "destinations": result }) @app.route("/destination/activate", methods=["POST"]) def activate_destination(): data = request.get_json() uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 source = data.get("source") dest_id = data.get("dest_id") if not source or not dest_id: return jsonify({ "status": "error", "msg": "Missing fields" }), 400 con = get_db() cur = con.cursor() try: # Disable all cur.execute(""" UPDATE destination_configs SET is_active=0 WHERE uid=? AND source=? """, (uid, source)) # Enable selected cur.execute(""" UPDATE destination_configs SET is_active=1 WHERE id=? AND uid=? AND source=? """, (dest_id, uid, source)) con.commit() except Exception as e: con.rollback() con.close() return jsonify({ "status": "error", "msg": str(e) }), 500 con.close() return jsonify({ "status": "ok", "message": "Destination activated" }) @app.route("/destination/delete", methods=["POST"]) def delete_destination(): data = request.json dest_id = data.get("id") uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 if not dest_id: return jsonify({"error": "Missing id"}), 400 con = get_db() cur = con.cursor() cur.execute(""" DELETE FROM destination_configs WHERE id=? AND uid=? """, (dest_id, uid)) con.commit() con.close() return jsonify({"status": "ok"}) #============ Helper Function ============== def get_connector_state(uid, source): con = get_db() cur = con.cursor() cur.execute(""" SELECT state_json FROM connector_state WHERE uid=? AND source=? """, (uid, source)) row = fetchone_secure(cur) # Dict con.close() if not row: return None return json.loads(row["state_json"]) # Use key def save_connector_state(uid, source, state_dict): con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_state (uid, source, state_json, updated_at) VALUES (?, ?, ?, ?) """, ( uid, source, json.dumps(state_dict), datetime.datetime.now(IST).isoformat() )) con.commit() con.close() def get_destination(uid, source): con = get_db() cur = con.cursor() cur.execute(""" SELECT dest_type, host, port, username, password, database_name, format FROM destination_configs WHERE uid=? AND source=? ORDER BY id DESC LIMIT 1 """, (uid, source)) row = fetchone_secure(cur) # Dict con.close() if not row: return None return { "type": row["dest_type"], # Use keys "host": row["host"], "port": row["port"], "username": row["username"], "password": row["password"], "database_name": row["database_name"] } def get_active_destination(uid, source): con = get_db() # con.row_factory = sqlite3.Row # Remove this; auto_decrypt_row handles dict conversion cur = con.cursor() cur.execute(""" SELECT * FROM destination_configs WHERE uid=? AND source=? AND is_active=1 LIMIT 1 """, (uid, source)) row = fetchone_secure(cur) # Already dict con.close() if not row: return None return row # No need for dict(row); it's already a dict # ---------- GA4 STATUS (FINAL) ---------- @app.route("/api/status/ga4") def ga4_status(): uid = getattr(g, "user_id", None) con = get_db() cur = con.cursor() cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='ga4' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) # Dict con.close() return jsonify({ "connected": bool(row and row["enabled"] == 1) # Use key }) @app.route("/connectors//job/save", methods=["POST"]) def universal_job_save(source): uid = get_uid() if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() schedule_time = data.get("schedule_time") sync_type = data.get("sync_type", "incremental") if not schedule_time: return jsonify({"error": "Schedule time required"}), 400 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time, enabled) VALUES (?, ?, ?, ?, 1) """, ( uid, source, sync_type, schedule_time )) con.commit() con.close() return jsonify({"status": "saved"}) @app.route("/connectors//job/get", methods=["GET"]) def universal_job_get(source): uid = get_uid() if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time, enabled FROM connector_jobs WHERE uid=? AND source=? LIMIT 1 """, (uid, source)) row = cur.fetchone() con.close() if not row: return jsonify({ "status": "not_configured" }) return jsonify({ "status": "configured", "sync_type": row[0], "schedule_time": row[1], "enabled": bool(row[2]) }) # ---------------- UNIVERSAL SYNC ENGINE ---------------- @app.route("/connectors//sync") def universal_sync(source): uid = get_uid() if not uid: return jsonify({"error": "Unauthorized"}), 401 # execution mode (manual / scheduled) mode = request.args.get("mode", "manual") if mode not in ["manual", "scheduled"]: mode = "manual" run_id = log_sync_start(uid, source, mode) try: # Try dynamic function resolution possible_names = [ f"sync_{source}", f"sync_{source}_files", f"sync_{source}_data" ] sync_func = None for name in possible_names: if name in globals(): sync_func = globals()[name] break if not sync_func: raise Exception(f"No sync function found for source: {source}") print(f"[UNIVERSAL SYNC] Running {source} ({mode})", flush=True) # CALL WITHOUT ARGUMENTS result = sync_func() rows_synced = result if isinstance(result, int) else 0 log_sync_finish(run_id, rows_synced, "success") return jsonify({ "status": "success", "rows_synced": rows_synced }) except Exception as e: print("[SYNC ERROR]", str(e), flush=True) log_sync_finish(run_id, 0, "failed", str(e)) return jsonify({ "status": "failed", "error": str(e) }), 500 # ---------------- WHATSAPP ---------------- from backend.connectors.whatsapp import ( sync_whatsapp, disconnect_whatsapp ) @app.route("/connectors/whatsapp/save_app", methods=["POST"]) def whatsapp_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} access_token = data.get("access_token") waba_id = data.get("waba_id") phone_number_id = data.get("phone_number_id") if not access_token or not waba_id or not phone_number_id: return jsonify({"error": "access_token, waba_id, and phone_number_id are required"}), 400 # Validate credentials with Meta Graph API url = f"https://graph.facebook.com/v18.0/{waba_id}" response = requests.get( url, headers={"Authorization": f"Bearer {access_token}"} ) if response.status_code != 200: return jsonify({ "error": "Invalid WhatsApp credentials", "details": response.json() }), 403 con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO whatsapp_connections (uid, access_token_encrypted, waba_id, phone_number_id, connected_at) VALUES (?, ?, ?, ?, datetime('now')) """, ( uid, encrypt_value(access_token), waba_id, phone_number_id )) con.commit() con.close() ensure_connector_initialized(uid, "whatsapp") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO google_connections (uid, source, enabled) VALUES (?, 'whatsapp', 1) """, (uid,)) con.commit() con.close() # Initialize connector state from backend.connectors.whatsapp import save_state save_state(uid, {"last_sync_date": None}) return jsonify({"status": "saved"}), 200 @app.route("/api/status/whatsapp") def whatsapp_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM whatsapp_connections WHERE uid=?", (uid,)) row = cur.fetchone() con.close() if row: return jsonify({"connected": True, "has_credentials": True}) return jsonify({"connected": False, "has_credentials": False}) @app.route("/connectors/whatsapp/sync") def whatsapp_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='whatsapp' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" try: rows = sync_whatsapp(uid, sync_type) return jsonify({"status": "success", "rows_synced": rows}) except Exception as e: return jsonify({"error": str(e)}), 400 @app.route("/connectors/whatsapp/disconnect") def whatsapp_disconnect_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_whatsapp(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/whatsapp/job/get") def whatsapp_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time, enabled FROM connector_jobs WHERE uid=? AND source='whatsapp' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() if row: return jsonify({ "status": "configured", "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], "enabled": bool(row["enabled"]) }) return jsonify({"status": "not_configured", "sync_type": "historical", "schedule_time": "00:00"}) @app.route("/connectors/whatsapp/job/save", methods=["POST"]) def whatsapp_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "historical") schedule_time = data.get("schedule_time", "00:00") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time, enabled, created_at) VALUES (?, 'whatsapp', ?, ?, 1, datetime('now')) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # UNIVERSAL SYNC LOGGER (SAFE VERSION) # NOTE: # Logging is handled centrally via Flask middleware. # This prevents scheduler argument conflicts. def wrap_sync_function(func_name, func): def wrapper(*args, **kwargs): uid = get_uid() source = func_name.replace("sync_", "") run_id = None try: if uid: mode = request.args.get( "mode", request.headers.get( "X-Sync-Mode", "manual" ) ) run_id = log_sync_start(uid, source, mode) print(f"[USAGE] Sync started → {source}", flush=True) # SAFE CALL result = func() rows = result if isinstance(result, int) else 0 if run_id: log_sync_finish(run_id, rows, "success") return result except Exception as e: if run_id: log_sync_finish(run_id, 0, "failed", str(e)) raise e return wrapper def auto_wrap_all_syncs(): """ Disabled auto wrapping. Middleware logging replaces this safely. """ print("[USAGE] Sync auto-wrapper disabled (middleware active)", flush=True) # DO NOT WRAP FUNCTIONS ANYMORE auto_wrap_all_syncs() # USAGE ANALYTICS API (FIXED & PRODUCTION READY) @app.route("/api/usage") def usage_analytics(): uid = get_uid() if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() # ---------------- ACCOUNT OVERVIEW ---------------- cur.execute(""" SELECT created_at, company_name, is_individual FROM users WHERE id=? """, (uid,)) user_row = cur.fetchone() account_created = user_row[0] if user_row else None company_name = user_row[1] if user_row else "" is_individual = bool(user_row[2]) if user_row else True cur.execute(""" SELECT COUNT(*) FROM user_sessions WHERE user_id=? """, (uid,)) total_sessions = cur.fetchone()[0] cur.execute(""" SELECT COUNT(*) FROM api_usage_logs WHERE uid=? """, (uid,)) total_api_calls = cur.fetchone()[0] # ---------------- CONNECTOR METRICS ---------------- TOTAL_CONNECTORS = 48 cur.execute(""" SELECT COUNT(*) FROM google_connections WHERE uid=? AND enabled=1 """, (uid,)) connected_connectors = cur.fetchone()[0] disconnected_connectors = TOTAL_CONNECTORS - connected_connectors cur.execute(""" SELECT MIN(started_at) FROM sync_runs WHERE uid=? """, (uid,)) first_connected_date = cur.fetchone()[0] cur.execute(""" SELECT MAX(finished_at) FROM sync_runs WHERE uid=? AND status='success' """, (uid,)) last_sync_time = cur.fetchone()[0] cur.execute(""" SELECT COUNT(*) FROM sync_runs WHERE uid=? """, (uid,)) total_sync_runs = cur.fetchone()[0] cur.execute(""" SELECT COUNT(*) FROM sync_runs WHERE uid=? AND status='failed' """, (uid,)) failed_sync_runs = cur.fetchone()[0] cur.execute(""" SELECT sync_type, COUNT(*) FROM sync_runs WHERE uid=? GROUP BY sync_type """, (uid,)) sync_type_breakdown = dict(cur.fetchall()) # ---------------- DATA VOLUME (REAL SOURCE) ---------------- # IMPORTANT: Now reading from destination_push_logs cur.execute(""" SELECT COALESCE(SUM(rows_pushed),0) FROM destination_push_logs WHERE uid=? """, (uid,)) total_records_synced = cur.fetchone()[0] cur.execute(""" SELECT COALESCE(SUM(rows_pushed),0) FROM destination_push_logs WHERE uid=? AND date(pushed_at)=date('now') """, (uid,)) records_today = cur.fetchone()[0] cur.execute(""" SELECT COALESCE(SUM(rows_pushed),0) FROM destination_push_logs WHERE uid=? AND strftime('%Y-%m', pushed_at)=strftime('%Y-%m','now') """, (uid,)) records_this_month = cur.fetchone()[0] cur.execute(""" SELECT source, SUM(rows_pushed) FROM destination_push_logs WHERE uid=? GROUP BY source """, (uid,)) records_per_connector = cur.fetchall() largest_connector = None if records_per_connector: largest_connector = max( records_per_connector, key=lambda x: x[1] ) # ---------------- DESTINATION METRICS ---------------- cur.execute(""" SELECT COUNT(*) FROM destination_configs WHERE uid=? """, (uid,)) total_destinations = cur.fetchone()[0] cur.execute(""" SELECT COUNT(*) FROM destination_configs WHERE uid=? AND is_active=1 """, (uid,)) active_destinations = cur.fetchone()[0] cur.execute(""" SELECT destination_type, SUM(rows_pushed) FROM destination_push_logs WHERE uid=? GROUP BY destination_type """, (uid,)) rows_per_destination = cur.fetchall() cur.execute(""" SELECT MAX(pushed_at) FROM destination_push_logs WHERE uid=? """, (uid,)) last_push_time = cur.fetchone()[0] cur.execute(""" SELECT COUNT(*) FROM destination_push_logs WHERE uid=? AND status='failed' """, (uid,)) push_failures = cur.fetchone()[0] # ---------------- SCHEDULER ---------------- cur.execute(""" SELECT COUNT(*) FROM connector_jobs WHERE uid=? AND enabled=1 """, (uid,)) scheduled_jobs = cur.fetchone()[0] # ---------------- DAILY USAGE (last 14 days) ---------------- cur.execute(""" SELECT DATE(pushed_at) as date, SUM(rows_pushed) as rows FROM destination_push_logs WHERE uid=? GROUP BY DATE(pushed_at) ORDER BY DATE(pushed_at) DESC LIMIT 14 """, (uid,)) raw_daily = {row[0]: row[1] for row in cur.fetchall()} daily_usage = [] for offset in range(13, -1, -1): day_str = (datetime.date.today() - datetime.timedelta(days=offset)).isoformat() daily_usage.append({ "date": day_str, "rows": raw_daily.get(day_str, 0) }) # ---------------- TOP CONNECTORS ---------------- cur.execute(""" SELECT source, SUM(rows_pushed) as rows FROM destination_push_logs WHERE uid=? GROUP BY source ORDER BY rows DESC LIMIT 10 """, (uid,)) top_connectors = [ {"source": row[0], "rows": row[1]} for row in cur.fetchall() ] con.close() # ---------------- HEALTH CALCULATION ---------------- success_rate = 0 if total_sync_runs > 0: success_rate = round( ((total_sync_runs - failed_sync_runs) / total_sync_runs) * 100, 2 ) return jsonify({ "account": { "created_at": account_created, "company_name": company_name, "is_individual": is_individual, "total_sessions": total_sessions, "total_api_calls": total_api_calls }, "connectors": { "total_available": TOTAL_CONNECTORS, "connected": connected_connectors, "disconnected": disconnected_connectors, "first_connected": first_connected_date, "last_sync": last_sync_time, "total_sync_runs": total_sync_runs, "failed_sync_runs": failed_sync_runs, "sync_type_breakdown": sync_type_breakdown }, "data_volume": { "total_records_synced": total_records_synced, "records_today": records_today, "records_this_month": records_this_month, "records_per_connector": records_per_connector, "largest_connector": largest_connector }, "destinations": { "total": total_destinations, "active": active_destinations, "rows_per_destination": rows_per_destination, "last_push_time": last_push_time, "push_failures": push_failures }, "scheduler": { "scheduled_jobs": scheduled_jobs }, "health": { "sync_success_rate": success_rate }, "daily_usage": daily_usage, "top_connectors": top_connectors, "last_sync": last_sync_time, "stats": { "connectedConnectors": connected_connectors, "totalRecords": total_records_synced, "activeDestinations": active_destinations, "successRate": success_rate } }) # ---------------- RUN ---------------- def seed_test_user(): con = get_db() cur = con.cursor() try: cur.execute("SELECT id FROM users WHERE email='test@example.com'") if cur.fetchone(): return import uuid from werkzeug.security import generate_password_hash import datetime user_id = str(uuid.uuid4()) password_hash = generate_password_hash("testpassword") secured = encrypt_payload({"password": password_hash}) cur.execute(""" INSERT INTO users( id,email,password, first_name,last_name, company_name,company_size, country,phone, is_individual, created_at ) VALUES(?,?,?,?,?,?,?,?,?,?,?) """, ( user_id, "test@example.com", secured["password"], "Test", "User", "Segmento Test", "1-50", "Testland", "1234567890", 1, datetime.datetime.utcnow().isoformat() )) con.commit() print("Seeded test user: test@example.com / testpassword", flush=True) except Exception as e: print("Failed to seed test user:", e, flush=True) finally: con.close() # -------- CHARTBEAT -------- @app.route("/connectors/chartbeat/save_app", methods=["POST"]) def chartbeat_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} api_key = data.get("api_key", "").strip() host = data.get("host", "").strip() query_id = data.get("query_id", "").strip() or None if not api_key or not host: return jsonify({"error": "api_key and host are required"}), 400 from backend.connectors.chartbeat import save_credentials save_credentials(uid, api_key, host, query_id) ensure_connector_initialized(uid, "chartbeat") return jsonify({"status": "saved"}) @app.route("/connectors/chartbeat/connect") def chartbeat_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_chartbeat(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/chartbeat/disconnect") def chartbeat_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_chartbeat(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/chartbeat/sync") def chartbeat_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='chartbeat' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_chartbeat(uid, sync_type=sync_type)) @app.route("/api/status/chartbeat") def chartbeat_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='chartbeat' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='chartbeat' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT host FROM chartbeat_connections WHERE uid=? LIMIT 1 """, (uid,)) cb = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "host": cb["host"] if cb else None, }) @app.route("/connectors/chartbeat/job/get") def chartbeat_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='chartbeat' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], }) @app.route("/connectors/chartbeat/job/save", methods=["POST"]) def chartbeat_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'chartbeat', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # -------- SLACK -------- @app.route("/connectors/slack/save_app", methods=["POST"]) def api_slack_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.json or {} bot_token = data.get("bot_token") if not bot_token: return jsonify({"error": "Missing bot token"}), 400 save_slack_config(uid, bot_token) ensure_connector_initialized(uid, "slack") return jsonify({"status": "saved"}) @app.route("/connectors/slack/connect") def api_slack_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_slack(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/slack/sync") def api_slack_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='slack' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "incremental" return jsonify(sync_slack(uid, sync_type=sync_type)) @app.route("/connectors/slack/disconnect") def api_slack_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 return jsonify(disconnect_slack(uid)) @app.route("/api/status/slack") def slack_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='slack' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='slack' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1) }) @app.route("/connectors/slack/job/get") def slack_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='slack' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], }) @app.route("/connectors/slack/job/save", methods=["POST"]) def slack_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'slack', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # -------- STRIPE -------- @app.route("/connectors/stripe/save_app", methods=["POST"]) def stripe_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} secret_key = (data.get("secret_key") or "").strip() if not secret_key: return jsonify({"error": "secret_key is required"}), 400 save_stripe_credentials(uid, secret_key) ensure_connector_initialized(uid, "stripe") return jsonify({"status": "saved"}) @app.route("/connectors/stripe/connect") def stripe_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_stripe(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/stripe/disconnect") def stripe_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_stripe(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/stripe/sync") def stripe_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='stripe' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_stripe(uid, sync_type=sync_type)) @app.route("/api/status/stripe") def stripe_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='stripe' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='stripe' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT account_id, display_name FROM stripe_connections WHERE uid=? LIMIT 1 """, (uid,)) stripe_conn = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "account_id": stripe_conn["account_id"] if stripe_conn else None, "display_name": stripe_conn["display_name"] if stripe_conn else None, }) @app.route("/connectors/stripe/job/get") def stripe_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='stripe' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], }) @app.route("/connectors/stripe/job/save", methods=["POST"]) def stripe_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'stripe', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # -------- SOCIAL INSIDER -------- @app.route("/connectors/socialinsider/save_app", methods=["POST"]) def socialinsider_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} api_key = data.get("api_key", "").strip() platform = data.get("platform", "").strip() handle = data.get("handle", "").strip() if not api_key or not platform or not handle: return jsonify({"error": "api_key, platform, and handle are required"}), 400 from backend.connectors.socialinsider import save_credentials save_credentials(uid, api_key, platform, handle) ensure_connector_initialized(uid, "socialinsider") return jsonify({"status": "saved"}) @app.route("/connectors/socialinsider/connect") def socialinsider_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_socialinsider(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/socialinsider/disconnect") def socialinsider_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_socialinsider(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/socialinsider/sync") def socialinsider_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='socialinsider' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_socialinsider(uid, sync_type=sync_type)) @app.route("/api/status/socialinsider") def socialinsider_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='socialinsider' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='socialinsider' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) cur.execute(""" SELECT platform, handle FROM socialinsider_connections WHERE uid=? LIMIT 1 """, (uid,)) si = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1), "platform": si["platform"] if si else None, "handle": si["handle"] if si else None, }) @app.route("/connectors/socialinsider/job/get") def socialinsider_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='socialinsider' """, (uid,)) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], }) @app.route("/connectors/socialinsider/job/save", methods=["POST"]) def socialinsider_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'socialinsider', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # -------- AWS RDS -------- @app.route("/connectors/aws_rds/save_app", methods=["POST"]) def aws_rds_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} engine = data.get("engine", "").strip() host = data.get("host", "").strip() port = data.get("port") database = data.get("database", "").strip() username = data.get("username", "").strip() password = data.get("password", "") if not engine or not host or not port or not database or not username or not password: return jsonify({"error": "All fields are required: engine, host, port, database, username, password"}), 400 try: port = int(port) except (TypeError, ValueError): return jsonify({"error": "Port must be a valid integer"}), 400 save_rds_config(uid, engine, host, port, database, username, password) ensure_connector_initialized(uid, "aws_rds") return jsonify({"status": "saved"}) @app.route("/connectors/aws_rds/connect") def aws_rds_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_rds(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/aws_rds/disconnect") def aws_rds_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_rds(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/aws_rds/sync") def aws_rds_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='aws_rds' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_rds(uid, sync_type=sync_type)) @app.route("/api/status/aws_rds") def aws_rds_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='aws_rds' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='aws_rds' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() # Decode config to surface engine/host in status response engine = None host = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) engine = cfg.get("engine") host = cfg.get("host") except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row["enabled"] == 1), "engine": engine, "host": host, }) @app.route("/connectors/aws_rds/job/get") def aws_rds_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='aws_rds' """, (uid,), ) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], }) @app.route("/connectors/aws_rds/job/save", methods=["POST"]) def aws_rds_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'aws_rds', ?, ?) """, (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # -------- AWS DYNAMODB -------- @app.route("/connectors/dynamodb/save_app", methods=["POST"]) def dynamodb_save_app(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} access_key = data.get("access_key", "").strip() secret_key = data.get("secret_key", "") region = data.get("region", "").strip() if not access_key or not secret_key or not region: return jsonify({"error": "All fields are required: access_key, secret_key, region"}), 400 save_dynamodb_config(uid, access_key, secret_key, region) ensure_connector_initialized(uid, "dynamodb") return jsonify({"status": "saved"}) @app.route("/connectors/dynamodb/connect") def dynamodb_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 result = connect_dynamodb(uid) if result.get("status") != "success": return jsonify(result), 400 return jsonify(result) @app.route("/connectors/dynamodb/disconnect") def dynamodb_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 disconnect_dynamodb(uid) return jsonify({"status": "disconnected"}) @app.route("/connectors/dynamodb/sync") def dynamodb_sync_route(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='dynamodb' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_dynamodb(uid, sync_type=sync_type)) @app.route("/api/status/dynamodb") def dynamodb_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='dynamodb' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='dynamodb' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() region = None access_key = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) region = cfg.get("region") raw_access_key = cfg.get("access_key") if raw_access_key: access_key = f"{raw_access_key[:4]}{'*' * max(len(raw_access_key) - 8, 4)}{raw_access_key[-4:]}" except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row["enabled"] == 1), "region": region, "access_key": access_key, }) @app.route("/connectors/dynamodb/job/get") def dynamodb_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 con = get_db() cur = con.cursor() try: cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='dynamodb' """, (uid,), ) row = fetchone_secure(cur) finally: con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], }) @app.route("/connectors/dynamodb/job/save", methods=["POST"]) def dynamodb_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "Unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'dynamodb', ?, ?) """, (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= NOTION ======================== @app.route("/connectors/notion/save_app", methods=["POST"]) def _notion_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} access_token = (data.get("access_token") or "").strip() if not access_token: return jsonify({"error": "missing token"}), 400 save_notion_config(uid, access_token) ensure_connector_initialized(uid, "notion") return jsonify({"status": "saved"}) @app.route("/connectors/notion/connect") def _notion_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_notion(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/notion/disconnect") def _notion_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = disconnect_notion(uid) return jsonify(res) @app.route("/connectors/notion/sync") def _notion_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='notion' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_notion(uid, sync_type=sync_type)) @app.route("/api/status/notion") def _notion_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='notion' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='notion' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() access_token = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) raw_token = cfg.get("access_token") if raw_token: access_token = f"{raw_token[:4]}{'*' * max(len(raw_token) - 8, 4)}{raw_token[-4:]}" except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify( { "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), "access_token": access_token, } ) @app.route("/connectors/notion/job/get") def get_notion_job(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source=? """, (uid, "notion"), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify( { "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], } ) @app.route("/connectors/notion/job/save", methods=["POST"]) def save_notion_job(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, ?, ?, ?) """, (uid, "notion", sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= HUBSPOT ======================== @app.route("/connectors/hubspot/save_app", methods=["POST"]) def hubspot_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} access_token = (data.get("access_token") or "").strip() if not access_token: return jsonify({"error": "missing access_token"}), 400 save_hubspot_config(uid, access_token) ensure_connector_initialized(uid, "hubspot") return jsonify({"status": "saved"}) @app.route("/connectors/hubspot/connect") def hubspot_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_hubspot(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/hubspot/disconnect") def hubspot_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_hubspot(uid)) @app.route("/connectors/hubspot/sync") def hubspot_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='hubspot' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_hubspot(uid, sync_type=sync_type)) @app.route("/api/status/hubspot") def hubspot_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='hubspot' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='hubspot' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() access_token = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) raw_token = cfg.get("access_token") if raw_token: access_token = f"{raw_token[:4]}{'*' * max(len(raw_token) - 8, 4)}{raw_token[-4:]}" except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify( { "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), "access_token": access_token, } ) @app.route("/connectors/hubspot/job/get") def hubspot_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source=? """, (uid, "hubspot"), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify( { "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], } ) @app.route("/connectors/hubspot/job/save", methods=["POST"]) def hubspot_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, ?, ?, ?) """, (uid, "hubspot", sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= ZENDESK ======================== @app.route("/connectors/zendesk/save_app", methods=["POST"]) def _zendesk_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} subdomain = data.get("subdomain") email = data.get("email") api_token = data.get("api_token") if not subdomain or not email or not api_token: return jsonify({"error": "missing subdomain, email or api_token"}), 400 save_zendesk_config(uid, subdomain, email, api_token) ensure_connector_initialized(uid, "zendesk") return jsonify({"status": "saved"}) @app.route("/connectors/zendesk/connect") def _zendesk_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_zendesk(uid) return jsonify(res) @app.route("/connectors/zendesk/disconnect") def _zendesk_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_zendesk(uid)) @app.route("/connectors/zendesk/sync") def _zendesk_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT sync_type FROM connector_jobs WHERE uid=? AND source='zendesk' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_zendesk(uid, sync_type=sync_type)) @app.route("/api/status/zendesk") def _zendesk_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM connector_configs WHERE uid=? AND connector='zendesk'", (uid,)) creds = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='zendesk'", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(creds), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/zendesk/job/get") def _zendesk_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='zendesk'", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/zendesk/job/save", methods=["POST"]) def _zendesk_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'zendesk', ?, ?)", (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= INTERCOM ======================== @app.route("/connectors/intercom/save_app", methods=["POST"]) def _intercom_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} access_token = data.get("access_token") if not access_token: return jsonify({"error": "missing access_token"}), 400 save_intercom_config(uid, access_token) ensure_connector_initialized(uid, "intercom") return jsonify({"status": "saved"}) @app.route("/connectors/intercom/connect") def _intercom_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_intercom(uid)) @app.route("/connectors/intercom/disconnect") def _intercom_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_intercom(uid)) @app.route("/connectors/intercom/sync") def _intercom_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT sync_type FROM connector_jobs WHERE uid=? AND source='intercom' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_intercom(uid, sync_type=sync_type)) @app.route("/api/status/intercom") def _intercom_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM connector_configs WHERE uid=? AND connector='intercom'", (uid,)) creds = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='intercom'", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(creds), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/intercom/job/get") def _intercom_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='intercom'", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/intercom/job/save", methods=["POST"]) def _intercom_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'intercom', ?, ?)", (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= SHOPIFY ======================== @app.route("/connectors/shopify/save_app", methods=["POST"]) def _shopify_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} shop_domain = (data.get("shopDomain") or "").strip() access_token = (data.get("accessToken") or "").strip() if not shop_domain or not access_token: return jsonify({"error": "missing shopDomain or accessToken"}), 400 save_shopify_config(uid, shop_domain, access_token) ensure_connector_initialized(uid, "shopify") return jsonify({"status": "saved"}) @app.route("/connectors/shopify/connect") def _shopify_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_shopify(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/shopify/disconnect") def _shopify_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = disconnect_shopify(uid) return jsonify(res) @app.route("/connectors/shopify/sync") def _shopify_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type FROM connector_jobs WHERE uid=? AND source='shopify' LIMIT 1 """, (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_shopify(uid, sync_type=sync_type)) @app.route("/api/status/shopify") def _shopify_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT 1 FROM connector_configs WHERE uid=? AND connector='shopify' LIMIT 1 """, (uid,)) creds = fetchone_secure(cur) cur.execute(""" SELECT enabled FROM google_connections WHERE uid=? AND source='shopify' LIMIT 1 """, (uid,)) conn = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(creds), "connected": bool(conn and conn["enabled"] == 1) }) @app.route("/connectors/shopify/job/get") def _shopify_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute(""" SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='shopify' """, (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], }) @app.route("/connectors/shopify/job/save", methods=["POST"]) def _shopify_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute(""" INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'shopify', ?, ?) """, (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= MAILCHIMP ======================== @app.route("/connectors/mailchimp/save_app", methods=["POST"]) def _mailchimp_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_key = data.get("api_key") if not api_key: return jsonify({"error": "missing api_key"}), 400 save_mailchimp_config(uid, api_key) ensure_connector_initialized(uid, "mailchimp") return jsonify({"status": "saved"}) @app.route("/connectors/mailchimp/connect") def _mailchimp_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_mailchimp(uid)) @app.route("/connectors/mailchimp/disconnect") def _mailchimp_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_mailchimp(uid)) @app.route("/connectors/mailchimp/sync") def _mailchimp_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT sync_type FROM connector_jobs WHERE uid=? AND source='mailchimp' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_mailchimp(uid, sync_type=sync_type)) @app.route("/api/status/mailchimp") def _mailchimp_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM connector_configs WHERE uid=? AND connector='mailchimp'", (uid,)) creds = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='mailchimp'", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(creds), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/mailchimp/job/get") def _mailchimp_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='mailchimp'", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/mailchimp/job/save", methods=["POST"]) def _mailchimp_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'mailchimp', ?, ?)", (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= TWILIO ======================== @app.route("/connectors/twilio/save_app", methods=["POST"]) def _twilio_save_config(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} account_sid = data.get("account_sid") auth_token = data.get("auth_token") if not account_sid or not auth_token: return jsonify({"error": "missing account_sid or auth_token"}), 400 save_twilio_config(uid, account_sid, auth_token) ensure_connector_initialized(uid, "twilio") return jsonify({"status": "saved"}) @app.route("/connectors/twilio/connect") def _twilio_connect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_twilio(uid)) @app.route("/connectors/twilio/disconnect") def _twilio_disconnect(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_twilio(uid)) @app.route("/connectors/twilio/sync") def _twilio_sync(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT sync_type FROM connector_jobs WHERE uid=? AND source='twilio' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_twilio(uid, sync_type=sync_type)) @app.route("/api/status/twilio") def _twilio_status(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM connector_configs WHERE uid=? AND connector='twilio'", (uid,)) creds = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='twilio'", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(creds), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/twilio/job/get") def _twilio_job_get(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='twilio'", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/twilio/job/save", methods=["POST"]) def _twilio_job_save(): uid = getattr(g, "user_id", None) if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'twilio', ?, ?)", (uid, sync_type, schedule_time)) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= AIRTABLE ======================== @app.route("/connectors/airtable/save_app", methods=["POST"]) def airtable_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} access_token = (data.get("access_token") or "").strip() base_id = (data.get("base_id") or "").strip() table_name = (data.get("table_name") or "").strip() if not access_token or not base_id or not table_name: return jsonify({"error": "missing fields: access_token, base_id, table_name"}), 400 save_airtable_config(uid, access_token, base_id, table_name) ensure_connector_initialized(uid, "airtable") return jsonify({"status": "saved"}) @app.route("/connectors/airtable/connect") def airtable_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_airtable(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/airtable/disconnect") def airtable_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = disconnect_airtable(uid) return jsonify(res) @app.route("/connectors/airtable/sync") def airtable_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='airtable' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_airtable(uid, sync_type=sync_type)) @app.route("/api/status/airtable") def airtable_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='airtable' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='airtable' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() access_token = None base_id = None table_name = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) base_id = cfg.get("base_id") table_name = cfg.get("table_name") raw_token = cfg.get("access_token") if raw_token: access_token = f"{raw_token[:4]}{'*' * max(len(raw_token) - 8, 4)}{raw_token[-4:]}" except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify( { "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), "access_token": access_token, "base_id": base_id, "table_name": table_name, } ) @app.route("/connectors/airtable/job/get") def airtable_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source=? """, (uid, "airtable"), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify( { "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], } ) @app.route("/connectors/airtable/job/save", methods=["POST"]) def airtable_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, ?, ?, ?) """, (uid, "airtable", sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- PIPEDRIVE ---------------- @app.route("/connectors/pipedrive/save_app", methods=["POST"]) def pipedrive_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_token = (data.get("api_token") or "").strip() if not api_token: return jsonify({"error": "missing field: api_token"}), 400 save_pipedrive_config(uid, api_token) ensure_connector_initialized(uid, "pipedrive") return jsonify({"status": "saved"}) @app.route("/connectors/pipedrive/connect") def pipedrive_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_pipedrive(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/pipedrive/disconnect") def pipedrive_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = disconnect_pipedrive(uid) return jsonify(res) @app.route("/connectors/pipedrive/sync") def pipedrive_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='pipedrive' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_pipedrive(uid, sync_type=sync_type)) @app.route("/api/status/pipedrive") def pipedrive_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='pipedrive' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='pipedrive' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() api_token = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) raw_token = cfg.get("api_token") if raw_token: api_token = f"{raw_token[:4]}{'*' * max(len(raw_token) - 8, 4)}{raw_token[-4:]}" except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify( { "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), "api_token": api_token, } ) @app.route("/connectors/pipedrive/job/get") def pipedrive_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source=? """, (uid, "pipedrive"), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify( { "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], } ) @app.route("/connectors/pipedrive/job/save", methods=["POST"]) def pipedrive_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, ?, ?, ?) """, (uid, "pipedrive", sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- FRESHDESK ---------------- @app.route("/connectors/freshdesk/save_app", methods=["POST"]) def freshdesk_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} domain = (data.get("domain") or "").strip() api_key = (data.get("api_key") or "").strip() if not domain or not api_key: return jsonify({"error": "missing fields: domain, api_key"}), 400 save_freshdesk_config(uid, domain, api_key) ensure_connector_initialized(uid, "freshdesk") return jsonify({"status": "saved"}) @app.route("/connectors/freshdesk/connect") def freshdesk_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_freshdesk(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/freshdesk/disconnect") def freshdesk_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = disconnect_freshdesk(uid) return jsonify(res) @app.route("/connectors/freshdesk/sync") def freshdesk_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='freshdesk' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_freshdesk(uid, sync_type=sync_type)) @app.route("/api/status/freshdesk") def freshdesk_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='freshdesk' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='freshdesk' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() api_key = None domain = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) domain = cfg.get("domain") raw_key = cfg.get("api_key") if raw_key: api_key = f"{raw_key[:4]}{'*' * max(len(raw_key) - 8, 4)}{raw_key[-4:]}" except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify( { "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), "api_key": api_key, "domain": domain, } ) @app.route("/connectors/freshdesk/job/get") def freshdesk_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source=? """, (uid, "freshdesk"), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify( { "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], } ) @app.route("/connectors/freshdesk/job/save", methods=["POST"]) def freshdesk_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, ?, ?, ?) """, (uid, "freshdesk", sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- KLAVIYO ---------------- @app.route("/connectors/klaviyo/save_app", methods=["POST"]) def klaviyo_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_key = (data.get("api_key") or "").strip() if not api_key: return jsonify({"error": "missing field: api_key"}), 400 save_klaviyo_config(uid, api_key) ensure_connector_initialized(uid, "klaviyo") return jsonify({"status": "saved"}) @app.route("/connectors/klaviyo/connect") def klaviyo_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_klaviyo(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/klaviyo/disconnect") def klaviyo_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = disconnect_klaviyo(uid) return jsonify(res) @app.route("/connectors/klaviyo/sync") def klaviyo_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='klaviyo' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_klaviyo(uid, sync_type=sync_type)) @app.route("/api/status/klaviyo") def klaviyo_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='klaviyo' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='klaviyo' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() api_key = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) raw_key = cfg.get("api_key") if raw_key: api_key = f"{raw_key[:4]}{'*' * max(len(raw_key) - 8, 4)}{raw_key[-4:]}" except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify( { "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), "api_key": api_key, } ) @app.route("/connectors/klaviyo/job/get") def klaviyo_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source=? """, (uid, "klaviyo"), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify( { "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], } ) @app.route("/connectors/klaviyo/job/save", methods=["POST"]) def klaviyo_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, ?, ?, ?) """, (uid, "klaviyo", sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ---------------- AMPLITUDE ---------------- @app.route("/connectors/amplitude/save_app", methods=["POST"]) def amplitude_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_key = (data.get("api_key") or "").strip() secret_key = (data.get("secret_key") or "").strip() if not api_key or not secret_key: return jsonify({"error": "missing fields: api_key, secret_key"}), 400 save_amplitude_config(uid, api_key, secret_key) ensure_connector_initialized(uid, "amplitude") return jsonify({"status": "saved"}) @app.route("/connectors/amplitude/connect") def amplitude_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_amplitude(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/amplitude/disconnect") def amplitude_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = disconnect_amplitude(uid) return jsonify(res) @app.route("/connectors/amplitude/sync") def amplitude_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type FROM connector_jobs WHERE uid=? AND source='amplitude' LIMIT 1 """, (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_amplitude(uid, sync_type=sync_type)) @app.route("/api/status/amplitude") def amplitude_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT config_json FROM connector_configs WHERE uid=? AND connector='amplitude' LIMIT 1 """, (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( """ SELECT enabled FROM google_connections WHERE uid=? AND source='amplitude' LIMIT 1 """, (uid,), ) conn_row = fetchone_secure(cur) con.close() api_key = None secret_key = None if cfg_row and cfg_row.get("config_json"): try: cfg = json.loads(cfg_row["config_json"]) raw_api_key = cfg.get("api_key") raw_secret_key = cfg.get("secret_key") if raw_api_key: api_key = f"{raw_api_key[:4]}{'*' * max(len(raw_api_key) - 8, 4)}{raw_api_key[-4:]}" if raw_secret_key: secret_key = f"{raw_secret_key[:4]}{'*' * max(len(raw_secret_key) - 8, 4)}{raw_secret_key[-4:]}" except Exception: import traceback; traceback.print_exc() print('Exception caught', flush=True) return jsonify( { "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), "api_key": api_key, "secret_key": secret_key, } ) @app.route("/connectors/amplitude/job/get") def amplitude_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( """ SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source=? """, (uid, "amplitude"), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify( { "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"], } ) @app.route("/connectors/amplitude/job/save", methods=["POST"]) def amplitude_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( """ INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, ?, ?, ?) """, (uid, "amplitude", sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # SALESFORCE @app.route("/connectors/salesforce/save_app", methods=["POST"]) def salesforce_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_salesforce_config(uid, client_id=data.get("client_id", ""), client_secret=data.get("client_secret", ""), instance_url=data.get("instance_url", "")) return jsonify({"status": "saved"}) @app.route("/connectors/salesforce/connect") def salesforce_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_salesforce(uid)) @app.route("/connectors/salesforce/disconnect") def salesforce_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_salesforce(uid)) @app.route("/connectors/salesforce/sync") def salesforce_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_salesforce(uid, sync_type)) @app.route("/api/status/salesforce") def status_salesforce(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='salesforce' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='salesforce' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/salesforce/job/get") def salesforce_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='salesforce' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/salesforce/job/save", methods=["POST"]) def salesforce_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'salesforce', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # JIRA @app.route("/connectors/jira/save_app", methods=["POST"]) def jira_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_jira_config(uid, email=data.get("email", ""), api_token=data.get("api_token", ""), domain=data.get("domain", "")) return jsonify({"status": "saved"}) @app.route("/connectors/jira/connect") def jira_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_jira(uid)) @app.route("/connectors/jira/disconnect") def jira_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_jira(uid)) @app.route("/connectors/jira/sync") def jira_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_jira(uid, sync_type)) @app.route("/api/status/jira") def status_jira(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='jira' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='jira' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/jira/job/get") def jira_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='jira' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/jira/job/save", methods=["POST"]) def jira_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'jira', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # ZOHO CRM @app.route("/connectors/zoho_crm/save_app", methods=["POST"]) def zoho_crm_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_zoho_crm_config(uid, client_id=data.get("client_id", ""), client_secret=data.get("client_secret", ""), refresh_token=data.get("refresh_token", "")) return jsonify({"status": "saved"}) @app.route("/connectors/zoho_crm/connect") def zoho_crm_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_zoho_crm(uid)) @app.route("/connectors/zoho_crm/disconnect") def zoho_crm_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_zoho_crm(uid)) @app.route("/connectors/zoho_crm/sync") def zoho_crm_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_zoho_crm(uid, sync_type)) @app.route("/api/status/zoho_crm") def status_zoho_crm(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='zoho_crm' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='zoho_crm' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/zoho_crm/job/get") def zoho_crm_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='zoho_crm' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/zoho_crm/job/save", methods=["POST"]) def zoho_crm_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'zoho_crm', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # PAYPAL @app.route("/connectors/paypal/save_app", methods=["POST"]) def paypal_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_paypal_config(uid, client_id=data.get("client_id", ""), client_secret=data.get("client_secret", ""), use_sandbox=data.get("use_sandbox", False)) return jsonify({"status": "saved"}) @app.route("/connectors/paypal/connect") def paypal_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_paypal(uid)) @app.route("/connectors/paypal/disconnect") def paypal_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_paypal(uid)) @app.route("/connectors/paypal/sync") def paypal_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_paypal(uid, sync_type)) @app.route("/api/status/paypal") def status_paypal(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='paypal' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='paypal' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/paypal/job/get") def paypal_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='paypal' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/paypal/job/save", methods=["POST"]) def paypal_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'paypal', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # ASANA @app.route("/connectors/asana/save_app", methods=["POST"]) def asana_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_asana_config(uid, access_token=data.get("access_token", "")) return jsonify({"status": "saved"}) @app.route("/connectors/asana/connect") def asana_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_asana(uid)) @app.route("/connectors/asana/disconnect") def asana_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_asana(uid)) @app.route("/connectors/asana/sync") def asana_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_asana(uid, sync_type)) @app.route("/api/status/asana") def status_asana(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='asana' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='asana' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/asana/job/get") def asana_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='asana' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/asana/job/save", methods=["POST"]) def asana_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'asana', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # SENDGRID @app.route("/connectors/sendgrid/save_app", methods=["POST"]) def sendgrid_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_sendgrid_config(uid, api_key=data.get("api_key", "")) return jsonify({"status": "saved"}) @app.route("/connectors/sendgrid/connect") def sendgrid_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_sendgrid(uid)) @app.route("/connectors/sendgrid/disconnect") def sendgrid_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_sendgrid(uid)) @app.route("/connectors/sendgrid/sync") def sendgrid_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_sendgrid(uid, sync_type)) @app.route("/api/status/sendgrid") def status_sendgrid(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='sendgrid' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='sendgrid' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/sendgrid/job/get") def sendgrid_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='sendgrid' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/sendgrid/job/save", methods=["POST"]) def sendgrid_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'sendgrid', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # TABLEAU @app.route("/connectors/tableau/save_app", methods=["POST"]) def tableau_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 save_tableau_config(uid, request.json or {}) ensure_connector_initialized(uid, "tableau") return jsonify({"status": "saved"}) @app.route("/connectors/tableau/connect") def tableau_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_tableau(uid)) @app.route("/connectors/tableau/disconnect") def tableau_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_tableau(uid)) @app.route("/connectors/tableau/sync") def tableau_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_tableau(uid, sync_type)) @app.route("/api/status/tableau") def status_tableau(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='tableau' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='tableau' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/tableau/job/get") def tableau_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='tableau' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/tableau/job/save", methods=["POST"]) def tableau_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json or {} con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'tableau', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.datetime.utcnow().isoformat()), ) con.commit() con.close() return jsonify({"status": "saved"}) # POWER BI @app.route("/connectors/power_bi/save_app", methods=["POST"]) def power_bi_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 save_power_bi_config(uid, request.json or {}) ensure_connector_initialized(uid, "power_bi") return jsonify({"status": "saved"}) @app.route("/connectors/power_bi/connect") def power_bi_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_power_bi(uid)) @app.route("/connectors/power_bi/disconnect") def power_bi_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_power_bi(uid)) @app.route("/connectors/power_bi/sync") def power_bi_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_power_bi(uid, sync_type)) @app.route("/api/status/power_bi") def status_power_bi(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='power_bi' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='power_bi' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/power_bi/job/get") def power_bi_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='power_bi' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/power_bi/job/save", methods=["POST"]) def power_bi_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json or {} con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'power_bi', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.datetime.utcnow().isoformat()), ) con.commit() con.close() return jsonify({"status": "saved"}) # WORKDAY @app.route("/connectors/workday/save_app", methods=["POST"]) def workday_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 save_workday_config(uid, request.json or {}) ensure_connector_initialized(uid, "workday") return jsonify({"status": "saved"}) @app.route("/connectors/workday/connect") def workday_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_workday(uid)) @app.route("/connectors/workday/disconnect") def workday_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_workday(uid)) @app.route("/connectors/workday/sync") def workday_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_workday(uid, sync_type)) @app.route("/api/status/workday") def status_workday(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='workday' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='workday' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/workday/job/get") def workday_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='workday' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/workday/job/save", methods=["POST"]) def workday_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json or {} con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'workday', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.datetime.utcnow().isoformat()), ) con.commit() con.close() return jsonify({"status": "saved"}) # EBAY @app.route("/connectors/ebay/save_app", methods=["POST"]) def ebay_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 save_ebay_config(uid, request.json or {}) ensure_connector_initialized(uid, "ebay") return jsonify({"status": "saved"}) @app.route("/connectors/ebay/connect") def ebay_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_ebay(uid)) @app.route("/connectors/ebay/disconnect") def ebay_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_ebay(uid)) @app.route("/connectors/ebay/sync") def ebay_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_ebay(uid, sync_type)) @app.route("/api/status/ebay") def status_ebay(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='ebay' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='ebay' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/ebay/job/get") def ebay_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='ebay' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/ebay/job/save", methods=["POST"]) def ebay_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json or {} con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'ebay', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.datetime.utcnow().isoformat()), ) con.commit() con.close() return jsonify({"status": "saved"}) # MIXPANEL @app.route("/connectors/mixpanel/save_app", methods=["POST"]) def mixpanel_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_mixpanel_config(uid, api_secret=data.get("api_secret", "")) return jsonify({"status": "saved"}) @app.route("/connectors/mixpanel/connect") def mixpanel_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_mixpanel(uid)) @app.route("/connectors/mixpanel/disconnect") def mixpanel_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_mixpanel(uid)) @app.route("/connectors/mixpanel/sync") def mixpanel_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_mixpanel(uid, sync_type)) @app.route("/api/status/mixpanel") def status_mixpanel(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='mixpanel' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='mixpanel' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/mixpanel/job/get") def mixpanel_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='mixpanel' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/mixpanel/job/save", methods=["POST"]) def mixpanel_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'mixpanel', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # MONDAY.COM @app.route("/connectors/monday/save_app", methods=["POST"]) def monday_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_monday_config(uid, api_token=data.get("api_token", "")) return jsonify({"status": "saved"}) @app.route("/connectors/monday/connect") def monday_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_monday(uid)) @app.route("/connectors/monday/disconnect") def monday_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_monday(uid)) @app.route("/connectors/monday/sync") def monday_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_monday(uid, sync_type)) @app.route("/api/status/monday") def status_monday(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='monday' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='monday' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/monday/job/get") def monday_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='monday' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/monday/job/save", methods=["POST"]) def monday_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'monday', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # CLICKUP @app.route("/connectors/clickup/save_app", methods=["POST"]) def clickup_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_clickup_config(uid, api_token=data.get("api_token", "")) return jsonify({"status": "saved"}) @app.route("/connectors/clickup/connect") def clickup_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_clickup(uid)) @app.route("/connectors/clickup/disconnect") def clickup_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_clickup(uid)) @app.route("/connectors/clickup/sync") def clickup_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_clickup(uid, sync_type)) @app.route("/api/status/clickup") def status_clickup(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='clickup' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='clickup' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/clickup/job/get") def clickup_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='clickup' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/clickup/job/save", methods=["POST"]) def clickup_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'clickup', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # HELPSCOUT @app.route("/connectors/helpscout/save_app", methods=["POST"]) def helpscout_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_helpscout_config(uid, api_key=data.get("api_key", "")) return jsonify({"status": "saved"}) @app.route("/connectors/helpscout/connect") def helpscout_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_helpscout(uid)) @app.route("/connectors/helpscout/disconnect") def helpscout_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_helpscout(uid)) @app.route("/connectors/helpscout/sync") def helpscout_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_helpscout(uid, sync_type)) @app.route("/api/status/helpscout") def status_helpscout(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='helpscout' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='helpscout' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/helpscout/job/get") def helpscout_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='helpscout' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/helpscout/job/save", methods=["POST"]) def helpscout_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'helpscout', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # ================= LOOKER ================= @app.route("/connectors/looker/connect") def looker_connect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(connect_looker(uid)) @app.route("/connectors/looker/disconnect") def looker_disconnect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(disconnect_looker(uid)) @app.route("/connectors/looker/sync") def looker_sync_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_looker(uid, sync_type)) @app.route("/connectors/looker/save_app", methods=["POST"]) def looker_save_app_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 save_looker_config(uid, request.json) return jsonify({"status":"success"}) # ================= SUPERSET ================= @app.route("/connectors/superset/connect") def superset_connect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(connect_superset(uid)) @app.route("/connectors/superset/disconnect") def superset_disconnect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(disconnect_superset(uid)) @app.route("/connectors/superset/sync") def superset_sync_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_superset(uid, sync_type)) @app.route("/connectors/superset/save_app", methods=["POST"]) def superset_save_app_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 save_superset_config(uid, request.json) return jsonify({"status":"success"}) # ================= AZURE_BLOB ================= @app.route("/connectors/azure_blob/connect") def azure_blob_connect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(connect_azure_blob(uid)) @app.route("/connectors/azure_blob/disconnect") def azure_blob_disconnect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(disconnect_azure_blob(uid)) @app.route("/connectors/azure_blob/sync") def azure_blob_sync_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_azure_blob(uid, sync_type)) @app.route("/connectors/azure_blob/save_app", methods=["POST"]) def azure_blob_save_app_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 save_azure_blob_config(uid, request.json) return jsonify({"status":"success"}) # ================= DATADOG ================= @app.route("/connectors/datadog/connect") def datadog_connect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(connect_datadog(uid)) @app.route("/connectors/datadog/disconnect") def datadog_disconnect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(disconnect_datadog(uid)) @app.route("/connectors/datadog/sync") def datadog_sync_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_datadog(uid, sync_type)) @app.route("/connectors/datadog/save_app", methods=["POST"]) def datadog_save_app_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 save_datadog_config(uid, request.json) return jsonify({"status":"success"}) # ================= OKTA ================= @app.route("/connectors/okta/connect") def okta_connect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(connect_okta(uid)) @app.route("/connectors/okta/disconnect") def okta_disconnect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(disconnect_okta(uid)) @app.route("/connectors/okta/sync") def okta_sync_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_okta(uid, sync_type)) @app.route("/connectors/okta/save_app", methods=["POST"]) def okta_save_app_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 save_okta_config(uid, request.json) return jsonify({"status":"success"}) @app.route("/api/status/okta") def okta_status_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT status, config_json FROM connector_configs WHERE uid=? AND connector='okta' LIMIT 1", (uid,)) row = cur.fetchone() con.close() if not row: return jsonify({"connected": False, "has_credentials": False}) status, config_json = row return jsonify({"connected": status == "connected", "has_credentials": bool(config_json), "status": status}) # ================= AUTH0 ================= @app.route("/connectors/auth0/connect") def auth0_connect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(connect_auth0(uid)) @app.route("/connectors/auth0/disconnect") def auth0_disconnect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(disconnect_auth0(uid)) @app.route("/connectors/auth0/sync") def auth0_sync_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_auth0(uid, sync_type)) @app.route("/connectors/auth0/save_app", methods=["POST"]) def auth0_save_app_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 save_auth0_config(uid, request.json) return jsonify({"status":"success"}) @app.route("/api/status/auth0") def auth0_status_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT status, config_json FROM connector_configs WHERE uid=? AND connector='auth0' LIMIT 1", (uid,)) row = cur.fetchone() con.close() if not row: return jsonify({"connected": False, "has_credentials": False}) status, config_json = row return jsonify({"connected": status == "connected", "has_credentials": bool(config_json), "status": status}) # ================= CLOUDFLARE ================= @app.route("/connectors/cloudflare/connect") def cloudflare_connect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(connect_cloudflare(uid)) @app.route("/connectors/cloudflare/disconnect") def cloudflare_disconnect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(disconnect_cloudflare(uid)) @app.route("/connectors/cloudflare/sync") def cloudflare_sync_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_cloudflare(uid, sync_type)) @app.route("/connectors/cloudflare/save_app", methods=["POST"]) def cloudflare_save_app_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 save_cloudflare_config(uid, request.json) return jsonify({"status":"success"}) @app.route("/api/status/cloudflare") def cloudflare_status_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT status, config_json FROM connector_configs WHERE uid=? AND connector='cloudflare' LIMIT 1", (uid,)) row = cur.fetchone() con.close() if not row: return jsonify({"connected": False, "has_credentials": False}) status, config_json = row return jsonify({"connected": status == "connected", "has_credentials": bool(config_json), "status": status}) # ================= SENTRY ================= @app.route("/connectors/sentry/connect") def sentry_connect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(connect_sentry(uid)) @app.route("/connectors/sentry/disconnect") def sentry_disconnect_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 return jsonify(disconnect_sentry(uid)) @app.route("/connectors/sentry/sync") def sentry_sync_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_sentry(uid, sync_type)) @app.route("/connectors/sentry/save_app", methods=["POST"]) def sentry_save_app_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 save_sentry_config(uid, request.json) return jsonify({"status":"success"}) @app.route("/api/status/sentry") def sentry_status_api(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT status, config_json FROM connector_configs WHERE uid=? AND connector='sentry' LIMIT 1", (uid,)) row = cur.fetchone() con.close() if not row: return jsonify({"connected": False, "has_credentials": False}) status, config_json = row return jsonify({"connected": status == "connected", "has_credentials": bool(config_json), "status": status}) # ---------------- QUICKBOOKS ---------------- @app.route("/connectors/quickbooks/save_app", methods=["POST"]) def qb_save(): data = request.json return jsonify(quickbooks.save_app_quickbooks(data.get("client_id"), data.get("client_secret"))) @app.route("/connectors/quickbooks/connect") def qb_connect(): return quickbooks.connect_quickbooks(uid=uid, redirect_uri=get_base_url() + "/oauth/callback") @app.route("/connectors/quickbooks/callback") def qb_callback(): return quickbooks.callback_quickbooks() @app.route("/connectors/quickbooks/sync") def qb_sync(): return jsonify(quickbooks.sync_quickbooks()) @app.route("/connectors/quickbooks/disconnect") def qb_disconnect(): return jsonify(quickbooks.disconnect_quickbooks()) @app.route("/api/status/quickbooks") def qb_status(): con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM quickbooks_config LIMIT 1") has_creds = bool(cur.fetchone()) cur.execute("SELECT 1 FROM quickbooks_auth LIMIT 1") connected = bool(cur.fetchone()) con.close() return jsonify({"has_credentials": has_creds, "connected": connected}) # ---------------- XERO ---------------- @app.route("/connectors/xero/save_app", methods=["POST"]) def xero_save(): data = request.json return jsonify(xero.save_app_xero(data.get("client_id"), data.get("client_secret"))) @app.route("/connectors/xero/connect") def xero_connect(): return xero.connect_xero(uid=uid, redirect_uri=get_base_url() + "/oauth/callback") @app.route("/connectors/xero/callback") def xero_callback(): return xero.callback_xero() @app.route("/connectors/xero/sync") def xero_sync(): return jsonify(xero.sync_xero()) @app.route("/connectors/xero/disconnect") def xero_disconnect(): return jsonify(xero.disconnect_xero()) @app.route("/api/status/xero") def xero_status_route(): con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM xero_config LIMIT 1") has_creds = bool(cur.fetchone()) cur.execute("SELECT tenant_name FROM xero_auth LIMIT 1") row = cur.fetchone() con.close() return jsonify({"has_credentials": has_creds, "connected": bool(row), "tenant_name": row[0] if row else None}) @app.route("/api/status/google_gmail") @app.route("/api/status/gmail") def gmail_status(): uid = get_uid() if not uid: return jsonify({"error":"unauthorized"}), 401 con = get_db() cur = con.cursor() # Check if we have an account for this user and source with BOTH tokens cur.execute(""" SELECT 1 FROM google_accounts WHERE uid=? AND source='gmail' AND access_token IS NOT NULL AND access_token != '' AND refresh_token IS NOT NULL AND refresh_token != '' LIMIT 1 """, (uid,)) row = cur.fetchone() con.close() return jsonify({"connected": bool(row)}) # ---------------- AMAZON SELLER ---------------- @app.route("/connectors/amazon_seller/save_app", methods=["POST"]) def amz_save(): data = request.json return jsonify(amazon_seller.save_app_amazon_seller(data.get("client_id"), data.get("client_secret"), data.get("seller_id"), data.get("region"))) @app.route("/connectors/amazon_seller/connect") def amz_connect(): return amazon_seller.connect_amazon_seller(uid=uid, redirect_uri=get_base_url() + "/oauth/callback") @app.route("/connectors/amazon_seller/callback") def amz_callback(): return amazon_seller.callback_amazon_seller() @app.route("/connectors/amazon_seller/sync") def amz_sync(): return jsonify(amazon_seller.sync_amazon_seller()) @app.route("/connectors/amazon_seller/disconnect") def amz_disconnect(): return jsonify(amazon_seller.disconnect_amazon_seller()) @app.route("/api/status/amazon_seller") def amz_status(): con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM amazon_seller_config LIMIT 1") has_creds = bool(cur.fetchone()) cur.execute("SELECT 1 FROM amazon_seller_auth LIMIT 1") connected = bool(cur.fetchone()) con.close() return jsonify({"has_credentials": has_creds, "connected": connected}) # ---------------- NEW RELIC ---------------- @app.route("/connectors/newrelic/save_app", methods=["POST"]) def nr_save(): data = request.json return jsonify(newrelic.save_app_newrelic(data.get("api_key"), data.get("account_id"), data.get("region"))) @app.route("/connectors/newrelic/sync") def nr_sync(): return jsonify(newrelic.sync_newrelic()) @app.route("/connectors/newrelic/disconnect") def nr_disconnect(): return jsonify(newrelic.disconnect_newrelic()) @app.route("/api/status/newrelic") def nr_status(): con = get_db() cur = con.cursor() cur.execute("SELECT 1 FROM newrelic_auth LIMIT 1") connected = bool(cur.fetchone()) con.close() return jsonify({"has_credentials": connected, "connected": connected}) # OPENAI @app.route("/connectors/openai/save_app", methods=["POST"]) def openai_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_openai_config(uid, api_key=data.get("api_key", "")) return jsonify({"status": "saved"}) @app.route("/connectors/openai/connect") def openai_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_openai(uid)) @app.route("/connectors/openai/disconnect") def openai_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_openai(uid)) @app.route("/connectors/openai/sync") def openai_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_openai(uid, sync_type)) @app.route("/api/status/openai") def status_openai(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='openai' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='openai' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/openai/job/get") def openai_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='openai' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/openai/job/save", methods=["POST"]) def openai_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'openai', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # HUGGINGFACE @app.route("/connectors/huggingface/save_app", methods=["POST"]) def huggingface_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_huggingface_config(uid, access_token=data.get("access_token", "")) return jsonify({"status": "saved"}) @app.route("/connectors/huggingface/connect") def huggingface_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_huggingface(uid)) @app.route("/connectors/huggingface/disconnect") def huggingface_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_huggingface(uid)) @app.route("/connectors/huggingface/sync") def huggingface_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_huggingface(uid, sync_type)) @app.route("/api/status/huggingface") def status_huggingface(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='huggingface' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='huggingface' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/huggingface/job/get") def huggingface_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='huggingface' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/huggingface/job/save", methods=["POST"]) def huggingface_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'huggingface', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # AIRFLOW @app.route("/connectors/airflow/save_app", methods=["POST"]) def airflow_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_airflow_config(uid, base_url=data.get("base_url", ""), username=data.get("username", ""), password=data.get("password", "")) return jsonify({"status": "saved"}) @app.route("/connectors/airflow/connect") def airflow_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_airflow(uid)) @app.route("/connectors/airflow/disconnect") def airflow_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_airflow(uid)) @app.route("/connectors/airflow/sync") def airflow_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_airflow(uid, sync_type)) @app.route("/api/status/airflow") def status_airflow(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='airflow' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='airflow' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/airflow/job/get") def airflow_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='airflow' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/airflow/job/save", methods=["POST"]) def airflow_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'airflow', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # KAFKA @app.route("/connectors/kafka/save_app", methods=["POST"]) def kafka_save_app(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json save_kafka_config(uid, bootstrap_servers=data.get("bootstrap_servers", "")) return jsonify({"status": "saved"}) @app.route("/connectors/kafka/connect") def kafka_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(connect_kafka(uid)) @app.route("/connectors/kafka/disconnect") def kafka_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_kafka(uid)) @app.route("/connectors/kafka/sync") def kafka_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 sync_type = request.args.get("type", "incremental") return jsonify(sync_kafka(uid, sync_type)) @app.route("/api/status/kafka") def status_kafka(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT config_json FROM connector_configs WHERE uid=? AND connector='kafka' LIMIT 1", (uid,)) cfg_row = fetchone_secure(cur) cur.execute("SELECT enabled FROM google_connections WHERE uid=? AND source='kafka' LIMIT 1", (uid,)) conn_row = fetchone_secure(cur) con.close() return jsonify({"has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1)}) @app.route("/connectors/kafka/job/get") def kafka_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute("SELECT schedule_time, sync_type FROM connector_jobs WHERE uid=? AND source='kafka' LIMIT 1", (uid,)) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "schedule_time": row.get("schedule_time"), "sync_type": row.get("sync_type", "incremental")}) @app.route("/connectors/kafka/job/save", methods=["POST"]) def kafka_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.json con = get_db() cur = con.cursor() cur.execute("INSERT OR REPLACE INTO connector_jobs (uid, source, schedule_time, sync_type, updated_at) VALUES (?, 'kafka', ?, ?, ?)", (uid, data.get("schedule_time"), data.get("sync_type", "incremental"), datetime.now().isoformat())) con.commit() con.close() return jsonify({"status": "saved"}) # ================= DBT ======================== @app.route("/connectors/dbt/save_app", methods=["POST"]) def _dbt_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_token = (data.get("api_token") or "").strip() account_id = (data.get("account_id") or "").strip() if not api_token or not account_id: return jsonify({"error": "missing api_token or account_id"}), 400 save_dbt_config(uid, api_token, account_id) ensure_connector_initialized(uid, "dbt") return jsonify({"status": "saved"}) @app.route("/connectors/dbt/connect") def _dbt_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_dbt(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/dbt/disconnect") def _dbt_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_dbt(uid)) @app.route("/connectors/dbt/sync") def _dbt_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type FROM connector_jobs WHERE uid=? AND source='dbt' LIMIT 1", (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_dbt(uid, sync_type=sync_type)) @app.route("/api/status/dbt") def _dbt_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT config_json FROM connector_configs WHERE uid=? AND connector='dbt' LIMIT 1", (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( "SELECT enabled FROM google_connections WHERE uid=? AND source='dbt' LIMIT 1", (uid,), ) conn_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), }) @app.route("/connectors/dbt/job/get") def _dbt_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='dbt'", (uid,), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/dbt/job/save", methods=["POST"]) def _dbt_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'dbt', ?, ?)", (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= TYPEFORM ======================== @app.route("/connectors/typeform/save_app", methods=["POST"]) def _typeform_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} access_token = (data.get("access_token") or "").strip() if not access_token: return jsonify({"error": "missing access_token"}), 400 save_typeform_config(uid, access_token) ensure_connector_initialized(uid, "typeform") return jsonify({"status": "saved"}) @app.route("/connectors/typeform/connect") def _typeform_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_typeform(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/typeform/disconnect") def _typeform_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_typeform(uid)) @app.route("/connectors/typeform/sync") def _typeform_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type FROM connector_jobs WHERE uid=? AND source='typeform' LIMIT 1", (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_typeform(uid, sync_type=sync_type)) @app.route("/api/status/typeform") def _typeform_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT config_json FROM connector_configs WHERE uid=? AND connector='typeform' LIMIT 1", (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( "SELECT enabled FROM google_connections WHERE uid=? AND source='typeform' LIMIT 1", (uid,), ) conn_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), }) @app.route("/connectors/typeform/job/get") def _typeform_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='typeform'", (uid,), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/typeform/job/save", methods=["POST"]) def _typeform_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'typeform', ?, ?)", (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= SURVEYMONKEY ======================== @app.route("/connectors/surveymonkey/save_app", methods=["POST"]) def _surveymonkey_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} access_token = (data.get("access_token") or "").strip() if not access_token: return jsonify({"error": "missing access_token"}), 400 save_surveymonkey_config(uid, access_token) ensure_connector_initialized(uid, "surveymonkey") return jsonify({"status": "saved"}) @app.route("/connectors/surveymonkey/connect") def _surveymonkey_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_surveymonkey(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/surveymonkey/disconnect") def _surveymonkey_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_surveymonkey(uid)) @app.route("/connectors/surveymonkey/sync") def _surveymonkey_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type FROM connector_jobs WHERE uid=? AND source='surveymonkey' LIMIT 1", (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_surveymonkey(uid, sync_type=sync_type)) @app.route("/api/status/surveymonkey") def _surveymonkey_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT config_json FROM connector_configs WHERE uid=? AND connector='surveymonkey' LIMIT 1", (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( "SELECT enabled FROM google_connections WHERE uid=? AND source='surveymonkey' LIMIT 1", (uid,), ) conn_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), }) @app.route("/connectors/surveymonkey/job/get") def _surveymonkey_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='surveymonkey'", (uid,), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/surveymonkey/job/save", methods=["POST"]) def _surveymonkey_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'surveymonkey', ?, ?)", (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= PINECONE ======================== @app.route("/connectors/pinecone/save_app", methods=["POST"]) def _pinecone_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_key = (data.get("api_key") or "").strip() environment = (data.get("environment") or "").strip() if not api_key or not environment: return jsonify({"error": "missing api_key or environment"}), 400 save_pinecone_config(uid, api_key, environment) ensure_connector_initialized(uid, "pinecone") return jsonify({"status": "saved"}) @app.route("/connectors/pinecone/connect") def _pinecone_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_pinecone(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/pinecone/disconnect") def _pinecone_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_pinecone(uid)) @app.route("/connectors/pinecone/sync") def _pinecone_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type FROM connector_jobs WHERE uid=? AND source='pinecone' LIMIT 1", (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_pinecone(uid, sync_type=sync_type)) @app.route("/api/status/pinecone") def _pinecone_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT config_json FROM connector_configs WHERE uid=? AND connector='pinecone' LIMIT 1", (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( "SELECT enabled FROM google_connections WHERE uid=? AND source='pinecone' LIMIT 1", (uid,), ) conn_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), }) @app.route("/connectors/pinecone/job/get") def _pinecone_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='pinecone'", (uid,), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/pinecone/job/save", methods=["POST"]) def _pinecone_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'pinecone', ?, ?)", (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= BITBUCKET ======================== @app.route("/connectors/bitbucket/save_app", methods=["POST"]) def _bitbucket_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} username = (data.get("username") or "").strip() api_token = (data.get("api_token") or "").strip() if not username or not api_token: return jsonify({"error": "missing username or api_token"}), 400 save_bitbucket_config(uid, username, api_token) ensure_connector_initialized(uid, "bitbucket") return jsonify({"status": "saved"}) @app.route("/connectors/bitbucket/connect") def _bitbucket_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_bitbucket(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/bitbucket/disconnect") def _bitbucket_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_bitbucket(uid)) @app.route("/connectors/bitbucket/sync") def _bitbucket_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type FROM connector_jobs WHERE uid=? AND source='bitbucket' LIMIT 1", (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_bitbucket(uid, sync_type=sync_type)) @app.route("/api/status/bitbucket") def _bitbucket_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT config_json FROM connector_configs WHERE uid=? AND connector='bitbucket' LIMIT 1", (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( "SELECT enabled FROM google_connections WHERE uid=? AND source='bitbucket' LIMIT 1", (uid,), ) conn_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), }) @app.route("/connectors/bitbucket/job/get") def _bitbucket_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='bitbucket'", (uid,), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({"exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"]}) @app.route("/connectors/bitbucket/job/save", methods=["POST"]) def _bitbucket_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'bitbucket', ?, ?)", (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= VERCEL ======================== @app.route("/connectors/vercel/save_app", methods=["POST"]) def _vercel_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_key = (data.get("api_key") or "").strip() if not api_key: return jsonify({"error": "missing api_key"}), 400 save_vercel_config(uid, api_key) ensure_connector_initialized(uid, "vercel") return jsonify({"status": "saved"}) @app.route("/connectors/vercel/connect") def _vercel_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_vercel(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/vercel/disconnect") def _vercel_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_vercel(uid)) @app.route("/connectors/vercel/sync") def _vercel_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type FROM connector_jobs WHERE uid=? AND source='vercel' LIMIT 1", (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_vercel(uid, sync_type=sync_type)) @app.route("/api/status/vercel") def _vercel_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT config_json FROM connector_configs WHERE uid=? AND connector='vercel' LIMIT 1", (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( "SELECT enabled FROM google_connections WHERE uid=? AND source='vercel' LIMIT 1", (uid,), ) conn_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), }) @app.route("/connectors/vercel/job/get") def _vercel_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='vercel'", (uid,), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/vercel/job/save", methods=["POST"]) def _vercel_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'vercel', ?, ?)", (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= NETLIFY ======================== @app.route("/connectors/netlify/save_app", methods=["POST"]) def _netlify_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_key = (data.get("api_key") or "").strip() if not api_key: return jsonify({"error": "missing api_key"}), 400 save_netlify_config(uid, api_key) ensure_connector_initialized(uid, "netlify") return jsonify({"status": "saved"}) @app.route("/connectors/netlify/connect") def _netlify_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_netlify(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/netlify/disconnect") def _netlify_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_netlify(uid)) @app.route("/connectors/netlify/sync") def _netlify_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type FROM connector_jobs WHERE uid=? AND source='netlify' LIMIT 1", (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_netlify(uid, sync_type=sync_type)) @app.route("/api/status/netlify") def _netlify_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT config_json FROM connector_configs WHERE uid=? AND connector='netlify' LIMIT 1", (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( "SELECT enabled FROM google_connections WHERE uid=? AND source='netlify' LIMIT 1", (uid,), ) conn_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), }) @app.route("/connectors/netlify/job/get") def _netlify_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='netlify'", (uid,), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/netlify/job/save", methods=["POST"]) def _netlify_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'netlify', ?, ?)", (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= LINEAR ======================== @app.route("/connectors/linear/save_app", methods=["POST"]) def _linear_save_config(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} api_key = (data.get("api_key") or "").strip() if not api_key: return jsonify({"error": "missing api_key"}), 400 save_linear_config(uid, api_key) ensure_connector_initialized(uid, "linear") return jsonify({"status": "saved"}) @app.route("/connectors/linear/connect") def _linear_connect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 res = connect_linear(uid) if res.get("status") != "success": return jsonify(res), 400 return jsonify(res) @app.route("/connectors/linear/disconnect") def _linear_disconnect(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 return jsonify(disconnect_linear(uid)) @app.route("/connectors/linear/sync") def _linear_sync(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type FROM connector_jobs WHERE uid=? AND source='linear' LIMIT 1", (uid,), ) row = fetchone_secure(cur) con.close() sync_type = row["sync_type"] if row and row.get("sync_type") else "historical" return jsonify(sync_linear(uid, sync_type=sync_type)) @app.route("/api/status/linear") def _linear_status(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT config_json FROM connector_configs WHERE uid=? AND connector='linear' LIMIT 1", (uid,), ) cfg_row = fetchone_secure(cur) cur.execute( "SELECT enabled FROM google_connections WHERE uid=? AND source='linear' LIMIT 1", (uid,), ) conn_row = fetchone_secure(cur) con.close() return jsonify({ "has_credentials": bool(cfg_row), "connected": bool(conn_row and conn_row.get("enabled") == 1), }) @app.route("/connectors/linear/job/get") def _linear_job_get(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 con = get_db() cur = con.cursor() cur.execute( "SELECT sync_type, schedule_time FROM connector_jobs WHERE uid=? AND source='linear'", (uid,), ) row = fetchone_secure(cur) con.close() if not row: return jsonify({"exists": False}) return jsonify({ "exists": True, "sync_type": row["sync_type"], "schedule_time": row["schedule_time"] }) @app.route("/connectors/linear/job/save", methods=["POST"]) def _linear_job_save(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json() or {} sync_type = data.get("sync_type", "incremental") schedule_time = data.get("schedule_time") con = get_db() cur = con.cursor() cur.execute( "INSERT OR REPLACE INTO connector_jobs (uid, source, sync_type, schedule_time) VALUES (?, 'linear', ?, ?)", (uid, sync_type, schedule_time), ) con.commit() con.close() return jsonify({"status": "job_saved"}) # ================= AI COMPANION HELPERS ================= def create_chat(user_id): chat_id = "chat_" + secrets.token_hex(8) title = "New Conversation" created_at = datetime.datetime.utcnow().isoformat() con = get_db() cur = con.cursor() cur.execute("INSERT INTO ai_chats (id, user_id, title, created_at) VALUES (?, ?, ?, ?)", (chat_id, user_id, title, created_at)) con.commit() con.close() return {"id": chat_id, "user_id": user_id, "title": title, "created_at": created_at} def save_message(chat_id, role, content): msg_id = "msg_" + secrets.token_hex(8) created_at = datetime.datetime.utcnow().isoformat() con = get_db() cur = con.cursor() cur.execute("INSERT INTO ai_messages (id, chat_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)", (msg_id, chat_id, role, content, created_at)) con.commit() con.close() return {"id": msg_id, "chat_id": chat_id, "role": role, "content": content, "created_at": created_at} def get_chats(user_id): con = get_db() con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT * FROM ai_chats WHERE user_id=? ORDER BY created_at DESC", (user_id,)) rows = cur.fetchall() con.close() return [dict(r) for r in rows] def get_messages(chat_id): con = get_db() con.row_factory = sqlite3.Row cur = con.cursor() cur.execute("SELECT * FROM ai_messages WHERE chat_id=? ORDER BY created_at ASC", (chat_id,)) rows = cur.fetchall() con.close() return [dict(r) for r in rows] # ================= AI COMPANION ROUTES ================= @app.route("/ai/chats", methods=["GET"]) def ai_chats(): uid = get_uid() if not uid: return jsonify({"chats": []}), 200 chats = get_chats(uid) return jsonify({"chats": chats}), 200 @app.route("/ai/chat/", methods=["GET"]) def ai_chat_history(chat_id): uid = get_uid() if not uid: return jsonify({"messages": []}), 200 con = get_db() cur = con.cursor() cur.execute("SELECT user_id FROM ai_chats WHERE id=?", (chat_id,)) row = cur.fetchone() con.close() if not row or row[0] != uid: return jsonify({"error": "unauthorized"}), 403 messages = get_messages(chat_id) return jsonify({"messages": messages}), 200 def get_ai_state(chat_id): con = get_db() cur = con.cursor() try: cur.execute("SELECT state_json FROM ai_state WHERE chat_id=?", (chat_id,)) row = cur.fetchone() con.close() if not row or not row[0]: return None return json.loads(row[0]) except: con.close() return None def save_ai_state(chat_id, uid, state): con = get_db() cur = con.cursor() if not state: cur.execute("DELETE FROM ai_state WHERE chat_id=?", (chat_id,)) else: cur.execute(""" INSERT OR REPLACE INTO ai_state (chat_id, uid, state_json, updated_at) VALUES (?, ?, ?, ?) """, ( chat_id, uid, json.dumps(state), datetime.datetime.utcnow().isoformat() )) con.commit() con.close() @app.route("/ai/chat", methods=["POST"]) def ai_chat_message(): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 data = request.get_json(silent=True) or {} message = (data.get("message") or "").strip() chat_id = data.get("chat_id") if not message: return jsonify({"error": "message required"}), 400 # ── Resolve / create chat ────────────────────────────────── if not chat_id: chat_res = create_chat(uid) chat_id = chat_res["id"] title = message[:40] + ("..." if len(message) > 40 else "") con = get_db() cur = con.cursor() cur.execute("UPDATE ai_chats SET title=? WHERE id=?", (title, chat_id)) con.commit() con.close() else: con = get_db() cur = con.cursor() cur.execute("SELECT user_id FROM ai_chats WHERE id=?", (chat_id,)) row = cur.fetchone() con.close() if not row or row[0] != uid: return jsonify({"error": "unauthorized"}), 403 # ── Persist user message ─────────────────────────────────── save_message(chat_id, "user", message) # ── Orchestration ────────────────────────────────────────── try: # Load state state = data.get("state") or get_ai_state(chat_id) intent = resolve_intent(message) print(f"[AI] Intent -> {intent}", flush=True) result = orchestrate(intent, uid, chat_id, message, state) # Save state save_ai_state(chat_id, uid, result.get("state")) except Exception as exc: print(f"[AI] Error in orchestration: {exc}", flush=True) import traceback traceback.print_exc() result = { "type": "error", "message": "Something went wrong while processing your request. Please try again.", "connectors": [], "links": [], "data": None, } # ── Persist AI reply ────────────────────────────────────── save_message(chat_id, "ai", result["message"]) # ── Return result ── return jsonify({ "message": result["message"], "chat_id": chat_id, "type": result["type"], "connectors": result.get("connectors", []), "links": result.get("links", []), "data": result.get("data"), "state": result.get("state") }), 200 # ---------------- SYNC RECOVERY ROUTE ---------------- @app.route("/connectors//recover", methods=["POST"]) def recover_connector_data(source): uid = get_uid() if not uid: return jsonify({"error": "unauthorized"}), 401 try: from backend.utils.sync_storage import get_recent_sync_data recent_data = get_recent_sync_data(uid, source) if not recent_data: return jsonify({"error": "No recent valid data found to recover"}), 404 dest_cfg = get_active_destination(uid, source) if not dest_cfg: return jsonify({"error": "No active destination configured"}), 400 dest_cfg["type"] = dest_cfg.get("dest_type") total_pushed = 0 batches = 0 from backend.destinations.destination_router import push_to_destination for stored_data in recent_data: count = push_to_destination(dest_cfg, source, stored_data, skip_storage=True) total_pushed += count batches += 1 return jsonify({ "message": "Recovery successful", "total_rows": total_pushed, "batches": batches }), 200 except Exception as e: print("[RECOVER ERROR]", e, flush=True) return jsonify({"error": str(e)}), 500 init_db() seed_test_user() if __name__=="__main__": app.run(port=4000,debug=True,host="0.0.0.0",use_reloader=False)