Skip to content

Commit e0e6110

Browse files
committed
v2.3.10b
1 parent 7afecaa commit e0e6110

22 files changed

Lines changed: 1114 additions & 239 deletions

File tree

backend/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ trans.sh
3232
extract_messages.py
3333
babel.cfg
3434
legacy/
35+
jwt

backend/app/configs/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Settings(BaseSettings):
3838
FEISHU_WEBHOOK_URL: str = ""
3939
VITE_JS_API_TOKEN: str = ""
4040
AMAP_SECURITY_CODE: str = ""
41+
AMAP_WEB_KEY: str = ""
4142

4243
model_config = SettingsConfigDict(
4344
env_file=get_env_file_path(),

backend/app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ async def lifespan(app: FastAPI):
6060
database=app.state.mongo,
6161
document_models=[MessageBoard, Post, RssArticle],
6262
)
63-
app.state.redis, app.state.redis2 = await init_redis()
63+
app.state.redis, app.state.redis2 = await init_redis() # type: ignore
6464

6565
logger.debug(f"Settings:{get_settings().model_dump()}")
6666
logger.info("FastAPI started successfully.")

backend/app/routers/public.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from datetime import UTC, datetime
1313
from xml.etree.ElementTree import Element, SubElement, tostring
1414

15+
import httpx
1516
from fastapi import APIRouter, Body, Depends, Request
1617
from fastapi.responses import JSONResponse, PlainTextResponse
1718
from redis.asyncio import Redis as AsyncRedis
@@ -316,3 +317,93 @@ async def get_amap_security_key(request: Request) -> JSONResponse:
316317
data={"securityJsCode": encoded_key},
317318
message="Amap security key retrieved successfully",
318319
)
320+
321+
322+
@router.post("/weather")
323+
@limiter.limit("100/hour")
324+
async def get_weather(
325+
request: Request,
326+
city: str = Body(..., description="City adcode"),
327+
extensions: str = Body("base", description="Weather type: base/all"),
328+
) -> JSONResponse:
329+
"""Get weather information from Amap API.
330+
331+
Args:
332+
city: City adcode
333+
extensions: Weather type (base for current, all for forecast)
334+
335+
Returns:
336+
JSONResponse: Weather data from Amap API
337+
"""
338+
url = "https://restapi.amap.com/v3/weather/weatherInfo"
339+
params = {
340+
"key": get_settings().AMAP_WEB_KEY,
341+
"city": city,
342+
"extensions": extensions,
343+
}
344+
async with httpx.AsyncClient() as client:
345+
response = await client.get(url, params=params)
346+
data = response.json()
347+
348+
return APIResponse.ok(
349+
data=data,
350+
message="Weather information retrieved successfully",
351+
)
352+
353+
354+
@router.post("/geocode/regeo")
355+
@limiter.limit("100/hour")
356+
async def reverse_geocode(
357+
request: Request,
358+
location: str = Body(..., description="Location coordinates: lng,lat"),
359+
extensions: str = Body("base", description="Extensions type"),
360+
) -> JSONResponse:
361+
"""Reverse geocode coordinates to address using Amap API.
362+
363+
Args:
364+
location: Location coordinates in format "lng,lat"
365+
extensions: Extensions type
366+
367+
Returns:
368+
JSONResponse: Address information including city adcode
369+
"""
370+
url = "https://restapi.amap.com/v3/geocode/regeo"
371+
params = {
372+
"key": get_settings().AMAP_WEB_KEY,
373+
"location": location,
374+
"extensions": extensions,
375+
}
376+
async with httpx.AsyncClient() as client:
377+
response = await client.get(url, params=params)
378+
data = response.json()
379+
380+
return APIResponse.ok(
381+
data=data,
382+
message="Reverse geocode completed successfully",
383+
)
384+
385+
386+
# @router.post("/geocode/regeo")
387+
# @limiter.limit("100/hour")
388+
# async def reverse_geocode(
389+
# request: Request,
390+
# params: dict = Body(..., description="Reverse geocode parameters"),
391+
# ) -> JSONResponse:
392+
# """Reverse geocode coordinates to address using Amap API.
393+
394+
# Args:
395+
# params: Reverse geocode parameters including location coordinates
396+
397+
# Returns:
398+
# JSONResponse: Address information including city adcode
399+
# """
400+
# url = "https://restapi.amap.com/v3/geocode/regeo"
401+
# params["key"] = get_settings().AMAP_WEB_KEY
402+
# async with httpx.AsyncClient() as client:
403+
# response = await client.get(url, params=params)
404+
# data = response.json()
405+
406+
# return APIResponse.ok(
407+
# data=data,
408+
# message="Reverse geocode completed successfully",
409+
# )

