Skip to content

Commit

Permalink
dev: add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
pangbo13 committed Apr 10, 2024
1 parent a6ec556 commit f2ef4aa
Show file tree
Hide file tree
Showing 15 changed files with 796 additions and 224 deletions.
47 changes: 47 additions & 0 deletions app/controllers/remake_limit_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module ::RemakeLimit
class RemakeLimitController < ::ApplicationController
def fetch_record_from_params
query_args = params.permit(:user_id, :email, :jaccount_name, :jaccount_id)
if query_args.keys.length == 0
raise Discourse::InvalidParameters.new("At least one of user_id, email, jaccount_name, jaccount_id should be provided")
end
UserDeletionLog.where(query_args)
end

def query
record = fetch_record_from_params
raise Discourse::NotFound.new("Record not found") if record.length == 0
render_serialized(record, UserDeletionLogSerializer)
end

def ignore
params.require(:id)
record = UserDeletionLog.find_by(id: params[:id])
raise Discourse::NotFound.new("Record not found") if record.nil?
record.ignore_limit = true
record.save!
render json: { success: "ok" }
end

def create_for_user
params.require(:user_id)
user = User.find_by(id: params[:user_id])
raise Discourse::NotFound.new("User not found") if user.nil?
record = UserDeletionLog.create_log(user, refresh_delete_time: true)
if record.nil?
render json: { success: "fail"} , status: :unprocessable_entity
else
render json: { success: "ok" }
end
end

def ignore_for_user
params.require(:user_id)
record = UserDeletionLog.find_by(user_id: params[:user_id])
raise Discourse::NotFound.new("Record not found") if record.nil?
record.ignore_limit = true
record.save!
render json: { success: "ok", record: UserDeletionLogSerializer.new(record).as_json }
end
end
end
178 changes: 108 additions & 70 deletions app/models/user_deletion_log.rb
Original file line number Diff line number Diff line change
@@ -1,85 +1,123 @@
# frozen_string_literal: true

# Note: 1.`email` and `jaccount_name` are stored in downcase
# `jaccount_id` is stored in original
# 2. If `ignore_limit` is true, do not count this record when calculate cooldown time
# 3. `user_deleted_at` is the time when user is deleted,
# do not use `created_at` to calculate cooldown time

class UserDeletionLog < ActiveRecord::Base
JACCOUNT_PROVIDER_NAME ||= 'jaccount'.freeze
def self.create_log(user, refresh_delete_time = true, ignore_limit = false)
record = UserDeletionLog.find_or_initialize_by(user_id: user.id)
record.username = user.username
record.email = user.email.downcase
jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME)
if jaccount_account.nil?
Rails.logger.warn("User #{user.id} has no associated jaccount")
else
record.jaccount_id = jaccount_account.provider_uid
record.jaccount_name = jaccount_account.info&.[]('account').downcase
if record.jaccount_name.nil?
Rails.logger.warn("User #{user.id} has an associated jaccount, but has no jaccount_name \n #{jaccount_account}")
end
end
pc = TrustLevel3Requirements.new(user).penalty_counts_all_time
record.silence_count = pc.silenced
record.suspend_count = pc.suspended
if record.user_deleted_at.nil? && ignore_limit
# A new record and ignore_limit is true
record.ignore_limit = true
end
if record.user_deleted_at.nil? || refresh_delete_time
record.user_deleted_at = Time.now
end
record.save!
JACCOUNT_PROVIDER_NAME ||= 'jaccount'.freeze
def self.create_log(user, refresh_delete_time: true, ignore_limit: false)
record = UserDeletionLog.find_or_initialize_by(user_id: user.id)
record.username = user.username
record.email = user.email.downcase
jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME)
if jaccount_account.nil?
Rails.logger.warn("User #{user.id} has no associated jaccount")
else
record.jaccount_id = jaccount_account.provider_uid
record.jaccount_name = jaccount_account.info&.[]('account').downcase
if record.jaccount_name.nil?
Rails.logger.warn("User #{user.id} has an associated jaccount, but has no jaccount_name \n #{jaccount_account}")
end
end

def self.find_latest_time_by_email(email,jaccount_name=nil)
email = email.downcase
if jaccount_name.nil?
jaccount_name = email.split("@").first
end
record = UserDeletionLog.where("email = ? OR jaccount_name = ?",email,jaccount_name).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first
record&.user_deleted_at
pc = TrustLevel3Requirements.new(user).penalty_counts_all_time
record.silence_count = pc.silenced
record.suspend_count = pc.suspended
if record.user_deleted_at.nil? && ignore_limit
# A new record and ignore_limit is true
record.ignore_limit = true
end

def self.find_latest_time_by_jaccount_id(jaccount_id)
record = UserDeletionLog.where("jaccount_id = ?",jaccount_id).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first
record&.user_deleted_at
if record.user_deleted_at.nil? || refresh_delete_time
record.user_deleted_at = Time.now
end
record.save!
record
end

