discourse_subscription_client 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +10 -0
  5. data/app/assets/config/discourse_subscription_client_manifest.js +1 -0
  6. data/app/assets/stylesheets/discourse_subscription_client/application.css +15 -0
  7. data/app/controllers/discourse_subscription_client/admin_controller.rb +46 -0
  8. data/app/controllers/discourse_subscription_client/notices_controller.rb +82 -0
  9. data/app/controllers/discourse_subscription_client/subscriptions_controller.rb +17 -0
  10. data/app/controllers/discourse_subscription_client/suppliers_controller.rb +62 -0
  11. data/app/jobs/regular/discourse_subscription_client/find_resources.rb +9 -0
  12. data/app/jobs/scheduled/discourse_subscription_client/update_notices.rb +11 -0
  13. data/app/jobs/scheduled/discourse_subscription_client/update_subscriptions.rb +11 -0
  14. data/app/models/subscription_client_notice.rb +223 -0
  15. data/app/models/subscription_client_request.rb +4 -0
  16. data/app/models/subscription_client_resource.rb +27 -0
  17. data/app/models/subscription_client_subscription.rb +54 -0
  18. data/app/models/subscription_client_supplier.rb +54 -0
  19. data/app/serializers/discourse_subscription_client/notice_serializer.rb +52 -0
  20. data/app/serializers/discourse_subscription_client/resource_serializer.rb +8 -0
  21. data/app/serializers/discourse_subscription_client/subscription_serializer.rb +12 -0
  22. data/app/serializers/discourse_subscription_client/supplier_serializer.rb +16 -0
  23. data/config/locales/server.en.yml +40 -0
  24. data/config/routes.rb +19 -0
  25. data/config/settings.yml +16 -0
  26. data/db/migrate/20220318160955_create_subscription_client_suppliers.rb +17 -0
  27. data/db/migrate/20220318181029_create_subscription_client_resources.rb +14 -0
  28. data/db/migrate/20220318181054_create_subscription_client_notices.rb +22 -0
  29. data/db/migrate/20220318181140_create_subscription_client_subscriptions.rb +19 -0
  30. data/db/migrate/20230220130259_create_subscription_client_requests.rb +16 -0
  31. data/lib/discourse_subscription_client/authorization.rb +100 -0
  32. data/lib/discourse_subscription_client/engine.rb +97 -0
  33. data/lib/discourse_subscription_client/notices.rb +154 -0
  34. data/lib/discourse_subscription_client/request.rb +136 -0
  35. data/lib/discourse_subscription_client/resources.rb +97 -0
  36. data/lib/discourse_subscription_client/subscriptions/result.rb +118 -0
  37. data/lib/discourse_subscription_client/subscriptions.rb +93 -0
  38. data/lib/discourse_subscription_client/version.rb +5 -0
  39. data/lib/discourse_subscription_client.rb +8 -0
  40. metadata +252 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SubscriptionClientSupplier < ActiveRecord::Base
