Class: Moderate::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/moderate/configuration.rb

Overview

The single configuration object the host populates in ‘config/initializers/moderate.rb` via `Moderate.configure do |config| … end`.

Design rules, straight from docs/configuration.md:

- Config is read AT THE POINT OF USE, not frozen at boot. Class names are
  stored as strings and constantized lazily, so the initializer works no
  matter when the app loads (the User model may not exist yet at boot).
- Validating setters normalize their input (`to_s.strip.downcase.to_sym`) so
  "Block", :block and " block " all mean the same thing, and raise a
  plain-English ArgumentError on a bad value — failing fast at the assignment
  line. `validate!` runs once more at the end of `configure` for cross-field
  checks (e.g. a :block filter pointed at an async adapter).
- Every hook (`audit`/`notify`/`on_block`/`ban_handler`) defaults to a no-op,
  so the gem works untouched and a host wires hooks only as needed.

This mirrors the validating-setter convention across the ecosystem (usage_credits’ ‘default_currency=`, wallets’ ‘default_asset=`).

Defined Under Namespace

Classes: FilterPolicy

Constant Summary collapse

FILTER_MODES =

The three filter modes a ‘moderates :field` / `config.filter` can use.

:off   — no check
:block — reject the save with a validation error if the filter trips
:flag  — allow the save, create a Moderate::Flag after commit for review

Order matters for the error message (“must be one of: off, block, flag”).

%i[off block flag].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Returns a new instance of Configuration.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/moderate/configuration.rb', line 82

def initialize
  # Identity. "User" is the overwhelmingly common case; the host overrides it
  # if their actor model is "Account", "Member", etc.
  @user_class = "User"

  # Filtering defaults. :block is the safe default per docs (reject objectionable
  # writes); :wordlist is the offline, zero-dependency default text adapter.
  @default_filter_mode = :block
  @filter_adapter = :wordlist
  @additional_words = []
  @excluded_words = []

  # Community report category override. nil ⇒ Moderate::Report::DEFAULT_CATEGORIES.
  # No migration needed to add a category — `category` is validated in the model.
  @report_categories = nil

  # Adapters registry: name (Symbol) => adapter (an object responding to
  # `classify`, OR a String class name to constantize lazily at use time, so we
  # don't force the built-in adapter files to be loaded before the initializer
  # runs). Seeded with the ONE built-in (the offline :wordlist) the README
  # documents; OpenAI/Rekognition/etc. are reference adapters in examples/ a host
  # copies in and registers — they are not shipped, loaded, or a dependency.
  #
  # The class behind this name is the gem's own adapter; we reference it by string
  # to keep this file decoupled from its load order (and there is no
  # `Moderate::Adapters` alias namespace — point straight at the Filters class).
  @adapters = {
    wordlist: "Moderate::Filters::Wordlist"
  }

  # Per-field filter policies, keyed by [class_name_string, field_string].
  @filters = {}

  # Hooks — no-ops by default. These exact signatures are documented:
  #   audit/notify take a single Moderate::Event
  #   on_block/ban_handler take keyword args
  @audit = ->(_event) {}
  @notify = ->(_event) {}
  @on_block = ->(blocker:, blocked:, at:) {}
  @ban_handler = ->(user:, by:, reason:) {}

  # Misc. nil locale ⇒ follow I18n.default_locale at use time.
  @locale = nil

  # Engine controller defaults. The parent controller defaults to a stock base so
  # the engine works even on API-only apps — the same `parent_controller`
  # indirection Devise and api_keys use.
  @parent_controller = "::ActionController::Base"

  # DSA notice-form defaults (see docs/dsa-notice-form.md). The form is on by
  # default; both bot-gates no-op when their keys are blank; the rate limit is a
  # sane per-IP throttle.
  @notice_form_enabled = true
  @notice_rate_limit = { max: 5, within: 3600 } # 1.hour, expressed in seconds to avoid an ActiveSupport dependency here
  @notice_turnstile_site_key = nil
  @notice_turnstile_secret_key = nil
  @notice_captcha_verifier = nil
  # Gem-absent fallback bot gate. nil ⇒ no extra gate (the form just works); the
  # controller only consults it when rails_cloudflare_turnstile is NOT installed.
  @notice_guard = nil
  @notice_human_verification_skip_if = nil

  # DSA internal complaint / appeal form defaults. Same shape as the notice
  # form: public route, optional bot gate, runtime rate limit, and a redirect
  # target the host can choose.
  @appeal_form_enabled = true
  @appeal_rate_limit = { max: 10, within: 60 }
  @appeal_guard = nil
  @appeal_human_verification_skip_if = nil
  @appeal_return_path = "/"

  # The public Art. 24 transparency report. OFF by default — opt in with
  # `config.transparency_report_enabled = true`. A *live* transparency portal is
  # not itself a legal requirement: the DSA obligation is to *publish* a report at
  # least annually (a static page/file is fine), and micro/small enterprises are
  # exempt from the transparency tier entirely (Art. 15(2) / Art. 19). So we don't
  # publicly expose moderation counts unless the host explicitly turns it on. When
  # off, the mounted `/transparency` route 404s; the aggregation stays queryable in
  # code so a host can still build/publish its own report.
  @transparency_report_enabled = false

  @signed_gid_purposes = %i[appeal confirm_notice unsubscribe]
