Skip to content
Open
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
129 changes: 78 additions & 51 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ async def create_blog(
if existing_record:
return {
"status": "error",
"message": f"Solution for '{problem.title}' has already been published!",
"message": f"Solution for '{problem.title}' has already been published! Keep up the great streak!",
}

if problem.custom_prompt and len(problem.custom_prompt.strip()) > 1000:
Expand All @@ -490,8 +490,10 @@ async def create_blog(
if not problem.code or problem.code.strip() == "":
return {"status": "error", "message": "Code is empty, cannot generate blog."}

user_settings = await _settings_for_user(user_id)
user_settings = await _settings_for_user(user_id) if user_id else {}

# --- Atomic Lock to prevent Race Conditions ---
lock_id = f"generate_blog_{problem.title}_{problem.author}_{user_email}"
try:
blog_content = await run_in_threadpool(generate_blog, problem, credentials=user_settings)
efficiency = rate_code_efficiency(
Expand All @@ -516,58 +518,83 @@ async def create_blog(
pass

try:
platform_results = await publish_to_platforms(
problem.title,
blog_content,
platforms=problem.platforms or user_settings.get("publish_platforms"),
published=not problem.publish_as_draft,
tags=problem.tags,
credentials=devto_creds, # Using user specific keys
)
successful = [r for r in platform_results if r.get("status") == "success"]
overall_status = (
"success"
if len(successful) == len(platform_results)
else "partial_success" if successful else "error"
)
except Exception as e:
return {"status": "error", "message": f"Publishing failure: {str(e)}"}
try:
blog_content = await run_in_threadpool(generate_blog, problem, credentials=user_settings)
efficiency = rate_code_efficiency(
problem.title,
problem.code,
problem.language or "python"
)
except Exception as e:
return {"status": "error", "message": f"AI provider failure: {str(e)}"}

try:
record = PublishRecord(
title=problem.title,
date=datetime.now(timezone.utc).isoformat(),
platforms=[r["platform"] for r in successful],
status=overall_status,
author=problem.author,
user_email=user_email,
)
# Resolve platform-specific credentials from database securely at runtime
devto_creds = await resolve_user_credentials(db, user_id, "devto") if user_id else {}

await db.problem_info.update_one(
{"title": problem.title, "author": problem.author, "user_email": user_email},
{"$set": record.model_dump()},
upsert=True,
)
try:
platform_results = await publish_to_platforms(
problem.title,
blog_content,
platforms=problem.platforms or user_settings.get("publish_platforms"),
published=not problem.publish_as_draft,
tags=problem.tags,
credentials=devto_creds, # Using user specific keys
)
successful = [r for r in platform_results if r.get("status") == "success"]
overall_status = (
"success"
if len(successful) == len(platform_results)
else "partial_success" if successful else "error"
)
except Exception as e:
return {"status": "error", "message": f"Publishing failure: {str(e)}"}

except Exception as e:
print(f"Database logging failed: {e}")
try:
record = PublishRecord(
title=problem.title,
date=datetime.now(timezone.utc).isoformat(),
platforms=[r["platform"] for r in successful],
status=overall_status,
author=problem.author,
user_email=user_email,
)

social_results = []
if problem.share_to_social and successful:
post_url = next((res["url"] for res in successful if res.get("url")), None)
await db.problem_info.update_one(
{"title": problem.title, "author": problem.author, "user_email": user_email},
{"$set": record.model_dump()},
upsert=True,
)

if post_url:
try:
# Dynamically fetch encrypted LinkedIn credentials
linkedin_creds = await resolve_user_credentials(db, user_id, "linkedin")
social_results = share_to_platforms(
title=problem.title,
post_url=post_url,
tags=problem.tags,
credentials=linkedin_creds, # Decrypted user scope profile object
)
except Exception as e:
print(f"Social sharing failed: {e}")
except Exception as e:
print(f"Database logging failed: {e}")

social_results = []
if problem.share_to_social and successful:
post_url = next((res["url"] for res in successful if res.get("url")), None)

if post_url:
try:
# Dynamically fetch encrypted LinkedIn credentials
linkedin_creds = await resolve_user_credentials(db, user_id, "linkedin")
social_results = share_to_platforms(
title=problem.title,
post_url=post_url,
tags=problem.tags,
credentials=linkedin_creds, # Decrypted user scope profile object
)
except Exception as e:
print(f"Social sharing failed: {e}")
return {
"status": overall_status,
"data": {
"blog_content": blog_content,"efficiency": efficiency,
"platforms": platform_results,
"social": social_results,
},
}
finally:
# Release the lock so future attempts can proceed if this failed
await db.locks.delete_one({"_id": lock_id})

# GitHub automatic commit integration
if successful and user_settings.get("github_access_token") and user_settings.get("github_repo_name"):
Expand Down Expand Up @@ -617,8 +644,8 @@ async def publish_blog(
user_email = require_user(x_user_email)
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")
user_settings = await _settings_for_user(user_id) if user_id else {}
devto_creds = await resolve_user_credentials(db, user_id, "devto") if user_id else {}

try:
platform_results = await publish_to_platforms(
Expand Down
24 changes: 24 additions & 0 deletions backend/test_race.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import threading

import requests


def send_request():
payload = {
"title": "Race Condition Test",
"code": "print('hello')",
"author": "Anonymous Developer",
"publish_as_draft": True
}
# You might need to add authentication headers depending on your setup
response = requests.post("http://127.0.0.1:10000/generate-blog", json=payload)
print(f"Response: {response.json()}")

threads = []
for _ in range(5):
t = threading.Thread(target=send_request)
threads.append(t)
t.start()

for t in threads:
t.join()
9 changes: 9 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(self) -> None:
self.find_one = AsyncMock(side_effect=self._find_one)
self.count_documents = AsyncMock(side_effect=self._count_documents)
self.delete_many = AsyncMock(side_effect=self._delete_many)
self.delete_one = AsyncMock(side_effect=self._delete_one)

async def _find_one(self, query, *args, **kwargs):
for record in self.records:
Expand Down Expand Up @@ -111,6 +112,13 @@ async def _delete_many(self, query, *args, **kwargs):
self.records = [r for r in self.records if not self._matches(r, query)]
return Mock(deleted_count=initial_count - len(self.records))

async def _delete_one(self, query, *args, **kwargs):
for i, r in enumerate(self.records):
if self._matches(r, query):
del self.records[i]
return Mock(deleted_count=1)
return Mock(deleted_count=0)

def find(self, *args, **kwargs):
query = args[0] if args else {}
return FakeCursor(
Expand Down Expand Up @@ -153,6 +161,7 @@ def __init__(self) -> None:
self.integration_settings = FakeCollection()
self.reminder_jobs = FakeCollection()
self.reminder_alerts = FakeCollection()
self.locks = FakeCollection()


class FakeMotorClient:
Expand Down
Loading