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,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class Request
5
+ SUPPLIER_ERROR_LIMIT = 3
6
+ RESOURCE_ERROR_LIMIT = 5
7
+ VALID_TYPES = %w[resource supplier].freeze
8
+
9
+ attr_reader :type,
10
+ :id
11
+
12
+ def initialize(type, id)
13
+ @type = type.to_s
14
+ @id = id
15
+ end
16
+
17
+ def perform(url, headers: {}, body: nil, opts: { method: "GET" })
18
+ return nil unless VALID_TYPES.include?(type)
19
+
20
+ if body
21
+ uri = URI.parse(url)
22
+ uri.query = CGI.unescape(body.to_query)
23
+ url = uri.to_s
24
+ end
25
+
26
+ headers.merge!({ "Origin" => Discourse.base_url_no_prefix })
27
+
28
+ connection = Excon.new(url, headers: headers)
29
+
30
+ begin
31
+ response = connection.request(opts)
32
+ rescue Excon::Error::Socket, Excon::Error::Timeout
33
+ response = nil
34
+ end
35
+
36
+ if response && response.status == 200
37
+ expire_error
38
+
39
+ begin
40
+ data = JSON.parse(response.body).deep_symbolize_keys
41
+ rescue JSON::ParserError
42
+ return nil
43
+ end
44
+
45
+ data
46
+ else
47
+ create_error(url, response)
48
+ nil
49
+ end
50
+ end
51
+
52
+ def current_error(query_only: false)
53
+ @current_error ||= begin
54
+ query = SubscriptionClientRequest
55
+ .where("request_id = :request_id AND request_type = :request_type AND expired_at IS NULL", request_id: id, request_type: type)
56
+
57
+ return nil unless query.exists?
58
+ return query if query_only
59
+
60
+ query.first
61
+ end
62
+ end
63
+
64
+ def self.current_error(type, id)
65
+ new(type, id).current_error
66
+ end
67
+
68
+ def limit
69
+ send("#{@type}_limit")
70
+ end
71
+
72
+ def create_error(url, response)
73
+ if (error = current_error)
74
+ error.count = error.count.to_i + 1
75
+ else
76
+ error = SubscriptionClientRequest.new(
77
+ request_id: id,
78
+ request_type: type,
79
+ message: I18n.t("subscription_client.notices.connection_error", url: url),
80
+ count: 1
81
+ )
82
+ end
83
+
84
+ if response.present?
85
+ begin
86
+ body = JSON.parse(response.body)
87
+ rescue JSON::ParserError
88
+ body = nil
89
+ end
90
+
91
+ error.response = {
92
+ status: response.status,
93
+ body: body
94
+ }
95
+
96
+ error.message = body["error"] if body && body["error"]
97
+ end
98
+
99
+ error.save
100
+
101
+ @current_error = nil
102
+
103
+ return unless reached_limit?
104
+
105
+ SubscriptionClientNotice.notify_connection_error(type, id)
106
+
107
+ return unless type === "supplier" && (supplier = SubscriptionClientSupplier.find_by_id(id))
108
+
109
+ supplier.deactivate_all_subscriptions!
110
+ end
111
+
112
+ def expire_error
113
+ if (query = current_error(query_only: true))
114
+ record = query.first
115
+ record.expired_at = Time.now
116
+ record.save
117
+ end
118
+
119
+ SubscriptionClientNotice.expire_connection_error(type, id)
120
+ end
121
+
122
+ def supplier_limit
123
+ SUPPLIER_ERROR_LIMIT
124
+ end
125
+
126
+ def resource_limit
127
+ RESOURCE_ERROR_LIMIT
128
+ end
129
+
130
+ def reached_limit?
131
+ return false unless current_error.present?
132
+
133
+ current_error.count.to_i >= limit
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class Resources
5
+ attr_accessor :suppliers,
6
+ :resources
7
+
8
+ def initialize
9
+ @suppliers = []
10
+ @resources = []
11
+ end
12
+
13
+ def self.find_all
14
+ RailsMultisite::ConnectionManagement.each_connection do
15
+ new.find_all
16
+ end
17
+ end
18
+
19
+ def find_all
20
+ return unless DiscourseSubscriptionClient.database_exists?
21
+
22
+ setup_resources
23
+ find_resources
24
+
25
+ return unless @resources.any?
26
+
27
+ ActiveRecord::Base.transaction do
28
+ find_suppliers
29
+ save_resources
30
+ end
31
+ end
32
+
33
+ def setup_resources
34
+ setup_plugins
35
+ end
36
+
37
+ def find_resources
38
+ find_plugins
39
+ end
40
+
41
+ def find_suppliers
42
+ supplier_urls = @resources.map { |resource| resource[:supplier_url] }.uniq.compact
43
+
44
+ supplier_urls.each do |url|
45
+ supplier = SubscriptionClientSupplier.find_by(url: url)
46
+
47
+ if supplier&.name
48
+ @suppliers << supplier
49
+ else
50
+ supplier ||= SubscriptionClientSupplier.create!(url: url)
51
+ request = DiscourseSubscriptionClient::Request.new(:supplier, supplier.id)
52
+ data = request.perform("#{url}/subscription-server")
53
+
54
+ if data
55
+ supplier.update(name: data[:supplier])
56
+ @suppliers << supplier
57
+ else
58
+ supplier.destroy!
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def save_resources
65
+ @resources.each do |resource|
66
+ supplier = @suppliers.select { |s| s.url === resource[:supplier_url] }.first
67
+
68
+ next unless supplier.present?
69
+
70
+ attrs = {
71
+ supplier_id: supplier.id,
72
+ name: resource[:name]
73
+ }
74
+ SubscriptionClientResource.create!(attrs) unless SubscriptionClientResource.exists?(attrs)
75
+ end
76
+ end
77
+
78
+ def setup_plugins
79
+ Plugin::Metadata::FIELDS << :subscription_url unless Plugin::Metadata::FIELDS.include?(:subscription_url)
80
+ Plugin::Metadata.attr_accessor(:subscription_url)
81
+ end
82
+
83
+ def find_plugins
84
+ Dir["#{DiscourseSubscriptionClient.root}/plugins/*/plugin.rb"].sort.each do |path|
85
+ source = File.read(path)
86
+ metadata = Plugin::Metadata.parse(source)
87
+
88
+ next unless metadata.subscription_url.present?
89
+
90
+ @resources << {
91
+ name: metadata.name,
92
+ supplier_url: metadata.subscription_url
93
+ }
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::DiscourseSubscriptionClient
4
+ class Subscriptions
5
+ class Result
6
+ REQUIRED_KEYS ||= %i[
7
+ resource_id
8
+ product_id
9
+ price_id
10
+ ].freeze
11
+ OPTIONAL_KEYS ||= %i[
12
+ product_name
13
+ price_name
14
+ ].freeze
15
+ KEYS = REQUIRED_KEYS + OPTIONAL_KEYS
16
+
17
+ attr_reader :errors,
18
+ :errored_suppliers,
19
+ :infos
20
+
21
+ def initialize
22
+ @errors = []
23
+ @infos = []
24
+ end
25
+
26
+ def not_authorized(supplier)
27
+ error("not_authorized", supplier)
28
+ end
29
+
30
+ def retrieve_subscriptions(supplier, raw_data)
31
+ subscriptions_data = raw_data[:subscriptions].compact
32
+
33
+ unless subscriptions_data.present? && subscriptions_data.is_a?(Array)
34
+ error("invalid_response", supplier)
35
+ return []
36
+ end
37
+
38
+ # subscriptions must be properly formed
39
+
40
+ subscriptions_data =
41
+ subscriptions_data
42
+ .map(&:symbolize_keys)
43
+ .each { |data| data[:resource_id] = data[:resource] }
44
+ .select { |data| REQUIRED_KEYS.all? { |key| data.key?(key) } }
45
+
46
+ # we only care about subscriptions for resources on this instance
47
+
48
+ resources = SubscriptionClientResource.where(
49
+ supplier_id: supplier.id,
50
+ name: subscriptions_data.map { |data| data[:resource] }
51
+ )
52
+
53
+ subscriptions_data.each_with_object([]) do |data, result|
54
+ resource = resources.select { |r| r.name === data[:resource] }.first
55
+
56
+ if resource.present?
57
+ data[:resource_id] = resource.id
58
+ result << OpenStruct.new(
59
+ required: data.slice(*REQUIRED_KEYS),
60
+ create: data.slice(*KEYS),
61
+ subscription: nil
62
+ )
63
+ else
64
+ info("no_resource", supplier, resource: data[:resource])
65
+ end
66
+ end
67
+ end
68
+
69
+ def connection_error(supplier)
70
+ error("supplier_connection", supplier)
71
+ end
72
+
73
+ def no_suppliers
74
+ info("no_suppliers")
75
+ end
76
+
77
+ def no_subscriptions(supplier)
78
+ info("no_subscriptions", supplier)
79
+ end
80
+
81
+ def updated_subscription(supplier, subscription_ids: nil)
82
+ info("updated_subscription", supplier, subscription_ids: subscription_ids)
83
+ end
84
+
85
+ def created_subscription(supplier, subscription_ids: nil)
86
+ info("created_subscription", supplier, subscription_ids: subscription_ids)
87
+ end
88
+
89
+ def failed_to_create_subscription(supplier, subscription_ids: nil)
90
+ info("failed_to_create_subscription", supplier, subscription_ids: subscription_ids)
91
+ end
92
+
93
+ def info(key, supplier = nil, subscription_ids: nil, resource: nil)
94
+ attrs = {}
95
+
96
+ if supplier
97
+ attrs = {
98
+ supplier: supplier.name,
99
+ supplier_url: supplier.url,
100
+ deep_interpolation: true,
101
+ price: "",
102
+ resource_name: ""
103
+ }
104
+ end
105
+
106
+ attrs.merge!(subscription_ids) if subscription_ids.present?
107
+ attrs[:resource] if resource.present?
108
+
109
+ @infos << I18n.t("subscription_client.subscriptions.info.#{key}", **attrs)
110
+ end
111
+
112
+ def error(key, supplier)
113
+ @errors << I18n.t("subscription_client.subscriptions.error.#{key}", supplier: supplier.name,
114
+ supplier_url: supplier.url)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ class Subscriptions
5
+ def initialize
6
+ @suppliers = SubscriptionClientSupplier.authorized
7
+ end
8
+
9
+ def self.update
10
+ new.update
11
+ end
12
+
13
+ def update
14
+ return unless SiteSetting.subscription_client_enabled
15
+
16
+ @result = DiscourseSubscriptionClient::Subscriptions::Result.new
17
+
18
+ if @suppliers.blank?
19
+ @result.no_suppliers
20
+ else
21
+ @suppliers.each do |supplier|
22
+ update_supplier(supplier)
23
+ end
24
+ end
25
+
26
+ if @result.errors.any?
27
+ @result.errors.each do |error|
28
+ Rails.logger.error "DiscourseSubscriptionClient::Subscriptions.update: #{error}"
29
+ end
30
+ end
31
+
32
+ if SiteSetting.subscription_client_verbose_logs && @result.infos.any?
33
+ @result.infos.each do |info|
34
+ Rails.logger.info "DiscourseSubscriptionClient::Subscriptions.update: #{info}"
35
+ end
36
+ end
37
+
38
+ @result.errors.blank?
39
+ end
40
+
41
+ def update_supplier(supplier)
42
+ resources = supplier.resources
43
+ return unless resources.present?
44
+
45
+ request = DiscourseSubscriptionClient::Request.new(:supplier, supplier.id)
46
+ headers = { "User-Api-Key" => supplier.api_key }
47
+ url = "#{supplier.url}/subscription-server/user-subscriptions"
48
+
49
+ response = request.perform(url, headers: headers, body: { resources: resources.map(&:name) })
50
+ return (supplier.deactivate_all_subscriptions! && @result.connection_error(supplier)) if response.nil?
51
+
52
+ subscription_data = @result.retrieve_subscriptions(supplier, response)
53
+ return supplier.deactivate_all_subscriptions! if @result.errors.any?
54
+
55
+ # deactivate any of the supplier's subscriptions not retrieved from supplier
56
+ if supplier.subscriptions.present?
57
+ supplier.subscriptions.each do |subscription|
58
+ has_match = false
59
+ subscription_data.each do |data|
60
+ if data_matches_subscription(data, subscription)
61
+ data.subscription = subscription
62
+ has_match = true
63
+ end
64
+ end
65
+ subscription.deactivate! unless has_match
66
+ end
67
+ end
68
+
69
+ return @result.no_subscriptions(supplier) if subscription_data.blank?
70
+
71
+ subscription_data.each do |data|
72
+ if data.subscription.present?
73
+ data.subscription.update(subscribed: true)
74
+ data.subscription.touch
75
+
76
+ @result.updated_subscription(supplier, subscription_ids: data.required)
77
+ else
78
+ subscription = SubscriptionClientSubscription.create!(data.create.merge(subscribed: true))
79
+
80
+ if subscription
81
+ @result.created_subscription(supplier, subscription_ids: data.required)
82
+ else
83
+ @result.failed_to_create_subscription(supplier, subscription_ids: data.required)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def data_matches_subscription(data, subscription)
90
+ data.required.all? { |k, v| subscription.send(k.to_s) == v }
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscourseSubscriptionClient
4
+ VERSION = "0.1.0.pre1"
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[
4
+ ./discourse_subscription_client/version
5
+ ./discourse_subscription_client/engine
6
+ ].each do |path|
7
+ require_relative path
8
+ end
metadata ADDED
@@ -0,0 +1,252 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: discourse_subscription_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre1
5
+ platform: ruby
6
+ authors:
7
+ - Angus McLeod
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: active_model_serializers
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.8.3
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: annotate
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: excon
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: fabrication
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: message_bus
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: mini_scheduler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rails
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: 7.0.4.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 7.0.4.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec-activemodel-mocks
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec-rails
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: webmock
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Adds a backend subscription client to a Discourse plugin.
182
+ email:
183
+ - angus@mcleod.org.au
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - MIT-LICENSE
189
+ - README.md
190
+ - Rakefile
191
+ - app/assets/config/discourse_subscription_client_manifest.js
192
+ - app/assets/stylesheets/discourse_subscription_client/application.css
193
+ - app/controllers/discourse_subscription_client/admin_controller.rb
194
+ - app/controllers/discourse_subscription_client/notices_controller.rb
195
+ - app/controllers/discourse_subscription_client/subscriptions_controller.rb
196
+ - app/controllers/discourse_subscription_client/suppliers_controller.rb
197
+ - app/jobs/regular/discourse_subscription_client/find_resources.rb
198
+ - app/jobs/scheduled/discourse_subscription_client/update_notices.rb
199
+ - app/jobs/scheduled/discourse_subscription_client/update_subscriptions.rb
200
+ - app/models/subscription_client_notice.rb
201
+ - app/models/subscription_client_request.rb
202
+ - app/models/subscription_client_resource.rb
203
+ - app/models/subscription_client_subscription.rb
204
+ - app/models/subscription_client_supplier.rb
205
+ - app/serializers/discourse_subscription_client/notice_serializer.rb
206
+ - app/serializers/discourse_subscription_client/resource_serializer.rb
207
+ - app/serializers/discourse_subscription_client/subscription_serializer.rb
208
+ - app/serializers/discourse_subscription_client/supplier_serializer.rb
209
+ - config/locales/server.en.yml
210
+ - config/routes.rb
211
+ - config/settings.yml
212
+ - db/migrate/20220318160955_create_subscription_client_suppliers.rb
213
+ - db/migrate/20220318181029_create_subscription_client_resources.rb
214
+ - db/migrate/20220318181054_create_subscription_client_notices.rb
215
+ - db/migrate/20220318181140_create_subscription_client_subscriptions.rb
216
+ - db/migrate/20230220130259_create_subscription_client_requests.rb
217
+ - lib/discourse_subscription_client.rb
218
+ - lib/discourse_subscription_client/authorization.rb
219
+ - lib/discourse_subscription_client/engine.rb
220
+ - lib/discourse_subscription_client/notices.rb
221
+ - lib/discourse_subscription_client/request.rb
222
+ - lib/discourse_subscription_client/resources.rb
223
+ - lib/discourse_subscription_client/subscriptions.rb
224
+ - lib/discourse_subscription_client/subscriptions/result.rb
225
+ - lib/discourse_subscription_client/version.rb
226
+ homepage: https://github.com/paviliondev/discourse_subscription_client
227
+ licenses:
228
+ - GPL-2.0-only
229
+ metadata:
230
+ homepage_uri: https://github.com/paviliondev/discourse_subscription_client
231
+ source_code_uri: https://github.com/paviliondev/discourse_subscription_client
232
+ changelog_uri: https://github.com/paviliondev/discourse_subscription_client
233
+ post_install_message:
234
+ rdoc_options: []
235
+ require_paths:
236
+ - lib
237
+ required_ruby_version: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - ">="
240
+ - !ruby/object:Gem::Version
241
+ version: 2.6.0
242
+ required_rubygems_version: !ruby/object:Gem::Requirement
243
+ requirements:
244
+ - - ">"
245
+ - !ruby/object:Gem::Version
246
+ version: 1.3.1
247
+ requirements: []
248
+ rubygems_version: 3.3.26
249
+ signing_key:
250
+ specification_version: 4
251
+ summary: Discourse plugin subscription client.
252
+ test_files: []