end

Instance Attribute Details

#adaptersObject (readonly)

Returns the value of attribute adapters.



48
49
50
# File 'lib/moderate/configuration.rb', line 48

def adapters
  @adapters
end

#additional_wordsObject

Returns the value of attribute additional_words.



47
48
49
# File 'lib/moderate/configuration.rb', line 47

def additional_words
  @additional_words
end

#appeal_form_enabledObject

Returns the value of attribute appeal_form_enabled.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def appeal_form_enabled
  @appeal_form_enabled
end

#appeal_guardObject

Returns the value of attribute appeal_guard.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def appeal_guard
  @appeal_guard
end

#appeal_human_verification_skip_ifObject

Returns the value of attribute appeal_human_verification_skip_if.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def appeal_human_verification_skip_if
  @appeal_human_verification_skip_if
end

#appeal_rate_limitObject

Returns the value of attribute appeal_rate_limit.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def appeal_rate_limit
  @appeal_rate_limit
end

#appeal_return_pathObject

Returns the value of attribute appeal_return_path.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def appeal_return_path
  @appeal_return_path
end

#auditObject

— Hooks (all no-op by default) —————————————-



60
61
62
# File 'lib/moderate/configuration.rb', line 60

def audit
  @audit
end

#ban_handlerObject

— Hooks (all no-op by default) —————————————-



60
61
62
# File 'lib/moderate/configuration.rb', line 60

def ban_handler
  @ban_handler
end

#default_filter_modeObject

— Filtering ————————————————————



46
47
48
# File 'lib/moderate/configuration.rb', line 46

def default_filter_mode
  @default_filter_mode
end

#excluded_wordsObject

Returns the value of attribute excluded_words.



47
48
49
# File 'lib/moderate/configuration.rb', line 47

def excluded_words
  @excluded_words
end

#filter_adapterObject

— Filtering ————————————————————



46
47
48
# File 'lib/moderate/configuration.rb', line 46

def filter_adapter
  @filter_adapter
end

#filtersObject (readonly)

Returns the value of attribute filters.



48
49
50
# File 'lib/moderate/configuration.rb', line 48

def filters
  @filters
end

#localeObject

— Misc —————————————————————–



63
64
65
# File 'lib/moderate/configuration.rb', line 63

def locale
  @locale
end

#notice_captcha_verifierObject

Returns the value of attribute notice_captcha_verifier.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def notice_captcha_verifier
  @notice_captcha_verifier
end

#notice_form_enabledObject

Returns the value of attribute notice_form_enabled.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def notice_form_enabled
  @notice_form_enabled
end

#notice_guardObject

Returns the value of attribute notice_guard.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def notice_guard
  @notice_guard
end

#notice_human_verification_skip_ifObject

Returns the value of attribute notice_human_verification_skip_if.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def notice_human_verification_skip_if
  @notice_human_verification_skip_if
end

#notice_rate_limitObject

Returns the value of attribute notice_rate_limit.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def notice_rate_limit
  @notice_rate_limit
end

#notice_turnstile_secret_keyObject

Returns the value of attribute notice_turnstile_secret_key.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def notice_turnstile_secret_key
  @notice_turnstile_secret_key
end

#notice_turnstile_site_keyObject

Returns the value of attribute notice_turnstile_site_key.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def notice_turnstile_site_key
  @notice_turnstile_site_key
end

#notifyObject

— Hooks (all no-op by default) —————————————-



60
61
62
# File 'lib/moderate/configuration.rb', line 60

def notify
  @notify
end

#on_blockObject

— Hooks (all no-op by default) —————————————-



60
61
62
# File 'lib/moderate/configuration.rb', line 60

def on_block
  @on_block
end

#parent_controllerObject

