moderate 0.1.0 → 1.0.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -0
- data/.simplecov +62 -0
- data/AGENTS.md +7 -0
- data/Appraisals +16 -0
- data/CHANGELOG.md +109 -1
- data/CLAUDE.md +7 -0
- data/README.md +378 -29
- data/Rakefile +28 -2
- data/app/controllers/concerns/moderate/moderation.rb +161 -0
- data/app/controllers/moderate/appeals_controller.rb +190 -0
- data/app/controllers/moderate/application_controller.rb +45 -0
- data/app/controllers/moderate/notices_controller.rb +382 -0
- data/app/controllers/moderate/transparency_reports_controller.rb +30 -0
- data/app/helpers/moderate/engine_helper.rb +151 -0
- data/app/views/moderate/appeals/new.html.erb +78 -0
- data/app/views/moderate/notices/new.html.erb +255 -0
- data/app/views/moderate/transparency_reports/_summary_card.html.erb +20 -0
- data/app/views/moderate/transparency_reports/show.html.erb +52 -0
- data/config/moderate/blocklists/en.yml +81 -0
- data/config/moderate/blocklists/es.yml +40 -0
- data/config/routes.rb +36 -0
- data/context7.json +4 -0
- data/docs/compliance.md +178 -0
- data/docs/configuration.md +326 -0
- data/docs/dsa-notice-form.md +371 -0
- data/docs/images/moderate-user-report-block-actions.webp +0 -0
- data/docs/madmin.md +490 -0
- data/docs/notifications.md +363 -0
- data/examples/aws_rekognition_adapter.rb +140 -0
- data/examples/openai_moderation_adapter.rb +111 -0
- data/gemfiles/rails_7.1.gemfile +36 -0
- data/gemfiles/rails_7.2.gemfile +36 -0
- data/gemfiles/rails_8.1.gemfile +36 -0
- data/lib/generators/moderate/install_generator.rb +56 -0
- data/lib/generators/moderate/templates/create_moderate_tables.rb.erb +237 -0
- data/lib/generators/moderate/templates/initializer.rb +198 -0
- data/lib/generators/moderate/views_generator.rb +63 -0
- data/lib/moderate/configuration.rb +355 -0
- data/lib/moderate/engine.rb +138 -0
- data/lib/moderate/errors.rb +26 -0
- data/lib/moderate/event.rb +75 -0
- data/lib/moderate/filters/base.rb +126 -0
- data/lib/moderate/filters/wordlist.rb +255 -0
- data/lib/moderate/jobs/classify_job.rb +162 -0
- data/lib/moderate/label.rb +111 -0
- data/lib/moderate/macros.rb +90 -0
- data/lib/moderate/models/appeal.rb +154 -0
- data/lib/moderate/models/application_record.rb +31 -0
- data/lib/moderate/models/block.rb +203 -0
- data/lib/moderate/models/concerns/actor.rb +174 -0
- data/lib/moderate/models/concerns/content_filterable.rb +214 -0
- data/lib/moderate/models/concerns/reportable.rb +282 -0
- data/lib/moderate/models/flag.rb +169 -0
- data/lib/moderate/models/report.rb +620 -0
- data/lib/moderate/result.rb +176 -0
- data/lib/moderate/services/intake_appeal.rb +89 -0
- data/lib/moderate/services/intake_notice.rb +132 -0
- data/lib/moderate/services/intake_report.rb +132 -0
- data/lib/moderate/services/resolve_appeal.rb +134 -0
- data/lib/moderate/services/resolve_flag.rb +101 -0
- data/lib/moderate/services/resolve_report.rb +291 -0
- data/lib/moderate/version.rb +1 -1
- data/lib/moderate.rb +365 -18
- data/log/development.log +0 -0
- data/log/test.log +0 -0
- metadata +156 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20519d26bf4bc96f23e6f679b3b175376700280e36f21fffa409230688526b25
|
|
4
|
+
data.tar.gz: 8abc344dcf72c577de12ba70dbf96895f008a041355a75308f815d8b267f5dd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65f2ad5ae6dae9a4a1269900e67205cfedaa5f82ff3b87d218ac34dee0e05bdb417fd77289126c47c9cf372b0dfd710fc1fa5d455d1f1f256395d2ec4bc7e79c
|
|
7
|
+
data.tar.gz: 5d3a60adaef05e13b448c1563d3104423c2d94d63ae0b10c7f2432268188a745849ca29df9e99fdb4860670a4c4f3f6fe7eda8fa07a4dd4a8f47169eca97d3f8
|
data/.rubocop.yml
ADDED
data/.simplecov
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SimpleCov configuration file (auto-loaded before test suite)
|
|
4
|
+
# This keeps test_helper.rb clean and follows best practices.
|
|
5
|
+
# Coherent with the rest of the gem ecosystem (usage_credits, pricing_plans, …).
|
|
6
|
+
|
|
7
|
+
SimpleCov.start do
|
|
8
|
+
# Use SimpleFormatter for terminal-only output (no HTML generation)
|
|
9
|
+
formatter SimpleCov::Formatter::SimpleFormatter
|
|
10
|
+
|
|
11
|
+
# Don't count the test suite itself toward coverage
|
|
12
|
+
add_filter "/test/"
|
|
13
|
+
|
|
14
|
+
# Don't count code that ISN'T unit-testable by this suite and would only distort
|
|
15
|
+
# the numbers:
|
|
16
|
+
# - Generators + their templates: these run via `rails generate moderate:install`
|
|
17
|
+
# / `moderate:views` in a real host, not in the engine's own unit suite. (The
|
|
18
|
+
# migration template IS exercised indirectly — the dummy migrates a copy of it —
|
|
19
|
+
# but the .erb itself is never loaded as Ruby here.)
|
|
20
|
+
# - version.rb: a single constant; nothing to cover.
|
|
21
|
+
# - The 0.x compatibility shims (text / text_validator / word_list): legacy
|
|
22
|
+
# profanity-validator code kept only so `validates :field, moderate: true` from
|
|
23
|
+
# 0.x still loads (see README "Upgrading from 0.x"). They are NOT part of the
|
|
24
|
+
# 1.0 Trust & Safety surface this suite tests, so they shouldn't pull the 1.0
|
|
25
|
+
# coverage number down.
|
|
26
|
+
add_filter "/lib/generators/"
|
|
27
|
+
add_filter "/lib/moderate/version.rb"
|
|
28
|
+
add_filter "/lib/moderate/text.rb"
|
|
29
|
+
add_filter "/lib/moderate/text_validator.rb"
|
|
30
|
+
add_filter "/lib/moderate/word_list.rb"
|
|
31
|
+
|
|
32
|
+
# Track Ruby files in the lib directory (gem source code)
|
|
33
|
+
track_files "lib/**/*.rb"
|
|
34
|
+
|
|
35
|
+
# Enable branch coverage for more detailed metrics
|
|
36
|
+
enable_coverage :branch
|
|
37
|
+
|
|
38
|
+
# Minimum coverage thresholds to prevent coverage REGRESSION. These reflect what
|
|
39
|
+
# the current shipped suite actually exercises (line ~86%, branch ~65%): the
|
|
40
|
+
# primitives — models, concerns, services, adapters, the facade, the value objects —
|
|
41
|
+
# are thoroughly covered; the lower branch number is driven by the engine's
|
|
42
|
+
# CONTROLLERS (the public DSA notice form + the BYOUI moderation concern) and the
|
|
43
|
+
# async ClassifyJob, whose request/job paths the unit suite doesn't drive. The
|
|
44
|
+
# thresholds sit just under the current floor so the gate catches a real regression
|
|
45
|
+
# without failing on the existing baseline; raise them as request/job coverage grows.
|
|
46
|
+
minimum_coverage line: 80, branch: 60
|
|
47
|
+
|
|
48
|
+
# Disambiguate parallel test runs
|
|
49
|
+
command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV["TEST_ENV_NUMBER"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Print coverage summary to terminal after tests complete
|
|
53
|
+
SimpleCov.at_exit do
|
|
54
|
+
SimpleCov.result.format!
|
|
55
|
+
puts "\n" + "=" * 60
|
|
56
|
+
puts "COVERAGE SUMMARY"
|
|
57
|
+
puts "=" * 60
|
|
58
|
+
puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
|
|
59
|
+
branch_coverage = SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || "N/A"
|
|
60
|
+
puts "Branch Coverage: #{branch_coverage}%"
|
|
61
|
+
puts "=" * 60
|
|
62
|
+
end
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI Agents (like OpenAI's Codex, Cursor Agent, Claude Code, etc) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
Please read the `README.md` for a full overview of the gem's API and philosophy, and the `docs/` directory (`docs/configuration.md`, `docs/notifications.md`, `docs/compliance.md`, `docs/madmin.md`, `docs/dsa-notice-form.md`) for the detailed integration guides.
|
|
6
|
+
|
|
7
|
+
This gem is part of a coherent ecosystem (`railsfast`, `goodmail`, `telegrama`, `usage_credits`, `pricing_plans`, `wallets`, `api_keys`). Match the ecosystem conventions exactly: a single `Moderate.configure do |config| … end` block, `has_*`/verb-style class macros, adapter objects + no-op-default hook procs, string class names constantized lazily, adaptive install migrations, Minitest with a `test/dummy` app, SimpleCov, and the README/docs voice.
|
data/Appraisals
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Test the minimum supported Rails version (matches the gemspec floor and the
|
|
4
|
+
# README's "Rails 7.1+ schema" claim — the adaptive migration must work here).
|
|
5
|
+
appraise "rails-7.1" do
|
|
6
|
+
gem "rails", "~> 7.1.0"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
appraise "rails-7.2" do
|
|
10
|
+
gem "rails", "~> 7.2.0"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Test the latest Rails version — this is the default/main Gemfile anyway.
|
|
14
|
+
appraise "rails-8.1" do
|
|
15
|
+
gem "rails", "~> 8.1.0"
|
|
16
|
+
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,111 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.0.0.beta2] - 2026-07-10
|
|
8
|
+
|
|
9
|
+
Second beta on the road to 1.0. Fixes beta1's async-adapter routing bug (the one
|
|
10
|
+
that forced hosts to hand-roll their own enqueue), makes Active Storage
|
|
11
|
+
attachments filterable with zero wiring, and gives `Flag` first-class close
|
|
12
|
+
methods. Drop-in upgrade from beta1: no migrations, no breaking API changes —
|
|
13
|
+
and hosts that worked around the ClassifyJob bug can now delete the workaround.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Async adapters now actually run in `Moderate::ClassifyJob`.** beta1's `:flag`
|
|
18
|
+
after_commit called `Moderate.classify` inline for *every* adapter — including
|
|
19
|
+
ones declaring `synchronous? == false` — so a remote moderation API ran its
|
|
20
|
+
network call inside the request that saved the content, and `ClassifyJob`
|
|
21
|
+
(whose docs promised this routing) was never enqueued by anything. The concern
|
|
22
|
+
now checks `config.adapter_async?(policy.adapter)` (new public helper, same
|
|
23
|
+
probe as the `:block` validator) and enqueues the job instead; the job re-reads
|
|
24
|
+
the persisted value and files the Flag itself. Hosts that worked around this by
|
|
25
|
+
enqueuing `ClassifyJob` themselves and short-circuiting
|
|
26
|
+
`moderation_field_changed_for_commit?` can delete both workarounds. (#3)
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Active Storage attachments filter out of the box.** `moderates :avatar, with:
|
|
31
|
+
:your_image_adapter, mode: :flag` on a `has_one_attached` model needs zero extra
|
|
32
|
+
wiring now: the concern snapshots `attachment_changes` in a `before_save` (AR
|
|
33
|
+
dirty tracking can't see attachment writes, and Active Storage clears the
|
|
34
|
+
changes before after_commit), consumes the snapshot at commit time, and both
|
|
35
|
+
the concern and `ClassifyJob` treat an unattached `ActiveStorage::Attached`
|
|
36
|
+
proxy as blank (nothing to classify — covers purge-between-enqueue-and-run).
|
|
37
|
+
The three `moderation_field_*` seam overrides remain for richer cases. (#3)
|
|
38
|
+
- **Flag close sugar.** `Flag#action!(note:, by: nil)` / `Flag#dismiss!(note:, by: nil)` —
|
|
39
|
+
model-level closes mirroring `Report#resolve!`/`#dismiss!`, so hosts stop hand-writing
|
|
40
|
+
status updates. `by:` stays nil for automated closes (don't fake a human in the audit
|
|
41
|
+
trail). Canonical automated use: dismiss a pending flag whose flagged content was
|
|
42
|
+
**superseded** (text edited, photo replaced/reverted) — left pending it keeps `flagged?`
|
|
43
|
+
true and mislabels the NEW content in any host UI keyed on it. (#4)
|
|
44
|
+
|
|
45
|
+
## [1.0.0] - unreleased
|
|
46
|
+
|
|
47
|
+
A complete, ground-up rewrite. `moderate` graduates from a single-purpose profanity
|
|
48
|
+
validator (0.1.0) into a full **Trust & Safety** engine for Rails apps with user-generated
|
|
49
|
+
content: report, block, filter, a moderation queue, appeals, and EU DSA / App Store / Google
|
|
50
|
+
Play **aligned** primitives. (First cut shipped as `1.0.0.beta1`; second as `1.0.0.beta2`.)
|
|
51
|
+
|
|
52
|
+
> **Breaking:** 1.0 keeps the gem name but is an entirely new API. The 0.x profanity
|
|
53
|
+
> validator (`validates :field, moderate: true`) still loads for backward compatibility
|
|
54
|
+
> (see _Upgrading from 0.x_), but everything else is new. Pin `~> 0.1` if you relied on the
|
|
55
|
+
> old behavior and are not ready to adopt the new surface.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **Reporting.** `Moderate::Report` plus the `has_reportable_content :fields` macro and
|
|
60
|
+
`Actor#report!(reportable, category:, details:)`. Reports and DSA notices share one model
|
|
61
|
+
and one queue (`intake_kind: "community" | "dsa"`).
|
|
62
|
+
- **Blocking.** `Moderate::Block`, the `has_reporting_and_blocking` actor macro, and
|
|
63
|
+
`block!` / `unblock!` / `blocks?` / `blocked_by?` / `blocked_with?`. `Moderate.blocked_ids_for(user)`
|
|
64
|
+
is the bidirectional single source of truth you compose into feed/search/inbox queries.
|
|
65
|
+
Optional `config.on_block` teardown hook runs inside the block transaction.
|
|
66
|
+
- **Content filtering.** The `moderates :field, mode: :off|:block|:flag, with: :adapter` macro
|
|
67
|
+
(and the equivalent `config.filter`), the offline multilingual `:wordlist` adapter (the only
|
|
68
|
+
built-in), the `classify(value) => Moderate::Result` adapter contract with
|
|
69
|
+
`config.register_adapter`, asynchronous classification via `Moderate::ClassifyJob`, and
|
|
70
|
+
ready-to-copy reference adapters for OpenAI omni-moderation and AWS Rekognition under
|
|
71
|
+
`examples/` (bring-your-own, never a dependency).
|
|
72
|
+
- **Moderation queue & decisions.** `Moderate::Flag` and the service objects
|
|
73
|
+
`Moderate::Services::{IntakeReport, ResolveReport, ResolveFlag, IntakeAppeal, ResolveAppeal,
|
|
74
|
+
IntakeNotice}`. Decisions are taken under a row lock, re-check open state, apply enforcement
|
|
75
|
+
(remove content / ban) inside the transaction, and fire notifications outside it; the appeal
|
|
76
|
+
window and statement-of-reasons fields are stamped automatically.
|
|
77
|
+
- **DSA-aligned primitives.** A mountable public **notice-and-action** form (Art. 16) you mount
|
|
78
|
+
at any path, **statement of reasons** (Art. 17), internal **appeals** (Art. 20), and
|
|
79
|
+
**transparency** counters (Art. 24). The notice form prefills from query params + the signed-in
|
|
80
|
+
user, locks auto-filled identity fields, and auto-detects `rails_cloudflare_turnstile`.
|
|
81
|
+
- **Hooks (all no-op by default).** `config.audit`, `config.notify` (returns a delivery boolean
|
|
82
|
+
used to gate `decision_notified_at`), `config.on_block`, `config.ban_handler`, the
|
|
83
|
+
host-overridable `config.report_categories`, and `config.notice_human_verification_skip_if` /
|
|
84
|
+
`config.appeal_human_verification_skip_if` for native-app bot-gate carve-outs.
|
|
85
|
+
- **Optional integrations**, all auto-detected at runtime via `defined?`/`respond_to?` and never
|
|
86
|
+
hard dependencies: madmin, goodmail, telegrama, noticed, rails_cloudflare_turnstile.
|
|
87
|
+
- **Install tooling.** `rails generate moderate:install` writes a documented initializer and an
|
|
88
|
+
adaptive migration (uuid/bigint primary keys, jsonb/json/MySQL JSON columns); `moderate:views`
|
|
89
|
+
ejects the notice form for customization.
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
|
|
93
|
+
- Taxonomies (report categories, DSA legal reasons, country codes) are now frozen model
|
|
94
|
+
constants with inclusion validations instead of DB `CHECK` constraints — adding or
|
|
95
|
+
customizing a category needs no migration.
|
|
96
|
+
- External classifiers (OpenAI, image moderation) are reference adapters in `examples/`, not
|
|
97
|
+
shipped or loaded code — the gem core forces no service dependency on apps that never use it.
|
|
98
|
+
- All "DSA-compliant" / "App Store compliant" language reframed to **DSA-aligned primitives**:
|
|
99
|
+
the gem ships the mechanisms the law and the stores require; your policies, response times,
|
|
100
|
+
and operations are still yours.
|
|
101
|
+
|
|
102
|
+
### Upgrading from 0.x
|
|
103
|
+
|
|
104
|
+
- The 0.x profanity validator still loads: `validates :field, moderate: true` continues to work
|
|
105
|
+
via compatibility shims, so existing apps keep validating. To adopt 1.0, add
|
|
106
|
+
`has_reporting_and_blocking` to your user model and `has_reportable_content` / `moderates` to your
|
|
107
|
+
content models, run `rails generate moderate:install`, and migrate.
|
|
108
|
+
|
|
1
109
|
## [0.1.0] - 2024-11-03
|
|
2
110
|
|
|
3
|
-
- Initial release
|
|
111
|
+
- Initial release (profanity validator).
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
Please read the `README.md` for a full overview of the gem's API and philosophy, and the `docs/` directory (`docs/configuration.md`, `docs/notifications.md`, `docs/compliance.md`, `docs/madmin.md`, `docs/dsa-notice-form.md`) for the detailed integration guides.
|
|
6
|
+
|
|
7
|
+
This gem is part of a coherent ecosystem (`railsfast`, `goodmail`, `telegrama`, `usage_credits`, `pricing_plans`, `wallets`, `api_keys`). Match the ecosystem conventions exactly: a single `Moderate.configure do |config| … end` block, `has_*`/verb-style class macros, adapter objects + no-op-default hook procs, string class names constantized lazily, adaptive install migrations, Minitest with a `test/dummy` app, SimpleCov, and the README/docs voice.
|