Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
TWILIO_PHONE_NUMBER: "+10000000000"
MONGODB_URI: mongodb://localhost:27017/test
ELEVENLABS_API_KEY: test-elevenlabs-key
ENCRYPTION_KEY: -CDr5C80aFVRKszE6j4wyt3Jz9e9RyE2axn88AFDk4s=
APP_SECRET_KEY: test-app-secret-key-1234567890
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion backend/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def _build_prompt(problem, current_time: str) -> str:
- CELL CONTENT: If a cell contains a bitwise OR operator `|` or any pipe character, you MUST escape it as `\\|` (e.g., `(a \\| b)`). Failing to escape pipes inside cells will break the table structure.
- Ensure the separator line is continuous (no line breaks) and uses at least 3 dashes per column.
- Always provide an EMPTY LINE before and after the table to ensure correct rendering.
"""
"""
if hasattr(problem, "custom_prompt") and problem.custom_prompt:
cleaned = problem.custom_prompt.strip()
if cleaned:
Expand Down
1 change: 0 additions & 1 deletion backend/ai_core/blog_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from fastapi import HTTPException
from tenacity import retry, stop_after_attempt, wait_exponential

from .prompts import build_prompt, get_current_time
from .prompts import build_prompt, build_tag_prompt, get_current_time
from .provider_manager import ProviderManager

Expand Down
2 changes: 1 addition & 1 deletion backend/ai_core/provider_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import os

from .providers.gemini_provider import GeminiProvider
from .providers.grok_provider import GrokProvider
from .providers.openai_provider import OpenAIProvider
from .providers.perplexity_provider import PerplexityProvider
from .providers.grok_provider import GrokProvider

logger = logging.getLogger(__name__)

Expand Down
1 change: 0 additions & 1 deletion backend/ai_core/providers/openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,3 @@ async def generate_blog(self, payload: dict):
raise HTTPException(status_code=502, detail=f"OpenAI service error: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected internal error: {str(e)}")

99 changes: 7 additions & 92 deletions backend/alerts/progress_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,97 +226,12 @@ async def enqueue_due_reminders(now_utc: datetime | None = None) -> dict:
if await db.reminder_jobs.find_one({"key": queue_key}, {"_id": 0}):
skipped += 1
continue
# Check if there is a blog post created today
# Date is stored as ISO format string, we can do a regex or range query
# Since it's stored as '2026-05-23T...', we can do a prefix match
today_str = today.isoformat()

solved_today_count = await db.problem_info.count_documents({
"date": {"$regex": f"^{today_str}"}
})

has_solved = solved_today_count > 0

# Also check Leetcode submissions
lc_username = user.get("leetcode_username", "vanshaggarwal27")
if not has_solved and lc_username:
try:

import requests

def check_lc():
query = """
query($username: String!, $limit: Int!) {
recentAcSubmissionList(username: $username, limit: $limit) {
timestamp
}
}
"""
return requests.post("https://leetcode.com/graphql", json={
"query": query,
"variables": {"username": lc_username, "limit": 10}
}, timeout=10).json()

data = await asyncio.to_thread(check_lc)
submissions = data.get("data", {}).get("recentAcSubmissionList", [])

# Check if any submission has a timestamp from today (UTC)
midnight_utc = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)

midnight_timestamp = int(midnight_utc.timestamp())

for sub in submissions:
if int(sub["timestamp"]) >= midnight_timestamp:
has_solved = True
print(f"Found recent Leetcode submission today for {lc_username}!")
break
except Exception as e:
print(f"Failed to check Leetcode for {lc_username}:", e)

if not has_solved:
# Not solved today, send reminder!
name = "Vansh" # Fallback or could add name to DB
message = generate_message(name)

print("Triggering alert for:", name)
print(message)
try:
send_whatsapp_message(phone, message)
print(f"WhatsApp message sent successfully to {phone}!")
except Exception as e:
print(f"Failed to send WhatsApp message to {phone}:", e)

try:
# 1. Try to Generate Audio via ElevenLabs
from alerts.elevenlabs_service import generate_audio
from alerts.twilio_service import make_call

print("Generating audio via ElevenLabs...")
try:
audio_file = generate_audio(message)