— DSA notice form —————————————————— Documented in docs/dsa-notice-form.md. Held here so the engine/controller (written by other components) read them off the same Configuration object. ‘notice_guard` is the gem-absent fallback bot gate (see app/controllers/moderate/notices_controller.rb#verify_human!): a proc that receives the controller and returns truthy to allow the POST. nil/no-op ⇒ the form just works (the default). When the host installs `rails_cloudflare_turnstile`, the controller auto-uses Turnstile instead and this proc is bypassed.



73
74
75
# File 'lib/moderate/configuration.rb', line 73

def parent_controller
  @parent_controller
end

#report_categoriesObject

— Taxonomy (host-customizable) —————————————– Override the in-app COMMUNITY report category list. nil ⇒ the gem default (Moderate::Report::DEFAULT_CATEGORIES). Adding a category here requires NO migration: ‘category` is validated in the model (Moderate::Report), not by a DB check constraint. (The DSA legal-reason/country taxonomies are regulator-defined and NOT overridable.) A plain accessor — any value here is coerced to strings and compared at validation time by Report.report_categories.



57
58
59
# File 'lib/moderate/configuration.rb', line 57

def report_categories
  @report_categories
end

#signed_gid_purposesObject

Returns the value of attribute signed_gid_purposes.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def signed_gid_purposes
  @signed_gid_purposes
end

#transparency_report_enabledObject

Returns the value of attribute transparency_report_enabled.



74
75
76
# File 'lib/moderate/configuration.rb', line 74

def transparency_report_enabled
  @transparency_report_enabled
end

#user_classObject

— Identity ————————————————————-



43
44
45
# File 'lib/moderate/configuration.rb', line 43

def user_class
  @user_class
end

Instance Method Details

#adapter_for(name) ⇒ Object

Look up a registered adapter object by name, constantizing a String/Class reference (the built-ins) on first use. Returns nil for an unknown name —callers decide whether that’s an error (the validator/classify path raises a helpful message; see Moderate.classify).



219
220
221
222
223
224
225
226
227
# File 'lib/moderate/configuration.rb', line 219

def adapter_for(name)
  key = normalize_name(name)
  ref = @adapters[key]
  return nil if ref.nil?

  resolve_adapter(ref).tap do |adapter|
    @adapters[key] = adapter unless adapter.equal?(ref)
  end
end

#adapter_registered?(name) ⇒ Boolean

Returns:

  • (Boolean)


229
230
231
# File 'lib/moderate/configuration.rb', line 229

def adapter_registered?(name)
  @adapters.key?(normalize_name(name))
end

#filter(class_name, field, with: nil, mode: nil) ⇒ Object

Declare a per-field filter policy in the initializer — the twin of ‘moderates :field, with:, mode:` on the model. Stores the policy keyed by

class_name, field

so ‘filter_policy_for` can resolve it (including up the

ancestor chain) at classify time.



239
240
241
242
243
244
245
246
247
248
# File 'lib/moderate/configuration.rb', line 239

def filter(class_name, field, with: nil, mode: nil)
  name = class_name.is_a?(Class) ? class_name.name : class_name.to_s
  field_s = field.to_s
  adapter = with.nil? ? @filter_adapter : normalize_name(with)
  resolved_mode = mode.nil? ? @default_filter_mode : normalize_mode(mode)

  policy = FilterPolicy.new(class_name: name, field: field_s, adapter: adapter, mode: resolved_mode)
  @filters[[name, field_s]] = policy
  policy
end

#register_adapter(name, adapter) ⇒ Object

Register a host adapter under a name of the host’s choosing. The adapter is any object responding to ‘classify(value) → Moderate::Result`. The name is what gets recorded as `Moderate::Flag#source`, so it shows in the queue.

Both arg orders are accepted for ergonomics:

config.register_adapter :openai, OpenAIModerator.new
config.register_adapter(:openai, OpenAIModerator.new)


206
207
208
209
210
211
212
213
# File 'lib/moderate/configuration.rb', line 206

def register_adapter(name, adapter)
  key = normalize_name(name)
  unless adapter.is_a?(String) || adapter.is_a?(Class) || adapter.respond_to?(:classify)
    raise ArgumentError, "adapter for #{key.inspect} must respond to #classify"
  end

  @adapters[key] = adapter
end

#validate!Object

Cross-field validation run at the end of ‘Moderate.configure`. The per-setter checks already caught most typos; this catches the things that need the whole block resolved:

- the default text adapter must actually be registered
- every per-field filter must name a registered adapter
- a :block-mode filter must use a SYNCHRONOUS adapter — you can't reject a
  save on a background result, so :block + an async adapter (e.g. a remote
  classifier) is a configuration error. Async adapters run in :flag mode.
  (README: "`:block` requires a synchronous adapter".)


261
262
263
264
265
266
267
268
269
270
# File 'lib/moderate/configuration.rb', line 261

def validate!
  validate_adapter_name!(@filter_adapter, context: "filter_adapter")

  @filters.each_value do |policy|
    validate_adapter_name!(policy.adapter, context: "filter #{policy.class_name}##{policy.field}")
    validate_block_mode_adapter!(policy)
  end

  true
end