FEATURE: Custom unsubscribe options (#17090)

With this change, plugins can create custom unsubscribe keys, extend the unsubscribe view with custom preferences, and decide how they are updated.
This commit is contained in:
Roman Rizzi
2022-06-21 15:49:47 -03:00
committed by GitHub
parent deee3c6f02
commit e0ba35350e
15 changed files with 376 additions and 246 deletions

View File

@@ -95,6 +95,8 @@ class DiscoursePluginRegistry
define_filtered_register :notification_consolidation_plans
define_filtered_register :email_unsubscribers
def self.register_auth_provider(auth_provider)
self.auth_providers << auth_provider
end

View File

@@ -0,0 +1,57 @@
# frozen_string_literal: true
# This class and its children are instantiated and used by the EmailController.
module EmailControllerHelper
class BaseEmailUnsubscriber
def initialize(unsubscribe_key)
@unsubscribe_key = unsubscribe_key
end
attr_reader :unsubscribe_key
# Sets instance variables in the `EmailController#unsubscribe`, which are later available in the view.
# Don't forget to call super when extending this method.
def prepare_unsubscribe_options(controller)
controller.instance_variable_set(:@digest_unsubscribe, false)
controller.instance_variable_set(:@watched_count, nil)
controller.instance_variable_set(:@type, unsubscribe_key.unsubscribe_key_type)
controller.instance_variable_set(:@user, key_owner)
controller.instance_variable_set(
:@unsubscribed_from_all,
key_owner.user_option.unsubscribed_from_all?
)
end
# Called by the `EmailController#perform_unsubscribe` and defines what unsubscribing means.
#
# Receives the request params and returns a boolean indicating if any preferences were updated.
#
# Don't forget to call super when extending this method.
def unsubscribe(params)
updated = false
if params[:disable_mailing_list]
key_owner.user_option.update_columns(mailing_list_mode: false)
updated = true
end
if params[:unsubscribe_all]
key_owner.user_option.update_columns(email_digests: false,
email_level: UserOption.email_level_types[:never],
email_messages_level: UserOption.email_level_types[:never],
mailing_list_mode: false)
updated = true
end
updated
end
protected
def key_owner
unsubscribe_key.user
end
end
end

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
module EmailControllerHelper
class DigestEmailUnsubscriber < BaseEmailUnsubscriber
def prepare_unsubscribe_options(controller)
super(controller)
controller.instance_variable_set(:@digest_unsubscribe, !SiteSetting.disable_digest_emails)
frequency_in_minutes = key_owner.user_option.digest_after_minutes
email_digests = key_owner.user_option.email_digests
frequencies = DigestEmailSiteSetting.values.dup
never = frequencies.delete_at(0)
allowed_frequencies = %w[never weekly every_month every_six_months]
result = frequencies.reduce(frequencies: [], current: nil, selected: nil, take_next: false) do |memo, v|
memo[:current] = v[:name] if v[:value] == frequency_in_minutes && email_digests
next(memo) unless allowed_frequencies.include?(v[:name])
memo.tap do |m|
m[:selected] = v[:value] if m[:take_next] && email_digests
m[:frequencies] << [I18n.t("unsubscribe.digest_frequency.#{v[:name]}"), v[:value]]
m[:take_next] = !m[:take_next] && m[:current]
end
end
digest_frequencies = result.slice(:frequencies, :current, :selected).tap do |r|
r[:frequencies] << [I18n.t("unsubscribe.digest_frequency.#{never[:name]}"), never[:value]]
r[:selected] ||= never[:value]
r[:current] ||= never[:name]
end
controller.instance_variable_set(:@digest_frequencies, digest_frequencies)
end
def unsubscribe(params)
updated = super(params)
if params[:digest_after_minutes]
digest_frequency = params[:digest_after_minutes].to_i
unsubscribe_key.user.user_option.update_columns(
digest_after_minutes: digest_frequency,
email_digests: digest_frequency.positive?
)
updated = true
end
updated
end
end
end

View File

@@ -0,0 +1,62 @@
# frozen_string_literal: true
module EmailControllerHelper
class TopicEmailUnsubscriber < BaseEmailUnsubscriber
def prepare_unsubscribe_options(controller)
super(controller)
watching = TopicUser.notification_levels[:watching]
topic = unsubscribe_key.associated_topic
controller.instance_variable_set(:@topic, topic)
controller.instance_variable_set(
:@watching_topic,
TopicUser.exists?(user: key_owner, notification_level: watching, topic_id: topic.id)
)
return if topic.category_id.blank?
return if !CategoryUser.exists?(user: key_owner, notification_level: CategoryUser.watching_levels, category_id: topic.category_id)
controller.instance_variable_set(
:@watched_count,
TopicUser.joins(:topic)
.where(user: key_owner, notification_level: watching).where(topics: { category_id: topic.category_id }).count
)
end
def unsubscribe(params)
updated = super(params)
topic = unsubscribe_key.associated_topic
return updated if topic.nil?
if params[:unwatch_topic]
TopicUser.where(topic_id: topic.id, user_id: key_owner.id)
.update_all(notification_level: TopicUser.notification_levels[:tracking])
updated = true
end
if params[:unwatch_category] && topic.category_id
TopicUser.joins(:topic)
.where(user: key_owner, notification_level: TopicUser.notification_levels[:watching])
.where(topics: { category_id: topic.category_id })
.update_all(notification_level: TopicUser.notification_levels[:tracking])
CategoryUser
.where(user_id: key_owner.id, category_id: topic.category_id, notification_level: CategoryUser.watching_levels)
.destroy_all
updated = true
end
if params[:mute_topic]
TopicUser.where(topic_id: topic.id, user_id: key_owner.id).update_all(notification_level: TopicUser.notification_levels[:muted])
updated = true
end
updated
end
end
end

View File

@@ -1018,6 +1018,29 @@ class Plugin::Instance
StaticController::CUSTOM_PAGES[page] = blk ? { topic_id: blk } : options
end
# Let plugin define custom unsubscribe keys,
# set custom instance variables on the `EmailController#unsubscribe` action,
# and describe what unsubscribing for that key does.
#
# The method receives a class that inherits from `Email::BaseEmailUnsubscriber`.
# Take a look at it to know how to implement your child class.
#
# In conjunction with this, you'll have to:
#
# - Register a new connector under app/views/connectors/unsubscribe_options.
# We'll include the HTML inside the unsubscribe form, so you can add your fields using the
# instance variables you set in the controller previously. When the form is submitted,
# it sends the updated preferences to `EmailController#perform_unsubscribe`.
#
# - Your code is responsible for creating the custom key by calling `UnsubscribeKey#create_key_for`.
def register_email_unsubscriber(type, unsubscriber)
core_types = [UnsubscribeKey::ALL_TYPE, UnsubscribeKey::DIGEST_TYPE, UnsubscribeKey::TOPIC_TYPE]
raise ArgumentError.new('Type already exists') if core_types.include?(type)
raise ArgumentError.new('Not an email unsubscriber') if !unsubscriber.ancestors.include?(EmailControllerHelper::BaseEmailUnsubscriber)
DiscoursePluginRegistry.register_email_unsubscriber({ type => unsubscriber }, self)
end
protected
def self.js_path