generated from discourse/discourse-plugin-skeleton
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
796 additions
and
224 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
# |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.