Skip to content
Merged
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
286 changes: 240 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,240 @@
## Deploying

First create the necessary secrets for database and admin token access. Replace the placeholders with your actual values.
```bash
kubectl -n wynnsource-dev create secret generic wynnsource-secrets \
--from-literal=WCS_ADMIN_TOKEN='<your-admin-token>'
```
Or you can use sealed secrets for better security.

Then use this as an example to deploy the application using ArgoCD.
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: wynnsource-dev
namespace: argocd
spec:
project: default
source:
repoURL: git@github.com:WynnSource/WynnSourceServer.git
targetRevision: dev
path: deploy
destination:
server: https://kubernetes.default.svc
namespace: wynnsource-dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
```
You have to add your ingress configuration as well,
also make sure there is the `X-Real-IP` header from the ingress controller for proper client IP logging and rate limiting.

## License

`Copyright (C) <2026> <FYWinds>`

This project is licensed under the GNU Affero General Public License v3.0 (AGPL-v3) with the following [**Exception**](#exception-for-generated-client-code).

### Exception for Generated Client Code
As a special exception to the AGPL-v3, the copyright holders of this library give you permission to generate, use, distribute, and license the client libraries (SDKs) generated from this project's API specifications (e.g., OpenAPI/Swagger documents, Protocol Buffers, GraphQL schemas) under any license of your choice, including proprietary licenses. This exception does not apply to the backend logic itself.

### Reason
We've considered the implications of the AGPL-v3 and have decided to apply it to the backend logic of this project to ensure that any modifications to the server-side code are shared with the community. However, we recognize that client libraries generated from our API specifications may be used in a wide variety of applications, including minecraft mods, which may not be compatible with the AGPL-v3. By granting this exception, we aim to encourage the use of our API and allow developers to create client libraries without worrying about licensing issues, while still ensuring that contributions to the server-side code are shared with the community.
<div align="center">

# WynnSourceServer

The server component of [WynnSource](https://github.com/WynnSource) — a Wynncraft crowdsourcing mod.

[![Python 3.12+](https://img.shields.io/badge/python-3.12+-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![FastAPI](https://img.shields.io/badge/FastAPI-009688?logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/) [![Protobuf](https://img.shields.io/badge/Protocol%20Buffers-4285F4?logo=google&logoColor=white)](https://protobuf.dev/) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)

</div>

---

## Prerequisites

- Python 3.12+
- [uv](https://docs.astral.sh/uv/) — Python package manager
- [buf](https://buf.build/) — Protocol Buffers toolchain
- A PostgreSQL database
- A Redis instance

## Development Setup

```bash
# Clone with submodules
git clone --recurse-submodules https://github.com/WynnSource/WynnSourceServer.git
cd WynnSourceServer

# Generate protobuf code
buf generate --template buf.gen.yaml

# Install dependencies
uv sync

# Run database migrations
uv run alembic upgrade head

