Class: Collavre::User

Inherits:
ApplicationRecord show all
Includes:
HasInboxCreative
Defined in:
app/models/collavre/user.rb

Constant Summary collapse

AGENT_CONF_DEFAULTS =

Default context settings for AI agents

{
  "context" => {
    "chat_history" => 50,
    "chat_history_size" => 100_000,
    "creative_children_level" => nil
  },
  "session" => { "enabled" => nil }
}.freeze
DEFAULT_CREATIVE_CHILDREN_LEVEL =

Default creative children level when agent_conf doesn’t specify one

6
TYPO_CORRECTION_DEVICES =
%w[voice soft_keyboard physical_keyboard].freeze
TYPO_CORRECTION_LOCATIONS =
%w[chat editor].freeze
LLM_VENDOR_OPTIONS =
[
  [ "Google (Gemini)", "google" ],
  [ "OpenAI", "openai" ],
  [ "Anthropic", "anthropic" ],
  [ "OpenClaw", "openclaw" ]
].freeze
SUPPORTED_LLM_MODELS =
[
  "gemini-3.1-flash-lite",
  "gemini-1.5-flash",
  "gemini-1.5-pro"
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HasInboxCreative

#inbox_creative

Class Method Details

.accessible_ai_agents_for(user) ⇒ Object



209
210
211
212
213
# File 'app/models/collavre/user.rb', line 209

def self.accessible_ai_agents_for(user)
  owned = ai_agents.where(created_by_id: user.id)
  searchable = ai_agents.where(searchable: true)
  owned.or(searchable).distinct.order(:name)
end

.mentionable_for(creative) ⇒ Object



215
216
217
218
219
220
221
222
223
# File 'app/models/collavre/user.rb', line 215

def self.mentionable_for(creative)
  scope = where(searchable: true)
  return scope unless creative

  origin = creative.effective_origin
  permitted_users = [ origin.user ].compact + origin.all_shared_users(:feedback).map(&:user)
  permitted_ids = permitted_users.compact.map(&:id)
  permitted_ids.any? ? scope.or(where(id: permitted_ids)) : scope
end

Instance Method Details

#ai_user?Boolean

Returns:

  • (Boolean)


199
200
201
# File 'app/models/collavre/user.rb', line 199

def ai_user?
  llm_vendor.present?
end

#chat_history_limitObject

Convenience accessors for context settings



118
119
120
# File 'app/models/collavre/user.rb', line 118

def chat_history_limit
  parsed_agent_conf.dig("context", "chat_history") || 50
end

#chat_history_size_limitObject



122
123
124
# File 'app/models/collavre/user.rb', line 122

def chat_history_size_limit
  parsed_agent_conf.dig("context", "chat_history_size") || 100_000
end

#claude_channel_agent?Boolean

Returns:

  • (Boolean)


203
204
205
# File 'app/models/collavre/user.rb', line 203

def claude_channel_agent?
  llm_model == "claude-code"
end

#creative_children_levelObject

Returns the creative children depth level for AI context. nil in agent_conf means use the default (6). 0 = no children, 1 = direct children only, 2 = grandchildren, etc.



129
130
131
132
# File 'app/models/collavre/user.rb', line 129

def creative_children_level
  level = parsed_agent_conf.dig("context", "creative_children_level")
  level.nil? ? DEFAULT_CREATIVE_CHILDREN_LEVEL : level.to_i
end

#destroy_creatives_leaf_firstObject

Destroy creatives deepest-first so closure_tree always finds its parent



315
316
317
318
319
320
# File 'app/models/collavre/user.rb', line 315

def destroy_creatives_leaf_first
  all_creatives = creatives.flat_map { |c| c.self_and_descendants.to_a }.uniq
  all_creatives.sort_by { |c| -c.self_and_ancestors.count }.each do |c|
    c.reload.destroy! if Creative.exists?(c.id)
  end
end

#display_nameObject



254
255
256
# File 'app/models/collavre/user.rb', line 254

def display_name
  name.presence || email
end

#email_verified?Boolean

Returns:

  • (Boolean)


250
251
252
# File 'app/models/collavre/user.rb', line 250

def email_verified?
  email_verified_at.present?
end

#lock_account!Object



263
264
265
# File 'app/models/collavre/user.rb', line 263

def lock_account!
  update_columns(locked_at: Time.current)
end

#locked?Boolean

Account lockout methods

Returns:

  • (Boolean)


259
260
261
# File 'app/models/collavre/user.rb', line 259

def locked?
  locked_at.present? && locked_at > Collavre::SystemSetting.lockout_duration.ago
end

#parsed_agent_confObject

Returns parsed agent_conf merged with defaults



106
107
108
109
110
111
112
113
114
115
# File 'app/models/collavre/user.rb', line 106

def parsed_agent_conf
  defaults = AGENT_CONF_DEFAULTS.deep_dup
  return defaults if agent_conf.blank?

  user_conf = YAML.safe_load(agent_conf, permitted_classes: [ Symbol ]) || {}
  defaults.deep_merge(user_conf)
rescue Psych::SyntaxError => e
  Rails.logger.warn("[User#parsed_agent_conf] Invalid YAML for user #{id}: #{e.message}")
  AGENT_CONF_DEFAULTS.deep_dup
end

#password_meets_minimum_lengthObject



289
290
291
292
293
294
295
296
# File 'app/models/collavre/user.rb', line 289

def password_meets_minimum_length
  return if password.blank?

  min_length = Collavre::SystemSetting.password_min_length
  if password.length < min_length
    errors.add(:password, :too_short, count: min_length)
  end
end

#preserve_durable_summary_commentsObject

Keep durable compress/merge summaries (snapshot result comments) alive when their author is deleted: nullify authorship instead of cascading destroy.



308
309
310
311
312
# File 'app/models/collavre/user.rb', line 308

def preserve_durable_summary_comments
  Collavre::Comment
    .where(id: Collavre::CommentSnapshot.where(result_comment_id: comments.select(:id)).select(:result_comment_id))
    .update_all(user_id: nil)
end

#record_failed_login!Object



271
272
273
274
275
276
277
278
# File 'app/models/collavre/user.rb', line 271

def record_failed_login!
  new_count = ( || 0) + 1
  if new_count >= Collavre::SystemSetting.
    update_columns(failed_login_attempts: new_count, locked_at: Time.current)
  else
    update_column(:failed_login_attempts, new_count)
  end
end

#remaining_lockout_timeObject



284
285
286
287
# File 'app/models/collavre/user.rb', line 284

def remaining_lockout_time
  return 0 unless locked?
  ((locked_at + Collavre::SystemSetting.lockout_duration) - Time.current).to_i
end

#reset_failed_login_attempts!Object



280
281
282
# File 'app/models/collavre/user.rb', line 280

def 
  update_column(:failed_login_attempts, 0) if .to_i > 0
end

#supports_session?Boolean

Whether this agent uses stateful sessions (incremental messaging). nil in agent_conf = auto-detect by vendor (openclaw → true).

Returns:

  • (Boolean)


136
137
138
139
140
141
# File 'app/models/collavre/user.rb', line 136

def supports_session?
  explicit = parsed_agent_conf.dig("session", "enabled")
  return ActiveModel::Type::Boolean.new.cast(explicit) unless explicit.nil?

  llm_vendor&.downcase == "openclaw"
end

#theme_accessibilityObject



298
299
300
301
302
303
304
# File 'app/models/collavre/user.rb', line 298

def theme_accessibility
  return if theme.blank? || %w[light dark].include?(theme)

  unless user_themes.exists?(id: theme)
    errors.add(:theme, "is invalid")
  end
end

#typo_correction_active_for?(device:, location:) ⇒ Boolean

2D gating: typo correction runs only when the master switch is on AND the originating typing device AND the input location are both enabled. Unknown device/location values are treated as disabled (fail closed).

Returns:

  • (Boolean)


167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'app/models/collavre/user.rb', line 167

def typo_correction_active_for?(device:, location:)
  return false unless typo_correction_enabled

  device_on = case device.to_s
  when "voice" then typo_correction_on_voice
  when "soft_keyboard" then typo_correction_on_soft_keyboard
  when "physical_keyboard" then typo_correction_on_physical_keyboard
  else false
  end

  location_on = case location.to_s
  when "chat" then typo_correction_in_chat
  when "editor" then typo_correction_in_editor
  else false
  end

  device_on && location_on
end

#unlock_account!Object



267
268
269
# File 'app/models/collavre/user.rb', line 267

def unlock_account!
  update_columns(locked_at: nil, failed_login_attempts: 0)
end