Onboarding Manager and Router Manager refactored, analytics and background jobs added, database setup updated, environment setup updated, frontend updated, backend updated. Critical onboarding database migration implemented.
747 lines
32 KiB
Python
747 lines
32 KiB
Python
"""
|
|
Bing Webmaster OAuth2 Service
|
|
Handles Bing Webmaster Tools OAuth2 authentication flow for SEO analytics access.
|
|
"""
|
|
|
|
import os
|
|
import secrets
|
|
import sqlite3
|
|
import requests
|
|
from typing import Optional, Dict, Any, List
|
|
from datetime import datetime, timedelta
|
|
from loguru import logger
|
|
import json
|
|
from urllib.parse import quote
|
|
from ..analytics_cache_service import analytics_cache
|
|
|
|
class BingOAuthService:
|
|
"""Manages Bing Webmaster Tools OAuth2 authentication flow."""
|
|
|
|
def __init__(self, db_path: str = "alwrity.db"):
|
|
self.db_path = db_path
|
|
# Bing Webmaster OAuth2 credentials
|
|
self.client_id = os.getenv('BING_CLIENT_ID', '')
|
|
self.client_secret = os.getenv('BING_CLIENT_SECRET', '')
|
|
self.redirect_uri = os.getenv('BING_REDIRECT_URI', 'https://littery-sonny-unscrutinisingly.ngrok-free.dev/bing/callback')
|
|
self.base_url = "https://www.bing.com"
|
|
self.api_base_url = "https://www.bing.com/webmaster/api.svc/json"
|
|
|
|
# Validate configuration
|
|
if not self.client_id or not self.client_secret or self.client_id == 'your_bing_client_id_here':
|
|
logger.error("Bing Webmaster OAuth client credentials not configured. Please set BING_CLIENT_ID and BING_CLIENT_SECRET environment variables with valid Bing Webmaster application credentials.")
|
|
logger.error("To get credentials: 1. Go to https://www.bing.com/webmasters/ 2. Sign in to Bing Webmaster Tools 3. Go to Settings > API Access 4. Create OAuth client")
|
|
|
|
self._init_db()
|
|
|
|
def _init_db(self):
|
|
"""Initialize database tables for OAuth tokens."""
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS bing_oauth_tokens (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id TEXT NOT NULL,
|
|
access_token TEXT NOT NULL,
|
|
refresh_token TEXT,
|
|
token_type TEXT DEFAULT 'bearer',
|
|
expires_at TIMESTAMP,
|
|
scope TEXT,
|
|
site_url TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
is_active BOOLEAN DEFAULT TRUE
|
|
)
|
|
''')
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS bing_oauth_states (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
state TEXT NOT NULL UNIQUE,
|
|
user_id TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMP DEFAULT (datetime('now', '+10 minutes'))
|
|
)
|
|
''')
|
|
conn.commit()
|
|
logger.info("Bing Webmaster OAuth database initialized.")
|
|
|
|
def generate_authorization_url(self, user_id: str, scope: str = "webmaster.manage") -> Dict[str, Any]:
|
|
"""Generate Bing Webmaster OAuth2 authorization URL."""
|
|
try:
|
|
# Check if credentials are properly configured
|
|
if not self.client_id or not self.client_secret or self.client_id == 'your_bing_client_id_here':
|
|
logger.error("Bing Webmaster OAuth client credentials not configured")
|
|
return None
|
|
|
|
# Generate secure state parameter
|
|
state = secrets.token_urlsafe(32)
|
|
|
|
# Store state in database for validation
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
INSERT INTO bing_oauth_states (state, user_id)
|
|
VALUES (?, ?)
|
|
''', (state, user_id))
|
|
conn.commit()
|
|
|
|
# Build authorization URL with proper URL encoding
|
|
params = [
|
|
f"response_type=code",
|
|
f"client_id={self.client_id}",
|
|
f"redirect_uri={quote(self.redirect_uri, safe='')}",
|
|
f"scope={scope}",
|
|
f"state={state}"
|
|
]
|
|
|
|
auth_url = f"{self.base_url}/webmasters/OAuth/authorize?{'&'.join(params)}"
|
|
|
|
logger.info(f"Generated Bing Webmaster OAuth URL for user {user_id}")
|
|
logger.info(f"Bing OAuth redirect URI: {self.redirect_uri}")
|
|
return {
|
|
"auth_url": auth_url,
|
|
"state": state
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating Bing Webmaster OAuth URL: {e}")
|
|
return None
|
|
|
|
def handle_oauth_callback(self, code: str, state: str) -> Optional[Dict[str, Any]]:
|
|
"""Handle OAuth callback and exchange code for access token."""
|
|
try:
|
|
logger.info(f"Bing Webmaster OAuth callback started - code: {code[:20]}..., state: {state[:20]}...")
|
|
|
|
# Validate state parameter
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
SELECT user_id FROM bing_oauth_states
|
|
WHERE state = ? AND expires_at > datetime('now')
|
|
''', (state,))
|
|
result = cursor.fetchone()
|
|
|
|
if not result:
|
|
logger.error(f"Invalid or expired state parameter: {state}")
|
|
return None
|
|
|
|
user_id = result[0]
|
|
logger.info(f"Bing OAuth: State validated for user {user_id}")
|
|
|
|
# Clean up used state
|
|
cursor.execute('DELETE FROM bing_oauth_states WHERE state = ?', (state,))
|
|
conn.commit()
|
|
|
|
# Exchange authorization code for access token
|
|
token_data = {
|
|
'client_id': self.client_id,
|
|
'client_secret': self.client_secret,
|
|
'code': code,
|
|
'grant_type': 'authorization_code',
|
|
'redirect_uri': self.redirect_uri
|
|
}
|
|
|
|
logger.info(f"Bing OAuth: Exchanging code for token...")
|
|
response = requests.post(
|
|
f"{self.base_url}/webmasters/oauth/token",
|
|
data=token_data,
|
|
headers={'Content-Type': 'application/x-www-form-urlencoded'},
|
|
timeout=30
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
logger.error(f"Token exchange failed: {response.status_code} - {response.text}")
|
|
return None
|
|
|
|
token_info = response.json()
|
|
logger.info(f"Bing OAuth: Token received - expires_in: {token_info.get('expires_in')}")
|
|
|
|
# Store token information
|
|
access_token = token_info.get('access_token')
|
|
refresh_token = token_info.get('refresh_token')
|
|
expires_in = token_info.get('expires_in', 3600) # Default 1 hour
|
|
token_type = token_info.get('token_type', 'bearer')
|
|
|
|
# Calculate expiration
|
|
expires_at = datetime.now() + timedelta(seconds=expires_in)
|
|
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
INSERT INTO bing_oauth_tokens
|
|
(user_id, access_token, refresh_token, token_type, expires_at, scope)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
''', (user_id, access_token, refresh_token, token_type, expires_at, 'webmaster.manage'))
|
|
conn.commit()
|
|
logger.info(f"Bing OAuth: Token inserted into database for user {user_id}")
|
|
|
|
# Invalidate platform status and sites cache since connection status changed
|
|
# Don't invalidate analytics data cache as it's expensive to regenerate
|
|
analytics_cache.invalidate('platform_status', user_id)
|
|
analytics_cache.invalidate('bing_sites', user_id)
|
|
logger.info(f"Bing OAuth: Invalidated platform status and sites cache for user {user_id} due to new connection")
|
|
|
|
logger.info(f"Bing Webmaster OAuth token stored successfully for user {user_id}")
|
|
return {
|
|
"success": True,
|
|
"access_token": access_token,
|
|
"refresh_token": refresh_token,
|
|
"token_type": token_type,
|
|
"expires_in": expires_in,
|
|
"expires_at": expires_at.isoformat()
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error handling Bing Webmaster OAuth callback: {e}")
|
|
return None
|
|
|
|
def get_user_tokens(self, user_id: str) -> List[Dict[str, Any]]:
|
|
"""Get all active Bing tokens for a user."""
|
|
try:
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
SELECT id, access_token, refresh_token, token_type, expires_at, scope, created_at
|
|
FROM bing_oauth_tokens
|
|
WHERE user_id = ? AND is_active = TRUE AND expires_at > datetime('now')
|
|
ORDER BY created_at DESC
|
|
''', (user_id,))
|
|
|
|
tokens = []
|
|
for row in cursor.fetchall():
|
|
tokens.append({
|
|
"id": row[0],
|
|
"access_token": row[1],
|
|
"refresh_token": row[2],
|
|
"token_type": row[3],
|
|
"expires_at": row[4],
|
|
"scope": row[5],
|
|
"created_at": row[6]
|
|
})
|
|
|
|
return tokens
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting Bing tokens for user {user_id}: {e}")
|
|
return []
|
|
|
|
def test_token(self, access_token: str) -> bool:
|
|
"""Test if a Bing access token is valid."""
|
|
try:
|
|
headers = {'Authorization': f'Bearer {access_token}'}
|
|
# Try to get user's sites to test token validity
|
|
response = requests.get(
|
|
f"{self.api_base_url}/GetUserSites",
|
|
headers={
|
|
**headers,
|
|
'Origin': 'https://www.bing.com',
|
|
'Referer': 'https://www.bing.com/webmasters/'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
logger.info(f"Bing test_token: Status {response.status_code}")
|
|
if response.status_code != 200:
|
|
logger.warning(f"Bing test_token: API error {response.status_code} - {response.text}")
|
|
else:
|
|
logger.info(f"Bing test_token: Token is valid")
|
|
|
|
return response.status_code == 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error testing Bing token: {e}")
|
|
return False
|
|
|
|
def refresh_access_token(self, user_id: str, refresh_token: str) -> Optional[Dict[str, Any]]:
|
|
"""Refresh an expired access token using refresh token."""
|
|
try:
|
|
logger.info(f"Bing refresh_access_token: Attempting to refresh token for user {user_id}")
|
|
logger.debug(f"Bing refresh_access_token: Using client_id={self.client_id[:10]}..., refresh_token={refresh_token[:20]}...")
|
|
token_data = {
|
|
'client_id': self.client_id,
|
|
'client_secret': self.client_secret,
|
|
'refresh_token': refresh_token,
|
|
'grant_type': 'refresh_token'
|
|
}
|
|
|
|
response = requests.post(
|
|
f"{self.base_url}/webmasters/token",
|
|
data=token_data,
|
|
headers={
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Origin': 'https://www.bing.com',
|
|
'Referer': 'https://www.bing.com/webmasters/'
|
|
},
|
|
timeout=30
|
|
)
|
|
|
|
logger.info(f"Bing refresh_access_token: Response status {response.status_code}")
|
|
if response.status_code != 200:
|
|
logger.error(f"Token refresh failed: {response.status_code} - {response.text}")
|
|
return None
|
|
|
|
token_info = response.json()
|
|
logger.info(f"Bing refresh_access_token: Successfully refreshed token")
|
|
|
|
# Update token in database
|
|
access_token = token_info.get('access_token')
|
|
expires_in = token_info.get('expires_in', 3600)
|
|
expires_at = datetime.now() + timedelta(seconds=expires_in)
|
|
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
UPDATE bing_oauth_tokens
|
|
SET access_token = ?, expires_at = ?, updated_at = datetime('now')
|
|
WHERE user_id = ? AND refresh_token = ?
|
|
''', (access_token, expires_at, user_id, refresh_token))
|
|
conn.commit()
|
|
|
|
logger.info(f"Bing access token refreshed for user {user_id}")
|
|
return {
|
|
"access_token": access_token,
|
|
"expires_in": expires_in,
|
|
"expires_at": expires_at.isoformat()
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Bing refresh_access_token: Error refreshing token: {e}")
|
|
return None
|
|
|
|
def revoke_token(self, user_id: str, token_id: int) -> bool:
|
|
"""Revoke a Bing OAuth token."""
|
|
try:
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
UPDATE bing_oauth_tokens
|
|
SET is_active = FALSE, updated_at = datetime('now')
|
|
WHERE user_id = ? AND id = ?
|
|
''', (user_id, token_id))
|
|
conn.commit()
|
|
|
|
if cursor.rowcount > 0:
|
|
logger.info(f"Bing token {token_id} revoked for user {user_id}")
|
|
return True
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error revoking Bing token: {e}")
|
|
return False
|
|
|
|
def get_connection_status(self, user_id: str) -> Dict[str, Any]:
|
|
"""Get Bing connection status for a user."""
|
|
try:
|
|
tokens = self.get_user_tokens(user_id)
|
|
|
|
if not tokens:
|
|
return {
|
|
"connected": False,
|
|
"sites": [],
|
|
"total_sites": 0
|
|
}
|
|
|
|
# Check cache first for sites data
|
|
cached_sites = analytics_cache.get('bing_sites', user_id)
|
|
if cached_sites:
|
|
logger.info(f"Using cached Bing sites for user {user_id}")
|
|
return {
|
|
"connected": True,
|
|
"sites": cached_sites,
|
|
"total_sites": len(cached_sites)
|
|
}
|
|
|
|
# If no cache, return basic connection status without making API calls
|
|
# Sites will be fetched when needed for analytics
|
|
logger.info(f"Bing tokens found for user {user_id}, returning basic connection status")
|
|
active_sites = []
|
|
for token in tokens:
|
|
# Just check if token exists and is not expired (basic check)
|
|
# Don't make external API calls for connection status
|
|
active_sites.append({
|
|
"id": token["id"],
|
|
"access_token": token["access_token"],
|
|
"scope": token["scope"],
|
|
"created_at": token["created_at"],
|
|
"sites": [] # Sites will be fetched when needed for analytics
|
|
})
|
|
|
|
return {
|
|
"connected": len(active_sites) > 0,
|
|
"sites": active_sites,
|
|
"total_sites": len(active_sites)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting Bing connection status: {e}")
|
|
return {
|
|
"connected": False,
|
|
"sites": [],
|
|
"total_sites": 0
|
|
}
|
|
|
|
def get_user_sites(self, user_id: str) -> List[Dict[str, Any]]:
|
|
"""Get list of user's verified sites from Bing Webmaster."""
|
|
try:
|
|
tokens = self.get_user_tokens(user_id)
|
|
logger.info(f"Bing get_user_sites: Found {len(tokens)} tokens for user {user_id}")
|
|
if not tokens:
|
|
logger.warning(f"Bing get_user_sites: No tokens found for user {user_id}")
|
|
return []
|
|
|
|
all_sites = []
|
|
for i, token in enumerate(tokens):
|
|
logger.info(f"Bing get_user_sites: Testing token {i+1}/{len(tokens)}")
|
|
|
|
# Try to refresh token if it's invalid
|
|
if not self.test_token(token["access_token"]):
|
|
logger.info(f"Bing get_user_sites: Token {i+1} is invalid, attempting refresh")
|
|
if token.get("refresh_token"):
|
|
refreshed_token = self.refresh_access_token(user_id, token["refresh_token"])
|
|
if refreshed_token:
|
|
logger.info(f"Bing get_user_sites: Token {i+1} refreshed successfully")
|
|
# Update the token in the database
|
|
self.update_token_in_db(token["id"], refreshed_token)
|
|
# Use the new token
|
|
token["access_token"] = refreshed_token["access_token"]
|
|
else:
|
|
logger.warning(f"Bing get_user_sites: Failed to refresh token {i+1} - refresh token may be expired")
|
|
# Mark token as inactive since refresh failed
|
|
self.mark_token_inactive(token["id"])
|
|
continue
|
|
else:
|
|
logger.warning(f"Bing get_user_sites: No refresh token available for token {i+1}")
|
|
continue
|
|
|
|
if self.test_token(token["access_token"]):
|
|
try:
|
|
headers = {'Authorization': f'Bearer {token["access_token"]}'}
|
|
response = requests.get(
|
|
f"{self.api_base_url}/GetUserSites",
|
|
headers={
|
|
**headers,
|
|
'Origin': 'https://www.bing.com',
|
|
'Referer': 'https://www.bing.com/webmasters/'
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
sites_data = response.json()
|
|
logger.info(f"Bing API response: {response.status_code}, data type: {type(sites_data)}")
|
|
logger.debug(f"Bing API response structure: {type(sites_data)}, keys: {list(sites_data.keys()) if isinstance(sites_data, dict) else 'Not a dict'}")
|
|
logger.debug(f"Bing API response content: {sites_data}")
|
|
else:
|
|
logger.error(f"Bing API error: {response.status_code} - {response.text}")
|
|
continue
|
|
|
|
# Handle different response structures
|
|
if isinstance(sites_data, dict):
|
|
if 'd' in sites_data:
|
|
d_data = sites_data['d']
|
|
if isinstance(d_data, dict) and 'results' in d_data:
|
|
sites = d_data['results']
|
|
elif isinstance(d_data, list):
|
|
sites = d_data
|
|
else:
|
|
sites = []
|
|
else:
|
|
sites = []
|
|
elif isinstance(sites_data, list):
|
|
sites = sites_data
|
|
else:
|
|
sites = []
|
|
|
|
logger.info(f"Bing get_user_sites: Found {len(sites)} sites from token")
|
|
all_sites.extend(sites)
|
|
except Exception as e:
|
|
logger.error(f"Error getting Bing user sites: {e}")
|
|
|
|
logger.info(f"Bing get_user_sites: Returning {len(all_sites)} total sites for user {user_id}")
|
|
|
|
# If no sites found and we had tokens, it means all tokens failed
|
|
if len(all_sites) == 0 and len(tokens) > 0:
|
|
logger.warning(f"Bing get_user_sites: No sites found despite having {len(tokens)} tokens - all tokens may be expired")
|
|
|
|
return all_sites
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting Bing user sites: {e}")
|
|
return []
|
|
|
|
def update_token_in_db(self, token_id: str, refreshed_token: Dict[str, Any]) -> bool:
|
|
"""Update the access token in the database after refresh."""
|
|
try:
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
UPDATE bing_oauth_tokens
|
|
SET access_token = ?, expires_at = ?, updated_at = datetime('now')
|
|
WHERE id = ?
|
|
''', (
|
|
refreshed_token["access_token"],
|
|
refreshed_token.get("expires_at"),
|
|
token_id
|
|
))
|
|
conn.commit()
|
|
logger.info(f"Bing token {token_id} updated in database")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error updating Bing token in database: {e}")
|
|
return False
|
|
|
|
def mark_token_inactive(self, token_id: str) -> bool:
|
|
"""Mark a token as inactive in the database."""
|
|
try:
|
|
with sqlite3.connect(self.db_path) as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
UPDATE bing_oauth_tokens
|
|
SET is_active = FALSE, updated_at = datetime('now')
|
|
WHERE id = ?
|
|
''', (token_id,))
|
|
conn.commit()
|
|
logger.info(f"Bing token {token_id} marked as inactive")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error marking Bing token as inactive: {e}")
|
|
return False
|
|
|
|
def get_rank_and_traffic_stats(self, user_id: str, site_url: str, start_date: str = None, end_date: str = None) -> Dict[str, Any]:
|
|
"""Get rank and traffic statistics for a site."""
|
|
try:
|
|
tokens = self.get_user_tokens(user_id)
|
|
if not tokens:
|
|
return {"error": "No valid tokens found"}
|
|
|
|
# Use the first valid token
|
|
valid_token = None
|
|
for token in tokens:
|
|
if self.test_token(token["access_token"]):
|
|
valid_token = token
|
|
break
|
|
|
|
if not valid_token:
|
|
return {"error": "No valid access token"}
|
|
|
|
# Set default date range (last 30 days)
|
|
if not start_date:
|
|
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
|
if not end_date:
|
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
headers = {'Authorization': f'Bearer {valid_token["access_token"]}'}
|
|
params = {
|
|
'siteUrl': site_url,
|
|
'startDate': start_date,
|
|
'endDate': end_date
|
|
}
|
|
|
|
response = requests.get(
|
|
f"{self.api_base_url}/GetRankAndTrafficStats",
|
|
headers=headers,
|
|
params=params,
|
|
timeout=15
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
logger.error(f"Bing API error: {response.status_code} - {response.text}")
|
|
return {"error": f"API error: {response.status_code}"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting Bing rank and traffic stats: {e}")
|
|
return {"error": str(e)}
|
|
|
|
def get_query_stats(self, user_id: str, site_url: str, start_date: str = None, end_date: str = None, page: int = 0) -> Dict[str, Any]:
|
|
"""Get search query statistics for a site."""
|
|
try:
|
|
tokens = self.get_user_tokens(user_id)
|
|
if not tokens:
|
|
return {"error": "No valid tokens found"}
|
|
|
|
valid_token = None
|
|
for token in tokens:
|
|
if self.test_token(token["access_token"]):
|
|
valid_token = token
|
|
break
|
|
|
|
if not valid_token:
|
|
return {"error": "No valid access token"}
|
|
|
|
if not start_date:
|
|
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
|
if not end_date:
|
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
headers = {'Authorization': f'Bearer {valid_token["access_token"]}'}
|
|
params = {
|
|
'siteUrl': site_url,
|
|
'startDate': start_date,
|
|
'endDate': end_date,
|
|
'page': page
|
|
}
|
|
|
|
response = requests.get(
|
|
f"{self.api_base_url}/GetQueryStats",
|
|
headers=headers,
|
|
params=params,
|
|
timeout=15
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
logger.error(f"Bing API error: {response.status_code} - {response.text}")
|
|
return {"error": f"API error: {response.status_code}"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting Bing query stats: {e}")
|
|
return {"error": str(e)}
|
|
|
|
def get_page_stats(self, user_id: str, site_url: str, start_date: str = None, end_date: str = None, page: int = 0) -> Dict[str, Any]:
|
|
"""Get page-level statistics for a site."""
|
|
try:
|
|
tokens = self.get_user_tokens(user_id)
|
|
if not tokens:
|
|
return {"error": "No valid tokens found"}
|
|
|
|
valid_token = None
|
|
for token in tokens:
|
|
if self.test_token(token["access_token"]):
|
|
valid_token = token
|
|
break
|
|
|
|
if not valid_token:
|
|
return {"error": "No valid access token"}
|
|
|
|
if not start_date:
|
|
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
|
if not end_date:
|
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
headers = {'Authorization': f'Bearer {valid_token["access_token"]}'}
|
|
params = {
|
|
'siteUrl': site_url,
|
|
'startDate': start_date,
|
|
'endDate': end_date,
|
|
'page': page
|
|
}
|
|
|
|
response = requests.get(
|
|
f"{self.api_base_url}/GetPageStats",
|
|
headers=headers,
|
|
params=params,
|
|
timeout=15
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
logger.error(f"Bing API error: {response.status_code} - {response.text}")
|
|
return {"error": f"API error: {response.status_code}"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting Bing page stats: {e}")
|
|
return {"error": str(e)}
|
|
|
|
def get_keyword_stats(self, user_id: str, keyword: str, country: str = "us", language: str = "en-US") -> Dict[str, Any]:
|
|
"""Get keyword statistics for research purposes."""
|
|
try:
|
|
tokens = self.get_user_tokens(user_id)
|
|
if not tokens:
|
|
return {"error": "No valid tokens found"}
|
|
|
|
valid_token = None
|
|
for token in tokens:
|
|
if self.test_token(token["access_token"]):
|
|
valid_token = token
|
|
break
|
|
|
|
if not valid_token:
|
|
return {"error": "No valid access token"}
|
|
|
|
headers = {'Authorization': f'Bearer {valid_token["access_token"]}'}
|
|
params = {
|
|
'q': keyword,
|
|
'country': country,
|
|
'language': language
|
|
}
|
|
|
|
response = requests.get(
|
|
f"{self.api_base_url}/GetKeywordStats",
|
|
headers=headers,
|
|
params=params,
|
|
timeout=15
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
logger.error(f"Bing API error: {response.status_code} - {response.text}")
|
|
return {"error": f"API error: {response.status_code}"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting Bing keyword stats: {e}")
|
|
return {"error": str(e)}
|
|
|
|
def get_comprehensive_analytics(self, user_id: str, site_url: str = None) -> Dict[str, Any]:
|
|
"""Get comprehensive analytics data for all connected sites or a specific site."""
|
|
try:
|
|
# Get user's sites
|
|
sites = self.get_user_sites(user_id)
|
|
if not sites:
|
|
return {"error": "No sites found"}
|
|
|
|
# If no specific site URL provided, get data for all sites
|
|
target_sites = [site_url] if site_url else [site.get('url', '') for site in sites if site.get('url')]
|
|
|
|
analytics_data = {
|
|
"sites": [],
|
|
"summary": {
|
|
"total_sites": len(target_sites),
|
|
"total_clicks": 0,
|
|
"total_impressions": 0,
|
|
"total_ctr": 0.0
|
|
}
|
|
}
|
|
|
|
for site in target_sites:
|
|
if not site:
|
|
continue
|
|
|
|
site_data = {
|
|
"url": site,
|
|
"traffic_stats": {},
|
|
"query_stats": {},
|
|
"page_stats": {},
|
|
"error": None
|
|
}
|
|
|
|
try:
|
|
# Get traffic stats
|
|
traffic_stats = self.get_rank_and_traffic_stats(user_id, site)
|
|
if "error" not in traffic_stats:
|
|
site_data["traffic_stats"] = traffic_stats
|
|
|
|
# Get query stats (first page)
|
|
query_stats = self.get_query_stats(user_id, site)
|
|
if "error" not in query_stats:
|
|
site_data["query_stats"] = query_stats
|
|
|
|
# Get page stats (first page)
|
|
page_stats = self.get_page_stats(user_id, site)
|
|
if "error" not in page_stats:
|
|
site_data["page_stats"] = page_stats
|
|
|
|
except Exception as e:
|
|
site_data["error"] = str(e)
|
|
logger.error(f"Error getting analytics for site {site}: {e}")
|
|
|
|
analytics_data["sites"].append(site_data)
|
|
|
|
return analytics_data
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting comprehensive Bing analytics: {e}")
|
|
return {"error": str(e)} |