# Start the development server
uv run fastapi dev
```

### Environment Variables

| Variable | Description | Default |
|---|---|---|
| `POSTGRES_HOST` | PostgreSQL host | `localhost` |
| `POSTGRES_PORT` | PostgreSQL port | `5432` |
| `POSTGRES_USER` | PostgreSQL user | `postgres` |
| `POSTGRES_PASSWORD` | PostgreSQL password | `postgres` |
| `POSTGRES_DB` | PostgreSQL database name | `wcs_db` |
| `REDIS_HOST` | Redis host | `localhost` |
| `REDIS_PORT` | Redis port | `6379` |
| `WCS_ADMIN_TOKEN` | Admin API token | None |
| `LEVEL` | Log level | `DEBUG` |
| `BETA_ALLOWED_VERSIONS` | Comma-separated allowed mod versions | `` |

## Deploying to Kubernetes

WynnSourceServer uses [Kustomize](https://kustomize.io/) overlays for deployment and [ArgoCD](https://argo-cd.readthedocs.io/) with [Image Updater](https://argocd-image-updater.readthedocs.io/) for GitOps.

### Directory Structure

```
deploy/
base/ # Shared manifests
deployment.yaml # App deployment with health probes
service.yaml # ClusterIP service on port 8000
postgres.yaml # CNPG PostgreSQL cluster
redis.yaml # Redis instance (via Redis Operator)
migration-job.yaml # Alembic migration (ArgoCD sync hook)
kustomization.yaml
overlays/
dev/ # Dev environment (namespace: wynnsource-dev)
kustomization.yaml
prod/ # Prod environment (namespace: wynnsource, replicas: 2)
kustomization.yaml
```

### Cluster Prerequisites

The following operators must be installed in the cluster:

- [CloudNativePG](https://cloudnative-pg.io/) — PostgreSQL operator
- [Redis Operator](https://github.com/OT-CONTAINER-KIT/redis-operator) — Redis operator
- [ArgoCD Image Updater](https://argocd-image-updater.readthedocs.io/) — Automatic image updates

### Step 1: Create Secrets

Create the application secrets in the target namespace. Using [Sealed Secrets](https://sealed-secrets.netlify.app/) is recommended.

```bash
kubectl -n <namespace> create secret generic wynnsource-secrets \
--from-literal=WCS_ADMIN_TOKEN='<your-admin-token>'
```

### Step 2: Create ConfigMap

```bash
kubectl -n <namespace> create configmap wynnsource-config \
--from-literal=LEVEL='INFO' \
--from-literal=BETA_ALLOWED_VERSIONS='0.2.6, 0.2.7'
```

### Step 3: Create ArgoCD Application

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: wynnsource
namespace: argocd
annotations:
argocd-image-updater.argoproj.io/image-list: app=ghcr.io/wynnsource/wynnsource-server
argocd-image-updater.argoproj.io/app.update-strategy: newest-build
argocd-image-updater.argoproj.io/app.allow-tags: "regexp:^dev-"
argocd-image-updater.argoproj.io/app.ignore-tags: "latest,dev-latest"
spec:
project: default
source:
repoURL: https://github.com/WynnSource/WynnSourceServer.git
targetRevision: dev # or master for production
path: deploy/overlays/dev # or deploy/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: wynnsource-dev # or wynnsource
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
```

### Step 4: Configure Ingress

Add an IngressRoute (or Ingress) pointing to the `wynnsource-server` service on port 8000. Make sure the `X-Real-IP` header is forwarded from the ingress controller for proper client IP logging and rate limiting.

## Using the Schema (Protobuf)

