Base code

This commit is contained in:
Kunthawat Greethong
2026-01-08 22:39:53 +07:00
parent 697115c61a
commit c35fa52117
2169 changed files with 626670 additions and 0 deletions

View File

@@ -0,0 +1,925 @@
"""
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', '+20 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, expires_at)
VALUES (?, ?, datetime('now', '+20 minutes'))
''', (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()
# First, look up the state regardless of expiry to provide clearer logs
cursor.execute('''
SELECT user_id, created_at, expires_at FROM bing_oauth_states
WHERE state = ?
''', (state,))
row = cursor.fetchone()
if not row:
# State not found - likely already consumed (deleted) or never issued
logger.error(f"Bing OAuth: State not found or already used. state='{state[:12]}...'")
return None
user_id, created_at, expires_at = row
# Check expiry explicitly
cursor.execute("SELECT datetime('now') < ?", (expires_at,))
not_expired = cursor.fetchone()[0] == 1
if not not_expired:
logger.error(
f"Bing OAuth: State expired. state='{state[:12]}...', user_id='{user_id}', "
f"created_at='{created_at}', expires_at='{expires_at}'"
)
# Clean up expired state
cursor.execute('DELETE FROM bing_oauth_states WHERE state = ?', (state,))
conn.commit()
return None
# Valid, not expired
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}")
# Proactively fetch and cache user sites using the fresh token
try:
headers = {'Authorization': f'Bearer {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=15
)
sites = []
if response.status_code == 200:
sites_data = response.json()
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
elif isinstance(sites_data, list):
sites = sites_data
if sites:
analytics_cache.set('bing_sites', user_id, sites, ttl_override=2*60*60)
logger.info(f"Bing OAuth: Cached {len(sites)} sites for user {user_id} after OAuth callback")
except Exception as site_err:
logger.warning(f"Bing OAuth: Failed to prefetch sites after OAuth callback: {site_err}")
# 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 purge_expired_tokens(self, user_id: str) -> int:
"""Delete expired or inactive Bing tokens for a user to avoid refresh loops.
Returns number of rows deleted.
"""
try:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Delete tokens that are expired or explicitly inactive
cursor.execute('''
DELETE FROM bing_oauth_tokens
WHERE user_id = ? AND (is_active = FALSE OR (expires_at IS NOT NULL AND expires_at <= datetime('now')))
''', (user_id,))
deleted = cursor.rowcount or 0
conn.commit()
if deleted > 0:
logger.info(f"Bing OAuth: Purged {deleted} expired/inactive tokens for user {user_id}")
else:
logger.info(f"Bing OAuth: No expired/inactive tokens to purge for user {user_id}")
# Invalidate platform status cache so UI updates
analytics_cache.invalidate('platform_status', user_id)
return deleted
except Exception as e:
logger.error(f"Bing OAuth: Error purging expired tokens for user {user_id}: {e}")
return 0
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 get_user_token_status(self, user_id: str) -> Dict[str, Any]:
"""Get detailed token status for a user including expired tokens."""
try:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Get all tokens (active and expired)
cursor.execute('''
SELECT id, access_token, refresh_token, token_type, expires_at, scope, created_at, is_active
FROM bing_oauth_tokens
WHERE user_id = ?
ORDER BY created_at DESC
''', (user_id,))
all_tokens = []
active_tokens = []
expired_tokens = []
for row in cursor.fetchall():
token_data = {
"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],
"is_active": bool(row[7])
}
all_tokens.append(token_data)
# Determine expiry using robust parsing and is_active flag
is_active_flag = bool(row[7])
not_expired = False
try:
expires_at_val = row[4]
if expires_at_val:
# First try Python parsing
try:
dt = datetime.fromisoformat(expires_at_val) if isinstance(expires_at_val, str) else expires_at_val
not_expired = dt > datetime.now()
except Exception:
# Fallback to SQLite comparison
cursor.execute("SELECT datetime('now') < ?", (expires_at_val,))
not_expired = cursor.fetchone()[0] == 1
else:
# No expiry stored => consider not expired
not_expired = True
except Exception:
not_expired = False
if is_active_flag and not_expired:
active_tokens.append(token_data)
else:
expired_tokens.append(token_data)
return {
"has_tokens": len(all_tokens) > 0,
"has_active_tokens": len(active_tokens) > 0,
"has_expired_tokens": len(expired_tokens) > 0,
"active_tokens": active_tokens,
"expired_tokens": expired_tokens,
"total_tokens": len(all_tokens),
"last_token_date": all_tokens[0]["created_at"] if all_tokens else None
}
except Exception as e:
logger.error(f"Error getting Bing token status for user {user_id}: {e}")
return {
"has_tokens": False,
"has_active_tokens": False,
"has_expired_tokens": False,
"active_tokens": [],
"expired_tokens": [],
"total_tokens": 0,
"last_token_date": None,
"error": str(e)
}
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/oauth/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 = ?, is_active = TRUE, 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}")
# Invalidate caches that depend on token validity
try:
analytics_cache.invalidate('platform_status', user_id)
analytics_cache.invalidate('bing_sites', user_id)
except Exception as _:
pass
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:
# Fast path: return cached sites if available
try:
cached_sites = analytics_cache.get('bing_sites', user_id)
if cached_sites:
logger.info(f"Bing get_user_sites: Returning {len(cached_sites)} cached sites for user {user_id}")
return cached_sites
except Exception:
pass
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)
# Cache sites immediately for future calls
try:
analytics_cache.set('bing_sites', user_id, all_sites, ttl_override=2*60*60)
except Exception:
pass
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()
# Compute expires_at from expires_in if expires_at missing
expires_at_value = refreshed_token.get("expires_at")
if not expires_at_value and refreshed_token.get("expires_in"):
try:
expires_at_value = datetime.now() + timedelta(seconds=int(refreshed_token["expires_in"]))
except Exception:
expires_at_value = None
cursor.execute('''
UPDATE bing_oauth_tokens
SET access_token = ?, expires_at = ?, is_active = TRUE, updated_at = datetime('now')
WHERE id = ?
''', (
refreshed_token["access_token"],
expires_at_value,
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)}