+10
.dockerignore
+10
.dockerignore
+40
.env.example
+40
.env.example
···
1
+
# PDS Configuration
2
+
PDS_ENDPOINT=https://bsky.social
3
+
4
+
# PLC Configuration
5
+
PLC_ENDPOINT=https://plc.directory
6
+
7
+
# Automod Account Configuration (REQUIRED)
8
+
# The automod account has moderator permissions and is used for authentication
9
+
# DID is automatically sourced from the agent session after login
10
+
AUTOMOD_HANDLE=your-automod.bsky.social
11
+
AUTOMOD_PASSWORD=your-app-password-here
12
+
13
+
# Ozone Configuration (REQUIRED)
14
+
OZONE_URL=https://ozone.skywatch.blue
15
+
OZONE_PDS=https://blewit.us-west.host.bsky.network
16
+
17
+
# Labeler Configuration (REQUIRED)
18
+
# This is the DID of your main labeler account (e.g., skywatch.blue)
19
+
# NOT the automod account, NOT the ozone service
20
+
LABELER_DID=did:plc:your-labeler-did-here
21
+
RATE_LIMIT_MS=100
22
+
23
+
# Jetstream Configuration
24
+
JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
25
+
CURSOR_UPDATE_INTERVAL=10000
26
+
27
+
# Redis Configuration
28
+
REDIS_URL=redis://localhost:6379
29
+
30
+
# Cache Configuration
31
+
CACHE_ENABLED=true
32
+
CACHE_TTL_SECONDS=86400
33
+
34
+
# Processing Configuration
35
+
PROCESSING_CONCURRENCY=4
36
+
RETRY_ATTEMPTS=3
37
+
RETRY_DELAY_MS=1000
38
+
39
+
# Phash Configuration
40
+
PHASH_HAMMING_THRESHOLD=5
+1
.envrc
+1
.envrc
···
1
+
use flake
+18
.gitignore
+18
.gitignore
···
1
+
/target
2
+
/result
3
+
/result-lib
4
+
.direnv
5
+
.claude
6
+
/.pre-commit-config.yaml
7
+
CLAUDE.md
8
+
AGENTS.md
9
+
crates/jacquard-lexicon/target
10
+
/plans
11
+
/docs
12
+
/binaries/releases/
13
+
rustdoc-host.nix
14
+
**/**.car
15
+
cursor.txt
16
+
.session
17
+
.env
18
+
firehose_cursor.db
+18
.zed/settings.json
+18
.zed/settings.json
···
1
+
// Folder-specific settings
2
+
//
3
+
// For a full list of overridable settings, and general information on folder-specific settings,
4
+
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
5
+
{
6
+
"lsp": {
7
+
"rust-analyzer": {
8
+
"initialization_options": {
9
+
"rust": {
10
+
"analyzerTargetDir": true
11
+
},
12
+
"cargo": {
13
+
"allFeatures": true
14
+
}
15
+
}
16
+
}
17
+
}
18
+
}
+1018
ARCHITECTURE.md
+1018
ARCHITECTURE.md
···
1
+
# Skywatch-Phash: Complete Architecture & Implementation Guide
2
+
3
+
## Overview
4
+
5
+
**skywatch-phash** is a real-time perceptual hash-based image moderation service for Bluesky/ATProto. It:
6
+
1. Subscribes to the Bluesky firehose (Jetstream) in real-time
7
+
2. Extracts images from posts and computes perceptual hashes (phash)
8
+
3. Compares against known harmful image hashes using Hamming distance
9
+
4. Automatically applies moderation labels and reports to Bluesky Ozone moderators
10
+
11
+
**Tech Stack:** Bun/TypeScript, Redis, Jetstream, ATProto API, Sharp (image processing)
12
+
13
+
---
14
+
15
+
## Architecture Overview
16
+
17
+
```
18
+
┌─────────────────────────────────────────────────────────────┐
19
+
│ Main Application │
20
+
│ (main.ts - Jetstream event handler + orchestrator) │
21
+
└─────────────────┬───────────────────────────────────────────┘
22
+
│
23
+
┌─────────┼─────────┐
24
+
│ │ │
25
+
┌────▼──┐ ┌──▼────┐ ┌──▼─────────┐
26
+
│Redis │ │ Jet- │ │ Moderation │
27
+
│Queue │ │stream │ │ Agent │
28
+
└────┬──┘ └───┬───┘ └──┬─────────┘
29
+
│ │ │
30
+
└─────────┼────────┘
31
+
│
32
+
┌─────────▼──────────────┐
33
+
│ Queue Worker │
34
+
│ (concurrency control) │
35
+
└─────────┬──────────────┘
36
+
│
37
+
┌─────────▼──────────────────────────┐
38
+
│ Image Processor │
39
+
│ (phash + matching logic) │
40
+
├─────────────────────────────────────┤
41
+
│ ▪ Phash Computation (Sharp) │
42
+
│ ▪ Hamming Distance Matching │
43
+
│ ▪ Redis Phash Cache │
44
+
│ ▪ Moderation Claims Deduplication │
45
+
└────────────────────────────────────┘
46
+
```
47
+
48
+
---
49
+
50
+
## 1. Entry Point & Main Event Loop
51
+
52
+
### File: `src/main.ts`
53
+
54
+
**Responsibilities:**
55
+
- Jetstream connection management
56
+
- Cursor persistence (recovery from crashes)
57
+
- Queue/worker initialization
58
+
- Moderation action dispatch
59
+
60
+
**Key Flow:**
61
+
62
+
```typescript
63
+
// 1. Load cursor from disk (enables resuming from last processed event)
64
+
cursor = fs.readFileSync("cursor.txt") || Math.floor(Date.now() * 1000)
65
+
66
+
// 2. Connect to Jetstream
67
+
jetstream = new Jetstream({
68
+
endpoint: "wss://jetstream1.us-east.fire.hose.cam/subscribe",
69
+
cursor,
70
+
wantedCollections: ["app.bsky.feed.post"]
71
+
})
72
+
73
+
// 3. Register handler for new posts
74
+
jetstream.onCreate("app.bsky.feed.post", async (event) => {
75
+
// Extract image blobs (CIDs) from post
76
+
const blobs = extractBlobsFromEvent(event)
77
+
78
+
// Enqueue for processing
79
+
const job = { postUri, postCid, postDid, blobs, timestamp, attempts: 0 }
80
+
await queue.enqueue(job)
81
+
})
82
+
83
+
// 4. Start processing
84
+
await worker.start()
85
+
jetstream.start()
86
+
87
+
// 5. On match found, execute moderation actions
88
+
worker.onMatchFound((postUri, postCid, postDid, match) => {
89
+
if (match.matchedCheck.toLabel)
90
+
await createPostLabel(...)
91
+
if (match.matchedCheck.reportPost)
92
+
await createPostReport(...)
93
+
if (match.matchedCheck.labelAcct)
94
+
await createAccountLabel(...)
95
+
if (match.matchedCheck.reportAcct)
96
+
await createAccountReport(...)
97
+
})
98
+
```
99
+
100
+
**Cursor Handling (CRITICAL):**
101
+
- Cursor is microsecond timestamp (µs, not ms) from Jetstream
102
+
- Saved every 10 seconds (configurable via `CURSOR_UPDATE_INTERVAL`)
103
+
- On restart, reads from `cursor.txt` to resume from last processed event
104
+
- **This prevents duplicate processing of same posts**
105
+
106
+
**Graceful Shutdown:**
107
+
- Saves cursor before exit
108
+
- Waits for active jobs to complete
109
+
- Closes all connections
110
+
111
+
---
112
+
113
+
## 2. Jetstream Connection & Event Processing
114
+
115
+
### Event Structure
116
+
117
+
Posts with embedded images trigger:
118
+
119
+
```typescript
120
+
CommitCreateEvent<"app.bsky.feed.post"> {
121
+
did: string // Author DID
122
+
commit: {
123
+
cid: string // Post CID
124
+
collection: "app.bsky.feed.post"
125
+
rkey: string // Post key
126
+
record: {
127
+
embed: {
128
+
images?: [{image: {ref: {$link: CID}, mimeType}}]
129
+
media?: {images: [...]}
130
+
}
131
+
}
132
+
}
133
+
}
134
+
```
135
+
136
+
**Image Extraction Logic:**
137
+
- Handles both `embed.images` and `embed.media.images` (both are valid)
138
+
- Filters out SVG and non-image types
139
+
- Extracts CID from nested `ref.$link` field
140
+
141
+
---
142
+
143
+
## 3. Perceptual Hash Algorithm (CRITICAL - Exact Implementation)
144
+
145
+
### File: `src/hasher/phash.ts`
146
+
147
+
```typescript
148
+
async function computePerceptualHash(buffer: Buffer): Promise<string> {
149
+
// Step 1: Decode image via Sharp
150
+
const image = sharp(buffer)
151
+
const metadata = await image.metadata()
152
+
153
+
// Step 2: Resize to 8x8 grayscale
154
+
// CRITICAL: fit: "fill" preserves aspect ratio, may add padding
155
+
const resized = await image
156
+
.resize(8, 8, { fit: "fill" })
157
+
.grayscale()
158
+
.raw()
159
+
.toBuffer()
160
+
161
+
// Step 3: Extract pixel values (Uint8Array, 0-255 range)
162
+
const pixels = new Uint8Array(resized)
163
+
164
+
// Step 4: Compute average brightness
165
+
const avg = pixels.reduce((sum, val) => sum + val, 0) / pixels.length
166
+
167
+
// Step 5: Create 64-bit hash (8x8 = 64 pixels)
168
+
let hash = ""
169
+
for (let i = 0; i < pixels.length; i++) {
170
+
hash += pixels[i] > avg ? "1" : "0"
171
+
}
172
+
173
+
// Step 6: Convert binary string to hex (16 character string)
174
+
return BigInt(`0b${hash}`).toString(16).padStart(16, "0")
175
+
}
176
+
```
177
+
178
+
**Output Format:**
179
+
- **Exactly 16 hex characters** (64 bits for 8x8 image)
180
+
- Lowercase
181
+
- Zero-padded
182
+
183
+
**Example:** `"a1b2c3d4e5f6a7b8"`
184
+
185
+
**Important Details:**
186
+
187
+
1. **Sharp resize behavior:**
188
+
- `fit: "fill"` = no cropping, may add padding to preserve aspect ratio
189
+
- This is intentional - handles images of any aspect ratio
190
+
- Padding typically adds uniform brightness which affects hash slightly
191
+
192
+
2. **Grayscale conversion:**
193
+
- Sharp converts RGB to single channel (standard luminosity formula)
194
+
- Range: 0-255
195
+
196
+
3. **No normalization:**
197
+
- Raw pixel values compared to mean (not normalized)
198
+
- This is correct for perceptual hashing
199
+
200
+
4. **Deterministic:**
201
+
- Same image buffer always produces same hash
202
+
- Different images (even slight variations) can produce different hashes
203
+
- BUT: Can match within Hamming distance threshold
204
+
205
+
---
206
+
207
+
## 4. Hamming Distance Matching
208
+
209
+
### File: `src/matcher/hamming.ts`
210
+
211
+
```typescript
212
+
function hammingDistance(hash1: string, hash2: string): number {
213
+
// Convert hex to BigInt
214
+
const a = BigInt(`0x${hash1}`)
215
+
const b = BigInt(`0x${hash2}`)
216
+
217
+
// XOR finds differing bits
218
+
const xor = a ^ b
219
+
220
+
// Count set bits (Brian Kernighan's algorithm)
221
+
let count = 0
222
+
let n = xor
223
+
while (n > 0n) {
224
+
count++
225
+
n &= n - 1n // Remove rightmost set bit
226
+
}
227
+
228
+
return count
229
+
}
230
+
231
+
function findMatch(phash: string, checks: BlobCheck[]): BlobCheck | null {
232
+
for (const check of checks) {
233
+
const threshold = check.hammingThreshold ?? 5
234
+
235
+
for (const checkPhash of check.phashes) {
236
+
const distance = hammingDistance(phash, checkPhash)
237
+
238
+
if (distance <= threshold) {
239
+
return check // First match wins
240
+
}
241
+
}
242
+
}
243
+
244
+
return null
245
+
}
246
+
```
247
+
248
+
**Key Semantics:**
249
+
- Hamming distance = number of differing bits out of 64
250
+
- Range: 0-64
251
+
- Threshold comparison: `distance <= threshold` (inclusive)
252
+
253
+
**Threshold Guidelines (from README):**
254
+
- `0` = Exact match only (very strict)
255
+
- `1-2` = Nearly identical (minor compression artifacts)
256
+
- `3-4` = Very similar (slight edits, crops)
257
+
- `5-8` = Similar (moderate edits)
258
+
- `10+` = Loosely similar (too permissive)
259
+
260
+
**Default threshold:** 5 (configurable per check or globally)
261
+
262
+
---
263
+
264
+
## 5. Queue & Worker Implementation
265
+
266
+
### File: `src/queue/redis-queue.ts`
267
+
268
+
**Redis Keys:**
269
+
```
270
+
phash:queue:pending → List (FIFO for new jobs)
271
+
phash:queue:processing → List (jobs being worked on)
272
+
phash:queue:failed → List (jobs that exceeded retries)
273
+
```
274
+
275
+
**State Transitions:**
276
+
277
+
```
278
+
Pending → Pop (LPOP) → Processing (RPUSH) → Complete (LREM)
279
+
└─→ Retry (RPUSH to Pending)
280
+
└─→ Failed (RPUSH to Failed)
281
+
```
282
+
283
+
**Job Structure:**
284
+
285
+
```typescript
286
+
interface ImageJob {
287
+
postUri: string // "at://did/app.bsky.feed.post/rkey"
288
+
postCid: string // CID of the post
289
+
postDid: string // Author DID
290
+
blobs: BlobReference[] // [{cid, mimeType?}]
291
+
timestamp: number // When job was created
292
+
attempts: number // Retry counter
293
+
}
294
+
```
295
+
296
+
### File: `src/queue/worker.ts`
297
+
298
+
**Worker Configuration:**
299
+
```typescript
300
+
interface WorkerConfig {
301
+
concurrency: number // Number of parallel workers (default: 10)
302
+
retryAttempts: number // Max retries (default: 3)
303
+
retryDelay: number // MS between retries (default: 1000)
304
+
pollInterval?: number // MS to wait if queue empty (default: 1000)
305
+
}
306
+
```
307
+
308
+
**Processing Loop (per worker thread):**
309
+
310
+
```
311
+
1. Dequeue job
312
+
2. For each blob in job:
313
+
a. Check cache
314
+
b. If miss, fetch blob from PDS
315
+
c. Compute phash
316
+
d. Store in cache
317
+
e. Match against checks
318
+
f. If match, emit onMatchFound event
319
+
3. Mark job complete
320
+
4. If error and retries < max:
321
+
- Re-enqueue with incremented attempts
322
+
5. If error and retries exhausted:
323
+
- Move to failed queue
324
+
```
325
+
326
+
**Error Handling:**
327
+
- Corrupt/invalid images → logged at debug level (expected)
328
+
- Network errors → retry (with backoff)
329
+
- Moderation action failures → logged but don't block further processing
330
+
331
+
**Concurrency Strategy:**
332
+
- N workers running in parallel (default 10)
333
+
- Each worker independently polls queue
334
+
- Active job count tracked
335
+
- Graceful shutdown waits for all jobs
336
+
337
+
---
338
+
339
+
## 6. Image Processing Pipeline
340
+
341
+
### File: `src/processor/image-processor.ts`
342
+
343
+
**High-Level Flow:**
344
+
345
+
```
346
+
1. DID/Check filtering (ignoreDID)
347
+
2. Cache lookup → if hit, use cached phash
348
+
3. Cache miss:
349
+
a. Resolve PDS endpoint from DID document (PLCy)
350
+
b. Check if repo has been taken down
351
+
c. Fetch blob from PDS
352
+
d. Compute phash
353
+
e. Cache result
354
+
4. Match against checks
355
+
5. Return MatchResult (if match found)
356
+
```
357
+
358
+
**Key Implementation Details:**
359
+
360
+
**DID → PDS Resolution:**
361
+
```typescript
362
+
resolvePds(did: string) {
363
+
// GET https://plc.directory/{did}
364
+
// Extract service with id="atproto_pds" and type="AtprotoPersonalDataServer"
365
+
// Return serviceEndpoint URL
366
+
367
+
// Cached per DID to avoid repeated lookups
368
+
}
369
+
```
370
+
371
+
**Blob Fetch:**
372
+
```typescript
373
+
// GET {pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}
374
+
// Returns raw blob data as Buffer
375
+
// Includes redirect: 'follow' to handle PDS redirects
376
+
```
377
+
378
+
**Taken-Down Repo Check:**
379
+
```typescript
380
+
checkRepoTakendown(did: string, pdsEndpoint: string) {
381
+
// GET {pdsEndpoint}/xrpc/com.atproto.repo.describeRepo?repo={did}
382
+
// If error === "RepoTakendown", skip processing
383
+
// Cached per DID to avoid repeated checks
384
+
}
385
+
```
386
+
387
+
**MatchResult Creation:**
388
+
```typescript
389
+
createMatchResult(phash: string, check: BlobCheck): MatchResult | null {
390
+
for (const checkPhash of check.phashes) {
391
+
const distance = hammingDistance(phash, checkPhash)
392
+
const threshold = check.hammingThreshold ?? defaultThreshold
393
+
394
+
if (distance <= threshold) {
395
+
return {
396
+
phash,
397
+
matchedCheck: check,
398
+
matchedPhash: checkPhash,
399
+
hammingDistance: distance
400
+
}
401
+
}
402
+
}
403
+
return null
404
+
}
405
+
```
406
+
407
+
**Caches (in-memory, in ImageProcessor):**
408
+
```typescript
409
+
pdsCache: Map<did, pdsEndpoint|null> // PDS resolution
410
+
repoTakendownCache: Map<did, boolean> // Taken-down status
411
+
```
412
+
413
+
**These are NOT persisted** - they're per-process memory optimization.
414
+
415
+
---
416
+
417
+
## 7. Redis Cache Structure
418
+
419
+
### File: `src/cache/phash-cache.ts`
420
+
421
+
**Purpose:** Cache computed phashes to avoid re-fetching viral images
422
+
423
+
**Key Pattern:** `phash:cache:{cid}` → hex string (phash)
424
+
425
+
**TTL:** Configurable (default: 86400 seconds = 24 hours)
426
+
427
+
**Operations:**
428
+
- `get(cid)` → returns cached phash or null
429
+
- `set(cid, phash)` → stores with TTL
430
+
- `delete(cid)` → immediate removal
431
+
- `clear()` → remove all cache entries
432
+
- `getStats()` → returns cache size
433
+
434
+
**Cache Hit Rate:**
435
+
- Metrics track cache hits vs misses
436
+
- Typical: 20-40% hit rate (viral images)
437
+
438
+
---
439
+
440
+
## 8. Moderation Claims (Deduplication)
441
+
442
+
### File: `src/cache/moderation-claims.ts`
443
+
444
+
**Purpose:** Prevent duplicate moderation actions (labels/reports) within 7 days
445
+
446
+
**Key Patterns:**
447
+
```
448
+
claim:post:label:{uri}:{label} → "1" (TTL: 7 days)
449
+
claim:account:label:{did}:{label} → "1" (TTL: 7 days)
450
+
claim:account:comment:{did}:{uri} → "1" (TTL: 7 days)
451
+
```
452
+
453
+
**TTL:** 604800 seconds = 7 days (hardcoded, not configurable)
454
+
455
+
**Operations (all use SET ... NX):**
456
+
- `tryClaimPostLabel(uri, label)` → tries to acquire claim, returns boolean
457
+
- `tryClaimAccountLabel(did, label)` → tries to acquire claim, returns boolean
458
+
- `tryClaimAccountComment(did, uri)` → tries to acquire claim, returns boolean
459
+
460
+
**Flow in Moderation Actions:**
461
+
462
+
```typescript
463
+
// Before creating post label
464
+
const claimed = await claims.tryClaimPostLabel(uri, label)
465
+
if (!claimed) {
466
+
// Already labeled in past 7 days
467
+
metrics?.increment("labelsCached")
468
+
return
469
+
}
470
+
471
+
// Then check if label already exists in Ozone (race condition safety)
472
+
const hasLabel = await checkRecordLabels(uri, label)
473
+
if (hasLabel) {
474
+
metrics?.increment("labelsCached")
475
+
return
476
+
}
477
+
478
+
// Otherwise, create label via Ozone API
479
+
```
480
+
481
+
**Why both checks?**
482
+
1. Redis claim = "we acted" (fast, distributed)
483
+
2. Ozone check = "label still exists" (authoritative)
484
+
- This handles race conditions if multiple services label simultaneously
485
+
486
+
---
487
+
488
+
## 9. Moderation Action Flows
489
+
490
+
### File: `src/moderation/post.ts`
491
+
492
+
**createPostLabel:**
493
+
```typescript
494
+
// 1. Try claim → if fail, exit (already labeled recently)
495
+
// 2. Check if label exists → if yes, exit
496
+
// 3. Emit event via Ozone API:
497
+
// - $type: "tools.ozone.moderation.defs#modEventLabel"
498
+
// - subject: {uri, cid} (strongRef)
499
+
// - createLabelVals: [label]
500
+
// - comment: "{timestamp}: {comment} at {uri} with phash \"{phash}\""
501
+
// 4. Rate-limited via limit() function
502
+
```
503
+
504
+
**createPostReport:**
505
+
```typescript
506
+
// Same flow but:
507
+
// - $type: "tools.ozone.moderation.defs#modEventReport"
508
+
// - reportType: "com.atproto.moderation.defs#reasonOther"
509
+
// - No claim check needed (reports can repeat)
510
+
```
511
+
512
+
### File: `src/moderation/account.ts`
513
+
514
+
**createAccountLabel:**
515
+
```typescript
516
+
// Similar to post label but:
517
+
// - subject: {$type: "com.atproto.admin.defs#repoRef", did}
518
+
// - Claims work per (did, label) pair
519
+
```
520
+
521
+
**createAccountReport:**
522
+
```typescript
523
+
// Similar to post report but:
524
+
// - subject: {$type: "com.atproto.admin.defs#repoRef", did}
525
+
```
526
+
527
+
**createAccountComment:**
528
+
```typescript
529
+
// Comments on account (metadata only, no label)
530
+
// Claims prevent duplicate comments for same (did, uri) pair
531
+
```
532
+
533
+
**API Headers (All Moderation Calls):**
534
+
```typescript
535
+
{
536
+
encoding: "application/json",
537
+
headers: {
538
+
"atproto-proxy": `${modDid}#atproto_labeler`,
539
+
"atproto-accept-labelers": "did:plc:ar7c4by46qjdydhdevvrndac;redact"
540
+
}
541
+
}
542
+
```
543
+
544
+
**These headers:**
545
+
- Tell Ozone to proxy request through moderation account
546
+
- Request redaction of sensitive PII
547
+
- Are critical for proper audit trails
548
+
549
+
---
550
+
551
+
## 10. Rate Limiting
552
+
553
+
### File: `src/limits.ts`
554
+
555
+
**Two-Level Strategy:**
556
+
557
+
**Level 1: Concurrency Control**
558
+
- p-ratelimit library
559
+
- Limits to 24 concurrent requests
560
+
- Prevents overwhelming Ozone API with simultaneous requests
561
+
562
+
**Level 2: Header-Based Rate Limit Tracking**
563
+
- Ozone API returns headers: `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`
564
+
- Service maintains state and waits if critically low
565
+
- Safety buffer: 5 requests reserved
566
+
- Only waits if `remaining <= 5`
567
+
568
+
**Dynamic Rate Limit State:**
569
+
```typescript
570
+
interface RateLimitState {
571
+
limit: number // e.g., 280 requests per window
572
+
remaining: number // Current budget
573
+
reset: number // Unix timestamp when resets
574
+
}
575
+
```
576
+
577
+
**How moderation actions use limits:**
578
+
```typescript
579
+
await limit(async () => {
580
+
// This will:
581
+
// 1. Check concurrency (max 24 concurrent)
582
+
// 2. Check remaining budget (wait if <5 remaining)
583
+
// 3. Execute request
584
+
// 4. Parse RateLimit headers and update state
585
+
return await agent.tools.ozone.moderation.emitEvent(...)
586
+
})
587
+
```
588
+
589
+
**Prometheus Metrics:**
590
+
- `rate_limit_waits_total` → how many times we waited
591
+
- `rate_limit_wait_duration_seconds` → how long waits took
592
+
- `concurrent_requests` → live count of concurrent calls
593
+
594
+
---
595
+
596
+
## 11. Agent & Authentication
597
+
598
+
### File: `src/agent.ts`
599
+
600
+
**Session Management:**
601
+
602
+
```typescript
603
+
// Session file: .session (chmod 600, NOT in git)
604
+
// Contains: {accessJwt, refreshJwt, did, handle, email, ...}
605
+
606
+
// Login flow:
607
+
1. Try load session from .session file
608
+
2. If loaded, try resume session (verify with getProfile call)
609
+
3. If resume fails or no saved session, perform fresh login
610
+
4. Save session after successful login
611
+
612
+
// Token refresh:
613
+
- JWT lifetime: 2 hours
614
+
- Refresh scheduled at 80% of lifetime (~96 minutes)
615
+
- On refresh failure, fallback to fresh login with retries
616
+
```
617
+
618
+
**Retry Strategy:**
619
+
- MAX_LOGIN_RETRIES = 3
620
+
- RETRY_DELAY_MS = 2000
621
+
- If all retries fail, process exits with error
622
+
623
+
**Undici Configuration:**
624
+
```typescript
625
+
const dispatcher = new Agent({
626
+
connect: { timeout: 20_000 },
627
+
keepAliveTimeout: 10_000,
628
+
keepAliveMaxTimeout: 20_000
629
+
})
630
+
setGlobalDispatcher(dispatcher)
631
+
```
632
+
633
+
This ensures robust HTTP handling for PDS/Ozone API calls.
634
+
635
+
---
636
+
637
+
## 12. Configuration Management
638
+
639
+
### File: `src/config/index.ts`
640
+
641
+
**All settings sourced from environment variables:**
642
+
643
+
```typescript
644
+
// REQUIRED
645
+
LABELER_DID // e.g., "did:plc:..."
646
+
LABELER_HANDLE // e.g., "labeler.bsky.social"
647
+
LABELER_PASSWORD // app password (NOT user password)
648
+
649
+
// Processing
650
+
JETSTREAM_URL // (default: wss://jetstream.atproto.tools/subscribe)
651
+
REDIS_URL // (default: redis://localhost:6379)
652
+
PROCESSING_CONCURRENCY // (default: 10)
653
+
RETRY_ATTEMPTS // (default: 3)
654
+
RETRY_DELAY_MS // (default: 1000)
655
+
CACHE_ENABLED // (default: true)
656
+
CACHE_TTL_SECONDS // (default: 86400)
657
+
658
+
// PDS/Ozone
659
+
PDS_ENDPOINT // (default: https://bsky.social)
660
+
OZONE_URL // (no default)
661
+
OZONE_PDS // (no default - used for agent.service)
662
+
MOD_DID // (no default - used in moderation headers)
663
+
664
+
// Phash
665
+
PHASH_HAMMING_THRESHOLD // (default: 3)
666
+
667
+
// Logging
668
+
LOG_LEVEL // (default: debug/info)
669
+
NODE_ENV // (defaults to production)
670
+
```
671
+
672
+
---
673
+
674
+
## 13. Metrics & Observability
675
+
676
+
### File: `src/metrics/collector.ts`
677
+
678
+
**Simple in-memory metrics:**
679
+
680
+
```typescript
681
+
class MetricsCollector {
682
+
counters: Map<string, number>
683
+
684
+
increment(metric: string, value?: number)
685
+
get(metric: string): number
686
+
getAll(): Record<string, number>
687
+
getWithRates(): Record<string, number|string>
688
+
}
689
+
```
690
+
691
+
**Key Metrics Tracked:**
692
+
693
+
```
694
+
postsProcessed // Jobs completed
695
+
blobsFetched // Blobs downloaded
696
+
blobsProcessed // Blobs hashed successfully
697
+
blobsCorrupted // Invalid images skipped
698
+
fetchErrors // Blob fetch failures
699
+
cacheHits // Phash cache hits
700
+
cacheMisses // Phash cache misses
701
+
matchesFound // Images matching known phashes
702
+
labelsApplied // Labels successfully created
703
+
labelsCached // Labels skipped (already claimed)
704
+
reportsCreated // Reports successfully created
705
+
```
706
+
707
+
**Derived Rates (calculated on-demand):**
708
+
```
709
+
postsPerSecond = postsProcessed / uptimeSeconds
710
+
blobsPerSecond = blobsProcessed / uptimeSeconds
711
+
cacheHitRate = cacheHits / (cacheHits + cacheMisses) * 100
712
+
matchRate = matchesFound / blobsProcessed * 100
713
+
```
714
+
715
+
**Prometheus Integration:**
716
+
- Rate limit metrics via prom-client
717
+
- Not exposed as metrics server (would need HTTP server)
718
+
719
+
**Stats Logged:**
720
+
- Every 60 seconds to logger
721
+
- Includes worker stats, cache stats, metrics with rates
722
+
723
+
---
724
+
725
+
## 14. Logging
726
+
727
+
### File: `src/logger/index.ts`
728
+
729
+
**Pino-based structured logging:**
730
+
731
+
```typescript
732
+
logger.debug(...) // Development details
733
+
logger.info(...) // Normal operation milestones
734
+
logger.warn(...) // Unexpected but recoverable
735
+
logger.error(...) // Errors (usually don't stop service)
736
+
```
737
+
738
+
**Development Mode:**
739
+
- Pretty-printed output
740
+
- Colors, timestamps, field names
741
+
- Ignores pid/hostname
742
+
743
+
**Production Mode:**
744
+
- JSON logs (newline-delimited)
745
+
- Parse with jq or log aggregators
746
+
- Structured for monitoring
747
+
748
+
**Log Levels:**
749
+
- `process` field added manually to track sub-components
750
+
- Examples: `{process: "MAIN"}`, `{process: "MODERATION"}`
751
+
752
+
---
753
+
754
+
## 15. Rules Configuration
755
+
756
+
### File: `rules/blobs.ts`
757
+
758
+
**BlobCheck structure:**
759
+
760
+
```typescript
761
+
interface BlobCheck {
762
+
phashes: string[] // List of known bad phashes
763
+
label: string // Label to apply (e.g., "troll")
764
+
comment: string // Description of harm
765
+
reportAcct: boolean // Report the account?
766
+
labelAcct: boolean // Label the account?
767
+
reportPost: boolean // Report the post?
768
+
toLabel: boolean // Label the post?
769
+
hammingThreshold?: number // Per-check threshold (overrides default)
770
+
description?: string // Internal documentation
771
+
ignoreDID?: string[] // Exempt accounts
772
+
}
773
+
```
774
+
775
+
**Multiple checks evaluated in order:**
776
+
- First match wins (no combining of multiple checks)
777
+
- Each check can independently configure moderation actions
778
+
779
+
**Example:**
780
+
781
+
```typescript
782
+
{
783
+
phashes: ["e0e0e0e0e0fcfefe", "9b9e00008f8fffff"],
784
+
label: "harassment-image",
785
+
comment: "Known harassment image",
786
+
toLabel: true, // Label post
787
+
reportPost: false, // Don't report post
788
+
labelAcct: true, // Label account
789
+
reportAcct: false, // Don't report account
790
+
hammingThreshold: 3, // Strict threshold
791
+
ignoreDID: ["did:plc:trusted-account"] // Don't check this account
792
+
}
793
+
```
794
+
795
+
---
796
+
797
+
## 16. Critical Gotchas & Nuances for Rust Rewrite
798
+
799
+
### 1. **Phash Algorithm Details**
800
+
- Output MUST be exactly 16 hex chars (64-bit)
801
+
- Lowercase
802
+
- Zero-padded
803
+
- Grayscale conversion via luminosity formula (not simple average)
804
+
- 8x8 resize with aspect-ratio preservation
805
+
806
+
### 2. **Hamming Distance**
807
+
- Uses Brian Kernighan's bit-counting algorithm
808
+
- Range 0-64
809
+
- Threshold comparison is `distance <= threshold` (INCLUSIVE)
810
+
811
+
### 3. **Cursor Persistence**
812
+
- Is microsecond timestamp (NOT millisecond)
813
+
- Must persist every 10 seconds
814
+
- Resume from cursor prevents duplicate processing
815
+
- Write to `cursor.txt` in working directory
816
+
817
+
### 4. **Queue State Machine**
818
+
- Never lose jobs mid-processing
819
+
- Pending → Processing → Complete is atomic per job
820
+
- On crash, jobs in Processing list are reprocessed on restart
821
+
- Failed queue accumulates jobs beyond retry limit
822
+
823
+
### 5. **Blob Fetch Strategy**
824
+
- Requires PDS resolution from PLCy first
825
+
- Many PDSes redirect blob endpoints
826
+
- Must follow redirects
827
+
- Check repo.takedown status before fetching
828
+
- Cache both PDS endpoint and takedown status (per process)
829
+
830
+
### 6. **Moderation Claims Dedup**
831
+
- 7-day TTL (hardcoded in moderation-claims.ts)
832
+
- Uses Redis SET ... NX (atomic)
833
+
- Must still check Ozone API (race condition safety)
834
+
- Account claims are per (did, label) pair
835
+
836
+
### 7. **Rate Limiting**
837
+
- TWO separate mechanisms (concurrency + header-based)
838
+
- Only waits when `remaining <= 5` (safety buffer)
839
+
- Ozone returns RateLimit headers
840
+
- Conservative defaults: 280 requests per 30-second window
841
+
- Must not block job processing (only moderation action calls)
842
+
843
+
### 8. **Session Token Management**
844
+
- Tokens live ~2 hours
845
+
- Refresh at 80% of lifetime (~96 minutes)
846
+
- Session saved to `.session` file (must be chmod 600)
847
+
- Resume session before fresh login
848
+
- Multiple login attempts = shared promise (singleton)
849
+
850
+
### 9. **Image Mime Type Filtering**
851
+
- Skip SVG images
852
+
- Only process image/* types
853
+
- mimeType may be missing (optional field)
854
+
855
+
### 10. **Error Handling Patterns**
856
+
- Corrupt images → debug log, skip (expected)
857
+
- Network errors → retry with backoff
858
+
- Moderation action failures → log but continue (don't block)
859
+
- Takden-down repos → skip silently
860
+
- Missing PDS → warn and skip blob
861
+
862
+
### 11. **Config Environment Variables**
863
+
- No defaults for labeler credentials (must be provided)
864
+
- Ozone URL/PDS used differently (URL is separate from service endpoint)
865
+
- OZONE_PDS is the agent.service endpoint
866
+
- OZONE_URL is for display/configuration only
867
+
868
+
### 12. **Redis Key Patterns**
869
+
```
870
+
phash:queue:pending → List
871
+
phash:queue:processing → List
872
+
phash:queue:failed → List
873
+
phash:cache:{cid} → String
874
+
claim:post:label:{uri}:{label} → String
875
+
claim:account:label:{did}:{label} → String
876
+
claim:account:comment:{did}:{uri} → String
877
+
```
878
+
879
+
### 13. **Jetstream Event Structure**
880
+
- Only processes `app.bsky.feed.post` creates
881
+
- Ignores updates/deletes
882
+
- Image extraction handles nested embed structures
883
+
- Blob CID in `ref.$link` field
884
+
885
+
### 14. **Metrics Naming Conventions**
886
+
- Snake_case for Prometheus metrics
887
+
- Counter suffixes: `_total`
888
+
- Gauge suffixes: none (just name)
889
+
- Histogram suffixes: `_seconds` (for duration)
890
+
891
+
### 15. **Graceful Shutdown**
892
+
- Saves cursor to disk
893
+
- Waits for active jobs (polling every 100ms)
894
+
- Closes all connections
895
+
- SIGINT/SIGTERM trigger shutdown
896
+
897
+
---
898
+
899
+
## Architecture Strengths & Assumptions
900
+
901
+
**What Works Well:**
902
+
1. ✅ Decoupled Jetstream ingestion from processing (queue isolates)
903
+
2. ✅ Per-job retry strategy with exponential backoff
904
+
3. ✅ Cache hit rate for viral images reduces PDS load
905
+
4. ✅ Redis claims prevent duplicate moderation actions
906
+
5. ✅ Cursor persistence enables recovery
907
+
6. ✅ Rate limiting respects Ozone API limits
908
+
909
+
**Assumptions/Constraints:**
910
+
1. Redis is always available (no in-memory fallback)
911
+
2. PDS/Ozone/PLCy endpoints are reachable
912
+
3. Jetstream can resume from cursor
913
+
4. Single-process (no distributed workers)
914
+
5. In-memory caches (PDS/takdown) lost on restart
915
+
916
+
---
917
+
918
+
## Typical Performance Characteristics
919
+
920
+
**VM Requirements (from README):**
921
+
- **Minimal:** 2GB RAM, 2 vCPU, 10GB disk
922
+
- **Recommended:** 4GB RAM, 2-4 vCPU, 20GB disk
923
+
924
+
**Throughput:**
925
+
- Concurrency default: 10 workers
926
+
- Each worker can process ~1-5 images/second (depends on network)
927
+
- Total: 10-50 images/second throughput
928
+
- Cache hit rate: 20-40% (viral images)
929
+
930
+
**Latency:**
931
+
- Jetstream event → job enqueue: <100ms
932
+
- Job dequeue → phash computed: 200-500ms (depends on network/image size)
933
+
- Phash → moderation action: 100-200ms (rate-limited)
934
+
935
+
---
936
+
937
+
## Testing Coverage
938
+
939
+
**Unit Tests Exist For:**
940
+
- ✅ Phash algorithm (format, determinism, slight variations)
941
+
- ✅ Hamming distance (exact match, thresholds, edge cases)
942
+
- ✅ Cache hit/miss
943
+
- ✅ Queue operations
944
+
- ✅ Image processor logic
945
+
946
+
**Integration Tests:**
947
+
- ✅ Queue persistence across restart
948
+
- ✅ End-to-end job processing
949
+
950
+
**No Tests For:**
951
+
- Jetstream connection handling
952
+
- Moderation action API calls
953
+
- Session management
954
+
- Rate limiting
955
+
956
+
---
957
+
958
+
## File Structure Summary
959
+
960
+
```
961
+
src/
962
+
├── main.ts # Entry point, Jetstream orchestrator
963
+
├── types.ts # Core interfaces (BlobCheck, ImageJob, MatchResult)
964
+
├── agent.ts # ATProto agent, session management
965
+
├── session.ts # Session file persistence
966
+
├── limits.ts # Rate limiting (concurrency + header-based)
967
+
├── config/
968
+
│ └── index.ts # Environment variable parsing
969
+
├── logger/
970
+
│ └── index.ts # Pino structured logging
971
+
├── hasher/
972
+
│ └── phash.ts # Perceptual hash computation
973
+
├── matcher/
974
+
│ └── hamming.ts # Hamming distance, match finding
975
+
├── cache/
976
+
│ ├── phash-cache.ts # Redis phash cache
977
+
│ └── moderation-claims.ts # Redis deduplication claims
978
+
├── queue/
979
+
│ ├── redis-queue.ts # Redis job queue
980
+
│ └── worker.ts # Worker pool, job processing loop
981
+
├── processor/
982
+
│ └── image-processor.ts # Blob fetch, phash compute, matching
983
+
├── moderation/
984
+
│ ├── post.ts # Post label/report actions
985
+
│ └── account.ts # Account label/report/comment actions
986
+
└── metrics/
987
+
└── collector.ts # In-memory metrics
988
+
989
+
rules/
990
+
└── blobs.ts # BlobCheck definitions
991
+
992
+
tests/
993
+
├── unit/
994
+
│ ├── phash.test.ts
995
+
│ ├── hamming.test.ts
996
+
│ ├── phash-cache.test.ts
997
+
│ └── image-processor.test.ts
998
+
└── integration/
999
+
└── queue.test.ts
1000
+
```
1001
+
1002
+
---
1003
+
1004
+
## Entry Point Summary
1005
+
1006
+
The entire system starts with:
1007
+
1008
+
1. **main.ts** loads config, connects Redis, logs in to Ozone
1009
+
2. **Jetstream** connects and starts emitting post events
1010
+
3. **Queue** receives jobs (post + blob CIDs)
1011
+
4. **Worker pool** dequeues jobs and processes them in parallel
1012
+
5. **ImageProcessor** fetches blobs, computes phashes, finds matches
1013
+
6. **Moderation** actions (label/report) triggered on matches
1014
+
7. **Metrics/Logging** tracked throughout
1015
+
8. **Graceful shutdown** saves cursor and exits cleanly
1016
+
1017
+
This is a **real-time, event-driven system** - no polling, no batch processing. Every post on Bluesky is potentially seen and processed within seconds.
1018
+
+5737
Cargo.lock
+5737
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 = "abnf"
7
+
version = "0.13.0"
8
+
source = "registry+https://github.com/rust-lang/crates.io-index"
9
+
checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a"
10
+
dependencies = [
11
+
"abnf-core",
12
+
"nom 7.1.3",
13
+
]
14
+
15
+
[[package]]
16
+
name = "abnf-core"
17
+
version = "0.5.0"
18
+
source = "registry+https://github.com/rust-lang/crates.io-index"
19
+
checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d"
20
+
dependencies = [
21
+
"nom 7.1.3",
22
+
]
23
+
24
+
[[package]]
25
+
name = "addr2line"
26
+
version = "0.25.1"
27
+
source = "registry+https://github.com/rust-lang/crates.io-index"
28
+
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
29
+
dependencies = [
30
+
"gimli",
31
+
]
32
+
33
+
[[package]]
34
+
name = "adler2"
35
+
version = "2.0.1"
36
+
source = "registry+https://github.com/rust-lang/crates.io-index"
37
+
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
38
+
39
+
[[package]]
40
+
name = "adler32"
41
+
version = "1.2.0"
42
+
source = "registry+https://github.com/rust-lang/crates.io-index"
43
+
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
44
+
45
+
[[package]]
46
+
name = "aho-corasick"
47
+
version = "1.1.3"
48
+
source = "registry+https://github.com/rust-lang/crates.io-index"
49
+
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
50
+
dependencies = [
51
+
"memchr",
52
+
]
53
+
54
+
[[package]]
55
+
name = "aliasable"
56
+
version = "0.1.3"
57
+
source = "registry+https://github.com/rust-lang/crates.io-index"
58
+
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
59
+
60
+
[[package]]
61
+
name = "aligned-vec"
62
+
version = "0.6.4"
63
+
source = "registry+https://github.com/rust-lang/crates.io-index"
64
+
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
65
+
dependencies = [
66
+
"equator",
67
+
]
68
+
69
+
[[package]]
70
+
name = "alloc-no-stdlib"
71
+
version = "2.0.4"
72
+
source = "registry+https://github.com/rust-lang/crates.io-index"
73
+
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
74
+
75
+
[[package]]
76
+
name = "alloc-stdlib"
77
+
version = "0.2.2"
78
+
source = "registry+https://github.com/rust-lang/crates.io-index"
79
+
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
80
+
dependencies = [
81
+
"alloc-no-stdlib",
82
+
]
83
+
84
+
[[package]]
85
+
name = "android_system_properties"
86
+
version = "0.1.5"
87
+
source = "registry+https://github.com/rust-lang/crates.io-index"
88
+
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
89
+
dependencies = [
90
+
"libc",
91
+
]
92
+
93
+
[[package]]
94
+
name = "anyhow"
95
+
version = "1.0.100"
96
+
source = "registry+https://github.com/rust-lang/crates.io-index"
97
+
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
98
+
99
+
[[package]]
100
+
name = "arbitrary"
101
+
version = "1.4.2"
102
+
source = "registry+https://github.com/rust-lang/crates.io-index"
103
+
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
104
+
105
+
[[package]]
106
+
name = "arc-swap"
107
+
version = "1.7.1"
108
+
source = "registry+https://github.com/rust-lang/crates.io-index"
109
+
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
110
+
111
+
[[package]]
112
+
name = "arg_enum_proc_macro"
113
+
version = "0.3.4"
114
+
source = "registry+https://github.com/rust-lang/crates.io-index"
115
+
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
116
+
dependencies = [
117
+
"proc-macro2",
118
+
"quote",
119
+
"syn 2.0.108",
120
+
]
121
+
122
+
[[package]]
123
+
name = "arrayvec"
124
+
version = "0.7.6"
125
+
source = "registry+https://github.com/rust-lang/crates.io-index"
126
+
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
127
+
128
+
[[package]]
129
+
name = "ascii"
130
+
version = "1.1.0"
131
+
source = "registry+https://github.com/rust-lang/crates.io-index"
132
+
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
133
+
134
+
[[package]]
135
+
name = "assert-json-diff"
136
+
version = "2.0.2"
137
+
source = "registry+https://github.com/rust-lang/crates.io-index"
138
+
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
139
+
dependencies = [
140
+
"serde",
141
+
"serde_json",
142
+
]
143
+
144
+
[[package]]
145
+
name = "async-compression"
146
+
version = "0.4.32"
147
+
source = "registry+https://github.com/rust-lang/crates.io-index"
148
+
checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0"
149
+
dependencies = [
150
+
"compression-codecs",
151
+
"compression-core",
152
+
"futures-core",
153
+
"pin-project-lite",
154
+
"tokio",
155
+
]
156
+
157
+
[[package]]
158
+
name = "async-stream"
159
+
version = "0.3.6"
160
+
source = "registry+https://github.com/rust-lang/crates.io-index"
161
+
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
162
+
dependencies = [
163
+
"async-stream-impl",
164
+
"futures-core",
165
+
"pin-project-lite",
166
+
]
167
+
168
+
[[package]]
169
+
name = "async-stream-impl"
170
+
version = "0.3.6"
171
+
source = "registry+https://github.com/rust-lang/crates.io-index"
172
+
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
173
+
dependencies = [
174
+
"proc-macro2",
175
+
"quote",
176
+
"syn 2.0.108",
177
+
]
178
+
179
+
[[package]]
180
+
name = "async-trait"
181
+
version = "0.1.89"
182
+
source = "registry+https://github.com/rust-lang/crates.io-index"
183
+
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
184
+
dependencies = [
185
+
"proc-macro2",
186
+
"quote",
187
+
"syn 2.0.108",
188
+
]
189
+
190
+
[[package]]
191
+
name = "atomic-waker"
192
+
version = "1.1.2"
193
+
source = "registry+https://github.com/rust-lang/crates.io-index"
194
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
195
+
196
+
[[package]]
197
+
name = "autocfg"
198
+
version = "1.5.0"
199
+
source = "registry+https://github.com/rust-lang/crates.io-index"
200
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
201
+
202
+
[[package]]
203
+
name = "av1-grain"
204
+
version = "0.2.5"
205
+
source = "registry+https://github.com/rust-lang/crates.io-index"
206
+
checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
207
+
dependencies = [
208
+
"anyhow",
209
+
"arrayvec",
210
+
"log",
211
+
"nom 8.0.0",
212
+
"num-rational",
213
+
"v_frame",
214
+
]
215
+
216
+
[[package]]
217
+
name = "avif-serialize"
218
+
version = "0.8.6"
219
+
source = "registry+https://github.com/rust-lang/crates.io-index"
220
+
checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f"
221
+
dependencies = [
222
+
"arrayvec",
223
+
]
224
+
225
+
[[package]]
226
+
name = "backon"
227
+
version = "1.6.0"
228
+
source = "registry+https://github.com/rust-lang/crates.io-index"
229
+
checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"
230
+
dependencies = [
231
+
"fastrand",
232
+
]
233
+
234
+
[[package]]
235
+
name = "backtrace"
236
+
version = "0.3.76"
237
+
source = "registry+https://github.com/rust-lang/crates.io-index"
238
+
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
239
+
dependencies = [
240
+
"addr2line",
241
+
"cfg-if",
242
+
"libc",
243
+
"miniz_oxide",
244
+
"object",
245
+
"rustc-demangle",
246
+
"windows-link 0.2.1",
247
+
]
248
+
249
+
[[package]]
250
+
name = "backtrace-ext"
251
+
version = "0.2.1"
252
+
source = "registry+https://github.com/rust-lang/crates.io-index"
253
+
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
254
+
dependencies = [
255
+
"backtrace",
256
+
]
257
+
258
+
[[package]]
259
+
name = "base-x"
260
+
version = "0.2.11"
261
+
source = "registry+https://github.com/rust-lang/crates.io-index"
262
+
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
263
+
264
+
[[package]]
265
+
name = "base16ct"
266
+
version = "0.2.0"
267
+
source = "registry+https://github.com/rust-lang/crates.io-index"
268
+
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
269
+
270
+
[[package]]
271
+
name = "base256emoji"
272
+
version = "1.0.2"
273
+
source = "registry+https://github.com/rust-lang/crates.io-index"
274
+
checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c"
275
+
dependencies = [
276
+
"const-str",
277
+
"match-lookup",
278
+
]
279
+
280
+
[[package]]
281
+
name = "base64"
282
+
version = "0.13.1"
283
+
source = "registry+https://github.com/rust-lang/crates.io-index"
284
+
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
285
+
286
+
[[package]]
287
+
name = "base64"
288
+
version = "0.22.1"
289
+
source = "registry+https://github.com/rust-lang/crates.io-index"
290
+
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
291
+
292
+
[[package]]
293
+
name = "base64ct"
294
+
version = "1.8.0"
295
+
source = "registry+https://github.com/rust-lang/crates.io-index"
296
+
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
297
+
298
+
[[package]]
299
+
name = "bit_field"
300
+
version = "0.10.3"
301
+
source = "registry+https://github.com/rust-lang/crates.io-index"
302
+
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
303
+
304
+
[[package]]
305
+
name = "bitflags"
306
+
version = "2.10.0"
307
+
source = "registry+https://github.com/rust-lang/crates.io-index"
308
+
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
309
+
310
+
[[package]]
311
+
name = "bitstream-io"
312
+
version = "2.6.0"
313
+
source = "registry+https://github.com/rust-lang/crates.io-index"
314
+
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
315
+
316
+
[[package]]
317
+
name = "block-buffer"
318
+
version = "0.10.4"
319
+
source = "registry+https://github.com/rust-lang/crates.io-index"
320
+
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
321
+
dependencies = [
322
+
"generic-array",
323
+
]
324
+
325
+
[[package]]
326
+
name = "bon"
327
+
version = "3.8.1"
328
+
source = "registry+https://github.com/rust-lang/crates.io-index"
329
+
checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1"
330
+
dependencies = [
331
+
"bon-macros",
332
+
"rustversion",
333
+
]
334
+
335
+
[[package]]
336
+
name = "bon-macros"
337
+
version = "3.8.1"
338
+
source = "registry+https://github.com/rust-lang/crates.io-index"
339
+
checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645"
340
+
dependencies = [
341
+
"darling",
342
+
"ident_case",
343
+
"prettyplease",
344
+
"proc-macro2",
345
+
"quote",
346
+
"rustversion",
347
+
"syn 2.0.108",
348
+
]
349
+
350
+
[[package]]
351
+
name = "borsh"
352
+
version = "1.5.7"
353
+
source = "registry+https://github.com/rust-lang/crates.io-index"
354
+
checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
355
+
dependencies = [
356
+
"cfg_aliases",
357
+
]
358
+
359
+
[[package]]
360
+
name = "brotli"
361
+
version = "3.5.0"
362
+
source = "registry+https://github.com/rust-lang/crates.io-index"
363
+
checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
364
+
dependencies = [
365
+
"alloc-no-stdlib",
366
+
"alloc-stdlib",
367
+
"brotli-decompressor",
368
+
]
369
+
370
+
[[package]]
371
+
name = "brotli-decompressor"
372
+
version = "2.5.1"
373
+
source = "registry+https://github.com/rust-lang/crates.io-index"
374
+
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
375
+
dependencies = [
376
+
"alloc-no-stdlib",
377
+
"alloc-stdlib",
378
+
]
379
+
380
+
[[package]]
381
+
name = "btree-range-map"
382
+
version = "0.7.2"
383
+
source = "registry+https://github.com/rust-lang/crates.io-index"
384
+
checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33"
385
+
dependencies = [
386
+
"btree-slab",
387
+
"cc-traits",
388
+
"range-traits",
389
+
"serde",
390
+
"slab",
391
+
]
392
+
393
+
[[package]]
394
+
name = "btree-slab"
395
+
version = "0.6.1"
396
+
source = "registry+https://github.com/rust-lang/crates.io-index"
397
+
checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c"
398
+
dependencies = [
399
+
"cc-traits",
400
+
"slab",
401
+
"smallvec",
402
+
]
403
+
404
+
[[package]]
405
+
name = "buf_redux"
406
+
version = "0.8.4"
407
+
source = "registry+https://github.com/rust-lang/crates.io-index"
408
+
checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
409
+
dependencies = [
410
+
"memchr",
411
+
"safemem",
412
+
]
413
+
414
+
[[package]]
415
+
name = "built"
416
+
version = "0.7.7"
417
+
source = "registry+https://github.com/rust-lang/crates.io-index"
418
+
checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
419
+
420
+
[[package]]
421
+
name = "bumpalo"
422
+
version = "3.19.0"
423
+
source = "registry+https://github.com/rust-lang/crates.io-index"
424
+
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
425
+
426
+
[[package]]
427
+
name = "bytemuck"
428
+
version = "1.24.0"
429
+
source = "registry+https://github.com/rust-lang/crates.io-index"
430
+
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
431
+
432
+
[[package]]
433
+
name = "byteorder"
434
+
version = "1.5.0"
435
+
source = "registry+https://github.com/rust-lang/crates.io-index"
436
+
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
437
+
438
+
[[package]]
439
+
name = "byteorder-lite"
440
+
version = "0.1.0"
441
+
source = "registry+https://github.com/rust-lang/crates.io-index"
442
+
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
443
+
444
+
[[package]]
445
+
name = "bytes"
446
+
version = "1.10.1"
447
+
source = "registry+https://github.com/rust-lang/crates.io-index"
448
+
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
449
+
dependencies = [
450
+
"serde",
451
+
]
452
+
453
+
[[package]]
454
+
name = "cbor4ii"
455
+
version = "0.2.14"
456
+
source = "registry+https://github.com/rust-lang/crates.io-index"
457
+
checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4"
458
+
dependencies = [
459
+
"serde",
460
+
]
461
+
462
+
[[package]]
463
+
name = "cc"
464
+
version = "1.2.41"
465
+
source = "registry+https://github.com/rust-lang/crates.io-index"
466
+
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
467
+
dependencies = [
468
+
"find-msvc-tools",
469
+
"jobserver",
470
+
"libc",
471
+
"shlex",
472
+
]
473
+
474
+
[[package]]
475
+
name = "cc-traits"
476
+
version = "2.0.0"
477
+
source = "registry+https://github.com/rust-lang/crates.io-index"
478
+
checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5"
479
+
dependencies = [
480
+
"slab",
481
+
]
482
+
483
+
[[package]]
484
+
name = "cesu8"
485
+
version = "1.1.0"
486
+
source = "registry+https://github.com/rust-lang/crates.io-index"
487
+
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
488
+
489
+
[[package]]
490
+
name = "cfg-expr"
491
+
version = "0.15.8"
492
+
source = "registry+https://github.com/rust-lang/crates.io-index"
493
+
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
494
+
dependencies = [
495
+
"smallvec",
496
+
"target-lexicon",
497
+
]
498
+
499
+
[[package]]
500
+
name = "cfg-if"
501
+
version = "1.0.4"
502
+
source = "registry+https://github.com/rust-lang/crates.io-index"
503
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
504
+
505
+
[[package]]
506
+
name = "cfg_aliases"
507
+
version = "0.2.1"
508
+
source = "registry+https://github.com/rust-lang/crates.io-index"
509
+
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
510
+
511
+
[[package]]
512
+
name = "chrono"
513
+
version = "0.4.42"
514
+
source = "registry+https://github.com/rust-lang/crates.io-index"
515
+
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
516
+
dependencies = [
517
+
"iana-time-zone",
518
+
"js-sys",
519
+
"num-traits",
520
+
"wasm-bindgen",
521
+
"windows-link 0.2.1",
522
+
]
523
+
524
+
[[package]]
525
+
name = "chunked_transfer"
526
+
version = "1.5.0"
527
+
source = "registry+https://github.com/rust-lang/crates.io-index"
528
+
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
529
+
530
+
[[package]]
531
+
name = "ciborium"
532
+
version = "0.2.2"
533
+
source = "registry+https://github.com/rust-lang/crates.io-index"
534
+
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
535
+
dependencies = [
536
+
"ciborium-io",
537
+
"ciborium-ll",
538
+
"serde",
539
+
]
540
+
541
+
[[package]]
542
+
name = "ciborium-io"
543
+
version = "0.2.2"
544
+
source = "registry+https://github.com/rust-lang/crates.io-index"
545
+
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
546
+
547
+
[[package]]
548
+
name = "ciborium-ll"
549
+
version = "0.2.2"
550
+
source = "registry+https://github.com/rust-lang/crates.io-index"
551
+
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
552
+
dependencies = [
553
+
"ciborium-io",
554
+
"half",
555
+
]
556
+
557
+
[[package]]
558
+
name = "cid"
559
+
version = "0.11.1"
560
+
source = "registry+https://github.com/rust-lang/crates.io-index"
561
+
checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a"
562
+
dependencies = [
563
+
"core2",
564
+
"multibase",
565
+
"multihash",
566
+
"serde",
567
+
"serde_bytes",
568
+
"unsigned-varint",
569
+
]
570
+
571
+
[[package]]
572
+
name = "color_quant"
573
+
version = "1.1.0"
574
+
source = "registry+https://github.com/rust-lang/crates.io-index"
575
+
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
576
+
577
+
[[package]]
578
+
name = "colored"
579
+
version = "3.0.0"
580
+
source = "registry+https://github.com/rust-lang/crates.io-index"
581
+
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
582
+
dependencies = [
583
+
"windows-sys 0.59.0",
584
+
]
585
+
586
+
[[package]]
587
+
name = "combine"
588
+
version = "4.6.7"
589
+
source = "registry+https://github.com/rust-lang/crates.io-index"
590
+
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
591
+
dependencies = [
592
+
"bytes",
593
+
"futures-core",
594
+
"memchr",
595
+
"pin-project-lite",
596
+
"tokio",
597
+
"tokio-util",
598
+
]
599
+
600
+
[[package]]
601
+
name = "compression-codecs"
602
+
version = "0.4.31"
603
+
source = "registry+https://github.com/rust-lang/crates.io-index"
604
+
checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23"
605
+
dependencies = [
606
+
"compression-core",
607
+
"flate2",
608
+
"memchr",
609
+
]
610
+
611
+
[[package]]
612
+
name = "compression-core"
613
+
version = "0.4.29"
614
+
source = "registry+https://github.com/rust-lang/crates.io-index"
615
+
checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb"
616
+
617
+
[[package]]
618
+
name = "const-oid"
619
+
version = "0.9.6"
620
+
source = "registry+https://github.com/rust-lang/crates.io-index"
621
+
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
622
+
623
+
[[package]]
624
+
name = "const-str"
625
+
version = "0.4.3"
626
+
source = "registry+https://github.com/rust-lang/crates.io-index"
627
+
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
628
+
629
+
[[package]]
630
+
name = "cordyceps"
631
+
version = "0.3.4"
632
+
source = "registry+https://github.com/rust-lang/crates.io-index"
633
+
checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a"
634
+
dependencies = [
635
+
"loom",
636
+
"tracing",
637
+
]
638
+
639
+
[[package]]
640
+
name = "core-foundation"
641
+
version = "0.9.4"
642
+
source = "registry+https://github.com/rust-lang/crates.io-index"
643
+
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
644
+
dependencies = [
645
+
"core-foundation-sys",
646
+
"libc",
647
+
]
648
+
649
+
[[package]]
650
+
name = "core-foundation"
651
+
version = "0.10.1"
652
+
source = "registry+https://github.com/rust-lang/crates.io-index"
653
+
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
654
+
dependencies = [
655
+
"core-foundation-sys",
656
+
"libc",
657
+
]
658
+
659
+
[[package]]
660
+
name = "core-foundation-sys"
661
+
version = "0.8.7"
662
+
source = "registry+https://github.com/rust-lang/crates.io-index"
663
+
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
664
+
665
+
[[package]]
666
+
name = "core2"
667
+
version = "0.4.0"
668
+
source = "registry+https://github.com/rust-lang/crates.io-index"
669
+
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
670
+
dependencies = [
671
+
"memchr",
672
+
]
673
+
674
+
[[package]]
675
+
name = "cpufeatures"
676
+
version = "0.2.17"
677
+
source = "registry+https://github.com/rust-lang/crates.io-index"
678
+
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
679
+
dependencies = [
680
+
"libc",
681
+
]
682
+
683
+
[[package]]
684
+
name = "crc32fast"
685
+
version = "1.5.0"
686
+
source = "registry+https://github.com/rust-lang/crates.io-index"
687
+
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
688
+
dependencies = [
689
+
"cfg-if",
690
+
]
691
+
692
+
[[package]]
693
+
name = "crossbeam-deque"
694
+
version = "0.8.6"
695
+
source = "registry+https://github.com/rust-lang/crates.io-index"
696
+
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
697
+
dependencies = [
698
+
"crossbeam-epoch",
699
+
"crossbeam-utils",
700
+
]
701
+
702
+
[[package]]
703
+
name = "crossbeam-epoch"
704
+
version = "0.9.18"
705
+
source = "registry+https://github.com/rust-lang/crates.io-index"
706
+
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
707
+
dependencies = [
708
+
"crossbeam-utils",
709
+
]
710
+
711
+
[[package]]
712
+
name = "crossbeam-utils"
713
+
version = "0.8.21"
714
+
source = "registry+https://github.com/rust-lang/crates.io-index"
715
+
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
716
+
717
+
[[package]]
718
+
name = "crunchy"
719
+
version = "0.2.4"
720
+
source = "registry+https://github.com/rust-lang/crates.io-index"
721
+
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
722
+
723
+
[[package]]
724
+
name = "crypto-bigint"
725
+
version = "0.5.5"
726
+
source = "registry+https://github.com/rust-lang/crates.io-index"
727
+
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
728
+
dependencies = [
729
+
"generic-array",
730
+
"rand_core 0.6.4",
731
+
"subtle",
732
+
"zeroize",
733
+
]
734
+
735
+
[[package]]
736
+
name = "crypto-common"
737
+
version = "0.1.6"
738
+
source = "registry+https://github.com/rust-lang/crates.io-index"
739
+
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
740
+
dependencies = [
741
+
"generic-array",
742
+
"typenum",
743
+
]
744
+
745
+
[[package]]
746
+
name = "darling"
747
+
version = "0.21.3"
748
+
source = "registry+https://github.com/rust-lang/crates.io-index"
749
+
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
750
+
dependencies = [
751
+
"darling_core",
752
+
"darling_macro",
753
+
]
754
+
755
+
[[package]]
756
+
name = "darling_core"
757
+
version = "0.21.3"
758
+
source = "registry+https://github.com/rust-lang/crates.io-index"
759
+
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
760
+
dependencies = [
761
+
"fnv",
762
+
"ident_case",
763
+
"proc-macro2",
764
+
"quote",
765
+
"strsim",
766
+
"syn 2.0.108",
767
+
]
768
+
769
+
[[package]]
770
+
name = "darling_macro"
771
+
version = "0.21.3"
772
+
source = "registry+https://github.com/rust-lang/crates.io-index"
773
+
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
774
+
dependencies = [
775
+
"darling_core",
776
+
"quote",
777
+
"syn 2.0.108",
778
+
]
779
+
780
+
[[package]]
781
+
name = "dashmap"
782
+
version = "6.1.0"
783
+
source = "registry+https://github.com/rust-lang/crates.io-index"
784
+
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
785
+
dependencies = [
786
+
"cfg-if",
787
+
"crossbeam-utils",
788
+
"hashbrown 0.14.5",
789
+
"lock_api",
790
+
"once_cell",
791
+
"parking_lot_core",
792
+
]
793
+
794
+
[[package]]
795
+
name = "data-encoding"
796
+
version = "2.9.0"
797
+
source = "registry+https://github.com/rust-lang/crates.io-index"
798
+
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
799
+
800
+
[[package]]
801
+
name = "data-encoding-macro"
802
+
version = "0.1.18"
803
+
source = "registry+https://github.com/rust-lang/crates.io-index"
804
+
checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d"
805
+
dependencies = [
806
+
"data-encoding",
807
+
"data-encoding-macro-internal",
808
+
]
809
+
810
+
[[package]]
811
+
name = "data-encoding-macro-internal"
812
+
version = "0.1.16"
813
+
source = "registry+https://github.com/rust-lang/crates.io-index"
814
+
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
815
+
dependencies = [
816
+
"data-encoding",
817
+
"syn 2.0.108",
818
+
]
819
+
820
+
[[package]]
821
+
name = "deflate"
822
+
version = "1.0.0"
823
+
source = "registry+https://github.com/rust-lang/crates.io-index"
824
+
checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f"
825
+
dependencies = [
826
+
"adler32",
827
+
"gzip-header",
828
+
]
829
+
830
+
[[package]]
831
+
name = "der"
832
+
version = "0.7.10"
833
+
source = "registry+https://github.com/rust-lang/crates.io-index"
834
+
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
835
+
dependencies = [
836
+
"const-oid",
837
+
"pem-rfc7468",
838
+
"zeroize",
839
+
]
840
+
841
+
[[package]]
842
+
name = "deranged"
843
+
version = "0.5.4"
844
+
source = "registry+https://github.com/rust-lang/crates.io-index"
845
+
checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
846
+
dependencies = [
847
+
"powerfmt",
848
+
]
849
+
850
+
[[package]]
851
+
name = "derive_more"
852
+
version = "1.0.0"
853
+
source = "registry+https://github.com/rust-lang/crates.io-index"
854
+
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
855
+
dependencies = [
856
+
"derive_more-impl",
857
+
]
858
+
859
+
[[package]]
860
+
name = "derive_more-impl"
861
+
version = "1.0.0"
862
+
source = "registry+https://github.com/rust-lang/crates.io-index"
863
+
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
864
+
dependencies = [
865
+
"proc-macro2",
866
+
"quote",
867
+
"syn 2.0.108",
868
+
"unicode-xid",
869
+
]
870
+
871
+
[[package]]
872
+
name = "diatomic-waker"
873
+
version = "0.2.3"
874
+
source = "registry+https://github.com/rust-lang/crates.io-index"
875
+
checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c"
876
+
877
+
[[package]]
878
+
name = "digest"
879
+
version = "0.10.7"
880
+
source = "registry+https://github.com/rust-lang/crates.io-index"
881
+
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
882
+
dependencies = [
883
+
"block-buffer",
884
+
"const-oid",
885
+
"crypto-common",
886
+
"subtle",
887
+
]
888
+
889
+
[[package]]
890
+
name = "displaydoc"
891
+
version = "0.2.5"
892
+
source = "registry+https://github.com/rust-lang/crates.io-index"
893
+
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
894
+
dependencies = [
895
+
"proc-macro2",
896
+
"quote",
897
+
"syn 2.0.108",
898
+
]
899
+
900
+
[[package]]
901
+
name = "dotenvy"
902
+
version = "0.15.7"
903
+
source = "registry+https://github.com/rust-lang/crates.io-index"
904
+
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
905
+
906
+
[[package]]
907
+
name = "ecdsa"
908
+
version = "0.16.9"
909
+
source = "registry+https://github.com/rust-lang/crates.io-index"
910
+
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
911
+
dependencies = [
912
+
"der",
913
+
"digest",
914
+
"elliptic-curve",
915
+
"rfc6979",
916
+
"signature",
917
+
"spki",
918
+
]
919
+
920
+
[[package]]
921
+
name = "either"
922
+
version = "1.15.0"
923
+
source = "registry+https://github.com/rust-lang/crates.io-index"
924
+
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
925
+
926
+
[[package]]
927
+
name = "elliptic-curve"
928
+
version = "0.13.8"
929
+
source = "registry+https://github.com/rust-lang/crates.io-index"
930
+
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
931
+
dependencies = [
932
+
"base16ct",
933
+
"crypto-bigint",
934
+
"digest",
935
+
"ff",
936
+
"generic-array",
937
+
"group",
938
+
"pem-rfc7468",
939
+
"pkcs8",
940
+
"rand_core 0.6.4",
941
+
"sec1",
942
+
"subtle",
943
+
"zeroize",
944
+
]
945
+
946
+
[[package]]
947
+
name = "encoding_rs"
948
+
version = "0.8.35"
949
+
source = "registry+https://github.com/rust-lang/crates.io-index"
950
+
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
951
+
dependencies = [
952
+
"cfg-if",
953
+
]
954
+
955
+
[[package]]
956
+
name = "enum-as-inner"
957
+
version = "0.6.1"
958
+
source = "registry+https://github.com/rust-lang/crates.io-index"
959
+
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
960
+
dependencies = [
961
+
"heck 0.5.0",
962
+
"proc-macro2",
963
+
"quote",
964
+
"syn 2.0.108",
965
+
]
966
+
967
+
[[package]]
968
+
name = "equator"
969
+
version = "0.4.2"
970
+
source = "registry+https://github.com/rust-lang/crates.io-index"
971
+
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
972
+
dependencies = [
973
+
"equator-macro",
974
+
]
975
+
976
+
[[package]]
977
+
name = "equator-macro"
978
+
version = "0.4.2"
979
+
source = "registry+https://github.com/rust-lang/crates.io-index"
980
+
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
981
+
dependencies = [
982
+
"proc-macro2",
983
+
"quote",
984
+
"syn 2.0.108",
985
+
]
986
+
987
+
[[package]]
988
+
name = "equivalent"
989
+
version = "1.0.2"
990
+
source = "registry+https://github.com/rust-lang/crates.io-index"
991
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
992
+
993
+
[[package]]
994
+
name = "errno"
995
+
version = "0.3.14"
996
+
source = "registry+https://github.com/rust-lang/crates.io-index"
997
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
998
+
dependencies = [
999
+
"libc",
1000
+
"windows-sys 0.61.2",
1001
+
]
1002
+
1003
+
[[package]]
1004
+
name = "exr"
1005
+
version = "1.73.0"
1006
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1007
+
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
1008
+
dependencies = [
1009
+
"bit_field",
1010
+
"half",
1011
+
"lebe",
1012
+
"miniz_oxide",
1013
+
"rayon-core",
1014
+
"smallvec",
1015
+
"zune-inflate",
1016
+
]
1017
+
1018
+
[[package]]
1019
+
name = "fastrand"
1020
+
version = "2.3.0"
1021
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1022
+
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
1023
+
1024
+
[[package]]
1025
+
name = "fax"
1026
+
version = "0.2.6"
1027
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1028
+
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
1029
+
dependencies = [
1030
+
"fax_derive",
1031
+
]
1032
+
1033
+
[[package]]
1034
+
name = "fax_derive"
1035
+
version = "0.2.0"
1036
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1037
+
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
1038
+
dependencies = [
1039
+
"proc-macro2",
1040
+
"quote",
1041
+
"syn 2.0.108",
1042
+
]
1043
+
1044
+
[[package]]
1045
+
name = "fdeflate"
1046
+
version = "0.3.7"
1047
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1048
+
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
1049
+
dependencies = [
1050
+
"simd-adler32",
1051
+
]
1052
+
1053
+
[[package]]
1054
+
name = "ff"
1055
+
version = "0.13.1"
1056
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1057
+
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
1058
+
dependencies = [
1059
+
"rand_core 0.6.4",
1060
+
"subtle",
1061
+
]
1062
+
1063
+
[[package]]
1064
+
name = "filetime"
1065
+
version = "0.2.26"
1066
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1067
+
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
1068
+
dependencies = [
1069
+
"cfg-if",
1070
+
"libc",
1071
+
"libredox",
1072
+
"windows-sys 0.60.2",
1073
+
]
1074
+
1075
+
[[package]]
1076
+
name = "find-msvc-tools"
1077
+
version = "0.1.4"
1078
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1079
+
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
1080
+
1081
+
[[package]]
1082
+
name = "flate2"
1083
+
version = "1.1.4"
1084
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1085
+
checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9"
1086
+
dependencies = [
1087
+
"crc32fast",
1088
+
"miniz_oxide",
1089
+
]
1090
+
1091
+
[[package]]
1092
+
name = "fnv"
1093
+
version = "1.0.7"
1094
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1095
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
1096
+
1097
+
[[package]]
1098
+
name = "foreign-types"
1099
+
version = "0.3.2"
1100
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1101
+
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
1102
+
dependencies = [
1103
+
"foreign-types-shared",
1104
+
]
1105
+
1106
+
[[package]]
1107
+
name = "foreign-types-shared"
1108
+
version = "0.1.1"
1109
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1110
+
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
1111
+
1112
+
[[package]]
1113
+
name = "form_urlencoded"
1114
+
version = "1.2.2"
1115
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1116
+
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
1117
+
dependencies = [
1118
+
"percent-encoding",
1119
+
]
1120
+
1121
+
[[package]]
1122
+
name = "futf"
1123
+
version = "0.1.5"
1124
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1125
+
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
1126
+
dependencies = [
1127
+
"mac",
1128
+
"new_debug_unreachable",
1129
+
]
1130
+
1131
+
[[package]]
1132
+
name = "futures"
1133
+
version = "0.3.31"
1134
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1135
+
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
1136
+
dependencies = [
1137
+
"futures-channel",
1138
+
"futures-core",
1139
+
"futures-executor",
1140
+
"futures-io",
1141
+
"futures-sink",
1142
+
"futures-task",
1143
+
"futures-util",
1144
+
]
1145
+
1146
+
[[package]]
1147
+
name = "futures-buffered"
1148
+
version = "0.2.12"
1149
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1150
+
checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd"
1151
+
dependencies = [
1152
+
"cordyceps",
1153
+
"diatomic-waker",
1154
+
"futures-core",
1155
+
"pin-project-lite",
1156
+
"spin 0.10.0",
1157
+
]
1158
+
1159
+
[[package]]
1160
+
name = "futures-channel"
1161
+
version = "0.3.31"
1162
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1163
+
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
1164
+
dependencies = [
1165
+
"futures-core",
1166
+
"futures-sink",
1167
+
]
1168
+
1169
+
[[package]]
1170
+
name = "futures-core"
1171
+
version = "0.3.31"
1172
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1173
+
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
1174
+
1175
+
[[package]]
1176
+
name = "futures-executor"
1177
+
version = "0.3.31"
1178
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1179
+
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
1180
+
dependencies = [
1181
+
"futures-core",
1182
+
"futures-task",
1183
+
"futures-util",
1184
+
]
1185
+
1186
+
[[package]]
1187
+
name = "futures-io"
1188
+
version = "0.3.31"
1189
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1190
+
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
1191
+
1192
+
[[package]]
1193
+
name = "futures-lite"
1194
+
version = "2.6.1"
1195
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1196
+
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
1197
+
dependencies = [
1198
+
"fastrand",
1199
+
"futures-core",
1200
+
"futures-io",
1201
+
"parking",
1202
+
"pin-project-lite",
1203
+
]
1204
+
1205
+
[[package]]
1206
+
name = "futures-macro"
1207
+
version = "0.3.31"
1208
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1209
+
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
1210
+
dependencies = [
1211
+
"proc-macro2",
1212
+
"quote",
1213
+
"syn 2.0.108",
1214
+
]
1215
+
1216
+
[[package]]
1217
+
name = "futures-sink"
1218
+
version = "0.3.31"
1219
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1220
+
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
1221
+
1222
+
[[package]]
1223
+
name = "futures-task"
1224
+
version = "0.3.31"
1225
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1226
+
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
1227
+
1228
+
[[package]]
1229
+
name = "futures-timer"
1230
+
version = "3.0.3"
1231
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1232
+
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
1233
+
1234
+
[[package]]
1235
+
name = "futures-util"
1236
+
version = "0.3.31"
1237
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1238
+
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
1239
+
dependencies = [
1240
+
"futures-channel",
1241
+
"futures-core",
1242
+
"futures-io",
1243
+
"futures-macro",
1244
+
"futures-sink",
1245
+
"futures-task",
1246
+
"memchr",
1247
+
"pin-project-lite",
1248
+
"pin-utils",
1249
+
"slab",
1250
+
]
1251
+
1252
+
[[package]]
1253
+
name = "generator"
1254
+
version = "0.8.7"
1255
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1256
+
checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2"
1257
+
dependencies = [
1258
+
"cc",
1259
+
"cfg-if",
1260
+
"libc",
1261
+
"log",
1262
+
"rustversion",
1263
+
"windows",
1264
+
]
1265
+
1266
+
[[package]]
1267
+
name = "generic-array"
1268
+
version = "0.14.9"
1269
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1270
+
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
1271
+
dependencies = [
1272
+
"typenum",
1273
+
"version_check",
1274
+
"zeroize",
1275
+
]
1276
+
1277
+
[[package]]
1278
+
name = "getrandom"
1279
+
version = "0.2.16"
1280
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1281
+
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
1282
+
dependencies = [
1283
+
"cfg-if",
1284
+
"js-sys",
1285
+
"libc",
1286
+
"wasi",
1287
+
"wasm-bindgen",
1288
+
]
1289
+
1290
+
[[package]]
1291
+
name = "getrandom"
1292
+
version = "0.3.4"
1293
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1294
+
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
1295
+
dependencies = [
1296
+
"cfg-if",
1297
+
"js-sys",
1298
+
"libc",
1299
+
"r-efi",
1300
+
"wasip2",
1301
+
"wasm-bindgen",
1302
+
]
1303
+
1304
+
[[package]]
1305
+
name = "gif"
1306
+
version = "0.13.3"
1307
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1308
+
checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
1309
+
dependencies = [
1310
+
"color_quant",
1311
+
"weezl",
1312
+
]
1313
+
1314
+
[[package]]
1315
+
name = "gimli"
1316
+
version = "0.32.3"
1317
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1318
+
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
1319
+
1320
+
[[package]]
1321
+
name = "governor"
1322
+
version = "0.8.1"
1323
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1324
+
checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb"
1325
+
dependencies = [
1326
+
"cfg-if",
1327
+
"dashmap",
1328
+
"futures-sink",
1329
+
"futures-timer",
1330
+
"futures-util",
1331
+
"getrandom 0.3.4",
1332
+
"no-std-compat",
1333
+
"nonzero_ext",
1334
+
"parking_lot",
1335
+
"portable-atomic",
1336
+
"quanta",
1337
+
"rand 0.9.2",
1338
+
"smallvec",
1339
+
"spinning_top",
1340
+
"web-time",
1341
+
]
1342
+
1343
+
[[package]]
1344
+
name = "group"
1345
+
version = "0.13.0"
1346
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1347
+
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
1348
+
dependencies = [
1349
+
"ff",
1350
+
"rand_core 0.6.4",
1351
+
"subtle",
1352
+
]
1353
+
1354
+
[[package]]
1355
+
name = "gzip-header"
1356
+
version = "1.0.0"
1357
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1358
+
checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2"
1359
+
dependencies = [
1360
+
"crc32fast",
1361
+
]
1362
+
1363
+
[[package]]
1364
+
name = "h2"
1365
+
version = "0.4.12"
1366
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1367
+
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
1368
+
dependencies = [
1369
+
"atomic-waker",
1370
+
"bytes",
1371
+
"fnv",
1372
+
"futures-core",
1373
+
"futures-sink",
1374
+
"http",
1375
+
"indexmap",
1376
+
"slab",
1377
+
"tokio",
1378
+
"tokio-util",
1379
+
"tracing",
1380
+
]
1381
+
1382
+
[[package]]
1383
+
name = "half"
1384
+
version = "2.7.1"
1385
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1386
+
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
1387
+
dependencies = [
1388
+
"cfg-if",
1389
+
"crunchy",
1390
+
"zerocopy",
1391
+
]
1392
+
1393
+
[[package]]
1394
+
name = "hashbrown"
1395
+
version = "0.14.5"
1396
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1397
+
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
1398
+
1399
+
[[package]]
1400
+
name = "hashbrown"
1401
+
version = "0.16.0"
1402
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1403
+
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
1404
+
1405
+
[[package]]
1406
+
name = "heck"
1407
+
version = "0.4.1"
1408
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1409
+
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
1410
+
1411
+
[[package]]
1412
+
name = "heck"
1413
+
version = "0.5.0"
1414
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1415
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
1416
+
1417
+
[[package]]
1418
+
name = "hermit-abi"
1419
+
version = "0.5.2"
1420
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1421
+
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
1422
+
1423
+
[[package]]
1424
+
name = "hex_fmt"
1425
+
version = "0.3.0"
1426
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1427
+
checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f"
1428
+
1429
+
[[package]]
1430
+
name = "hickory-proto"
1431
+
version = "0.24.4"
1432
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1433
+
checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
1434
+
dependencies = [
1435
+
"async-trait",
1436
+
"cfg-if",
1437
+
"data-encoding",
1438
+
"enum-as-inner",
1439
+
"futures-channel",
1440
+
"futures-io",
1441
+
"futures-util",
1442
+
"idna",
1443
+
"ipnet",
1444
+
"once_cell",
1445
+
"rand 0.8.5",
1446
+
"thiserror 1.0.69",
1447
+
"tinyvec",
1448
+
"tokio",
1449
+
"tracing",
1450
+
"url",
1451
+
]
1452
+
1453
+
[[package]]
1454
+
name = "hickory-resolver"
1455
+
version = "0.24.4"
1456
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1457
+
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
1458
+
dependencies = [
1459
+
"cfg-if",
1460
+
"futures-util",
1461
+
"hickory-proto",
1462
+
"ipconfig",
1463
+
"lru-cache",
1464
+
"once_cell",
1465
+
"parking_lot",
1466
+
"rand 0.8.5",
1467
+
"resolv-conf",
1468
+
"smallvec",
1469
+
"thiserror 1.0.69",
1470
+
"tokio",
1471
+
"tracing",
1472
+
]
1473
+
1474
+
[[package]]
1475
+
name = "hmac"
1476
+
version = "0.12.1"
1477
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1478
+
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
1479
+
dependencies = [
1480
+
"digest",
1481
+
]
1482
+
1483
+
[[package]]
1484
+
name = "home"
1485
+
version = "0.5.12"
1486
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1487
+
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
1488
+
dependencies = [
1489
+
"windows-sys 0.61.2",
1490
+
]
1491
+
1492
+
[[package]]
1493
+
name = "html5ever"
1494
+
version = "0.27.0"
1495
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1496
+
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
1497
+
dependencies = [
1498
+
"log",
1499
+
"mac",
1500
+
"markup5ever",
1501
+
"proc-macro2",
1502
+
"quote",
1503
+
"syn 2.0.108",
1504
+
]
1505
+
1506
+
[[package]]
1507
+
name = "http"
1508
+
version = "1.3.1"
1509
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1510
+
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
1511
+
dependencies = [
1512
+
"bytes",
1513
+
"fnv",
1514
+
"itoa",
1515
+
]
1516
+
1517
+
[[package]]
1518
+
name = "http-body"
1519
+
version = "1.0.1"
1520
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1521
+
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
1522
+
dependencies = [
1523
+
"bytes",
1524
+
"http",
1525
+
]
1526
+
1527
+
[[package]]
1528
+
name = "http-body-util"
1529
+
version = "0.1.3"
1530
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1531
+
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
1532
+
dependencies = [
1533
+
"bytes",
1534
+
"futures-core",
1535
+
"http",
1536
+
"http-body",
1537
+
"pin-project-lite",
1538
+
]
1539
+
1540
+
[[package]]
1541
+
name = "httparse"
1542
+
version = "1.10.1"
1543
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1544
+
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
1545
+
1546
+
[[package]]
1547
+
name = "httpdate"
1548
+
version = "1.0.3"
1549
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1550
+
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
1551
+
1552
+
[[package]]
1553
+
name = "hyper"
1554
+
version = "1.7.0"
1555
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1556
+
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
1557
+
dependencies = [
1558
+
"atomic-waker",
1559
+
"bytes",
1560
+
"futures-channel",
1561
+
"futures-core",
1562
+
"h2",
1563
+
"http",
1564
+
"http-body",
1565
+
"httparse",
1566
+
"httpdate",
1567
+
"itoa",
1568
+
"pin-project-lite",
1569
+
"pin-utils",
1570
+
"smallvec",
1571
+
"tokio",
1572
+
"want",
1573
+
]
1574
+
1575
+
[[package]]
1576
+
name = "hyper-rustls"
1577
+
version = "0.27.7"
1578
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1579
+
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
1580
+
dependencies = [
1581
+
"http",
1582
+
"hyper",
1583
+
"hyper-util",
1584
+
"rustls",
1585
+
"rustls-pki-types",
1586
+
"tokio",
1587
+
"tokio-rustls",
1588
+
"tower-service",
1589
+
"webpki-roots",
1590
+
]
1591
+
1592
+
[[package]]
1593
+
name = "hyper-tls"
1594
+
version = "0.6.0"
1595
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1596
+
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
1597
+
dependencies = [
1598
+
"bytes",
1599
+
"http-body-util",
1600
+
"hyper",
1601
+
"hyper-util",
1602
+
"native-tls",
1603
+
"tokio",
1604
+
"tokio-native-tls",
1605
+
"tower-service",
1606
+
]
1607
+
1608
+
[[package]]
1609
+
name = "hyper-util"
1610
+
version = "0.1.17"
1611
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1612
+
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
1613
+
dependencies = [
1614
+
"base64 0.22.1",
1615
+
"bytes",
1616
+
"futures-channel",
1617
+
"futures-core",
1618
+
"futures-util",
1619
+
"http",
1620
+
"http-body",
1621
+
"hyper",
1622
+
"ipnet",
1623
+
"libc",
1624
+
"percent-encoding",
1625
+
"pin-project-lite",
1626
+
"socket2 0.6.1",
1627
+
"system-configuration",
1628
+
"tokio",
1629
+
"tower-service",
1630
+
"tracing",
1631
+
"windows-registry",
1632
+
]
1633
+
1634
+
[[package]]
1635
+
name = "iana-time-zone"
1636
+
version = "0.1.64"
1637
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1638
+
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
1639
+
dependencies = [
1640
+
"android_system_properties",
1641
+
"core-foundation-sys",
1642
+
"iana-time-zone-haiku",
1643
+
"js-sys",
1644
+
"log",
1645
+
"wasm-bindgen",
1646
+
"windows-core 0.62.2",
1647
+
]
1648
+
1649
+
[[package]]
1650
+
name = "iana-time-zone-haiku"
1651
+
version = "0.1.2"
1652
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1653
+
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
1654
+
dependencies = [
1655
+
"cc",
1656
+
]
1657
+
1658
+
[[package]]
1659
+
name = "icu_collections"
1660
+
version = "2.0.0"
1661
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1662
+
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
1663
+
dependencies = [
1664
+
"displaydoc",
1665
+
"potential_utf",
1666
+
"yoke",
1667
+
"zerofrom",
1668
+
"zerovec",
1669
+
]
1670
+
1671
+
[[package]]
1672
+
name = "icu_locale_core"
1673
+
version = "2.0.0"
1674
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1675
+
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
1676
+
dependencies = [
1677
+
"displaydoc",
1678
+
"litemap",
1679
+
"tinystr",
1680
+
"writeable",
1681
+
"zerovec",
1682
+
]
1683
+
1684
+
[[package]]
1685
+
name = "icu_normalizer"
1686
+
version = "2.0.0"
1687
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1688
+
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
1689
+
dependencies = [
1690
+
"displaydoc",
1691
+
"icu_collections",
1692
+
"icu_normalizer_data",
1693
+
"icu_properties",
1694
+
"icu_provider",
1695
+
"smallvec",
1696
+
"zerovec",
1697
+
]
1698
+
1699
+
[[package]]
1700
+
name = "icu_normalizer_data"
1701
+
version = "2.0.0"
1702
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1703
+
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
1704
+
1705
+
[[package]]
1706
+
name = "icu_properties"
1707
+
version = "2.0.1"
1708
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1709
+
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
1710
+
dependencies = [
1711
+
"displaydoc",
1712
+
"icu_collections",
1713
+
"icu_locale_core",
1714
+
"icu_properties_data",
1715
+
"icu_provider",
1716
+
"potential_utf",
1717
+
"zerotrie",
1718
+
"zerovec",
1719
+
]
1720
+
1721
+
[[package]]
1722
+
name = "icu_properties_data"
1723
+
version = "2.0.1"
1724
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1725
+
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
1726
+
1727
+
[[package]]
1728
+
name = "icu_provider"
1729
+
version = "2.0.0"
1730
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1731
+
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
1732
+
dependencies = [
1733
+
"displaydoc",
1734
+
"icu_locale_core",
1735
+
"stable_deref_trait",
1736
+
"tinystr",
1737
+
"writeable",
1738
+
"yoke",
1739
+
"zerofrom",
1740
+
"zerotrie",
1741
+
"zerovec",
1742
+
]
1743
+
1744
+
[[package]]
1745
+
name = "ident_case"
1746
+
version = "1.0.1"
1747
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1748
+
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
1749
+
1750
+
[[package]]
1751
+
name = "idna"
1752
+
version = "1.1.0"
1753
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1754
+
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
1755
+
dependencies = [
1756
+
"idna_adapter",
1757
+
"smallvec",
1758
+
"utf8_iter",
1759
+
]
1760
+
1761
+
[[package]]
1762
+
name = "idna_adapter"
1763
+
version = "1.2.1"
1764
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1765
+
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
1766
+
dependencies = [
1767
+
"icu_normalizer",
1768
+
"icu_properties",
1769
+
]
1770
+
1771
+
[[package]]
1772
+
name = "image"
1773
+
version = "0.25.8"
1774
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1775
+
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
1776
+
dependencies = [
1777
+
"bytemuck",
1778
+
"byteorder-lite",
1779
+
"color_quant",
1780
+
"exr",
1781
+
"gif",
1782
+
"image-webp",
1783
+
"moxcms",
1784
+
"num-traits",
1785
+
"png",
1786
+
"qoi",
1787
+
"ravif",
1788
+
"rayon",
1789
+
"rgb",
1790
+
"tiff",
1791
+
"zune-core",
1792
+
"zune-jpeg",
1793
+
]
1794
+
1795
+
[[package]]
1796
+
name = "image-webp"
1797
+
version = "0.2.4"
1798
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1799
+
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
1800
+
dependencies = [
1801
+
"byteorder-lite",
1802
+
"quick-error 2.0.1",
1803
+
]
1804
+
1805
+
[[package]]
1806
+
name = "image_hasher"
1807
+
version = "3.0.0"
1808
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1809
+
checksum = "7c191dc6138f559a0177b8413eaf2a37784d8e63c697e247aa3740930f1c9364"
1810
+
dependencies = [
1811
+
"base64 0.22.1",
1812
+
"image",
1813
+
"rustdct",
1814
+
"serde",
1815
+
"transpose",
1816
+
]
1817
+
1818
+
[[package]]
1819
+
name = "imgref"
1820
+
version = "1.12.0"
1821
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1822
+
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
1823
+
1824
+
[[package]]
1825
+
name = "indexmap"
1826
+
version = "2.12.0"
1827
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1828
+
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
1829
+
dependencies = [
1830
+
"equivalent",
1831
+
"hashbrown 0.16.0",
1832
+
]
1833
+
1834
+
[[package]]
1835
+
name = "indoc"
1836
+
version = "2.0.7"
1837
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1838
+
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
1839
+
dependencies = [
1840
+
"rustversion",
1841
+
]
1842
+
1843
+
[[package]]
1844
+
name = "interpolate_name"
1845
+
version = "0.2.4"
1846
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1847
+
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
1848
+
dependencies = [
1849
+
"proc-macro2",
1850
+
"quote",
1851
+
"syn 2.0.108",
1852
+
]
1853
+
1854
+
[[package]]
1855
+
name = "ipconfig"
1856
+
version = "0.3.2"
1857
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1858
+
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
1859
+
dependencies = [
1860
+
"socket2 0.5.10",
1861
+
"widestring",
1862
+
"windows-sys 0.48.0",
1863
+
"winreg",
1864
+
]
1865
+
1866
+
[[package]]
1867
+
name = "ipld-core"
1868
+
version = "0.4.2"
1869
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1870
+
checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db"
1871
+
dependencies = [
1872
+
"cid",
1873
+
"serde",
1874
+
"serde_bytes",
1875
+
]
1876
+
1877
+
[[package]]
1878
+
name = "ipnet"
1879
+
version = "2.11.0"
1880
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1881
+
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
1882
+
1883
+
[[package]]
1884
+
name = "iri-string"
1885
+
version = "0.7.8"
1886
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1887
+
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
1888
+
dependencies = [
1889
+
"memchr",
1890
+
"serde",
1891
+
]
1892
+
1893
+
[[package]]
1894
+
name = "is_ci"
1895
+
version = "1.2.0"
1896
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1897
+
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
1898
+
1899
+
[[package]]
1900
+
name = "itertools"
1901
+
version = "0.12.1"
1902
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1903
+
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
1904
+
dependencies = [
1905
+
"either",
1906
+
]
1907
+
1908
+
[[package]]
1909
+
name = "itertools"
1910
+
version = "0.13.0"
1911
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1912
+
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
1913
+
dependencies = [
1914
+
"either",
1915
+
]
1916
+
1917
+
[[package]]
1918
+
name = "itoa"
1919
+
version = "1.0.15"
1920
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1921
+
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1922
+
1923
+
[[package]]
1924
+
name = "jacquard"
1925
+
version = "0.7.0"
1926
+
dependencies = [
1927
+
"bon",
1928
+
"bytes",
1929
+
"getrandom 0.2.16",
1930
+
"http",
1931
+
"jacquard-api",
1932
+
"jacquard-common",
1933
+
"jacquard-derive",
1934
+
"jacquard-identity",
1935
+
"jacquard-oauth",
1936
+
"jose-jwk",
1937
+
"miette",
1938
+
"p256",
1939
+
"percent-encoding",
1940
+
"rand_core 0.6.4",
1941
+
"regex",
1942
+
"reqwest",
1943
+
"serde",
1944
+
"serde_html_form",
1945
+
"serde_ipld_dagcbor",
1946
+
"serde_json",
1947
+
"smol_str",
1948
+
"thiserror 2.0.17",
1949
+
"tokio",
1950
+
"trait-variant",
1951
+
"url",
1952
+
"webpage",
1953
+
]
1954
+
1955
+
[[package]]
1956
+
name = "jacquard-api"
1957
+
version = "0.7.1"
1958
+
dependencies = [
1959
+
"bon",
1960
+
"bytes",
1961
+
"jacquard-common",
1962
+
"jacquard-derive",
1963
+
"miette",
1964
+
"serde",
1965
+
"serde_ipld_dagcbor",
1966
+
"thiserror 2.0.17",
1967
+
]
1968
+
1969
+
[[package]]
1970
+
name = "jacquard-common"
1971
+
version = "0.7.0"
1972
+
dependencies = [
1973
+
"base64 0.22.1",
1974
+
"bon",
1975
+
"bytes",
1976
+
"chrono",
1977
+
"ciborium",
1978
+
"cid",
1979
+
"futures",
1980
+
"getrandom 0.2.16",
1981
+
"getrandom 0.3.4",
1982
+
"http",
1983
+
"ipld-core",
1984
+
"k256",
1985
+
"langtag",
1986
+
"miette",
1987
+
"multibase",
1988
+
"multihash",
1989
+
"n0-future",
1990
+
"ouroboros",
1991
+
"p256",
1992
+
"rand 0.9.2",
1993
+
"regex",
1994
+
"reqwest",
1995
+
"serde",
1996
+
"serde_html_form",
1997
+
"serde_ipld_dagcbor",
1998
+
"serde_json",
1999
+
"signature",
2000
+
"smol_str",
2001
+
"thiserror 2.0.17",
2002
+
"tokio",
2003
+
"tokio-tungstenite-wasm",
2004
+
"tokio-util",
2005
+
"trait-variant",
2006
+
"url",
2007
+
]
2008
+
2009
+
[[package]]
2010
+
name = "jacquard-derive"
2011
+
version = "0.7.0"
2012
+
dependencies = [
2013
+
"proc-macro2",
2014
+
"quote",
2015
+
"syn 2.0.108",
2016
+
]
2017
+
2018
+
[[package]]
2019
+
name = "jacquard-identity"
2020
+
version = "0.7.0"
2021
+
dependencies = [
2022
+
"bon",
2023
+
"bytes",
2024
+
"hickory-resolver",
2025
+
"http",
2026
+
"jacquard-api",
2027
+
"jacquard-common",
2028
+
"miette",
2029
+
"percent-encoding",
2030
+
"reqwest",
2031
+
"serde",
2032
+
"serde_html_form",
2033
+
"serde_json",
2034
+
"thiserror 2.0.17",
2035
+
"tokio",
2036
+
"trait-variant",
2037
+
"url",
2038
+
"urlencoding",
2039
+
]
2040
+
2041
+
[[package]]
2042
+
name = "jacquard-oauth"
2043
+
version = "0.7.0"
2044
+
dependencies = [
2045
+
"base64 0.22.1",
2046
+
"bytes",
2047
+
"chrono",
2048
+
"dashmap",
2049
+
"elliptic-curve",
2050
+
"http",
2051
+
"jacquard-common",
2052
+
"jacquard-identity",
2053
+
"jose-jwa",
2054
+
"jose-jwk",
2055
+
"miette",
2056
+
"p256",
2057
+
"rand 0.8.5",
2058
+
"rand_core 0.6.4",
2059
+
"reqwest",
2060
+
"rouille",
2061
+
"serde",
2062
+
"serde_html_form",
2063
+
"serde_json",
2064
+
"sha2",
2065
+
"signature",
2066
+
"smol_str",
2067
+
"thiserror 2.0.17",
2068
+
"tokio",
2069
+
"trait-variant",
2070
+
"url",
2071
+
"webbrowser",
2072
+
]
2073
+
2074
+
[[package]]
2075
+
name = "jni"
2076
+
version = "0.21.1"
2077
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2078
+
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
2079
+
dependencies = [
2080
+
"cesu8",
2081
+
"cfg-if",
2082
+
"combine",
2083
+
"jni-sys",
2084
+
"log",
2085
+
"thiserror 1.0.69",
2086
+
"walkdir",
2087
+
"windows-sys 0.45.0",
2088
+
]
2089
+
2090
+
[[package]]
2091
+
name = "jni-sys"
2092
+
version = "0.3.0"
2093
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2094
+
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
2095
+
2096
+
[[package]]
2097
+
name = "jobserver"
2098
+
version = "0.1.34"
2099
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2100
+
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
2101
+
dependencies = [
2102
+
"getrandom 0.3.4",
2103
+
"libc",
2104
+
]
2105
+
2106
+
[[package]]
2107
+
name = "jose-b64"
2108
+
version = "0.1.2"
2109
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2110
+
checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56"
2111
+
dependencies = [
2112
+
"base64ct",
2113
+
"serde",
2114
+
"subtle",
2115
+
"zeroize",
2116
+
]
2117
+
2118
+
[[package]]
2119
+
name = "jose-jwa"
2120
+
version = "0.1.2"
2121
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2122
+
checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7"
2123
+
dependencies = [
2124
+
"serde",
2125
+
]
2126
+
2127
+
[[package]]
2128
+
name = "jose-jwk"
2129
+
version = "0.1.2"
2130
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2131
+
checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7"
2132
+
dependencies = [
2133
+
"jose-b64",
2134
+
"jose-jwa",
2135
+
"p256",
2136
+
"p384",
2137
+
"rsa",
2138
+
"serde",
2139
+
"zeroize",
2140
+
]
2141
+
2142
+
[[package]]
2143
+
name = "js-sys"
2144
+
version = "0.3.81"
2145
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2146
+
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
2147
+
dependencies = [
2148
+
"once_cell",
2149
+
"wasm-bindgen",
2150
+
]
2151
+
2152
+
[[package]]
2153
+
name = "k256"
2154
+
version = "0.13.4"
2155
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2156
+
checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
2157
+
dependencies = [
2158
+
"cfg-if",
2159
+
"ecdsa",
2160
+
"elliptic-curve",
2161
+
"sha2",
2162
+
]
2163
+
2164
+
[[package]]
2165
+
name = "langtag"
2166
+
version = "0.4.0"
2167
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2168
+
checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600"
2169
+
dependencies = [
2170
+
"serde",
2171
+
"static-regular-grammar",
2172
+
"thiserror 1.0.69",
2173
+
]
2174
+
2175
+
[[package]]
2176
+
name = "lazy_static"
2177
+
version = "1.5.0"
2178
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2179
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
2180
+
dependencies = [
2181
+
"spin 0.9.8",
2182
+
]
2183
+
2184
+
[[package]]
2185
+
name = "lebe"
2186
+
version = "0.5.3"
2187
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2188
+
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
2189
+
2190
+
[[package]]
2191
+
name = "libc"
2192
+
version = "0.2.177"
2193
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2194
+
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
2195
+
2196
+
[[package]]
2197
+
name = "libfuzzer-sys"
2198
+
version = "0.4.10"
2199
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2200
+
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
2201
+
dependencies = [
2202
+
"arbitrary",
2203
+
"cc",
2204
+
]
2205
+
2206
+
[[package]]
2207
+
name = "libm"
2208
+
version = "0.2.15"
2209
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2210
+
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
2211
+
2212
+
[[package]]
2213
+
name = "libredox"
2214
+
version = "0.1.10"
2215
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2216
+
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
2217
+
dependencies = [
2218
+
"bitflags",
2219
+
"libc",
2220
+
"redox_syscall",
2221
+
]
2222
+
2223
+
[[package]]
2224
+
name = "linked-hash-map"
2225
+
version = "0.5.6"
2226
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2227
+
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
2228
+
2229
+
[[package]]
2230
+
name = "linux-raw-sys"
2231
+
version = "0.11.0"
2232
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2233
+
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
2234
+
2235
+
[[package]]
2236
+
name = "litemap"
2237
+
version = "0.8.0"
2238
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2239
+
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
2240
+
2241
+
[[package]]
2242
+
name = "lock_api"
2243
+
version = "0.4.14"
2244
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2245
+
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
2246
+
dependencies = [
2247
+
"scopeguard",
2248
+
]
2249
+
2250
+
[[package]]
2251
+
name = "log"
2252
+
version = "0.4.28"
2253
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2254
+
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
2255
+
2256
+
[[package]]
2257
+
name = "loom"
2258
+
version = "0.7.2"
2259
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2260
+
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
2261
+
dependencies = [
2262
+
"cfg-if",
2263
+
"generator",
2264
+
"scoped-tls",
2265
+
"tracing",
2266
+
"tracing-subscriber",
2267
+
]
2268
+
2269
+
[[package]]
2270
+
name = "loop9"
2271
+
version = "0.1.5"
2272
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2273
+
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
2274
+
dependencies = [
2275
+
"imgref",
2276
+
]
2277
+
2278
+
[[package]]
2279
+
name = "lru-cache"
2280
+
version = "0.1.2"
2281
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2282
+
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
2283
+
dependencies = [
2284
+
"linked-hash-map",
2285
+
]
2286
+
2287
+
[[package]]
2288
+
name = "lru-slab"
2289
+
version = "0.1.2"
2290
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2291
+
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
2292
+
2293
+
[[package]]
2294
+
name = "mac"
2295
+
version = "0.1.1"
2296
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2297
+
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
2298
+
2299
+
[[package]]
2300
+
name = "malloc_buf"
2301
+
version = "0.0.6"
2302
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2303
+
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
2304
+
dependencies = [
2305
+
"libc",
2306
+
]
2307
+
2308
+
[[package]]
2309
+
name = "markup5ever"
2310
+
version = "0.12.1"
2311
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2312
+
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
2313
+
dependencies = [
2314
+
"log",
2315
+
"phf",
2316
+
"phf_codegen",
2317
+
"string_cache",
2318
+
"string_cache_codegen",
2319
+
"tendril",
2320
+
]
2321
+
2322
+
[[package]]
2323
+
name = "markup5ever_rcdom"
2324
+
version = "0.3.0"
2325
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2326
+
checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
2327
+
dependencies = [
2328
+
"html5ever",
2329
+
"markup5ever",
2330
+
"tendril",
2331
+
"xml5ever",
2332
+
]
2333
+
2334
+
[[package]]
2335
+
name = "match-lookup"
2336
+
version = "0.1.1"
2337
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2338
+
checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e"
2339
+
dependencies = [
2340
+
"proc-macro2",
2341
+
"quote",
2342
+
"syn 1.0.109",
2343
+
]
2344
+
2345
+
[[package]]
2346
+
name = "matchers"
2347
+
version = "0.2.0"
2348
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2349
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
2350
+
dependencies = [
2351
+
"regex-automata",
2352
+
]
2353
+
2354
+
[[package]]
2355
+
name = "maybe-rayon"
2356
+
version = "0.1.1"
2357
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2358
+
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
2359
+
dependencies = [
2360
+
"cfg-if",
2361
+
"rayon",
2362
+
]
2363
+
2364
+
[[package]]
2365
+
name = "memchr"
2366
+
version = "2.7.6"
2367
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2368
+
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
2369
+
2370
+
[[package]]
2371
+
name = "miette"
2372
+
version = "7.6.0"
2373
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2374
+
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
2375
+
dependencies = [
2376
+
"backtrace",
2377
+
"backtrace-ext",
2378
+
"cfg-if",
2379
+
"miette-derive",
2380
+
"owo-colors",
2381
+
"supports-color",
2382
+
"supports-hyperlinks",
2383
+
"supports-unicode",
2384
+
"terminal_size",
2385
+
"textwrap",
2386
+
"unicode-width 0.1.14",
2387
+
]
2388
+
2389
+
[[package]]
2390
+
name = "miette-derive"
2391
+
version = "7.6.0"
2392
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2393
+
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
2394
+
dependencies = [
2395
+
"proc-macro2",
2396
+
"quote",
2397
+
"syn 2.0.108",
2398
+
]
2399
+
2400
+
[[package]]
2401
+
name = "mime"
2402
+
version = "0.3.17"
2403
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2404
+
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
2405
+
2406
+
[[package]]
2407
+
name = "mime_guess"
2408
+
version = "2.0.5"
2409
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2410
+
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
2411
+
dependencies = [
2412
+
"mime",
2413
+
"unicase",
2414
+
]
2415
+
2416
+
[[package]]
2417
+
name = "minimal-lexical"
2418
+
version = "0.2.1"
2419
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2420
+
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
2421
+
2422
+
[[package]]
2423
+
name = "miniz_oxide"
2424
+
version = "0.8.9"
2425
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2426
+
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
2427
+
dependencies = [
2428
+
"adler2",
2429
+
"simd-adler32",
2430
+
]
2431
+
2432
+
[[package]]
2433
+
name = "mio"
2434
+
version = "1.1.0"
2435
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2436
+
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
2437
+
dependencies = [
2438
+
"libc",
2439
+
"wasi",
2440
+
"windows-sys 0.61.2",
2441
+
]
2442
+
2443
+
[[package]]
2444
+
name = "mockito"
2445
+
version = "1.7.0"
2446
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2447
+
checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48"
2448
+
dependencies = [
2449
+
"assert-json-diff",
2450
+
"bytes",
2451
+
"colored",
2452
+
"futures-util",
2453
+
"http",
2454
+
"http-body",
2455
+
"http-body-util",
2456
+
"hyper",
2457
+
"hyper-util",
2458
+
"log",
2459
+
"rand 0.9.2",
2460
+
"regex",
2461
+
"serde_json",
2462
+
"serde_urlencoded",
2463
+
"similar",
2464
+
"tokio",
2465
+
]
2466
+
2467
+
[[package]]
2468
+
name = "moxcms"
2469
+
version = "0.7.7"
2470
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2471
+
checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40"
2472
+
dependencies = [
2473
+
"num-traits",
2474
+
"pxfm",
2475
+
]
2476
+
2477
+
[[package]]
2478
+
name = "multibase"
2479
+
version = "0.9.2"
2480
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2481
+
checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77"
2482
+
dependencies = [
2483
+
"base-x",
2484
+
"base256emoji",
2485
+
"data-encoding",
2486
+
"data-encoding-macro",
2487
+
]
2488
+
2489
+
[[package]]
2490
+
name = "multihash"
2491
+
version = "0.19.3"
2492
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2493
+
checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d"
2494
+
dependencies = [
2495
+
"core2",
2496
+
"serde",
2497
+
"unsigned-varint",
2498
+
]
2499
+
2500
+
[[package]]
2501
+
name = "multipart"
2502
+
version = "0.18.0"
2503
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2504
+
checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
2505
+
dependencies = [
2506
+
"buf_redux",
2507
+
"httparse",
2508
+
"log",
2509
+
"mime",
2510
+
"mime_guess",
2511
+
"quick-error 1.2.3",
2512
+
"rand 0.8.5",
2513
+
"safemem",
2514
+
"tempfile",
2515
+
"twoway",
2516
+
]
2517
+
2518
+
[[package]]
2519
+
name = "n0-future"
2520
+
version = "0.1.3"
2521
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2522
+
checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794"
2523
+
dependencies = [
2524
+
"cfg_aliases",
2525
+
"derive_more",
2526
+
"futures-buffered",
2527
+
"futures-lite",
2528
+
"futures-util",
2529
+
"js-sys",
2530
+
"pin-project",
2531
+
"send_wrapper",
2532
+
"tokio",
2533
+
"tokio-util",
2534
+
"wasm-bindgen",
2535
+
"wasm-bindgen-futures",
2536
+
"web-time",
2537
+
]
2538
+
2539
+
[[package]]
2540
+
name = "native-tls"
2541
+
version = "0.2.14"
2542
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2543
+
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
2544
+
dependencies = [
2545
+
"libc",
2546
+
"log",
2547
+
"openssl",
2548
+
"openssl-probe",
2549
+
"openssl-sys",
2550
+
"schannel",
2551
+
"security-framework 2.11.1",
2552
+
"security-framework-sys",
2553
+
"tempfile",
2554
+
]
2555
+
2556
+
[[package]]
2557
+
name = "ndk-context"
2558
+
version = "0.1.1"
2559
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2560
+
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
2561
+
2562
+
[[package]]
2563
+
name = "new_debug_unreachable"
2564
+
version = "1.0.6"
2565
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2566
+
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
2567
+
2568
+
[[package]]
2569
+
name = "no-std-compat"
2570
+
version = "0.4.1"
2571
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2572
+
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
2573
+
2574
+
[[package]]
2575
+
name = "nom"
2576
+
version = "7.1.3"
2577
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2578
+
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
2579
+
dependencies = [
2580
+
"memchr",
2581
+
"minimal-lexical",
2582
+
]
2583
+
2584
+
[[package]]
2585
+
name = "nom"
2586
+
version = "8.0.0"
2587
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2588
+
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
2589
+
dependencies = [
2590
+
"memchr",
2591
+
]
2592
+
2593
+
[[package]]
2594
+
name = "nonzero_ext"
2595
+
version = "0.3.0"
2596
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2597
+
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
2598
+
2599
+
[[package]]
2600
+
name = "noop_proc_macro"
2601
+
version = "0.3.0"
2602
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2603
+
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
2604
+
2605
+
[[package]]
2606
+
name = "nu-ansi-term"
2607
+
version = "0.50.3"
2608
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2609
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
2610
+
dependencies = [
2611
+
"windows-sys 0.61.2",
2612
+
]
2613
+
2614
+
[[package]]
2615
+
name = "num-bigint"
2616
+
version = "0.4.6"
2617
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2618
+
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
2619
+
dependencies = [
2620
+
"num-integer",
2621
+
"num-traits",
2622
+
]
2623
+
2624
+
[[package]]
2625
+
name = "num-bigint-dig"
2626
+
version = "0.8.4"
2627
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2628
+
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
2629
+
dependencies = [
2630
+
"byteorder",
2631
+
"lazy_static",
2632
+
"libm",
2633
+
"num-integer",
2634
+
"num-iter",
2635
+
"num-traits",
2636
+
"rand 0.8.5",
2637
+
"smallvec",
2638
+
"zeroize",
2639
+
]
2640
+
2641
+
[[package]]
2642
+
name = "num-complex"
2643
+
version = "0.4.6"
2644
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2645
+
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
2646
+
dependencies = [
2647
+
"num-traits",
2648
+
]
2649
+
2650
+
[[package]]
2651
+
name = "num-conv"
2652
+
version = "0.1.0"
2653
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2654
+
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
2655
+
2656
+
[[package]]
2657
+
name = "num-derive"
2658
+
version = "0.4.2"
2659
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2660
+
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
2661
+
dependencies = [
2662
+
"proc-macro2",
2663
+
"quote",
2664
+
"syn 2.0.108",
2665
+
]
2666
+
2667
+
[[package]]
2668
+
name = "num-integer"
2669
+
version = "0.1.46"
2670
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2671
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
2672
+
dependencies = [
2673
+
"num-traits",
2674
+
]
2675
+
2676
+
[[package]]
2677
+
name = "num-iter"
2678
+
version = "0.1.45"
2679
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2680
+
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
2681
+
dependencies = [
2682
+
"autocfg",
2683
+
"num-integer",
2684
+
"num-traits",
2685
+
]
2686
+
2687
+
[[package]]
2688
+
name = "num-rational"
2689
+
version = "0.4.2"
2690
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2691
+
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
2692
+
dependencies = [
2693
+
"num-bigint",
2694
+
"num-integer",
2695
+
"num-traits",
2696
+
]
2697
+
2698
+
[[package]]
2699
+
name = "num-traits"
2700
+
version = "0.2.19"
2701
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2702
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
2703
+
dependencies = [
2704
+
"autocfg",
2705
+
"libm",
2706
+
]
2707
+
2708
+
[[package]]
2709
+
name = "num_cpus"
2710
+
version = "1.17.0"
2711
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2712
+
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
2713
+
dependencies = [
2714
+
"hermit-abi",
2715
+
"libc",
2716
+
]
2717
+
2718
+
[[package]]
2719
+
name = "num_threads"
2720
+
version = "0.1.7"
2721
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2722
+
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
2723
+
dependencies = [
2724
+
"libc",
2725
+
]
2726
+
2727
+
[[package]]
2728
+
name = "objc"
2729
+
version = "0.2.7"
2730
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2731
+
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
2732
+
dependencies = [
2733
+
"malloc_buf",
2734
+
]
2735
+
2736
+
[[package]]
2737
+
name = "object"
2738
+
version = "0.37.3"
2739
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2740
+
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
2741
+
dependencies = [
2742
+
"memchr",
2743
+
]
2744
+
2745
+
[[package]]
2746
+
name = "once_cell"
2747
+
version = "1.21.3"
2748
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2749
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
2750
+
2751
+
[[package]]
2752
+
name = "openssl"
2753
+
version = "0.10.74"
2754
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2755
+
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
2756
+
dependencies = [
2757
+
"bitflags",
2758
+
"cfg-if",
2759
+
"foreign-types",
2760
+
"libc",
2761
+
"once_cell",
2762
+
"openssl-macros",
2763
+
"openssl-sys",
2764
+
]
2765
+
2766
+
[[package]]
2767
+
name = "openssl-macros"
2768
+
version = "0.1.1"
2769
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2770
+
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
2771
+
dependencies = [
2772
+
"proc-macro2",
2773
+
"quote",
2774
+
"syn 2.0.108",
2775
+
]
2776
+
2777
+
[[package]]
2778
+
name = "openssl-probe"
2779
+
version = "0.1.6"
2780
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2781
+
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
2782
+
2783
+
[[package]]
2784
+
name = "openssl-sys"
2785
+
version = "0.9.110"
2786
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2787
+
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
2788
+
dependencies = [
2789
+
"cc",
2790
+
"libc",
2791
+
"pkg-config",
2792
+
"vcpkg",
2793
+
]
2794
+
2795
+
[[package]]
2796
+
name = "ouroboros"
2797
+
version = "0.18.5"
2798
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2799
+
checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
2800
+
dependencies = [
2801
+
"aliasable",
2802
+
"ouroboros_macro",
2803
+
"static_assertions",
2804
+
]
2805
+
2806
+
[[package]]
2807
+
name = "ouroboros_macro"
2808
+
version = "0.18.5"
2809
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2810
+
checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
2811
+
dependencies = [
2812
+
"heck 0.4.1",
2813
+
"proc-macro2",
2814
+
"proc-macro2-diagnostics",
2815
+
"quote",
2816
+
"syn 2.0.108",
2817
+
]
2818
+
2819
+
[[package]]
2820
+
name = "owo-colors"
2821
+
version = "4.2.3"
2822
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2823
+
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
2824
+
2825
+
[[package]]
2826
+
name = "p256"
2827
+
version = "0.13.2"
2828
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2829
+
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
2830
+
dependencies = [
2831
+
"ecdsa",
2832
+
"elliptic-curve",
2833
+
"primeorder",
2834
+
"sha2",
2835
+
]
2836
+
2837
+
[[package]]
2838
+
name = "p384"
2839
+
version = "0.13.1"
2840
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2841
+
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
2842
+
dependencies = [
2843
+
"elliptic-curve",
2844
+
"primeorder",
2845
+
]
2846
+
2847
+
[[package]]
2848
+
name = "parking"
2849
+
version = "2.2.1"
2850
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2851
+
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
2852
+
2853
+
[[package]]
2854
+
name = "parking_lot"
2855
+
version = "0.12.5"
2856
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2857
+
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
2858
+
dependencies = [
2859
+
"lock_api",
2860
+
"parking_lot_core",
2861
+
]
2862
+
2863
+
[[package]]
2864
+
name = "parking_lot_core"
2865
+
version = "0.9.12"
2866
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2867
+
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
2868
+
dependencies = [
2869
+
"cfg-if",
2870
+
"libc",
2871
+
"redox_syscall",
2872
+
"smallvec",
2873
+
"windows-link 0.2.1",
2874
+
]
2875
+
2876
+
[[package]]
2877
+
name = "paste"
2878
+
version = "1.0.15"
2879
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2880
+
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
2881
+
2882
+
[[package]]
2883
+
name = "pem-rfc7468"
2884
+
version = "0.7.0"
2885
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2886
+
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
2887
+
dependencies = [
2888
+
"base64ct",
2889
+
]
2890
+
2891
+
[[package]]
2892
+
name = "percent-encoding"
2893
+
version = "2.3.2"
2894
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2895
+
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
2896
+
2897
+
[[package]]
2898
+
name = "phf"
2899
+
version = "0.11.3"
2900
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2901
+
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
2902
+
dependencies = [
2903
+
"phf_shared",
2904
+
]
2905
+
2906
+
[[package]]
2907
+
name = "phf_codegen"
2908
+
version = "0.11.3"
2909
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2910
+
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
2911
+
dependencies = [
2912
+
"phf_generator",
2913
+
"phf_shared",
2914
+
]
2915
+
2916
+
[[package]]
2917
+
name = "phf_generator"
2918
+
version = "0.11.3"
2919
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2920
+
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
2921
+
dependencies = [
2922
+
"phf_shared",
2923
+
"rand 0.8.5",
2924
+
]
2925
+
2926
+
[[package]]
2927
+
name = "phf_shared"
2928
+
version = "0.11.3"
2929
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2930
+
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
2931
+
dependencies = [
2932
+
"siphasher",
2933
+
]
2934
+
2935
+
[[package]]
2936
+
name = "pin-project"
2937
+
version = "1.1.10"
2938
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2939
+
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
2940
+
dependencies = [
2941
+
"pin-project-internal",
2942
+
]
2943
+
2944
+
[[package]]
2945
+
name = "pin-project-internal"
2946
+
version = "1.1.10"
2947
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2948
+
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
2949
+
dependencies = [
2950
+
"proc-macro2",
2951
+
"quote",
2952
+
"syn 2.0.108",
2953
+
]
2954
+
2955
+
[[package]]
2956
+
name = "pin-project-lite"
2957
+
version = "0.2.16"
2958
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2959
+
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
2960
+
2961
+
[[package]]
2962
+
name = "pin-utils"
2963
+
version = "0.1.0"
2964
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2965
+
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
2966
+
2967
+
[[package]]
2968
+
name = "pkcs1"
2969
+
version = "0.7.5"
2970
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2971
+
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
2972
+
dependencies = [
2973
+
"der",
2974
+
"pkcs8",
2975
+
"spki",
2976
+
]
2977
+
2978
+
[[package]]
2979
+
name = "pkcs8"
2980
+
version = "0.10.2"
2981
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2982
+
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
2983
+
dependencies = [
2984
+
"der",
2985
+
"spki",
2986
+
]
2987
+
2988
+
[[package]]
2989
+
name = "pkg-config"
2990
+
version = "0.3.32"
2991
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2992
+
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
2993
+
2994
+
[[package]]
2995
+
name = "png"
2996
+
version = "0.18.0"
2997
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2998
+
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
2999
+
dependencies = [
3000
+
"bitflags",
3001
+
"crc32fast",
3002
+
"fdeflate",
3003
+
"flate2",
3004
+
"miniz_oxide",
3005
+
]
3006
+
3007
+
[[package]]
3008
+
name = "portable-atomic"
3009
+
version = "1.11.1"
3010
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3011
+
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
3012
+
3013
+
[[package]]
3014
+
name = "potential_utf"
3015
+
version = "0.1.3"
3016
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3017
+
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
3018
+
dependencies = [
3019
+
"zerovec",
3020
+
]
3021
+
3022
+
[[package]]
3023
+
name = "powerfmt"
3024
+
version = "0.2.0"
3025
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3026
+
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
3027
+
3028
+
[[package]]
3029
+
name = "ppv-lite86"
3030
+
version = "0.2.21"
3031
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3032
+
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
3033
+
dependencies = [
3034
+
"zerocopy",
3035
+
]
3036
+
3037
+
[[package]]
3038
+
name = "precomputed-hash"
3039
+
version = "0.1.1"
3040
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3041
+
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
3042
+
3043
+
[[package]]
3044
+
name = "prettyplease"
3045
+
version = "0.2.37"
3046
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3047
+
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
3048
+
dependencies = [
3049
+
"proc-macro2",
3050
+
"syn 2.0.108",
3051
+
]
3052
+
3053
+
[[package]]
3054
+
name = "primal-check"
3055
+
version = "0.3.4"
3056
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3057
+
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
3058
+
dependencies = [
3059
+
"num-integer",
3060
+
]
3061
+
3062
+
[[package]]
3063
+
name = "primeorder"
3064
+
version = "0.13.6"
3065
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3066
+
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
3067
+
dependencies = [
3068
+
"elliptic-curve",
3069
+
]
3070
+
3071
+
[[package]]
3072
+
name = "proc-macro-error"
3073
+
version = "1.0.4"
3074
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3075
+
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
3076
+
dependencies = [
3077
+
"proc-macro-error-attr",
3078
+
"proc-macro2",
3079
+
"quote",
3080
+
"syn 1.0.109",
3081
+
"version_check",
3082
+
]
3083
+
3084
+
[[package]]
3085
+
name = "proc-macro-error-attr"
3086
+
version = "1.0.4"
3087
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3088
+
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
3089
+
dependencies = [
3090
+
"proc-macro2",
3091
+
"quote",
3092
+
"version_check",
3093
+
]
3094
+
3095
+
[[package]]
3096
+
name = "proc-macro2"
3097
+
version = "1.0.103"
3098
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3099
+
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
3100
+
dependencies = [
3101
+
"unicode-ident",
3102
+
]
3103
+
3104
+
[[package]]
3105
+
name = "proc-macro2-diagnostics"
3106
+
version = "0.10.1"
3107
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3108
+
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
3109
+
dependencies = [
3110
+
"proc-macro2",
3111
+
"quote",
3112
+
"syn 2.0.108",
3113
+
"version_check",
3114
+
"yansi",
3115
+
]
3116
+
3117
+
[[package]]
3118
+
name = "profiling"
3119
+
version = "1.0.17"
3120
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3121
+
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
3122
+
dependencies = [
3123
+
"profiling-procmacros",
3124
+
]
3125
+
3126
+
[[package]]
3127
+
name = "profiling-procmacros"
3128
+
version = "1.0.17"
3129
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3130
+
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
3131
+
dependencies = [
3132
+
"quote",
3133
+
"syn 2.0.108",
3134
+
]
3135
+
3136
+
[[package]]
3137
+
name = "pxfm"
3138
+
version = "0.1.25"
3139
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3140
+
checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
3141
+
dependencies = [
3142
+
"num-traits",
3143
+
]
3144
+
3145
+
[[package]]
3146
+
name = "qoi"
3147
+
version = "0.4.1"
3148
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3149
+
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
3150
+
dependencies = [
3151
+
"bytemuck",
3152
+
]
3153
+
3154
+
[[package]]
3155
+
name = "quanta"
3156
+
version = "0.12.6"
3157
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3158
+
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
3159
+
dependencies = [
3160
+
"crossbeam-utils",
3161
+
"libc",
3162
+
"once_cell",
3163
+
"raw-cpuid",
3164
+
"wasi",
3165
+
"web-sys",
3166
+
"winapi",
3167
+
]
3168
+
3169
+
[[package]]
3170
+
name = "quick-error"
3171
+
version = "1.2.3"
3172
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3173
+
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
3174
+
3175
+
[[package]]
3176
+
name = "quick-error"
3177
+
version = "2.0.1"
3178
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3179
+
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
3180
+
3181
+
[[package]]
3182
+
name = "quinn"
3183
+
version = "0.11.9"
3184
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3185
+
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
3186
+
dependencies = [
3187
+
"bytes",
3188
+
"cfg_aliases",
3189
+
"pin-project-lite",
3190
+
"quinn-proto",
3191
+
"quinn-udp",
3192
+
"rustc-hash",
3193
+
"rustls",
3194
+
"socket2 0.6.1",
3195
+
"thiserror 2.0.17",
3196
+
"tokio",
3197
+
"tracing",
3198
+
"web-time",
3199
+
]
3200
+
3201
+
[[package]]
3202
+
name = "quinn-proto"
3203
+
version = "0.11.13"
3204
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3205
+
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
3206
+
dependencies = [
3207
+
"bytes",
3208
+
"getrandom 0.3.4",
3209
+
"lru-slab",
3210
+
"rand 0.9.2",
3211
+
"ring",
3212
+
"rustc-hash",
3213
+
"rustls",
3214
+
"rustls-pki-types",
3215
+
"slab",
3216
+
"thiserror 2.0.17",
3217
+
"tinyvec",
3218
+
"tracing",
3219
+
"web-time",
3220
+
]
3221
+
3222
+
[[package]]
3223
+
name = "quinn-udp"
3224
+
version = "0.5.14"
3225
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3226
+
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
3227
+
dependencies = [
3228
+
"cfg_aliases",
3229
+
"libc",
3230
+
"once_cell",
3231
+
"socket2 0.6.1",
3232
+
"tracing",
3233
+
"windows-sys 0.60.2",
3234
+
]
3235
+
3236
+
[[package]]
3237
+
name = "quote"
3238
+
version = "1.0.41"
3239
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3240
+
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
3241
+
dependencies = [
3242
+
"proc-macro2",
3243
+
]
3244
+
3245
+
[[package]]
3246
+
name = "r-efi"
3247
+
version = "5.3.0"
3248
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3249
+
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
3250
+
3251
+
[[package]]
3252
+
name = "rand"
3253
+
version = "0.8.5"
3254
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3255
+
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
3256
+
dependencies = [
3257
+
"libc",
3258
+
"rand_chacha 0.3.1",
3259
+
"rand_core 0.6.4",
3260
+
]
3261
+
3262
+
[[package]]
3263
+
name = "rand"
3264
+
version = "0.9.2"
3265
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3266
+
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
3267
+
dependencies = [
3268
+
"rand_chacha 0.9.0",
3269
+
"rand_core 0.9.3",
3270
+
]
3271
+
3272
+
[[package]]
3273
+
name = "rand_chacha"
3274
+
version = "0.3.1"
3275
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3276
+
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
3277
+
dependencies = [
3278
+
"ppv-lite86",
3279
+
"rand_core 0.6.4",
3280
+
]
3281
+
3282
+
[[package]]
3283
+
name = "rand_chacha"
3284
+
version = "0.9.0"
3285
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3286
+
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
3287
+
dependencies = [
3288
+
"ppv-lite86",
3289
+
"rand_core 0.9.3",
3290
+
]
3291
+
3292
+
[[package]]
3293
+
name = "rand_core"
3294
+
version = "0.6.4"
3295
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3296
+
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
3297
+
dependencies = [
3298
+
"getrandom 0.2.16",
3299
+
]
3300
+
3301
+
[[package]]
3302
+
name = "rand_core"
3303
+
version = "0.9.3"
3304
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3305
+
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
3306
+
dependencies = [
3307
+
"getrandom 0.3.4",
3308
+
]
3309
+
3310
+
[[package]]
3311
+
name = "range-traits"
3312
+
version = "0.3.2"
3313
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3314
+
checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab"
3315
+
3316
+
[[package]]
3317
+
name = "rav1e"
3318
+
version = "0.7.1"
3319
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3320
+
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
3321
+
dependencies = [
3322
+
"arbitrary",
3323
+
"arg_enum_proc_macro",
3324
+
"arrayvec",
3325
+
"av1-grain",
3326
+
"bitstream-io",
3327
+
"built",
3328
+
"cfg-if",
3329
+
"interpolate_name",
3330
+
"itertools 0.12.1",
3331
+
"libc",
3332
+
"libfuzzer-sys",
3333
+
"log",
3334
+
"maybe-rayon",
3335
+
"new_debug_unreachable",
3336
+
"noop_proc_macro",
3337
+
"num-derive",
3338
+
"num-traits",
3339
+
"once_cell",
3340
+
"paste",
3341
+
"profiling",
3342
+
"rand 0.8.5",
3343
+
"rand_chacha 0.3.1",
3344
+
"simd_helpers",
3345
+
"system-deps",
3346
+
"thiserror 1.0.69",
3347
+
"v_frame",
3348
+
"wasm-bindgen",
3349
+
]
3350
+
3351
+
[[package]]
3352
+
name = "ravif"
3353
+
version = "0.11.20"
3354
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3355
+
checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b"
3356
+
dependencies = [
3357
+
"avif-serialize",
3358
+
"imgref",
3359
+
"loop9",
3360
+
"quick-error 2.0.1",
3361
+
"rav1e",
3362
+
"rayon",
3363
+
"rgb",
3364
+
]
3365
+
3366
+
[[package]]
3367
+
name = "raw-cpuid"
3368
+
version = "11.6.0"
3369
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3370
+
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
3371
+
dependencies = [
3372
+
"bitflags",
3373
+
]
3374
+
3375
+
[[package]]
3376
+
name = "raw-window-handle"
3377
+
version = "0.5.2"
3378
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3379
+
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
3380
+
3381
+
[[package]]
3382
+
name = "rayon"
3383
+
version = "1.11.0"
3384
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3385
+
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
3386
+
dependencies = [
3387
+
"either",
3388
+
"rayon-core",
3389
+
]
3390
+
3391
+
[[package]]
3392
+
name = "rayon-core"
3393
+
version = "1.13.0"
3394
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3395
+
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
3396
+
dependencies = [
3397
+
"crossbeam-deque",
3398
+
"crossbeam-utils",
3399
+
]
3400
+
3401
+
[[package]]
3402
+
name = "redis"
3403
+
version = "0.27.6"
3404
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3405
+
checksum = "09d8f99a4090c89cc489a94833c901ead69bfbf3877b4867d5482e321ee875bc"
3406
+
dependencies = [
3407
+
"arc-swap",
3408
+
"async-trait",
3409
+
"backon",
3410
+
"bytes",
3411
+
"combine",
3412
+
"futures",
3413
+
"futures-util",
3414
+
"itertools 0.13.0",
3415
+
"itoa",
3416
+
"num-bigint",
3417
+
"percent-encoding",
3418
+
"pin-project-lite",
3419
+
"ryu",
3420
+
"sha1_smol",
3421
+
"socket2 0.5.10",
3422
+
"tokio",
3423
+
"tokio-util",
3424
+
"url",
3425
+
]
3426
+
3427
+
[[package]]
3428
+
name = "redox_syscall"
3429
+
version = "0.5.18"
3430
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3431
+
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
3432
+
dependencies = [
3433
+
"bitflags",
3434
+
]
3435
+
3436
+
[[package]]
3437
+
name = "regex"
3438
+
version = "1.12.2"
3439
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3440
+
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
3441
+
dependencies = [
3442
+
"aho-corasick",
3443
+
"memchr",
3444
+
"regex-automata",
3445
+
"regex-syntax",
3446
+
]
3447
+
3448
+
[[package]]
3449
+
name = "regex-automata"
3450
+
version = "0.4.13"
3451
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3452
+
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
3453
+
dependencies = [
3454
+
"aho-corasick",
3455
+
"memchr",
3456
+
"regex-syntax",
3457
+
]
3458
+
3459
+
[[package]]
3460
+
name = "regex-syntax"
3461
+
version = "0.8.8"
3462
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3463
+
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
3464
+
3465
+
[[package]]
3466
+
name = "reqwest"
3467
+
version = "0.12.24"
3468
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3469
+
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
3470
+
dependencies = [
3471
+
"async-compression",
3472
+
"base64 0.22.1",
3473
+
"bytes",
3474
+
"encoding_rs",
3475
+
"futures-core",
3476
+
"futures-util",
3477
+
"h2",
3478
+
"http",
3479
+
"http-body",
3480
+
"http-body-util",
3481
+
"hyper",
3482
+
"hyper-rustls",
3483
+
"hyper-tls",
3484
+
"hyper-util",
3485
+
"js-sys",
3486
+
"log",
3487
+
"mime",
3488
+
"native-tls",
3489
+
"percent-encoding",
3490
+
"pin-project-lite",
3491
+
"quinn",
3492
+
"rustls",
3493
+
"rustls-pki-types",
3494
+
"serde",
3495
+
"serde_json",
3496
+
"serde_urlencoded",
3497
+
"sync_wrapper",
3498
+
"tokio",
3499
+
"tokio-native-tls",
3500
+
"tokio-rustls",
3501
+
"tokio-util",
3502
+
"tower",
3503
+
"tower-http",
3504
+
"tower-service",
3505
+
"url",
3506
+
"wasm-bindgen",
3507
+
"wasm-bindgen-futures",
3508
+
"wasm-streams",
3509
+
"web-sys",
3510
+
"webpki-roots",
3511
+
]
3512
+
3513
+
[[package]]
3514
+
name = "resolv-conf"
3515
+
version = "0.7.5"
3516
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3517
+
checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799"
3518
+
3519
+
[[package]]
3520
+
name = "rfc6979"
3521
+
version = "0.4.0"
3522
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3523
+
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
3524
+
dependencies = [
3525
+
"hmac",
3526
+
"subtle",
3527
+
]
3528
+
3529
+
[[package]]
3530
+
name = "rgb"
3531
+
version = "0.8.52"
3532
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3533
+
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
3534
+
3535
+
[[package]]
3536
+
name = "ring"
3537
+
version = "0.17.14"
3538
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3539
+
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
3540
+
dependencies = [
3541
+
"cc",
3542
+
"cfg-if",
3543
+
"getrandom 0.2.16",
3544
+
"libc",
3545
+
"untrusted",
3546
+
"windows-sys 0.52.0",
3547
+
]
3548
+
3549
+
[[package]]
3550
+
name = "rouille"
3551
+
version = "3.6.2"
3552
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3553
+
checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921"
3554
+
dependencies = [
3555
+
"base64 0.13.1",
3556
+
"brotli",
3557
+
"chrono",
3558
+
"deflate",
3559
+
"filetime",
3560
+
"multipart",
3561
+
"percent-encoding",
3562
+
"rand 0.8.5",
3563
+
"serde",
3564
+
"serde_derive",
3565
+
"serde_json",
3566
+
"sha1_smol",
3567
+
"threadpool",
3568
+
"time",
3569
+
"tiny_http",
3570
+
"url",
3571
+
]
3572
+
3573
+
[[package]]
3574
+
name = "rsa"
3575
+
version = "0.9.8"
3576
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3577
+
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
3578
+
dependencies = [
3579
+
"const-oid",
3580
+
"digest",
3581
+
"num-bigint-dig",
3582
+
"num-integer",
3583
+
"num-traits",
3584
+
"pkcs1",
3585
+
"pkcs8",
3586
+
"rand_core 0.6.4",
3587
+
"signature",
3588
+
"spki",
3589
+
"subtle",
3590
+
"zeroize",
3591
+
]
3592
+
3593
+
[[package]]
3594
+
name = "rustc-demangle"
3595
+
version = "0.1.26"
3596
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3597
+
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
3598
+
3599
+
[[package]]
3600
+
name = "rustc-hash"
3601
+
version = "2.1.1"
3602
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3603
+
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
3604
+
3605
+
[[package]]
3606
+
name = "rustdct"
3607
+
version = "0.7.1"
3608
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3609
+
checksum = "8b61555105d6a9bf98797c063c362a1d24ed8ab0431655e38f1cf51e52089551"
3610
+
dependencies = [
3611
+
"rustfft",
3612
+
]
3613
+
3614
+
[[package]]
3615
+
name = "rustfft"
3616
+
version = "6.4.1"
3617
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3618
+
checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89"
3619
+
dependencies = [
3620
+
"num-complex",
3621
+
"num-integer",
3622
+
"num-traits",
3623
+
"primal-check",
3624
+
"strength_reduce",
3625
+
"transpose",
3626
+
]
3627
+
3628
+
[[package]]
3629
+
name = "rustix"
3630
+
version = "1.1.2"
3631
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3632
+
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
3633
+
dependencies = [
3634
+
"bitflags",
3635
+
"errno",
3636
+
"libc",
3637
+
"linux-raw-sys",
3638
+
"windows-sys 0.61.2",
3639
+
]
3640
+
3641
+
[[package]]
3642
+
name = "rustls"
3643
+
version = "0.23.34"
3644
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3645
+
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
3646
+
dependencies = [
3647
+
"once_cell",
3648
+
"ring",
3649
+
"rustls-pki-types",
3650
+
"rustls-webpki",
3651
+
"subtle",
3652
+
"zeroize",
3653
+
]
3654
+
3655
+
[[package]]
3656
+
name = "rustls-native-certs"
3657
+
version = "0.8.2"
3658
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3659
+
checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923"
3660
+
dependencies = [
3661
+
"openssl-probe",
3662
+
"rustls-pki-types",
3663
+
"schannel",
3664
+
"security-framework 3.5.1",
3665
+
]
3666
+
3667
+
[[package]]
3668
+
name = "rustls-pki-types"
3669
+
version = "1.12.0"
3670
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3671
+
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
3672
+
dependencies = [
3673
+
"web-time",
3674
+
"zeroize",
3675
+
]
3676
+
3677
+
[[package]]
3678
+
name = "rustls-webpki"
3679
+
version = "0.103.7"
3680
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3681
+
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
3682
+
dependencies = [
3683
+
"ring",
3684
+
"rustls-pki-types",
3685
+
"untrusted",
3686
+
]
3687
+
3688
+
[[package]]
3689
+
name = "rustversion"
3690
+
version = "1.0.22"
3691
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3692
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
3693
+
3694
+
[[package]]
3695
+
name = "ryu"
3696
+
version = "1.0.20"
3697
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3698
+
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
3699
+
3700
+
[[package]]
3701
+
name = "safemem"
3702
+
version = "0.3.3"
3703
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3704
+
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
3705
+
3706
+
[[package]]
3707
+
name = "same-file"
3708
+
version = "1.0.6"
3709
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3710
+
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
3711
+
dependencies = [
3712
+
"winapi-util",
3713
+
]
3714
+
3715
+
[[package]]
3716
+
name = "schannel"
3717
+
version = "0.1.28"
3718
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3719
+
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
3720
+
dependencies = [
3721
+
"windows-sys 0.61.2",
3722
+
]
3723
+
3724
+
[[package]]
3725
+
name = "scoped-tls"
3726
+
version = "1.0.1"
3727
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3728
+
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
3729
+
3730
+
[[package]]
3731
+
name = "scopeguard"
3732
+
version = "1.2.0"
3733
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3734
+
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
3735
+
3736
+
[[package]]
3737
+
name = "sec1"
3738
+
version = "0.7.3"
3739
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3740
+
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
3741
+
dependencies = [
3742
+
"base16ct",
3743
+
"der",
3744
+
"generic-array",
3745
+
"pkcs8",
3746
+
"subtle",
3747
+
"zeroize",
3748
+
]
3749
+
3750
+
[[package]]
3751
+
name = "security-framework"
3752
+
version = "2.11.1"
3753
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3754
+
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
3755
+
dependencies = [
3756
+
"bitflags",
3757
+
"core-foundation 0.9.4",
3758
+
"core-foundation-sys",
3759
+
"libc",
3760
+
"security-framework-sys",
3761
+
]
3762
+
3763
+
[[package]]
3764
+
name = "security-framework"
3765
+
version = "3.5.1"
3766
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3767
+
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
3768
+
dependencies = [
3769
+
"bitflags",
3770
+
"core-foundation 0.10.1",
3771
+
"core-foundation-sys",
3772
+
"libc",
3773
+
"security-framework-sys",
3774
+
]
3775
+
3776
+
[[package]]
3777
+
name = "security-framework-sys"
3778
+
version = "2.15.0"
3779
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3780
+
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
3781
+
dependencies = [
3782
+
"core-foundation-sys",
3783
+
"libc",
3784
+
]
3785
+
3786
+
[[package]]
3787
+
name = "send_wrapper"
3788
+
version = "0.6.0"
3789
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3790
+
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
3791
+
3792
+
[[package]]
3793
+
name = "serde"
3794
+
version = "1.0.228"
3795
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3796
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
3797
+
dependencies = [
3798
+
"serde_core",
3799
+
"serde_derive",
3800
+
]
3801
+
3802
+
[[package]]
3803
+
name = "serde_bytes"
3804
+
version = "0.11.19"
3805
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3806
+
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
3807
+
dependencies = [
3808
+
"serde",
3809
+
"serde_core",
3810
+
]
3811
+
3812
+
[[package]]
3813
+
name = "serde_core"
3814
+
version = "1.0.228"
3815
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3816
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
3817
+
dependencies = [
3818
+
"serde_derive",
3819
+
]
3820
+
3821
+
[[package]]
3822
+
name = "serde_derive"
3823
+
version = "1.0.228"
3824
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3825
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
3826
+
dependencies = [
3827
+
"proc-macro2",
3828
+
"quote",
3829
+
"syn 2.0.108",
3830
+
]
3831
+
3832
+
[[package]]
3833
+
name = "serde_html_form"
3834
+
version = "0.2.8"
3835
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3836
+
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
3837
+
dependencies = [
3838
+
"form_urlencoded",
3839
+
"indexmap",
3840
+
"itoa",
3841
+
"ryu",
3842
+
"serde_core",
3843
+
]
3844
+
3845
+
[[package]]
3846
+
name = "serde_ipld_dagcbor"
3847
+
version = "0.6.4"
3848
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3849
+
checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778"
3850
+
dependencies = [
3851
+
"cbor4ii",
3852
+
"ipld-core",
3853
+
"scopeguard",
3854
+
"serde",
3855
+
]
3856
+
3857
+
[[package]]
3858
+
name = "serde_json"
3859
+
version = "1.0.145"
3860
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3861
+
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
3862
+
dependencies = [
3863
+
"itoa",
3864
+
"memchr",
3865
+
"ryu",
3866
+
"serde",
3867
+
"serde_core",
3868
+
]
3869
+
3870
+
[[package]]
3871
+
name = "serde_spanned"
3872
+
version = "0.6.9"
3873
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3874
+
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
3875
+
dependencies = [
3876
+
"serde",
3877
+
]
3878
+
3879
+
[[package]]
3880
+
name = "serde_urlencoded"
3881
+
version = "0.7.1"
3882
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3883
+
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
3884
+
dependencies = [
3885
+
"form_urlencoded",
3886
+
"itoa",
3887
+
"ryu",
3888
+
"serde",
3889
+
]
3890
+
3891
+
[[package]]
3892
+
name = "sha1"
3893
+
version = "0.10.6"
3894
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3895
+
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
3896
+
dependencies = [
3897
+
"cfg-if",
3898
+
"cpufeatures",
3899
+
"digest",
3900
+
]
3901
+
3902
+
[[package]]
3903
+
name = "sha1_smol"
3904
+
version = "1.0.1"
3905
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3906
+
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
3907
+
3908
+
[[package]]
3909
+
name = "sha2"
3910
+
version = "0.10.9"
3911
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3912
+
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
3913
+
dependencies = [
3914
+
"cfg-if",
3915
+
"cpufeatures",
3916
+
"digest",
3917
+
]
3918
+
3919
+
[[package]]
3920
+
name = "sharded-slab"
3921
+
version = "0.1.7"
3922
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3923
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
3924
+
dependencies = [
3925
+
"lazy_static",
3926
+
]
3927
+
3928
+
[[package]]
3929
+
name = "shlex"
3930
+
version = "1.3.0"
3931
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3932
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
3933
+
3934
+
[[package]]
3935
+
name = "signal-hook-registry"
3936
+
version = "1.4.6"
3937
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3938
+
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
3939
+
dependencies = [
3940
+
"libc",
3941
+
]
3942
+
3943
+
[[package]]
3944
+
name = "signature"
3945
+
version = "2.2.0"
3946
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3947
+
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
3948
+
dependencies = [
3949
+
"digest",
3950
+
"rand_core 0.6.4",
3951
+
]
3952
+
3953
+
[[package]]
3954
+
name = "simd-adler32"
3955
+
version = "0.3.7"
3956
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3957
+
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
3958
+
3959
+
[[package]]
3960
+
name = "simd_helpers"
3961
+
version = "0.1.0"
3962
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3963
+
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
3964
+
dependencies = [
3965
+
"quote",
3966
+
]
3967
+
3968
+
[[package]]
3969
+
name = "similar"
3970
+
version = "2.7.0"
3971
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3972
+
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
3973
+
3974
+
[[package]]
3975
+
name = "siphasher"
3976
+
version = "1.0.1"
3977
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3978
+
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
3979
+
3980
+
[[package]]
3981
+
name = "skywatch-phash-rs"
3982
+
version = "0.1.0"
3983
+
dependencies = [
3984
+
"chrono",
3985
+
"dotenvy",
3986
+
"futures",
3987
+
"futures-util",
3988
+
"governor",
3989
+
"image",
3990
+
"image_hasher",
3991
+
"jacquard",
3992
+
"jacquard-api",
3993
+
"jacquard-common",
3994
+
"jacquard-identity",
3995
+
"jacquard-oauth",
3996
+
"miette",
3997
+
"mockito",
3998
+
"redis",
3999
+
"reqwest",
4000
+
"serde",
4001
+
"serde_json",
4002
+
"thiserror 2.0.17",
4003
+
"tokio",
4004
+
"tokio-test",
4005
+
"tracing",
4006
+
"tracing-subscriber",
4007
+
"url",
4008
+
]
4009
+
4010
+
[[package]]
4011
+
name = "slab"
4012
+
version = "0.4.11"
4013
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4014
+
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
4015
+
4016
+
[[package]]
4017
+
name = "smallvec"
4018
+
version = "1.15.1"
4019
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4020
+
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
4021
+
4022
+
[[package]]
4023
+
name = "smol_str"
4024
+
version = "0.3.4"
4025
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4026
+
checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5"
4027
+
dependencies = [
4028
+
"borsh",
4029
+
"serde_core",
4030
+
]
4031
+
4032
+
[[package]]
4033
+
name = "socket2"
4034
+
version = "0.5.10"
4035
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4036
+
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
4037
+
dependencies = [
4038
+
"libc",
4039
+
"windows-sys 0.52.0",
4040
+
]
4041
+
4042
+
[[package]]
4043
+
name = "socket2"
4044
+
version = "0.6.1"
4045
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4046
+
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
4047
+
dependencies = [
4048
+
"libc",
4049
+
"windows-sys 0.60.2",
4050
+
]
4051
+
4052
+
[[package]]
4053
+
name = "spin"
4054
+
version = "0.9.8"
4055
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4056
+
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
4057
+
4058
+
[[package]]
4059
+
name = "spin"
4060
+
version = "0.10.0"
4061
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4062
+
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
4063
+
4064
+
[[package]]
4065
+
name = "spinning_top"
4066
+
version = "0.3.0"
4067
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4068
+
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
4069
+
dependencies = [
4070
+
"lock_api",
4071
+
]
4072
+
4073
+
[[package]]
4074
+
name = "spki"
4075
+
version = "0.7.3"
4076
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4077
+
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
4078
+
dependencies = [
4079
+
"base64ct",
4080
+
"der",
4081
+
]
4082
+
4083
+
[[package]]
4084
+
name = "stable_deref_trait"
4085
+
version = "1.2.1"
4086
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4087
+
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
4088
+
4089
+
[[package]]
4090
+
name = "static-regular-grammar"
4091
+
version = "2.0.2"
4092
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4093
+
checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957"
4094
+
dependencies = [
4095
+
"abnf",
4096
+
"btree-range-map",
4097
+
"ciborium",
4098
+
"hex_fmt",
4099
+
"indoc",
4100
+
"proc-macro-error",
4101
+
"proc-macro2",
4102
+
"quote",
4103
+
"serde",
4104
+
"sha2",
4105
+
"syn 2.0.108",
4106
+
"thiserror 1.0.69",
4107
+
]
4108
+
4109
+
[[package]]
4110
+
name = "static_assertions"
4111
+
version = "1.1.0"
4112
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4113
+
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
4114
+
4115
+
[[package]]
4116
+
name = "strength_reduce"
4117
+
version = "0.2.4"
4118
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4119
+
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
4120
+
4121
+
[[package]]
4122
+
name = "string_cache"
4123
+
version = "0.8.9"
4124
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4125
+
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
4126
+
dependencies = [
4127
+
"new_debug_unreachable",
4128
+
"parking_lot",
4129
+
"phf_shared",
4130
+
"precomputed-hash",
4131
+
"serde",
4132
+
]
4133
+
4134
+
[[package]]
4135
+
name = "string_cache_codegen"
4136
+
version = "0.5.4"
4137
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4138
+
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
4139
+
dependencies = [
4140
+
"phf_generator",
4141
+
"phf_shared",
4142
+
"proc-macro2",
4143
+
"quote",
4144
+
]
4145
+
4146
+
[[package]]
4147
+
name = "strsim"
4148
+
version = "0.11.1"
4149
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4150
+
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
4151
+
4152
+
[[package]]
4153
+
name = "subtle"
4154
+
version = "2.6.1"
4155
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4156
+
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
4157
+
4158
+
[[package]]
4159
+
name = "supports-color"
4160
+
version = "3.0.2"
4161
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4162
+
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
4163
+
dependencies = [
4164
+
"is_ci",
4165
+
]
4166
+
4167
+
[[package]]
4168
+
name = "supports-hyperlinks"
4169
+
version = "3.1.0"
4170
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4171
+
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
4172
+
4173
+
[[package]]
4174
+
name = "supports-unicode"
4175
+
version = "3.0.0"
4176
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4177
+
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
4178
+
4179
+
[[package]]
4180
+
name = "syn"
4181
+
version = "1.0.109"
4182
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4183
+
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
4184
+
dependencies = [
4185
+
"proc-macro2",
4186
+
"quote",
4187
+
"unicode-ident",
4188
+
]
4189
+
4190
+
[[package]]
4191
+
name = "syn"
4192
+
version = "2.0.108"
4193
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4194
+
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
4195
+
dependencies = [
4196
+
"proc-macro2",
4197
+
"quote",
4198
+
"unicode-ident",
4199
+
]
4200
+
4201
+
[[package]]
4202
+
name = "sync_wrapper"
4203
+
version = "1.0.2"
4204
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4205
+
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
4206
+
dependencies = [
4207
+
"futures-core",
4208
+
]
4209
+
4210
+
[[package]]
4211
+
name = "synstructure"
4212
+
version = "0.13.2"
4213
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4214
+
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
4215
+
dependencies = [
4216
+
"proc-macro2",
4217
+
"quote",
4218
+
"syn 2.0.108",
4219
+
]
4220
+
4221
+
[[package]]
4222
+
name = "system-configuration"
4223
+
version = "0.6.1"
4224
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4225
+
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
4226
+
dependencies = [
4227
+
"bitflags",
4228
+
"core-foundation 0.9.4",
4229
+
"system-configuration-sys",
4230
+
]
4231
+
4232
+
[[package]]
4233
+
name = "system-configuration-sys"
4234
+
version = "0.6.0"
4235
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4236
+
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
4237
+
dependencies = [
4238
+
"core-foundation-sys",
4239
+
"libc",
4240
+
]
4241
+
4242
+
[[package]]
4243
+
name = "system-deps"
4244
+
version = "6.2.2"
4245
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4246
+
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
4247
+
dependencies = [
4248
+
"cfg-expr",
4249
+
"heck 0.5.0",
4250
+
"pkg-config",
4251
+
"toml",
4252
+
"version-compare",
4253
+
]
4254
+
4255
+
[[package]]
4256
+
name = "target-lexicon"
4257
+
version = "0.12.16"
4258
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4259
+
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
4260
+
4261
+
[[package]]
4262
+
name = "tempfile"
4263
+
version = "3.23.0"
4264
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4265
+
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
4266
+
dependencies = [
4267
+
"fastrand",
4268
+
"getrandom 0.3.4",
4269
+
"once_cell",
4270
+
"rustix",
4271
+
"windows-sys 0.61.2",
4272
+
]
4273
+
4274
+
[[package]]
4275
+
name = "tendril"
4276
+
version = "0.4.3"
4277
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4278
+
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
4279
+
dependencies = [
4280
+
"futf",
4281
+
"mac",
4282
+
"utf-8",
4283
+
]
4284
+
4285
+
[[package]]
4286
+
name = "terminal_size"
4287
+
version = "0.4.3"
4288
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4289
+
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
4290
+
dependencies = [
4291
+
"rustix",
4292
+
"windows-sys 0.60.2",
4293
+
]
4294
+
4295
+
[[package]]
4296
+
name = "textwrap"
4297
+
version = "0.16.2"
4298
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4299
+
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
4300
+
dependencies = [
4301
+
"unicode-linebreak",
4302
+
"unicode-width 0.2.2",
4303
+
]
4304
+
4305
+
[[package]]
4306
+
name = "thiserror"
4307
+
version = "1.0.69"
4308
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4309
+
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
4310
+
dependencies = [
4311
+
"thiserror-impl 1.0.69",
4312
+
]
4313
+
4314
+
[[package]]
4315
+
name = "thiserror"
4316
+
version = "2.0.17"
4317
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4318
+
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
4319
+
dependencies = [
4320
+
"thiserror-impl 2.0.17",
4321
+
]
4322
+
4323
+
[[package]]
4324
+
name = "thiserror-impl"
4325
+
version = "1.0.69"
4326
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4327
+
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
4328
+
dependencies = [
4329
+
"proc-macro2",
4330
+
"quote",
4331
+
"syn 2.0.108",
4332
+
]
4333
+
4334
+
[[package]]
4335
+
name = "thiserror-impl"
4336
+
version = "2.0.17"
4337
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4338
+
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
4339
+
dependencies = [
4340
+
"proc-macro2",
4341
+
"quote",
4342
+
"syn 2.0.108",
4343
+
]
4344
+
4345
+
[[package]]
4346
+
name = "thread_local"
4347
+
version = "1.1.9"
4348
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4349
+
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
4350
+
dependencies = [
4351
+
"cfg-if",
4352
+
]
4353
+
4354
+
[[package]]
4355
+
name = "threadpool"
4356
+
version = "1.8.1"
4357
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4358
+
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
4359
+
dependencies = [
4360
+
"num_cpus",
4361
+
]
4362
+
4363
+
[[package]]
4364
+
name = "tiff"
4365
+
version = "0.10.3"
4366
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4367
+
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
4368
+
dependencies = [
4369
+
"fax",
4370
+
"flate2",
4371
+
"half",
4372
+
"quick-error 2.0.1",
4373
+
"weezl",
4374
+
"zune-jpeg",
4375
+
]
4376
+
4377
+
[[package]]
4378
+
name = "time"
4379
+
version = "0.3.44"
4380
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4381
+
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
4382
+
dependencies = [
4383
+
"deranged",
4384
+
"libc",
4385
+
"num-conv",
4386
+
"num_threads",
4387
+
"powerfmt",
4388
+
"serde",
4389
+
"time-core",
4390
+
]
4391
+
4392
+
[[package]]
4393
+
name = "time-core"
4394
+
version = "0.1.6"
4395
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4396
+
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
4397
+
4398
+
[[package]]
4399
+
name = "tiny_http"
4400
+
version = "0.12.0"
4401
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4402
+
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
4403
+
dependencies = [
4404
+
"ascii",
4405
+
"chunked_transfer",
4406
+
"httpdate",
4407
+
"log",
4408
+
]
4409
+
4410
+
[[package]]
4411
+
name = "tinystr"
4412
+
version = "0.8.1"
4413
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4414
+
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
4415
+
dependencies = [
4416
+
"displaydoc",
4417
+
"zerovec",
4418
+
]
4419
+
4420
+
[[package]]
4421
+
name = "tinyvec"
4422
+
version = "1.10.0"
4423
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4424
+
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
4425
+
dependencies = [
4426
+
"tinyvec_macros",
4427
+
]
4428
+
4429
+
[[package]]
4430
+
name = "tinyvec_macros"
4431
+
version = "0.1.1"
4432
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4433
+
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
4434
+
4435
+
[[package]]
4436
+
name = "tokio"
4437
+
version = "1.48.0"
4438
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4439
+
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
4440
+
dependencies = [
4441
+
"bytes",
4442
+
"libc",
4443
+
"mio",
4444
+
"parking_lot",
4445
+
"pin-project-lite",
4446
+
"signal-hook-registry",
4447
+
"socket2 0.6.1",
4448
+
"tokio-macros",
4449
+
"windows-sys 0.61.2",
4450
+
]
4451
+
4452
+
[[package]]
4453
+
name = "tokio-macros"
4454
+
version = "2.6.0"
4455
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4456
+
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
4457
+
dependencies = [
4458
+
"proc-macro2",
4459
+
"quote",
4460
+
"syn 2.0.108",
4461
+
]
4462
+
4463
+
[[package]]
4464
+
name = "tokio-native-tls"
4465
+
version = "0.3.1"
4466
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4467
+
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
4468
+
dependencies = [
4469
+
"native-tls",
4470
+
"tokio",
4471
+
]
4472
+
4473
+
[[package]]
4474
+
name = "tokio-rustls"
4475
+
version = "0.26.4"
4476
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4477
+
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
4478
+
dependencies = [
4479
+
"rustls",
4480
+
"tokio",
4481
+
]
4482
+
4483
+
[[package]]
4484
+
name = "tokio-stream"
4485
+
version = "0.1.17"
4486
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4487
+
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
4488
+
dependencies = [
4489
+
"futures-core",
4490
+
"pin-project-lite",
4491
+
"tokio",
4492
+
]
4493
+
4494
+
[[package]]
4495
+
name = "tokio-test"
4496
+
version = "0.4.4"
4497
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4498
+
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
4499
+
dependencies = [
4500
+
"async-stream",
4501
+
"bytes",
4502
+
"futures-core",
4503
+
"tokio",
4504
+
"tokio-stream",
4505
+
]
4506
+
4507
+
[[package]]
4508
+
name = "tokio-tungstenite"
4509
+
version = "0.24.0"
4510
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4511
+
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
4512
+
dependencies = [
4513
+
"futures-util",
4514
+
"log",
4515
+
"rustls",
4516
+
"rustls-native-certs",
4517
+
"rustls-pki-types",
4518
+
"tokio",
4519
+
"tokio-rustls",
4520
+
"tungstenite",
4521
+
]
4522
+
4523
+
[[package]]
4524
+
name = "tokio-tungstenite-wasm"
4525
+
version = "0.4.0"
4526
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4527
+
checksum = "e21a5c399399c3db9f08d8297ac12b500e86bca82e930253fdc62eaf9c0de6ae"
4528
+
dependencies = [
4529
+
"futures-channel",
4530
+
"futures-util",
4531
+
"http",
4532
+
"httparse",
4533
+
"js-sys",
4534
+
"rustls",
4535
+
"thiserror 1.0.69",
4536
+
"tokio",
4537
+
"tokio-tungstenite",
4538
+
"wasm-bindgen",
4539
+
"web-sys",
4540
+
]
4541
+
4542
+
[[package]]
4543
+
name = "tokio-util"
4544
+
version = "0.7.16"
4545
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4546
+
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
4547
+
dependencies = [
4548
+
"bytes",
4549
+
"futures-core",
4550
+
"futures-sink",
4551
+
"futures-util",
4552
+
"pin-project-lite",
4553
+
"tokio",
4554
+
]
4555
+
4556
+
[[package]]
4557
+
name = "toml"
4558
+
version = "0.8.23"
4559
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4560
+
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
4561
+
dependencies = [
4562
+
"serde",
4563
+
"serde_spanned",
4564
+
"toml_datetime",
4565
+
"toml_edit",
4566
+
]
4567
+
4568
+
[[package]]
4569
+
name = "toml_datetime"
4570
+
version = "0.6.11"
4571
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4572
+
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
4573
+
dependencies = [
4574
+
"serde",
4575
+
]
4576
+
4577
+
[[package]]
4578
+
name = "toml_edit"
4579
+
version = "0.22.27"
4580
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4581
+
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
4582
+
dependencies = [
4583
+
"indexmap",
4584
+
"serde",
4585
+
"serde_spanned",
4586
+
"toml_datetime",
4587
+
"winnow",
4588
+
]
4589
+
4590
+
[[package]]
4591
+
name = "tower"
4592
+
version = "0.5.2"
4593
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4594
+
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
4595
+
dependencies = [
4596
+
"futures-core",
4597
+
"futures-util",
4598
+
"pin-project-lite",
4599
+
"sync_wrapper",
4600
+
"tokio",
4601
+
"tower-layer",
4602
+
"tower-service",
4603
+
]
4604
+
4605
+
[[package]]
4606
+
name = "tower-http"
4607
+
version = "0.6.6"
4608
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4609
+
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
4610
+
dependencies = [
4611
+
"bitflags",
4612
+
"bytes",
4613
+
"futures-util",
4614
+
"http",
4615
+
"http-body",
4616
+
"iri-string",
4617
+
"pin-project-lite",
4618
+
"tower",
4619
+
"tower-layer",
4620
+
"tower-service",
4621
+
]
4622
+
4623
+
[[package]]
4624
+
name = "tower-layer"
4625
+
version = "0.3.3"
4626
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4627
+
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
4628
+
4629
+
[[package]]
4630
+
name = "tower-service"
4631
+
version = "0.3.3"
4632
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4633
+
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
4634
+
4635
+
[[package]]
4636
+
name = "tracing"
4637
+
version = "0.1.41"
4638
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4639
+
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
4640
+
dependencies = [
4641
+
"pin-project-lite",
4642
+
"tracing-attributes",
4643
+
"tracing-core",
4644
+
]
4645
+
4646
+
[[package]]
4647
+
name = "tracing-attributes"
4648
+
version = "0.1.30"
4649
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4650
+
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
4651
+
dependencies = [
4652
+
"proc-macro2",
4653
+
"quote",
4654
+
"syn 2.0.108",
4655
+
]
4656
+
4657
+
[[package]]
4658
+
name = "tracing-core"
4659
+
version = "0.1.34"
4660
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4661
+
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
4662
+
dependencies = [
4663
+
"once_cell",
4664
+
"valuable",
4665
+
]
4666
+
4667
+
[[package]]
4668
+
name = "tracing-log"
4669
+
version = "0.2.0"
4670
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4671
+
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
4672
+
dependencies = [
4673
+
"log",
4674
+
"once_cell",
4675
+
"tracing-core",
4676
+
]
4677
+
4678
+
[[package]]
4679
+
name = "tracing-serde"
4680
+
version = "0.2.0"
4681
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4682
+
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
4683
+
dependencies = [
4684
+
"serde",
4685
+
"tracing-core",
4686
+
]
4687
+
4688
+
[[package]]
4689
+
name = "tracing-subscriber"
4690
+
version = "0.3.20"
4691
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4692
+
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
4693
+
dependencies = [
4694
+
"matchers",
4695
+
"nu-ansi-term",
4696
+
"once_cell",
4697
+
"regex-automata",
4698
+
"serde",
4699
+
"serde_json",
4700
+
"sharded-slab",
4701
+
"smallvec",
4702
+
"thread_local",
4703
+
"tracing",
4704
+
"tracing-core",
4705
+
"tracing-log",
4706
+
"tracing-serde",
4707
+
]
4708
+
4709
+
[[package]]
4710
+
name = "trait-variant"
4711
+
version = "0.1.2"
4712
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4713
+
checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
4714
+
dependencies = [
4715
+
"proc-macro2",
4716
+
"quote",
4717
+
"syn 2.0.108",
4718
+
]
4719
+
4720
+
[[package]]
4721
+
name = "transpose"
4722
+
version = "0.2.3"
4723
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4724
+
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
4725
+
dependencies = [
4726
+
"num-integer",
4727
+
"strength_reduce",
4728
+
]
4729
+
4730
+
[[package]]
4731
+
name = "try-lock"
4732
+
version = "0.2.5"
4733
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4734
+
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
4735
+
4736
+
[[package]]
4737
+
name = "tungstenite"
4738
+
version = "0.24.0"
4739
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4740
+
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
4741
+
dependencies = [
4742
+
"byteorder",
4743
+
"bytes",
4744
+
"data-encoding",
4745
+
"http",
4746
+
"httparse",
4747
+
"log",
4748
+
"rand 0.8.5",
4749
+
"rustls",
4750
+
"rustls-pki-types",
4751
+
"sha1",
4752
+
"thiserror 1.0.69",
4753
+
"utf-8",
4754
+
]
4755
+
4756
+
[[package]]
4757
+
name = "twoway"
4758
+
version = "0.1.8"
4759
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4760
+
checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
4761
+
dependencies = [
4762
+
"memchr",
4763
+
]
4764
+
4765
+
[[package]]
4766
+
name = "typenum"
4767
+
version = "1.19.0"
4768
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4769
+
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
4770
+
4771
+
[[package]]
4772
+
name = "unicase"
4773
+
version = "2.8.1"
4774
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4775
+
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
4776
+
4777
+
[[package]]
4778
+
name = "unicode-ident"
4779
+
version = "1.0.20"
4780
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4781
+
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
4782
+
4783
+
[[package]]
4784
+
name = "unicode-linebreak"
4785
+
version = "0.1.5"
4786
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4787
+
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
4788
+
4789
+
[[package]]
4790
+
name = "unicode-width"
4791
+
version = "0.1.14"
4792
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4793
+
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
4794
+
4795
+
[[package]]
4796
+
name = "unicode-width"
4797
+
version = "0.2.2"
4798
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4799
+
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
4800
+
4801
+
[[package]]
4802
+
name = "unicode-xid"
4803
+
version = "0.2.6"
4804
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4805
+
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
4806
+
4807
+
[[package]]
4808
+
name = "unsigned-varint"
4809
+
version = "0.8.0"
4810
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4811
+
checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
4812
+
4813
+
[[package]]
4814
+
name = "untrusted"
4815
+
version = "0.9.0"
4816
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4817
+
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
4818
+
4819
+
[[package]]
4820
+
name = "url"
4821
+
version = "2.5.7"
4822
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4823
+
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
4824
+
dependencies = [
4825
+
"form_urlencoded",
4826
+
"idna",
4827
+
"percent-encoding",
4828
+
"serde",
4829
+
]
4830
+
4831
+
[[package]]
4832
+
name = "urlencoding"
4833
+
version = "2.1.3"
4834
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4835
+
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
4836
+
4837
+
[[package]]
4838
+
name = "utf-8"
4839
+
version = "0.7.6"
4840
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4841
+
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
4842
+
4843
+
[[package]]
4844
+
name = "utf8_iter"
4845
+
version = "1.0.4"
4846
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4847
+
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
4848
+
4849
+
[[package]]
4850
+
name = "v_frame"
4851
+
version = "0.3.9"
4852
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4853
+
checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
4854
+
dependencies = [
4855
+
"aligned-vec",
4856
+
"num-traits",
4857
+
"wasm-bindgen",
4858
+
]
4859
+
4860
+
[[package]]
4861
+
name = "valuable"
4862
+
version = "0.1.1"
4863
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4864
+
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
4865
+
4866
+
[[package]]
4867
+
name = "vcpkg"
4868
+
version = "0.2.15"
4869
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4870
+
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
4871
+
4872
+
[[package]]
4873
+
name = "version-compare"
4874
+
version = "0.2.0"
4875
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4876
+
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
4877
+
4878
+
[[package]]
4879
+
name = "version_check"
4880
+
version = "0.9.5"
4881
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4882
+
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
4883
+
4884
+
[[package]]
4885
+
name = "walkdir"
4886
+
version = "2.5.0"
4887
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4888
+
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
4889
+
dependencies = [
4890
+
"same-file",
4891
+
"winapi-util",
4892
+
]
4893
+
4894
+
[[package]]
4895
+
name = "want"
4896
+
version = "0.3.1"
4897
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4898
+
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
4899
+
dependencies = [
4900
+
"try-lock",
4901
+
]
4902
+
4903
+
[[package]]
4904
+
name = "wasi"
4905
+
version = "0.11.1+wasi-snapshot-preview1"
4906
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4907
+
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
4908
+
4909
+
[[package]]
4910
+
name = "wasip2"
4911
+
version = "1.0.1+wasi-0.2.4"
4912
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4913
+
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
4914
+
dependencies = [
4915
+
"wit-bindgen",
4916
+
]
4917
+
4918
+
[[package]]
4919
+
name = "wasm-bindgen"
4920
+
version = "0.2.104"
4921
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4922
+
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
4923
+
dependencies = [
4924
+
"cfg-if",
4925
+
"once_cell",
4926
+
"rustversion",
4927
+
"wasm-bindgen-macro",
4928
+
"wasm-bindgen-shared",
4929
+
]
4930
+
4931
+
[[package]]
4932
+
name = "wasm-bindgen-backend"
4933
+
version = "0.2.104"
4934
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4935
+
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
4936
+
dependencies = [
4937
+
"bumpalo",
4938
+
"log",
4939
+
"proc-macro2",
4940
+
"quote",
4941
+
"syn 2.0.108",
4942
+
"wasm-bindgen-shared",
4943
+
]
4944
+
4945
+
[[package]]
4946
+
name = "wasm-bindgen-futures"
4947
+
version = "0.4.54"
4948
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4949
+
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
4950
+
dependencies = [
4951
+
"cfg-if",
4952
+
"js-sys",
4953
+
"once_cell",
4954
+
"wasm-bindgen",
4955
+
"web-sys",
4956
+
]
4957
+
4958
+
[[package]]
4959
+
name = "wasm-bindgen-macro"
4960
+
version = "0.2.104"
4961
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4962
+
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
4963
+
dependencies = [
4964
+
"quote",
4965
+
"wasm-bindgen-macro-support",
4966
+
]
4967
+
4968
+
[[package]]
4969
+
name = "wasm-bindgen-macro-support"
4970
+
version = "0.2.104"
4971
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4972
+
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
4973
+
dependencies = [
4974
+
"proc-macro2",
4975
+
"quote",
4976
+
"syn 2.0.108",
4977
+
"wasm-bindgen-backend",
4978
+
"wasm-bindgen-shared",
4979
+
]
4980
+
4981
+
[[package]]
4982
+
name = "wasm-bindgen-shared"
4983
+
version = "0.2.104"
4984
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4985
+
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
4986
+
dependencies = [
4987
+
"unicode-ident",
4988
+
]
4989
+
4990
+
[[package]]
4991
+
name = "wasm-streams"
4992
+
version = "0.4.2"
4993
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4994
+
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
4995
+
dependencies = [
4996
+
"futures-util",
4997
+
"js-sys",
4998
+
"wasm-bindgen",
4999
+
"wasm-bindgen-futures",
5000
+
"web-sys",
5001
+
]
5002
+
5003
+
[[package]]
5004
+
name = "web-sys"
5005
+
version = "0.3.81"
5006
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5007
+
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
5008
+
dependencies = [
5009
+
"js-sys",
5010
+
"wasm-bindgen",
5011
+
]
5012
+
5013
+
[[package]]
5014
+
name = "web-time"
5015
+
version = "1.1.0"
5016
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5017
+
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
5018
+
dependencies = [
5019
+
"js-sys",
5020
+
"wasm-bindgen",
5021
+
]
5022
+
5023
+
[[package]]
5024
+
name = "webbrowser"
5025
+
version = "0.8.15"
5026
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5027
+
checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b"
5028
+
dependencies = [
5029
+
"core-foundation 0.9.4",
5030
+
"home",
5031
+
"jni",
5032
+
"log",
5033
+
"ndk-context",
5034
+
"objc",
5035
+
"raw-window-handle",
5036
+
"url",
5037
+
"web-sys",
5038
+
]
5039
+
5040
+
[[package]]
5041
+
name = "webpage"
5042
+
version = "2.0.1"
5043
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5044
+
checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac"
5045
+
dependencies = [
5046
+
"html5ever",
5047
+
"markup5ever_rcdom",
5048
+
"serde_json",
5049
+
"url",
5050
+
]
5051
+
5052
+
[[package]]
5053
+
name = "webpki-roots"
5054
+
version = "1.0.3"
5055
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5056
+
checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8"
5057
+
dependencies = [
5058
+
"rustls-pki-types",
5059
+
]
5060
+
5061
+
[[package]]
5062
+
name = "weezl"
5063
+
version = "0.1.10"
5064
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5065
+
checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
5066
+
5067
+
[[package]]
5068
+
name = "widestring"
5069
+
version = "1.2.1"
5070
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5071
+
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
5072
+
5073
+
[[package]]
5074
+
name = "winapi"
5075
+
version = "0.3.9"
5076
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5077
+
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
5078
+
dependencies = [
5079
+
"winapi-i686-pc-windows-gnu",
5080
+
"winapi-x86_64-pc-windows-gnu",
5081
+
]
5082
+
5083
+
[[package]]
5084
+
name = "winapi-i686-pc-windows-gnu"
5085
+
version = "0.4.0"
5086
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5087
+
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
5088
+
5089
+
[[package]]
5090
+
name = "winapi-util"
5091
+
version = "0.1.11"
5092
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5093
+
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
5094
+
dependencies = [
5095
+
"windows-sys 0.61.2",
5096
+
]
5097
+
5098
+
[[package]]
5099
+
name = "winapi-x86_64-pc-windows-gnu"
5100
+
version = "0.4.0"
5101
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5102
+
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
5103
+
5104
+
[[package]]
5105
+
name = "windows"
5106
+
version = "0.61.3"
5107
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5108
+
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
5109
+
dependencies = [
5110
+
"windows-collections",
5111
+
"windows-core 0.61.2",
5112
+
"windows-future",
5113
+
"windows-link 0.1.3",
5114
+
"windows-numerics",
5115
+
]
5116
+
5117
+
[[package]]
5118
+
name = "windows-collections"
5119
+
version = "0.2.0"
5120
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5121
+
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
5122
+
dependencies = [
5123
+
"windows-core 0.61.2",
5124
+
]
5125
+
5126
+
[[package]]
5127
+
name = "windows-core"
5128
+
version = "0.61.2"
5129
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5130
+
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
5131
+
dependencies = [
5132
+
"windows-implement",
5133
+
"windows-interface",
5134
+
"windows-link 0.1.3",
5135
+
"windows-result 0.3.4",
5136
+
"windows-strings 0.4.2",
5137
+
]
5138
+
5139
+
[[package]]
5140
+
name = "windows-core"
5141
+
version = "0.62.2"
5142
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5143
+
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
5144
+
dependencies = [
5145
+
"windows-implement",
5146
+
"windows-interface",
5147
+
"windows-link 0.2.1",
5148
+
"windows-result 0.4.1",
5149
+
"windows-strings 0.5.1",
5150
+
]
5151
+
5152
+
[[package]]
5153
+
name = "windows-future"
5154
+
version = "0.2.1"
5155
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5156
+
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
5157
+
dependencies = [
5158
+
"windows-core 0.61.2",
5159
+
"windows-link 0.1.3",
5160
+
"windows-threading",
5161
+
]
5162
+
5163
+
[[package]]
5164
+
name = "windows-implement"
5165
+
version = "0.60.2"
5166
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5167
+
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
5168
+
dependencies = [
5169
+
"proc-macro2",
5170
+
"quote",
5171
+
"syn 2.0.108",
5172
+
]
5173
+
5174
+
[[package]]
5175
+
name = "windows-interface"
5176
+
version = "0.59.3"
5177
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5178
+
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
5179
+
dependencies = [
5180
+
"proc-macro2",
5181
+
"quote",
5182
+
"syn 2.0.108",
5183
+
]
5184
+
5185
+
[[package]]
5186
+
name = "windows-link"
5187
+
version = "0.1.3"
5188
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5189
+
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
5190
+
5191
+
[[package]]
5192
+
name = "windows-link"
5193
+
version = "0.2.1"
5194
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5195
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
5196
+
5197
+
[[package]]
5198
+
name = "windows-numerics"
5199
+
version = "0.2.0"
5200
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5201
+
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
5202
+
dependencies = [
5203
+
"windows-core 0.61.2",
5204
+
"windows-link 0.1.3",
5205
+
]
5206
+
5207
+
[[package]]
5208
+
name = "windows-registry"
5209
+
version = "0.5.3"
5210
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5211
+
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
5212
+
dependencies = [
5213
+
"windows-link 0.1.3",
5214
+
"windows-result 0.3.4",
5215
+
"windows-strings 0.4.2",
5216
+
]
5217
+
5218
+
[[package]]
5219
+
name = "windows-result"
5220
+
version = "0.3.4"
5221
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5222
+
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
5223
+
dependencies = [
5224
+
"windows-link 0.1.3",
5225
+
]
5226
+
5227
+
[[package]]
5228
+
name = "windows-result"
5229
+
version = "0.4.1"
5230
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5231
+
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
5232
+
dependencies = [
5233
+
"windows-link 0.2.1",
5234
+
]
5235
+
5236
+
[[package]]
5237
+
name = "windows-strings"
5238
+
version = "0.4.2"
5239
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5240
+
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
5241
+
dependencies = [
5242
+
"windows-link 0.1.3",
5243
+
]
5244
+
5245
+
[[package]]
5246
+
name = "windows-strings"
5247
+
version = "0.5.1"
5248
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5249
+
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
5250
+
dependencies = [
5251
+
"windows-link 0.2.1",
5252
+
]
5253
+
5254
+
[[package]]
5255
+
name = "windows-sys"
5256
+
version = "0.45.0"
5257
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5258
+
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
5259
+
dependencies = [
5260
+
"windows-targets 0.42.2",
5261
+
]
5262
+
5263
+
[[package]]
5264
+
name = "windows-sys"
5265
+
version = "0.48.0"
5266
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5267
+
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
5268
+
dependencies = [
5269
+
"windows-targets 0.48.5",
5270
+
]
5271
+
5272
+
[[package]]
5273
+
name = "windows-sys"
5274
+
version = "0.52.0"
5275
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5276
+
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
5277
+
dependencies = [
5278
+
"windows-targets 0.52.6",
5279
+
]
5280
+
5281
+
[[package]]
5282
+
name = "windows-sys"
5283
+
version = "0.59.0"
5284
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5285
+
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
5286
+
dependencies = [
5287
+
"windows-targets 0.52.6",
5288
+
]
5289
+
5290
+
[[package]]
5291
+
name = "windows-sys"
5292
+
version = "0.60.2"
5293
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5294
+
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
5295
+
dependencies = [
5296
+
"windows-targets 0.53.5",
5297
+
]
5298
+
5299
+
[[package]]
5300
+
name = "windows-sys"
5301
+
version = "0.61.2"
5302
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5303
+
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
5304
+
dependencies = [
5305
+
"windows-link 0.2.1",
5306
+
]
5307
+
5308
+
[[package]]
5309
+
name = "windows-targets"
5310
+
version = "0.42.2"
5311
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5312
+
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
5313
+
dependencies = [
5314
+
"windows_aarch64_gnullvm 0.42.2",
5315
+
"windows_aarch64_msvc 0.42.2",
5316
+
"windows_i686_gnu 0.42.2",
5317
+
"windows_i686_msvc 0.42.2",
5318
+
"windows_x86_64_gnu 0.42.2",
5319
+
"windows_x86_64_gnullvm 0.42.2",
5320
+
"windows_x86_64_msvc 0.42.2",
5321
+
]
5322
+
5323
+
[[package]]
5324
+
name = "windows-targets"
5325
+
version = "0.48.5"
5326
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5327
+
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
5328
+
dependencies = [
5329
+
"windows_aarch64_gnullvm 0.48.5",
5330
+
"windows_aarch64_msvc 0.48.5",
5331
+
"windows_i686_gnu 0.48.5",
5332
+
"windows_i686_msvc 0.48.5",
5333
+
"windows_x86_64_gnu 0.48.5",
5334
+
"windows_x86_64_gnullvm 0.48.5",
5335
+
"windows_x86_64_msvc 0.48.5",
5336
+
]
5337
+
5338
+
[[package]]
5339
+
name = "windows-targets"
5340
+
version = "0.52.6"
5341
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5342
+
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
5343
+
dependencies = [
5344
+
"windows_aarch64_gnullvm 0.52.6",
5345
+
"windows_aarch64_msvc 0.52.6",
5346
+
"windows_i686_gnu 0.52.6",
5347
+
"windows_i686_gnullvm 0.52.6",
5348
+
"windows_i686_msvc 0.52.6",
5349
+
"windows_x86_64_gnu 0.52.6",
5350
+
"windows_x86_64_gnullvm 0.52.6",
5351
+
"windows_x86_64_msvc 0.52.6",
5352
+
]
5353
+
5354
+
[[package]]
5355
+
name = "windows-targets"
5356
+
version = "0.53.5"
5357
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5358
+
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
5359
+
dependencies = [
5360
+
"windows-link 0.2.1",
5361
+
"windows_aarch64_gnullvm 0.53.1",
5362
+
"windows_aarch64_msvc 0.53.1",
5363
+
"windows_i686_gnu 0.53.1",
5364
+
"windows_i686_gnullvm 0.53.1",
5365
+
"windows_i686_msvc 0.53.1",
5366
+
"windows_x86_64_gnu 0.53.1",
5367
+
"windows_x86_64_gnullvm 0.53.1",
5368
+
"windows_x86_64_msvc 0.53.1",
5369
+
]
5370
+
5371
+
[[package]]
5372
+
name = "windows-threading"
5373
+
version = "0.1.0"
5374
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5375
+
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
5376
+
dependencies = [
5377
+
"windows-link 0.1.3",
5378
+
]
5379
+
5380
+
[[package]]
5381
+
name = "windows_aarch64_gnullvm"
5382
+
version = "0.42.2"
5383
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5384
+
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
5385
+
5386
+
[[package]]
5387
+
name = "windows_aarch64_gnullvm"
5388
+
version = "0.48.5"
5389
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5390
+
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
5391
+
5392
+
[[package]]
5393
+
name = "windows_aarch64_gnullvm"
5394
+
version = "0.52.6"
5395
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5396
+
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
5397
+
5398
+
[[package]]
5399
+
name = "windows_aarch64_gnullvm"
5400
+
version = "0.53.1"
5401
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5402
+
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
5403
+
5404
+
[[package]]
5405
+
name = "windows_aarch64_msvc"
5406
+
version = "0.42.2"
5407
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5408
+
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
5409
+
5410
+
[[package]]
5411
+
name = "windows_aarch64_msvc"
5412
+
version = "0.48.5"
5413
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5414
+
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
5415
+
5416
+
[[package]]
5417
+
name = "windows_aarch64_msvc"
5418
+
version = "0.52.6"
5419
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5420
+
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
5421
+
5422
+
[[package]]
5423
+
name = "windows_aarch64_msvc"
5424
+
version = "0.53.1"
5425
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5426
+
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
5427
+
5428
+
[[package]]
5429
+
name = "windows_i686_gnu"
5430
+
version = "0.42.2"
5431
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5432
+
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
5433
+
5434
+
[[package]]
5435
+
name = "windows_i686_gnu"
5436
+
version = "0.48.5"
5437
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5438
+
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
5439
+
5440
+
[[package]]
5441
+
name = "windows_i686_gnu"
5442
+
version = "0.52.6"
5443
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5444
+
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
5445
+
5446
+
[[package]]
5447
+
name = "windows_i686_gnu"
5448
+
version = "0.53.1"
5449
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5450
+
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
5451
+
5452
+
[[package]]
5453
+
name = "windows_i686_gnullvm"
5454
+
version = "0.52.6"
5455
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5456
+
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
5457
+
5458
+
[[package]]
5459
+
name = "windows_i686_gnullvm"
5460
+
version = "0.53.1"
5461
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5462
+
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
5463
+
5464
+
[[package]]
5465
+
name = "windows_i686_msvc"
5466
+
version = "0.42.2"
5467
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5468
+
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
5469
+
5470
+
[[package]]
5471
+
name = "windows_i686_msvc"
5472
+
version = "0.48.5"
5473
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5474
+
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
5475
+
5476
+
[[package]]
5477
+
name = "windows_i686_msvc"
5478
+
version = "0.52.6"
5479
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5480
+
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
5481
+
5482
+
[[package]]
5483
+
name = "windows_i686_msvc"
5484
+
version = "0.53.1"
5485
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5486
+
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
5487
+
5488
+
[[package]]
5489
+
name = "windows_x86_64_gnu"
5490
+
version = "0.42.2"
5491
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5492
+
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
5493
+
5494
+
[[package]]
5495
+
name = "windows_x86_64_gnu"
5496
+
version = "0.48.5"
5497
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5498
+
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
5499
+
5500
+
[[package]]
5501
+
name = "windows_x86_64_gnu"
5502
+
version = "0.52.6"
5503
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5504
+
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
5505
+
5506
+
[[package]]
5507
+
name = "windows_x86_64_gnu"
5508
+
version = "0.53.1"
5509
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5510
+
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
5511
+
5512
+
[[package]]
5513
+
name = "windows_x86_64_gnullvm"
5514
+
version = "0.42.2"
5515
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5516
+
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
5517
+
5518
+
[[package]]
5519
+
name = "windows_x86_64_gnullvm"
5520
+
version = "0.48.5"
5521
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5522
+
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
5523
+
5524
+
[[package]]
5525
+
name = "windows_x86_64_gnullvm"
5526
+
version = "0.52.6"
5527
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5528
+
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
5529
+
5530
+
[[package]]
5531
+
name = "windows_x86_64_gnullvm"
5532
+
version = "0.53.1"
5533
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5534
+
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
5535
+
5536
+
[[package]]
5537
+
name = "windows_x86_64_msvc"
5538
+
version = "0.42.2"
5539
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5540
+
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
5541
+
5542
+
[[package]]
5543
+
name = "windows_x86_64_msvc"
5544
+
version = "0.48.5"
5545
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5546
+
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
5547
+
5548
+
[[package]]
5549
+
name = "windows_x86_64_msvc"
5550
+
version = "0.52.6"
5551
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5552
+
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
5553
+
5554
+
[[package]]
5555
+
name = "windows_x86_64_msvc"
5556
+
version = "0.53.1"
5557
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5558
+
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
5559
+
5560
+
[[package]]
5561
+
name = "winnow"
5562
+
version = "0.7.13"
5563
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5564
+
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
5565
+
dependencies = [
5566
+
"memchr",
5567
+
]
5568
+
5569
+
[[package]]
5570
+
name = "winreg"
5571
+
version = "0.50.0"
5572
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5573
+
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
5574
+
dependencies = [
5575
+
"cfg-if",
5576
+
"windows-sys 0.48.0",
5577
+
]
5578
+
5579
+
[[package]]
5580
+
name = "wit-bindgen"
5581
+
version = "0.46.0"
5582
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5583
+
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
5584
+
5585
+
[[package]]
5586
+
name = "writeable"
5587
+
version = "0.6.1"
5588
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5589
+
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
5590
+
5591
+
[[package]]
5592
+
name = "xml5ever"
5593
+
version = "0.18.1"
5594
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5595
+
checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69"
5596
+
dependencies = [
5597
+
"log",
5598
+
"mac",
5599
+
"markup5ever",
5600
+
]
5601
+
5602
+
[[package]]
5603
+
name = "yansi"
5604
+
version = "1.0.1"
5605
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5606
+
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
5607
+
5608
+
[[package]]
5609
+
name = "yoke"
5610
+
version = "0.8.0"
5611
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5612
+
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
5613
+
dependencies = [
5614
+
"serde",
5615
+
"stable_deref_trait",
5616
+
"yoke-derive",
5617
+
"zerofrom",
5618
+
]
5619
+
5620
+
[[package]]
5621
+
name = "yoke-derive"
5622
+
version = "0.8.0"
5623
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5624
+
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
5625
+
dependencies = [
5626
+
"proc-macro2",
5627
+
"quote",
5628
+
"syn 2.0.108",
5629
+
"synstructure",
5630
+
]
5631
+
5632
+
[[package]]
5633
+
name = "zerocopy"
5634
+
version = "0.8.27"
5635
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5636
+
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
5637
+
dependencies = [
5638
+
"zerocopy-derive",
5639
+
]
5640
+
5641
+
[[package]]
5642
+
name = "zerocopy-derive"
5643
+
version = "0.8.27"
5644
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5645
+
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
5646
+
dependencies = [
5647
+
"proc-macro2",
5648
+
"quote",
5649
+
"syn 2.0.108",
5650
+
]
5651
+
5652
+
[[package]]
5653
+
name = "zerofrom"
5654
+
version = "0.1.6"
5655
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5656
+
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
5657
+
dependencies = [
5658
+
"zerofrom-derive",
5659
+
]
5660
+
5661
+
[[package]]
5662
+
name = "zerofrom-derive"
5663
+
version = "0.1.6"
5664
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5665
+
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
5666
+
dependencies = [
5667
+
"proc-macro2",
5668
+
"quote",
5669
+
"syn 2.0.108",
5670
+
"synstructure",
5671
+
]
5672
+
5673
+
[[package]]
5674
+
name = "zeroize"
5675
+
version = "1.8.2"
5676
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5677
+
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
5678
+
dependencies = [
5679
+
"serde",
5680
+
]
5681
+
5682
+
[[package]]
5683
+
name = "zerotrie"
5684
+
version = "0.2.2"
5685
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5686
+
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
5687
+
dependencies = [
5688
+
"displaydoc",
5689
+
"yoke",
5690
+
"zerofrom",
5691
+
]
5692
+
5693
+
[[package]]
5694
+
name = "zerovec"
5695
+
version = "0.11.4"
5696
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5697
+
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
5698
+
dependencies = [
5699
+
"yoke",
5700
+
"zerofrom",
5701
+
"zerovec-derive",
5702
+
]
5703
+
5704
+
[[package]]
5705
+
name = "zerovec-derive"
5706
+
version = "0.11.1"
5707
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5708
+
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
5709
+
dependencies = [
5710
+
"proc-macro2",
5711
+
"quote",
5712
+
"syn 2.0.108",
5713
+
]
5714
+
5715
+
[[package]]
5716
+
name = "zune-core"
5717
+
version = "0.4.12"
5718
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5719
+
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
5720
+
5721
+
[[package]]
5722
+
name = "zune-inflate"
5723
+
version = "0.2.54"
5724
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5725
+
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
5726
+
dependencies = [
5727
+
"simd-adler32",
5728
+
]
5729
+
5730
+
[[package]]
5731
+
name = "zune-jpeg"
5732
+
version = "0.4.21"
5733
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5734
+
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
5735
+
dependencies = [
5736
+
"zune-core",
5737
+
]
+69
Cargo.toml
+69
Cargo.toml
···
1
+
[package]
2
+
name = "skywatch-phash-rs"
3
+
version = "0.1.0"
4
+
edition = "2024"
5
+
authors = ["Giulia <skywatch@skywatch.blue"]
6
+
description = "Perceptual hash-based image moderation for Bluesky (Rust rewrite)"
7
+
license = "MIT"
8
+
9
+
[dependencies]
10
+
# Async runtime
11
+
tokio = { version = "1", features = ["full"] }
12
+
futures-util = "0.3"
13
+
14
+
# ATProto client (Jacquard) - using local path
15
+
jacquard = { path = "../jacquard/crates/jacquard" }
16
+
jacquard-api = { path = "../jacquard/crates/jacquard-api" }
17
+
jacquard-common = { path = "../jacquard/crates/jacquard-common", features = ["websocket"] }
18
+
jacquard-identity = { path = "../jacquard/crates/jacquard-identity" }
19
+
jacquard-oauth = { path = "../jacquard/crates/jacquard-oauth" }
20
+
21
+
# Serialization
22
+
serde = { version = "1.0", features = ["derive"] }
23
+
serde_json = "1.0"
24
+
25
+
# HTTP client
26
+
reqwest = { version = "0.12", features = ["json"] }
27
+
28
+
# Redis
29
+
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
30
+
31
+
# Image processing
32
+
image = "0.25"
33
+
image_hasher = "3.0"
34
+
35
+
# Error handling
36
+
miette = { version = "7.6", features = ["fancy"] }
37
+
thiserror = "2.0"
38
+
39
+
# Logging
40
+
tracing = "0.1"
41
+
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
42
+
43
+
# Config
44
+
dotenvy = "0.15"
45
+
46
+
# Utilities
47
+
chrono = "0.4"
48
+
url = "2.5"
49
+
futures = "0.3"
50
+
51
+
# Rate limiting
52
+
governor = "0.8"
53
+
54
+
[dev-dependencies]
55
+
mockito = "1"
56
+
tokio-test = "0.4"
57
+
58
+
[[bin]]
59
+
name = "skywatch-phash"
60
+
path = "src/main.rs"
61
+
62
+
[profile.release]
63
+
opt-level = 3
64
+
lto = true
65
+
codegen-units = 1
66
+
strip = true
67
+
68
+
[profile.dev]
69
+
opt-level = 0
+29
Dockerfile
+29
Dockerfile
···
1
+
FROM rust:1.83 as builder
2
+
3
+
WORKDIR /usr/src/app
4
+
5
+
# Copy manifests
6
+
COPY Cargo.toml Cargo.lock ./
7
+
8
+
# Copy source
9
+
COPY src ./src
10
+
11
+
# Build release binary
12
+
RUN cargo build --release
13
+
14
+
# Runtime stage
15
+
FROM debian:bookworm-slim
16
+
17
+
RUN apt-get update && apt-get install -y \
18
+
ca-certificates \
19
+
&& rm -rf /var/lib/apt/lists/*
20
+
21
+
WORKDIR /app
22
+
23
+
# Copy binary from builder
24
+
COPY --from=builder /usr/src/app/target/release/skywatch-phash-rs .
25
+
26
+
# Copy rules directory
27
+
COPY rules ./rules
28
+
29
+
CMD ["./skywatch-phash-rs"]
+229
README.md
+229
README.md
···
1
+
# skywatch-phash-rs
2
+
3
+
Rust implementation of Bluesky image moderation service using perceptual hashing (aHash/average hash algorithm).
4
+
5
+
Monitors Bluesky's Jetstream for posts with images, computes perceptual hashes, matches against known bad images, and takes automated moderation actions (label/report posts and accounts).
6
+
7
+
## Features
8
+
9
+
- Real-time Jetstream subscription for post monitoring
10
+
- Perceptual hash (aHash) computation for images
11
+
- Configurable hamming distance thresholds per rule
12
+
- Redis-backed job queue and phash caching
13
+
- Concurrent worker pool for parallel processing
14
+
- Automatic retry with dead letter queue
15
+
- Metrics tracking and logging
16
+
- Graceful shutdown handling
17
+
18
+
## Prerequisites
19
+
20
+
- **For Docker deployment:**
21
+
- Docker and Docker Compose
22
+
- **For local development:**
23
+
- Nix with flakes enabled (recommended), OR
24
+
- Rust 1.83+
25
+
- **Required for all:**
26
+
- Bluesky labeler account with app password
27
+
28
+
## Quick Start
29
+
30
+
1. **Clone and setup:**
31
+
```bash
32
+
git clone <repo-url>
33
+
cd skywatch-phash-rs
34
+
```
35
+
36
+
2. **Configure environment:**
37
+
```bash
38
+
cp .env.example .env
39
+
# Edit .env and fill in your automod account credentials:
40
+
# - AUTOMOD_HANDLE
41
+
# - AUTOMOD_PASSWORD
42
+
```
43
+
44
+
3. **Start the service:**
45
+
```bash
46
+
docker compose up --build
47
+
```
48
+
49
+
4. **Monitor logs:**
50
+
```bash
51
+
docker compose logs -f app
52
+
```
53
+
54
+
5. **Stop the service:**
55
+
```bash
56
+
docker compose down
57
+
```
58
+
59
+
## Phash CLI Tool
60
+
61
+
Compute perceptual hash for a single image:
62
+
63
+
```bash
64
+
# Using cargo
65
+
cargo run --bin phash-cli path/to/image.jpg
66
+
67
+
# Or build and run
68
+
cargo build --release --bin phash-cli
69
+
./target/release/phash-cli image.png
70
+
```
71
+
72
+
Output is a 16-character hex string (64-bit hash):
73
+
```
74
+
e0e0e0e0e0fcfefe
75
+
```
76
+
77
+
Use this to generate hashes for your blob check rules.
78
+
79
+
## Configuration
80
+
81
+
All configuration is via environment variables (see `.env.example`):
82
+
83
+
### Required Variables
84
+
85
+
- `AUTOMOD_HANDLE` - Your automod account handle (e.g., automod.bsky.social)
86
+
- `AUTOMOD_PASSWORD` - App password for automod account
87
+
- `LABELER_DID` - DID of your main labeler account (e.g., skywatch.blue)
88
+
- `OZONE_URL` - Ozone moderation service URL
89
+
- `OZONE_PDS` - Ozone PDS endpoint (for authentication)
90
+
91
+
### Optional Variables
92
+
93
+
- `PROCESSING_CONCURRENCY` (default: 4) - Max parallel job processing
94
+
- `PHASH_HAMMING_THRESHOLD` (default: 5) - Global hamming distance threshold
95
+
- `CACHE_ENABLED` (default: true) - Enable Redis phash caching
96
+
- `CACHE_TTL_SECONDS` (default: 86400) - Cache TTL (24 hours)
97
+
- `RETRY_ATTEMPTS` (default: 3) - Max retry attempts for failed jobs
98
+
- `JETSTREAM_URL` - Jetstream websocket URL
99
+
- `REDIS_URL` - Redis connection string
100
+
101
+
## Blob Check Rules
102
+
103
+
Rules are defined in `rules/blobs.json`:
104
+
105
+
```json
106
+
[
107
+
{
108
+
"phashes": ["e0e0e0e0e0fcfefe", "9b9e00008f8fffff"],
109
+
"label": "spam",
110
+
"comment": "Known spam image detected",
111
+
"reportAcct": false,
112
+
"labelAcct": true,
113
+
"reportPost": true,
114
+
"toLabel": true,
115
+
"hammingThreshold": 3,
116
+
"description": "Optional description",
117
+
"ignoreDID": ["did:plc:exempted-user"]
118
+
}
119
+
]
120
+
```
121
+
122
+
### Rule Fields
123
+
124
+
- `phashes` - Array of 16-char hex hashes to match against
125
+
- `label` - Label to apply (e.g., "spam", "csam", "troll")
126
+
- `comment` - Comment for reports
127
+
- `reportAcct` - Report the account
128
+
- `labelAcct` - Label the account
129
+
- `reportPost` - Report the post
130
+
- `toLabel` - Label the post
131
+
- `hammingThreshold` - Max hamming distance for match (overrides global)
132
+
- `description` - Optional description (not used by system)
133
+
- `ignoreDID` - Optional array of DIDs to skip
134
+
135
+
## Architecture
136
+
137
+
```
138
+
Jetstream WebSocket
139
+
↓
140
+
Job Channel (mpsc)
141
+
↓
142
+
Redis Queue (FIFO)
143
+
↓
144
+
Worker Pool (semaphore-controlled concurrency)
145
+
↓
146
+
┌─────────────────┐
147
+
│ For each job: │
148
+
│ 1. Check cache │
149
+
│ 2. Download blob│
150
+
│ 3. Compute phash│
151
+
│ 4. Match rules │
152
+
│ 5. Take actions │
153
+
└─────────────────┘
154
+
↓
155
+
Metrics Tracking
156
+
```
157
+
158
+
### Components
159
+
160
+
- **Jetstream Client** - Subscribes to Bluesky firehose, filters posts with images
161
+
- **Job Queue** - Redis-backed FIFO queue with retry logic
162
+
- **Worker Pool** - Configurable concurrency with semaphore control
163
+
- **Phash Cache** - Redis-backed cache for computed hashes (reduces redundant work)
164
+
- **Agent Session** - Authenticated session with automatic token refresh
165
+
- **Metrics** - Lock-free atomic counters for monitoring
166
+
167
+
## Development
168
+
169
+
### Using Nix (Recommended)
170
+
171
+
If you have Nix with flakes enabled:
172
+
173
+
```bash
174
+
# Enter dev shell with all dependencies
175
+
nix develop
176
+
177
+
# Or use direnv for automatic environment loading
178
+
direnv allow
179
+
180
+
# Build the project
181
+
nix build
182
+
183
+
# Run the binary
184
+
nix run
185
+
```
186
+
187
+
The Nix flake provides:
188
+
- Rust toolchain (stable latest)
189
+
- Native dependencies (OpenSSL, pkg-config)
190
+
- Development tools (cargo-watch, redis)
191
+
- Reproducible builds across Linux and macOS
192
+
193
+
### Without Nix
194
+
195
+
Run locally without Docker:
196
+
197
+
```bash
198
+
# Start Redis
199
+
docker run -d -p 6379:6379 redis
200
+
201
+
# Create .env
202
+
cp .env.example .env
203
+
# Edit .env with your credentials
204
+
205
+
# Run service
206
+
cargo run
207
+
208
+
# Run tests
209
+
cargo test
210
+
211
+
# Run specific binary
212
+
cargo run --bin phash-cli image.jpg
213
+
```
214
+
215
+
## Metrics
216
+
217
+
Logged every 60 seconds:
218
+
219
+
- **Jobs**: received, processed, failed, retried
220
+
- **Blobs**: processed, downloaded
221
+
- **Matches**: found
222
+
- **Cache**: hits, misses, hit rate
223
+
- **Moderation**: posts/accounts labeled and reported
224
+
225
+
Final metrics are logged on graceful shutdown (Ctrl+C).
226
+
227
+
## License
228
+
229
+
See LICENSE file.
+15
docker-compose.yml
+15
docker-compose.yml
+42
rules/blobs.json
+42
rules/blobs.json
···
1
+
[
2
+
{
3
+
"phashes": [
4
+
"07870707073f7f7f",
5
+
"d9794408f1f3fffb",
6
+
"0f093139797b7967",
7
+
"fdedc3030100c0fd",
8
+
"0f7f707dcc0c0600"
9
+
],
10
+
"label": "troll",
11
+
"comment": "Image is used in harrassment campaign targeting Will Stancil",
12
+
"reportAcct": false,
13
+
"labelAcct": true,
14
+
"reportPost": false,
15
+
"toLabel": true,
16
+
"hammingThreshold": 3,
17
+
"description": "Sample harassment image variants (placeholder hashes)",
18
+
"ignoreDID": ["did:plc:7umvpuxe2vbrc3zrzuquzniu"]
19
+
},
20
+
{
21
+
"phashes": ["00fffd7cd8da5000"],
22
+
"label": "maga-trump",
23
+
"comment": "Pro-trump imagery",
24
+
"reportAcct": true,
25
+
"labelAcct": false,
26
+
"reportPost": false,
27
+
"toLabel": true,
28
+
"hammingThreshold": 3,
29
+
"description": "Sample harassment image variants (placeholder hashes)"
30
+
},
31
+
{
32
+
"phashes": ["3e7e6e561202627c"],
33
+
"label": "sensual-alf",
34
+
"comment": "Posting Alf",
35
+
"reportAcct": false,
36
+
"labelAcct": false,
37
+
"reportPost": false,
38
+
"toLabel": true,
39
+
"hammingThreshold": 3,
40
+
"description": "Sample harassment image variants (placeholder hashes)"
41
+
}
42
+
]
+4
rust-toolchain.toml
+4
rust-toolchain.toml
+68
src/agent/README.md
+68
src/agent/README.md
···
1
+
# Agent Module
2
+
3
+
## Purpose
4
+
Manages authenticated sessions with the Bluesky PDS (Personal Data Server) for making moderation API calls.
5
+
6
+
## Key Components
7
+
8
+
### `session.rs`
9
+
- **`AgentSession`** - Main session management struct
10
+
- Wraps reqwest Client with automatic token management
11
+
- Stores session tokens in Arc<RwLock<>> for thread-safe access
12
+
- Provides automatic token refresh on expiry
13
+
14
+
### Key Methods
15
+
16
+
- **`new(config, client)`** - Create new agent session
17
+
- **`login()`** - Authenticate with PDS using handle + password from config
18
+
- Calls `com.atproto.server.createSession`
19
+
- Stores `access_jwt` and `refresh_jwt`
20
+
- **`refresh()`** - Refresh expired access token using refresh_jwt
21
+
- Calls `com.atproto.server.refreshSession`
22
+
- **`get_token()`** - Get valid access token (refreshes if needed)
23
+
- Used by all moderation functions before making API calls
24
+
- **`did()`** - Get the labeler's DID from current session
25
+
- **`ensure_authenticated()`** - Login if not already authenticated
26
+
27
+
### Session Flow
28
+
29
+
```
30
+
1. AgentSession::new() -> Creates empty session
31
+
2. login() -> Authenticates and stores tokens
32
+
3. get_token() -> Returns access_jwt (auto-refreshes if expired)
33
+
4. Moderation functions use access_jwt in Authorization header
34
+
```
35
+
36
+
### Token Storage
37
+
38
+
Session tokens are stored in:
39
+
```rust
40
+
Arc<RwLock<Option<CreateSessionResponse>>>
41
+
```
42
+
43
+
This allows:
44
+
- Thread-safe concurrent access from multiple workers
45
+
- Shared refresh state across all API calls
46
+
- Automatic token updates visible to all workers
47
+
48
+
## Usage Pattern
49
+
50
+
```rust
51
+
let agent = AgentSession::new(config, client);
52
+
agent.login().await?;
53
+
54
+
// Later in worker:
55
+
let token = agent.get_token().await?;
56
+
// Use token in API calls
57
+
```
58
+
59
+
## Dependencies
60
+
61
+
- `reqwest::Client` - HTTP client
62
+
- `Config` - Labeler credentials (handle, password, DID)
63
+
- `miette` - Error handling
64
+
65
+
## Related Modules
66
+
67
+
- Used by: `queue/worker.rs` - Workers get tokens for moderation actions
68
+
- Uses: `config` - For labeler credentials
+49
src/agent/session.rs
+49
src/agent/session.rs
···
1
+
use miette::Result;
2
+
use std::sync::Arc;
3
+
use jacquard::client::{Agent, MemoryCredentialSession};
4
+
use jacquard::CowStr;
5
+
6
+
use crate::config::Config;
7
+
8
+
/// Agent session wrapper using Jacquard
9
+
/// All internal types use 'static lifetime to enable Send + Sync across threads
10
+
#[derive(Clone)]
11
+
pub struct AgentSession {
12
+
agent: Arc<Agent<MemoryCredentialSession>>,
13
+
did: Arc<str>,
14
+
}
15
+
16
+
impl AgentSession {
17
+
/// Create a new agent session by authenticating with handle and password
18
+
pub async fn new(config: &Config) -> Result<Self> {
19
+
tracing::info!("Logging in as {}", config.automod.handle);
20
+
21
+
let handle = CowStr::from(config.automod.handle.clone());
22
+
let password = CowStr::from(config.automod.password.clone());
23
+
let pds = Some(CowStr::from(config.ozone.pds.clone()));
24
+
25
+
let (session, auth) = MemoryCredentialSession::authenticated(handle, password, pds).await?;
26
+
27
+
tracing::info!("Successfully logged in as {} ({})", auth.handle, auth.did);
28
+
29
+
let did: Arc<str> = Arc::from(auth.did.to_string());
30
+
let agent = Arc::new(Agent::from(session));
31
+
32
+
Ok(Self { agent, did })
33
+
}
34
+
35
+
/// Get the authenticated agent
36
+
pub fn agent(&self) -> &Arc<Agent<MemoryCredentialSession>> {
37
+
&self.agent
38
+
}
39
+
40
+
/// Get the authenticated DID
41
+
pub fn did(&self) -> &str {
42
+
&*self.did
43
+
}
44
+
}
45
+
46
+
#[cfg(test)]
47
+
mod tests {
48
+
// Note: Integration tests require valid credentials
49
+
}
+33
src/bin/phash-cli.rs
+33
src/bin/phash-cli.rs
···
1
+
use miette::{IntoDiagnostic, Result};
2
+
use std::env;
3
+
use std::fs;
4
+
use std::path::Path;
5
+
6
+
fn main() -> Result<()> {
7
+
let args: Vec<String> = env::args().collect();
8
+
9
+
if args.len() != 2 {
10
+
eprintln!("Usage: {} <image-path>", args[0]);
11
+
eprintln!("\nComputes perceptual hash (aHash/average hash) for an image.");
12
+
eprintln!("Outputs 16-character hex string (64-bit hash).");
13
+
std::process::exit(1);
14
+
}
15
+
16
+
let image_path = Path::new(&args[1]);
17
+
18
+
if !image_path.exists() {
19
+
eprintln!("Error: File not found: {}", image_path.display());
20
+
std::process::exit(1);
21
+
}
22
+
23
+
// Read image bytes
24
+
let image_bytes = fs::read(image_path).into_diagnostic()?;
25
+
26
+
// Compute phash
27
+
let phash = skywatch_phash_rs::processor::phash::compute_phash(&image_bytes)?;
28
+
29
+
// Output just the hash
30
+
println!("{}", phash);
31
+
32
+
Ok(())
33
+
}
+106
src/bin/test-image.rs
+106
src/bin/test-image.rs
···
1
+
use miette::{IntoDiagnostic, Result};
2
+
use std::env;
3
+
use std::fs;
4
+
use std::path::Path;
5
+
6
+
fn main() -> Result<()> {
7
+
let args: Vec<String> = env::args().collect();
8
+
9
+
if args.len() != 2 {
10
+
eprintln!("Usage: {} <image-path>", args[0]);
11
+
eprintln!("\nTests an image against blob check rules.");
12
+
eprintln!("Computes phash and checks for matches in rules/blobs.json");
13
+
std::process::exit(1);
14
+
}
15
+
16
+
let image_path = Path::new(&args[1]);
17
+
18
+
if !image_path.exists() {
19
+
eprintln!("Error: File not found: {}", image_path.display());
20
+
std::process::exit(1);
21
+
}
22
+
23
+
tokio::runtime::Runtime::new()
24
+
.into_diagnostic()?
25
+
.block_on(async {
26
+
test_image(image_path).await
27
+
})
28
+
}
29
+
30
+
async fn test_image(image_path: &Path) -> Result<()> {
31
+
// Load blob checks
32
+
let rules_path = Path::new("rules/blobs.json");
33
+
if !rules_path.exists() {
34
+
eprintln!("Error: rules/blobs.json not found");
35
+
std::process::exit(1);
36
+
}
37
+
38
+
println!("Loading rules from rules/blobs.json...");
39
+
let blob_checks = skywatch_phash_rs::processor::matcher::load_blob_checks(rules_path).await?;
40
+
println!("Loaded {} blob check rules\n", blob_checks.len());
41
+
42
+
// Read and compute phash
43
+
println!("Computing phash for: {}", image_path.display());
44
+
let image_bytes = fs::read(image_path).into_diagnostic()?;
45
+
let phash = skywatch_phash_rs::processor::phash::compute_phash(&image_bytes)?;
46
+
println!("Computed phash: {}\n", phash);
47
+
48
+
// Check against rules
49
+
println!("Checking against rules...\n");
50
+
let test_did = "did:plc:test123456789";
51
+
52
+
let mut found_match = false;
53
+
for check in &blob_checks {
54
+
// Check if test DID is ignored
55
+
if let Some(ignore_list) = &check.ignore_did {
56
+
if ignore_list.contains(&test_did.to_string()) {
57
+
println!("⏭️ Skipped rule '{}' (test DID is ignored)", check.label);
58
+
continue;
59
+
}
60
+
}
61
+
62
+
let threshold = check.hamming_threshold.unwrap_or(5);
63
+
64
+
// Check each phash in the rule
65
+
for check_phash in &check.phashes {
66
+
match skywatch_phash_rs::processor::phash::hamming_distance(&phash, check_phash) {
67
+
Ok(distance) => {
68
+
if distance <= threshold {
69
+
found_match = true;
70
+
println!("✅ MATCH FOUND!");
71
+
println!(" Rule: {}", check.label);
72
+
println!(" Description: {}", check.description.as_ref().unwrap_or(&"N/A".to_string()));
73
+
println!(" Matched phash: {}", check_phash);
74
+
println!(" Hamming distance: {} (threshold: {})", distance, threshold);
75
+
println!(" Actions:");
76
+
if check.to_label {
77
+
println!(" - Label post with '{}'", check.label);
78
+
}
79
+
if check.report_post {
80
+
println!(" - Report post");
81
+
}
82
+
if check.label_acct {
83
+
println!(" - Label account");
84
+
}
85
+
if check.report_acct {
86
+
println!(" - Report account");
87
+
}
88
+
println!();
89
+
} else {
90
+
println!("❌ No match for rule '{}' (distance: {}, threshold: {})",
91
+
check.label, distance, threshold);
92
+
}
93
+
}
94
+
Err(e) => {
95
+
eprintln!("⚠️ Error computing distance for rule '{}': {}", check.label, e);
96
+
}
97
+
}
98
+
}
99
+
}
100
+
101
+
if !found_match {
102
+
println!("\n🔍 No matches found. This image is not flagged by any rules.");
103
+
}
104
+
105
+
Ok(())
106
+
}
+83
src/cache/README.md
+83
src/cache/README.md
···
1
+
# Cache Module
2
+
3
+
## Purpose
4
+
Redis-backed cache for perceptual hashes to avoid recomputing hashes for images we've already processed.
5
+
6
+
## Key Components
7
+
8
+
### `mod.rs`
9
+
- **`PhashCache`** - Redis connection wrapper for phash storage
10
+
- Uses CID (Content Identifier) as key
11
+
- Stores computed phash as value
12
+
- TTL controlled by `config.cache.ttl`
13
+
14
+
### Key Methods
15
+
16
+
- **`new(config)`** - Create cache with Redis connection
17
+
- Connects to `config.redis.url`
18
+
- Uses multiplexed async connection for concurrent access
19
+
- **`get(cid)`** - Retrieve cached phash for a CID
20
+
- Returns `Ok(Some(phash))` if cached
21
+
- Returns `Ok(None)` if not in cache
22
+
- **`set(cid, phash)`** - Store phash in cache
23
+
- Sets TTL from config (default 24 hours)
24
+
- Uses `SET` with `EX` for expiration
25
+
26
+
### Cache Flow
27
+
28
+
```
29
+
1. Worker gets job with blob CID
30
+
2. cache.get(cid) -> Check if phash exists
31
+
3. If cache hit:
32
+
- Use cached phash
33
+
- Skip download & computation
34
+
4. If cache miss:
35
+
- Download blob
36
+
- Compute phash
37
+
- cache.set(cid, phash) -> Store for next time
38
+
```
39
+
40
+
### Key Design Decisions
41
+
42
+
**Why cache by CID?**
43
+
- CIDs are content-addressed (same content = same CID)
44
+
- Same image will always have same CID across all posts
45
+
- Deduplicates work when same image is posted multiple times
46
+
47
+
**Why use Redis?**
48
+
- Shared cache across all workers
49
+
- Persists across service restarts
50
+
- TTL handles cache invalidation automatically
51
+
52
+
**TTL Strategy**
53
+
- Default 24 hours (86400 seconds)
54
+
- Balances memory usage vs recomputation
55
+
- Can be disabled with `CACHE_ENABLED=false`
56
+
57
+
## Performance Impact
58
+
59
+
With cache enabled:
60
+
- **Cache hit** - ~1ms (Redis lookup)
61
+
- **Cache miss** - ~200-500ms (download + compute + store)
62
+
63
+
For frequently reposted images (spam), cache hit rate can be >90%, massively reducing load.
64
+
65
+
## Configuration
66
+
67
+
```env
68
+
CACHE_ENABLED=true # Enable/disable cache
69
+
CACHE_TTL_SECONDS=86400 # Cache TTL (24 hours)
70
+
REDIS_URL=redis://localhost:6379
71
+
```
72
+
73
+
## Dependencies
74
+
75
+
- `redis` - Async Redis client with tokio support
76
+
- `Config` - Cache settings
77
+
- `miette` - Error handling
78
+
79
+
## Related Modules
80
+
81
+
- Used by: `queue/worker.rs` - Workers check cache before processing blobs
82
+
- Uses: `config` - For Redis URL and TTL settings
83
+
- Works with: `metrics` - Tracks cache hit/miss rates
+123
src/cache/mod.rs
+123
src/cache/mod.rs
···
1
+
use miette::{IntoDiagnostic, Result};
2
+
use redis::AsyncCommands;
3
+
use tracing::{debug, info};
4
+
5
+
use crate::config::Config;
6
+
7
+
/// Redis key prefix for phash cache
8
+
const PHASH_CACHE_PREFIX: &str = "phash";
9
+
10
+
/// Phash cache for storing computed image hashes
11
+
#[derive(Clone)]
12
+
pub struct PhashCache {
13
+
redis: redis::aio::MultiplexedConnection,
14
+
ttl: u64,
15
+
enabled: bool,
16
+
}
17
+
18
+
impl PhashCache {
19
+
/// Create a new phash cache
20
+
pub async fn new(config: &Config) -> Result<Self> {
21
+
info!("Connecting to Redis: {}", config.redis.url);
22
+
23
+
let client = redis::Client::open(config.redis.url.as_str()).into_diagnostic()?;
24
+
let redis = client
25
+
.get_multiplexed_async_connection()
26
+
.await
27
+
.into_diagnostic()?;
28
+
29
+
info!("Connected to Redis, cache enabled: {}", config.cache.enabled);
30
+
31
+
Ok(Self {
32
+
redis,
33
+
ttl: config.cache.ttl,
34
+
enabled: config.cache.enabled,
35
+
})
36
+
}
37
+
38
+
/// Get cached phash for a blob CID
39
+
pub async fn get(&mut self, cid: &str) -> Result<Option<String>> {
40
+
if !self.enabled {
41
+
return Ok(None);
42
+
}
43
+
44
+
let key = format!("{}:{}", PHASH_CACHE_PREFIX, cid);
45
+
46
+
let result: Option<String> = self.redis.get(&key).await.into_diagnostic()?;
47
+
48
+
if result.is_some() {
49
+
debug!("Cache hit for CID: {}", cid);
50
+
} else {
51
+
debug!("Cache miss for CID: {}", cid);
52
+
}
53
+
54
+
Ok(result)
55
+
}
56
+
57
+
/// Set cached phash for a blob CID
58
+
pub async fn set(&mut self, cid: &str, phash: &str) -> Result<()> {
59
+
if !self.enabled {
60
+
return Ok(());
61
+
}
62
+
63
+
let key = format!("{}:{}", PHASH_CACHE_PREFIX, cid);
64
+
65
+
let _: () = self
66
+
.redis
67
+
.set_ex(&key, phash, self.ttl)
68
+
.await
69
+
.into_diagnostic()?;
70
+
71
+
debug!("Cached phash for CID: {} -> {}", cid, phash);
72
+
73
+
Ok(())
74
+
}
75
+
76
+
/// Delete cached phash for a blob CID
77
+
pub async fn delete(&mut self, cid: &str) -> Result<()> {
78
+
if !self.enabled {
79
+
return Ok(());
80
+
}
81
+
82
+
let key = format!("{}:{}", PHASH_CACHE_PREFIX, cid);
83
+
84
+
let _: () = self.redis.del(&key).await.into_diagnostic()?;
85
+
86
+
debug!("Deleted cached phash for CID: {}", cid);
87
+
88
+
Ok(())
89
+
}
90
+
91
+
/// Check if cache is enabled
92
+
pub fn is_enabled(&self) -> bool {
93
+
self.enabled
94
+
}
95
+
96
+
/// Get or compute phash with caching
97
+
pub async fn get_or_compute<F, Fut>(&mut self, cid: &str, compute_fn: F) -> Result<String>
98
+
where
99
+
F: FnOnce() -> Fut,
100
+
Fut: std::future::Future<Output = Result<String>>,
101
+
{
102
+
// Try to get from cache
103
+
if let Some(cached) = self.get(cid).await? {
104
+
return Ok(cached);
105
+
}
106
+
107
+
// Compute if not cached
108
+
let phash = compute_fn().await?;
109
+
110
+
// Store in cache
111
+
self.set(cid, &phash).await?;
112
+
113
+
Ok(phash)
114
+
}
115
+
}
116
+
117
+
#[cfg(test)]
118
+
mod tests {
119
+
use super::*;
120
+
121
+
// Note: These are integration tests that require a running Redis instance
122
+
// Run with: cargo test --test cache -- --ignored
123
+
}
+108
src/config/README.md
+108
src/config/README.md
···
1
+
# Config Module
2
+
3
+
## Purpose
4
+
Centralized configuration management from environment variables with sensible defaults.
5
+
6
+
## Key Components
7
+
8
+
### `mod.rs`
9
+
- **`Config`** - Root configuration struct containing all sub-configs
10
+
- **Sub-config structs**:
11
+
- `JetstreamConfig` - Jetstream connection settings
12
+
- `RedisConfig` - Redis connection
13
+
- `ProcessingConfig` - Worker pool settings
14
+
- `CacheConfig` - Cache behavior
15
+
- `PdsConfig` - PDS endpoint
16
+
- `PlcConfig` - PLC directory endpoint
17
+
- `LabelerConfig` - Labeler credentials
18
+
- `OzoneConfig` - Ozone moderation service
19
+
- `ModerationConfig` - Moderation behavior
20
+
- `PhashConfig` - Phash matching settings
21
+
22
+
### Key Methods
23
+
24
+
- **`Config::from_env()`** - Load all configuration from environment
25
+
- Calls `dotenvy::dotenv()` to load `.env` file
26
+
- Falls back to defaults where provided
27
+
- Returns error for missing required vars
28
+
29
+
### Environment Variables
30
+
31
+
**Required:**
32
+
```env
33
+
LABELER_DID=did:plc:xxxxx # Your labeler DID
34
+
LABELER_HANDLE=labeler.bsky.social # Your labeler handle
35
+
LABELER_PASSWORD=xxxx-xxxx-xxxx-xxxx # App password
36
+
OZONE_URL=https://ozone.example.com
37
+
OZONE_PDS=https://bsky.social
38
+
MOD_DID=did:plc:xxxxx # Moderator DID
39
+
```
40
+
41
+
**Optional with defaults:**
42
+
```env
43
+
JETSTREAM_URL=wss://jetstream.atproto.tools/subscribe
44
+
REDIS_URL=redis://localhost:6379
45
+
PDS_ENDPOINT=https://bsky.social
46
+
PLC_ENDPOINT=https://plc.directory
47
+
48
+
PROCESSING_CONCURRENCY=10 # Max concurrent workers
49
+
RETRY_ATTEMPTS=3 # Max retries per job
50
+
RETRY_DELAY_MS=1000 # Delay between retries
51
+
RATE_LIMIT_MS=100 # Min ms between API calls
52
+
53
+
CACHE_ENABLED=true # Enable phash cache
54
+
CACHE_TTL_SECONDS=86400 # Cache TTL (24h)
55
+
56
+
PHASH_HAMMING_THRESHOLD=3 # Default hamming threshold
57
+
CURSOR_UPDATE_INTERVAL=10000 # Cursor save interval (ms)
58
+
```
59
+
60
+
### Helper Functions
61
+
62
+
- **`get_env(key, default)`** - Get string with optional default
63
+
- **`get_env_u32/u64/usize(key, default)`** - Get numbers with defaults
64
+
- **`get_env_bool(key, default)`** - Parse bool (accepts "true", "1", "yes")
65
+
66
+
### Configuration Flow
67
+
68
+
```
69
+
1. Service starts
70
+
2. Config::from_env() loads .env file
71
+
3. Validates required vars exist
72
+
4. Applies defaults for optional vars
73
+
5. Config passed to all modules
74
+
```
75
+
76
+
### Design Decisions
77
+
78
+
**Why environment variables?**
79
+
- 12-factor app pattern
80
+
- Easy to configure in Docker/k8s
81
+
- No config files to manage
82
+
- Secrets stay out of code
83
+
84
+
**Why dotenvy?**
85
+
- Compatible with .env files
86
+
- Only used for local dev (prod uses real env vars)
87
+
- Simplifies local testing
88
+
89
+
## Usage Pattern
90
+
91
+
```rust
92
+
// At startup:
93
+
let config = Config::from_env()?;
94
+
95
+
// Pass to modules:
96
+
let agent = AgentSession::new(config.clone(), client);
97
+
let cache = PhashCache::new(config.clone()).await?;
98
+
```
99
+
100
+
## Dependencies
101
+
102
+
- `dotenvy` - .env file loading
103
+
- `miette` - Error handling
104
+
105
+
## Related Modules
106
+
107
+
- Used by: All modules - everything needs config
108
+
- Cloned extensively (all fields implement `Clone`)
+207
src/config/mod.rs
+207
src/config/mod.rs
···
1
+
use miette::{Context, IntoDiagnostic, Result};
2
+
use std::env;
3
+
4
+
#[derive(Debug, Clone)]
5
+
pub struct Config {
6
+
pub jetstream: JetstreamConfig,
7
+
pub redis: RedisConfig,
8
+
pub processing: ProcessingConfig,
9
+
pub cache: CacheConfig,
10
+
pub pds: PdsConfig,
11
+
pub plc: PlcConfig,
12
+
pub automod: AutomodConfig,
13
+
pub ozone: OzoneConfig,
14
+
pub moderation: ModerationConfig,
15
+
pub phash: PhashConfig,
16
+
}
17
+
18
+
#[derive(Debug, Clone)]
19
+
pub struct JetstreamConfig {
20
+
pub url: String,
21
+
pub wanted_collections: Vec<String>,
22
+
pub cursor_update_interval: u64,
23
+
}
24
+
25
+
#[derive(Debug, Clone)]
26
+
pub struct RedisConfig {
27
+
pub url: String,
28
+
}
29
+
30
+
#[derive(Debug, Clone)]
31
+
pub struct ProcessingConfig {
32
+
pub concurrency: usize,
33
+
pub retry_attempts: u32,
34
+
pub retry_delay: u64,
35
+
}
36
+
37
+
#[derive(Debug, Clone)]
38
+
pub struct CacheConfig {
39
+
pub enabled: bool,
40
+
pub ttl: u64,
41
+
}
42
+
43
+
#[derive(Debug, Clone)]
44
+
pub struct PdsConfig {
45
+
pub endpoint: String,
46
+
}
47
+
48
+
#[derive(Debug, Clone)]
49
+
pub struct PlcConfig {
50
+
pub endpoint: String,
51
+
}
52
+
53
+
#[derive(Debug, Clone)]
54
+
pub struct AutomodConfig {
55
+
pub handle: String,
56
+
pub password: String,
57
+
}
58
+
59
+
#[derive(Debug, Clone)]
60
+
pub struct OzoneConfig {
61
+
pub url: String,
62
+
pub pds: String,
63
+
}
64
+
65
+
#[derive(Debug, Clone)]
66
+
pub struct ModerationConfig {
67
+
pub labeler_did: String,
68
+
pub rate_limit: u64,
69
+
}
70
+
71
+
#[derive(Debug, Clone)]
72
+
pub struct PhashConfig {
73
+
pub default_hamming_threshold: u32,
74
+
}
75
+
76
+
impl Config {
77
+
/// Load configuration from environment variables
78
+
pub fn from_env() -> Result<Self> {
79
+
// Load .env file if it exists (ignore if missing)
80
+
dotenvy::dotenv().ok();
81
+
82
+
Ok(Config {
83
+
jetstream: JetstreamConfig {
84
+
url: get_env(
85
+
"JETSTREAM_URL",
86
+
Some("wss://jetstream.atproto.tools/subscribe"),
87
+
)?,
88
+
wanted_collections: vec!["app.bsky.feed.post".to_string()],
89
+
cursor_update_interval: get_env_u64("CURSOR_UPDATE_INTERVAL", 10_000),
90
+
},
91
+
redis: RedisConfig {
92
+
url: get_env("REDIS_URL", Some("redis://localhost:6379"))?,
93
+
},
94
+
processing: ProcessingConfig {
95
+
concurrency: get_env_usize("PROCESSING_CONCURRENCY", 10),
96
+
retry_attempts: get_env_u32("RETRY_ATTEMPTS", 3),
97
+
retry_delay: get_env_u64("RETRY_DELAY_MS", 1000),
98
+
},
99
+
cache: CacheConfig {
100
+
enabled: get_env_bool("CACHE_ENABLED", true),
101
+
ttl: get_env_u64("CACHE_TTL_SECONDS", 86400),
102
+
},
103
+
pds: PdsConfig {
104
+
endpoint: get_env("PDS_ENDPOINT", Some("https://bsky.social"))?,
105
+
},
106
+
plc: PlcConfig {
107
+
endpoint: get_env("PLC_ENDPOINT", Some("https://plc.directory"))?,
108
+
},
109
+
automod: AutomodConfig {
110
+
handle: get_env("AUTOMOD_HANDLE", None)
111
+
.context("AUTOMOD_HANDLE is required for authentication")?,
112
+
password: get_env("AUTOMOD_PASSWORD", None)
113
+
.context("AUTOMOD_PASSWORD is required for authentication")?,
114
+
},
115
+
ozone: OzoneConfig {
116
+
url: get_env("OZONE_URL", None).context("OZONE_URL is required")?,
117
+
pds: get_env("OZONE_PDS", None).context("OZONE_PDS is required")?,
118
+
},
119
+
moderation: ModerationConfig {
120
+
labeler_did: get_env("LABELER_DID", None).context("LABELER_DID is required")?,
121
+
rate_limit: get_env_u64("RATE_LIMIT_MS", 100),
122
+
},
123
+
phash: PhashConfig {
124
+
default_hamming_threshold: get_env_u32("PHASH_HAMMING_THRESHOLD", 3),
125
+
},
126
+
})
127
+
}
128
+
}
129
+
130
+
/// Get environment variable with optional default
131
+
fn get_env(key: &str, default: Option<&str>) -> Result<String> {
132
+
env::var(key)
133
+
.into_diagnostic()
134
+
.or_else(|_| {
135
+
default
136
+
.ok_or_else(|| miette::miette!("Missing required environment variable: {}", key))
137
+
.map(String::from)
138
+
})
139
+
}
140
+
141
+
/// Get environment variable as u32 with default
142
+
fn get_env_u32(key: &str, default: u32) -> u32 {
143
+
env::var(key)
144
+
.ok()
145
+
.and_then(|v| v.parse().ok())
146
+
.unwrap_or(default)
147
+
}
148
+
149
+
/// Get environment variable as u64 with default
150
+
fn get_env_u64(key: &str, default: u64) -> u64 {
151
+
env::var(key)
152
+
.ok()
153
+
.and_then(|v| v.parse().ok())
154
+
.unwrap_or(default)
155
+
}
156
+
157
+
/// Get environment variable as usize with default
158
+
fn get_env_usize(key: &str, default: usize) -> usize {
159
+
env::var(key)
160
+
.ok()
161
+
.and_then(|v| v.parse().ok())
162
+
.unwrap_or(default)
163
+
}
164
+
165
+
/// Get environment variable as bool with default
166
+
/// Accepts "true", "1", "yes" (case-insensitive) as true
167
+
fn get_env_bool(key: &str, default: bool) -> bool {
168
+
env::var(key)
169
+
.ok()
170
+
.map(|v| {
171
+
let v = v.to_lowercase();
172
+
v == "true" || v == "1" || v == "yes"
173
+
})
174
+
.unwrap_or(default)
175
+
}
176
+
177
+
#[cfg(test)]
178
+
mod tests {
179
+
use super::*;
180
+
181
+
#[test]
182
+
fn test_get_env_bool() {
183
+
unsafe {
184
+
std::env::set_var("TEST_BOOL_TRUE", "true");
185
+
std::env::set_var("TEST_BOOL_1", "1");
186
+
std::env::set_var("TEST_BOOL_YES", "yes");
187
+
std::env::set_var("TEST_BOOL_FALSE", "false");
188
+
std::env::set_var("TEST_BOOL_0", "0");
189
+
}
190
+
191
+
assert!(get_env_bool("TEST_BOOL_TRUE", false));
192
+
assert!(get_env_bool("TEST_BOOL_1", false));
193
+
assert!(get_env_bool("TEST_BOOL_YES", false));
194
+
assert!(!get_env_bool("TEST_BOOL_FALSE", true));
195
+
assert!(!get_env_bool("TEST_BOOL_0", true));
196
+
assert!(get_env_bool("TEST_BOOL_MISSING", true));
197
+
}
198
+
199
+
#[test]
200
+
fn test_get_env_u32() {
201
+
unsafe {
202
+
std::env::set_var("TEST_U32", "42");
203
+
}
204
+
assert_eq!(get_env_u32("TEST_U32", 0), 42);
205
+
assert_eq!(get_env_u32("TEST_U32_MISSING", 99), 99);
206
+
}
207
+
}
+125
src/jetstream/README.md
+125
src/jetstream/README.md
···
1
+
# Jetstream Module
2
+
3
+
## Purpose
4
+
Connects to Bluesky's Jetstream firehose to receive real-time post events and extract image blobs for processing.
5
+
6
+
## Key Components
7
+
8
+
### `mod.rs`
9
+
- **`start_jetstream_client()`** - Main entry point
10
+
- Establishes WebSocket connection to Jetstream
11
+
- Subscribes to `app.bsky.feed.post` collection
12
+
- Sends jobs to processing queue via mpsc channel
13
+
- Handles cursor persistence for resume capability
14
+
15
+
### `events.rs`
16
+
- **`JetstreamEvent`** - Deserialized Jetstream message
17
+
- Contains `did`, `commit.record`, `commit.cid`
18
+
- Record type: `app.bsky.feed.post`
19
+
- **`extract_blobs()`** - Extract image CIDs from post record
20
+
- Handles direct post images (`record.embed.images`)
21
+
- Handles quoted posts with images (`record.embed.record.embed.images`)
22
+
- Returns `Vec<BlobReference>` with CIDs and mime types
23
+
24
+
### `cursor.rs`
25
+
- **`CursorManager`** - Persists Jetstream cursor to SQLite
26
+
- Enables resume from last position after restart
27
+
- Prevents reprocessing old events
28
+
- Updates every `config.cursor_update_interval` ms
29
+
30
+
## Message Flow
31
+
32
+
```
33
+
1. WebSocket connects to Jetstream
34
+
2. Subscribe to app.bsky.feed.post collection
35
+
3. For each message:
36
+
a. Deserialize JetstreamEvent
37
+
b. Extract blobs from post record
38
+
c. If blobs found:
39
+
- Create ImageJob
40
+
- Send to job_tx channel
41
+
d. Update cursor periodically
42
+
4. On disconnect: Reconnect with last cursor
43
+
```
44
+
45
+
## Event Filtering
46
+
47
+
**What gets processed:**
48
+
- Posts with `app.bsky.feed.post` type
49
+
- Contains images in `embed.images` or `embed.record.embed.images`
50
+
- Commit type is "create" (new posts only)
51
+
52
+
**What gets skipped:**
53
+
- Posts without images
54
+
- Deletes/updates (only creates)
55
+
- Other record types (likes, reposts, etc.)
56
+
57
+
## Cursor Persistence
58
+
59
+
The cursor marks our position in the firehose:
60
+
```
61
+
Initial connect -> No cursor (start from now)
62
+
Save cursor -> Every 10 seconds (configurable)
63
+
Restart -> Resume from saved cursor
64
+
```
65
+
66
+
**Storage:** SQLite database (`firehose_cursor.db`)
67
+
**Why:** Prevents missing events during downtime
68
+
69
+
## Blob Extraction Logic
70
+
71
+
### Direct Images
72
+
```json
73
+
{
74
+
"embed": {
75
+
"$type": "app.bsky.embed.images",
76
+
"images": [
77
+
{
78
+
"image": { "ref": { "$link": "bafkrei..." } },
79
+
"mimeType": "image/jpeg"
80
+
}
81
+
]
82
+
}
83
+
}
84
+
```
85
+
86
+
### Quoted Posts with Images
87
+
```json
88
+
{
89
+
"embed": {
90
+
"$type": "app.bsky.embed.record#withMedia",
91
+
"record": {...},
92
+
"media": {
93
+
"images": [...]
94
+
}
95
+
}
96
+
}
97
+
```
98
+
99
+
The `extract_blobs()` function handles both patterns.
100
+
101
+
## Error Handling
102
+
103
+
- **WebSocket disconnect** - Auto-reconnect with cursor
104
+
- **Parse errors** - Log and skip message
105
+
- **Channel closed** - Shutdown gracefully
106
+
107
+
## Configuration
108
+
109
+
```env
110
+
JETSTREAM_URL=wss://jetstream.atproto.tools/subscribe
111
+
CURSOR_UPDATE_INTERVAL=10000 # Save cursor every 10s
112
+
```
113
+
114
+
## Dependencies
115
+
116
+
- `tokio-tungstenite` - WebSocket client
117
+
- `serde_json` - JSON parsing
118
+
- `tokio::sync::mpsc` - Job channel
119
+
- `rusqlite` - Cursor persistence
120
+
121
+
## Related Modules
122
+
123
+
- Sends to: `queue` - Jobs sent via mpsc channel
124
+
- Uses: `types::ImageJob` - Job format
125
+
- Uses: `config` - Jetstream URL and settings
+41
src/jetstream/cursor.rs
+41
src/jetstream/cursor.rs
···
1
+
use miette::{IntoDiagnostic, Result};
2
+
use std::fs;
3
+
use std::path::Path;
4
+
use tracing::{info, warn};
5
+
6
+
const CURSOR_FILE: &str = "firehose_cursor.db";
7
+
8
+
/// Read cursor from disk
9
+
pub fn read_cursor() -> Option<i64> {
10
+
let path = Path::new(CURSOR_FILE);
11
+
12
+
if !path.exists() {
13
+
info!("No cursor file found, starting from current");
14
+
return None;
15
+
}
16
+
17
+
match fs::read_to_string(path) {
18
+
Ok(content) => {
19
+
match content.trim().parse::<i64>() {
20
+
Ok(cursor) => {
21
+
info!("Loaded cursor from disk: {}", cursor);
22
+
Some(cursor)
23
+
}
24
+
Err(e) => {
25
+
warn!("Failed to parse cursor file: {}", e);
26
+
None
27
+
}
28
+
}
29
+
}
30
+
Err(e) => {
31
+
warn!("Failed to read cursor file: {}", e);
32
+
None
33
+
}
34
+
}
35
+
}
36
+
37
+
/// Write cursor to disk
38
+
pub fn write_cursor(cursor: i64) -> Result<()> {
39
+
fs::write(CURSOR_FILE, cursor.to_string()).into_diagnostic()?;
40
+
Ok(())
41
+
}
+155
src/jetstream/events.rs
+155
src/jetstream/events.rs
···
1
+
use miette::Result;
2
+
use serde_json::Value;
3
+
4
+
use crate::types::BlobReference;
5
+
6
+
/// Extract blob references from a post record
7
+
///
8
+
/// Handles two cases:
9
+
/// 1. Direct images: record.embed.images[].image.ref.$link
10
+
/// 2. Quote posts with media: record.embed.media.images[].image.ref.$link
11
+
pub fn extract_blobs_from_record(record: &Value) -> Result<Vec<BlobReference>> {
12
+
let mut blobs = Vec::new();
13
+
14
+
let Some(embed) = record.get("embed") else {
15
+
return Ok(blobs);
16
+
};
17
+
18
+
// Case 1: Direct images (embed.images)
19
+
if let Some(images) = embed.get("images").and_then(|v| v.as_array()) {
20
+
for img in images {
21
+
if let Some(blob_ref) = extract_blob_from_image(img) {
22
+
blobs.push(blob_ref);
23
+
}
24
+
}
25
+
}
26
+
27
+
// Case 2: Quote posts with media (embed.media.images)
28
+
if let Some(media) = embed.get("media") {
29
+
if let Some(images) = media.get("images").and_then(|v| v.as_array()) {
30
+
for img in images {
31
+
if let Some(blob_ref) = extract_blob_from_image(img) {
32
+
blobs.push(blob_ref);
33
+
}
34
+
}
35
+
}
36
+
}
37
+
38
+
Ok(blobs)
39
+
}
40
+
41
+
/// Extract a single blob reference from an image object
42
+
fn extract_blob_from_image(img: &Value) -> Option<BlobReference> {
43
+
let image_obj = img.get("image")?;
44
+
let ref_obj = image_obj.get("ref")?;
45
+
let cid = ref_obj.get("$link")?.as_str()?;
46
+
47
+
let mime_type = image_obj
48
+
.get("mimeType")
49
+
.and_then(|v| v.as_str())
50
+
.map(String::from);
51
+
52
+
Some(BlobReference {
53
+
cid: cid.to_string(),
54
+
mime_type,
55
+
})
56
+
}
57
+
58
+
#[cfg(test)]
59
+
mod tests {
60
+
use super::*;
61
+
use serde_json::json;
62
+
63
+
#[test]
64
+
fn test_extract_blobs_direct_images() {
65
+
let record = json!({
66
+
"embed": {
67
+
"$type": "app.bsky.embed.images",
68
+
"images": [
69
+
{
70
+
"alt": "Test image",
71
+
"image": {
72
+
"ref": {
73
+
"$link": "bafyreiabc123"
74
+
},
75
+
"mimeType": "image/jpeg"
76
+
}
77
+
}
78
+
]
79
+
}
80
+
});
81
+
82
+
let blobs = extract_blobs_from_record(&record).unwrap();
83
+
assert_eq!(blobs.len(), 1);
84
+
assert_eq!(blobs[0].cid, "bafyreiabc123");
85
+
assert_eq!(blobs[0].mime_type.as_deref(), Some("image/jpeg"));
86
+
}
87
+
88
+
#[test]
89
+
fn test_extract_blobs_quote_with_media() {
90
+
let record = json!({
91
+
"embed": {
92
+
"$type": "app.bsky.embed.recordWithMedia",
93
+
"media": {
94
+
"images": [
95
+
{
96
+
"alt": "Test image",
97
+
"image": {
98
+
"ref": {
99
+
"$link": "bafyreiabc456"
100
+
},
101
+
"mimeType": "image/png"
102
+
}
103
+
}
104
+
]
105
+
}
106
+
}
107
+
});
108
+
109
+
let blobs = extract_blobs_from_record(&record).unwrap();
110
+
assert_eq!(blobs.len(), 1);
111
+
assert_eq!(blobs[0].cid, "bafyreiabc456");
112
+
assert_eq!(blobs[0].mime_type.as_deref(), Some("image/png"));
113
+
}
114
+
115
+
#[test]
116
+
fn test_extract_blobs_no_embed() {
117
+
let record = json!({
118
+
"text": "Just a text post"
119
+
});
120
+
121
+
let blobs = extract_blobs_from_record(&record).unwrap();
122
+
assert_eq!(blobs.len(), 0);
123
+
}
124
+
125
+
#[test]
126
+
fn test_extract_blobs_multiple_images() {
127
+
let record = json!({
128
+
"embed": {
129
+
"images": [
130
+
{
131
+
"image": {
132
+
"ref": {
133
+
"$link": "bafyreiabc111"
134
+
},
135
+
"mimeType": "image/jpeg"
136
+
}
137
+
},
138
+
{
139
+
"image": {
140
+
"ref": {
141
+
"$link": "bafyreiabc222"
142
+
},
143
+
"mimeType": "image/png"
144
+
}
145
+
}
146
+
]
147
+
}
148
+
});
149
+
150
+
let blobs = extract_blobs_from_record(&record).unwrap();
151
+
assert_eq!(blobs.len(), 2);
152
+
assert_eq!(blobs[0].cid, "bafyreiabc111");
153
+
assert_eq!(blobs[1].cid, "bafyreiabc222");
154
+
}
155
+
}
+191
src/jetstream/mod.rs
+191
src/jetstream/mod.rs
···
1
+
use jacquard_common::jetstream::{CommitOperation, JetstreamMessage, JetstreamParams};
2
+
use jacquard_common::xrpc::{SubscriptionClient, TungsteniteSubscriptionClient};
3
+
use miette::{IntoDiagnostic, Result};
4
+
use futures::StreamExt;
5
+
use tokio::sync::mpsc;
6
+
use tracing::{debug, error, info, warn};
7
+
use url::Url;
8
+
9
+
use crate::types::ImageJob;
10
+
11
+
pub mod cursor;
12
+
pub mod events;
13
+
14
+
pub struct JetstreamClient {
15
+
url: Url,
16
+
cursor: Option<i64>,
17
+
}
18
+
19
+
impl JetstreamClient {
20
+
pub fn new(url: String, cursor: Option<i64>) -> Result<Self> {
21
+
let url = Url::parse(&url).into_diagnostic()?;
22
+
Ok(Self { url, cursor })
23
+
}
24
+
25
+
/// Start jetstream subscription and send jobs to the provided channel
26
+
pub async fn subscribe(
27
+
self,
28
+
job_sender: mpsc::UnboundedSender<ImageJob>,
29
+
mut shutdown_rx: tokio::sync::oneshot::Receiver<()>,
30
+
) -> Result<()> {
31
+
info!("Connecting to Jetstream: {}", self.url);
32
+
33
+
let client = TungsteniteSubscriptionClient::from_base_uri(self.url.clone());
34
+
35
+
// Configure subscription parameters
36
+
let params = JetstreamParams {
37
+
wanted_collections: Some(vec!["app.bsky.feed.post".to_string().into()]),
38
+
wanted_dids: None,
39
+
cursor: self.cursor,
40
+
compress: None,
41
+
max_message_size_bytes: None,
42
+
require_hello: None,
43
+
};
44
+
45
+
let stream = client.subscribe(¶ms).await.into_diagnostic()?;
46
+
47
+
info!("Connected to Jetstream, streaming messages...");
48
+
49
+
// Convert to typed message stream
50
+
let (_sink, mut messages) = stream.into_stream();
51
+
52
+
let mut message_count = 0u64;
53
+
let mut last_cursor: Option<i64> = None;
54
+
let mut cursor_update_interval = tokio::time::interval(std::time::Duration::from_secs(10));
55
+
56
+
loop {
57
+
tokio::select! {
58
+
Some(result) = messages.next() => {
59
+
match result {
60
+
Ok(msg) => {
61
+
message_count += 1;
62
+
if message_count % 1000 == 0 {
63
+
debug!("Processed {} messages", message_count);
64
+
}
65
+
66
+
// Extract cursor from message
67
+
let cursor = match &msg {
68
+
JetstreamMessage::Commit { time_us, .. } => Some(*time_us),
69
+
JetstreamMessage::Identity { time_us, .. } => Some(*time_us),
70
+
JetstreamMessage::Account { time_us, .. } => Some(*time_us),
71
+
};
72
+
73
+
if let Some(c) = cursor {
74
+
last_cursor = Some(c);
75
+
}
76
+
77
+
if let Err(e) = self.process_message(msg, &job_sender) {
78
+
error!("Error processing message: {}", e);
79
+
}
80
+
}
81
+
Err(e) => {
82
+
error!("Jetstream error: {}", e);
83
+
}
84
+
}
85
+
}
86
+
_ = cursor_update_interval.tick() => {
87
+
if let Some(cursor) = last_cursor {
88
+
if let Err(e) = cursor::write_cursor(cursor) {
89
+
warn!("Failed to write cursor: {}", e);
90
+
} else {
91
+
debug!("Wrote cursor: {}", cursor);
92
+
}
93
+
}
94
+
}
95
+
_ = &mut shutdown_rx => {
96
+
info!("Shutting down Jetstream client");
97
+
info!("Processed {} total messages", message_count);
98
+
99
+
// Write final cursor
100
+
if let Some(cursor) = last_cursor {
101
+
if let Err(e) = cursor::write_cursor(cursor) {
102
+
warn!("Failed to write final cursor: {}", e);
103
+
} else {
104
+
info!("Wrote final cursor: {}", cursor);
105
+
}
106
+
}
107
+
108
+
break;
109
+
}
110
+
}
111
+
}
112
+
113
+
Ok(())
114
+
}
115
+
116
+
fn process_message(
117
+
&self,
118
+
msg: JetstreamMessage,
119
+
job_sender: &mpsc::UnboundedSender<ImageJob>,
120
+
) -> Result<()> {
121
+
match msg {
122
+
JetstreamMessage::Commit {
123
+
did,
124
+
time_us: _,
125
+
commit,
126
+
} => {
127
+
// Only process create operations on posts
128
+
if commit.collection.as_ref() != "app.bsky.feed.post" {
129
+
return Ok(());
130
+
}
131
+
132
+
if !matches!(commit.operation, CommitOperation::Create) {
133
+
return Ok(());
134
+
}
135
+
136
+
// Parse record to extract blobs (skip if no record)
137
+
let Some(record_data) = &commit.record else {
138
+
return Ok(());
139
+
};
140
+
141
+
// Convert Data to serde_json::Value
142
+
let record_value: serde_json::Value =
143
+
serde_json::to_value(record_data).into_diagnostic()?;
144
+
let blobs = events::extract_blobs_from_record(&record_value)?;
145
+
146
+
if blobs.is_empty() {
147
+
return Ok(());
148
+
}
149
+
150
+
let post_uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey);
151
+
152
+
debug!(
153
+
"Post with {} blob(s): {}",
154
+
blobs.len(),
155
+
post_uri
156
+
);
157
+
158
+
// Create job
159
+
let post_cid = commit
160
+
.cid
161
+
.as_ref()
162
+
.map(|cid| cid.to_string())
163
+
.unwrap_or_default();
164
+
165
+
let job = ImageJob {
166
+
post_uri: post_uri.clone(),
167
+
post_cid,
168
+
post_did: did.to_string(),
169
+
blobs,
170
+
timestamp: chrono::Utc::now().timestamp_millis(),
171
+
attempts: 0,
172
+
};
173
+
174
+
// Send to queue
175
+
if let Err(e) = job_sender.send(job) {
176
+
warn!("Failed to send job to queue: {}", e);
177
+
}
178
+
179
+
Ok(())
180
+
}
181
+
JetstreamMessage::Identity { .. } => {
182
+
// Ignore identity updates for now
183
+
Ok(())
184
+
}
185
+
JetstreamMessage::Account { .. } => {
186
+
// Ignore account updates for now
187
+
Ok(())
188
+
}
189
+
}
190
+
}
191
+
}
+28
src/lib.rs
+28
src/lib.rs
···
1
+
// Core modules
2
+
pub mod config;
3
+
pub mod types;
4
+
5
+
// Processing modules
6
+
pub mod processor;
7
+
8
+
// Jetstream client
9
+
pub mod jetstream;
10
+
11
+
// Moderation actions
12
+
pub mod moderation;
13
+
14
+
// Agent/authentication
15
+
pub mod agent;
16
+
17
+
// Cache
18
+
pub mod cache;
19
+
20
+
// Queue
21
+
pub mod queue;
22
+
23
+
// Metrics
24
+
pub mod metrics;
25
+
26
+
// Re-export commonly used types
27
+
pub use config::Config;
28
+
pub use types::{BlobCheck, BlobReference, ImageJob, MatchResult};
+192
src/main.rs
+192
src/main.rs
···
1
+
use miette::{IntoDiagnostic, Result};
2
+
use reqwest::Client;
3
+
use std::path::Path;
4
+
use std::time::Duration;
5
+
use tokio::sync::mpsc;
6
+
use tokio::time::interval;
7
+
use tracing::{error, info};
8
+
9
+
use skywatch_phash_rs::{
10
+
agent::AgentSession,
11
+
cache::PhashCache,
12
+
config::Config,
13
+
jetstream::JetstreamClient,
14
+
metrics::Metrics,
15
+
processor::matcher,
16
+
queue::{JobQueue, WorkerPool},
17
+
};
18
+
19
+
#[tokio::main]
20
+
async fn main() -> Result<()> {
21
+
// Initialize tracing/logging
22
+
tracing_subscriber::fmt()
23
+
.with_env_filter(
24
+
tracing_subscriber::EnvFilter::from_default_env()
25
+
.add_directive(tracing::Level::INFO.into()),
26
+
)
27
+
.init();
28
+
29
+
info!("skywatch-phash-rs starting...");
30
+
31
+
// Load configuration
32
+
let config = Config::from_env()?;
33
+
info!("Configuration loaded");
34
+
info!("Jetstream URL: {}", config.jetstream.url);
35
+
info!("Redis URL: {}", config.redis.url);
36
+
info!("PDS Endpoint: {}", config.pds.endpoint);
37
+
info!("Processing concurrency: {}", config.processing.concurrency);
38
+
39
+
// Create HTTP client
40
+
let client = Client::builder()
41
+
.timeout(Duration::from_secs(30))
42
+
.build()
43
+
.into_diagnostic()?;
44
+
45
+
// Create and authenticate agent
46
+
info!("Authenticating labeler agent...");
47
+
let agent = AgentSession::new(&config).await?;
48
+
info!("Agent authenticated as {}", config.automod.handle);
49
+
50
+
// Load blob checks from rules file
51
+
let rules_path = Path::new("rules/blobs.json");
52
+
info!("Loading blob checks from {:?}", rules_path);
53
+
let blob_checks = matcher::load_blob_checks(rules_path).await?;
54
+
info!("Loaded {} blob check rules", blob_checks.len());
55
+
56
+
// Create metrics tracker
57
+
let metrics = Metrics::new();
58
+
info!("Metrics tracker initialized");
59
+
60
+
// Create cache
61
+
let cache = PhashCache::new(&config).await?;
62
+
info!("Cache initialized (enabled: {})", cache.is_enabled());
63
+
64
+
// Create job queue
65
+
let queue = JobQueue::new(&config).await?;
66
+
info!("Job queue initialized");
67
+
68
+
// Create worker pool
69
+
let worker_pool = WorkerPool::new(
70
+
config.clone(),
71
+
client.clone(),
72
+
agent.clone(),
73
+
blob_checks.clone(),
74
+
metrics.clone(),
75
+
);
76
+
info!("Worker pool created with {} workers", config.processing.concurrency);
77
+
78
+
// Create shutdown channels
79
+
let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
80
+
let (jetstream_shutdown_tx, jetstream_shutdown_rx) = tokio::sync::oneshot::channel::<()>();
81
+
82
+
// Create job channel for jetstream -> queue
83
+
let (job_tx, mut job_rx) = mpsc::unbounded_channel();
84
+
85
+
// Load cursor from disk
86
+
let cursor = skywatch_phash_rs::jetstream::cursor::read_cursor();
87
+
if let Some(c) = cursor {
88
+
info!("Resuming from cursor: {}", c);
89
+
}
90
+
91
+
// Start jetstream subscriber
92
+
info!("Starting Jetstream subscriber...");
93
+
let jetstream_config = config.clone();
94
+
let jetstream_handle = tokio::spawn(async move {
95
+
let jetstream = JetstreamClient::new(jetstream_config.jetstream.url.clone(), cursor)
96
+
.expect("Failed to create Jetstream client");
97
+
98
+
if let Err(e) = jetstream.subscribe(job_tx, jetstream_shutdown_rx).await {
99
+
error!("Jetstream subscriber failed: {}", e);
100
+
}
101
+
});
102
+
103
+
// Start job receiver (receives from jetstream, pushes to queue)
104
+
info!("Starting job receiver...");
105
+
let mut queue_for_receiver = queue.clone();
106
+
let receiver_metrics = metrics.clone();
107
+
let receiver_handle = tokio::spawn(async move {
108
+
while let Some(job) = job_rx.recv().await {
109
+
receiver_metrics.inc_jobs_received();
110
+
if let Err(e) = queue_for_receiver.push(&job).await {
111
+
error!("Failed to push job to queue: {}", e);
112
+
receiver_metrics.inc_jobs_failed();
113
+
}
114
+
}
115
+
info!("Job receiver stopped");
116
+
});
117
+
118
+
// Start worker pool with N concurrent workers
119
+
// Run directly without tokio::spawn to avoid HRTB lifetime issues with MemoryCredentialSession
120
+
let concurrency = config.processing.concurrency;
121
+
info!("Starting {} concurrent workers...", concurrency);
122
+
123
+
let mut worker_futures = Vec::new();
124
+
let (broadcast_shutdown_tx, _) = tokio::sync::broadcast::channel(1);
125
+
126
+
for worker_id in 0..concurrency {
127
+
let worker_pool_clone = worker_pool.clone();
128
+
let queue_clone = queue.clone();
129
+
let cache_clone = cache.clone();
130
+
let worker_shutdown = broadcast_shutdown_tx.subscribe();
131
+
132
+
let worker_future = async move {
133
+
info!("Worker {} starting", worker_id);
134
+
if let Err(e) = worker_pool_clone.start(queue_clone, cache_clone, worker_shutdown).await {
135
+
error!("Worker {} failed: {}", worker_id, e);
136
+
}
137
+
info!("Worker {} stopped", worker_id);
138
+
};
139
+
140
+
worker_futures.push(worker_future);
141
+
}
142
+
143
+
let all_workers_future = futures::future::join_all(worker_futures);
144
+
145
+
// Start metrics logger
146
+
info!("Starting metrics logger...");
147
+
let metrics_for_logger = metrics.clone();
148
+
let metrics_handle = tokio::spawn(async move {
149
+
let mut ticker = interval(Duration::from_secs(60));
150
+
loop {
151
+
ticker.tick().await;
152
+
metrics_for_logger.log_stats();
153
+
}
154
+
});
155
+
156
+
// Wait for shutdown signal or task completion
157
+
info!("Service started successfully, waiting for shutdown signal...");
158
+
tokio::select! {
159
+
_ = tokio::signal::ctrl_c() => {
160
+
info!("Received Ctrl+C, initiating graceful shutdown...");
161
+
}
162
+
_ = shutdown_rx => {
163
+
info!("Received shutdown signal");
164
+
}
165
+
_ = all_workers_future => {
166
+
info!("All workers completed");
167
+
}
168
+
_ = jetstream_handle => {
169
+
info!("Jetstream subscriber completed");
170
+
}
171
+
}
172
+
173
+
// Send shutdown signals to all tasks
174
+
info!("Shutting down Jetstream subscriber...");
175
+
let _ = jetstream_shutdown_tx.send(());
176
+
177
+
info!("Shutting down workers...");
178
+
let _ = broadcast_shutdown_tx.send(());
179
+
180
+
// Stop metrics logger
181
+
metrics_handle.abort();
182
+
183
+
// Stop job receiver
184
+
receiver_handle.abort();
185
+
186
+
// Log final metrics
187
+
info!("=== Final Metrics ===");
188
+
metrics.log_stats();
189
+
190
+
info!("Shutdown complete");
191
+
Ok(())
192
+
}
src/matcher/mod.rs
src/matcher/mod.rs
This is a binary file and will not be displayed.
+130
src/metrics/README.md
+130
src/metrics/README.md
···
1
+
# Metrics Module
2
+
3
+
## Purpose
4
+
Lock-free atomic counters for tracking system performance and moderation actions.
5
+
6
+
## Key Components
7
+
8
+
### `mod.rs`
9
+
- **`Metrics`** - Collection of atomic counters
10
+
- Thread-safe (Arc-wrapped)
11
+
- Lock-free operations (AtomicU64)
12
+
- Cheap to clone and share across workers
13
+
14
+
### Tracked Metrics
15
+
16
+
**Job Processing:**
17
+
- `jobs_received` - Total jobs from Jetstream
18
+
- `jobs_processed` - Successfully completed jobs
19
+
- `jobs_failed` - Failed jobs (after all retries)
20
+
- `jobs_retried` - Jobs sent to retry queue
21
+
22
+
**Blob Processing:**
23
+
- `blobs_processed` - Total blobs checked
24
+
- `blobs_downloaded` - Blobs fetched from CDN/PDS
25
+
- `matches_found` - Phashes matching known bad images
26
+
27
+
**Cache Performance:**
28
+
- `cache_hits` - Phash found in cache
29
+
- `cache_misses` - Phash not in cache (download needed)
30
+
31
+
**Moderation Actions:**
32
+
- `posts_labeled` - Posts labeled via Ozone
33
+
- `posts_reported` - Posts reported to moderators
34
+
- `accounts_labeled` - Accounts labeled
35
+
- `accounts_reported` - Accounts reported
36
+
37
+
### Key Methods
38
+
39
+
**Increment counters:**
40
+
- `inc_jobs_received()`
41
+
- `inc_jobs_processed()`
42
+
- `inc_cache_hits()`
43
+
- etc.
44
+
45
+
**Read values:**
46
+
- `snapshot()` - Get all current values as `MetricsSnapshot`
47
+
- Returns struct with all counter values
48
+
- Used for logging/monitoring
49
+
50
+
**Computed metrics:**
51
+
- `cache_hit_rate()` - Percentage of cache hits
52
+
- Returns `hits / (hits + misses)`
53
+
- 0.0 if no cache operations yet
54
+
55
+
## Atomic Operations
56
+
57
+
Uses `AtomicU64` with `Ordering::Relaxed`:
58
+
- **Why Relaxed?** - Only need atomicity, not ordering guarantees
59
+
- **Performance** - Fastest atomic operations
60
+
- **Safe** - Multiple workers can increment concurrently
61
+
62
+
Example:
63
+
```rust
64
+
// Thread-safe increment from any worker
65
+
metrics.inc_jobs_processed();
66
+
67
+
// Lock-free read
68
+
let processed = metrics.jobs_processed.load(Ordering::Relaxed);
69
+
```
70
+
71
+
## Logging Pattern
72
+
73
+
Metrics are logged:
74
+
1. **Periodically** - Every 60 seconds while running
75
+
2. **On shutdown** - Final totals when service stops
76
+
77
+
Log format:
78
+
```
79
+
Metrics snapshot:
80
+
Jobs: received=1234, processed=1200, failed=5, retried=29
81
+
Blobs: processed=3456, downloaded=890
82
+
Cache: hits=2566 (74.3%), misses=890
83
+
Matches: found=42
84
+
Moderation: posts_labeled=38, posts_reported=42, accounts_labeled=10, accounts_reported=12
85
+
```
86
+
87
+
## Usage Pattern
88
+
89
+
```rust
90
+
// Create once at startup
91
+
let metrics = Metrics::new();
92
+
93
+
// Clone into workers
94
+
let worker_metrics = metrics.clone();
95
+
96
+
// Increment from workers
97
+
worker_metrics.inc_jobs_processed();
98
+
worker_metrics.inc_cache_hits();
99
+
100
+
// Read snapshot for logging
101
+
let snapshot = metrics.snapshot();
102
+
info!("Cache hit rate: {:.1}%", snapshot.cache_hit_rate() * 100.0);
103
+
```
104
+
105
+
## Design Decisions
106
+
107
+
**Why AtomicU64 instead of Mutex<u64>?**
108
+
- No lock contention across 10+ workers
109
+
- Much faster for increment operations
110
+
- Can't overflow in practice (u64 max = 18 quintillion)
111
+
112
+
**Why snapshot()?**
113
+
- Get consistent view of all metrics at once
114
+
- Prevents reading torn values during logging
115
+
- Easy to extend with new computed metrics
116
+
117
+
**Why Relaxed ordering?**
118
+
- Counters are independent
119
+
- Don't need cross-counter synchronization
120
+
- Exact timing doesn't matter (eventually consistent)
121
+
122
+
## Dependencies
123
+
124
+
- `std::sync::atomic` - Atomic operations
125
+
- `std::sync::Arc` - Shared ownership
126
+
127
+
## Related Modules
128
+
129
+
- Used by: All modules - everything increments metrics
130
+
- Read by: `main.rs` - Periodic logging task
+352
src/metrics/mod.rs
+352
src/metrics/mod.rs
···
1
+
use std::sync::atomic::{AtomicU64, Ordering};
2
+
use std::sync::Arc;
3
+
use tracing::info;
4
+
5
+
/// Global metrics tracker for the service
6
+
#[derive(Clone)]
7
+
pub struct Metrics {
8
+
inner: Arc<MetricsInner>,
9
+
}
10
+
11
+
struct MetricsInner {
12
+
// Job metrics
13
+
jobs_received: AtomicU64,
14
+
jobs_processed: AtomicU64,
15
+
jobs_failed: AtomicU64,
16
+
jobs_retried: AtomicU64,
17
+
18
+
// Blob metrics
19
+
blobs_processed: AtomicU64,
20
+
blobs_downloaded: AtomicU64,
21
+
22
+
// Match metrics
23
+
matches_found: AtomicU64,
24
+
25
+
// Cache metrics
26
+
cache_hits: AtomicU64,
27
+
cache_misses: AtomicU64,
28
+
29
+
// Moderation metrics
30
+
posts_labeled: AtomicU64,
31
+
posts_reported: AtomicU64,
32
+
accounts_labeled: AtomicU64,
33
+
accounts_reported: AtomicU64,
34
+
35
+
// Skip metrics (already handled)
36
+
posts_already_labeled: AtomicU64,
37
+
posts_already_reported: AtomicU64,
38
+
accounts_already_labeled: AtomicU64,
39
+
accounts_already_reported: AtomicU64,
40
+
}
41
+
42
+
impl Metrics {
43
+
/// Create a new metrics tracker
44
+
pub fn new() -> Self {
45
+
Self {
46
+
inner: Arc::new(MetricsInner {
47
+
jobs_received: AtomicU64::new(0),
48
+
jobs_processed: AtomicU64::new(0),
49
+
jobs_failed: AtomicU64::new(0),
50
+
jobs_retried: AtomicU64::new(0),
51
+
blobs_processed: AtomicU64::new(0),
52
+
blobs_downloaded: AtomicU64::new(0),
53
+
matches_found: AtomicU64::new(0),
54
+
cache_hits: AtomicU64::new(0),
55
+
cache_misses: AtomicU64::new(0),
56
+
posts_labeled: AtomicU64::new(0),
57
+
posts_reported: AtomicU64::new(0),
58
+
accounts_labeled: AtomicU64::new(0),
59
+
accounts_reported: AtomicU64::new(0),
60
+
posts_already_labeled: AtomicU64::new(0),
61
+
posts_already_reported: AtomicU64::new(0),
62
+
accounts_already_labeled: AtomicU64::new(0),
63
+
accounts_already_reported: AtomicU64::new(0),
64
+
}),
65
+
}
66
+
}
67
+
68
+
// Job metrics
69
+
pub fn inc_jobs_received(&self) {
70
+
self.inner.jobs_received.fetch_add(1, Ordering::Relaxed);
71
+
}
72
+
73
+
pub fn inc_jobs_processed(&self) {
74
+
self.inner.jobs_processed.fetch_add(1, Ordering::Relaxed);
75
+
}
76
+
77
+
pub fn inc_jobs_failed(&self) {
78
+
self.inner.jobs_failed.fetch_add(1, Ordering::Relaxed);
79
+
}
80
+
81
+
pub fn inc_jobs_retried(&self) {
82
+
self.inner.jobs_retried.fetch_add(1, Ordering::Relaxed);
83
+
}
84
+
85
+
// Blob metrics
86
+
pub fn inc_blobs_processed(&self) {
87
+
self.inner.blobs_processed.fetch_add(1, Ordering::Relaxed);
88
+
}
89
+
90
+
pub fn inc_blobs_downloaded(&self) {
91
+
self.inner.blobs_downloaded.fetch_add(1, Ordering::Relaxed);
92
+
}
93
+
94
+
// Match metrics
95
+
pub fn inc_matches_found(&self) {
96
+
self.inner.matches_found.fetch_add(1, Ordering::Relaxed);
97
+
}
98
+
99
+
// Cache metrics
100
+
pub fn inc_cache_hits(&self) {
101
+
self.inner.cache_hits.fetch_add(1, Ordering::Relaxed);
102
+
}
103
+
104
+
pub fn inc_cache_misses(&self) {
105
+
self.inner.cache_misses.fetch_add(1, Ordering::Relaxed);
106
+
}
107
+
108
+
// Moderation metrics
109
+
pub fn inc_posts_labeled(&self) {
110
+
self.inner.posts_labeled.fetch_add(1, Ordering::Relaxed);
111
+
}
112
+
113
+
pub fn inc_posts_reported(&self) {
114
+
self.inner.posts_reported.fetch_add(1, Ordering::Relaxed);
115
+
}
116
+
117
+
pub fn inc_accounts_labeled(&self) {
118
+
self.inner.accounts_labeled.fetch_add(1, Ordering::Relaxed);
119
+
}
120
+
121
+
pub fn inc_accounts_reported(&self) {
122
+
self.inner.accounts_reported.fetch_add(1, Ordering::Relaxed);
123
+
}
124
+
125
+
// Skip metrics
126
+
pub fn inc_posts_already_labeled(&self) {
127
+
self.inner.posts_already_labeled.fetch_add(1, Ordering::Relaxed);
128
+
}
129
+
130
+
pub fn inc_posts_already_reported(&self) {
131
+
self.inner.posts_already_reported.fetch_add(1, Ordering::Relaxed);
132
+
}
133
+
134
+
pub fn inc_accounts_already_labeled(&self) {
135
+
self.inner.accounts_already_labeled.fetch_add(1, Ordering::Relaxed);
136
+
}
137
+
138
+
pub fn inc_accounts_already_reported(&self) {
139
+
self.inner.accounts_already_reported.fetch_add(1, Ordering::Relaxed);
140
+
}
141
+
142
+
// Getters
143
+
pub fn jobs_received(&self) -> u64 {
144
+
self.inner.jobs_received.load(Ordering::Relaxed)
145
+
}
146
+
147
+
pub fn jobs_processed(&self) -> u64 {
148
+
self.inner.jobs_processed.load(Ordering::Relaxed)
149
+
}
150
+
151
+
pub fn jobs_failed(&self) -> u64 {
152
+
self.inner.jobs_failed.load(Ordering::Relaxed)
153
+
}
154
+
155
+
pub fn jobs_retried(&self) -> u64 {
156
+
self.inner.jobs_retried.load(Ordering::Relaxed)
157
+
}
158
+
159
+
pub fn blobs_processed(&self) -> u64 {
160
+
self.inner.blobs_processed.load(Ordering::Relaxed)
161
+
}
162
+
163
+
pub fn blobs_downloaded(&self) -> u64 {
164
+
self.inner.blobs_downloaded.load(Ordering::Relaxed)
165
+
}
166
+
167
+
pub fn matches_found(&self) -> u64 {
168
+
self.inner.matches_found.load(Ordering::Relaxed)
169
+
}
170
+
171
+
pub fn cache_hits(&self) -> u64 {
172
+
self.inner.cache_hits.load(Ordering::Relaxed)
173
+
}
174
+
175
+
pub fn cache_misses(&self) -> u64 {
176
+
self.inner.cache_misses.load(Ordering::Relaxed)
177
+
}
178
+
179
+
pub fn posts_labeled(&self) -> u64 {
180
+
self.inner.posts_labeled.load(Ordering::Relaxed)
181
+
}
182
+
183
+
pub fn posts_reported(&self) -> u64 {
184
+
self.inner.posts_reported.load(Ordering::Relaxed)
185
+
}
186
+
187
+
pub fn accounts_labeled(&self) -> u64 {
188
+
self.inner.accounts_labeled.load(Ordering::Relaxed)
189
+
}
190
+
191
+
pub fn accounts_reported(&self) -> u64 {
192
+
self.inner.accounts_reported.load(Ordering::Relaxed)
193
+
}
194
+
195
+
pub fn posts_already_labeled(&self) -> u64 {
196
+
self.inner.posts_already_labeled.load(Ordering::Relaxed)
197
+
}
198
+
199
+
pub fn posts_already_reported(&self) -> u64 {
200
+
self.inner.posts_already_reported.load(Ordering::Relaxed)
201
+
}
202
+
203
+
pub fn accounts_already_labeled(&self) -> u64 {
204
+
self.inner.accounts_already_labeled.load(Ordering::Relaxed)
205
+
}
206
+
207
+
pub fn accounts_already_reported(&self) -> u64 {
208
+
self.inner.accounts_already_reported.load(Ordering::Relaxed)
209
+
}
210
+
211
+
/// Log current metrics
212
+
pub fn log_stats(&self) {
213
+
info!("=== Metrics ===");
214
+
info!("Jobs: received={}, processed={}, failed={}, retried={}",
215
+
self.jobs_received(),
216
+
self.jobs_processed(),
217
+
self.jobs_failed(),
218
+
self.jobs_retried()
219
+
);
220
+
info!("Blobs: processed={}, downloaded={}",
221
+
self.blobs_processed(),
222
+
self.blobs_downloaded()
223
+
);
224
+
info!("Matches: found={}",
225
+
self.matches_found()
226
+
);
227
+
info!("Cache: hits={}, misses={}, hit_rate={:.2}%",
228
+
self.cache_hits(),
229
+
self.cache_misses(),
230
+
self.cache_hit_rate()
231
+
);
232
+
info!("Moderation: posts_labeled={}, posts_reported={}, accounts_labeled={}, accounts_reported={}",
233
+
self.posts_labeled(),
234
+
self.posts_reported(),
235
+
self.accounts_labeled(),
236
+
self.accounts_reported()
237
+
);
238
+
info!("Skipped (deduplication): posts_already_labeled={}, posts_already_reported={}, accounts_already_labeled={}, accounts_already_reported={}",
239
+
self.posts_already_labeled(),
240
+
self.posts_already_reported(),
241
+
self.accounts_already_labeled(),
242
+
self.accounts_already_reported()
243
+
);
244
+
}
245
+
246
+
/// Calculate cache hit rate
247
+
pub fn cache_hit_rate(&self) -> f64 {
248
+
let hits = self.cache_hits() as f64;
249
+
let total = (self.cache_hits() + self.cache_misses()) as f64;
250
+
if total == 0.0 {
251
+
0.0
252
+
} else {
253
+
(hits / total) * 100.0
254
+
}
255
+
}
256
+
257
+
/// Get snapshot of current metrics
258
+
pub fn snapshot(&self) -> MetricsSnapshot {
259
+
MetricsSnapshot {
260
+
jobs_received: self.jobs_received(),
261
+
jobs_processed: self.jobs_processed(),
262
+
jobs_failed: self.jobs_failed(),
263
+
jobs_retried: self.jobs_retried(),
264
+
blobs_processed: self.blobs_processed(),
265
+
blobs_downloaded: self.blobs_downloaded(),
266
+
matches_found: self.matches_found(),
267
+
cache_hits: self.cache_hits(),
268
+
cache_misses: self.cache_misses(),
269
+
posts_labeled: self.posts_labeled(),
270
+
posts_reported: self.posts_reported(),
271
+
accounts_labeled: self.accounts_labeled(),
272
+
accounts_reported: self.accounts_reported(),
273
+
posts_already_labeled: self.posts_already_labeled(),
274
+
posts_already_reported: self.posts_already_reported(),
275
+
accounts_already_labeled: self.accounts_already_labeled(),
276
+
accounts_already_reported: self.accounts_already_reported(),
277
+
}
278
+
}
279
+
}
280
+
281
+
impl Default for Metrics {
282
+
fn default() -> Self {
283
+
Self::new()
284
+
}
285
+
}
286
+
287
+
#[derive(Debug, Clone)]
288
+
pub struct MetricsSnapshot {
289
+
pub jobs_received: u64,
290
+
pub jobs_processed: u64,
291
+
pub jobs_failed: u64,
292
+
pub jobs_retried: u64,
293
+
pub blobs_processed: u64,
294
+
pub blobs_downloaded: u64,
295
+
pub matches_found: u64,
296
+
pub cache_hits: u64,
297
+
pub cache_misses: u64,
298
+
pub posts_labeled: u64,
299
+
pub posts_reported: u64,
300
+
pub accounts_labeled: u64,
301
+
pub accounts_reported: u64,
302
+
pub posts_already_labeled: u64,
303
+
pub posts_already_reported: u64,
304
+
pub accounts_already_labeled: u64,
305
+
pub accounts_already_reported: u64,
306
+
}
307
+
308
+
#[cfg(test)]
309
+
mod tests {
310
+
use super::*;
311
+
312
+
#[test]
313
+
fn test_metrics_increment() {
314
+
let metrics = Metrics::new();
315
+
316
+
assert_eq!(metrics.jobs_received(), 0);
317
+
318
+
metrics.inc_jobs_received();
319
+
metrics.inc_jobs_received();
320
+
321
+
assert_eq!(metrics.jobs_received(), 2);
322
+
}
323
+
324
+
#[test]
325
+
fn test_cache_hit_rate() {
326
+
let metrics = Metrics::new();
327
+
328
+
// No cache operations yet
329
+
assert_eq!(metrics.cache_hit_rate(), 0.0);
330
+
331
+
// 3 hits, 1 miss = 75%
332
+
metrics.inc_cache_hits();
333
+
metrics.inc_cache_hits();
334
+
metrics.inc_cache_hits();
335
+
metrics.inc_cache_misses();
336
+
337
+
assert_eq!(metrics.cache_hit_rate(), 75.0);
338
+
}
339
+
340
+
#[test]
341
+
fn test_metrics_snapshot() {
342
+
let metrics = Metrics::new();
343
+
344
+
metrics.inc_jobs_received();
345
+
metrics.inc_matches_found();
346
+
347
+
let snapshot = metrics.snapshot();
348
+
349
+
assert_eq!(snapshot.jobs_received, 1);
350
+
assert_eq!(snapshot.matches_found, 1);
351
+
}
352
+
}
+146
src/moderation/README.md
+146
src/moderation/README.md
···
1
+
# Moderation Module
2
+
3
+
## Purpose
4
+
Interacts with Bluesky's Ozone moderation API to take actions (label, report, takedown) on posts and accounts.
5
+
6
+
## Key Components
7
+
8
+
### `post.rs`
9
+
Moderation actions on posts:
10
+
- **`label_post()`** - Apply label to post (e.g., "spam", "csam")
11
+
- **`unlabel_post()`** - Remove label from post
12
+
- **`report_post()`** - Report post to moderators
13
+
- **`takedown_post()`** - Takedown/remove post
14
+
15
+
### `account.rs`
16
+
Moderation actions on accounts:
17
+
- **`label_account()`** - Apply label to account
18
+
- **`unlabel_account()`** - Remove label from account
19
+
- **`report_account()`** - Report account to moderators
20
+
- **`takedown_account()`** - Takedown/suspend account
21
+
22
+
### `claims.rs`
23
+
Deduplication to prevent duplicate reports:
24
+
- **`claim_post_report()`** - Check if post already reported
25
+
- **`claim_account_report()`** - Check if account already reported
26
+
- Uses Redis to track `post_uri:label` pairs
27
+
- Returns `true` if claim successful (first time), `false` if duplicate
28
+
29
+
### `rate_limiter.rs`
30
+
Global rate limiting across all workers:
31
+
- **`RateLimiter`** - Thread-safe rate limiter using governor crate
32
+
- **`new(rate_limit_ms)`** - Create limiter (e.g., 100ms = 10 req/sec)
33
+
- **`wait()`** - Block until allowed to make request
34
+
- Enforces global limit across all concurrent workers
35
+
36
+
## API Endpoints
37
+
38
+
All functions call Ozone API at `{OZONE_URL}/xrpc/tools.ozone.moderation.emitEvent`:
39
+
40
+
**Event types:**
41
+
- `tools.ozone.moderation.defs#modEventLabel` - Label action
42
+
- `tools.ozone.moderation.defs#modEventReport` - Report action
43
+
- `tools.ozone.moderation.defs#modEventTakedown` - Takedown action
44
+
45
+
## Comment Format
46
+
47
+
All moderation actions include detailed comments:
48
+
```
49
+
{timestamp}: {check_comment} at https://pdsls.dev/{post_uri} with phash "{phash}"
50
+
```
51
+
52
+
Example:
53
+
```
54
+
2025-10-24T12:34:56Z: Known spam image detected at https://pdsls.dev/at://did:plc:xyz/app.bsky.feed.post/abc with phash "e0e0e0e0e0fcfefe"
55
+
```
56
+
57
+
This provides:
58
+
- **When** - ISO 8601 timestamp
59
+
- **Why** - Comment from blob check rule
60
+
- **Where** - pdsls.dev link to the post
61
+
- **Evidence** - The phash that matched
62
+
63
+
## Rate Limiting
64
+
65
+
**Problem:** 10 workers × 4 actions/match = 40 concurrent requests could overwhelm API
66
+
67
+
**Solution:** Global rate limiter
68
+
```rust
69
+
// Before each API call:
70
+
rate_limiter.wait().await; // Blocks until safe to proceed
71
+
// Make request
72
+
```
73
+
74
+
With `RATE_LIMIT_MS=100`:
75
+
- Maximum 10 requests/second globally
76
+
- All workers share the same limiter
77
+
- Prevents rate limit errors from API
78
+
79
+
## Deduplication
80
+
81
+
**Problem:** Same post might match multiple times (retries, multiple rules)
82
+
83
+
**Solution:** Redis-backed claims
84
+
```rust
85
+
if claim_post_report(redis, post_uri, label).await? {
86
+
report_post(...).await?; // First time, do it
87
+
} else {
88
+
// Already reported, skip
89
+
}
90
+
```
91
+
92
+
**Storage:** Redis key = `report:post:{post_uri}:{label}` with 24h TTL
93
+
94
+
## Authentication
95
+
96
+
All functions require `access_token` from AgentSession:
97
+
```rust
98
+
let token = agent.get_token().await?;
99
+
label_post(client, &token, config, ...).await?;
100
+
```
101
+
102
+
Token is included in `Authorization: Bearer {token}` header.
103
+
104
+
## Required Headers
105
+
106
+
**For Ozone API:**
107
+
```
108
+
Authorization: Bearer {access_token}
109
+
atproto-proxy: {mod_did}#atproto_labeler
110
+
atproto-accept-labelers: did:plc:ar7c4by46qjdydhdevvrndac;redact
111
+
```
112
+
113
+
## Usage Flow
114
+
115
+
```
116
+
1. Worker finds match
117
+
2. Get access token from agent
118
+
3. For each configured action:
119
+
a. Check claim (dedupe)
120
+
b. Wait for rate limiter
121
+
c. Call moderation function
122
+
d. Increment metrics
123
+
```
124
+
125
+
## Configuration
126
+
127
+
```env
128
+
MOD_DID=did:plc:xxxxx # Moderator DID
129
+
OZONE_URL=https://ozone.example.com
130
+
OZONE_PDS=https://bsky.social
131
+
RATE_LIMIT_MS=100 # 10 req/sec max
132
+
```
133
+
134
+
## Dependencies
135
+
136
+
- `reqwest::Client` - HTTP client
137
+
- `redis` - Claims storage
138
+
- `governor` - Rate limiting
139
+
- `chrono` - Timestamps
140
+
- `serde_json` - JSON building
141
+
142
+
## Related Modules
143
+
144
+
- Used by: `queue/worker.rs` - Takes actions on matches
145
+
- Uses: `agent` - For access tokens
146
+
- Uses: `config` - For Ozone URL and rate limit
+220
src/moderation/account.rs
+220
src/moderation/account.rs
···
1
+
use jacquard::client::{Agent, MemoryCredentialSession};
2
+
use jacquard_api::com_atproto::admin::RepoRef;
3
+
use jacquard_api::com_atproto::moderation::ReasonType;
4
+
use jacquard_api::tools_ozone::moderation::emit_event::{
5
+
EmitEvent, EmitEventEvent, EmitEventSubject,
6
+
};
7
+
use jacquard_api::tools_ozone::moderation::{
8
+
ModEventLabel, ModEventReport, ModEventTakedown, ModTool,
9
+
};
10
+
use jacquard_common::CowStr;
11
+
use jacquard_common::types::string::Did;
12
+
use jacquard_common::types::value::{Data, Object};
13
+
use jacquard_common::xrpc::{CallOptions, XrpcClient};
14
+
15
+
use jacquard_common::smol_str::SmolStr;
16
+
use miette::{IntoDiagnostic, Result};
17
+
use std::collections::BTreeMap;
18
+
use tracing::{debug, info};
19
+
20
+
use crate::config::Config;
21
+
use crate::moderation::rate_limiter::RateLimiter;
22
+
23
+
/// Label an account with a specific label via Ozone moderation API
24
+
pub async fn label_account(
25
+
agent: &Agent<MemoryCredentialSession>,
26
+
config: &Config,
27
+
rate_limiter: &RateLimiter,
28
+
did: &str,
29
+
label_val: &str,
30
+
check_comment: &str,
31
+
post_uri: &str,
32
+
phash: &str,
33
+
created_by: &str,
34
+
) -> Result<()> {
35
+
// Wait for rate limiter before making request
36
+
rate_limiter.wait().await;
37
+
38
+
info!("Labeling account {} with label: {}", did, label_val);
39
+
40
+
let timestamp = chrono::Utc::now().to_rfc3339();
41
+
let comment = format!("{}: {}", timestamp, check_comment);
42
+
43
+
// Build mod_tool meta
44
+
let mut meta_map = BTreeMap::new();
45
+
meta_map.insert(
46
+
SmolStr::new("externalUrl"),
47
+
format!("https://pdsls.dev/{}", post_uri).into(),
48
+
);
49
+
meta_map.insert(SmolStr::new("phash"), phash.into());
50
+
51
+
// Create moderation label event using jacquard-api types
52
+
let event = EmitEvent::new()
53
+
.created_by(Did::new(created_by).into_diagnostic()?)
54
+
.event(EmitEventEvent::ModEventLabel(Box::new(
55
+
ModEventLabel::builder()
56
+
.create_label_vals(vec![CowStr::from(label_val)])
57
+
.negate_label_vals(vec![])
58
+
.comment(CowStr::from(comment))
59
+
.build(),
60
+
)))
61
+
.subject(EmitEventSubject::RepoRef(Box::new(
62
+
RepoRef::builder()
63
+
.did(Did::new(did).into_diagnostic()?)
64
+
.build(),
65
+
)))
66
+
.mod_tool(ModTool {
67
+
name: CowStr::from("skywatch/skywatch-phash-rs"),
68
+
meta: Some(Data::Object(Object::from(meta_map))),
69
+
extra_data: BTreeMap::new(),
70
+
})
71
+
.build();
72
+
73
+
// Build call options with proxy headers
74
+
let opts = CallOptions {
75
+
auth: None,
76
+
atproto_proxy: Some(CowStr::from(format!(
77
+
"{}#atproto_labeler",
78
+
config.moderation.labeler_did
79
+
))),
80
+
atproto_accept_labelers: Some(vec![CowStr::from(
81
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
82
+
)]),
83
+
extra_headers: vec![],
84
+
};
85
+
86
+
// Send request via jacquard agent
87
+
let _response = agent.send_with_opts(event, opts).await.into_diagnostic()?;
88
+
89
+
debug!("Successfully labeled account: {}", did);
90
+
91
+
Ok(())
92
+
}
93
+
94
+
/// Report an account to ozone moderation
95
+
pub async fn report_account(
96
+
agent: &Agent<MemoryCredentialSession>,
97
+
config: &Config,
98
+
rate_limiter: &RateLimiter,
99
+
did: &str,
100
+
reason: ReasonType<'static>,
101
+
check_comment: &str,
102
+
post_uri: &str,
103
+
phash: &str,
104
+
created_by: &str,
105
+
) -> Result<()> {
106
+
// Wait for rate limiter before making request
107
+
rate_limiter.wait().await;
108
+
109
+
info!("Reporting account {} to ozone: {:?}", did, reason);
110
+
111
+
let timestamp = chrono::Utc::now().to_rfc3339();
112
+
let comment = format!("{}: {}", timestamp, check_comment);
113
+
114
+
// Build mod_tool meta
115
+
let mut meta_map = BTreeMap::new();
116
+
meta_map.insert(
117
+
SmolStr::new("externalUrl"),
118
+
format!("https://pdsls.dev/{}", post_uri).into(),
119
+
);
120
+
meta_map.insert(SmolStr::new("phash"), phash.into());
121
+
122
+
// Create moderation report event using jacquard-api types
123
+
let event = EmitEvent::new()
124
+
.created_by(Did::new(created_by).into_diagnostic()?)
125
+
.event(EmitEventEvent::ModEventReport(Box::new(
126
+
ModEventReport::builder()
127
+
.report_type(reason)
128
+
.comment(CowStr::from(comment))
129
+
.build(),
130
+
)))
131
+
.subject(EmitEventSubject::RepoRef(Box::new(
132
+
RepoRef::builder()
133
+
.did(Did::new(did).into_diagnostic()?)
134
+
.build(),
135
+
)))
136
+
.subject_blob_cids(vec![])
137
+
.mod_tool(ModTool {
138
+
name: CowStr::from("skywatch/skywatch-phash-rs"),
139
+
meta: Some(Data::Object(Object::from(meta_map))),
140
+
extra_data: BTreeMap::new(),
141
+
})
142
+
.build();
143
+
144
+
// Build call options with proxy headers
145
+
let opts = CallOptions {
146
+
auth: None,
147
+
atproto_proxy: Some(CowStr::from(format!(
148
+
"{}#atproto_labeler",
149
+
config.moderation.labeler_did
150
+
))),
151
+
atproto_accept_labelers: Some(vec![CowStr::from(
152
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
153
+
)]),
154
+
extra_headers: vec![],
155
+
};
156
+
157
+
// Send request via jacquard agent
158
+
let _response = agent.send_with_opts(event, opts).await.into_diagnostic()?;
159
+
160
+
debug!("Successfully reported account: {}", did);
161
+
162
+
Ok(())
163
+
}
164
+
165
+
/// Takedown an account via Ozone moderation API
166
+
pub async fn takedown_account(
167
+
agent: &Agent<MemoryCredentialSession>,
168
+
config: &Config,
169
+
rate_limiter: &RateLimiter,
170
+
did: &str,
171
+
comment: &str,
172
+
created_by: &str,
173
+
) -> Result<()> {
174
+
// Wait for rate limiter before making request
175
+
rate_limiter.wait().await;
176
+
177
+
info!("Taking down account: {}", did);
178
+
179
+
// Create moderation takedown event using jacquard-api types
180
+
let event = EmitEvent::new()
181
+
.created_by(Did::new(created_by).into_diagnostic()?)
182
+
.event(EmitEventEvent::ModEventTakedown(Box::new(
183
+
ModEventTakedown {
184
+
comment: Some(CowStr::from(comment)),
185
+
..Default::default()
186
+
},
187
+
)))
188
+
.subject(EmitEventSubject::RepoRef(Box::new(
189
+
RepoRef::builder()
190
+
.did(Did::new(did).into_diagnostic()?)
191
+
.build(),
192
+
)))
193
+
.build();
194
+
195
+
// Build call options with proxy headers
196
+
let opts = CallOptions {
197
+
auth: None,
198
+
atproto_proxy: Some(CowStr::from(format!(
199
+
"{}#atproto_labeler",
200
+
config.moderation.labeler_did
201
+
))),
202
+
atproto_accept_labelers: Some(vec![CowStr::from(
203
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
204
+
)]),
205
+
extra_headers: vec![],
206
+
};
207
+
208
+
// Send request via jacquard agent
209
+
let _response = agent.send_with_opts(event, opts).await.into_diagnostic()?;
210
+
211
+
debug!("Successfully took down account: {}", did);
212
+
213
+
Ok(())
214
+
}
215
+
216
+
#[cfg(test)]
217
+
mod tests {
218
+
// Note: These are integration tests that require a real client and auth
219
+
// Unit tests for account operations would require mocking the HTTP client
220
+
}
+147
src/moderation/claims.rs
+147
src/moderation/claims.rs
···
1
+
use miette::{IntoDiagnostic, Result};
2
+
use redis::AsyncCommands;
3
+
use tracing::debug;
4
+
5
+
/// Redis key prefix for moderation claims
6
+
const CLAIMS_PREFIX: &str = "mod:claims";
7
+
8
+
/// Redis key prefix for label claims
9
+
const LABEL_PREFIX: &str = "mod:labels";
10
+
11
+
/// Default TTL for claims (24 hours)
12
+
const DEFAULT_CLAIM_TTL: u64 = 86400;
13
+
14
+
/// Check if a moderation action has already been claimed
15
+
pub async fn has_claim(
16
+
redis: &mut redis::aio::MultiplexedConnection,
17
+
action_type: &str,
18
+
subject: &str,
19
+
label: &str,
20
+
) -> Result<bool> {
21
+
let key = format!("{}:{}:{}:{}", CLAIMS_PREFIX, action_type, subject, label);
22
+
let exists: bool = redis.exists(&key).await.into_diagnostic()?;
23
+
Ok(exists)
24
+
}
25
+
26
+
/// Create a claim for a moderation action
27
+
pub async fn create_claim(
28
+
redis: &mut redis::aio::MultiplexedConnection,
29
+
action_type: &str,
30
+
subject: &str,
31
+
label: &str,
32
+
ttl: Option<u64>,
33
+
) -> Result<()> {
34
+
let key = format!("{}:{}:{}:{}", CLAIMS_PREFIX, action_type, subject, label);
35
+
let ttl_seconds = ttl.unwrap_or(DEFAULT_CLAIM_TTL);
36
+
37
+
// Set with expiration
38
+
let _: () = redis
39
+
.set_ex(&key, "1", ttl_seconds)
40
+
.await
41
+
.into_diagnostic()?;
42
+
43
+
debug!(
44
+
"Created claim: action={}, subject={}, label={}, ttl={}s",
45
+
action_type, subject, label, ttl_seconds
46
+
);
47
+
48
+
Ok(())
49
+
}
50
+
51
+
/// Remove a claim for a moderation action
52
+
pub async fn remove_claim(
53
+
redis: &mut redis::aio::MultiplexedConnection,
54
+
action_type: &str,
55
+
subject: &str,
56
+
label: &str,
57
+
) -> Result<()> {
58
+
let key = format!("{}:{}:{}:{}", CLAIMS_PREFIX, action_type, subject, label);
59
+
let _: () = redis.del(&key).await.into_diagnostic()?;
60
+
61
+
debug!(
62
+
"Removed claim: action={}, subject={}, label={}",
63
+
action_type, subject, label
64
+
);
65
+
66
+
Ok(())
67
+
}
68
+
69
+
/// Check if a label has already been applied
70
+
pub async fn has_label(
71
+
redis: &mut redis::aio::MultiplexedConnection,
72
+
subject: &str,
73
+
label: &str,
74
+
) -> Result<bool> {
75
+
let key = format!("{}:{}:{}", LABEL_PREFIX, subject, label);
76
+
let exists: bool = redis.exists(&key).await.into_diagnostic()?;
77
+
Ok(exists)
78
+
}
79
+
80
+
/// Record that a label has been applied
81
+
pub async fn set_label(
82
+
redis: &mut redis::aio::MultiplexedConnection,
83
+
subject: &str,
84
+
label: &str,
85
+
ttl: Option<u64>,
86
+
) -> Result<()> {
87
+
let key = format!("{}:{}:{}", LABEL_PREFIX, subject, label);
88
+
let ttl_seconds = ttl.unwrap_or(DEFAULT_CLAIM_TTL);
89
+
90
+
let _: () = redis
91
+
.set_ex(&key, "1", ttl_seconds)
92
+
.await
93
+
.into_diagnostic()?;
94
+
95
+
debug!(
96
+
"Set label: subject={}, label={}, ttl={}s",
97
+
subject, label, ttl_seconds
98
+
);
99
+
100
+
Ok(())
101
+
}
102
+
103
+
/// Remove a label record
104
+
pub async fn remove_label(
105
+
redis: &mut redis::aio::MultiplexedConnection,
106
+
subject: &str,
107
+
label: &str,
108
+
) -> Result<()> {
109
+
let key = format!("{}:{}:{}", LABEL_PREFIX, subject, label);
110
+
let _: () = redis.del(&key).await.into_diagnostic()?;
111
+
112
+
debug!("Removed label: subject={}, label={}", subject, label);
113
+
114
+
Ok(())
115
+
}
116
+
117
+
/// Claim a post report action (returns true if claimed successfully, false if already claimed)
118
+
pub async fn claim_post_report(
119
+
redis: &mut redis::aio::MultiplexedConnection,
120
+
post_uri: &str,
121
+
label: &str,
122
+
) -> Result<bool> {
123
+
if has_claim(redis, "report_post", post_uri, label).await? {
124
+
return Ok(false);
125
+
}
126
+
create_claim(redis, "report_post", post_uri, label, None).await?;
127
+
Ok(true)
128
+
}
129
+
130
+
/// Claim an account report action (returns true if claimed successfully, false if already claimed)
131
+
pub async fn claim_account_report(
132
+
redis: &mut redis::aio::MultiplexedConnection,
133
+
did: &str,
134
+
label: &str,
135
+
) -> Result<bool> {
136
+
if has_claim(redis, "report_account", did, label).await? {
137
+
return Ok(false);
138
+
}
139
+
create_claim(redis, "report_account", did, label, None).await?;
140
+
Ok(true)
141
+
}
142
+
143
+
#[cfg(test)]
144
+
mod tests {
145
+
// Note: These are integration tests that require a running Redis instance
146
+
// Run with: cargo test --test moderation_claims -- --ignored
147
+
}
+4
src/moderation/mod.rs
+4
src/moderation/mod.rs
+276
src/moderation/post.rs
+276
src/moderation/post.rs
···
1
+
use jacquard::client::{Agent, MemoryCredentialSession};
2
+
use jacquard_api::com_atproto::admin::RepoRef;
3
+
use jacquard_api::com_atproto::moderation::ReasonType;
4
+
use jacquard_api::com_atproto::repo::strong_ref::StrongRef;
5
+
use jacquard_api::tools_ozone::moderation::emit_event::{
6
+
EmitEvent, EmitEventEvent, EmitEventSubject,
7
+
};
8
+
use jacquard_api::tools_ozone::moderation::{
9
+
ModEventLabel, ModEventReport, ModEventTakedown, ModTool,
10
+
};
11
+
use jacquard_common::CowStr;
12
+
use jacquard_common::types::string::{AtUri, Cid, Did};
13
+
use jacquard_common::types::value::{Data, Object};
14
+
use jacquard_common::xrpc::{CallOptions, XrpcClient};
15
+
use miette::{IntoDiagnostic, Result};
16
+
use tracing::{debug, info};
17
+
18
+
use jacquard_common::smol_str::SmolStr;
19
+
use std::collections::BTreeMap;
20
+
21
+
use crate::config::Config;
22
+
use crate::moderation::rate_limiter::RateLimiter;
23
+
24
+
/// Label a post with a specific label via Ozone moderation API
25
+
pub async fn label_post(
26
+
agent: &Agent<MemoryCredentialSession>,
27
+
config: &Config,
28
+
rate_limiter: &RateLimiter,
29
+
post_uri: &str,
30
+
post_cid: &str,
31
+
label_val: &str,
32
+
check_comment: &str,
33
+
phash: &str,
34
+
created_by: &str,
35
+
) -> Result<()> {
36
+
// Wait for rate limiter before making request
37
+
rate_limiter.wait().await;
38
+
39
+
info!("Labeling post {} with label: {}", post_uri, label_val);
40
+
41
+
let timestamp = chrono::Utc::now().to_rfc3339();
42
+
let comment = format!("{}: {}", timestamp, check_comment);
43
+
44
+
// Build mod_tool meta
45
+
let mut meta_map = BTreeMap::new();
46
+
meta_map.insert(
47
+
SmolStr::new("externalUrl"),
48
+
format!("https://pdsls.dev/{}", post_uri).into(),
49
+
);
50
+
meta_map.insert(SmolStr::new("phash"), phash.into());
51
+
52
+
// Create moderation label event using jacquard-api types
53
+
let event = EmitEvent::new()
54
+
.created_by(Did::new(created_by).into_diagnostic()?)
55
+
.event(EmitEventEvent::ModEventLabel(Box::new(
56
+
ModEventLabel::builder()
57
+
.create_label_vals(vec![CowStr::from(label_val)])
58
+
.negate_label_vals(vec![])
59
+
.comment(CowStr::from(comment))
60
+
.build(),
61
+
)))
62
+
.subject(EmitEventSubject::StrongRef(Box::new(
63
+
StrongRef::builder()
64
+
.uri(AtUri::new(post_uri).into_diagnostic()?)
65
+
.cid(Cid::str(post_cid))
66
+
.build(),
67
+
)))
68
+
.mod_tool(ModTool {
69
+
name: CowStr::from("skywatch/skywatch-phash-rs"),
70
+
meta: Some(Data::Object(Object::from(meta_map))),
71
+
extra_data: BTreeMap::new(),
72
+
})
73
+
.build();
74
+
75
+
// Build call options with proxy headers
76
+
let opts = CallOptions {
77
+
auth: None, // Agent handles auth automatically
78
+
atproto_proxy: Some(CowStr::from(format!(
79
+
"{}#atproto_labeler",
80
+
config.moderation.labeler_did
81
+
))),
82
+
atproto_accept_labelers: Some(vec![CowStr::from(
83
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
84
+
)]),
85
+
extra_headers: vec![],
86
+
};
87
+
88
+
// Send request via jacquard agent
89
+
let _response = agent.send_with_opts(event, opts).await.into_diagnostic()?;
90
+
91
+
debug!("Successfully labeled post: {}", post_uri);
92
+
93
+
Ok(())
94
+
}
95
+
96
+
/// Report a post to ozone moderation
97
+
pub async fn report_post(
98
+
agent: &Agent<MemoryCredentialSession>,
99
+
config: &Config,
100
+
rate_limiter: &RateLimiter,
101
+
post_uri: &str,
102
+
_post_cid: &str,
103
+
reason: ReasonType<'static>,
104
+
check_comment: &str,
105
+
phash: &str,
106
+
created_by: &str,
107
+
) -> Result<()> {
108
+
// Wait for rate limiter before making request
109
+
rate_limiter.wait().await;
110
+
111
+
info!("Reporting post {} to ozone: {:?}", post_uri, reason);
112
+
113
+
let timestamp = chrono::Utc::now().to_rfc3339();
114
+
let comment = format!("{}: {}", timestamp, check_comment);
115
+
116
+
// Extract DID from URI
117
+
let did_str = extract_did_from_uri(post_uri)?;
118
+
119
+
// Build mod_tool meta
120
+
let mut meta_map = BTreeMap::new();
121
+
meta_map.insert(
122
+
SmolStr::new("externalUrl"),
123
+
format!("https://pdsls.dev/{}", post_uri).into(),
124
+
);
125
+
meta_map.insert(SmolStr::new("phash"), phash.into());
126
+
127
+
// Create moderation report event using jacquard-api types
128
+
let event = EmitEvent::new()
129
+
.created_by(Did::new(created_by).into_diagnostic()?)
130
+
.event(EmitEventEvent::ModEventReport(Box::new(
131
+
ModEventReport::builder()
132
+
.report_type(reason)
133
+
.comment(CowStr::from(comment))
134
+
.build(),
135
+
)))
136
+
.subject(EmitEventSubject::RepoRef(Box::new(
137
+
RepoRef::builder()
138
+
.did(Did::new(&did_str).into_diagnostic()?)
139
+
.build(),
140
+
)))
141
+
.subject_blob_cids(vec![])
142
+
.mod_tool(ModTool {
143
+
name: CowStr::from("skywatch/skywatch-phash-rs"),
144
+
meta: Some(Data::Object(Object::from(meta_map))),
145
+
extra_data: BTreeMap::new(),
146
+
})
147
+
.build();
148
+
149
+
// Build call options with proxy headers
150
+
let opts = CallOptions {
151
+
auth: None,
152
+
atproto_proxy: Some(CowStr::from(format!(
153
+
"{}#atproto_labeler",
154
+
config.moderation.labeler_did
155
+
))),
156
+
atproto_accept_labelers: Some(vec![CowStr::from(
157
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
158
+
)]),
159
+
extra_headers: vec![],
160
+
};
161
+
162
+
// Send request via jacquard agent
163
+
let _response = agent.send_with_opts(event, opts).await.into_diagnostic()?;
164
+
165
+
debug!("Successfully reported post: {}", post_uri);
166
+
167
+
Ok(())
168
+
}
169
+
170
+
/// Takedown a post via Ozone moderation API
171
+
pub async fn takedown_post(
172
+
agent: &Agent<MemoryCredentialSession>,
173
+
config: &Config,
174
+
rate_limiter: &RateLimiter,
175
+
post_uri: &str,
176
+
post_cid: &str,
177
+
comment: &str,
178
+
created_by: &str,
179
+
) -> Result<()> {
180
+
// Wait for rate limiter before making request
181
+
rate_limiter.wait().await;
182
+
183
+
info!("Taking down post: {}", post_uri);
184
+
185
+
// Create moderation takedown event using jacquard-api types
186
+
let event = EmitEvent::new()
187
+
.created_by(Did::new(created_by).into_diagnostic()?)
188
+
.event(EmitEventEvent::ModEventTakedown(Box::new(
189
+
ModEventTakedown {
190
+
comment: Some(CowStr::from(comment)),
191
+
..Default::default()
192
+
},
193
+
)))
194
+
.subject(EmitEventSubject::StrongRef(Box::new(
195
+
StrongRef::builder()
196
+
.uri(AtUri::new(post_uri).into_diagnostic()?)
197
+
.cid(Cid::str(post_cid))
198
+
.build(),
199
+
)))
200
+
.build();
201
+
202
+
// Build call options with proxy headers
203
+
let opts = CallOptions {
204
+
auth: None,
205
+
atproto_proxy: Some(CowStr::from(format!(
206
+
"{}#atproto_labeler",
207
+
config.moderation.labeler_did
208
+
))),
209
+
atproto_accept_labelers: Some(vec![CowStr::from(
210
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
211
+
)]),
212
+
extra_headers: vec![],
213
+
};
214
+
215
+
// Send request via jacquard agent
216
+
let _response = agent.send_with_opts(event, opts).await.into_diagnostic()?;
217
+
218
+
debug!("Successfully took down post: {}", post_uri);
219
+
220
+
Ok(())
221
+
}
222
+
223
+
/// Parse an AT URI into its components
224
+
/// Format: at://did:plc:xxx/app.bsky.feed.post/rkey
225
+
fn parse_at_uri(uri: &str) -> Result<(String, String, String)> {
226
+
let uri = uri
227
+
.strip_prefix("at://")
228
+
.ok_or_else(|| miette::miette!("Invalid AT URI format: missing 'at://' prefix"))?;
229
+
230
+
let parts: Vec<&str> = uri.split('/').collect();
231
+
if parts.len() != 3 {
232
+
return Err(miette::miette!(
233
+
"Invalid AT URI format: expected 3 parts, got {}",
234
+
parts.len()
235
+
));
236
+
}
237
+
238
+
Ok((
239
+
parts[0].to_string(), // repo (DID)
240
+
parts[1].to_string(), // collection
241
+
parts[2].to_string(), // rkey
242
+
))
243
+
}
244
+
245
+
/// Extract DID from AT URI
246
+
fn extract_did_from_uri(uri: &str) -> Result<String> {
247
+
let (did, _, _) = parse_at_uri(uri)?;
248
+
Ok(did)
249
+
}
250
+
251
+
#[cfg(test)]
252
+
mod tests {
253
+
use super::*;
254
+
255
+
#[test]
256
+
fn test_parse_at_uri() {
257
+
let uri = "at://did:plc:xyz123/app.bsky.feed.post/abc456";
258
+
let (repo, collection, rkey) = parse_at_uri(uri).unwrap();
259
+
assert_eq!(repo, "did:plc:xyz123");
260
+
assert_eq!(collection, "app.bsky.feed.post");
261
+
assert_eq!(rkey, "abc456");
262
+
}
263
+
264
+
#[test]
265
+
fn test_parse_at_uri_invalid() {
266
+
let uri = "https://example.com/post/123";
267
+
assert!(parse_at_uri(uri).is_err());
268
+
}
269
+
270
+
#[test]
271
+
fn test_extract_did_from_uri() {
272
+
let uri = "at://did:plc:xyz123/app.bsky.feed.post/abc456";
273
+
let did = extract_did_from_uri(uri).unwrap();
274
+
assert_eq!(did, "did:plc:xyz123");
275
+
}
276
+
}
+91
src/moderation/rate_limiter.rs
+91
src/moderation/rate_limiter.rs
···
1
+
use governor::{
2
+
clock::DefaultClock,
3
+
state::{InMemoryState, NotKeyed},
4
+
Quota, RateLimiter as GovernorRateLimiter,
5
+
};
6
+
use std::sync::Arc;
7
+
use std::time::Duration;
8
+
9
+
/// Thread-safe rate limiter for API requests
10
+
#[derive(Clone)]
11
+
pub struct RateLimiter {
12
+
limiter: Arc<GovernorRateLimiter<NotKeyed, InMemoryState, DefaultClock>>,
13
+
}
14
+
15
+
impl RateLimiter {
16
+
/// Create a new rate limiter with the given rate limit in milliseconds
17
+
/// For example, rate_limit_ms = 100 means 100ms minimum between requests (10 requests per second)
18
+
pub fn new(rate_limit_ms: u64) -> Self {
19
+
let duration = if rate_limit_ms == 0 {
20
+
Duration::from_millis(1)
21
+
} else {
22
+
Duration::from_millis(rate_limit_ms)
23
+
};
24
+
25
+
// 1 request per rate_limit_ms duration
26
+
let quota = Quota::with_period(duration).unwrap();
27
+
let limiter = GovernorRateLimiter::direct(quota);
28
+
29
+
Self {
30
+
limiter: Arc::new(limiter),
31
+
}
32
+
}
33
+
34
+
/// Wait until a request can be made according to the rate limit
35
+
pub async fn wait(&self) {
36
+
while self.limiter.check().is_err() {
37
+
tokio::time::sleep(Duration::from_millis(1)).await;
38
+
}
39
+
}
40
+
}
41
+
42
+
#[cfg(test)]
43
+
mod tests {
44
+
use super::*;
45
+
use std::time::Instant;
46
+
47
+
#[tokio::test]
48
+
async fn test_rate_limiter() {
49
+
// 100ms between requests = 10 requests per second
50
+
let limiter = RateLimiter::new(100);
51
+
52
+
let start = Instant::now();
53
+
54
+
// Make 3 requests
55
+
for _ in 0..3 {
56
+
limiter.wait().await;
57
+
}
58
+
59
+
let elapsed = start.elapsed();
60
+
61
+
// Should take at least 200ms (2 intervals between 3 requests)
62
+
assert!(elapsed >= Duration::from_millis(180));
63
+
}
64
+
65
+
#[tokio::test]
66
+
async fn test_rate_limiter_concurrent() {
67
+
// 100ms between requests = 10 requests per second
68
+
let limiter = RateLimiter::new(100);
69
+
70
+
let start = Instant::now();
71
+
72
+
// Spawn 5 concurrent tasks
73
+
let mut handles = vec![];
74
+
for _ in 0..5 {
75
+
let limiter_clone = limiter.clone();
76
+
handles.push(tokio::spawn(async move {
77
+
limiter_clone.wait().await;
78
+
}));
79
+
}
80
+
81
+
// Wait for all to complete
82
+
for handle in handles {
83
+
handle.await.unwrap();
84
+
}
85
+
86
+
let elapsed = start.elapsed();
87
+
88
+
// Should take at least 400ms (4 intervals between 5 requests)
89
+
assert!(elapsed >= Duration::from_millis(380));
90
+
}
91
+
}
+154
src/processor/README.md
+154
src/processor/README.md
···
1
+
# Processor Module
2
+
3
+
## Purpose
4
+
Download images, compute perceptual hashes, and match against known bad image hashes.
5
+
6
+
## Key Components
7
+
8
+
### `phash.rs`
9
+
Perceptual hash computation:
10
+
- **`compute_phash(image_bytes)`** - Calculate 64-bit aHash from image
11
+
- Uses `image_hasher` crate with aHash (average hash) algorithm
12
+
- Returns 16-character hex string (e.g., "e0e0e0e0e0fcfefe")
13
+
- Hash size: 8×8 = 64 bits
14
+
- **`hamming_distance(hash1, hash2)`** - Compare two phashes
15
+
- Counts differing bits between two hashes
16
+
- Returns u32 (0 = identical, 64 = completely different)
17
+
- Used to find "similar" images (threshold typically 3-5)
18
+
19
+
### `matcher.rs`
20
+
Image download and rule matching:
21
+
- **`download_blob(client, config, did, cid)`** - Download image from CDN/PDS
22
+
- Tries CDN first: `https://cdn.bsky.app/img/feed_fullsize/plain/{did}/{cid}@{format}`
23
+
- Attempts common formats: jpeg, png, webp
24
+
- Falls back to PDS: `com.atproto.sync.getBlob`
25
+
- Returns raw image bytes
26
+
- **`load_blob_checks(path)`** - Load rules from JSON file
27
+
- Reads `rules/blobs.json`
28
+
- Deserializes into `Vec<BlobCheck>`
29
+
- **`match_phash(phash, checks, did, threshold)`** - Check if phash matches rules
30
+
- Compares computed phash against all rules
31
+
- Checks hamming distance against threshold
32
+
- Skips DIDs in `ignore_did` list
33
+
- Returns `Some(MatchResult)` on match, `None` otherwise
34
+
35
+
## Perceptual Hashing (aHash)
36
+
37
+
**What is aHash?**
38
+
- Algorithm: Average Hash
39
+
- Process:
40
+
1. Resize image to 8×8 pixels
41
+
2. Convert to grayscale
42
+
3. Calculate average brightness
43
+
4. For each pixel: bit = 1 if brighter than average, 0 if darker
44
+
5. Concatenate 64 bits into hash
45
+
46
+
**Why aHash?**
47
+
- Fast to compute (~50ms)
48
+
- Robust to resizing, cropping, minor edits
49
+
- Not robust to rotation, color changes (by design)
50
+
- Good for finding exact or near-exact reposts
51
+
52
+
**Example:**
53
+
```
54
+
Original image: 1920×1080 JPEG
55
+
→ Resize: 8×8 grayscale
56
+
→ Average: 128
57
+
→ Bits: [1,0,1,1,0,0,1,1, ...]
58
+
→ Hash: "e0e0e0e0e0fcfefe"
59
+
```
60
+
61
+
## Hamming Distance
62
+
63
+
Counts bit differences between two hashes:
64
+
```
65
+
Hash A: 1010
66
+
Hash B: 1100
67
+
XOR: 0110 (2 bits differ)
68
+
Hamming: 2
69
+
```
70
+
71
+
**Thresholds:**
72
+
- 0 - Exact match (same image)
73
+
- 1-3 - Very similar (minor edits, recompression)
74
+
- 4-8 - Similar (cropping, watermarks)
75
+
- 9+ - Different images
76
+
77
+
## CDN-First Download Strategy
78
+
79
+
**Why CDN first?**
80
+
- Faster (geographically distributed)
81
+
- Cached (reduces PDS load)
82
+
- Free (no quota concerns)
83
+
84
+
**Why try multiple formats?**
85
+
- CDN URL requires format extension (@jpeg, @png, @webp)
86
+
- Blob CID doesn't tell us the format
87
+
- Try common formats until one works
88
+
89
+
**Flow:**
90
+
```
91
+
1. Try: cdn.bsky.app/.../cid@jpeg
92
+
→ 404
93
+
2. Try: cdn.bsky.app/.../cid@png
94
+
→ 200 OK (download)
95
+
3. Return bytes
96
+
```
97
+
98
+
**Fallback to PDS:**
99
+
- If all CDN attempts fail
100
+
- Uses getBlob API (format-agnostic)
101
+
- Slower but always works
102
+
103
+
## Rule Matching Logic
104
+
105
+
```rust
106
+
for check in blob_checks {
107
+
// Skip ignored DIDs
108
+
if check.ignore_did.contains(did) {
109
+
continue;
110
+
}
111
+
112
+
// Check each known bad hash
113
+
for bad_hash in check.phashes {
114
+
let distance = hamming_distance(computed_hash, bad_hash);
115
+
let threshold = check.hamming_threshold.unwrap_or(default);
116
+
117
+
if distance <= threshold {
118
+
return Some(MatchResult {
119
+
matched_check: check,
120
+
matched_phash: bad_hash,
121
+
hamming_distance: distance,
122
+
phash: computed_hash,
123
+
});
124
+
}
125
+
}
126
+
}
127
+
```
128
+
129
+
## Performance
130
+
131
+
**Typical timings:**
132
+
- Download (cache hit): ~1ms
133
+
- Download (CDN): ~100-200ms
134
+
- Download (PDS fallback): ~200-500ms
135
+
- Compute phash: ~20-50ms
136
+
- Match against 100 rules: ~0.1ms
137
+
138
+
**Optimization:**
139
+
- Cache phashes by CID (avoid recomputation)
140
+
- CDN preferred over PDS (faster)
141
+
- Hamming distance is bitwise XOR (very fast)
142
+
143
+
## Dependencies
144
+
145
+
- `reqwest::Client` - HTTP downloads
146
+
- `image` - Image loading/decoding
147
+
- `image_hasher` - Phash computation
148
+
- `serde_json` - Rule parsing
149
+
150
+
## Related Modules
151
+
152
+
- Used by: `queue/worker.rs` - Downloads and matches blobs
153
+
- Returns: `types::MatchResult` - What matched and why
154
+
- Uses: `types::BlobCheck` - Rule definitions
src/processor/fetcher.rs
src/processor/fetcher.rs
This is a binary file and will not be displayed.
+295
src/processor/matcher.rs
+295
src/processor/matcher.rs
···
1
+
use miette::{IntoDiagnostic, Result};
2
+
use reqwest::Client;
3
+
use std::path::Path;
4
+
use tracing::{debug, info, warn};
5
+
6
+
use crate::config::Config;
7
+
use crate::processor::phash;
8
+
use crate::types::{BlobCheck, BlobReference, ImageJob, MatchResult};
9
+
10
+
/// Load blob checks from a JSON file
11
+
pub async fn load_blob_checks(path: &Path) -> Result<Vec<BlobCheck>> {
12
+
let contents = tokio::fs::read_to_string(path).await.into_diagnostic()?;
13
+
let checks: Vec<BlobCheck> = serde_json::from_str(&contents).into_diagnostic()?;
14
+
info!("Loaded {} blob checks from {:?}", checks.len(), path);
15
+
Ok(checks)
16
+
}
17
+
18
+
/// Download a blob from the Bluesky CDN, falling back to PDS if necessary
19
+
pub async fn download_blob(
20
+
client: &Client,
21
+
config: &Config,
22
+
did: &str,
23
+
cid: &str,
24
+
) -> Result<Vec<u8>> {
25
+
// Try CDN first - attempt common image formats
26
+
for format in ["jpeg", "png", "webp"] {
27
+
let cdn_url = format!(
28
+
"https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}",
29
+
did, cid, format
30
+
);
31
+
32
+
debug!("Trying CDN download: {}", cdn_url);
33
+
34
+
match client.get(&cdn_url).send().await {
35
+
Ok(response) if response.status().is_success() => {
36
+
debug!("Successfully downloaded from CDN: did={}, cid={}", did, cid);
37
+
let bytes = response.bytes().await.into_diagnostic()?;
38
+
return Ok(bytes.to_vec());
39
+
}
40
+
Ok(response) => {
41
+
debug!("CDN returned status {}, trying next format", response.status());
42
+
}
43
+
Err(e) => {
44
+
debug!("CDN request failed: {}, trying next format", e);
45
+
}
46
+
}
47
+
}
48
+
49
+
// Fall back to PDS if CDN fails
50
+
warn!("CDN failed for did={}, cid={}, falling back to PDS", did, cid);
51
+
52
+
let pds_url = format!(
53
+
"{}/xrpc/com.atproto.sync.getBlob?did={}&cid={}",
54
+
config.pds.endpoint, did, cid
55
+
);
56
+
57
+
debug!("Downloading from PDS: {}", pds_url);
58
+
59
+
let response = client
60
+
.get(&pds_url)
61
+
.send()
62
+
.await
63
+
.into_diagnostic()?
64
+
.error_for_status()
65
+
.into_diagnostic()?;
66
+
67
+
let bytes = response.bytes().await.into_diagnostic()?;
68
+
Ok(bytes.to_vec())
69
+
}
70
+
71
+
/// Match a computed phash against blob checks
72
+
pub fn match_phash(
73
+
phash: &str,
74
+
blob_checks: &[BlobCheck],
75
+
did: &str,
76
+
default_threshold: u32,
77
+
) -> Option<MatchResult> {
78
+
for check in blob_checks {
79
+
// Check if DID is in ignore list
80
+
if let Some(ignore_list) = &check.ignore_did {
81
+
if ignore_list.contains(&did.to_string()) {
82
+
debug!("Skipping check '{}' for ignored DID: {}", check.label, did);
83
+
continue;
84
+
}
85
+
}
86
+
87
+
let threshold = check.hamming_threshold.unwrap_or(default_threshold);
88
+
89
+
// Check each phash in the check
90
+
for check_phash in &check.phashes {
91
+
match phash::hamming_distance(phash, check_phash) {
92
+
Ok(distance) => {
93
+
if distance <= threshold {
94
+
info!(
95
+
"Match found! label={}, distance={}, threshold={}",
96
+
check.label, distance, threshold
97
+
);
98
+
return Some(MatchResult {
99
+
phash: phash.to_string(),
100
+
matched_check: check.clone(),
101
+
matched_phash: check_phash.clone(),
102
+
hamming_distance: distance,
103
+
});
104
+
}
105
+
}
106
+
Err(e) => {
107
+
warn!("Failed to compute hamming distance: {}", e);
108
+
continue;
109
+
}
110
+
}
111
+
}
112
+
}
113
+
114
+
None
115
+
}
116
+
117
+
/// Process a single blob reference
118
+
pub async fn process_blob(
119
+
client: &Client,
120
+
config: &Config,
121
+
blob_checks: &[BlobCheck],
122
+
did: &str,
123
+
blob: &BlobReference,
124
+
) -> Result<Option<MatchResult>> {
125
+
// Download the blob
126
+
let image_bytes = download_blob(client, config, did, &blob.cid).await?;
127
+
128
+
// Compute phash
129
+
let phash = phash::compute_phash(&image_bytes)?;
130
+
debug!("Computed phash for blob {}: {}", blob.cid, phash);
131
+
132
+
// Match against checks
133
+
let match_result = match_phash(&phash, blob_checks, did, config.phash.default_hamming_threshold);
134
+
135
+
Ok(match_result)
136
+
}
137
+
138
+
/// Process an image job - check all blobs and return matches
139
+
pub async fn process_image_job(
140
+
client: &Client,
141
+
config: &Config,
142
+
blob_checks: &[BlobCheck],
143
+
job: &ImageJob,
144
+
) -> Result<Vec<MatchResult>> {
145
+
info!(
146
+
"Processing job: post={}, blobs={}",
147
+
job.post_uri,
148
+
job.blobs.len()
149
+
);
150
+
151
+
let mut matches = Vec::new();
152
+
153
+
for blob in &job.blobs {
154
+
match process_blob(client, config, blob_checks, &job.post_did, blob).await {
155
+
Ok(Some(result)) => {
156
+
matches.push(result);
157
+
}
158
+
Ok(None) => {
159
+
debug!("No match for blob: {}", blob.cid);
160
+
}
161
+
Err(e) => {
162
+
warn!("Error processing blob {}: {}", blob.cid, e);
163
+
// Continue processing other blobs
164
+
}
165
+
}
166
+
}
167
+
168
+
if !matches.is_empty() {
169
+
info!(
170
+
"Found {} match(es) for post: {}",
171
+
matches.len(),
172
+
job.post_uri
173
+
);
174
+
}
175
+
176
+
Ok(matches)
177
+
}
178
+
179
+
#[cfg(test)]
180
+
mod tests {
181
+
use super::*;
182
+
183
+
#[test]
184
+
fn test_match_phash_exact() {
185
+
let checks = vec![BlobCheck {
186
+
phashes: vec!["deadbeefdeadbeef".to_string()],
187
+
label: "test-label".to_string(),
188
+
comment: "Test".to_string(),
189
+
report_acct: false,
190
+
label_acct: false,
191
+
report_post: true,
192
+
to_label: true,
193
+
takedown_post: false,
194
+
takedown_acct: false,
195
+
hamming_threshold: Some(3),
196
+
description: None,
197
+
ignore_did: None,
198
+
}];
199
+
200
+
let result = match_phash("deadbeefdeadbeef", &checks, "did:plc:test", 3);
201
+
assert!(result.is_some());
202
+
assert_eq!(result.unwrap().hamming_distance, 0);
203
+
}
204
+
205
+
#[test]
206
+
fn test_match_phash_within_threshold() {
207
+
let checks = vec![BlobCheck {
208
+
phashes: vec!["deadbeefdeadbeef".to_string()],
209
+
label: "test-label".to_string(),
210
+
comment: "Test".to_string(),
211
+
report_acct: false,
212
+
label_acct: false,
213
+
report_post: true,
214
+
to_label: true,
215
+
takedown_post: false,
216
+
takedown_acct: false,
217
+
hamming_threshold: Some(3),
218
+
description: None,
219
+
ignore_did: None,
220
+
}];
221
+
222
+
// deadbeefdeadbeef vs deadbeefdeadbeee = 1 bit difference in last nibble
223
+
let result = match_phash("deadbeefdeadbeee", &checks, "did:plc:test", 3);
224
+
assert!(result.is_some());
225
+
assert_eq!(result.unwrap().hamming_distance, 1);
226
+
}
227
+
228
+
#[test]
229
+
fn test_match_phash_exceeds_threshold() {
230
+
let checks = vec![BlobCheck {
231
+
phashes: vec!["deadbeefdeadbeef".to_string()],
232
+
label: "test-label".to_string(),
233
+
comment: "Test".to_string(),
234
+
report_acct: false,
235
+
label_acct: false,
236
+
report_post: true,
237
+
to_label: true,
238
+
takedown_post: false,
239
+
takedown_acct: false,
240
+
hamming_threshold: Some(1),
241
+
description: None,
242
+
ignore_did: None,
243
+
}];
244
+
245
+
// More than 1 bit difference
246
+
let result = match_phash("deadbeefdeadbee0", &checks, "did:plc:test", 1);
247
+
assert!(result.is_none());
248
+
}
249
+
250
+
#[test]
251
+
fn test_match_phash_ignored_did() {
252
+
let checks = vec![BlobCheck {
253
+
phashes: vec!["deadbeefdeadbeef".to_string()],
254
+
label: "test-label".to_string(),
255
+
comment: "Test".to_string(),
256
+
report_acct: false,
257
+
label_acct: false,
258
+
report_post: true,
259
+
to_label: true,
260
+
takedown_post: false,
261
+
takedown_acct: false,
262
+
hamming_threshold: Some(3),
263
+
description: None,
264
+
ignore_did: Some(vec!["did:plc:ignored".to_string()]),
265
+
}];
266
+
267
+
let result = match_phash("deadbeefdeadbeef", &checks, "did:plc:ignored", 3);
268
+
assert!(result.is_none());
269
+
}
270
+
271
+
#[tokio::test]
272
+
async fn test_load_real_rules() {
273
+
let path = std::path::Path::new("rules/blobs.json");
274
+
if !path.exists() {
275
+
// Skip test if rules file doesn't exist
276
+
return;
277
+
}
278
+
279
+
let result = load_blob_checks(path).await;
280
+
assert!(result.is_ok());
281
+
282
+
let checks = result.unwrap();
283
+
assert!(!checks.is_empty());
284
+
285
+
// Verify first check has expected fields
286
+
let first = &checks[0];
287
+
assert!(!first.phashes.is_empty());
288
+
assert!(!first.label.is_empty());
289
+
290
+
// Check that ignoreDID alias works
291
+
if let Some(ignore_list) = &first.ignore_did {
292
+
assert!(!ignore_list.is_empty());
293
+
}
294
+
}
295
+
}
+5
src/processor/mod.rs
+5
src/processor/mod.rs
+165
src/processor/phash.rs
+165
src/processor/phash.rs
···
1
+
use image::DynamicImage;
2
+
use image_hasher::{HashAlg, HasherConfig, ImageHash};
3
+
use miette::{Diagnostic, IntoDiagnostic, Result};
4
+
use thiserror::Error;
5
+
6
+
#[derive(Debug, Error, Diagnostic)]
7
+
pub enum PhashError {
8
+
#[error("Failed to decode image: {0}")]
9
+
ImageDecodeError(String),
10
+
11
+
#[error("Failed to compute hash: {0}")]
12
+
HashComputeError(String),
13
+
14
+
#[error("Invalid hash format: {0}")]
15
+
InvalidHashFormat(String),
16
+
}
17
+
18
+
/// Compute perceptual hash for an image using average hash (aHash) algorithm
19
+
///
20
+
/// This matches the TypeScript implementation:
21
+
/// 1. Resize to 8x8 (64 pixels)
22
+
/// 2. Convert to grayscale
23
+
/// 3. Compute average pixel value
24
+
/// 4. Create 64-bit binary: 1 if pixel > avg, 0 otherwise
25
+
/// 5. Convert to hex string (16 chars)
26
+
pub fn compute_phash(image_bytes: &[u8]) -> Result<String> {
27
+
// Decode image from bytes
28
+
let img = image::load_from_memory(image_bytes)
29
+
.into_diagnostic()
30
+
.map_err(|e| PhashError::ImageDecodeError(e.to_string()))?;
31
+
32
+
compute_phash_from_image(&img)
33
+
}
34
+
35
+
/// Compute perceptual hash from a DynamicImage
36
+
pub fn compute_phash_from_image(img: &DynamicImage) -> Result<String> {
37
+
// Configure hasher with aHash (Mean) algorithm and 8x8 size
38
+
let hasher = HasherConfig::new()
39
+
.hash_alg(HashAlg::Mean) // average hash
40
+
.hash_size(8, 8) // 64 bits
41
+
.to_hasher();
42
+
43
+
// Compute hash
44
+
let hash = hasher.hash_image(img);
45
+
46
+
// Convert to hex string
47
+
hash_to_hex(&hash)
48
+
}
49
+
50
+
/// Convert ImageHash to hex string format (16 chars, matching TS output)
51
+
fn hash_to_hex(hash: &ImageHash) -> Result<String> {
52
+
// Get hash bytes
53
+
let bytes = hash.as_bytes();
54
+
55
+
// Convert to hex string
56
+
let hex = bytes
57
+
.iter()
58
+
.map(|b| format!("{:02x}", b))
59
+
.collect::<String>();
60
+
61
+
// Ensure it's 16 characters (64 bits = 8 bytes = 16 hex chars)
62
+
if hex.len() != 16 {
63
+
return Err(PhashError::InvalidHashFormat(format!(
64
+
"Expected 16 hex characters, got {}",
65
+
hex.len()
66
+
))
67
+
.into());
68
+
}
69
+
70
+
Ok(hex)
71
+
}
72
+
73
+
/// Compute hamming distance between two phash hex strings
74
+
///
75
+
/// Uses Brian Kernighan's algorithm to count set bits
76
+
pub fn hamming_distance(hash1: &str, hash2: &str) -> Result<u32> {
77
+
// Validate input lengths
78
+
if hash1.len() != 16 || hash2.len() != 16 {
79
+
return Err(PhashError::InvalidHashFormat(format!(
80
+
"Hashes must be 16 hex characters, got {} and {}",
81
+
hash1.len(),
82
+
hash2.len()
83
+
))
84
+
.into());
85
+
}
86
+
87
+
// Parse hex strings to u64
88
+
let a = u64::from_str_radix(hash1, 16)
89
+
.into_diagnostic()
90
+
.map_err(|_| PhashError::InvalidHashFormat(format!("Invalid hex string: {}", hash1)))?;
91
+
92
+
let b = u64::from_str_radix(hash2, 16)
93
+
.into_diagnostic()
94
+
.map_err(|_| PhashError::InvalidHashFormat(format!("Invalid hex string: {}", hash2)))?;
95
+
96
+
// XOR to find differing bits
97
+
let xor = a ^ b;
98
+
99
+
// Count set bits using Brian Kernighan's algorithm
100
+
let mut count = 0u32;
101
+
let mut n = xor;
102
+
while n > 0 {
103
+
count += 1;
104
+
n &= n - 1; // clear the lowest set bit
105
+
}
106
+
107
+
Ok(count)
108
+
}
109
+
110
+
#[cfg(test)]
111
+
mod tests {
112
+
use super::*;
113
+
114
+
#[test]
115
+
fn test_hamming_distance_identical() {
116
+
let hash = "e0e0e0e0e0fcfefe";
117
+
let distance = hamming_distance(hash, hash).unwrap();
118
+
assert_eq!(distance, 0);
119
+
}
120
+
121
+
#[test]
122
+
fn test_hamming_distance_different() {
123
+
let hash1 = "0000000000000000";
124
+
let hash2 = "ffffffffffffffff";
125
+
let distance = hamming_distance(hash1, hash2).unwrap();
126
+
assert_eq!(distance, 64); // all bits different
127
+
}
128
+
129
+
#[test]
130
+
fn test_hamming_distance_one_bit() {
131
+
let hash1 = "0000000000000000";
132
+
let hash2 = "0000000000000001";
133
+
let distance = hamming_distance(hash1, hash2).unwrap();
134
+
assert_eq!(distance, 1);
135
+
}
136
+
137
+
#[test]
138
+
fn test_hamming_distance_invalid_length() {
139
+
let hash1 = "e0e0e0e0e0fcfefe";
140
+
let hash2 = "short";
141
+
let result = hamming_distance(hash1, hash2);
142
+
assert!(result.is_err());
143
+
}
144
+
145
+
#[test]
146
+
fn test_hamming_distance_invalid_hex() {
147
+
let hash1 = "e0e0e0e0e0fcfefe";
148
+
let hash2 = "gggggggggggggggg";
149
+
let result = hamming_distance(hash1, hash2);
150
+
assert!(result.is_err());
151
+
}
152
+
153
+
#[test]
154
+
fn test_phash_format() {
155
+
// Create a simple test image (1x1 black pixel)
156
+
let img = DynamicImage::new_luma8(1, 1);
157
+
let hash = compute_phash_from_image(&img).unwrap();
158
+
159
+
// Should be 16 hex characters
160
+
assert_eq!(hash.len(), 16);
161
+
162
+
// Should be valid hex
163
+
u64::from_str_radix(&hash, 16).unwrap();
164
+
}
165
+
}
+187
src/queue/README.md
+187
src/queue/README.md
···
1
+
# Queue Module
2
+
3
+
## Purpose
4
+
Redis-backed job queue with concurrent worker pool for processing image moderation jobs.
5
+
6
+
## Key Components
7
+
8
+
### `redis_queue.rs`
9
+
- **`JobQueue`** - Redis-backed FIFO queue
10
+
- **`push(job)`** - Add job to main queue (`queue:main`)
11
+
- **`pop(timeout)`** - Block-pop next job from queue
12
+
- **`retry(job)`** - Add failed job to retry queue
13
+
- Uses Redis LIST operations (LPUSH/BRPOP)
14
+
- **Dead letter queue** - Jobs that fail after max retries
15
+
- Stored in `queue:dead_letter`
16
+
- Can be inspected/reprocessed manually
17
+
18
+
### `worker.rs`
19
+
Main worker pool implementation:
20
+
- **`WorkerPool`** - Manages concurrent job processing
21
+
- Semaphore-controlled concurrency (max N workers)
22
+
- Shared rate limiter across all workers
23
+
- Each worker gets own Redis connection
24
+
- **`start(queue, cache, shutdown_rx)`** - Main event loop
25
+
- Pops jobs from queue
26
+
- Spawns worker tasks up to concurrency limit
27
+
- Handles graceful shutdown
28
+
- **`process_job()`** - Process single job
29
+
1. Check cache for each blob
30
+
2. Download and compute phash if needed
31
+
3. Match against rules
32
+
4. Take moderation actions on matches
33
+
- **`process_job_blobs()`** - Download/compute phashes for all blobs
34
+
- **`take_moderation_action()`** - Execute configured actions
35
+
36
+
## Job Flow
37
+
38
+
```
39
+
Jetstream → JobQueue.push() → Redis queue:main
40
+
↓
41
+
WorkerPool.start()
42
+
↓
43
+
Semaphore.acquire() (limit concurrency)
44
+
↓
45
+
Spawn worker task
46
+
↓
47
+
process_job()
48
+
↓
49
+
┌──────────────┴──────────────┐
50
+
↓ ↓
51
+
Success Failure
52
+
↓ ↓
53
+
Complete JobQueue.retry()
54
+
↓
55
+
(job.attempts < max_retries?)
56
+
↓ ↓
57
+
Yes No
58
+
↓ ↓
59
+
queue:main queue:dead_letter
60
+
```
61
+
62
+
## Concurrency Control
63
+
64
+
**Semaphore pattern:**
65
+
```rust
66
+
let semaphore = Arc::new(Semaphore::new(concurrency));
67
+
68
+
loop {
69
+
let permit = semaphore.acquire().await;
70
+
tokio::spawn(async move {
71
+
process_job(...).await;
72
+
drop(permit); // Release permit when done
73
+
});
74
+
}
75
+
```
76
+
77
+
**Why semaphore?**
78
+
- Limits max concurrent jobs (prevents resource exhaustion)
79
+
- Workers block when limit reached
80
+
- Auto-released on task completion (even if panics)
81
+
82
+
**Concurrency setting:**
83
+
- Default: 10 workers
84
+
- Configurable via `PROCESSING_CONCURRENCY`
85
+
- Each worker needs: memory for image, Redis conn, HTTP conn
86
+
87
+
## Retry Logic
88
+
89
+
**Retry conditions:**
90
+
- Image download fails
91
+
- Phash computation fails
92
+
- Moderation API returns error
93
+
- Any error in `process_job()`
94
+
95
+
**Retry mechanism:**
96
+
```rust
97
+
job.attempts += 1;
98
+
if job.attempts < max_retries {
99
+
queue.retry(job).await?; // Back to queue:main
100
+
} else {
101
+
// Move to dead letter queue
102
+
}
103
+
```
104
+
105
+
**Retry delay:**
106
+
- Controlled by `RETRY_DELAY_MS` (default 1000ms)
107
+
- Applied when job is retried
108
+
- Prevents hammering failing resources
109
+
110
+
## Worker Task Lifecycle
111
+
112
+
```
113
+
1. Main loop pops job from queue
114
+
2. Acquire semaphore permit (blocks if at limit)
115
+
3. Clone data for worker (config, client, etc.)
116
+
4. Spawn tokio task with job
117
+
5. Worker creates own Redis connection
118
+
6. Process job (download, compute, match, act)
119
+
7. On success: increment metrics, complete
120
+
8. On failure: retry job, increment failed metrics
121
+
9. Drop permit (allows next worker to start)
122
+
```
123
+
124
+
## Job Processing Details
125
+
126
+
### process_job_blobs()
127
+
For each blob in job:
128
+
1. Check cache: `cache.get(cid)`
129
+
2. If cache hit: use cached phash
130
+
3. If cache miss:
131
+
- Download blob from CDN/PDS
132
+
- Compute phash
133
+
- Store in cache: `cache.set(cid, phash)`
134
+
4. Match phash against rules
135
+
5. Collect all matches
136
+
137
+
### take_moderation_action()
138
+
For each match:
139
+
1. Get access token from agent
140
+
2. Check configured actions (from BlobCheck):
141
+
- `report_post` → `post::report_post()`
142
+
- `to_label` → `post::label_post()`
143
+
- `report_acct` → `account::report_account()`
144
+
- `label_acct` → `account::label_account()`
145
+
3. Use claims to prevent duplicates
146
+
4. Rate limit before each API call
147
+
5. Increment metrics
148
+
149
+
## Redis Queues
150
+
151
+
**Main queue:** `queue:main`
152
+
- All new jobs and retries
153
+
- FIFO order
154
+
- Block-pop with timeout
155
+
156
+
**Dead letter queue:** `queue:dead_letter`
157
+
- Jobs that failed max_retries times
158
+
- Preserved for manual inspection
159
+
- Never auto-retried
160
+
161
+
**Storage format:** JSON-serialized `ImageJob`
162
+
163
+
## Configuration
164
+
165
+
```env
166
+
PROCESSING_CONCURRENCY=10 # Max concurrent workers
167
+
RETRY_ATTEMPTS=3 # Max retries per job
168
+
RETRY_DELAY_MS=1000 # Delay between retries
169
+
REDIS_URL=redis://localhost:6379
170
+
```
171
+
172
+
## Dependencies
173
+
174
+
- `redis` - Queue storage
175
+
- `tokio::sync::Semaphore` - Concurrency control
176
+
- `reqwest::Client` - Image downloads
177
+
- `AgentSession` - Authentication
178
+
- `PhashCache` - Phash caching
179
+
- `Metrics` - Performance tracking
180
+
181
+
## Related Modules
182
+
183
+
- Receives from: `jetstream` - Jobs via mpsc channel
184
+
- Uses: `processor` - Download and match blobs
185
+
- Uses: `moderation` - Take actions on matches
186
+
- Uses: `cache` - Phash caching
187
+
- Uses: `agent` - API authentication
+5
src/queue/mod.rs
+5
src/queue/mod.rs
+160
src/queue/redis_queue.rs
+160
src/queue/redis_queue.rs
···
1
+
use miette::{IntoDiagnostic, Result};
2
+
use redis::AsyncCommands;
3
+
use tracing::{debug, info, warn};
4
+
5
+
use crate::config::Config;
6
+
use crate::types::ImageJob;
7
+
8
+
/// Redis queue names
9
+
const PENDING_QUEUE: &str = "jobs:pending";
10
+
const PROCESSING_QUEUE: &str = "jobs:processing";
11
+
const DEAD_LETTER_QUEUE: &str = "jobs:dead";
12
+
13
+
/// Redis-based job queue for ImageJob processing
14
+
#[derive(Clone)]
15
+
pub struct JobQueue {
16
+
redis: redis::aio::MultiplexedConnection,
17
+
max_retries: u32,
18
+
}
19
+
20
+
impl JobQueue {
21
+
/// Create a new job queue
22
+
pub async fn new(config: &Config) -> Result<Self> {
23
+
info!("Connecting to Redis for job queue: {}", config.redis.url);
24
+
25
+
let client = redis::Client::open(config.redis.url.as_str()).into_diagnostic()?;
26
+
let redis = client
27
+
.get_multiplexed_async_connection()
28
+
.await
29
+
.into_diagnostic()?;
30
+
31
+
info!("Job queue connected to Redis");
32
+
33
+
Ok(Self {
34
+
redis,
35
+
max_retries: config.processing.retry_attempts,
36
+
})
37
+
}
38
+
39
+
/// Push a job to the pending queue
40
+
pub async fn push(&mut self, job: &ImageJob) -> Result<()> {
41
+
let job_json = serde_json::to_string(job).into_diagnostic()?;
42
+
43
+
let _: () = self
44
+
.redis
45
+
.rpush(PENDING_QUEUE, &job_json)
46
+
.await
47
+
.into_diagnostic()?;
48
+
49
+
debug!("Pushed job to queue: {}", job.post_uri);
50
+
51
+
Ok(())
52
+
}
53
+
54
+
/// Pop a job from the pending queue (blocking with timeout)
55
+
pub async fn pop(&mut self, timeout_secs: usize) -> Result<Option<ImageJob>> {
56
+
let result: Option<Vec<String>> = self
57
+
.redis
58
+
.blpop(PENDING_QUEUE, timeout_secs as f64)
59
+
.await
60
+
.into_diagnostic()?;
61
+
62
+
match result {
63
+
Some(items) => {
64
+
// blpop returns [key, value]
65
+
if items.len() >= 2 {
66
+
let job_json = &items[1];
67
+
let job: ImageJob = serde_json::from_str(job_json).into_diagnostic()?;
68
+
debug!("Popped job from queue: {}", job.post_uri);
69
+
Ok(Some(job))
70
+
} else {
71
+
Ok(None)
72
+
}
73
+
}
74
+
None => Ok(None),
75
+
}
76
+
}
77
+
78
+
/// Retry a failed job (increment attempts and re-queue)
79
+
pub async fn retry(&mut self, mut job: ImageJob) -> Result<()> {
80
+
job.attempts += 1;
81
+
82
+
if job.attempts >= self.max_retries {
83
+
warn!(
84
+
"Job exceeded max retries ({}), moving to dead letter queue: {}",
85
+
self.max_retries, job.post_uri
86
+
);
87
+
self.move_to_dead_letter(&job).await?;
88
+
} else {
89
+
info!(
90
+
"Retrying job (attempt {}/{}): {}",
91
+
job.attempts, self.max_retries, job.post_uri
92
+
);
93
+
self.push(&job).await?;
94
+
}
95
+
96
+
Ok(())
97
+
}
98
+
99
+
/// Move a job to the dead letter queue
100
+
async fn move_to_dead_letter(&mut self, job: &ImageJob) -> Result<()> {
101
+
let job_json = serde_json::to_string(job).into_diagnostic()?;
102
+
103
+
let _: () = self
104
+
.redis
105
+
.rpush(DEAD_LETTER_QUEUE, &job_json)
106
+
.await
107
+
.into_diagnostic()?;
108
+
109
+
warn!("Moved job to dead letter queue: {}", job.post_uri);
110
+
111
+
Ok(())
112
+
}
113
+
114
+
/// Get queue statistics
115
+
pub async fn stats(&mut self) -> Result<QueueStats> {
116
+
let pending: usize = self.redis.llen(PENDING_QUEUE).await.into_diagnostic()?;
117
+
let processing: usize = self
118
+
.redis
119
+
.llen(PROCESSING_QUEUE)
120
+
.await
121
+
.into_diagnostic()?;
122
+
let dead: usize = self.redis.llen(DEAD_LETTER_QUEUE).await.into_diagnostic()?;
123
+
124
+
Ok(QueueStats {
125
+
pending,
126
+
processing,
127
+
dead,
128
+
})
129
+
}
130
+
131
+
/// Clear all queues (for testing/maintenance)
132
+
pub async fn clear_all(&mut self) -> Result<()> {
133
+
let _: () = self.redis.del(PENDING_QUEUE).await.into_diagnostic()?;
134
+
let _: () = self.redis.del(PROCESSING_QUEUE).await.into_diagnostic()?;
135
+
let _: () = self
136
+
.redis
137
+
.del(DEAD_LETTER_QUEUE)
138
+
.await
139
+
.into_diagnostic()?;
140
+
141
+
info!("Cleared all job queues");
142
+
143
+
Ok(())
144
+
}
145
+
}
146
+
147
+
#[derive(Debug, Clone)]
148
+
pub struct QueueStats {
149
+
pub pending: usize,
150
+
pub processing: usize,
151
+
pub dead: usize,
152
+
}
153
+
154
+
#[cfg(test)]
155
+
mod tests {
156
+
use super::*;
157
+
158
+
// Note: These are integration tests that require a running Redis instance
159
+
// Run with: cargo test --test queue -- --ignored
160
+
}
+344
src/queue/worker.rs
+344
src/queue/worker.rs
···
1
+
use miette::Result;
2
+
use reqwest::Client;
3
+
use std::sync::Arc;
4
+
use std::time::Duration;
5
+
use tokio::time::sleep;
6
+
use tracing::{error, info};
7
+
use jacquard::client::{Agent, MemoryCredentialSession};
8
+
use jacquard_api::com_atproto::moderation::ReasonType;
9
+
10
+
use crate::agent::AgentSession;
11
+
use crate::cache::PhashCache;
12
+
use crate::config::Config;
13
+
use crate::metrics::Metrics;
14
+
use crate::moderation::{account, claims, post, rate_limiter::RateLimiter};
15
+
use crate::processor::matcher;
16
+
use crate::queue::redis_queue::JobQueue;
17
+
use crate::types::{BlobCheck, ImageJob, MatchResult};
18
+
19
+
/// Worker pool for processing image jobs
20
+
pub struct WorkerPool {
21
+
config: Config,
22
+
client: Client,
23
+
agent: AgentSession,
24
+
blob_checks: Vec<BlobCheck>,
25
+
metrics: Metrics,
26
+
rate_limiter: RateLimiter,
27
+
}
28
+
29
+
impl WorkerPool {
30
+
/// Create a new worker pool
31
+
pub fn new(
32
+
config: Config,
33
+
client: Client,
34
+
agent: AgentSession,
35
+
blob_checks: Vec<BlobCheck>,
36
+
metrics: Metrics,
37
+
) -> Self {
38
+
let rate_limiter = RateLimiter::new(config.moderation.rate_limit);
39
+
40
+
Self {
41
+
config,
42
+
client,
43
+
agent,
44
+
blob_checks,
45
+
metrics,
46
+
rate_limiter,
47
+
}
48
+
}
49
+
50
+
/// Start the worker pool - processes jobs sequentially
51
+
/// Concurrency is achieved by running multiple instances of this concurrently
52
+
pub async fn start(
53
+
&self,
54
+
mut queue: JobQueue,
55
+
mut cache: PhashCache,
56
+
mut shutdown_rx: tokio::sync::broadcast::Receiver<()>,
57
+
) -> Result<()> {
58
+
loop {
59
+
tokio::select! {
60
+
_ = shutdown_rx.recv() => {
61
+
info!("Worker shutting down");
62
+
break;
63
+
}
64
+
65
+
job_result = queue.pop(1) => {
66
+
match job_result {
67
+
Ok(Some(job)) => {
68
+
// Create new redis connection for this job
69
+
let redis_client = match redis::Client::open(self.config.redis.url.as_str()) {
70
+
Ok(c) => c,
71
+
Err(e) => {
72
+
error!("Failed to create Redis client: {}", e);
73
+
continue;
74
+
}
75
+
};
76
+
77
+
let mut redis_conn = match redis_client.get_multiplexed_async_connection().await {
78
+
Ok(conn) => conn,
79
+
Err(e) => {
80
+
error!("Failed to connect to Redis: {}", e);
81
+
continue;
82
+
}
83
+
};
84
+
85
+
// Process job
86
+
if let Err(e) = Self::process_job(
87
+
&self.config,
88
+
&self.client,
89
+
self.agent.agent(),
90
+
&self.blob_checks,
91
+
&self.metrics,
92
+
&self.rate_limiter,
93
+
&mut cache,
94
+
&mut redis_conn,
95
+
job,
96
+
&mut queue,
97
+
self.agent.did(),
98
+
)
99
+
.await
100
+
{
101
+
error!("Worker task failed: {}", e);
102
+
}
103
+
}
104
+
Ok(None) => {
105
+
// Timeout, continue loop
106
+
}
107
+
Err(e) => {
108
+
error!("Error popping job from queue: {}", e);
109
+
sleep(Duration::from_millis(self.config.processing.retry_delay)).await;
110
+
}
111
+
}
112
+
}
113
+
}
114
+
}
115
+
116
+
Ok(())
117
+
}
118
+
119
+
/// Process a single job
120
+
async fn process_job(
121
+
config: &Config,
122
+
client: &Client,
123
+
agent: &Arc<Agent<MemoryCredentialSession>>,
124
+
blob_checks: &[BlobCheck],
125
+
metrics: &Metrics,
126
+
rate_limiter: &RateLimiter,
127
+
cache: &mut PhashCache,
128
+
redis_conn: &mut redis::aio::MultiplexedConnection,
129
+
job: ImageJob,
130
+
queue: &mut JobQueue,
131
+
created_by: &str,
132
+
) -> Result<()> {
133
+
info!("Processing job: {}", job.post_uri);
134
+
135
+
// Process all blobs and find matches
136
+
let matches = Self::process_job_blobs(config, client, blob_checks, metrics, cache, &job).await?;
137
+
138
+
if matches.is_empty() {
139
+
info!("No matches found for job: {}", job.post_uri);
140
+
metrics.inc_jobs_processed();
141
+
return Ok(());
142
+
}
143
+
144
+
// Take moderation actions for each match
145
+
for match_result in matches {
146
+
if let Err(e) =
147
+
Self::take_moderation_action(config, agent, metrics, rate_limiter, redis_conn, &job, &match_result, created_by)
148
+
.await
149
+
{
150
+
error!("Failed to take moderation action: {}", e);
151
+
metrics.inc_jobs_failed();
152
+
// Retry the job
153
+
queue.retry(job.clone()).await?;
154
+
return Err(e);
155
+
}
156
+
}
157
+
158
+
info!("Successfully processed job: {}", job.post_uri);
159
+
metrics.inc_jobs_processed();
160
+
161
+
Ok(())
162
+
}
163
+
164
+
/// Process all blobs in a job and return matches
165
+
async fn process_job_blobs(
166
+
config: &Config,
167
+
client: &Client,
168
+
blob_checks: &[BlobCheck],
169
+
metrics: &Metrics,
170
+
cache: &mut PhashCache,
171
+
job: &ImageJob,
172
+
) -> Result<Vec<MatchResult>> {
173
+
let mut matches = Vec::new();
174
+
175
+
for blob in &job.blobs {
176
+
metrics.inc_blobs_processed();
177
+
178
+
// Check cache first
179
+
let cache_result = cache.get(&blob.cid).await?;
180
+
let phash = if let Some(cached_phash) = cache_result {
181
+
metrics.inc_cache_hits();
182
+
cached_phash
183
+
} else {
184
+
metrics.inc_cache_misses();
185
+
metrics.inc_blobs_downloaded();
186
+
187
+
// Download and compute
188
+
let image_bytes = matcher::download_blob(client, config, &job.post_did, &blob.cid).await?;
189
+
let computed_phash = crate::processor::phash::compute_phash(&image_bytes)?;
190
+
191
+
// Store in cache
192
+
cache.set(&blob.cid, &computed_phash).await?;
193
+
computed_phash
194
+
};
195
+
196
+
// Check for matches
197
+
if let Some(match_result) = matcher::match_phash(
198
+
&phash,
199
+
blob_checks,
200
+
&job.post_did,
201
+
config.phash.default_hamming_threshold,
202
+
) {
203
+
metrics.inc_matches_found();
204
+
matches.push(match_result);
205
+
}
206
+
}
207
+
208
+
Ok(matches)
209
+
}
210
+
211
+
/// Take moderation action based on match result
212
+
async fn take_moderation_action(
213
+
config: &Config,
214
+
agent: &Arc<Agent<MemoryCredentialSession>>,
215
+
metrics: &Metrics,
216
+
rate_limiter: &RateLimiter,
217
+
redis_conn: &mut redis::aio::MultiplexedConnection,
218
+
job: &ImageJob,
219
+
match_result: &MatchResult,
220
+
created_by: &str,
221
+
) -> Result<()> {
222
+
let check = &match_result.matched_check;
223
+
224
+
info!(
225
+
"Taking moderation action for label '{}' on post: {}",
226
+
check.label, job.post_uri
227
+
);
228
+
229
+
// Report post if configured
230
+
if check.report_post {
231
+
if claims::claim_post_report(redis_conn, &job.post_uri, &check.label).await? {
232
+
post::report_post(
233
+
agent.as_ref(),
234
+
config,
235
+
rate_limiter,
236
+
&job.post_uri,
237
+
&job.post_cid,
238
+
ReasonType::ComAtprotoModerationDefsReasonSpam,
239
+
&check.comment,
240
+
&match_result.phash,
241
+
created_by,
242
+
)
243
+
.await?;
244
+
metrics.inc_posts_reported();
245
+
info!("Reported post: {}", job.post_uri);
246
+
} else {
247
+
metrics.inc_posts_already_reported();
248
+
info!("Post already reported, skipping: {}", job.post_uri);
249
+
}
250
+
}
251
+
252
+
// Label post if configured
253
+
if check.to_label {
254
+
if !claims::has_label(redis_conn, &job.post_uri, &check.label).await? {
255
+
post::label_post(
256
+
agent.as_ref(),
257
+
config,
258
+
rate_limiter,
259
+
&job.post_uri,
260
+
&job.post_cid,
261
+
&check.label,
262
+
&check.comment,
263
+
&match_result.phash,
264
+
created_by,
265
+
)
266
+
.await?;
267
+
metrics.inc_posts_labeled();
268
+
claims::set_label(redis_conn, &job.post_uri, &check.label, None).await?;
269
+
info!("Labeled post: {}", job.post_uri);
270
+
} else {
271
+
metrics.inc_posts_already_labeled();
272
+
info!("Post already labeled, skipping: {}", job.post_uri);
273
+
}
274
+
}
275
+
276
+
// Report account if configured
277
+
if check.report_acct {
278
+
if claims::claim_account_report(redis_conn, &job.post_did, &check.label).await? {
279
+
account::report_account(
280
+
agent.as_ref(),
281
+
config,
282
+
rate_limiter,
283
+
&job.post_did,
284
+
ReasonType::ComAtprotoModerationDefsReasonSpam,
285
+
&check.comment,
286
+
&job.post_uri,
287
+
&match_result.phash,
288
+
created_by,
289
+
)
290
+
.await?;
291
+
metrics.inc_accounts_reported();
292
+
info!("Reported account: {}", job.post_did);
293
+
} else {
294
+
metrics.inc_accounts_already_reported();
295
+
info!("Account already reported, skipping: {}", job.post_did);
296
+
}
297
+
}
298
+
299
+
// Label account if configured
300
+
if check.label_acct {
301
+
if !claims::has_label(redis_conn, &job.post_did, &check.label).await? {
302
+
account::label_account(
303
+
agent.as_ref(),
304
+
config,
305
+
rate_limiter,
306
+
&job.post_did,
307
+
&check.label,
308
+
&check.comment,
309
+
&job.post_uri,
310
+
&match_result.phash,
311
+
created_by,
312
+
)
313
+
.await?;
314
+
metrics.inc_accounts_labeled();
315
+
claims::set_label(redis_conn, &job.post_did, &check.label, None).await?;
316
+
info!("Labeled account: {}", job.post_did);
317
+
} else {
318
+
metrics.inc_accounts_already_labeled();
319
+
info!("Account already labeled, skipping: {}", job.post_did);
320
+
}
321
+
}
322
+
323
+
Ok(())
324
+
}
325
+
}
326
+
327
+
// Manual Clone implementation
328
+
impl Clone for WorkerPool {
329
+
fn clone(&self) -> Self {
330
+
Self {
331
+
config: self.config.clone(),
332
+
client: self.client.clone(),
333
+
agent: self.agent.clone(),
334
+
blob_checks: self.blob_checks.clone(),
335
+
metrics: self.metrics.clone(),
336
+
rate_limiter: self.rate_limiter.clone(),
337
+
}
338
+
}
339
+
}
340
+
341
+
#[cfg(test)]
342
+
mod tests {
343
+
// Note: Worker tests require integration testing with Redis
344
+
}
+164
src/types/README.md
+164
src/types/README.md
···
1
+
# Types Module
2
+
3
+
## Purpose
4
+
Shared data structures used across the entire application for jobs, rules, matches, and blob references.
5
+
6
+
## Key Types
7
+
8
+
### `BlobCheck`
9
+
Rule definition for matching and taking action on bad images.
10
+
11
+
**Fields:**
12
+
- `phashes: Vec<String>` - List of known bad phashes (16-char hex)
13
+
- `label: String` - Label to apply (e.g., "spam", "csam", "troll")
14
+
- `comment: String` - Human-readable description of violation
15
+
- `report_acct: bool` - Report the account to moderators
16
+
- `label_acct: bool` - Apply label to account
17
+
- `report_post: bool` - Report the post to moderators
18
+
- `to_label: bool` - Apply label to post
19
+
- `takedown_post: bool` - Takedown/remove the post
20
+
- `takedown_acct: bool` - Takedown/suspend the account
21
+
- `hamming_threshold: Option<u32>` - Max distance for match (overrides global)
22
+
- `description: Option<String>` - Optional human description
23
+
- `ignore_did: Option<Vec<String>>` - DIDs to skip checking
24
+
25
+
**JSON format (rules/blobs.json):**
26
+
```json
27
+
{
28
+
"phashes": ["e0e0e0e0e0fcfefe"],
29
+
"label": "spam",
30
+
"comment": "Known spam image detected",
31
+
"reportAcct": false,
32
+
"labelAcct": true,
33
+
"reportPost": true,
34
+
"toLabel": true,
35
+
"takedownPost": false,
36
+
"takedownAcct": false,
37
+
"hammingThreshold": 3,
38
+
"description": "Spam campaign from Oct 2024",
39
+
"ignoreDID": ["did:plc:exempted-account"]
40
+
}
41
+
```
42
+
43
+
**Usage:**
44
+
- Loaded at startup from JSON file
45
+
- Cloned into each worker
46
+
- Used by `match_phash()` to determine matches
47
+
- Defines what actions to take on match
48
+
49
+
### `BlobReference`
50
+
Reference to an image blob in a post.
51
+
52
+
**Fields:**
53
+
- `cid: String` - Content identifier (blob hash)
54
+
- `mime_type: Option<String>` - MIME type (e.g., "image/jpeg")
55
+
56
+
**Source:** Extracted from Jetstream post records
57
+
**Usage:** Part of `ImageJob`, tells worker which blobs to check
58
+
59
+
### `ImageJob`
60
+
A job representing a post with images to check.
61
+
62
+
**Fields:**
63
+
- `post_uri: String` - AT-URI of the post (e.g., "at://did:plc:xyz/app.bsky.feed.post/abc")
64
+
- `post_cid: String` - Post commit CID
65
+
- `post_did: String` - DID of post author
66
+
- `blobs: Vec<BlobReference>` - Images to check
67
+
- `timestamp: i64` - Unix timestamp when job created
68
+
- `attempts: u32` - Retry counter (0 = first attempt)
69
+
70
+
**Lifecycle:**
71
+
1. Created by Jetstream client from post event
72
+
2. Pushed to Redis queue (JSON serialized)
73
+
3. Popped by worker
74
+
4. Processed (download, compute, match)
75
+
5. If retry needed: `attempts++`, push back to queue
76
+
6. If max retries: move to dead letter queue
77
+
78
+
### `MatchResult`
79
+
Result of a successful phash match.
80
+
81
+
**Fields:**
82
+
- `phash: String` - Computed phash from image
83
+
- `matched_check: BlobCheck` - Which rule matched
84
+
- `matched_phash: String` - Specific phash from rule that matched
85
+
- `hamming_distance: u32` - How close the match was (0 = exact)
86
+
87
+
**Usage:**
88
+
- Returned by `match_phash()` on successful match
89
+
- Contains all info needed for moderation actions
90
+
- Passed to `take_moderation_action()`
91
+
- Used to build detailed moderation comments
92
+
93
+
**Example:**
94
+
```rust
95
+
MatchResult {
96
+
phash: "e0e0e0e0e0fcfefe",
97
+
matched_check: BlobCheck { label: "spam", ... },
98
+
matched_phash: "e0e0e0e0fcfef0",
99
+
hamming_distance: 2, // 2 bits different
100
+
}
101
+
```
102
+
103
+
## Serde Configuration
104
+
105
+
All types use `#[serde(rename_all = "camelCase")]`:
106
+
- Rust: `report_acct`
107
+
- JSON: `reportAcct`
108
+
109
+
**Why?** JSON format matches AT Protocol conventions (camelCase).
110
+
111
+
## Type Flow
112
+
113
+
```
114
+
Jetstream Event
115
+
↓ extract blobs
116
+
BlobReference (cid, mime_type)
117
+
↓ create job
118
+
ImageJob (post_uri, post_cid, post_did, blobs, ...)
119
+
↓ serialize to JSON
120
+
Redis Queue
121
+
↓ deserialize from JSON
122
+
Worker
123
+
↓ download & compute
124
+
↓ match against
125
+
BlobCheck (phashes, label, actions, ...)
126
+
↓ if match
127
+
MatchResult (phash, matched_check, distance)
128
+
↓ execute actions
129
+
Moderation API
130
+
```
131
+
132
+
## Design Decisions
133
+
134
+
**Why separate BlobReference from ImageJob?**
135
+
- Post can have multiple images
136
+
- Each image needs separate processing
137
+
- But all share same post metadata
138
+
139
+
**Why include post_uri in ImageJob?**
140
+
- Needed for moderation actions
141
+
- Included in moderation comments
142
+
- Used for deduplication in claims
143
+
144
+
**Why store attempts in ImageJob?**
145
+
- Retry logic needs to track failure count
146
+
- Prevents infinite retry loops
147
+
- Used to determine dead letter queue
148
+
149
+
**Why include matched_check in MatchResult?**
150
+
- Need to know which actions to take
151
+
- Need comment for moderation API
152
+
- Avoids re-looking up the rule
153
+
154
+
## Dependencies
155
+
156
+
- `serde` - Serialization/deserialization
157
+
- All types derive: `Debug`, `Clone`, `Serialize`, `Deserialize`
158
+
159
+
## Related Modules
160
+
161
+
- Used by: All modules - these are the core data types
162
+
- Loaded by: `processor::matcher` - BlobCheck from JSON
163
+
- Created by: `jetstream` - ImageJob and BlobReference
164
+
- Returned by: `processor::matcher` - MatchResult
+51
src/types/mod.rs
+51
src/types/mod.rs
···
1
+
use serde::{Deserialize, Serialize};
2
+
3
+
#[derive(Debug, Clone, Serialize, Deserialize)]
4
+
#[serde(rename_all = "camelCase")]
5
+
pub struct BlobCheck {
6
+
pub phashes: Vec<String>,
7
+
pub label: String,
8
+
pub comment: String,
9
+
pub report_acct: bool,
10
+
pub label_acct: bool,
11
+
pub report_post: bool,
12
+
pub to_label: bool,
13
+
#[serde(default)]
14
+
pub takedown_post: bool,
15
+
#[serde(default)]
16
+
pub takedown_acct: bool,
17
+
#[serde(default)]
18
+
pub hamming_threshold: Option<u32>,
19
+
#[serde(default)]
20
+
pub description: Option<String>,
21
+
#[serde(default, alias = "ignoreDID")]
22
+
pub ignore_did: Option<Vec<String>>,
23
+
}
24
+
25
+
#[derive(Debug, Clone, Serialize, Deserialize)]
26
+
#[serde(rename_all = "camelCase")]
27
+
pub struct BlobReference {
28
+
pub cid: String,
29
+
#[serde(default)]
30
+
pub mime_type: Option<String>,
31
+
}
32
+
33
+
#[derive(Debug, Clone, Serialize, Deserialize)]
34
+
#[serde(rename_all = "camelCase")]
35
+
pub struct ImageJob {
36
+
pub post_uri: String,
37
+
pub post_cid: String,
38
+
pub post_did: String,
39
+
pub blobs: Vec<BlobReference>,
40
+
pub timestamp: i64,
41
+
pub attempts: u32,
42
+
}
43
+
44
+
#[derive(Debug, Clone, Serialize, Deserialize)]
45
+
#[serde(rename_all = "camelCase")]
46
+
pub struct MatchResult {
47
+
pub phash: String,
48
+
pub matched_check: BlobCheck,
49
+
pub matched_phash: String,
50
+
pub hamming_distance: u32,
51
+
}
tests/integration/queue.rs
tests/integration/queue.rs
This is a binary file and will not be displayed.
tests/unit/matcher.rs
tests/unit/matcher.rs
This is a binary file and will not be displayed.
tests/unit/phash.rs
tests/unit/phash.rs
This is a binary file and will not be displayed.