2019-05-03 06:17:27 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
#
|
|
|
|
# A class that handles interaction between a plugin and the Discourse App.
|
|
|
|
#
|
|
|
|
class DiscoursePluginRegistry
|
2024-07-09 23:56:22 +08:00
|
|
|
@@register_names = Set.new
|
|
|
|
|
2022-12-19 11:46:17 +08:00
|
|
|
# Plugins often need to be able to register additional handlers, data, or
|
|
|
|
# classes that will be used by core classes. This should be used if you
|
|
|
|
# need to control which type the registry is, and if it doesn't need to
|
|
|
|
# be removed if the plugin is disabled.
|
|
|
|
#
|
2020-05-13 19:25:34 +08:00
|
|
|
# Shortcut to create new register in the plugin registry
|
|
|
|
# - Register is created in a class variable using the specified name/type
|
|
|
|
# - Defines singleton method to access the register
|
|
|
|
# - Defines instance method as a shortcut to the singleton method
|
2022-12-19 11:46:17 +08:00
|
|
|
# - Automatically deletes the register on registry.reset!
|
2020-05-13 19:25:34 +08:00
|
|
|
def self.define_register(register_name, type)
|
2024-07-09 23:56:22 +08:00
|
|
|
return if respond_to?(register_name)
|
2020-05-13 19:25:34 +08:00
|
|
|
@@register_names << register_name
|
|
|
|
|
|
|
|
define_singleton_method(register_name) do
|
|
|
|
instance_variable_get(:"@#{register_name}") ||
|
|
|
|
instance_variable_set(:"@#{register_name}", type.new)
|
|
|
|
end
|
|
|
|
|
|
|
|
define_method(register_name) { self.class.public_send(register_name) }
|
|
|
|
end
|
|
|
|
|
2022-12-19 11:46:17 +08:00
|
|
|
# Plugins often need to add values to a list, and we need to filter those
|
|
|
|
# lists at runtime to ignore values from disabled plugins. Unlike define_register,
|
|
|
|
# the type of the register cannot be defined, and is always Array.
|
|
|
|
#
|
2020-05-15 21:04:38 +08:00
|
|
|
# Create a new register (see `define_register`) with some additions:
|
|
|
|
# - Register is created in a class variable using the specified name/type
|
|
|
|
# - Defines singleton method to access the register
|
|
|
|
# - Defines instance method as a shortcut to the singleton method
|
2022-12-19 11:46:17 +08:00
|
|
|
# - Automatically deletes the register on registry.reset!
|
2020-05-15 21:04:38 +08:00
|
|
|
def self.define_filtered_register(register_name)
|
2024-07-09 23:56:22 +08:00
|
|
|
return if respond_to?(register_name)
|
2020-05-15 21:04:38 +08:00
|
|
|
define_register(register_name, Array)
|
|
|
|
|
|
|
|
singleton_class.alias_method :"_raw_#{register_name}", :"#{register_name}"
|
|
|
|
|
|
|
|
define_singleton_method(register_name) do
|
2024-07-09 23:56:22 +08:00
|
|
|
public_send(:"_raw_#{register_name}").filter_map { |h| h[:value] if h[:plugin].enabled? }.uniq
|
2020-05-15 21:04:38 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
define_singleton_method("register_#{register_name.to_s.singularize}") do |value, plugin|
|
|
|
|
public_send(:"_raw_#{register_name}") << { plugin: plugin, value: value }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-05-13 19:25:34 +08:00
|
|
|
define_register :javascripts, Set
|
|
|
|
define_register :auth_providers, Set
|
|
|
|
define_register :service_workers, Set
|
|
|
|
define_register :stylesheets, Hash
|
|
|
|
define_register :mobile_stylesheets, Hash
|
|
|
|
define_register :desktop_stylesheets, Hash
|
2020-08-06 21:46:17 +08:00
|
|
|
define_register :color_definition_stylesheets, Hash
|
2020-05-13 19:25:34 +08:00
|
|
|
define_register :serialized_current_user_fields, Set
|
|
|
|
define_register :seed_data, HashWithIndifferentAccess
|
|
|
|
define_register :locales, HashWithIndifferentAccess
|
|
|
|
define_register :svg_icons, Set
|
|
|
|
define_register :custom_html, Hash
|
|
|
|
define_register :html_builders, Hash
|
|
|
|
define_register :seed_path_builders, Set
|
|
|
|
define_register :vendored_pretty_text, Set
|
|
|
|
define_register :vendored_core_pretty_text, Set
|
2020-06-16 05:28:07 +08:00
|
|
|
define_register :seedfu_filter, Set
|
2020-12-16 17:43:39 +08:00
|
|
|
define_register :demon_processes, Set
|
2021-09-08 07:38:45 +08:00
|
|
|
define_register :groups_callback_for_users_search_controller_action, Hash
|
2023-06-26 13:16:03 +08:00
|
|
|
define_register :mail_pollers, Set
|
2013-08-01 13:59:57 +08:00
|
|
|
|
2020-05-15 21:04:38 +08:00
|
|
|
define_filtered_register :staff_user_custom_fields
|
|
|
|
define_filtered_register :public_user_custom_fields
|
|
|
|
|
2023-10-10 11:23:56 +08:00
|
|
|
define_filtered_register :staff_editable_topic_custom_fields
|
|
|
|
define_filtered_register :public_editable_topic_custom_fields
|
|
|
|
|
2020-05-15 21:04:38 +08:00
|
|
|
define_filtered_register :self_editable_user_custom_fields
|
|
|
|
define_filtered_register :staff_editable_user_custom_fields
|
|
|
|
|
|
|
|
define_filtered_register :editable_group_custom_fields
|
2021-09-06 08:18:51 +08:00
|
|
|
define_filtered_register :group_params
|
2020-05-15 21:04:38 +08:00
|
|
|
|
2020-05-23 12:56:13 +08:00
|
|
|
define_filtered_register :topic_thumbnail_sizes
|
2022-12-12 22:08:13 +08:00
|
|
|
define_filtered_register :topic_preloader_associations
|
2020-05-23 12:56:13 +08:00
|
|
|
|
2020-08-24 17:24:52 +08:00
|
|
|
define_filtered_register :api_parameter_routes
|
2020-07-17 02:51:24 +08:00
|
|
|
define_filtered_register :api_key_scope_mappings
|
2020-10-09 21:52:48 +08:00
|
|
|
define_filtered_register :user_api_key_scope_mappings
|
2020-07-17 02:51:24 +08:00
|
|
|
|
2020-09-11 00:18:45 +08:00
|
|
|
define_filtered_register :permitted_bulk_action_parameters
|
2021-03-03 00:28:27 +08:00
|
|
|
define_filtered_register :reviewable_params
|
2021-10-07 23:41:57 +08:00
|
|
|
define_filtered_register :reviewable_score_links
|
2020-09-11 00:18:45 +08:00
|
|
|
|
DEV: Introduce PresenceChannel API for core and plugin use
PresenceChannel aims to be a generic system for allow the server, and end-users, to track the number and identity of users performing a specific task on the site. For example, it might be used to track who is currently 'replying' to a specific topic, editing a specific wiki post, etc.
A few key pieces of information about the system:
- PresenceChannels are identified by a name of the format `/prefix/blah`, where `prefix` has been configured by some core/plugin implementation, and `blah` can be any string the implementation wants to use.
- Presence is a boolean thing - each user is either present, or not present. If a user has multiple clients 'present' in a channel, they will be deduplicated so that the user is only counted once
- Developers can configure the existence and configuration of channels 'just in time' using a callback. The result of this is cached for 2 minutes.
- Configuration of a channel can specify permissions in a similar way to MessageBus (public boolean, a list of allowed_user_ids, and a list of allowed_group_ids). A channel can also be placed in 'count_only' mode, where the identity of present users is not revealed to end-users.
- The backend implementation uses redis lua scripts, and is designed to scale well. In the future, hard limits may be introduced on the maximum number of users that can be present in a channel.
- Clients can enter/leave at will. If a client has not marked itself 'present' in the last 60 seconds, they will automatically 'leave' the channel. The JS implementation takes care of this regular check-in.
- On the client-side, PresenceChannel instances can be fetched from the `presence` ember service. Each PresenceChannel can be used entered/left/subscribed/unsubscribed, and the service will automatically deduplicate information before interacting with the server.
- When a client joins a PresenceChannel, the JS implementation will automatically make a GET request for the current channel state. To avoid this, the channel state can be serialized into one of your existing endpoints, and then passed to the `subscribe` method on the channel.
- The PresenceChannel JS object is an ember object. The `users` and `count` property can be used directly in ember templates, and in computed properties.
- It is important to make sure that you `unsubscribe()` and `leave()` any PresenceChannel objects after use
An example implementation may look something like this. On the server:
```ruby
register_presence_channel_prefix("site") do |channel|
next nil unless channel == "/site/online"
PresenceChannel::Config.new(public: true)
end
```
And on the client, a component could be implemented like this:
```javascript
import Component from "@ember/component";
import { inject as service } from "@ember/service";
export default Component.extend({
presence: service(),
init() {
this._super(...arguments);
this.set("presenceChannel", this.presence.getChannel("/site/online"));
},
didInsertElement() {
this.presenceChannel.enter();
this.presenceChannel.subscribe();
},
willDestroyElement() {
this.presenceChannel.leave();
this.presenceChannel.unsubscribe();
},
});
```
With this template:
```handlebars
Online: {{presenceChannel.count}}
<ul>
{{#each presenceChannel.users as |user|}}
<li>{{avatar user imageSize="tiny"}} {{user.username}}</li>
{{/each}}
</ul>
```
2021-08-27 21:43:39 +08:00
|
|
|
define_filtered_register :presence_channel_prefixes
|
|
|
|
|
2023-11-09 00:29:00 +08:00
|
|
|
define_filtered_register :email_notification_filters
|
2021-11-04 01:21:33 +08:00
|
|
|
define_filtered_register :push_notification_filters
|
|
|
|
|
REFACTOR: Improve support for consolidating notifications. (#14904)
* REFACTOR: Improve support for consolidating notifications.
Before this commit, we didn't have a single way of consolidating notifications. For notifications like group summaries, we manually removed old ones before creating a new one. On the other hand, we used an after_create callback for likes and group membership requests, which caused unnecessary work, as we need to delete the record we created to replace it with a consolidated one.
We now have all the consolidation rules centralized in a single place: the consolidation planner class. Other parts of the app looking to create a consolidable notification can do so by calling Notification#consolidate_or_save!, instead of the default Notification#create! method.
Finally, we added two more rules: one for re-using existing group summaries and another for deleting duplicated dashboard problems PMs notifications when the user is tracking the moderator's inbox. Setting the threshold to one forces the planner to apply this rule every time.
I plan to add plugin support for adding custom rules in another PR to keep this one relatively small.
* DEV: Introduces a plugin API for consolidating notifications.
This commit removes the `Notification#filter_by_consolidation_data` scope since plugins could have to define their criteria. The Plan class now receives two blocks, one to query for an already consolidated notification, which we'll try to update, and another to query for existing ones to consolidate.
It also receives a consolidation window, which accepts an ActiveSupport::Duration object, and filter notifications created since that value.
2021-12-01 00:36:14 +08:00
|
|
|
define_filtered_register :notification_consolidation_plans
|
|
|
|
|
2022-06-22 02:49:47 +08:00
|
|
|
define_filtered_register :email_unsubscribers
|
|
|
|
|
2022-11-29 00:32:57 +08:00
|
|
|
define_filtered_register :user_destroyer_on_content_deletion_callbacks
|
|
|
|
|
2022-12-19 11:46:17 +08:00
|
|
|
define_filtered_register :hashtag_autocomplete_data_sources
|
|
|
|
define_filtered_register :hashtag_autocomplete_contextual_type_priorities
|
|
|
|
|
2023-01-17 02:48:00 +08:00
|
|
|
define_filtered_register :search_groups_set_query_callbacks
|
|
|
|
|
2023-11-10 04:44:05 +08:00
|
|
|
define_filtered_register :stats
|
2023-03-08 08:39:12 +08:00
|
|
|
define_filtered_register :bookmarkables
|
2023-03-02 06:10:16 +08:00
|
|
|
|
2023-03-14 02:37:49 +08:00
|
|
|
define_filtered_register :list_suggested_for_providers
|
|
|
|
|
2023-06-14 01:21:46 +08:00
|
|
|
define_filtered_register :summarization_strategies
|
|
|
|
|
2023-10-11 01:21:57 +08:00
|
|
|
define_filtered_register :post_action_notify_user_handlers
|
|
|
|
|
2023-11-09 03:13:25 +08:00
|
|
|
define_filtered_register :post_strippers
|
|
|
|
|
2024-03-28 14:00:47 +08:00
|
|
|
define_filtered_register :problem_checks
|
|
|
|
|
2024-07-03 06:45:37 +08:00
|
|
|
define_filtered_register :flag_applies_to_types
|
|
|
|
|
2024-07-18 00:36:38 +08:00
|
|
|
define_filtered_register :custom_filter_mappings
|
|
|
|
|
2018-07-23 23:51:57 +08:00
|
|
|
def self.register_auth_provider(auth_provider)
|
|
|
|
self.auth_providers << auth_provider
|
|
|
|
end
|
|
|
|
|
2023-06-26 13:16:03 +08:00
|
|
|
def self.register_mail_poller(mail_poller)
|
|
|
|
self.mail_pollers << mail_poller
|
|
|
|
end
|
|
|
|
|
2013-02-08 15:56:12 +08:00
|
|
|
def register_js(filename, options = {})
|
2013-02-06 03:16:51 +08:00
|
|
|
# If we have a server side option, add that too.
|
|
|
|
self.class.javascripts << filename
|
|
|
|
end
|
|
|
|
|
2017-11-23 09:02:01 +08:00
|
|
|
def self.register_service_worker(filename, options = {})
|
|
|
|
self.service_workers << filename
|
|
|
|
end
|
|
|
|
|
2018-11-27 05:49:57 +08:00
|
|
|
def self.register_svg_icon(icon)
|
|
|
|
self.svg_icons << icon
|
|
|
|
end
|
|
|
|
|
2019-08-21 00:39:52 +08:00
|
|
|
def register_css(filename, plugin_directory_name)
|
|
|
|
self.class.stylesheets[plugin_directory_name] ||= Set.new
|
|
|
|
self.class.stylesheets[plugin_directory_name] << filename
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2018-01-25 19:09:18 +08:00
|
|
|
def self.register_locale(locale, options = {})
|
|
|
|
self.locales[locale] = options
|
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def register_archetype(name, options = {})
|
|
|
|
Archetype.register(name, options)
|
|
|
|
end
|
|
|
|
|
2023-01-21 02:52:49 +08:00
|
|
|
JS_REGEX = /\.js$|\.js\.erb$|\.js\.es6\z/
|
2016-11-15 08:42:55 +08:00
|
|
|
|
2019-08-21 00:39:52 +08:00
|
|
|
def self.register_asset(asset, opts = nil, plugin_directory_name = nil)
|
2016-11-15 08:42:55 +08:00
|
|
|
if asset =~ JS_REGEX
|
2024-02-01 19:48:31 +08:00
|
|
|
if opts == :vendored_pretty_text
|
2017-04-19 05:49:56 +08:00
|
|
|
self.vendored_pretty_text << asset
|
2018-04-10 14:37:16 +08:00
|
|
|
elsif opts == :vendored_core_pretty_text
|
|
|
|
self.vendored_core_pretty_text << asset
|
2014-12-10 03:20:53 +08:00
|
|
|
else
|
|
|
|
self.javascripts << asset
|
|
|
|
end
|
2023-01-21 02:52:49 +08:00
|
|
|
elsif asset =~ /\.css$|\.scss\z/
|
2014-12-10 03:20:53 +08:00
|
|
|
if opts == :mobile
|
2019-08-21 00:39:52 +08:00
|
|
|
self.mobile_stylesheets[plugin_directory_name] ||= Set.new
|
|
|
|
self.mobile_stylesheets[plugin_directory_name] << asset
|
2014-12-10 03:20:53 +08:00
|
|
|
elsif opts == :desktop
|
2019-08-21 00:39:52 +08:00
|
|
|
self.desktop_stylesheets[plugin_directory_name] ||= Set.new
|
|
|
|
self.desktop_stylesheets[plugin_directory_name] << asset
|
2020-08-06 21:46:17 +08:00
|
|
|
elsif opts == :color_definitions
|
|
|
|
self.color_definition_stylesheets[plugin_directory_name] = asset
|
2014-12-10 03:20:53 +08:00
|
|
|
else
|
2019-08-21 00:39:52 +08:00
|
|
|
self.stylesheets[plugin_directory_name] ||= Set.new
|
|
|
|
self.stylesheets[plugin_directory_name] << asset
|
2014-12-10 03:20:53 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-08-22 11:09:10 +08:00
|
|
|
def self.stylesheets_exists?(plugin_directory_name, target = nil)
|
|
|
|
case target
|
|
|
|
when :desktop
|
|
|
|
self.desktop_stylesheets[plugin_directory_name].present?
|
|
|
|
when :mobile
|
|
|
|
self.mobile_stylesheets[plugin_directory_name].present?
|
|
|
|
else
|
|
|
|
self.stylesheets[plugin_directory_name].present?
|
|
|
|
end
|
2019-08-21 00:39:52 +08:00
|
|
|
end
|
|
|
|
|
2015-06-05 03:56:17 +08:00
|
|
|
def self.register_seed_data(key, value)
|
|
|
|
self.seed_data[key] = value
|
|
|
|
end
|
|
|
|
|
2017-11-17 03:42:38 +08:00
|
|
|
def self.register_seed_path_builder(&block)
|
|
|
|
seed_path_builders << block
|
|
|
|
end
|
|
|
|
|
2017-04-18 03:47:21 +08:00
|
|
|
def self.register_html_builder(name, &block)
|
2017-11-03 23:32:32 +08:00
|
|
|
html_builders[name] ||= []
|
|
|
|
html_builders[name] << block
|
2017-04-18 03:47:21 +08:00
|
|
|
end
|
|
|
|
|
2017-04-19 01:06:11 +08:00
|
|
|
def self.build_html(name, ctx = nil)
|
2017-11-03 23:32:32 +08:00
|
|
|
builders = html_builders[name] || []
|
2017-11-15 05:31:44 +08:00
|
|
|
builders.map { |b| b.call(ctx) }.join("\n").html_safe
|
2017-04-18 03:47:21 +08:00
|
|
|
end
|
|
|
|
|
2017-11-17 03:42:38 +08:00
|
|
|
def self.seed_paths
|
|
|
|
result = SeedFu.fixture_paths.dup
|
|
|
|
unless Rails.env.test? && ENV["LOAD_PLUGINS"] != "1"
|
|
|
|
seed_path_builders.each { |b| result += b.call }
|
|
|
|
end
|
2017-11-17 04:22:05 +08:00
|
|
|
result.uniq
|
2017-11-17 03:42:38 +08:00
|
|
|
end
|
|
|
|
|
2020-06-16 05:28:07 +08:00
|
|
|
def self.register_seedfu_filter(filter = nil)
|
|
|
|
self.seedfu_filter << filter
|
|
|
|
end
|
|
|
|
|
2018-04-10 14:37:16 +08:00
|
|
|
VENDORED_CORE_PRETTY_TEXT_MAP = {
|
2019-02-13 02:57:52 +08:00
|
|
|
"moment.js" => "vendor/assets/javascripts/moment.js",
|
|
|
|
"moment-timezone.js" => "vendor/assets/javascripts/moment-timezone-with-data.js",
|
2018-04-10 14:37:16 +08:00
|
|
|
}
|
|
|
|
def self.core_asset_for_name(name)
|
|
|
|
asset = VENDORED_CORE_PRETTY_TEXT_MAP[name]
|
|
|
|
raise KeyError, "Asset #{name} not found in #{VENDORED_CORE_PRETTY_TEXT_MAP}" unless asset
|
|
|
|
asset
|
|
|
|
end
|
|
|
|
|
2023-03-30 11:39:55 +08:00
|
|
|
def self.clear_modifiers!
|
2023-05-03 17:18:08 +08:00
|
|
|
if Rails.env.test? && GlobalSetting.load_plugins?
|
|
|
|
raise "Clearing modifiers during a plugin spec run will affect all future specs. Use unregister_modifier instead."
|
|
|
|
end
|
2023-03-30 11:39:55 +08:00
|
|
|
@modifiers = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.register_modifier(plugin_instance, name, &blk)
|
|
|
|
@modifiers ||= {}
|
|
|
|
modifiers = @modifiers[name] ||= []
|
|
|
|
modifiers << [plugin_instance, blk]
|
|
|
|
end
|
|
|
|
|
2023-05-03 17:18:08 +08:00
|
|
|
def self.unregister_modifier(plugin_instance, name, &blk)
|
|
|
|
raise "unregister_modifier can only be used in tests" if !Rails.env.test?
|
|
|
|
|
|
|
|
modifiers_for_name = @modifiers&.[](name)
|
|
|
|
raise "no #{name} modifiers found" if !modifiers_for_name
|
|
|
|
|
|
|
|
i = modifiers_for_name.find_index { |info| info == [plugin_instance, blk] }
|
|
|
|
raise "no modifier found for that plugin/block combination" if !i
|
|
|
|
|
|
|
|
modifiers_for_name.delete_at(i)
|
|
|
|
end
|
|
|
|
|
2023-03-30 11:39:55 +08:00
|
|
|
def self.apply_modifier(name, arg, *more_args)
|
|
|
|
return arg if !@modifiers
|
|
|
|
|
|
|
|
registered_modifiers = @modifiers[name]
|
|
|
|
return arg if !registered_modifiers
|
|
|
|
|
|
|
|
# iterate as fast as possible to minimize cost (avoiding each)
|
|
|
|
# also erases one stack frame
|
|
|
|
length = registered_modifiers.length
|
|
|
|
index = 0
|
|
|
|
while index < length
|
|
|
|
plugin_instance, block = registered_modifiers[index]
|
|
|
|
arg = block.call(arg, *more_args) if plugin_instance.enabled?
|
|
|
|
|
|
|
|
index += 1
|
|
|
|
end
|
|
|
|
|
|
|
|
arg
|
|
|
|
end
|
|
|
|
|
2014-12-10 03:20:53 +08:00
|
|
|
def self.reset!
|
2020-05-13 19:25:34 +08:00
|
|
|
@@register_names.each { |name| instance_variable_set(:"@#{name}", nil) }
|
2023-03-30 11:39:55 +08:00
|
|
|
clear_modifiers!
|
2014-12-10 03:20:53 +08:00
|
|
|
end
|
|
|
|
|
2021-07-02 23:40:50 +08:00
|
|
|
def self.reset_register!(register_name)
|
|
|
|
found_register = @@register_names.detect { |name| name == register_name }
|
|
|
|
|
|
|
|
instance_variable_set(:"@#{found_register}", nil) if found_register
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|