feat: add moderation service for copyright detection (#347)

* feat: add moderation service for copyright detection

- add Rust moderation service using AuDD API for music recognition
- add copyright_scans table and migration for tracking scan results
- service deployed to Fly.io at plyr-moderation.fly.dev
- follows Ozone pattern: track flags without immediate enforcement

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: remove enterprise references from moderation docs

we're on the standard AuDD API, not enterprise

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub 0935cc60 e8c1fe15

+1
backend/alembic/env.py
··· 8 8 from backend.config import settings 9 9 from backend.models import ( # noqa: F401 10 10 Artist, 11 + CopyrightScan, 11 12 Track, 12 13 TrackLike, 13 14 UserSession,
+3
backend/src/backend/models/__init__.py
··· 2 2 3 3 from backend.models.album import Album 4 4 from backend.models.artist import Artist 5 + from backend.models.copyright_scan import CopyrightScan, ScanResolution 5 6 from backend.models.database import Base 6 7 from backend.models.exchange_token import ExchangeToken 7 8 from backend.models.job import Job ··· 17 18 "Album", 18 19 "Artist", 19 20 "Base", 21 + "CopyrightScan", 20 22 "ExchangeToken", 21 23 "Job", 22 24 "OAuthStateModel", 23 25 "QueueState", 26 + "ScanResolution", 24 27 "Track", 25 28 "TrackLike", 26 29 "UserPreferences",
+76
backend/src/backend/models/copyright_scan.py
··· 1 + """copyright scan model for tracking moderation results.""" 2 + 3 + from datetime import UTC, datetime 4 + from enum import Enum 5 + from typing import Any 6 + 7 + from sqlalchemy import DateTime, ForeignKey, Index, Integer, String 8 + from sqlalchemy.dialects.postgresql import JSONB 9 + from sqlalchemy.orm import Mapped, mapped_column 10 + 11 + from backend.models.database import Base 12 + 13 + 14 + class ScanResolution(str, Enum): 15 + """resolution status for a flagged scan.""" 16 + 17 + PENDING = "pending" # awaiting review 18 + DISMISSED = "dismissed" # false positive, no action needed 19 + REMOVED = "removed" # track was removed 20 + LICENSED = "licensed" # verified to be properly licensed 21 + 22 + 23 + class CopyrightScan(Base): 24 + """copyright scan result from moderation service. 25 + 26 + stores scan results from AuDD API for tracking potential 27 + copyright issues without immediate enforcement (ozone pattern). 28 + """ 29 + 30 + __tablename__ = "copyright_scans" 31 + 32 + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) 33 + 34 + # link to track 35 + track_id: Mapped[int] = mapped_column( 36 + Integer, 37 + ForeignKey("tracks.id", ondelete="CASCADE"), 38 + nullable=False, 39 + index=True, 40 + ) 41 + 42 + # scan results 43 + scanned_at: Mapped[datetime] = mapped_column( 44 + DateTime(timezone=True), 45 + default=lambda: datetime.now(UTC), 46 + nullable=False, 47 + ) 48 + is_flagged: Mapped[bool] = mapped_column(nullable=False, default=False) 49 + highest_score: Mapped[int] = mapped_column(Integer, nullable=False, default=0) 50 + 51 + # detailed match data 52 + matches: Mapped[list[dict[str, Any]]] = mapped_column( 53 + JSONB, 54 + nullable=False, 55 + default=list, 56 + server_default="[]", 57 + ) 58 + raw_response: Mapped[dict[str, Any]] = mapped_column( 59 + JSONB, 60 + nullable=False, 61 + default=dict, 62 + server_default="{}", 63 + ) 64 + 65 + # review tracking (for later human review) 66 + resolution: Mapped[str | None] = mapped_column(String, nullable=True) 67 + reviewed_at: Mapped[datetime | None] = mapped_column( 68 + DateTime(timezone=True), nullable=True 69 + ) 70 + reviewed_by: Mapped[str | None] = mapped_column(String, nullable=True) # DID 71 + review_notes: Mapped[str | None] = mapped_column(String, nullable=True) 72 + 73 + __table_args__ = ( 74 + Index("idx_copyright_scans_flagged", "is_flagged"), 75 + Index("idx_copyright_scans_scanned_at", "scanned_at"), 76 + )
+238
docs/moderation/copyright-detection.md
··· 1 + # copyright detection 2 + 3 + technical documentation for the copyright scanning system. 4 + 5 + ## how it works 6 + 7 + ``` 8 + upload completes 9 + 10 + 11 + ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ 12 + │ backend │────▶│ AuDD API │────▶│ database │ 13 + │ (background) │ │ │ │ (copyright_ │ 14 + │ │◀────│ │ │ flags) │ 15 + └──────────────┘ └─────────────┘ └──────────────┘ 16 + 17 + 18 + music recognition 19 + against licensed 20 + database 21 + ``` 22 + 23 + 1. track upload completes, file stored in R2 24 + 2. background job sends R2 URL to AuDD API 25 + 3. AuDD scans file against their music database 26 + 4. results stored in `copyright_flags` table 27 + 5. admin can query flagged tracks 28 + 29 + ## AuDD API 30 + 31 + [AuDD](https://audd.io/) is a music recognition service similar to Shazam. their API scans audio and returns matched songs with confidence scores. 32 + 33 + ### request 34 + 35 + ```bash 36 + curl -X POST https://api.audd.io/ \ 37 + -F "api_token=YOUR_TOKEN" \ 38 + -F "url=https://your-r2-bucket.com/audio/abc123.mp3" \ 39 + -F "accurate_offsets=1" 40 + ``` 41 + 42 + ### response 43 + 44 + ```json 45 + { 46 + "status": "success", 47 + "result": [ 48 + { 49 + "offset": 0, 50 + "songs": [ 51 + { 52 + "artist": "Artist Name", 53 + "title": "Song Title", 54 + "album": "Album Name", 55 + "score": 85, 56 + "isrc": "USRC12345678", 57 + "timecode": "01:30" 58 + } 59 + ] 60 + }, 61 + { 62 + "offset": 180000, 63 + "songs": [ 64 + { 65 + "artist": "Another Artist", 66 + "title": "Another Song", 67 + "score": 72 68 + } 69 + ] 70 + } 71 + ] 72 + } 73 + ``` 74 + 75 + ### pricing 76 + 77 + - $2 per 1000 requests 78 + - 1 request = 12 seconds of audio 79 + - 5-minute track ≈ 25 requests ≈ $0.05 80 + - first 300 requests free 81 + 82 + ## database schema 83 + 84 + ```sql 85 + CREATE TABLE copyright_flags ( 86 + id SERIAL PRIMARY KEY, 87 + track_id INTEGER NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, 88 + 89 + -- status: pending | scanning | clear | flagged | error 90 + status VARCHAR(20) NOT NULL DEFAULT 'pending', 91 + 92 + -- AuDD data 93 + audd_response JSONB, -- full API response 94 + matched_tracks JSONB, -- [{artist, title, score, isrc}] 95 + confidence_score INTEGER, -- highest match score (0-100) 96 + 97 + -- timestamps 98 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 99 + scanned_at TIMESTAMPTZ, 100 + resolved_at TIMESTAMPTZ, 101 + 102 + -- metadata 103 + scanned_by VARCHAR(50), -- 'audd', 'manual' 104 + error_message TEXT, 105 + 106 + UNIQUE(track_id) 107 + ); 108 + ``` 109 + 110 + ### status meanings 111 + 112 + | status | description | 113 + |--------|-------------| 114 + | `pending` | awaiting scan | 115 + | `scanning` | scan in progress | 116 + | `clear` | no matches above threshold | 117 + | `flagged` | matches found above threshold | 118 + | `error` | scan failed | 119 + 120 + ## configuration 121 + 122 + ```bash 123 + # required 124 + AUDD_API_TOKEN=your_token_here 125 + 126 + # optional (have defaults) 127 + AUDD_API_URL=https://api.audd.io/ 128 + AUDD_TIMEOUT_SECONDS=300 129 + 130 + # scan behavior 131 + MODERATION_SCORE_THRESHOLD=70 # flag if score >= this 132 + MODERATION_AUTO_SCAN=true # scan on upload 133 + MODERATION_ENABLED=true # master switch 134 + ``` 135 + 136 + ## interpreting results 137 + 138 + ### confidence scores 139 + 140 + AuDD returns a score (0-100) for each match: 141 + 142 + | score | meaning | 143 + |-------|---------| 144 + | 90-100 | very high confidence, almost certainly a match | 145 + | 70-89 | high confidence, likely a match | 146 + | 50-69 | moderate confidence, may be similar but not exact | 147 + | < 50 | low confidence, probably not a match | 148 + 149 + default threshold is 70. tracks with any match >= 70 are flagged. 150 + 151 + ### false positives 152 + 153 + common causes: 154 + - generic beats/samples used in multiple songs 155 + - covers or remixes (legal gray area) 156 + - similar chord progressions 157 + - audio artifacts matching by coincidence 158 + 159 + this is why we flag but don't enforce. human review is needed. 160 + 161 + ### ISRC codes 162 + 163 + [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code) - unique identifier for recordings. when present, this is strong evidence of a specific recording match (not just similar audio). 164 + 165 + ## admin queries 166 + 167 + ### list all flagged tracks 168 + 169 + ```sql 170 + SELECT t.id, t.title, a.handle, cf.confidence_score, cf.matched_tracks 171 + FROM copyright_flags cf 172 + JOIN tracks t ON t.id = cf.track_id 173 + JOIN artists a ON a.did = t.artist_did 174 + WHERE cf.status = 'flagged' 175 + ORDER BY cf.confidence_score DESC; 176 + ``` 177 + 178 + ### scan statistics 179 + 180 + ```sql 181 + SELECT 182 + status, 183 + COUNT(*) as count, 184 + AVG(confidence_score) as avg_score 185 + FROM copyright_flags 186 + GROUP BY status; 187 + ``` 188 + 189 + ### tracks pending scan 190 + 191 + ```sql 192 + SELECT t.id, t.title, t.created_at 193 + FROM tracks t 194 + LEFT JOIN copyright_flags cf ON cf.track_id = t.id 195 + WHERE cf.id IS NULL OR cf.status = 'pending' 196 + ORDER BY t.created_at DESC; 197 + ``` 198 + 199 + ## future considerations 200 + 201 + ### batch scanning existing tracks 202 + 203 + ```python 204 + # scan all tracks that haven't been scanned 205 + async def backfill_scans(): 206 + async with get_session() as session: 207 + unscanned = await session.execute( 208 + select(Track) 209 + .outerjoin(CopyrightFlag) 210 + .where(CopyrightFlag.id.is_(None)) 211 + ) 212 + for track in unscanned.scalars(): 213 + await scan_track_for_copyright(track.id, track.r2_url) 214 + ``` 215 + 216 + ### ATProto labels 217 + 218 + future integration could publish copyright status as ATProto labels: 219 + 220 + ```json 221 + { 222 + "$type": "com.atproto.label.defs#label", 223 + "src": "did:plc:plyr-moderation", 224 + "uri": "at://did:plc:artist/fm.plyr.track/abc123", 225 + "val": "copyright-flagged", 226 + "cts": "2025-11-24T12:00:00Z" 227 + } 228 + ``` 229 + 230 + this would allow other apps in the ATProto ecosystem to see and act on our moderation signals. 231 + 232 + ### user-facing appeals 233 + 234 + eventual flow: 235 + 1. artist sees flag on their track 236 + 2. artist submits dispute with evidence (license, original work proof) 237 + 3. admin reviews dispute 238 + 4. flag status updated to `resolved` or `confirmed`
+124
docs/moderation/overview.md
··· 1 + # moderation on plyr.fm 2 + 3 + ## philosophy 4 + 5 + plyr.fm's approach to moderation is inspired by [Bluesky's stackable moderation architecture](https://bsky.social/about/blog/03-12-2024-stackable-moderation). the core insight: **moderation is information, not enforcement**. 6 + 7 + rather than building systems that automatically remove content, we build systems that produce *signals* about content. what happens with those signals is a separate decision - made by humans, configurable per context, and transparent to all parties. 8 + 9 + ## why this matters for a music platform 10 + 11 + music platforms face unique moderation challenges: 12 + 13 + 1. **copyright is murky** - fair use, samples, remixes, covers all exist in gray areas 14 + 2. **false positives are costly** - removing an original track because it "sounds like" something else destroys trust 15 + 3. **enforcement has legal weight** - DMCA takedowns have real consequences for creators 16 + 4. **context matters** - a DJ mix is different from a stolen track 17 + 18 + a system that auto-deletes on detection would be: 19 + - legally risky (wrongful takedowns) 20 + - user-hostile (no recourse) 21 + - technically brittle (AI isn't perfect) 22 + 23 + instead, we produce signals and defer enforcement to humans who can apply judgment. 24 + 25 + ## the bluesky model 26 + 27 + from [Bluesky's march 2024 blog post](https://bsky.social/about/blog/03-12-2024-stackable-moderation): 28 + 29 + > "In designing these moderation services, Bluesky operated by three principles: 30 + > 1. **Simple and Powerful**: Give users a pleasant default experience, with customization options under the hood 31 + > 2. **User Choice**: Empower users and communities to develop their own moderation systems 32 + > 3. **Openness**: Create an open system that increases trust in the governance of our digital spaces" 33 + 34 + their system uses **labels** - metadata attached to content that different layers can interpret differently. a label might mean "hide this" in one context and "show with warning" in another. 35 + 36 + [Ozone](https://github.com/bluesky-social/ozone), their open-source moderation tool, lets teams collaboratively review and label content. labels flow through the network, and clients decide how to render them. 37 + 38 + ## how plyr.fm applies these principles 39 + 40 + ### labels, not deletions 41 + 42 + we scan uploaded tracks for potential copyright matches using [AuDD](https://audd.io/), a music recognition API. the scan produces: 43 + 44 + - match confidence (0-100) 45 + - matched song metadata (artist, title, ISRC) 46 + - timestamp offsets (where in the file matches occur) 47 + 48 + this data is stored as a **flag** - a label attached to the track. the flag doesn't delete anything. it's information that enables informed decisions. 49 + 50 + ### stackable architecture 51 + 52 + copyright detection is one module in what could become a larger moderation ecosystem: 53 + 54 + ``` 55 + ┌─────────────────────────────────────────────────┐ 56 + │ enforcement layer │ 57 + │ (admin review, user settings, policies) │ 58 + └─────────────────────┬───────────────────────────┘ 59 + │ consumes signals 60 + ┌───────────────┼───────────────┐ 61 + │ │ │ 62 + ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ 63 + │ copyright │ │ quality │ │ reports │ 64 + │ scanner │ │ checker │ │ service │ 65 + └───────────┘ └───────────┘ └───────────┘ 66 + AuDD (future) (future) 67 + ``` 68 + 69 + each service produces labels independently. enforcement is a separate concern. 70 + 71 + ### transparency and audit trails 72 + 73 + every scan stores: 74 + - full API response (for disputes) 75 + - confidence scores (not just binary flags) 76 + - timestamps (when scanned) 77 + - scanner identifier (which system made the call) 78 + 79 + if someone disputes a flag, we can show exactly what matched and why. 80 + 81 + ### sensible defaults, user choice later 82 + 83 + current state: 84 + - scans run automatically on upload 85 + - results visible to admins only 86 + - no automatic enforcement 87 + 88 + future possibilities: 89 + - artists see their own copyright status 90 + - artists can contest flags 91 + - configurable thresholds per user/context 92 + - integration with ATProto labeling 93 + 94 + ## what we're building 95 + 96 + ### phase 1: detection infrastructure 97 + 98 + - `copyright_flags` table storing scan results 99 + - AuDD integration for music recognition 100 + - background job triggered on upload 101 + - admin endpoints to query flagged tracks 102 + 103 + ### phase 2: visibility 104 + 105 + - admin dashboard for reviewing flags 106 + - stats and trends 107 + - manual rescan capability 108 + 109 + ### phase 3: user-facing (future) 110 + 111 + - artists see flags on their own tracks 112 + - dispute/appeal workflow 113 + - notification on flag status change 114 + 115 + ## references 116 + 117 + - [Bluesky's Stackable Approach to Moderation](https://bsky.social/about/blog/03-12-2024-stackable-moderation) - the blog post that inspired this architecture 118 + - [Ozone GitHub](https://github.com/bluesky-social/ozone) - Bluesky's open-source moderation tool 119 + - [AuDD API](https://docs.audd.io/) - music recognition service we use for copyright detection 120 + - [AT Protocol](https://atproto.com/) - the protocol plyr.fm is built on 121 + 122 + ## related documentation 123 + 124 + - [copyright-detection.md](./copyright-detection.md) - technical implementation details
+1
justfile
··· 1 1 # plyr.fm dev workflows 2 2 mod frontend 3 3 mod transcoder 4 + mod moderation 4 5 mod backend 5 6 6 7
+1
moderation/.gitignore
··· 1 + /target/
+1644
moderation/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "aho-corasick" 7 + version = "1.1.4" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 + dependencies = [ 11 + "memchr", 12 + ] 13 + 14 + [[package]] 15 + name = "anyhow" 16 + version = "1.0.100" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 19 + 20 + [[package]] 21 + name = "async-trait" 22 + version = "0.1.89" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 25 + dependencies = [ 26 + "proc-macro2", 27 + "quote", 28 + "syn", 29 + ] 30 + 31 + [[package]] 32 + name = "atomic-waker" 33 + version = "1.1.2" 34 + source = "registry+https://github.com/rust-lang/crates.io-index" 35 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 36 + 37 + [[package]] 38 + name = "axum" 39 + version = "0.7.9" 40 + source = "registry+https://github.com/rust-lang/crates.io-index" 41 + checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" 42 + dependencies = [ 43 + "async-trait", 44 + "axum-core", 45 + "axum-macros", 46 + "bytes", 47 + "futures-util", 48 + "http", 49 + "http-body", 50 + "http-body-util", 51 + "hyper", 52 + "hyper-util", 53 + "itoa", 54 + "matchit", 55 + "memchr", 56 + "mime", 57 + "percent-encoding", 58 + "pin-project-lite", 59 + "rustversion", 60 + "serde", 61 + "serde_json", 62 + "serde_path_to_error", 63 + "serde_urlencoded", 64 + "sync_wrapper", 65 + "tokio", 66 + "tower", 67 + "tower-layer", 68 + "tower-service", 69 + "tracing", 70 + ] 71 + 72 + [[package]] 73 + name = "axum-core" 74 + version = "0.4.5" 75 + source = "registry+https://github.com/rust-lang/crates.io-index" 76 + checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" 77 + dependencies = [ 78 + "async-trait", 79 + "bytes", 80 + "futures-util", 81 + "http", 82 + "http-body", 83 + "http-body-util", 84 + "mime", 85 + "pin-project-lite", 86 + "rustversion", 87 + "sync_wrapper", 88 + "tower-layer", 89 + "tower-service", 90 + "tracing", 91 + ] 92 + 93 + [[package]] 94 + name = "axum-macros" 95 + version = "0.4.2" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" 98 + dependencies = [ 99 + "proc-macro2", 100 + "quote", 101 + "syn", 102 + ] 103 + 104 + [[package]] 105 + name = "base64" 106 + version = "0.22.1" 107 + source = "registry+https://github.com/rust-lang/crates.io-index" 108 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 109 + 110 + [[package]] 111 + name = "bitflags" 112 + version = "2.10.0" 113 + source = "registry+https://github.com/rust-lang/crates.io-index" 114 + checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 115 + 116 + [[package]] 117 + name = "bumpalo" 118 + version = "3.19.0" 119 + source = "registry+https://github.com/rust-lang/crates.io-index" 120 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 121 + 122 + [[package]] 123 + name = "bytes" 124 + version = "1.11.0" 125 + source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 127 + 128 + [[package]] 129 + name = "cc" 130 + version = "1.2.47" 131 + source = "registry+https://github.com/rust-lang/crates.io-index" 132 + checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" 133 + dependencies = [ 134 + "find-msvc-tools", 135 + "shlex", 136 + ] 137 + 138 + [[package]] 139 + name = "cfg-if" 140 + version = "1.0.4" 141 + source = "registry+https://github.com/rust-lang/crates.io-index" 142 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 143 + 144 + [[package]] 145 + name = "cfg_aliases" 146 + version = "0.2.1" 147 + source = "registry+https://github.com/rust-lang/crates.io-index" 148 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 149 + 150 + [[package]] 151 + name = "displaydoc" 152 + version = "0.2.5" 153 + source = "registry+https://github.com/rust-lang/crates.io-index" 154 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 155 + dependencies = [ 156 + "proc-macro2", 157 + "quote", 158 + "syn", 159 + ] 160 + 161 + [[package]] 162 + name = "find-msvc-tools" 163 + version = "0.1.5" 164 + source = "registry+https://github.com/rust-lang/crates.io-index" 165 + checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 166 + 167 + [[package]] 168 + name = "form_urlencoded" 169 + version = "1.2.2" 170 + source = "registry+https://github.com/rust-lang/crates.io-index" 171 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 172 + dependencies = [ 173 + "percent-encoding", 174 + ] 175 + 176 + [[package]] 177 + name = "futures-channel" 178 + version = "0.3.31" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 181 + dependencies = [ 182 + "futures-core", 183 + ] 184 + 185 + [[package]] 186 + name = "futures-core" 187 + version = "0.3.31" 188 + source = "registry+https://github.com/rust-lang/crates.io-index" 189 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 190 + 191 + [[package]] 192 + name = "futures-task" 193 + version = "0.3.31" 194 + source = "registry+https://github.com/rust-lang/crates.io-index" 195 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 196 + 197 + [[package]] 198 + name = "futures-util" 199 + version = "0.3.31" 200 + source = "registry+https://github.com/rust-lang/crates.io-index" 201 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 202 + dependencies = [ 203 + "futures-core", 204 + "futures-task", 205 + "pin-project-lite", 206 + "pin-utils", 207 + ] 208 + 209 + [[package]] 210 + name = "getrandom" 211 + version = "0.2.16" 212 + source = "registry+https://github.com/rust-lang/crates.io-index" 213 + checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 214 + dependencies = [ 215 + "cfg-if", 216 + "js-sys", 217 + "libc", 218 + "wasi", 219 + "wasm-bindgen", 220 + ] 221 + 222 + [[package]] 223 + name = "getrandom" 224 + version = "0.3.4" 225 + source = "registry+https://github.com/rust-lang/crates.io-index" 226 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 227 + dependencies = [ 228 + "cfg-if", 229 + "js-sys", 230 + "libc", 231 + "r-efi", 232 + "wasip2", 233 + "wasm-bindgen", 234 + ] 235 + 236 + [[package]] 237 + name = "http" 238 + version = "1.4.0" 239 + source = "registry+https://github.com/rust-lang/crates.io-index" 240 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 241 + dependencies = [ 242 + "bytes", 243 + "itoa", 244 + ] 245 + 246 + [[package]] 247 + name = "http-body" 248 + version = "1.0.1" 249 + source = "registry+https://github.com/rust-lang/crates.io-index" 250 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 251 + dependencies = [ 252 + "bytes", 253 + "http", 254 + ] 255 + 256 + [[package]] 257 + name = "http-body-util" 258 + version = "0.1.3" 259 + source = "registry+https://github.com/rust-lang/crates.io-index" 260 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 261 + dependencies = [ 262 + "bytes", 263 + "futures-core", 264 + "http", 265 + "http-body", 266 + "pin-project-lite", 267 + ] 268 + 269 + [[package]] 270 + name = "httparse" 271 + version = "1.10.1" 272 + source = "registry+https://github.com/rust-lang/crates.io-index" 273 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 274 + 275 + [[package]] 276 + name = "httpdate" 277 + version = "1.0.3" 278 + source = "registry+https://github.com/rust-lang/crates.io-index" 279 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 280 + 281 + [[package]] 282 + name = "hyper" 283 + version = "1.8.1" 284 + source = "registry+https://github.com/rust-lang/crates.io-index" 285 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 286 + dependencies = [ 287 + "atomic-waker", 288 + "bytes", 289 + "futures-channel", 290 + "futures-core", 291 + "http", 292 + "http-body", 293 + "httparse", 294 + "httpdate", 295 + "itoa", 296 + "pin-project-lite", 297 + "pin-utils", 298 + "smallvec", 299 + "tokio", 300 + "want", 301 + ] 302 + 303 + [[package]] 304 + name = "hyper-rustls" 305 + version = "0.27.7" 306 + source = "registry+https://github.com/rust-lang/crates.io-index" 307 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 308 + dependencies = [ 309 + "http", 310 + "hyper", 311 + "hyper-util", 312 + "rustls", 313 + "rustls-pki-types", 314 + "tokio", 315 + "tokio-rustls", 316 + "tower-service", 317 + "webpki-roots", 318 + ] 319 + 320 + [[package]] 321 + name = "hyper-util" 322 + version = "0.1.18" 323 + source = "registry+https://github.com/rust-lang/crates.io-index" 324 + checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" 325 + dependencies = [ 326 + "base64", 327 + "bytes", 328 + "futures-channel", 329 + "futures-core", 330 + "futures-util", 331 + "http", 332 + "http-body", 333 + "hyper", 334 + "ipnet", 335 + "libc", 336 + "percent-encoding", 337 + "pin-project-lite", 338 + "socket2", 339 + "tokio", 340 + "tower-service", 341 + "tracing", 342 + ] 343 + 344 + [[package]] 345 + name = "icu_collections" 346 + version = "2.1.1" 347 + source = "registry+https://github.com/rust-lang/crates.io-index" 348 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 349 + dependencies = [ 350 + "displaydoc", 351 + "potential_utf", 352 + "yoke", 353 + "zerofrom", 354 + "zerovec", 355 + ] 356 + 357 + [[package]] 358 + name = "icu_locale_core" 359 + version = "2.1.1" 360 + source = "registry+https://github.com/rust-lang/crates.io-index" 361 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 362 + dependencies = [ 363 + "displaydoc", 364 + "litemap", 365 + "tinystr", 366 + "writeable", 367 + "zerovec", 368 + ] 369 + 370 + [[package]] 371 + name = "icu_normalizer" 372 + version = "2.1.1" 373 + source = "registry+https://github.com/rust-lang/crates.io-index" 374 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 375 + dependencies = [ 376 + "icu_collections", 377 + "icu_normalizer_data", 378 + "icu_properties", 379 + "icu_provider", 380 + "smallvec", 381 + "zerovec", 382 + ] 383 + 384 + [[package]] 385 + name = "icu_normalizer_data" 386 + version = "2.1.1" 387 + source = "registry+https://github.com/rust-lang/crates.io-index" 388 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 389 + 390 + [[package]] 391 + name = "icu_properties" 392 + version = "2.1.1" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" 395 + dependencies = [ 396 + "icu_collections", 397 + "icu_locale_core", 398 + "icu_properties_data", 399 + "icu_provider", 400 + "zerotrie", 401 + "zerovec", 402 + ] 403 + 404 + [[package]] 405 + name = "icu_properties_data" 406 + version = "2.1.1" 407 + source = "registry+https://github.com/rust-lang/crates.io-index" 408 + checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" 409 + 410 + [[package]] 411 + name = "icu_provider" 412 + version = "2.1.1" 413 + source = "registry+https://github.com/rust-lang/crates.io-index" 414 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 415 + dependencies = [ 416 + "displaydoc", 417 + "icu_locale_core", 418 + "writeable", 419 + "yoke", 420 + "zerofrom", 421 + "zerotrie", 422 + "zerovec", 423 + ] 424 + 425 + [[package]] 426 + name = "idna" 427 + version = "1.1.0" 428 + source = "registry+https://github.com/rust-lang/crates.io-index" 429 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 430 + dependencies = [ 431 + "idna_adapter", 432 + "smallvec", 433 + "utf8_iter", 434 + ] 435 + 436 + [[package]] 437 + name = "idna_adapter" 438 + version = "1.2.1" 439 + source = "registry+https://github.com/rust-lang/crates.io-index" 440 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 441 + dependencies = [ 442 + "icu_normalizer", 443 + "icu_properties", 444 + ] 445 + 446 + [[package]] 447 + name = "ipnet" 448 + version = "2.11.0" 449 + source = "registry+https://github.com/rust-lang/crates.io-index" 450 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 451 + 452 + [[package]] 453 + name = "iri-string" 454 + version = "0.7.9" 455 + source = "registry+https://github.com/rust-lang/crates.io-index" 456 + checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" 457 + dependencies = [ 458 + "memchr", 459 + "serde", 460 + ] 461 + 462 + [[package]] 463 + name = "itoa" 464 + version = "1.0.15" 465 + source = "registry+https://github.com/rust-lang/crates.io-index" 466 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 467 + 468 + [[package]] 469 + name = "js-sys" 470 + version = "0.3.82" 471 + source = "registry+https://github.com/rust-lang/crates.io-index" 472 + checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" 473 + dependencies = [ 474 + "once_cell", 475 + "wasm-bindgen", 476 + ] 477 + 478 + [[package]] 479 + name = "lazy_static" 480 + version = "1.5.0" 481 + source = "registry+https://github.com/rust-lang/crates.io-index" 482 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 483 + 484 + [[package]] 485 + name = "libc" 486 + version = "0.2.177" 487 + source = "registry+https://github.com/rust-lang/crates.io-index" 488 + checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 489 + 490 + [[package]] 491 + name = "litemap" 492 + version = "0.8.1" 493 + source = "registry+https://github.com/rust-lang/crates.io-index" 494 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 495 + 496 + [[package]] 497 + name = "log" 498 + version = "0.4.28" 499 + source = "registry+https://github.com/rust-lang/crates.io-index" 500 + checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 501 + 502 + [[package]] 503 + name = "lru-slab" 504 + version = "0.1.2" 505 + source = "registry+https://github.com/rust-lang/crates.io-index" 506 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 507 + 508 + [[package]] 509 + name = "matchers" 510 + version = "0.2.0" 511 + source = "registry+https://github.com/rust-lang/crates.io-index" 512 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 513 + dependencies = [ 514 + "regex-automata", 515 + ] 516 + 517 + [[package]] 518 + name = "matchit" 519 + version = "0.7.3" 520 + source = "registry+https://github.com/rust-lang/crates.io-index" 521 + checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 522 + 523 + [[package]] 524 + name = "memchr" 525 + version = "2.7.6" 526 + source = "registry+https://github.com/rust-lang/crates.io-index" 527 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 528 + 529 + [[package]] 530 + name = "mime" 531 + version = "0.3.17" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 534 + 535 + [[package]] 536 + name = "mio" 537 + version = "1.1.0" 538 + source = "registry+https://github.com/rust-lang/crates.io-index" 539 + checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 540 + dependencies = [ 541 + "libc", 542 + "wasi", 543 + "windows-sys 0.61.2", 544 + ] 545 + 546 + [[package]] 547 + name = "moderation" 548 + version = "0.1.0" 549 + dependencies = [ 550 + "anyhow", 551 + "axum", 552 + "reqwest", 553 + "serde", 554 + "serde_json", 555 + "thiserror 1.0.69", 556 + "tokio", 557 + "tracing", 558 + "tracing-subscriber", 559 + ] 560 + 561 + [[package]] 562 + name = "nu-ansi-term" 563 + version = "0.50.3" 564 + source = "registry+https://github.com/rust-lang/crates.io-index" 565 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 566 + dependencies = [ 567 + "windows-sys 0.61.2", 568 + ] 569 + 570 + [[package]] 571 + name = "once_cell" 572 + version = "1.21.3" 573 + source = "registry+https://github.com/rust-lang/crates.io-index" 574 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 575 + 576 + [[package]] 577 + name = "percent-encoding" 578 + version = "2.3.2" 579 + source = "registry+https://github.com/rust-lang/crates.io-index" 580 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 581 + 582 + [[package]] 583 + name = "pin-project-lite" 584 + version = "0.2.16" 585 + source = "registry+https://github.com/rust-lang/crates.io-index" 586 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 587 + 588 + [[package]] 589 + name = "pin-utils" 590 + version = "0.1.0" 591 + source = "registry+https://github.com/rust-lang/crates.io-index" 592 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 593 + 594 + [[package]] 595 + name = "potential_utf" 596 + version = "0.1.4" 597 + source = "registry+https://github.com/rust-lang/crates.io-index" 598 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 599 + dependencies = [ 600 + "zerovec", 601 + ] 602 + 603 + [[package]] 604 + name = "ppv-lite86" 605 + version = "0.2.21" 606 + source = "registry+https://github.com/rust-lang/crates.io-index" 607 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 608 + dependencies = [ 609 + "zerocopy", 610 + ] 611 + 612 + [[package]] 613 + name = "proc-macro2" 614 + version = "1.0.103" 615 + source = "registry+https://github.com/rust-lang/crates.io-index" 616 + checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 617 + dependencies = [ 618 + "unicode-ident", 619 + ] 620 + 621 + [[package]] 622 + name = "quinn" 623 + version = "0.11.9" 624 + source = "registry+https://github.com/rust-lang/crates.io-index" 625 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 626 + dependencies = [ 627 + "bytes", 628 + "cfg_aliases", 629 + "pin-project-lite", 630 + "quinn-proto", 631 + "quinn-udp", 632 + "rustc-hash", 633 + "rustls", 634 + "socket2", 635 + "thiserror 2.0.17", 636 + "tokio", 637 + "tracing", 638 + "web-time", 639 + ] 640 + 641 + [[package]] 642 + name = "quinn-proto" 643 + version = "0.11.13" 644 + source = "registry+https://github.com/rust-lang/crates.io-index" 645 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 646 + dependencies = [ 647 + "bytes", 648 + "getrandom 0.3.4", 649 + "lru-slab", 650 + "rand", 651 + "ring", 652 + "rustc-hash", 653 + "rustls", 654 + "rustls-pki-types", 655 + "slab", 656 + "thiserror 2.0.17", 657 + "tinyvec", 658 + "tracing", 659 + "web-time", 660 + ] 661 + 662 + [[package]] 663 + name = "quinn-udp" 664 + version = "0.5.14" 665 + source = "registry+https://github.com/rust-lang/crates.io-index" 666 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 667 + dependencies = [ 668 + "cfg_aliases", 669 + "libc", 670 + "once_cell", 671 + "socket2", 672 + "tracing", 673 + "windows-sys 0.60.2", 674 + ] 675 + 676 + [[package]] 677 + name = "quote" 678 + version = "1.0.42" 679 + source = "registry+https://github.com/rust-lang/crates.io-index" 680 + checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 681 + dependencies = [ 682 + "proc-macro2", 683 + ] 684 + 685 + [[package]] 686 + name = "r-efi" 687 + version = "5.3.0" 688 + source = "registry+https://github.com/rust-lang/crates.io-index" 689 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 690 + 691 + [[package]] 692 + name = "rand" 693 + version = "0.9.2" 694 + source = "registry+https://github.com/rust-lang/crates.io-index" 695 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 696 + dependencies = [ 697 + "rand_chacha", 698 + "rand_core", 699 + ] 700 + 701 + [[package]] 702 + name = "rand_chacha" 703 + version = "0.9.0" 704 + source = "registry+https://github.com/rust-lang/crates.io-index" 705 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 706 + dependencies = [ 707 + "ppv-lite86", 708 + "rand_core", 709 + ] 710 + 711 + [[package]] 712 + name = "rand_core" 713 + version = "0.9.3" 714 + source = "registry+https://github.com/rust-lang/crates.io-index" 715 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 716 + dependencies = [ 717 + "getrandom 0.3.4", 718 + ] 719 + 720 + [[package]] 721 + name = "regex-automata" 722 + version = "0.4.13" 723 + source = "registry+https://github.com/rust-lang/crates.io-index" 724 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 725 + dependencies = [ 726 + "aho-corasick", 727 + "memchr", 728 + "regex-syntax", 729 + ] 730 + 731 + [[package]] 732 + name = "regex-syntax" 733 + version = "0.8.8" 734 + source = "registry+https://github.com/rust-lang/crates.io-index" 735 + checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 736 + 737 + [[package]] 738 + name = "reqwest" 739 + version = "0.12.24" 740 + source = "registry+https://github.com/rust-lang/crates.io-index" 741 + checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" 742 + dependencies = [ 743 + "base64", 744 + "bytes", 745 + "futures-core", 746 + "http", 747 + "http-body", 748 + "http-body-util", 749 + "hyper", 750 + "hyper-rustls", 751 + "hyper-util", 752 + "js-sys", 753 + "log", 754 + "percent-encoding", 755 + "pin-project-lite", 756 + "quinn", 757 + "rustls", 758 + "rustls-pki-types", 759 + "serde", 760 + "serde_json", 761 + "serde_urlencoded", 762 + "sync_wrapper", 763 + "tokio", 764 + "tokio-rustls", 765 + "tower", 766 + "tower-http", 767 + "tower-service", 768 + "url", 769 + "wasm-bindgen", 770 + "wasm-bindgen-futures", 771 + "web-sys", 772 + "webpki-roots", 773 + ] 774 + 775 + [[package]] 776 + name = "ring" 777 + version = "0.17.14" 778 + source = "registry+https://github.com/rust-lang/crates.io-index" 779 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 780 + dependencies = [ 781 + "cc", 782 + "cfg-if", 783 + "getrandom 0.2.16", 784 + "libc", 785 + "untrusted", 786 + "windows-sys 0.52.0", 787 + ] 788 + 789 + [[package]] 790 + name = "rustc-hash" 791 + version = "2.1.1" 792 + source = "registry+https://github.com/rust-lang/crates.io-index" 793 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 794 + 795 + [[package]] 796 + name = "rustls" 797 + version = "0.23.35" 798 + source = "registry+https://github.com/rust-lang/crates.io-index" 799 + checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 800 + dependencies = [ 801 + "once_cell", 802 + "ring", 803 + "rustls-pki-types", 804 + "rustls-webpki", 805 + "subtle", 806 + "zeroize", 807 + ] 808 + 809 + [[package]] 810 + name = "rustls-pki-types" 811 + version = "1.13.0" 812 + source = "registry+https://github.com/rust-lang/crates.io-index" 813 + checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" 814 + dependencies = [ 815 + "web-time", 816 + "zeroize", 817 + ] 818 + 819 + [[package]] 820 + name = "rustls-webpki" 821 + version = "0.103.8" 822 + source = "registry+https://github.com/rust-lang/crates.io-index" 823 + checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" 824 + dependencies = [ 825 + "ring", 826 + "rustls-pki-types", 827 + "untrusted", 828 + ] 829 + 830 + [[package]] 831 + name = "rustversion" 832 + version = "1.0.22" 833 + source = "registry+https://github.com/rust-lang/crates.io-index" 834 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 835 + 836 + [[package]] 837 + name = "ryu" 838 + version = "1.0.20" 839 + source = "registry+https://github.com/rust-lang/crates.io-index" 840 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 841 + 842 + [[package]] 843 + name = "serde" 844 + version = "1.0.228" 845 + source = "registry+https://github.com/rust-lang/crates.io-index" 846 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 847 + dependencies = [ 848 + "serde_core", 849 + "serde_derive", 850 + ] 851 + 852 + [[package]] 853 + name = "serde_core" 854 + version = "1.0.228" 855 + source = "registry+https://github.com/rust-lang/crates.io-index" 856 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 857 + dependencies = [ 858 + "serde_derive", 859 + ] 860 + 861 + [[package]] 862 + name = "serde_derive" 863 + version = "1.0.228" 864 + source = "registry+https://github.com/rust-lang/crates.io-index" 865 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 866 + dependencies = [ 867 + "proc-macro2", 868 + "quote", 869 + "syn", 870 + ] 871 + 872 + [[package]] 873 + name = "serde_json" 874 + version = "1.0.145" 875 + source = "registry+https://github.com/rust-lang/crates.io-index" 876 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 877 + dependencies = [ 878 + "itoa", 879 + "memchr", 880 + "ryu", 881 + "serde", 882 + "serde_core", 883 + ] 884 + 885 + [[package]] 886 + name = "serde_path_to_error" 887 + version = "0.1.20" 888 + source = "registry+https://github.com/rust-lang/crates.io-index" 889 + checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" 890 + dependencies = [ 891 + "itoa", 892 + "serde", 893 + "serde_core", 894 + ] 895 + 896 + [[package]] 897 + name = "serde_urlencoded" 898 + version = "0.7.1" 899 + source = "registry+https://github.com/rust-lang/crates.io-index" 900 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 901 + dependencies = [ 902 + "form_urlencoded", 903 + "itoa", 904 + "ryu", 905 + "serde", 906 + ] 907 + 908 + [[package]] 909 + name = "sharded-slab" 910 + version = "0.1.7" 911 + source = "registry+https://github.com/rust-lang/crates.io-index" 912 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 913 + dependencies = [ 914 + "lazy_static", 915 + ] 916 + 917 + [[package]] 918 + name = "shlex" 919 + version = "1.3.0" 920 + source = "registry+https://github.com/rust-lang/crates.io-index" 921 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 922 + 923 + [[package]] 924 + name = "signal-hook-registry" 925 + version = "1.4.7" 926 + source = "registry+https://github.com/rust-lang/crates.io-index" 927 + checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" 928 + dependencies = [ 929 + "libc", 930 + ] 931 + 932 + [[package]] 933 + name = "slab" 934 + version = "0.4.11" 935 + source = "registry+https://github.com/rust-lang/crates.io-index" 936 + checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 937 + 938 + [[package]] 939 + name = "smallvec" 940 + version = "1.15.1" 941 + source = "registry+https://github.com/rust-lang/crates.io-index" 942 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 943 + 944 + [[package]] 945 + name = "socket2" 946 + version = "0.6.1" 947 + source = "registry+https://github.com/rust-lang/crates.io-index" 948 + checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 949 + dependencies = [ 950 + "libc", 951 + "windows-sys 0.60.2", 952 + ] 953 + 954 + [[package]] 955 + name = "stable_deref_trait" 956 + version = "1.2.1" 957 + source = "registry+https://github.com/rust-lang/crates.io-index" 958 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 959 + 960 + [[package]] 961 + name = "subtle" 962 + version = "2.6.1" 963 + source = "registry+https://github.com/rust-lang/crates.io-index" 964 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 965 + 966 + [[package]] 967 + name = "syn" 968 + version = "2.0.111" 969 + source = "registry+https://github.com/rust-lang/crates.io-index" 970 + checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 971 + dependencies = [ 972 + "proc-macro2", 973 + "quote", 974 + "unicode-ident", 975 + ] 976 + 977 + [[package]] 978 + name = "sync_wrapper" 979 + version = "1.0.2" 980 + source = "registry+https://github.com/rust-lang/crates.io-index" 981 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 982 + dependencies = [ 983 + "futures-core", 984 + ] 985 + 986 + [[package]] 987 + name = "synstructure" 988 + version = "0.13.2" 989 + source = "registry+https://github.com/rust-lang/crates.io-index" 990 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 991 + dependencies = [ 992 + "proc-macro2", 993 + "quote", 994 + "syn", 995 + ] 996 + 997 + [[package]] 998 + name = "thiserror" 999 + version = "1.0.69" 1000 + source = "registry+https://github.com/rust-lang/crates.io-index" 1001 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1002 + dependencies = [ 1003 + "thiserror-impl 1.0.69", 1004 + ] 1005 + 1006 + [[package]] 1007 + name = "thiserror" 1008 + version = "2.0.17" 1009 + source = "registry+https://github.com/rust-lang/crates.io-index" 1010 + checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 1011 + dependencies = [ 1012 + "thiserror-impl 2.0.17", 1013 + ] 1014 + 1015 + [[package]] 1016 + name = "thiserror-impl" 1017 + version = "1.0.69" 1018 + source = "registry+https://github.com/rust-lang/crates.io-index" 1019 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1020 + dependencies = [ 1021 + "proc-macro2", 1022 + "quote", 1023 + "syn", 1024 + ] 1025 + 1026 + [[package]] 1027 + name = "thiserror-impl" 1028 + version = "2.0.17" 1029 + source = "registry+https://github.com/rust-lang/crates.io-index" 1030 + checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 1031 + dependencies = [ 1032 + "proc-macro2", 1033 + "quote", 1034 + "syn", 1035 + ] 1036 + 1037 + [[package]] 1038 + name = "thread_local" 1039 + version = "1.1.9" 1040 + source = "registry+https://github.com/rust-lang/crates.io-index" 1041 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 1042 + dependencies = [ 1043 + "cfg-if", 1044 + ] 1045 + 1046 + [[package]] 1047 + name = "tinystr" 1048 + version = "0.8.2" 1049 + source = "registry+https://github.com/rust-lang/crates.io-index" 1050 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 1051 + dependencies = [ 1052 + "displaydoc", 1053 + "zerovec", 1054 + ] 1055 + 1056 + [[package]] 1057 + name = "tinyvec" 1058 + version = "1.10.0" 1059 + source = "registry+https://github.com/rust-lang/crates.io-index" 1060 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 1061 + dependencies = [ 1062 + "tinyvec_macros", 1063 + ] 1064 + 1065 + [[package]] 1066 + name = "tinyvec_macros" 1067 + version = "0.1.1" 1068 + source = "registry+https://github.com/rust-lang/crates.io-index" 1069 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1070 + 1071 + [[package]] 1072 + name = "tokio" 1073 + version = "1.48.0" 1074 + source = "registry+https://github.com/rust-lang/crates.io-index" 1075 + checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 1076 + dependencies = [ 1077 + "bytes", 1078 + "libc", 1079 + "mio", 1080 + "pin-project-lite", 1081 + "signal-hook-registry", 1082 + "socket2", 1083 + "tokio-macros", 1084 + "windows-sys 0.61.2", 1085 + ] 1086 + 1087 + [[package]] 1088 + name = "tokio-macros" 1089 + version = "2.6.0" 1090 + source = "registry+https://github.com/rust-lang/crates.io-index" 1091 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 1092 + dependencies = [ 1093 + "proc-macro2", 1094 + "quote", 1095 + "syn", 1096 + ] 1097 + 1098 + [[package]] 1099 + name = "tokio-rustls" 1100 + version = "0.26.4" 1101 + source = "registry+https://github.com/rust-lang/crates.io-index" 1102 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 1103 + dependencies = [ 1104 + "rustls", 1105 + "tokio", 1106 + ] 1107 + 1108 + [[package]] 1109 + name = "tower" 1110 + version = "0.5.2" 1111 + source = "registry+https://github.com/rust-lang/crates.io-index" 1112 + checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1113 + dependencies = [ 1114 + "futures-core", 1115 + "futures-util", 1116 + "pin-project-lite", 1117 + "sync_wrapper", 1118 + "tokio", 1119 + "tower-layer", 1120 + "tower-service", 1121 + "tracing", 1122 + ] 1123 + 1124 + [[package]] 1125 + name = "tower-http" 1126 + version = "0.6.7" 1127 + source = "registry+https://github.com/rust-lang/crates.io-index" 1128 + checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" 1129 + dependencies = [ 1130 + "bitflags", 1131 + "bytes", 1132 + "futures-util", 1133 + "http", 1134 + "http-body", 1135 + "iri-string", 1136 + "pin-project-lite", 1137 + "tower", 1138 + "tower-layer", 1139 + "tower-service", 1140 + ] 1141 + 1142 + [[package]] 1143 + name = "tower-layer" 1144 + version = "0.3.3" 1145 + source = "registry+https://github.com/rust-lang/crates.io-index" 1146 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1147 + 1148 + [[package]] 1149 + name = "tower-service" 1150 + version = "0.3.3" 1151 + source = "registry+https://github.com/rust-lang/crates.io-index" 1152 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1153 + 1154 + [[package]] 1155 + name = "tracing" 1156 + version = "0.1.41" 1157 + source = "registry+https://github.com/rust-lang/crates.io-index" 1158 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1159 + dependencies = [ 1160 + "log", 1161 + "pin-project-lite", 1162 + "tracing-attributes", 1163 + "tracing-core", 1164 + ] 1165 + 1166 + [[package]] 1167 + name = "tracing-attributes" 1168 + version = "0.1.30" 1169 + source = "registry+https://github.com/rust-lang/crates.io-index" 1170 + checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 1171 + dependencies = [ 1172 + "proc-macro2", 1173 + "quote", 1174 + "syn", 1175 + ] 1176 + 1177 + [[package]] 1178 + name = "tracing-core" 1179 + version = "0.1.34" 1180 + source = "registry+https://github.com/rust-lang/crates.io-index" 1181 + checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1182 + dependencies = [ 1183 + "once_cell", 1184 + "valuable", 1185 + ] 1186 + 1187 + [[package]] 1188 + name = "tracing-log" 1189 + version = "0.2.0" 1190 + source = "registry+https://github.com/rust-lang/crates.io-index" 1191 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1192 + dependencies = [ 1193 + "log", 1194 + "once_cell", 1195 + "tracing-core", 1196 + ] 1197 + 1198 + [[package]] 1199 + name = "tracing-subscriber" 1200 + version = "0.3.20" 1201 + source = "registry+https://github.com/rust-lang/crates.io-index" 1202 + checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 1203 + dependencies = [ 1204 + "matchers", 1205 + "nu-ansi-term", 1206 + "once_cell", 1207 + "regex-automata", 1208 + "sharded-slab", 1209 + "smallvec", 1210 + "thread_local", 1211 + "tracing", 1212 + "tracing-core", 1213 + "tracing-log", 1214 + ] 1215 + 1216 + [[package]] 1217 + name = "try-lock" 1218 + version = "0.2.5" 1219 + source = "registry+https://github.com/rust-lang/crates.io-index" 1220 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1221 + 1222 + [[package]] 1223 + name = "unicode-ident" 1224 + version = "1.0.22" 1225 + source = "registry+https://github.com/rust-lang/crates.io-index" 1226 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1227 + 1228 + [[package]] 1229 + name = "untrusted" 1230 + version = "0.9.0" 1231 + source = "registry+https://github.com/rust-lang/crates.io-index" 1232 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1233 + 1234 + [[package]] 1235 + name = "url" 1236 + version = "2.5.7" 1237 + source = "registry+https://github.com/rust-lang/crates.io-index" 1238 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 1239 + dependencies = [ 1240 + "form_urlencoded", 1241 + "idna", 1242 + "percent-encoding", 1243 + "serde", 1244 + ] 1245 + 1246 + [[package]] 1247 + name = "utf8_iter" 1248 + version = "1.0.4" 1249 + source = "registry+https://github.com/rust-lang/crates.io-index" 1250 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1251 + 1252 + [[package]] 1253 + name = "valuable" 1254 + version = "0.1.1" 1255 + source = "registry+https://github.com/rust-lang/crates.io-index" 1256 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1257 + 1258 + [[package]] 1259 + name = "want" 1260 + version = "0.3.1" 1261 + source = "registry+https://github.com/rust-lang/crates.io-index" 1262 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1263 + dependencies = [ 1264 + "try-lock", 1265 + ] 1266 + 1267 + [[package]] 1268 + name = "wasi" 1269 + version = "0.11.1+wasi-snapshot-preview1" 1270 + source = "registry+https://github.com/rust-lang/crates.io-index" 1271 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1272 + 1273 + [[package]] 1274 + name = "wasip2" 1275 + version = "1.0.1+wasi-0.2.4" 1276 + source = "registry+https://github.com/rust-lang/crates.io-index" 1277 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 1278 + dependencies = [ 1279 + "wit-bindgen", 1280 + ] 1281 + 1282 + [[package]] 1283 + name = "wasm-bindgen" 1284 + version = "0.2.105" 1285 + source = "registry+https://github.com/rust-lang/crates.io-index" 1286 + checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" 1287 + dependencies = [ 1288 + "cfg-if", 1289 + "once_cell", 1290 + "rustversion", 1291 + "wasm-bindgen-macro", 1292 + "wasm-bindgen-shared", 1293 + ] 1294 + 1295 + [[package]] 1296 + name = "wasm-bindgen-futures" 1297 + version = "0.4.55" 1298 + source = "registry+https://github.com/rust-lang/crates.io-index" 1299 + checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" 1300 + dependencies = [ 1301 + "cfg-if", 1302 + "js-sys", 1303 + "once_cell", 1304 + "wasm-bindgen", 1305 + "web-sys", 1306 + ] 1307 + 1308 + [[package]] 1309 + name = "wasm-bindgen-macro" 1310 + version = "0.2.105" 1311 + source = "registry+https://github.com/rust-lang/crates.io-index" 1312 + checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" 1313 + dependencies = [ 1314 + "quote", 1315 + "wasm-bindgen-macro-support", 1316 + ] 1317 + 1318 + [[package]] 1319 + name = "wasm-bindgen-macro-support" 1320 + version = "0.2.105" 1321 + source = "registry+https://github.com/rust-lang/crates.io-index" 1322 + checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" 1323 + dependencies = [ 1324 + "bumpalo", 1325 + "proc-macro2", 1326 + "quote", 1327 + "syn", 1328 + "wasm-bindgen-shared", 1329 + ] 1330 + 1331 + [[package]] 1332 + name = "wasm-bindgen-shared" 1333 + version = "0.2.105" 1334 + source = "registry+https://github.com/rust-lang/crates.io-index" 1335 + checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" 1336 + dependencies = [ 1337 + "unicode-ident", 1338 + ] 1339 + 1340 + [[package]] 1341 + name = "web-sys" 1342 + version = "0.3.82" 1343 + source = "registry+https://github.com/rust-lang/crates.io-index" 1344 + checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" 1345 + dependencies = [ 1346 + "js-sys", 1347 + "wasm-bindgen", 1348 + ] 1349 + 1350 + [[package]] 1351 + name = "web-time" 1352 + version = "1.1.0" 1353 + source = "registry+https://github.com/rust-lang/crates.io-index" 1354 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 1355 + dependencies = [ 1356 + "js-sys", 1357 + "wasm-bindgen", 1358 + ] 1359 + 1360 + [[package]] 1361 + name = "webpki-roots" 1362 + version = "1.0.4" 1363 + source = "registry+https://github.com/rust-lang/crates.io-index" 1364 + checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" 1365 + dependencies = [ 1366 + "rustls-pki-types", 1367 + ] 1368 + 1369 + [[package]] 1370 + name = "windows-link" 1371 + version = "0.2.1" 1372 + source = "registry+https://github.com/rust-lang/crates.io-index" 1373 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1374 + 1375 + [[package]] 1376 + name = "windows-sys" 1377 + version = "0.52.0" 1378 + source = "registry+https://github.com/rust-lang/crates.io-index" 1379 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1380 + dependencies = [ 1381 + "windows-targets 0.52.6", 1382 + ] 1383 + 1384 + [[package]] 1385 + name = "windows-sys" 1386 + version = "0.60.2" 1387 + source = "registry+https://github.com/rust-lang/crates.io-index" 1388 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1389 + dependencies = [ 1390 + "windows-targets 0.53.5", 1391 + ] 1392 + 1393 + [[package]] 1394 + name = "windows-sys" 1395 + version = "0.61.2" 1396 + source = "registry+https://github.com/rust-lang/crates.io-index" 1397 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1398 + dependencies = [ 1399 + "windows-link", 1400 + ] 1401 + 1402 + [[package]] 1403 + name = "windows-targets" 1404 + version = "0.52.6" 1405 + source = "registry+https://github.com/rust-lang/crates.io-index" 1406 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1407 + dependencies = [ 1408 + "windows_aarch64_gnullvm 0.52.6", 1409 + "windows_aarch64_msvc 0.52.6", 1410 + "windows_i686_gnu 0.52.6", 1411 + "windows_i686_gnullvm 0.52.6", 1412 + "windows_i686_msvc 0.52.6", 1413 + "windows_x86_64_gnu 0.52.6", 1414 + "windows_x86_64_gnullvm 0.52.6", 1415 + "windows_x86_64_msvc 0.52.6", 1416 + ] 1417 + 1418 + [[package]] 1419 + name = "windows-targets" 1420 + version = "0.53.5" 1421 + source = "registry+https://github.com/rust-lang/crates.io-index" 1422 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 1423 + dependencies = [ 1424 + "windows-link", 1425 + "windows_aarch64_gnullvm 0.53.1", 1426 + "windows_aarch64_msvc 0.53.1", 1427 + "windows_i686_gnu 0.53.1", 1428 + "windows_i686_gnullvm 0.53.1", 1429 + "windows_i686_msvc 0.53.1", 1430 + "windows_x86_64_gnu 0.53.1", 1431 + "windows_x86_64_gnullvm 0.53.1", 1432 + "windows_x86_64_msvc 0.53.1", 1433 + ] 1434 + 1435 + [[package]] 1436 + name = "windows_aarch64_gnullvm" 1437 + version = "0.52.6" 1438 + source = "registry+https://github.com/rust-lang/crates.io-index" 1439 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1440 + 1441 + [[package]] 1442 + name = "windows_aarch64_gnullvm" 1443 + version = "0.53.1" 1444 + source = "registry+https://github.com/rust-lang/crates.io-index" 1445 + checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 1446 + 1447 + [[package]] 1448 + name = "windows_aarch64_msvc" 1449 + version = "0.52.6" 1450 + source = "registry+https://github.com/rust-lang/crates.io-index" 1451 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1452 + 1453 + [[package]] 1454 + name = "windows_aarch64_msvc" 1455 + version = "0.53.1" 1456 + source = "registry+https://github.com/rust-lang/crates.io-index" 1457 + checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1458 + 1459 + [[package]] 1460 + name = "windows_i686_gnu" 1461 + version = "0.52.6" 1462 + source = "registry+https://github.com/rust-lang/crates.io-index" 1463 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1464 + 1465 + [[package]] 1466 + name = "windows_i686_gnu" 1467 + version = "0.53.1" 1468 + source = "registry+https://github.com/rust-lang/crates.io-index" 1469 + checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1470 + 1471 + [[package]] 1472 + name = "windows_i686_gnullvm" 1473 + version = "0.52.6" 1474 + source = "registry+https://github.com/rust-lang/crates.io-index" 1475 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1476 + 1477 + [[package]] 1478 + name = "windows_i686_gnullvm" 1479 + version = "0.53.1" 1480 + source = "registry+https://github.com/rust-lang/crates.io-index" 1481 + checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1482 + 1483 + [[package]] 1484 + name = "windows_i686_msvc" 1485 + version = "0.52.6" 1486 + source = "registry+https://github.com/rust-lang/crates.io-index" 1487 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1488 + 1489 + [[package]] 1490 + name = "windows_i686_msvc" 1491 + version = "0.53.1" 1492 + source = "registry+https://github.com/rust-lang/crates.io-index" 1493 + checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1494 + 1495 + [[package]] 1496 + name = "windows_x86_64_gnu" 1497 + version = "0.52.6" 1498 + source = "registry+https://github.com/rust-lang/crates.io-index" 1499 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1500 + 1501 + [[package]] 1502 + name = "windows_x86_64_gnu" 1503 + version = "0.53.1" 1504 + source = "registry+https://github.com/rust-lang/crates.io-index" 1505 + checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1506 + 1507 + [[package]] 1508 + name = "windows_x86_64_gnullvm" 1509 + version = "0.52.6" 1510 + source = "registry+https://github.com/rust-lang/crates.io-index" 1511 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1512 + 1513 + [[package]] 1514 + name = "windows_x86_64_gnullvm" 1515 + version = "0.53.1" 1516 + source = "registry+https://github.com/rust-lang/crates.io-index" 1517 + checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1518 + 1519 + [[package]] 1520 + name = "windows_x86_64_msvc" 1521 + version = "0.52.6" 1522 + source = "registry+https://github.com/rust-lang/crates.io-index" 1523 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1524 + 1525 + [[package]] 1526 + name = "windows_x86_64_msvc" 1527 + version = "0.53.1" 1528 + source = "registry+https://github.com/rust-lang/crates.io-index" 1529 + checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1530 + 1531 + [[package]] 1532 + name = "wit-bindgen" 1533 + version = "0.46.0" 1534 + source = "registry+https://github.com/rust-lang/crates.io-index" 1535 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 1536 + 1537 + [[package]] 1538 + name = "writeable" 1539 + version = "0.6.2" 1540 + source = "registry+https://github.com/rust-lang/crates.io-index" 1541 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 1542 + 1543 + [[package]] 1544 + name = "yoke" 1545 + version = "0.8.1" 1546 + source = "registry+https://github.com/rust-lang/crates.io-index" 1547 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 1548 + dependencies = [ 1549 + "stable_deref_trait", 1550 + "yoke-derive", 1551 + "zerofrom", 1552 + ] 1553 + 1554 + [[package]] 1555 + name = "yoke-derive" 1556 + version = "0.8.1" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 1559 + dependencies = [ 1560 + "proc-macro2", 1561 + "quote", 1562 + "syn", 1563 + "synstructure", 1564 + ] 1565 + 1566 + [[package]] 1567 + name = "zerocopy" 1568 + version = "0.8.30" 1569 + source = "registry+https://github.com/rust-lang/crates.io-index" 1570 + checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" 1571 + dependencies = [ 1572 + "zerocopy-derive", 1573 + ] 1574 + 1575 + [[package]] 1576 + name = "zerocopy-derive" 1577 + version = "0.8.30" 1578 + source = "registry+https://github.com/rust-lang/crates.io-index" 1579 + checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" 1580 + dependencies = [ 1581 + "proc-macro2", 1582 + "quote", 1583 + "syn", 1584 + ] 1585 + 1586 + [[package]] 1587 + name = "zerofrom" 1588 + version = "0.1.6" 1589 + source = "registry+https://github.com/rust-lang/crates.io-index" 1590 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1591 + dependencies = [ 1592 + "zerofrom-derive", 1593 + ] 1594 + 1595 + [[package]] 1596 + name = "zerofrom-derive" 1597 + version = "0.1.6" 1598 + source = "registry+https://github.com/rust-lang/crates.io-index" 1599 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1600 + dependencies = [ 1601 + "proc-macro2", 1602 + "quote", 1603 + "syn", 1604 + "synstructure", 1605 + ] 1606 + 1607 + [[package]] 1608 + name = "zeroize" 1609 + version = "1.8.2" 1610 + source = "registry+https://github.com/rust-lang/crates.io-index" 1611 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 1612 + 1613 + [[package]] 1614 + name = "zerotrie" 1615 + version = "0.2.3" 1616 + source = "registry+https://github.com/rust-lang/crates.io-index" 1617 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 1618 + dependencies = [ 1619 + "displaydoc", 1620 + "yoke", 1621 + "zerofrom", 1622 + ] 1623 + 1624 + [[package]] 1625 + name = "zerovec" 1626 + version = "0.11.5" 1627 + source = "registry+https://github.com/rust-lang/crates.io-index" 1628 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 1629 + dependencies = [ 1630 + "yoke", 1631 + "zerofrom", 1632 + "zerovec-derive", 1633 + ] 1634 + 1635 + [[package]] 1636 + name = "zerovec-derive" 1637 + version = "0.11.2" 1638 + source = "registry+https://github.com/rust-lang/crates.io-index" 1639 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 1640 + dependencies = [ 1641 + "proc-macro2", 1642 + "quote", 1643 + "syn", 1644 + ]
+15
moderation/Cargo.toml
··· 1 + [package] 2 + name = "moderation" 3 + version = "0.1.0" 4 + edition = "2021" 5 + 6 + [dependencies] 7 + anyhow = "1.0" 8 + axum = { version = "0.7", features = ["macros", "json"] } 9 + reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 10 + serde = { version = "1.0", features = ["derive"] } 11 + serde_json = "1.0" 12 + thiserror = "1.0" 13 + tokio = { version = "1.40", features = ["rt-multi-thread", "macros", "signal"] } 14 + tracing = "0.1" 15 + tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
+15
moderation/Dockerfile
··· 1 + FROM rust:1.83-slim as builder 2 + 3 + WORKDIR /app 4 + COPY Cargo.toml Cargo.lock* ./ 5 + COPY src ./src 6 + 7 + RUN cargo build --release 8 + 9 + FROM debian:bookworm-slim 10 + 11 + RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* 12 + 13 + COPY --from=builder /app/target/release/moderation /usr/local/bin/moderation 14 + 15 + CMD ["moderation"]
+32
moderation/Justfile
··· 1 + set shell := ["bash", "-eu", "-o", "pipefail", "-c"] 2 + default := "run" 3 + 4 + alias r := run 5 + alias b := build 6 + 7 + run: 8 + MODERATION_HOST="${MODERATION_HOST:-127.0.0.1}" \ 9 + MODERATION_PORT="${MODERATION_PORT:-8083}" \ 10 + MODERATION_AUDD_API_TOKEN="${MODERATION_AUDD_API_TOKEN:-}" \ 11 + cargo watch -x run 12 + 13 + build: 14 + cargo build --release 15 + 16 + check: 17 + cargo check 18 + 19 + fmt: 20 + cargo fmt 21 + 22 + clippy: 23 + cargo clippy --all-targets --all-features 24 + 25 + image tag="plyr-moderation:local": 26 + docker build -t {{tag}} . 27 + 28 + docker-run TAG="plyr-moderation:local" PORT="8083": 29 + docker run --rm -p {{PORT}}:8080 {{TAG}} 30 + 31 + deploy ARGS="": 32 + fly deploy --config fly.toml {{ARGS}}
+27
moderation/fly.toml
··· 1 + app = "plyr-moderation" 2 + primary_region = "iad" 3 + 4 + [build] 5 + dockerfile = "Dockerfile" 6 + 7 + [http_service] 8 + internal_port = 8080 9 + force_https = true 10 + auto_stop_machines = "stop" 11 + auto_start_machines = true 12 + min_machines_running = 0 13 + 14 + [http_service.concurrency] 15 + type = "requests" 16 + hard_limit = 50 17 + soft_limit = 25 18 + 19 + [[vm]] 20 + cpu_kind = "shared" 21 + cpus = 1 22 + memory = "256mb" 23 + 24 + [env] 25 + MODERATION_HOST = "0.0.0.0" 26 + MODERATION_PORT = "8080" 27 + MODERATION_SCORE_THRESHOLD = "70"
+303
moderation/src/main.rs
··· 1 + use std::{env, net::SocketAddr}; 2 + 3 + use anyhow::anyhow; 4 + use axum::{ 5 + extract::Request, 6 + http::StatusCode, 7 + middleware::{self, Next}, 8 + response::{IntoResponse, Response}, 9 + routing::{get, post}, 10 + Json, Router, 11 + }; 12 + use serde::{Deserialize, Serialize}; 13 + use tokio::net::TcpListener; 14 + use tracing::{error, info, warn}; 15 + 16 + // --- config --- 17 + 18 + struct Config { 19 + host: String, 20 + port: u16, 21 + auth_token: Option<String>, 22 + audd_api_token: String, 23 + audd_api_url: String, 24 + score_threshold: i32, 25 + } 26 + 27 + impl Config { 28 + fn from_env() -> anyhow::Result<Self> { 29 + Ok(Self { 30 + host: env::var("MODERATION_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), 31 + port: env::var("MODERATION_PORT") 32 + .ok() 33 + .and_then(|v| v.parse().ok()) 34 + .unwrap_or(8083), 35 + auth_token: env::var("MODERATION_AUTH_TOKEN").ok(), 36 + audd_api_token: env::var("MODERATION_AUDD_API_TOKEN") 37 + .map_err(|_| anyhow!("MODERATION_AUDD_API_TOKEN is required"))?, 38 + audd_api_url: env::var("MODERATION_AUDD_API_URL") 39 + .unwrap_or_else(|_| "https://api.audd.io/".to_string()), 40 + score_threshold: env::var("MODERATION_SCORE_THRESHOLD") 41 + .ok() 42 + .and_then(|v| v.parse().ok()) 43 + .unwrap_or(70), 44 + }) 45 + } 46 + } 47 + 48 + // --- request/response types --- 49 + 50 + #[derive(Debug, Deserialize)] 51 + struct ScanRequest { 52 + audio_url: String, 53 + } 54 + 55 + #[derive(Debug, Serialize)] 56 + struct ScanResponse { 57 + matches: Vec<AuddMatch>, 58 + is_flagged: bool, 59 + highest_score: i32, 60 + raw_response: serde_json::Value, 61 + } 62 + 63 + #[derive(Debug, Serialize, Clone)] 64 + struct AuddMatch { 65 + artist: String, 66 + title: String, 67 + #[serde(skip_serializing_if = "Option::is_none")] 68 + album: Option<String>, 69 + score: i32, 70 + #[serde(skip_serializing_if = "Option::is_none")] 71 + isrc: Option<String>, 72 + #[serde(skip_serializing_if = "Option::is_none")] 73 + timecode: Option<String>, 74 + #[serde(skip_serializing_if = "Option::is_none")] 75 + offset_ms: Option<i64>, 76 + } 77 + 78 + #[derive(Debug, Serialize)] 79 + struct HealthResponse { 80 + status: &'static str, 81 + } 82 + 83 + // --- audd api types --- 84 + 85 + #[derive(Debug, Deserialize)] 86 + struct AuddResponse { 87 + status: Option<String>, 88 + result: Option<AuddResult>, 89 + } 90 + 91 + #[derive(Debug, Deserialize)] 92 + #[serde(untagged)] 93 + enum AuddResult { 94 + Groups(Vec<AuddGroup>), 95 + Single(AuddSong), 96 + } 97 + 98 + #[derive(Debug, Deserialize)] 99 + struct AuddGroup { 100 + offset: Option<i64>, 101 + songs: Option<Vec<AuddSong>>, 102 + } 103 + 104 + #[derive(Debug, Deserialize)] 105 + struct AuddSong { 106 + artist: Option<String>, 107 + title: Option<String>, 108 + album: Option<String>, 109 + score: Option<i32>, 110 + isrc: Option<String>, 111 + timecode: Option<String>, 112 + } 113 + 114 + // --- main --- 115 + 116 + #[tokio::main] 117 + async fn main() -> anyhow::Result<()> { 118 + tracing_subscriber::fmt() 119 + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 120 + .with_target(false) 121 + .init(); 122 + 123 + let config = Config::from_env()?; 124 + let auth_token = config.auth_token.clone(); 125 + 126 + let app = Router::new() 127 + .route("/health", get(health)) 128 + .route("/scan", post(scan)) 129 + .layer(middleware::from_fn(move |req, next| { 130 + auth_middleware(req, next, auth_token.clone()) 131 + })) 132 + .with_state(AppState { 133 + audd_api_token: config.audd_api_token, 134 + audd_api_url: config.audd_api_url, 135 + score_threshold: config.score_threshold, 136 + }); 137 + 138 + let addr: SocketAddr = format!("{}:{}", config.host, config.port) 139 + .parse() 140 + .map_err(|e| anyhow!("invalid bind addr: {e}"))?; 141 + info!(%addr, "moderation service listening"); 142 + 143 + let listener = TcpListener::bind(addr).await?; 144 + axum::serve(listener, app).await?; 145 + Ok(()) 146 + } 147 + 148 + // --- state --- 149 + 150 + #[derive(Clone)] 151 + struct AppState { 152 + audd_api_token: String, 153 + audd_api_url: String, 154 + score_threshold: i32, 155 + } 156 + 157 + // --- middleware --- 158 + 159 + async fn auth_middleware( 160 + req: Request, 161 + next: Next, 162 + auth_token: Option<String>, 163 + ) -> Result<Response, StatusCode> { 164 + if req.uri().path() == "/health" { 165 + return Ok(next.run(req).await); 166 + } 167 + 168 + let Some(expected_token) = auth_token else { 169 + warn!("no MODERATION_AUTH_TOKEN set - accepting all requests"); 170 + return Ok(next.run(req).await); 171 + }; 172 + 173 + let token = req 174 + .headers() 175 + .get("X-Moderation-Key") 176 + .and_then(|v| v.to_str().ok()); 177 + 178 + match token { 179 + Some(t) if t == expected_token => Ok(next.run(req).await), 180 + Some(_) => { 181 + warn!("invalid auth token provided"); 182 + Err(StatusCode::UNAUTHORIZED) 183 + } 184 + None => { 185 + warn!("missing X-Moderation-Key header"); 186 + Err(StatusCode::UNAUTHORIZED) 187 + } 188 + } 189 + } 190 + 191 + // --- handlers --- 192 + 193 + async fn health() -> Json<HealthResponse> { 194 + Json(HealthResponse { status: "ok" }) 195 + } 196 + 197 + async fn scan( 198 + axum::extract::State(state): axum::extract::State<AppState>, 199 + Json(request): Json<ScanRequest>, 200 + ) -> Result<Json<ScanResponse>, AppError> { 201 + info!(audio_url = %request.audio_url, "scanning audio"); 202 + 203 + let client = reqwest::Client::new(); 204 + let response = client 205 + .post(&state.audd_api_url) 206 + .form(&[ 207 + ("api_token", &state.audd_api_token), 208 + ("url", &request.audio_url), 209 + ("accurate_offsets", &"1".to_string()), 210 + ]) 211 + .send() 212 + .await 213 + .map_err(|e| AppError::Audd(format!("request failed: {e}")))?; 214 + 215 + let raw_response: serde_json::Value = response 216 + .json() 217 + .await 218 + .map_err(|e| AppError::Audd(format!("failed to parse response: {e}")))?; 219 + 220 + let audd_response: AuddResponse = serde_json::from_value(raw_response.clone()) 221 + .map_err(|e| AppError::Audd(format!("failed to parse audd response: {e}")))?; 222 + 223 + if audd_response.status.as_deref() == Some("error") { 224 + return Err(AppError::Audd(format!( 225 + "audd returned error: {}", 226 + raw_response 227 + ))); 228 + } 229 + 230 + let matches = extract_matches(&audd_response); 231 + let highest_score = matches.iter().map(|m| m.score).max().unwrap_or(0); 232 + let is_flagged = highest_score >= state.score_threshold; 233 + 234 + info!( 235 + match_count = matches.len(), 236 + highest_score, 237 + is_flagged, 238 + "scan complete" 239 + ); 240 + 241 + Ok(Json(ScanResponse { 242 + matches, 243 + is_flagged, 244 + highest_score, 245 + raw_response, 246 + })) 247 + } 248 + 249 + fn extract_matches(response: &AuddResponse) -> Vec<AuddMatch> { 250 + let Some(result) = &response.result else { 251 + return vec![]; 252 + }; 253 + 254 + match result { 255 + AuddResult::Groups(groups) => groups 256 + .iter() 257 + .flat_map(|group| { 258 + group 259 + .songs 260 + .as_ref() 261 + .map(|songs| { 262 + songs 263 + .iter() 264 + .map(|song| parse_song(song, group.offset)) 265 + .collect::<Vec<_>>() 266 + }) 267 + .unwrap_or_default() 268 + }) 269 + .collect(), 270 + AuddResult::Single(song) => vec![parse_song(song, None)], 271 + } 272 + } 273 + 274 + fn parse_song(song: &AuddSong, offset_ms: Option<i64>) -> AuddMatch { 275 + AuddMatch { 276 + artist: song.artist.clone().unwrap_or_else(|| "Unknown".to_string()), 277 + title: song.title.clone().unwrap_or_else(|| "Unknown".to_string()), 278 + album: song.album.clone(), 279 + score: song.score.unwrap_or(0), 280 + isrc: song.isrc.clone(), 281 + timecode: song.timecode.clone(), 282 + offset_ms, 283 + } 284 + } 285 + 286 + // --- errors --- 287 + 288 + #[derive(Debug, thiserror::Error)] 289 + enum AppError { 290 + #[error("audd error: {0}")] 291 + Audd(String), 292 + } 293 + 294 + impl IntoResponse for AppError { 295 + fn into_response(self) -> Response { 296 + error!(error = %self, "request failed"); 297 + let status = match self { 298 + AppError::Audd(_) => StatusCode::BAD_GATEWAY, 299 + }; 300 + let body = serde_json::json!({ "error": self.to_string() }); 301 + (status, Json(body)).into_response() 302 + } 303 + }