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", ""), ("planted_at", ""),
("ready_at", ""), ("ready_at", ""),
("watered_by", "[]"), ("watered_by", "[]"),
("cooldown_until", ""),
("created_at", ""), ("created_at", ""),
("updated_at", ""), ("updated_at", ""),
): ):

View File

@ -1,5 +1,6 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
import time
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Form, HTTPException, Request 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 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() router = APIRouter()
@ -53,6 +73,7 @@ async def water_farm(
request: Request, username: str, data: Annotated[GameSlotForm, Form()] request: Request, username: str, data: Annotated[GameSlotForm, Form()]
): ):
viewer = require_user(request) viewer = require_user(request)
_check_farm_rate_limit(viewer["uid"])
owner = owner_by_username(username) owner = owner_by_username(username)
if not owner: if not owner:
raise HTTPException(status_code=404, detail="Farm not found") 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()] request: Request, username: str, data: Annotated[GameSlotForm, Form()]
): ):
viewer = require_user(request) viewer = require_user(request)
_check_farm_rate_limit(viewer["uid"])
owner = owner_by_username(username) owner = owner_by_username(username)
if not owner: if not owner:
raise HTTPException(status_code=404, detail="Farm not found") raise HTTPException(status_code=404, detail="Farm not found")

View File

@ -1,8 +1,9 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
import time
from typing import Annotated 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 fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from devplacepy.models import ( 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 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() router = APIRouter()
@ -58,6 +78,7 @@ async def game_leaderboard(request: Request):
async def _respond_action(request: Request, user: dict, fn, on_success=None): async def _respond_action(request: Request, user: dict, fn, on_success=None):
_check_rate_limit(user["uid"])
try: try:
result = fn() result = fn()
except GameError as exc: except GameError as exc:

View File

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

View File

@ -39,6 +39,9 @@ def plant(user: dict, slot: int, crop_key: str) -> dict:
plot = _plot_at(farm["uid"], slot) plot = _plot_at(farm["uid"], slot)
if not plot: if not plot:
raise GameError("That plot does not exist.") 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"): if plot.get("crop_key"):
raise GameError("That plot is already in use.") raise GameError("That plot is already in use.")
coins = int(farm.get("coins", 0)) coins = int(farm.get("coins", 0))
@ -85,6 +88,7 @@ def harvest(user: dict, slot: int) -> dict:
"planted_at": "", "planted_at": "",
"ready_at": "", "ready_at": "",
"watered_by": "[]", "watered_by": "[]",
"cooldown_until": _iso(now + timedelta(seconds=5)),
"updated_at": _iso(now), "updated_at": _iso(now),
}, },
["uid"], ["uid"],