1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114 | # metergate
Per-subject usage quotas with **groups**, **per-user overrides**, an automatic
**monthly reset**, and **capability gating** — i.e. *"you've spent your media
allowance for the month, so uploads are off, but you can still type."*
Standalone, zero required dependencies, and you can drive it from **Python, a
CLI, or an HTTP API**.
*Initially developed for Noctentrion (a continuwuity-based Matrix homeserver).*
## Why this exists
Synapse enforces per-user rate limits and per-user media quotas natively.
conduwuit — and continuwuity, the fork it became — does not. It is a lightweight,
RocksDB-backed homeserver: it has a global media size cap and admin-side media
pruning, but no per-user rate-limit or media-quota subsystem, and no admin
surface to configure one. That gap has held across conduwuit's lifetime and
through continuwuity's current 0.5.x line (0.5.9 as of writing).
So the limit has to live outside the homeserver. metergate is that: a small,
generic quota engine you point at whatever per-user usage signal you already
collect — here, upload bytes — and let it decide what is still allowed.
> Packaging, tests, the HTTP API, and final refinement done with Claude.
## Install
```
pip install metergate # core: zero dependencies
pip install "metergate[postgres]" # or [redis]
```
## Concepts
- **resource** — a quantity you count: `media_bytes`, `requests`, `messages`.
- **group** — a tier with per-resource limits, e.g. `free` or `patron`.
- **override** — a per-user limit that takes precedence over the group.
- **period** — when the counter resets: `month` (default), `week`, `day`, or
`never`. Usage is bucketed by period, so the reset is automatic.
- **capability** — an action gated by one or more resources. If any of them is
over its limit, the capability is denied and nothing else is affected.
Limit resolution is override → group → unlimited.
## Use it from Python
```python
from metergate import MeterGate, SQLiteStorage
mg = MeterGate(SQLiteStorage("quota.db"), period="month")
mg.define_group("free", {"media_bytes": "500MB"})
mg.define_group("patron", {"media_bytes": "50GB"})
mg.define_capability("upload_media", ["media_bytes"]) # gated by media_bytes
mg.set_subject("@alice", group="free")
mg.set_override("@alice", "media_bytes", "1GB") # @alice gets a bump
mg.use("@alice", "media_bytes", "300MB") # record an upload
mg.allowed("@alice", "upload_media") # True — still under 1GB
mg.check("@alice", "media_bytes").remaining # bytes left this month
mg.over_quota("media_bytes") # everyone who's blown it
```
## …or the CLI
```
metergate group set free media_bytes=500MB requests=10000
metergate cap set upload_media media_bytes
metergate user set @alice --group free
metergate use @alice media_bytes 300MB
metergate check @alice --cap upload_media # prints ALLOWED/DENIED, exits 0/1
metergate over media_bytes # who's over this month
metergate top media_bytes -n 10 # biggest users
metergate export > quota.json # config out; `import` puts it back
```
Pick the backend with `--storage` (or `$METERGATE_STORAGE`):
`memory` · `sqlite:quota.db` · `postgresql://…` · `redis://…`.
## …or over HTTP, for everything that isn't Python
```
metergate serve --port 8400 --token s3cret
```
```
curl -H 'Authorization: Bearer s3cret' \
'http://localhost:8400/v1/check?subject=@alice&capability=upload_media'
# {"subject":"@alice","capability":"upload_media","allowed":true}
curl -H 'Authorization: Bearer s3cret' -X POST localhost:8400/v1/use \
-d '{"subject":"@alice","resource":"media_bytes","amount":"300MB"}'
```
Full surface: `GET /v1/check` · `GET /v1/usage` · `GET /v1/over` · `GET /v1/top`
· `GET|POST /v1/groups` · `POST /v1/subjects` · `POST /v1/use` · `GET /healthz`.
JSON in, JSON out, optional bearer token — so any platform that can hold a
conversation over HTTP can integrate it. (That was the point of the question,
yes — it has a real API now.)
## The continuwuity use-case, concretely
```python
# in your media-upload path:
if not mg.allowed(user_id, "upload_media"):
return forbidden("monthly media quota reached — text still works")
mg.use(user_id, "media_bytes", len(blob))
```
## License
MIT — see [`LICENSE`](LICENSE). There is also a [`LICENSES/`](LICENSES/) archive,
explained (with a mostly straight face) in [`LICENSES/README.md`](LICENSES/README.md).
|