def self.find_latest_time(user)
jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME)
jaccount_id = jaccount_account.provider_uid
jaccount_name = jaccount_account.info&.[]('account')&.downcase
email = user.email.downcase

record = UserDeletionLog.where("email = ? OR jaccount_name = ? OR jaccount_id = ?",email,jaccount_name,jaccount_id).where("user_id != ? ",user.id).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first
record&.user_deleted_at
def self.find_latest_time_by_email(email, jaccount_name: nil)
email = email.downcase
if jaccount_name.nil?
jaccount_name = email.split("@").first
elsif email.split("@").first != jaccount_name.downcase \
&& email.split("@").last == "sjtu.edu.cn"
Rails.logger.warn("email and jaccount_name do not match: #{email} #{jaccount_name}")
end
record = UserDeletionLog.where("email = ? OR jaccount_name = ?",email,jaccount_name).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first
record&.user_deleted_at
end

def self.find_latest_time_by_jaccount_id(jaccount_id)
record = UserDeletionLog.where("jaccount_id = ?",jaccount_id).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first
record&.user_deleted_at
end

def self.find_user_penalty_history(user, ignore_jaccount_not_found = false)
# ignore `ignore_limit` field as it is only used for cooldown time calculation
# do not count current user
email = user.email
jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME)
if jaccount_account.nil?
if !ignore_jaccount_not_found
Rails.logger.warn("User #{user.id} has no jaccount_account")
end
records = UserDeletionLog.where(email: email).where("user_id != ? ",user.id)
else
jaccount_id = jaccount_account.provider_uid
jaccount_name = jaccount_account.info&.[]('account')&.downcase
def self.find_latest_time(user)
jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME)
jaccount_id = jaccount_account.provider_uid
jaccount_name = jaccount_account.info&.[]('account')&.downcase
email = user.email.downcase

if !jaccount_name.nil?
records = UserDeletionLog.where("email = ? OR jaccount_name = ? OR jaccount_id = ?",email,jaccount_name,jaccount_id).where("user_id != ? ",user.id)
else
records = UserDeletionLog.where("email = ? OR jaccount_id = ?",email,jaccount_id).where("user_id != ? ",user.id)
end
end
account_count = records.count
silence_count = records.sum(:silence_count)
suspend_count = records.sum(:suspend_count)
return account_count, silence_count, suspend_count
record = UserDeletionLog.where("email = ? OR jaccount_name = ? OR jaccount_id = ?",email,jaccount_name,jaccount_id).where("user_id != ? ",user.id).where("user_deleted_at is NOT NULL").where(ignore_limit:false).order(user_deleted_at: :desc).first
record&.user_deleted_at
end

def self.find_user_penalty_history(user, ignore_jaccount_not_found: false)
# ignore `ignore_limit` field as it is only used for cooldown time calculation
# do not count current user

# DEVELOPMENT NOTE: BUG && WON'T FIX
# `user_id != ?` will always return false if user_id is NULL in DB
# this is an unexpected behavior
# However, those affected records came from migration of old data
# and they do not have penalty counts either
# Alough it is a bug, it can still return correct (mostly) result
email = user.email
jaccount_account = user.user_associated_accounts.find_by(provider_name: JACCOUNT_PROVIDER_NAME)
if jaccount_account.nil?
if !ignore_jaccount_not_found
Rails.logger.warn("User #{user.id} has no jaccount_account")
end
records = UserDeletionLog.where(email: email).where("user_id != ?",user.id)
else
jaccount_id = jaccount_account.provider_uid
jaccount_name = jaccount_account.info&.[]('account')&.downcase

if !jaccount_name.nil?
records = UserDeletionLog.where("email = ? OR jaccount_name = ? OR jaccount_id = ?",email,jaccount_name,jaccount_id).where("user_id != ? ",user.id)
else
records = UserDeletionLog.where("email = ? OR jaccount_id = ?",email,jaccount_id).where("user_id != ? ",user.id)
end
end
end
account_count = records.count
silence_count = records.sum(:silence_count)
suspend_count = records.sum(:suspend_count)
return account_count, silence_count, suspend_count
end
end