WynnSourceServer uses [Protocol Buffers](https://protobuf.dev/) to define item encoding schemas. The `.proto` definitions live in the [`schema`](https://github.com/WynnSource/schema) submodule under `schema/proto/wynnsource/`.

### Generating Python Code

#### With buf (recommended)

Install [buf](https://buf.build/docs/installation), then from the repository root:

```bash
buf generate --template buf.gen.yaml
```

This generates Python protobuf modules into `generated/wynnsource/` based on:

```yaml
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/protocolbuffers/python:v33.5
out: generated
- remote: buf.build/protocolbuffers/pyi:v33.5
out: generated
inputs:
- directory: schema/proto
```

#### With protoc

```bash
protoc \
--proto_path=schema/proto \
--python_out=generated \
--pyi_out=generated \
schema/proto/wynnsource/**/*.proto
```

### Using Generated Code

Install the `protobuf` runtime, then import the generated modules:

```bash
pip install protobuf
```

```python
from wynnsource.item.gear_pb2 import IdentifiedGear
from wynnsource.item.consumable_pb2 import Consumable
from wynnsource.common.enums_pb2 import Rarity

# Create an item
gear = IdentifiedGear()
gear.name = "Cataclysm"

# Serialize to bytes
data = gear.SerializeToString()

# Deserialize from bytes
parsed = IdentifiedGear()
parsed.ParseFromString(data)
```

### Using as a Workspace Package

In this repository, the generated code is a [uv workspace](https://docs.astral.sh/uv/concepts/workspaces/) package called `wynnsource-proto`. After running `buf generate` and `uv sync`, you can import directly:

```python
from wynnsource.item.wynn_source_item_pb2 import WynnSourceItem
```

### Mappings

The schema repository includes JSON mapping files:

- `schema/mapping/identification.json` — Identification ID mappings
- `schema/mapping/shiny.json` — Shiny stat mappings

### Generating for Other Languages

buf supports many languages. See the [buf plugin registry](https://buf.build/plugins) for available plugins. Example for TypeScript:

```yaml
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/connectrpc/es
out: gen/ts
inputs:
- directory: schema/proto
```

## License

This project is licensed under the [GNU Affero General Public License v3.0 (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0) with the following exception.

### Exception for Generated Client Code

As a special exception, the copyright holders grant permission to generate, use, distribute, and license client libraries and SDKs produced from this project's API specifications (including OpenAPI documents, Protocol Buffer definitions, and GraphQL schemas) under any license of your choice, including proprietary licenses. This exception applies solely to the generated client code — the server-side source code remains subject to the AGPL-3.0 in full.

### Rationale

The AGPL-3.0 ensures that improvements to the server are shared with the community. However, client libraries derived from our API specifications are commonly embedded in Minecraft mods and other applications whose licenses may be incompatible with the AGPL. This exception removes that friction: developers can freely build and ship clients against our API without licensing concerns, while contributions to the backend itself continue to benefit everyone.
10 changes: 5 additions & 5 deletions app/module/raid/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

GAMBIT_COUNT = 4
GAMBIT_SEPARATOR = "|"
GAMBIT_REGION = "global"
FUZZY_WINDOW = timedelta(minutes=30)
CONSENSUS_THRESHOLD = 0.6

Expand All @@ -19,17 +20,16 @@ class GambitRotation:
def get_gambit_rotation(time: datetime, shift: int = 0) -> GambitRotation:
"""Get the daily gambit rotation window for the given timestamp.

Gambits rotate daily at EST/EDT midnight (00:00 America/New_York).
Gambits rotate daily at EST/EDT noon (12:00 America/New_York).
"""
if time.tzinfo is None:
raise ValueError("The 'time' parameter must be timezone-aware.")

local_time = time.astimezone(SERVER_TZ)

# Today's reset at midnight EST
today_reset = datetime.combine(
local_time.date(), datetime.min.time(), tzinfo=SERVER_TZ
)
# Today's reset at noon EST
today_reset = datetime.combine(local_time.date(), datetime.min.time(), tzinfo=SERVER_TZ)
today_reset += timedelta(hours=12)

if local_time < today_reset:
today_reset -= timedelta(days=1)
Expand Down
8 changes: 2 additions & 6 deletions app/module/raid/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from app.core.rate_limiter import ip_based_key_func, user_based_key_func
from app.core.router import DocedAPIRoute
from app.core.security.auth import UserDep
from app.module.pool.schema import RaidRegion
from app.schemas.enums import ApiTag
from app.schemas.response import EMPTY_RESPONSE, EmptyResponse, WCSResponse

Expand Down Expand Up @@ -47,23 +46,21 @@ async def submit_gambit_data(data: list[GambitSubmissionSchema], user: UserDep)
return EMPTY_RESPONSE


@RaidRouter.get("/gambit/{region}", summary="Get Current Gambit Consensus")
@RaidRouter.get("/gambit", summary="Get Current Gambit Consensus")
@metadata.rate_limit(limit=10, period=60, key_func=ip_based_key_func)
@metadata.cached(expire=120)
async def get_gambit_by_region(
region: RaidRegion,
session: SessionDep,
) -> WCSResponse[GambitConsensusResponse]:
"""
Get gambit consensus data for a raid region.
"""
try:
rotation = get_gambit_rotation(datetime.datetime.now(tz=datetime.UTC))
result = await get_gambit_consensus(session, region, rotation.start)
result = await get_gambit_consensus(session, rotation.start)

if result is None:
data = GambitConsensusResponse(
region=region,
rotation_start=rotation.start,
rotation_end=rotation.end,
gambits=[],
Expand All @@ -72,7 +69,6 @@ async def get_gambit_by_region(
else:
pairs, confidence = result
data = GambitConsensusResponse(
region=region,
rotation_start=rotation.start,
rotation_end=rotation.end,
gambits=[
Expand Down
4 changes: 0 additions & 4 deletions app/module/raid/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

from pydantic import BaseModel, Field, model_validator

from app.module.pool.schema import RaidRegion

from .config import GAMBIT_COUNT


Expand All @@ -13,7 +11,6 @@ class GambitEntry(BaseModel):


class GambitSubmissionSchema(BaseModel):
region: RaidRegion
client_timestamp: datetime
mod_version: str
gambits: list[GambitEntry] = Field(
Expand All @@ -37,7 +34,6 @@ class GambitConsensusEntry(BaseModel):


class GambitConsensusResponse(BaseModel):
region: str
rotation_start: datetime
rotation_end: datetime
gambits: list[GambitConsensusEntry]
Expand Down
Loading
Loading