Files
gochat/docs/requirements/M4-codegraph-context.md
T
2026-06-04 15:44:48 +08:00

8.1 KiB

Code Context

Query: contact management, contact inbox, custom attribute definition, custom filter, contact merge, contact search, CRM, note, label

Entry Points

  • AddRegexToCustomAttributeDefinition (class) - db/migrate/20230905060223_add_regex_to_custom_attribute_definition.rb:1
  • CustomAttributeDefinition (class) - app/models/custom_attribute_definition.rb:24
  • CustomAttributeDefinitionPolicy (class) - app/policies/custom_attribute_definition_policy.rb:1
  • app/models/application_record.rb: ApplicationRecord:1
  • app/policies/application_policy.rb: ApplicationPolicy:1
  • db/migrate/20230905060223_add_regex_to_custom_attribute_definition.rb: change:2
  • app/models/custom_attribute_definition.rb: normalize_attribute_fields:54, sync_widget_pre_chat_custom_fields:59, update_widget_pre_chat_custom_fields:63, attribute_must_not_conflict:67
  • app/policies/custom_attribute_definition_policy.rb: index?:2, show?:6, create?:10

Code

AddRegexToCustomAttributeDefinition (db/migrate/20230905060223_add_regex_to_custom_attribute_definition.rb:1)

class AddRegexToCustomAttributeDefinition < ActiveRecord::Migration[7.0]
  def change
    add_column :custom_attribute_definitions, :regex_pattern, :string
    add_column :custom_attribute_definitions, :regex_cue, :string
  end
end

CustomAttributeDefinition (app/models/custom_attribute_definition.rb:24)

class CustomAttributeDefinition < ApplicationRecord
  STANDARD_ATTRIBUTES = {
    :conversation => %w[status priority assignee_id inbox_id team_id display_id campaign_id labels browser_language country_code referer created_at
                        last_activity_at],
    :contact => %w[name email phone_number identifier country_code city company_name created_at last_activity_at referer blocked],
    :company => %w[name domain description contacts_count created_at updated_at last_activity_at]
  }.freeze

  scope :with_attribute_model, ->(attribute_model) { attribute_model.presence && where(attribute_model: attribute_model) }
  validates :attribute_display_name, presence: true
  before_validation :normalize_attribute_fields

  validates :attribute_key,
            presence: true,
            uniqueness: { scope: [:account_id, :attribute_model] },
            format: { with: /\A[\p{L}\p{N}_.\-]+\z/, message: I18n.t('errors.custom_attribute_definition.attribute_key_format') }

  validates :attribute_display_type, presence: true
  validates :attribute_model, presence: true
  validate :attribute_must_not_conflict, on: :create

  enum attribute_model: { conversation_attribute: 0, contact_attribute: 1, company_attribute: 2 }
  enum attribute_display_type: { text: 0, number: 1, currency: 2, percent: 3, link: 4, date: 5, list: 6, checkbox: 7 }

  belongs_to :account
  after_update :update_widget_pre_chat_custom_fields, unless: :company_attribute?
  after_destroy :sync_widget_pre_chat_
// ... truncated ...

CustomAttributeDefinitionPolicy (app/policies/custom_attribute_definition_policy.rb:1)

class CustomAttributeDefinitionPolicy < ApplicationPolicy
  def index?
    @account_user.administrator? || @account_user.agent?
  end

  def show?
    @account_user.administrator? || @account_user.agent?
  end

  def create?
    @account_user.administrator?
  end

  def update?
    @account_user.administrator?
  end

  def destroy?
    @account_user.administrator?
  end
end

change (db/migrate/20230905060223_add_regex_to_custom_attribute_definition.rb:2)

  def change
    add_column :custom_attribute_definitions, :regex_pattern, :string
    add_column :custom_attribute_definitions, :regex_cue, :string
  end

normalize_attribute_fields (app/models/custom_attribute_definition.rb:54)

  def normalize_attribute_fields
    self.attribute_key = attribute_key.strip if attribute_key.present?
    self.attribute_display_name = attribute_display_name.strip if attribute_display_name.present?
  end

