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,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: []