# 2. Construct public URL to the static file
backend_url = os.getenv("BACKEND_URL", "https://leetcodeai-backend.onrender.com")
if backend_url.endswith("/"):
backend_url = backend_url[:-1]

audio_url = f"{backend_url}/{audio_file}"
print(f"Audio available at: {audio_url}, making voice call...")

call_sid = make_call(phone, audio_url=audio_url)
print(f"Call placed successfully with ElevenLabs to {phone}, SID: {call_sid}")
except Exception as el_err:
print("ElevenLabs failed (possibly Free Tier VPN block):", el_err)
print("Falling back to standard Twilio Robot Voice...")
# Fallback to standard Twilio voice
call_sid = make_call(phone, text_to_say=message)
print(f"Call placed successfully with Twilio TTS to {phone}, SID: {call_sid}")

except Exception as e:
print(f"Failed to generate audio or make call to {phone}:", e)

else:
print(f"User {phone} has already solved {solved_today_count} problems today!")

await db.reminder_jobs.insert_one({"key": queue_key, "queued_at": now_utc.isoformat()})
check_user_progress_and_alert_task.delay(user_id)
queued += 1

return {"queued": queued, "skipped": skipped}

def check_unsolved_users() -> dict:
return asyncio.run(enqueue_due_reminders())
return asyncio.run(enqueue_due_reminders())
6 changes: 3 additions & 3 deletions backend/devto.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async def publish(
api_key = None
if credentials:
api_key = credentials.get("access_token") or credentials.get("devto_api_key")

if not api_key:
api_key = os.getenv("DEVTO_API_KEY")

Expand Down Expand Up @@ -163,7 +163,7 @@ async def publish(
}
}
"""
response = self._post_with_retries(
response = await self._post_with_retries(
"https://gql.hashnode.com/",
headers={
"Authorization": token,
Expand Down Expand Up @@ -352,7 +352,7 @@ async def publish_to_platforms(

async def post_to_platform(title: str, content: str) -> dict[str, Any]:
"""Backward-compatible Dev.to-only wrapper used by older integrations."""
results = publish_to_platforms(title, content, platforms=["devto"])
results = await publish_to_platforms(title, content, platforms=["devto"])
first = results[0]
if first["status"] != "success":
raise Exception(first.get("message", "Dev.to publishing failed."))
Expand Down
4 changes: 3 additions & 1 deletion backend/github_integration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import base64

import requests


def push_solution_to_github(title: str, code: str, access_token: str, repo_name: str) -> dict:
"""
Pushes the LeetCode solution code to the user's GitHub repository.
Expand All @@ -13,7 +15,7 @@ def push_solution_to_github(title: str, code: str, access_token: str, repo_name:
file_path = f"solutions/{filename}.py"

url = f"https://api.github.com/repos/{repo_name}/contents/{file_path}"

headers = {
"Authorization": f"token {access_token}",
"Accept": "application/vnd.github.v3+json"
Expand Down
67 changes: 41 additions & 26 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,75 @@
import base64
from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone
import hashlib
import hmac
import json
import logging
import os
import secrets
from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone
from typing import Annotated, Any, Optional

import httpx
import motor.motor_asyncio
import uvicorn
import httpx

from dotenv import load_dotenv
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request, status
from fastapi.concurrency import run_in_threadpool
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
from dotenv import load_dotenv
from pydantic import BaseModel
from pymongo.errors import PyMongoError
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
from pymongo.errors import PyMongoError
from twilio.rest import Client

from ai import rate_code_efficiency

# --- UPDATED AI PATH ---
from ai_core.blog_generator import generate_blog, generate_tags
from devto import publish_to_platforms
from github_integration import push_solution_to_github
from models.reminder import PublishRecord
from services.reminder_scheduler import start_scheduler
from models.user import PlatformCredential
from services.complexity_analyzer import analyze_code
from services.credential_service import resolve_user_credentials
from services.reminder_scheduler import start_scheduler
from social import share_to_platforms
from github_integration import push_solution_to_github
from utils.crypto import encrypt

load_dotenv()

logger = logging.getLogger("leetcodeai")

# -----------------------------
# MongoDB Setup
# -----------------------------
mongo_client = motor.motor_asyncio.AsyncIOMotorClient(os.getenv("MONGODB_URI"))
db = mongo_client.leetcodeai


@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Start background schedulers when server starts.
Start background schedulers and initialize MongoDB indexes when server starts.
"""
try:
# Create indexes to prevent COLLSCAN on dashboard queries
await db.problem_info.create_index(
[("user_email", 1), ("date", -1)],
background=True
)
await db.users.create_index(
[("email", 1)],
unique=True,
background=True
)
print("MongoDB indexes ensured successfully.")
except Exception as e:
logger.error(f"Failed to create MongoDB indexes: {e}")

try:
start_scheduler()
print("Reminder scheduler started successfully.")
Expand Down Expand Up @@ -95,12 +116,6 @@ async def mongodb_exception_handler(request: Request, exc: PyMongoError):
# -----------------------------
twilio_client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))

# -----------------------------
# MongoDB Setup
# -----------------------------
mongo_client = motor.motor_asyncio.AsyncIOMotorClient(os.getenv("MONGODB_URI"))

db = mongo_client.leetcodeai


# -----------------------------
Expand Down Expand Up @@ -446,15 +461,15 @@ def health_check():
async def create_blog(
request: Request,
problem: Problem,
current_user: Annotated[dict[str, Any], Depends(get_current_user)],
current_user: Annotated[dict[str, Any] | None, Depends(get_optional_user)] = None,
x_user_email: Optional[str] = Header(default=None),
):
"""
Accepts a LeetCode problem, pulls user-specific database integration credentials,
generates a blog post using AI, and publishes it dynamically.
"""
user_email = require_user(x_user_email)
user_id = current_user["id"]
user_id = current_user["id"] if current_user else "anonymous"

existing_record = await db.problem_info.find_one(
{"title": problem.title, "author": problem.author, "status": "success"}
Expand Down Expand Up @@ -491,14 +506,14 @@ async def create_blog(
devto_creds = await resolve_user_credentials(db, user_id, "devto")

try:
suggested_tags = await run_in_threadpool(
_ = await run_in_threadpool(
generate_tags,
problem,
blog_content,
credentials=user_settings,
)
except Exception:
suggested_tags = ""
pass

try:
platform_results = await publish_to_platforms(
Expand Down Expand Up @@ -593,14 +608,14 @@ class EditedBlog(BaseModel):
@app.post("/publish-blog")
async def publish_blog(
blog: EditedBlog,
current_user: Annotated[dict[str, Any], Depends(get_current_user)],
current_user: Annotated[dict[str, Any] | None, Depends(get_optional_user)] = None,
x_user_email: Optional[str] = Header(default=None),
):
"""
Accepts an edited blog post and distributes it using safe user-isolated tokens.
"""
user_email = require_user(x_user_email)
user_id = current_user["id"]
user_id = current_user["id"] if current_user else "anonymous"

user_settings = await _settings_for_user(user_id)
devto_creds = await resolve_user_credentials(db, user_id, "devto")
Expand Down Expand Up @@ -679,7 +694,7 @@ async def get_dashboard_stats(
user_email = current_user["email"]
else:
user_email = require_user(x_user_email)

user_filter = {"user_email": user_email}

try:
Expand Down Expand Up @@ -726,11 +741,11 @@ async def get_dashboard_stats(
if daily_activity:
dates_set = {doc["date"] for doc in daily_activity}
today = datetime.now(timezone.utc).date()

current_date = today
if current_date.isoformat() not in dates_set:
current_date = today - timedelta(days=1)

while current_date.isoformat() in dates_set:
current_streak += 1
current_date -= timedelta(days=1)
Expand Down
7 changes: 4 additions & 3 deletions backend/models/user.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# backend/models/user.py

from typing import Dict, Optional

from pydantic import BaseModel, Field
from typing import Optional, Dict
from datetime import datetime


class PlatformCredential(BaseModel):
access_token: str
Expand All @@ -22,4 +23,4 @@ class User(BaseModel):
credentials: Dict[str, PlatformCredential] = Field(default_factory=dict)

class Config:
populate_by_name = True
populate_by_name = True
2 changes: 1 addition & 1 deletion backend/services/complexity_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,4 @@ def analyze_code(code: str):
"spaceComplexity": space_complexity,
"pattern": pattern,
"suggestions": suggestions,
}
}
Loading
Loading