discourse_subscription_client 0.1.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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