sync_widget_pre_chat_custom_fields (app/models/custom_attribute_definition.rb:59)

  def sync_widget_pre_chat_custom_fields
    ::Inboxes::SyncWidgetPreChatCustomFieldsJob.perform_later(account, attribute_key)
  end

update_widget_pre_chat_custom_fields (app/models/custom_attribute_definition.rb:63)

  def update_widget_pre_chat_custom_fields
    ::Inboxes::UpdateWidgetPreChatCustomFieldsJob.perform_later(account, self)
  end

attribute_must_not_conflict (app/models/custom_attribute_definition.rb:67)

  def attribute_must_not_conflict
    model_keys = attribute_model.to_s.delete_suffix('_attribute').to_sym
    standard_attributes = STANDARD_ATTRIBUTES[model_keys]
    return if standard_attributes.blank?
    return unless attribute_key.in?(standard_attributes)

    errors.add(:attribute_key, I18n.t('errors.custom_attribute_definition.key_conflict'))
  end

index? (app/policies/custom_attribute_definition_policy.rb:2)

  def index?
    @account_user.administrator? || @account_user.agent?
  end

show? (app/policies/custom_attribute_definition_policy.rb:6)

  def show?
    @account_user.administrator? || @account_user.agent?
  end

create? (app/policies/custom_attribute_definition_policy.rb:10)

  def create?
    @account_user.administrator?
  end

update? (app/policies/custom_attribute_definition_policy.rb:14)

  def update?
    @account_user.administrator?
  end

destroy? (app/policies/custom_attribute_definition_policy.rb:18)

  def destroy?
    @account_user.administrator?
  end

ApplicationRecord (app/models/application_record.rb:1)

class ApplicationRecord < ActiveRecord::Base
  include Events::Types
  self.abstract_class = true

  before_validation :validates_column_content_length

  # the models that exposed in email templates through liquid
  def droppables
    %w[Account Channel Conversation Inbox User Message]
  end

  # ModelDrop class should exist in app/drops
  def to_drop
    return unless droppables.include?(self.class.name)

    "#{self.class.name}Drop".constantize.new(self)
  end

  private

  # Generic validation for all columns of type string and text
  # Validates the length of the column to prevent DOS via large payloads
  # if a custom length validation is already present, skip the validation
  def validates_column_content_length
    self.class.columns.each do |column|
      check_and_validate_content_length(column) if column_of_type_string_or_text?(column)
    end
  end

  def column_of_type_string_or_text?(column)
    %i[string text].include?(column.type)
  end

  def check_and_validate_content_length(column)
    length_validator = self.class.validators_on(column.name).find { |v| v.kind == :length }
    validate_content_length(column) if length_validator.blank?
  end

  def validate_content_length(column)
    max_length = column.type == :text ? 20_000 : 255
    return if self[column.name].nil? || self[column.name].length <= max_length

    errors.add(column.name.to_sym, "is too long (maximum is #{max_length} characters)")
  end

  def normalize_empty_string_to_nil(attrs = [])
    attrs
// ... truncated ...

ApplicationPolicy (app/policies/application_policy.rb:1)

class ApplicationPolicy
  attr_reader :user_context, :user, :record, :account, :account_user

  def initialize(user_context, record)
    @user_context = user_context
    @user = user_context[:user]
    @account = user_context[:account]
    @account_user = user_context[:account_user]
    @record = record
  end

  def index?
    false
  end

  def show?
    scope.exists?(id: record.id)
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  def scope
    Pundit.policy_scope!(user_context, record.class)
  end

  class Scope
    attr_reader :user_context, :user, :scope, :account, :account_user

    def initialize(user_context, scope)
      @user_context = user_context
      @user = user_context[:user]
      @account = user_context[:account]
      @account_user = user_context[:account_user]
      @scope = scope
    end

    def resolve
      scope
    end
  end
end