2026-05-11 07:02:06 +02:00
import asyncio
2026-05-10 09:08:12 +02:00
import logging
2026-05-11 22:12:43 +02:00
import os
2026-05-12 15:07:34 +02:00
import time
from collections import defaultdict
2026-05-10 09:08:12 +02:00
from fastapi import FastAPI , Request
2026-05-12 15:07:34 +02:00
from fastapi . responses import HTMLResponse , RedirectResponse
2026-05-10 09:08:12 +02:00
from fastapi . staticfiles import StaticFiles
2026-05-12 12:45:52 +02:00
from devplacepy . config import STATIC_DIR , PORT
from devplacepy . database import init_db , get_table , db , get_users_by_uids , get_comment_counts_by_post_uids , get_vote_counts
2026-05-10 09:08:12 +02:00
from devplacepy . templating import templates
2026-05-12 12:45:52 +02:00
from devplacepy . utils import get_current_user , time_ago
2026-05-11 05:30:51 +02:00
from devplacepy . seo import base_seo_context , site_url , website_schema , breadcrumb_schema , combine
2026-05-12 15:07:34 +02:00
from devplacepy . routers import auth , feed , posts , comments , projects , profile , messages , notifications , votes , avatar , follow , admin , seo , bugs , news , gists , services as services_router , uploads
2026-05-11 07:02:06 +02:00
from devplacepy . services . manager import service_manager
from devplacepy . services . news import NewsService
2026-05-10 09:08:12 +02:00
logging . basicConfig (
level = logging . INFO ,
format = " %(asctime)s [ %(levelname)s ] %(name)s : %(message)s " ,
)
logger = logging . getLogger ( __name__ )
2026-05-12 15:07:34 +02:00
_rate_limit_store = defaultdict ( list )
RATE_LIMIT = 60
RATE_WINDOW = 60
2026-05-10 09:08:12 +02:00
app = FastAPI ( title = " DevPlace " )
app . mount ( " /static " , StaticFiles ( directory = str ( STATIC_DIR ) ) , name = " static " )
2026-05-12 15:07:34 +02:00
@app.exception_handler ( 404 )
async def not_found ( request : Request , exc ) :
seo_ctx = base_seo_context ( request , title = " Not Found — DevPlace " , description = " The page you requested does not exist. " , robots = " noindex " )
2026-05-13 23:03:06 +02:00
return templates . TemplateResponse ( request , " error.html " , { * * seo_ctx , " request " : request , " error_code " : 404 , " error_message " : " Page not found " } , status_code = 404 )
2026-05-12 15:07:34 +02:00
@app.exception_handler ( 500 )
async def server_error ( request : Request , exc ) :
2026-05-14 04:12:19 +02:00
logger . exception ( " 500 error on %s %s " , request . method , request . url . path )
2026-05-12 15:07:34 +02:00
seo_ctx = base_seo_context ( request , title = " Server Error — DevPlace " , description = " Something went wrong. " , robots = " noindex " )
2026-05-13 23:03:06 +02:00
return templates . TemplateResponse ( request , " error.html " , { * * seo_ctx , " request " : request , " error_code " : 500 , " error_message " : " Internal server error " } , status_code = 500 )
2026-05-12 15:07:34 +02:00
2026-05-10 09:08:12 +02:00
app . include_router ( auth . router , prefix = " /auth " )
app . include_router ( feed . router , prefix = " /feed " )
app . include_router ( posts . router , prefix = " /posts " )
app . include_router ( comments . router , prefix = " /comments " )
app . include_router ( projects . router , prefix = " /projects " )
app . include_router ( profile . router , prefix = " /profile " )
app . include_router ( messages . router , prefix = " /messages " )
app . include_router ( notifications . router , prefix = " /notifications " )
app . include_router ( votes . router , prefix = " /votes " )
2026-05-10 21:33:53 +02:00
app . include_router ( avatar . router , prefix = " /avatar " )
2026-05-11 00:41:41 +02:00
app . include_router ( follow . router , prefix = " /follow " )
2026-05-11 05:30:51 +02:00
app . include_router ( admin . router , prefix = " /admin " )
app . include_router ( seo . router )
2026-05-11 07:02:06 +02:00
app . include_router ( bugs . router , prefix = " /bugs " )
2026-05-12 15:07:34 +02:00
app . include_router ( gists . router , prefix = " /gists " )
2026-05-11 22:12:43 +02:00
app . include_router ( news . router , prefix = " /news " )
2026-05-11 07:02:06 +02:00
app . include_router ( services_router . router , prefix = " /admin/services " )
2026-05-12 15:07:34 +02:00
app . include_router ( uploads . router , prefix = " /uploads " )
2026-05-11 05:30:51 +02:00
@app.middleware ( " http " )
async def add_security_headers ( request : Request , call_next ) :
response = await call_next ( request )
if not response . headers . get ( " X-Robots-Tag " ) :
response . headers [ " X-Robots-Tag " ] = " index, follow "
response . headers [ " X-Content-Type-Options " ] = " nosniff "
return response
2026-05-10 09:08:12 +02:00
2026-05-12 15:07:34 +02:00
@app.middleware ( " http " )
async def rate_limit_middleware ( request : Request , call_next ) :
if request . method in ( " POST " , " PUT " , " DELETE " , " PATCH " ) :
ip = request . client . host if request . client else " unknown "
now = time . time ( )
window_start = now - RATE_WINDOW
_rate_limit_store [ ip ] = [ t for t in _rate_limit_store [ ip ] if t > window_start ]
if len ( _rate_limit_store [ ip ] ) > = RATE_LIMIT :
return HTMLResponse ( " Rate limit exceeded. Try again later. " , status_code = 429 )
_rate_limit_store [ ip ] . append ( now )
return await call_next ( request )
2026-05-10 09:08:12 +02:00
@app.on_event ( " startup " )
async def startup ( ) :
init_db ( )
2026-05-11 22:12:43 +02:00
if not os . environ . get ( " DEVPLACE_DISABLE_SERVICES " ) :
news_service = NewsService ( )
service_manager . register ( news_service )
asyncio . create_task ( service_manager . start_all ( ) )
2026-05-12 12:45:52 +02:00
logger . info ( f " DevPlace started on port { PORT } " )
2026-05-10 09:08:12 +02:00
2026-05-11 07:02:06 +02:00
@app.on_event ( " shutdown " )
async def shutdown ( ) :
logger . info ( " Shutting down services... " )
await service_manager . stop_all ( )
2026-05-10 09:08:12 +02:00
@app.get ( " / " )
async def landing ( request : Request ) :
user = get_current_user ( request )
if user :
return RedirectResponse ( url = " /feed " , status_code = 302 )
2026-05-11 22:12:43 +02:00
landing_articles = [ ]
if " news " in db . tables :
news_table = get_table ( " news " )
2026-05-13 21:17:57 +02:00
raw = list ( news_table . find ( show_on_landing = 1 , order_by = [ " -synced_at " ] , _limit = 6 ) )
2026-05-11 22:12:43 +02:00
images_table = get_table ( " news_images " ) if " news_images " in db . tables else None
for a in raw :
image_url = " "
if images_table :
img = images_table . find_one ( news_uid = a [ " uid " ] )
if img :
image_url = img [ " url " ]
landing_articles . append ( {
2026-05-12 12:45:52 +02:00
" uid " : a [ " uid " ] ,
" slug " : a . get ( " slug " , " " ) ,
2026-05-11 22:12:43 +02:00
" title " : a . get ( " title " , " " ) ,
2026-05-12 12:45:52 +02:00
" description " : ( a . get ( " description " , " " ) or " " ) [ : 250 ] ,
2026-05-11 22:12:43 +02:00
" url " : a . get ( " url " , " " ) ,
" source_name " : a . get ( " source_name " , " " ) ,
2026-05-12 12:45:52 +02:00
" grade " : a . get ( " grade " , 0 ) ,
" synced_at " : a . get ( " synced_at " , " " ) or " " ,
" time_ago " : time_ago ( a [ " synced_at " ] ) ,
2026-05-11 22:12:43 +02:00
" image_url " : image_url ,
} )
2026-05-12 12:45:52 +02:00
landing_posts = [ ]
if " posts " in db . tables :
posts_table = get_table ( " posts " )
raw_posts = list ( posts_table . find ( order_by = [ " -created_at " ] , _limit = 6 ) )
if raw_posts :
post_uids = [ p [ " uid " ] for p in raw_posts ]
author_uids = [ p [ " user_uid " ] for p in raw_posts ]
authors = get_users_by_uids ( author_uids )
comment_counts = get_comment_counts_by_post_uids ( post_uids )
upvotes , downvotes = get_vote_counts ( post_uids )
for p in raw_posts :
landing_posts . append ( {
" post " : p ,
" author " : authors . get ( p [ " user_uid " ] ) ,
" time_ago " : time_ago ( p [ " created_at " ] ) ,
" comment_count " : comment_counts . get ( p [ " uid " ] , 0 ) ,
" stars " : upvotes . get ( p [ " uid " ] , 0 ) - downvotes . get ( p [ " uid " ] , 0 ) ,
" slug " : p . get ( " slug " , " " ) or p [ " uid " ] ,
} )
2026-05-11 05:30:51 +02:00
base = site_url ( request )
seo_ctx = base_seo_context (
request ,
title = " DevPlace — The Developer Social Network " ,
description = " Track industry shifts. Discover bold releases. Share what you ' re building in an open, uncensored environment. " ,
breadcrumbs = [ ] ,
schemas = [ website_schema ( base ) ] ,
)
2026-05-13 23:03:06 +02:00
return templates . TemplateResponse ( request , " landing.html " , {
2026-05-12 12:45:52 +02:00
* * seo_ctx , " request " : request ,
2026-05-11 22:12:43 +02:00
" landing_articles " : landing_articles ,
2026-05-12 12:45:52 +02:00
" landing_posts " : landing_posts ,
2026-05-11 22:12:43 +02:00
} )