backend/app/routers/todos.py

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from datetime import datetime, timezone
66

77
import orjson
8-
from fastapi import APIRouter, Depends, status
8+
from fastapi import APIRouter, Depends, Query, status
99
from redis.asyncio import Redis as AsyncRedis
1010

1111
from app.dependencies.auth import manager
@@ -50,11 +50,15 @@ async def _release_lock(redis: AsyncRedis, lock_key: str) -> None:
5050

5151
@router.get("")
5252
async def get_todos(
53-
user: User = Depends(manager), redis: AsyncRedis = Depends(get_async_redis)
53+
include_archived: bool = Query(False),
54+
user: User = Depends(manager),
55+
redis: AsyncRedis = Depends(get_async_redis),
5456
):
55-
"""Get current user's todos."""
57+
"""Get current user's todos. Excludes archived by default."""
5658
key = f"todos:{user.id}"
5759
todos = await _read_todos(redis, key)
60+
if not include_archived:
61+
todos = [t for t in todos if not t.get("archived")]
5862
return APIResponse.ok(data={"todos": todos})
5963

6064

@@ -84,6 +88,8 @@ async def create_todo(
8488
"dueDate": data.dueDate,
8589
"priority": data.priority or "medium",
8690
"category": data.category,
91+
"archived": bool(data.archived),
92+
"archivedAt": data.archivedAt,
8793
}
8894
todos.insert(0, todo)
8995
await _write_todos(redis, key, todos)
@@ -128,6 +134,10 @@ async def patch_todo(
128134
t["category"] = data.category
129135
if data.completed is not None:
130136
t["completed"] = bool(data.completed)
137+
if data.archived is not None:
138+
t["archived"] = bool(data.archived)
139+
if data.archivedAt is not None:
140+
t["archivedAt"] = data.archivedAt
131141
todos[i] = t
132142
updated = t
133143
break
@@ -171,6 +181,8 @@ async def replace_todo(
171181
"dueDate": data.dueDate,
172182
"priority": data.priority or "medium",
173183
"category": data.category,
184+
"archived": bool(data.archived),
185+
"archivedAt": data.archivedAt,
174186
}
175187
todos[i] = updated
176188
break
@@ -241,6 +253,8 @@ async def import_todos(
241253
"dueDate": item.dueDate,
242254
"priority": item.priority or "medium",
243255
"category": item.category,
256+
"archived": bool(item.archived),
257+
"archivedAt": item.archivedAt,
244258
}
245259
)
246260
merged = new_items + existing
@@ -251,6 +265,112 @@ async def import_todos(
251265
return APIResponse.ok(message="Todos imported")
252266

253267

268+
@router.get("/archived")
269+
async def get_archived_todos(
270+
user: User = Depends(manager), redis: AsyncRedis = Depends(get_async_redis)
271+
):
272+
"""Get all archived todos for current user."""
273+
key = f"todos:{user.id}"
274+
todos = await _read_todos(redis, key)
275+
archived = [t for t in todos if t.get("archived")]
276+
return APIResponse.ok(data={"todos": archived})
277+
278+
279+
@router.post("/{todo_id}/archive")
280+
async def archive_todo(
281+
todo_id: str,
282+
user: User = Depends(manager),
283+
redis: AsyncRedis = Depends(get_async_redis),
284+
):
285+
"""Archive a todo."""
286+
key = f"todos:{user.id}"
287+
lock_key = f"todos:lock:{user.id}"
288+
if not await _acquire_lock(redis, lock_key):
289+
return APIResponse.error(
290+
message="Server busy, please retry.", code=status.HTTP_423_LOCKED
291+
)
292+
updated = None
293+
try:
294+
todos = await _read_todos(redis, key)
295+
for i, t in enumerate(todos):
296+
if t.get("id") == todo_id:
297+
t["archived"] = True
298+
t["archivedAt"] = datetime.now(timezone.utc).isoformat() # noqa: UP017
299+
todos[i] = t
300+
updated = t
301+
break
302+
if updated is None:
303+
return APIResponse.error(
304+
message="Todo not found", code=status.HTTP_404_NOT_FOUND
305+
)
306+
await _write_todos(redis, key, todos)
307+
finally:
308+
await _release_lock(redis, lock_key)
309+
310+
return APIResponse.ok(data={"todo": updated}, message="Todo archived")
311+
312+
313+
@router.post("/{todo_id}/unarchive")
314+
async def unarchive_todo(
315+
todo_id: str,
316+
user: User = Depends(manager),
317+
redis: AsyncRedis = Depends(get_async_redis),
318+
):
319+
"""Unarchive a todo."""
320+
key = f"todos:{user.id}"
321+
lock_key = f"todos:lock:{user.id}"
322+
if not await _acquire_lock(redis, lock_key):
323+
return APIResponse.error(
324+
message="Server busy, please retry.", code=status.HTTP_423_LOCKED
325+
)
326+
updated = None
327+
try:
328+
todos = await _read_todos(redis, key)
329+
for i, t in enumerate(todos):
330+
if t.get("id") == todo_id:
331+
t["archived"] = False
332+
t["archivedAt"] = None
333+
todos[i] = t
334+
updated = t
335+
break
336+
if updated is None:
337+
return APIResponse.error(
338+
message="Todo not found", code=status.HTTP_404_NOT_FOUND
339+
)
340+
await _write_todos(redis, key, todos)
341+
finally:
342+
await _release_lock(redis, lock_key)
343+
344+
return APIResponse.ok(data={"todo": updated}, message="Todo unarchived")
345+
346+
347+
@router.post("/archive-completed")
348+
async def archive_completed(
349+
user: User = Depends(manager), redis: AsyncRedis = Depends(get_async_redis)
350+
):
351+
"""Archive all completed (non-archived) todos."""
352+
key = f"todos:{user.id}"
353+
lock_key = f"todos:lock:{user.id}"
354+
if not await _acquire_lock(redis, lock_key):
355+
return APIResponse.error(
356+
message="Server busy, please retry.", code=status.HTTP_423_LOCKED
357+
)
358+
try:
359+
todos = await _read_todos(redis, key)
360+
now = datetime.now(timezone.utc).isoformat() # noqa: UP017
361+
count = 0
362+
for t in todos:
363+
if t.get("completed") and not t.get("archived"):
364+
t["archived"] = True
365+
t["archivedAt"] = now
366+
count += 1
367+
await _write_todos(redis, key, todos)
368+
finally:
369+
await _release_lock(redis, lock_key)
370+
371+
return APIResponse.ok(message=f"Archived {count} completed todos")
372+
373+
254374
@router.post("/clear-completed")
255375
async def clear_completed(
256376
user: User = Depends(manager), redis: AsyncRedis = Depends(get_async_redis)

backend/app/schemas/schemas.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,8 @@ class TodoIn(BaseModel):
458458
category: str | None = None
459459
completed: bool = False
460460
id: str | None = None # optional client-supplied id
461+
archived: bool = False
462+
archivedAt: str | None = None # noqa: N815
461463

462464

463465
class TodoUpdate(BaseModel):
@@ -467,6 +469,8 @@ class TodoUpdate(BaseModel):
467469
priority: str | None = None
468470
category: str | None = None
469471
completed: bool | None = None
472+
archived: bool | None = None
473+
archivedAt: str | None = None # noqa: N815
470474

471475

472476
class TodoOut(BaseModel):
@@ -478,3 +482,5 @@ class TodoOut(BaseModel):
478482
dueDate: str | None = None # noqa: N815
479483
priority: str = "medium"
480484
category: str | None = None
485+
archived: bool = False
486+
archivedAt: str | None = None # noqa: N815

config/mcporter.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"exa": {
4+
"baseUrl": "https://mcp.exa.ai/mcp"
5+
}
6+
},
7+
"imports": []
8+
}

0 commit comments

Comments
 (0)