4
+ has_many :resources, foreign_key: "supplier_id", class_name: "SubscriptionClientResource", dependent: :destroy
5
+ has_many :subscriptions, through: :resources
6
+ has_many :notices, class_name: "SubscriptionClientNotice", as: :notice_subject, dependent: :destroy
7
+
8
+ belongs_to :user
9
+
10
+ scope :authorized, -> { where("api_key IS NOT NULL") }
11
+
12
+ def destroy_authorization
13
+ if DiscourseSubscriptionClient::Authorization.revoke(self)
14
+ update(api_key: nil, user_id: nil, authorized_at: nil)
15
+ deactivate_all_subscriptions!
16
+ true
17
+ else
18
+ false
19
+ end
20
+ end
21
+
22
+ def authorized?
23
+ api_key.present?
24
+ end
25
+
26
+ def deactivate_all_subscriptions!
27
+ subscriptions.update_all(subscribed: false)
28
+ end
29
+
30
+ def self.publish_authorized_supplier_count
31
+ payload = { authorized_supplier_count: authorized.count }
32
+ group_id_key = SiteSetting.subscription_client_allow_moderator_subscription_management ? :staff : :admins
33
+ MessageBus.publish("/subscription_client", payload, group_ids: [Group::AUTO_GROUPS[group_id_key.to_sym]])
34
+ end
35
+ end
36
+
37
+ # == Schema Information
38
+ #
39
+ # Table name: subscription_client_suppliers
40
+ #
41
+ # id :bigint not null, primary key
42
+ # name :string
43
+ # url :string not null
44
+ # api_key :string
45
+ # user_id :bigint
46
+ # authorized_at :datetime
47
+ # created_at :datetime not null
48
+ # updated_at :datetime not null
49
+ #
50
+ # Indexes
51
+ #
52
+ # index_subscription_client_suppliers_on_url (url) UNIQUE
53
+ # index_subscription_client_suppliers_on_user_id (user_id)
54
+ #
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class NoticeSerializer < ApplicationSerializer
5
+ attributes :id,
6
+ :title,
7
+ :message,
8
+ :notice_type,
9
+ :notice_subject_type,
10
+ :notice_subject_id,
11
+ :plugin_status_resource,
12
+ :created_at,
13
+ :expired_at,
14
+ :updated_at,
15
+ :dismissed_at,
16
+ :retrieved_at,
17
+ :hidden_at,
18
+ :dismissable,
19
+ :can_hide
20
+
21
+ has_one :supplier, serializer: DiscourseSubscriptionClient::SupplierSerializer, embed: :objects
22
+ has_one :resource, serializer: DiscourseSubscriptionClient::ResourceSerializer, embed: :objects
23
+
24
+ def include_supplier?
25
+ object.supplier.present?
26
+ end
27
+
28
+ def include_resource?
29
+ object.resource.present?
30
+ end
31
+
32
+ def plugin_status_resource
33
+ object.plugin_status_resource?
34
+ end
35
+
36
+ def dismissable
37
+ object.dismissable?
38
+ end
39
+
40
+ def can_hide
41
+ object.can_hide?
42
+ end
43
+
44
+ def notice_type
45
+ SubscriptionClientNotice.types.key(object.notice_type)
46
+ end
47
+
48
+ def messsage
49
+ PrettyText.cook(object.message)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class ResourceSerializer < ApplicationSerializer
5
+ attributes :id,
6
+ :name
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class SubscriptionSerializer < ApplicationSerializer
5
+ attributes :supplier_name,
6
+ :resource_name,
7
+ :product_name,
8
+ :price_name,
9
+ :active,
10
+ :updated_at
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class SupplierSerializer < ApplicationSerializer
5
+ attributes :id,
6
+ :name,
7
+ :authorized,
8
+ :authorized_at
9
+
10
+ has_one :user, serializer: BasicUserSerializer, embed: :objects
11
+
12
+ def authorized
13
+ object.api_key.present? && object.authorized_at.present?
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,40 @@
1
+ en:
2
+ subscription_client:
3
+ custom_title: "Subscription"
4
+
5
+ notices:
6
+ connection_error: "Failed to connect to %{url}"
7
+ compatibility_issue:
8
+ title: The %{resource} is incompatibile with the latest version of Discourse.
9
+ message: Please check the %{resource} status before updating Discourse.
10
+ resource:
11
+ connection_error:
12
+ title: Unable to connect to the plugin status server
13
+ message: Please check your plugins' compatibility with the latest version of Discourse before updating.
14
+ supplier:
15
+ connection_error:
16
+ title: Unable to connect to %{supplier}
17
+ message: Any subscriptions you have with %{supplier} have been deactivated.
18
+
19
+ subscriptions:
20
+ error:
21
+ supplier_connection: Failed to connect to %{supplier} (%{supplier_url})
22
+ invalid_response: Invalid response from %{supplier} (%{supplier_url})
23
+ info:
24
+ no_suppliers: "Update was not run as there are no authorized suppliers."
25
+ no_subscriptions: "There are no active subscriptions for %{supplier} (%{supplier_url})."
26
+ no_resource: "There is no %{resource} resource associated with %{supplier} (%{supplier_url})."
27
+ deactivated_subscription: "Deactivated subscription for %{resource_name} (%{resource_id})."
28
+ updated_subscription: "Updated active subscription for %{resource_name} (%{resource_id}). Product: (%{product_id}), Price: %{price}, Supplier: %{supplier}"
29
+ created_subscription: "Created active subscription for resource: %{resource-id}. Product: %{product_id}; Price: %{price}, Supplier: %{supplier}"
30
+ failed_to_create_subscription: "Failed to create subscription for %{resource_name} (%{resource-id}). product: %{product_id}; price: %{price}. From %{supplier} (%{supplier_url})"
31
+
32
+ site_settings:
33
+ subscription_client_enabled: "Enable the subscription client."
34
+ subscription_client_verbose_logs: "Enable verbose logs for the subscription client."
35
+ subscription_client_warning_notices_on_dashboard: "Show warning notices about subscriptions on the admin dashboard."
36
+ subscription_client_allow_moderator_subscription_management: "Allow moderators to manage subscriptions."
37
+ subscription_client_allow_moderator_supplier_management: "Allow moderators to manage subscription suppliers."
38
+ subscription_client_request_plugin_statuses: Request the status of plugins from discourse.pluginmanager.org.
39
+ errors:
40
+ allow_moderator_subscription_management_not_enabled: "Allow moderator subscription management must be enabled."
data/config/routes.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ DiscourseSubscriptionClient::Engine.routes.draw do
4
+ get "" => "admin#index"
5
+ get ".json" => "admin#index"
6
+
7
+ get "suppliers" => "suppliers#index"
8
+ get "suppliers/authorize" => "suppliers#authorize"
9
+ get "suppliers/authorize/callback" => "suppliers#authorize_callback"
10
+ delete "suppliers/authorize" => "suppliers#destroy"
11
+
12
+ get "subscriptions" => "subscriptions#index"
13
+ post "subscriptions" => "subscriptions#update"
14
+
15
+ get "notices" => "notices#index"
16
+ put "notices/:notice_id/dismiss" => "notices#dismiss"
17
+ put "notices/:notice_id/hide" => "notices#hide"
18
+ put "notices/:notice_id/show" => "notices#show"
19
+ end
@@ -0,0 +1,16 @@
1
+ plugins:
2
+ subscription_client_enabled:
3
+ default: true
4
+ client: true
5
+ subscription_client_verbose_logs:
6
+ default: false
7
+ subscription_client_warning_notices_on_dashboard:
8
+ default: true
9
+ subscription_client_allow_moderator_subscription_management:
10
+ default: false
11
+ subscription_client_allow_moderator_supplier_management:
12
+ default: false
13
+ client: true
14
+ subscription_client_request_plugin_statuses:
15
+ default: false
16
+ hidden: true
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSubscriptionClientSuppliers < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :subscription_client_suppliers do |t|
6
+ t.string :name
7
+ t.string :url, null: false
8
+ t.string :api_key
9
+ t.references :user
10
+ t.datetime :authorized_at
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :subscription_client_suppliers, [:url], unique: true
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSubscriptionClientResources < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :subscription_client_resources do |t|
6
+ t.references :supplier, foreign_key: { to_table: :subscription_client_suppliers }
7
+ t.string :name, null: false
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :subscription_client_resources, %i[supplier_id name], unique: true
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSubscriptionClientNotices < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :subscription_client_notices do |t|
6
+ t.string :title, null: false
7
+ t.string :message
8
+ t.integer :notice_type, null: false
9
+ t.references :notice_subject, polymorphic: true
10
+ t.datetime :changed_at
11
+ t.datetime :retrieved_at
12
+ t.datetime :dismissed_at
13
+ t.datetime :expired_at
14
+ t.datetime :hidden_at
15
+
16
+ t.timestamps null: false
17
+ end
18
+
19
+ add_index :subscription_client_notices, %i[notice_type notice_subject_type notice_subject_id changed_at],
20
+ unique: true, name: "sc_unique_notices"
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSubscriptionClientSubscriptions < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :subscription_client_subscriptions do |t|
6
+ t.references :resource, foreign_key: { to_table: :subscription_client_resources }
7
+ t.string :product_id, null: false
8
+ t.string :product_name
9
+ t.string :price_id, null: false
10
+ t.string :price_name
11
+ t.boolean :subscribed, default: false, null: false
12
+
13
+ t.timestamps null: false
14
+ end
15
+
16
+ add_index :subscription_client_subscriptions, %i[resource_id product_id price_id], unique: true,
17
+ name: "sc_unique_subscriptions"
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSubscriptionClientRequests < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :subscription_client_requests do |t|
6
+ t.bigint :request_id
7
+ t.string :request_type
8
+ t.datetime :expired_at
9
+ t.string :message
10
+ t.integer :count
11
+ t.json :response
12
+
13
+ t.timestamps
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class Authorization
5
+ SCOPE ||= "discourse-subscription-server:user_subscription"
6
+
7
+ def self.request_id(supplier_id)
8
+ "#{supplier_id}-#{SecureRandom.hex(32)}"
9
+ end
10
+
11
+ def self.url(user, supplier, request_id)
12
+ keys = generate_keys(user.id, request_id)
13
+ params = {
14
+ public_key: keys.public_key,
15
+ nonce: keys.nonce,
16
+ client_id: client_id(user.id),
17
+ auth_redirect: "#{Discourse.base_url}/admin/plugins/subscription-client/suppliers/authorize/callback",
18
+ application_name: SiteSetting.title,
19
+ scopes: SCOPE
20
+ }
21
+ uri = URI.parse("#{supplier.url}/user-api-key/new")
22
+ uri.query = URI.encode_www_form(params)
23
+ uri.to_s
24
+ end
25
+
26
+ def self.process_response(request_id, payload)
27
+ data = decrypt_payload(request_id, payload)
28
+ return false unless data.is_a?(Hash) && data[:key] && data[:user_id]
29
+
30
+ data
31
+ end
32
+
33
+ def self.generate_keys(user_id, request_id)
34
+ rsa = OpenSSL::PKey::RSA.generate(2048)
35
+ nonce = SecureRandom.hex(32)
36
+ set_keys(request_id, user_id, rsa, nonce)
37
+ OpenStruct.new(nonce: nonce, public_key: rsa.public_key)
38
+ end
39
+
40
+ def self.decrypt_payload(request_id, payload)
41
+ keys = get_keys(request_id)
42
+
43
+ return false unless keys.present? && keys.pem
44
+
45
+ delete_keys(request_id)
46
+
47
+ rsa = OpenSSL::PKey::RSA.new(keys.pem)
48
+ decrypted_payload = rsa.private_decrypt(Base64.decode64(payload))
49
+
50
+ return false unless decrypted_payload.present?
51
+
52
+ begin
53
+ data = JSON.parse(decrypted_payload).symbolize_keys
54
+ rescue JSON::ParserError
55
+ return false
56
+ end
57
+
58
+ return false unless data[:nonce] == keys.nonce
59
+
60
+ data[:user_id] = keys.user_id
61
+ data
62
+ end
63
+
64
+ def self.get_keys(request_id)
65
+ raw = PluginStore.get(DiscourseSubscriptionClient::PLUGIN_NAME, "#{keys_db_key}_#{request_id}")
66
+ OpenStruct.new(
67
+ user_id: raw && raw["user_id"],
68
+ pem: raw && raw["pem"],
69
+ nonce: raw && raw["nonce"]
70
+ )
71
+ end
72
+
73
+ def self.revoke(supplier)
74
+ url = "#{supplier.url}/user-api-key/revoke"
75
+ request = DiscourseSubscriptionClient::Request.new(:supplier, supplier.id)
76
+ headers = { "User-Api-Key" => supplier.api_key }
77
+ result = request.perform(url, headers: headers, body: nil, opts: { method: "POST" })
78
+ result && result[:success] == "OK"
79
+ end
80
+
81
+ def self.client_id(user_id)
82
+ "#{Discourse.current_hostname}:#{user_id}:#{SecureRandom.hex(8)}"
83
+ end
84
+
85
+ def self.keys_db_key
86
+ "keys"
87
+ end
88
+
89
+ def self.set_keys(request_id, user_id, rsa, nonce)
90
+ PluginStore.set(DiscourseSubscriptionClient::PLUGIN_NAME, "#{keys_db_key}_#{request_id}",
91
+ user_id: user_id,
92
+ pem: rsa.export,
93
+ nonce: nonce)
94
+ end
95
+
96
+ def self.delete_keys(request_id)
97
+ PluginStore.remove(DiscourseSubscriptionClient::PLUGIN_NAME, "#{keys_db_key}_#{request_id}")
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ PLUGIN_NAME ||= "discourse_subscription_client"
5
+
6
+ class Engine < ::Rails::Engine
7
+ engine_name PLUGIN_NAME
8
+ isolate_namespace DiscourseSubscriptionClient
9
+
10
+ config.before_initialize do
11
+ config.i18n.load_path += Dir["#{config.root}/config/locales/**/*.yml"]
12
+ end
13
+
14
+ config.after_initialize do
15
+ gem_root = File.expand_path("../..", __dir__)
16
+
17
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths << "#{gem_root}/db/migrate"
18
+
19
+ %w[
20
+ ./request
21
+ ./authorization
22
+ ./resources
23
+ ./notices
24
+ ./subscriptions
25
+ ./subscriptions/result
26
+ ../../app/models/subscription_client_notice
27
+ ../../app/models/subscription_client_resource
28
+ ../../app/models/subscription_client_subscription
29
+ ../../app/models/subscription_client_supplier
30
+ ../../app/controllers/discourse_subscription_client/admin_controller
31
+ ../../app/controllers/discourse_subscription_client/subscriptions_controller
32
+ ../../app/controllers/discourse_subscription_client/suppliers_controller
33
+ ../../app/controllers/discourse_subscription_client/notices_controller
34
+ ../../app/serializers/discourse_subscription_client/supplier_serializer
35
+ ../../app/serializers/discourse_subscription_client/resource_serializer
36
+ ../../app/serializers/discourse_subscription_client/notice_serializer
37
+ ../../app/serializers/discourse_subscription_client/subscription_serializer
38
+ ../../app/jobs/regular/discourse_subscription_client/find_resources
39
+ ../../app/jobs/scheduled/discourse_subscription_client/update_subscriptions
40
+ ../../app/jobs/scheduled/discourse_subscription_client/update_notices
41
+ ../../extensions/discourse_subscription_client/current_user
42
+ ../../extensions/discourse_subscription_client/guardian
43
+ ].each do |path|
44
+ require_relative path
45
+ end
46
+
47
+ Jobs.enqueue(:subscription_client_find_resources) if DiscourseSubscriptionClient.database_exists? && !Rails.env.test?
48
+
49
+ Rails.application.routes.append do
50
+ mount DiscourseSubscriptionClient::Engine, at: "/admin/plugins/subscription-client"
51
+ end
52
+
53
+ SiteSetting.load_settings("#{gem_root}/config/settings.yml", plugin: PLUGIN_NAME)
54
+
55
+ Guardian.prepend DiscourseSubscriptionClient::GuardianExtension
56
+ CurrentUserSerializer.prepend DiscourseSubscriptionClient::CurrentUserSerializerExtension
57
+
58
+ User.has_many(:subscription_client_suppliers)
59
+
60
+ AdminDashboardData.add_scheduled_problem_check(:subscription_client) do
61
+ return unless SiteSetting.subscription_client_warning_notices_on_dashboard
62
+
63
+ notices = SubscriptionClientNotice.list(
64
+ notice_type: SubscriptionClientNotice.error_types,
65
+ visible: true
66
+ )
67
+ notices.map do |notice|
68
+ AdminDashboardData::Problem.new(
69
+ "#{notice.title}: #{notice.message}",
70
+ priority: "high",
71
+ identifier: "subscription_client_notice_#{notice.id}"
72
+ )
73
+ end
74
+ end
75
+
76
+ DiscourseEvent.trigger(:subscription_client_ready)
77
+ end
78
+ end
79
+
80
+ class << self
81
+ def root
82
+ Rails.root
83
+ end
84
+
85
+ def plugin_status_server_url
86
+ "https://discourse.pluginmanager.org"
87
+ end
88
+
89
+ def database_exists?
90
+ ActiveRecord::Base.connection
91
+ rescue ActiveRecord::NoDatabaseError
92
+ false
93
+ else
94
+ true
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class Notices
5
+ PLUGIN_STATUS_RESOURCE_ID = -1
6
+ PLUGIN_STATUSES_TO_WARN = %w[incompatible tests_failing].freeze
7
+
8
+ def initialize
9
+ @suppliers = SubscriptionClientSupplier.authorized
10
+ end
11
+
12
+ def self.update(subscription: true, plugin: true)
13
+ new.update(subscription: subscription, plugin: plugin)
14
+ end
15
+
16
+ def update(subscription: true, plugin: true)
17
+ return if !SiteSetting.subscription_client_enabled || @suppliers.blank?
18
+
19
+ if subscription
20
+ @suppliers.each do |supplier|
21
+ update_subscription_messages(supplier)
22
+ end
23
+ end
24
+
25
+ update_plugin_statuses if plugin && SiteSetting.subscription_client_request_plugin_statuses
26
+
27
+ SubscriptionClientNotice.publish_notice_count
28
+ end
29
+
30
+ def update_subscription_messages(supplier)
31
+ url = "#{supplier.url}/subscription-server/messages"
32
+ request = DiscourseSubscriptionClient::Request.new(:supplier, supplier.id)
33
+ messages = request.perform(url)
34
+
35
+ return unless messages.present?
36
+
37
+ messages[:messages].each do |message|
38
+ notice_type = SubscriptionClientNotice.types[message[:type].to_sym]
39
+
40
+ if message[:resource] && (resource = SubscriptionClientResource.find_by(name: message[:resource],
41
+ supplier_id: supplier.id))
42
+ notice_subject_type = SubscriptionClientNotice.notice_subject_types[:resource]
43
+ notice_subject_id = resource.id
44
+ else
45
+ notice_subject_type = SubscriptionClientNotice.notice_subject_types[:supplier]
46
+ notice_subject_id = supplier.id
47
+ end
48
+
49
+ changed_at = message[:created_at]
50
+ notice = SubscriptionClientNotice.find_by(
51
+ notice_type: notice_type,
52
+ notice_subject_type: notice_subject_type,
53
+ notice_subject_id: notice_subject_id,
54
+ changed_at: changed_at
55
+ )
56
+
57
+ if notice
58
+ if message[:expired_at]
59
+ notice.expired_at = message[:expired_at]
60
+ notice.save
61
+ end
62
+ else
63
+ SubscriptionClientNotice.create!(
64
+ title: message[:title],
65
+ message: message[:message],
66
+ notice_type: notice_type,
67
+ notice_subject_type: notice_subject_type,
68
+ notice_subject_id: notice_subject_id,
69
+ changed_at: changed_at,
70
+ expired_at: message[:expired_at],
71
+ retrieved_at: DateTime.now.iso8601(3)
72
+ )
73
+ end
74
+ end
75
+ end
76
+
77
+ def update_plugin_statuses
78
+ request = DiscourseSubscriptionClient::Request.new(:resource, PLUGIN_STATUS_RESOURCE_ID)
79
+ response = request.perform(DiscourseSubscriptionClient.plugin_status_server_url)
80
+ return false unless response && response[:statuses].present?
81
+
82
+ statuses = response[:statuses]
83
+
84
+ return unless statuses.present?
85
+
86
+ warnings = statuses.select { |status| PLUGIN_STATUSES_TO_WARN.include?(status[:status]) }
87
+ expiries = statuses - warnings
88
+
89
+ create_plugin_warning_notices(warnings) if warnings.any?
90
+ expire_plugin_warning_notices(expiries) if expiries.any?
91
+ end
92
+
93
+ def expire_plugin_warning_notices(expiries)
94
+ plugin_names = expiries.map { |expiry| expiry[:name] }
95
+ sql = <<~SQL
96
+ UPDATE subscription_client_notices AS notices
97
+ SET expired_at = now()
98
+ FROM subscription_client_resources AS resources
99
+ WHERE resources.name IN (:plugin_names)
100
+ AND notices.notice_subject_id = resources.id
101
+ AND notices.notice_subject_type = 'SubscriptionClientResource'
102
+ AND notices.notice_type = :notice_type
103
+ SQL
104
+
105
+ ActiveRecord::Base.connection.execute(
106
+ ActiveRecord::Base.sanitize_sql(
107
+ [
108
+ sql, {
109
+ notice_type: SubscriptionClientNotice.types[:warning],
110
+ plugin_names: plugin_names
111
+ }
112
+ ]
113
+ )
114
+ )
115
+ end
116
+
117
+ def create_plugin_warning_notices(warnings)
118
+ plugin_names = warnings.map { |warning| warning[:name] }
119
+ resource_ids = SubscriptionClientResource.where(name: plugin_names)
120
+ .each_with_object({}) do |resource, result|
121
+ result[resource.name] =
122
+ resource.id
123
+ end
124
+
125
+ warnings.each do |warning|
126
+ notice_type = SubscriptionClientNotice.types[:warning]
127
+ notice_subject_type = SubscriptionClientNotice.notice_subject_types[:resource]
128
+ notice_subject_id = resource_ids[warning[:name]]
129
+ changed_at = warning[:status_changed_at]
130
+
131
+ notice = SubscriptionClientNotice.find_by(
132
+ notice_type: notice_type,
133
+ notice_subject_type: notice_subject_type,
134
+ notice_subject_id: notice_subject_id,
135
+ changed_at: changed_at
136
+ )
137
+
138
+ if notice
139
+ notice.touch
140
+ else
141
+ SubscriptionClientNotice.create!(
142
+ title: I18n.t("subscription_client.notices.compatibility_issue.title", resource: warning[:name]),
143
+ message: I18n.t("subscription_client.notices.compatibility_issue.message", resource: warning[:name]),
144
+ notice_type: notice_type,
145
+ notice_subject_type: notice_subject_type,
146
+ notice_subject_id: notice_subject_id,
147
+ changed_at: changed_at,
148
+ retrieved_at: DateTime.now.iso8601(3)
149
+ )
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end