# == Schema Information
# Schema version: 20240327000440
#
# Table name: user_deletion_logs
#
# id :bigint not null, primary key
# user_id :integer
# username :string
# email :string
# jaccount_name :string
# jaccount_id :string
# silence_count :integer
# suspend_count :integer
# ignore_limit :boolean default(FALSE)
# user_deleted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_user_deletion_logs_on_email (email)
# index_user_deletion_logs_on_jaccount_id (jaccount_id)
# index_user_deletion_logs_on_jaccount_name (jaccount_name)
#
13 changes: 13 additions & 0 deletions app/serializers/user_deletion_log_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module ::RemakeLimit
class UserDeletionLogSerializer < ::ApplicationSerializer
attributes :id
attributes :user_id
attributes :username
attributes :user_deleted_at
attributes :ignore_limit
attributes :silence_count
attributes :suspend_count
end
end
36 changes: 36 additions & 0 deletions lib/override_jaccount_authenticator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module ::RemakeLimit
module OverrideJAccountAuthenticator
def after_authenticate(auth_token)
result = super(auth_token)
if result.failed || !result.user.nil? || !SiteSetting.remake_limit_enabled
return result
else
# For more detail:
# https://github.com/ShuiyuanSJTU/discourse-omniauth-jaccount/blob/e535f263fbfa71149d14b75b141cbb4827eb5498/plugin.rb#L147-L155
email = result.email.downcase
jaccount_name = result.username.downcase
jaccount_id = result.extra_data[:jaccount_uid]
old_by_email = UserDeletionLog.find_latest_time_by_email(email, jaccount_name: jaccount_name)
old_by_jaccount_id = UserDeletionLog.find_latest_time_by_jaccount_id(jaccount_id)
# find the latest time, use compact to remove nil
old = [old_by_email, old_by_jaccount_id].compact.max
if !old.nil?
time = old.to_datetime + SiteSetting.remake_limit_period.days
if Time.now < time
result.failed = true
result.failed_reason = "您的账号正处于注册限制期,请于#{time.in_time_zone('Asia/Shanghai').strftime("%Y-%m-%d %H:%M:%S %Z")}之后再登录!"
result.name = nil
result.username = nil
result.email = nil
result.email_valid = nil
result.extra_data = nil
result
end
end
return result
end
end
end
end
39 changes: 39 additions & 0 deletions lib/override_trust_level_3_requirements.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module RemakeLimit
module OverrideTrustLevel3Requirements
def penalty_counts_all_time
args = {
user_id: @user.id,
system_user_id: Discourse.system_user.id,
silence_user: UserHistory.actions[:silence_user],
unsilence_user: UserHistory.actions[:unsilence_user],
suspend_user: UserHistory.actions[:suspend_user],
unsuspend_user: UserHistory.actions[:unsuspend_user],
}

sql = <<~SQL
SELECT
SUM(
CASE
WHEN action = :silence_user THEN 1
WHEN action = :unsilence_user AND acting_user_id != :system_user_id THEN -1
ELSE 0
END
) AS silence_count,
SUM(
CASE
WHEN action = :suspend_user THEN 1
WHEN action = :unsuspend_user AND acting_user_id != :system_user_id THEN -1
ELSE 0
END
) AS suspend_count
FROM user_histories AS uh
WHERE uh.target_user_id = :user_id
AND uh.action IN (:silence_user, :suspend_user, :unsilence_user, :unsuspend_user)
SQL

::TrustLevel3Requirements::PenaltyCounts.new(@user, DB.query_hash(sql, args).first)
end
end
end
59 changes: 59 additions & 0 deletions lib/override_users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module RemakeLimit
module OverrideUsersController
extend ActiveSupport::Concern

prepended do
before_action :check_remake_limit, only: [:create]
before_action :add_remake_limit, only: [:destroy]
after_action :add_user_note, only: [:create]
end

def check_remake_limit
if SiteSetting.remake_limit_enabled
old = UserDeletionLog.find_latest_time_by_email(params[:email])
if old
time = old.to_datetime + SiteSetting.remake_limit_period.days
if Time.now < time
render json: { success: false, message: "您的邮箱正处于注册限制期,请于#{time.in_time_zone('Asia/Shanghai').strftime("%Y-%m-%d %H:%M:%S %Z")}之后再注册!" }
end
end
end
end

def add_user_note
# add penalty history to user notes
if defined?(::DiscourseUserNotes)
begin
user = fetch_user_from_params(include_inactive:true)
rescue Discourse::NotFound
rails_logger.warn("User not found when adding user note")
return
end
account_count, silence_count, suspend_count = UserDeletionLog.find_user_penalty_history(user)
if account_count > 0
::DiscourseUserNotes.add_note(
user,
I18n.t("remake_limit.user_note_text", account_count: account_count, silence_count: silence_count, suspend_count: suspend_count),
Discourse.system_user.id
)
end
end
end

def add_remake_limit
if SiteSetting.remake_limit_enabled
user = fetch_user_from_params
guardian.ensure_can_delete_user!(user)
::UserDeletionLog.create_log(user, refresh_delete_time: true)
if defined?(::DiscourseUserNotes)
::DiscourseUserNotes.add_note(user, "用户尝试删除账号", Discourse.system_user.id)
end
if user.silenced? && !SiteSetting.remake_silenced_can_delete
render json: { error: "您的账号处于禁言状态,无法自助删除账户,请与管理人员联系!" }, status: :unprocessable_entity
end
end
end
end
end
Loading

0 comments on commit f2ef4aa

Please sign in to comment.