ticket #88 attempt 1

This commit is contained in:
Typosaurus 2026-07-19 21:05:14 +00:00
parent 43c5a948e8
commit aa178a8c62
6 changed files with 53 additions and 1 deletions

1
.dpc/verify_cache.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1070,6 +1070,7 @@ def init_db():
("planted_at", ""),
("ready_at", ""),
("watered_by", "[]"),
("cooldown_until", ""),
("created_at", ""),
("updated_at", ""),
):

View File

@ -1,5 +1,6 @@
# retoor <retoor@molodetz.nl>
import time
from typing import Annotated
from fastapi import APIRouter, Form, HTTPException, Request
@ -18,6 +19,25 @@ from devplacepy.utils import (
from ._shared import game_seo, notify_farm, owner_by_username
_farm_action_rate: dict[str, list[float]] = {}
_MAX_ACTIONS_PER_MINUTE = 30
_RATE_WINDOW_SECONDS = 60
def _check_farm_rate_limit(user_uid: str) -> None:
now = time.time()
window_start = now - _RATE_WINDOW_SECONDS
times = _farm_action_rate.setdefault(user_uid, [])
times[:] = [t for t in times if t > window_start]
if len(times) >= _MAX_ACTIONS_PER_MINUTE:
raise HTTPException(
status_code=429,
detail="Rate limit exceeded. Max 30 actions per minute.",
)
times.append(now)
router = APIRouter()
@ -53,6 +73,7 @@ async def water_farm(
request: Request, username: str, data: Annotated[GameSlotForm, Form()]
):
viewer = require_user(request)
_check_farm_rate_limit(viewer["uid"])
owner = owner_by_username(username)
if not owner:
raise HTTPException(status_code=404, detail="Farm not found")
@ -77,6 +98,7 @@ async def steal_farm(
request: Request, username: str, data: Annotated[GameSlotForm, Form()]
):
viewer = require_user(request)
_check_farm_rate_limit(viewer["uid"])
owner = owner_by_username(username)
if not owner:
raise HTTPException(status_code=404, detail="Farm not found")

View File

@ -1,8 +1,9 @@
# retoor <retoor@molodetz.nl>
import time
from typing import Annotated
from fastapi import APIRouter, Form, Request
from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from devplacepy.models import (
@ -20,6 +21,25 @@ from devplacepy.utils import award_rewards, get_current_user, require_user, trac
from ._shared import game_seo, notify_farm, state_payload
_action_rate: dict[str, list[float]] = {}
_MAX_ACTIONS_PER_MINUTE = 30
_RATE_WINDOW_SECONDS = 60
def _check_rate_limit(user_uid: str) -> None:
now = time.time()
window_start = now - _RATE_WINDOW_SECONDS
times = _action_rate.setdefault(user_uid, [])
times[:] = [t for t in times if t > window_start]
if len(times) >= _MAX_ACTIONS_PER_MINUTE:
raise HTTPException(
status_code=429,
detail="Rate limit exceeded. Max 30 actions per minute.",
)
times.append(now)
router = APIRouter()
@ -58,6 +78,7 @@ async def game_leaderboard(request: Request):
async def _respond_action(request: Request, user: dict, fn, on_success=None):
_check_rate_limit(user["uid"])
try:
result = fn()
except GameError as exc:

View File

@ -60,6 +60,9 @@ CONFIRM_REQUIRED = {
"gateway_model_delete",
"email_account_delete",
"email_delete_message",
"game_plant",
"game_harvest",
"game_fertilize",
}
CONDITIONAL_CONFIRM = {

View File

@ -39,6 +39,9 @@ def plant(user: dict, slot: int, crop_key: str) -> dict:
plot = _plot_at(farm["uid"], slot)
if not plot:
raise GameError("That plot does not exist.")
cooldown = plot.get("cooldown_until", "")
if cooldown and _now() < _parse_date(cooldown):
raise GameError("This plot is on cooldown. Wait a moment before planting.")
if plot.get("crop_key"):
raise GameError("That plot is already in use.")
coins = int(farm.get("coins", 0))
@ -85,6 +88,7 @@ def harvest(user: dict, slot: int) -> dict:
"planted_at": "",
"ready_at": "",
"watered_by": "[]",
"cooldown_until": _iso(now + timedelta(seconds=5)),
"updated_at": _iso(now),
},
["uid"],