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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/.simplecov +62 -0
  4. data/AGENTS.md +7 -0
  5. data/Appraisals +16 -0
  6. data/CHANGELOG.md +109 -1
  7. data/CLAUDE.md +7 -0
  8. data/README.md +378 -29
  9. data/Rakefile +28 -2
  10. data/app/controllers/concerns/moderate/moderation.rb +161 -0
  11. data/app/controllers/moderate/appeals_controller.rb +190 -0
  12. data/app/controllers/moderate/application_controller.rb +45 -0
  13. data/app/controllers/moderate/notices_controller.rb +382 -0
  14. data/app/controllers/moderate/transparency_reports_controller.rb +30 -0
  15. data/app/helpers/moderate/engine_helper.rb +151 -0
  16. data/app/views/moderate/appeals/new.html.erb +78 -0
  17. data/app/views/moderate/notices/new.html.erb +255 -0
  18. data/app/views/moderate/transparency_reports/_summary_card.html.erb +20 -0
  19. data/app/views/moderate/transparency_reports/show.html.erb +52 -0
  20. data/config/moderate/blocklists/en.yml +81 -0
  21. data/config/moderate/blocklists/es.yml +40 -0
  22. data/config/routes.rb +36 -0
  23. data/context7.json +4 -0
  24. data/docs/compliance.md +178 -0
  25. data/docs/configuration.md +326 -0
  26. data/docs/dsa-notice-form.md +371 -0
  27. data/docs/images/moderate-user-report-block-actions.webp +0 -0
  28. data/docs/madmin.md +490 -0
  29. data/docs/notifications.md +363 -0
  30. data/examples/aws_rekognition_adapter.rb +140 -0
  31. data/examples/openai_moderation_adapter.rb +111 -0
  32. data/gemfiles/rails_7.1.gemfile +36 -0
  33. data/gemfiles/rails_7.2.gemfile +36 -0
  34. data/gemfiles/rails_8.1.gemfile +36 -0
  35. data/lib/generators/moderate/install_generator.rb +56 -0
  36. data/lib/generators/moderate/templates/create_moderate_tables.rb.erb +237 -0
  37. data/lib/generators/moderate/templates/initializer.rb +198 -0
  38. data/lib/generators/moderate/views_generator.rb +63 -0
  39. data/lib/moderate/configuration.rb +355 -0
  40. data/lib/moderate/engine.rb +138 -0
  41. data/lib/moderate/errors.rb +26 -0
  42. data/lib/moderate/event.rb +75 -0
  43. data/lib/moderate/filters/base.rb +126 -0
  44. data/lib/moderate/filters/wordlist.rb +255 -0
  45. data/lib/moderate/jobs/classify_job.rb +162 -0
  46. data/lib/moderate/label.rb +111 -0
  47. data/lib/moderate/macros.rb +90 -0
  48. data/lib/moderate/models/appeal.rb +154 -0
  49. data/lib/moderate/models/application_record.rb +31 -0
  50. data/lib/moderate/models/block.rb +203 -0
  51. data/lib/moderate/models/concerns/actor.rb +174 -0
  52. data/lib/moderate/models/concerns/content_filterable.rb +214 -0
  53. data/lib/moderate/models/concerns/reportable.rb +282 -0
  54. data/lib/moderate/models/flag.rb +169 -0
  55. data/lib/moderate/models/report.rb +620 -0
  56. data/lib/moderate/result.rb +176 -0
  57. data/lib/moderate/services/intake_appeal.rb +89 -0
  58. data/lib/moderate/services/intake_notice.rb +132 -0
  59. data/lib/moderate/services/intake_report.rb +132 -0
  60. data/lib/moderate/services/resolve_appeal.rb +134 -0
  61. data/lib/moderate/services/resolve_flag.rb +101 -0
  62. data/lib/moderate/services/resolve_report.rb +291 -0
  63. data/lib/moderate/version.rb +1 -1
  64. data/lib/moderate.rb +365 -18
  65. data/log/development.log +0 -0
  66. data/log/test.log +0 -0
  67. metadata +156 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7c7f6fd1cfa1c2ba6b28e2d0b75ce803d0ae2eec002011b18a9a25b2b98ce6f
4
- data.tar.gz: 71bab2651af6c3ff8520360281811b53d21a711496a9ef392334e8b2a59646b5
3
+ metadata.gz: 20519d26bf4bc96f23e6f679b3b175376700280e36f21fffa409230688526b25
4
+ data.tar.gz: 8abc344dcf72c577de12ba70dbf96895f008a041355a75308f815d8b267f5dd1
5
5
  SHA512:
6
- metadata.gz: adfb034df21d5ecfa8fec498f34df2d6595d33033f05e7fb2832ec52f7c0012fa43806950b9c9d723cc0508563159972e57ceb051e3263ca0cfff11583bb9959
7
- data.tar.gz: 2dd06af75442e89d39e18f44c7dc61e37030dfbee6406f5be096903418489f9061dea5262aeaa874431ec4c3ac82f3c93933036d7715e3255e6431715c6923ff
6
+ metadata.gz: 65f2ad5ae6dae9a4a1269900e67205cfedaa5f82ff3b87d218ac34dee0e05bdb417fd77289126c47c9cf372b0dfd710fc1fa5d455d1f1f256395d2ec4bc7e79c
7
+ data.tar.gz: 5d3a60adaef05e13b448c1563d3104423c2d94d63ae0b10c7f2432268188a745849ca29df9e99fdb4860670a4c4f3f6fe7eda8fa07a4dd4a8f47169eca97d3f8
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
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.