blog_verification 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +413 -0
- data/blog_verification.gemspec +40 -0
- data/lib/blog_verification/adapters/base_adapter.rb +30 -0
- data/lib/blog_verification/adapters/devto_adapter.rb +98 -0
- data/lib/blog_verification/adapters/medium_adapter.rb +271 -0
- data/lib/blog_verification/configuration.rb +29 -0
- data/lib/blog_verification/constants/error_codes.rb +65 -0
- data/lib/blog_verification/constants/taxonomy.rb +248 -0
- data/lib/blog_verification/errors.rb +34 -0
- data/lib/blog_verification/rails/railtie.rb +13 -0
- data/lib/blog_verification/rails/validator.rb +79 -0
- data/lib/blog_verification/services/validation_service.rb +468 -0
- data/lib/blog_verification/services/verification_service.rb +274 -0
- data/lib/blog_verification/utils/date_helper.rb +167 -0
- data/lib/blog_verification/utils/normalizer.rb +104 -0
- data/lib/blog_verification/utils/url_security.rb +314 -0
- data/lib/blog_verification/version.rb +5 -0
- data/lib/blog_verification.rb +56 -0
- metadata +143 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../constants/error_codes'
|
|
4
|
+
|
|
5
|
+
module BlogVerification
|
|
6
|
+
module Rails
|
|
7
|
+
# Custom ActiveModel validator for Rails models:
|
|
8
|
+
#
|
|
9
|
+
# class ArticleSubmission < ApplicationRecord
|
|
10
|
+
# validates :url, blog_verification: {
|
|
11
|
+
# platform: :platform,
|
|
12
|
+
# username: :author_username,
|
|
13
|
+
# published_at: :published_date,
|
|
14
|
+
# topic: :expected_topic,
|
|
15
|
+
# summary: :expected_summary,
|
|
16
|
+
# tags: :expected_tags,
|
|
17
|
+
# min_word_count: 500
|
|
18
|
+
# }
|
|
19
|
+
# end
|
|
20
|
+
class BlogVerificationValidator < ::ActiveModel::EachValidator
|
|
21
|
+
def validate_each(record, attribute, value)
|
|
22
|
+
return if value.nil? || value.to_s.strip.empty?
|
|
23
|
+
|
|
24
|
+
resolve_attr = lambda do |key|
|
|
25
|
+
val = options[key]
|
|
26
|
+
if val.is_a?(Symbol) && record.respond_to?(val)
|
|
27
|
+
record.public_send(val)
|
|
28
|
+
elsif val.is_a?(Proc)
|
|
29
|
+
val.call(record)
|
|
30
|
+
else
|
|
31
|
+
val
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
platform = resolve_attr.call(:platform)
|
|
36
|
+
username = resolve_attr.call(:username)
|
|
37
|
+
published_at = resolve_attr.call(:published_at) || resolve_attr.call(:publishedAt)
|
|
38
|
+
topic = resolve_attr.call(:topic)
|
|
39
|
+
summary = resolve_attr.call(:summary)
|
|
40
|
+
tags = resolve_attr.call(:tags)
|
|
41
|
+
min_word_count = resolve_attr.call(:min_word_count) || resolve_attr.call(:minWordCount) || 500
|
|
42
|
+
reference_date = resolve_attr.call(:reference_date) || resolve_attr.call(:referenceDate)
|
|
43
|
+
|
|
44
|
+
payload = {
|
|
45
|
+
'platform' => platform,
|
|
46
|
+
'url' => value,
|
|
47
|
+
'username' => username,
|
|
48
|
+
'publishedAt' => published_at,
|
|
49
|
+
'topic' => topic,
|
|
50
|
+
'summary' => summary,
|
|
51
|
+
'tags' => tags,
|
|
52
|
+
'minWordCount' => min_word_count,
|
|
53
|
+
'referenceDate' => reference_date
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
result = BlogVerification.verify(payload)
|
|
57
|
+
|
|
58
|
+
# Store full verification result on the record if accessor exists
|
|
59
|
+
if record.respond_to?(:verification_result=)
|
|
60
|
+
record.verification_result = result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if result['status'] == 'error'
|
|
64
|
+
(result['errors'] || []).each do |err|
|
|
65
|
+
record.errors.add(attribute, err['message'] || 'is invalid')
|
|
66
|
+
end
|
|
67
|
+
elsif result['status'] == BlogVerification::ErrorCodes::VerificationStatus::INVALID
|
|
68
|
+
(result['errors'] || []).each do |err|
|
|
69
|
+
record.errors.add(attribute, err['message'] || 'failed blog verification')
|
|
70
|
+
end
|
|
71
|
+
elsif result['status'] == BlogVerification::ErrorCodes::VerificationStatus::UNVERIFIED
|
|
72
|
+
if options[:allow_unverified] != true
|
|
73
|
+
record.errors.add(attribute, 'could not be verified from the platform')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../constants/error_codes'
|
|
4
|
+
require_relative '../constants/taxonomy'
|
|
5
|
+
require_relative '../utils/normalizer'
|
|
6
|
+
require_relative '../utils/date_helper'
|
|
7
|
+
|
|
8
|
+
module BlogVerification
|
|
9
|
+
module Services
|
|
10
|
+
class ValidationService
|
|
11
|
+
include ErrorCodes
|
|
12
|
+
|
|
13
|
+
# Evaluates all validation checks against retrieved source metadata
|
|
14
|
+
#
|
|
15
|
+
# @param [Hash] request_data User request hash
|
|
16
|
+
# @param [Hash] source_metadata Metadata extracted by platform adapter
|
|
17
|
+
# @return [Hash] JSON-compliant verification response structure
|
|
18
|
+
def evaluate(request_data, source_metadata)
|
|
19
|
+
req = normalize_request_data(request_data)
|
|
20
|
+
src = normalize_source_metadata(source_metadata)
|
|
21
|
+
|
|
22
|
+
checks = {}
|
|
23
|
+
errors = []
|
|
24
|
+
|
|
25
|
+
# 1. URL check
|
|
26
|
+
checks['url'] = { 'status' => CheckStatus::PASSED }
|
|
27
|
+
|
|
28
|
+
# 2. Author username check (FR-04)
|
|
29
|
+
author_username = src.dig('author', 'username')
|
|
30
|
+
if author_username && !author_username.to_s.strip.empty?
|
|
31
|
+
norm_expected = Utils::Normalizer.normalize_username(req['username'])
|
|
32
|
+
norm_actual = Utils::Normalizer.normalize_username(author_username)
|
|
33
|
+
|
|
34
|
+
if norm_expected == norm_actual
|
|
35
|
+
checks['username'] = {
|
|
36
|
+
'status' => CheckStatus::PASSED,
|
|
37
|
+
'expected' => req['username'],
|
|
38
|
+
'actual' => author_username
|
|
39
|
+
}
|
|
40
|
+
else
|
|
41
|
+
checks['username'] = {
|
|
42
|
+
'status' => CheckStatus::FAILED,
|
|
43
|
+
'expected' => req['username'],
|
|
44
|
+
'actual' => author_username
|
|
45
|
+
}
|
|
46
|
+
errors << {
|
|
47
|
+
'code' => AUTHOR_MISMATCH,
|
|
48
|
+
'field' => 'username',
|
|
49
|
+
'message' => DEFAULT_ERROR_MESSAGES[AUTHOR_MISMATCH],
|
|
50
|
+
'expected' => req['username'],
|
|
51
|
+
'actual' => author_username
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
checks['username'] = {
|
|
56
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
57
|
+
'message' => 'Author username could not be established from source metadata.'
|
|
58
|
+
}
|
|
59
|
+
errors << {
|
|
60
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
61
|
+
'field' => 'username',
|
|
62
|
+
'message' => 'Author username could not be established from source metadata.'
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 3. Publication date check (FR-05)
|
|
67
|
+
published_at_src = src['published_at']
|
|
68
|
+
if published_at_src && !published_at_src.to_s.strip.empty?
|
|
69
|
+
date_comp = Utils::DateHelper.compare_calendar_dates(req['published_at'], published_at_src)
|
|
70
|
+
normalized_actual = date_comp[:normalized_actual]
|
|
71
|
+
|
|
72
|
+
if normalized_actual
|
|
73
|
+
if date_comp[:match]
|
|
74
|
+
checks['publishedAt'] = {
|
|
75
|
+
'status' => CheckStatus::PASSED,
|
|
76
|
+
'expected' => req['published_at'],
|
|
77
|
+
'actual' => normalized_actual
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
checks['publishedAt'] = {
|
|
81
|
+
'status' => CheckStatus::FAILED,
|
|
82
|
+
'expected' => req['published_at'],
|
|
83
|
+
'actual' => normalized_actual
|
|
84
|
+
}
|
|
85
|
+
errors << {
|
|
86
|
+
'code' => PUBLICATION_DATE_MISMATCH,
|
|
87
|
+
'field' => 'publishedAt',
|
|
88
|
+
'message' => DEFAULT_ERROR_MESSAGES[PUBLICATION_DATE_MISMATCH],
|
|
89
|
+
'expected' => req['published_at'],
|
|
90
|
+
'actual' => normalized_actual
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
checks['publishedAt'] = {
|
|
95
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
96
|
+
'message' => 'Source publication date could not be parsed to a valid UTC date.'
|
|
97
|
+
}
|
|
98
|
+
errors << {
|
|
99
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
100
|
+
'field' => 'publishedAt',
|
|
101
|
+
'message' => 'Source publication date could not be parsed to a valid UTC date.'
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
checks['publishedAt'] = {
|
|
106
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
107
|
+
'message' => 'Original publication date could not be established from source metadata.'
|
|
108
|
+
}
|
|
109
|
+
errors << {
|
|
110
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
111
|
+
'field' => 'publishedAt',
|
|
112
|
+
'message' => 'Original publication date could not be established from source metadata.'
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# 4. Quarter verification check
|
|
117
|
+
if published_at_src && !published_at_src.to_s.strip.empty?
|
|
118
|
+
quarter_result = Utils::DateHelper.date_in_current_quarter?(published_at_src, req['reference_date'])
|
|
119
|
+
if quarter_result[:in_current_quarter]
|
|
120
|
+
checks['quarter'] = {
|
|
121
|
+
'status' => CheckStatus::PASSED,
|
|
122
|
+
'currentQuarter' => quarter_result[:current_quarter][:label],
|
|
123
|
+
'blogQuarter' => quarter_result[:blog_quarter][:label],
|
|
124
|
+
'message' => "Blog publication date is in the current quarter (#{quarter_result[:current_quarter][:label]})."
|
|
125
|
+
}
|
|
126
|
+
elsif quarter_result[:blog_quarter] && quarter_result[:current_quarter]
|
|
127
|
+
checks['quarter'] = {
|
|
128
|
+
'status' => CheckStatus::FAILED,
|
|
129
|
+
'currentQuarter' => quarter_result[:current_quarter][:label],
|
|
130
|
+
'blogQuarter' => quarter_result[:blog_quarter][:label],
|
|
131
|
+
'message' => "Blog publication date is not in the current quarter. Expected #{quarter_result[:current_quarter][:label]}, but blog was published in #{quarter_result[:blog_quarter][:label]}."
|
|
132
|
+
}
|
|
133
|
+
errors << {
|
|
134
|
+
'code' => NOT_IN_CURRENT_QUARTER,
|
|
135
|
+
'field' => 'publishedAt',
|
|
136
|
+
'message' => "Blog publication date is not in the current quarter. Expected #{quarter_result[:current_quarter][:label]}, but blog was published in #{quarter_result[:blog_quarter][:label]}.",
|
|
137
|
+
'expected' => quarter_result[:current_quarter][:label],
|
|
138
|
+
'actual' => quarter_result[:blog_quarter][:label]
|
|
139
|
+
}
|
|
140
|
+
else
|
|
141
|
+
checks['quarter'] = {
|
|
142
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
143
|
+
'message' => 'Quarter could not be established from source publication date.'
|
|
144
|
+
}
|
|
145
|
+
errors << {
|
|
146
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
147
|
+
'field' => 'quarter',
|
|
148
|
+
'message' => 'Quarter could not be established from source publication date.'
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
checks['quarter'] = {
|
|
153
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
154
|
+
'message' => 'Quarter could not be verified because source publication date is unavailable.'
|
|
155
|
+
}
|
|
156
|
+
errors << {
|
|
157
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
158
|
+
'field' => 'quarter',
|
|
159
|
+
'message' => 'Quarter could not be verified because source publication date is unavailable.'
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# 5. Tags check (FR-06 - Optional)
|
|
164
|
+
has_supplied_tags = req['tags'].is_a?(Array) && !req['tags'].empty?
|
|
165
|
+
if !has_supplied_tags
|
|
166
|
+
checks['tags'] = {
|
|
167
|
+
'status' => CheckStatus::SKIPPED,
|
|
168
|
+
'message' => 'Tag validation was skipped because tags were not provided.'
|
|
169
|
+
}
|
|
170
|
+
else
|
|
171
|
+
source_tags = src['tags']
|
|
172
|
+
if source_tags.is_a?(Array)
|
|
173
|
+
comp = Utils::Normalizer.compare_tag_sets(req['tags'], source_tags)
|
|
174
|
+
if comp[:match]
|
|
175
|
+
checks['tags'] = {
|
|
176
|
+
'status' => CheckStatus::PASSED,
|
|
177
|
+
'expected' => comp[:normalized_expected],
|
|
178
|
+
'actual' => comp[:normalized_actual]
|
|
179
|
+
}
|
|
180
|
+
else
|
|
181
|
+
checks['tags'] = {
|
|
182
|
+
'status' => CheckStatus::FAILED,
|
|
183
|
+
'expected' => comp[:normalized_expected],
|
|
184
|
+
'actual' => comp[:normalized_actual]
|
|
185
|
+
}
|
|
186
|
+
errors << {
|
|
187
|
+
'code' => TAGS_MISMATCH,
|
|
188
|
+
'field' => 'tags',
|
|
189
|
+
'message' => DEFAULT_ERROR_MESSAGES[TAGS_MISMATCH],
|
|
190
|
+
'expected' => comp[:normalized_expected],
|
|
191
|
+
'actual' => comp[:normalized_actual]
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
else
|
|
195
|
+
checks['tags'] = {
|
|
196
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
197
|
+
'message' => 'Source platform does not expose reliable tag metadata.'
|
|
198
|
+
}
|
|
199
|
+
errors << {
|
|
200
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
201
|
+
'field' => 'tags',
|
|
202
|
+
'message' => 'Source platform does not expose reliable tag metadata.'
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# 6. Topic check (FR-07)
|
|
208
|
+
verified_topic = Taxonomy.derive_topic_from_metadata(src)
|
|
209
|
+
if verified_topic
|
|
210
|
+
if Taxonomy.compare_topics(req['topic'], verified_topic)
|
|
211
|
+
checks['topic'] = {
|
|
212
|
+
'status' => CheckStatus::PASSED,
|
|
213
|
+
'expected' => req['topic'],
|
|
214
|
+
'actual' => verified_topic
|
|
215
|
+
}
|
|
216
|
+
else
|
|
217
|
+
checks['topic'] = {
|
|
218
|
+
'status' => CheckStatus::FAILED,
|
|
219
|
+
'expected' => req['topic'],
|
|
220
|
+
'actual' => verified_topic
|
|
221
|
+
}
|
|
222
|
+
errors << {
|
|
223
|
+
'code' => TOPIC_MISMATCH,
|
|
224
|
+
'field' => 'topic',
|
|
225
|
+
'message' => DEFAULT_ERROR_MESSAGES[TOPIC_MISMATCH],
|
|
226
|
+
'expected' => req['topic'],
|
|
227
|
+
'actual' => verified_topic
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
else
|
|
231
|
+
checks['topic'] = {
|
|
232
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
233
|
+
'message' => 'Article topic could not be established reliably from source metadata.'
|
|
234
|
+
}
|
|
235
|
+
errors << {
|
|
236
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
237
|
+
'field' => 'topic',
|
|
238
|
+
'message' => 'Article topic could not be established reliably from source metadata.'
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# 7. Summary check (FR-08)
|
|
243
|
+
source_description = src['description']
|
|
244
|
+
if source_description && !source_description.to_s.strip.empty?
|
|
245
|
+
comp = Utils::Normalizer.compare_summaries(req['summary'], source_description)
|
|
246
|
+
if comp[:match]
|
|
247
|
+
checks['summary'] = { 'status' => CheckStatus::PASSED }
|
|
248
|
+
else
|
|
249
|
+
checks['summary'] = { 'status' => CheckStatus::FAILED }
|
|
250
|
+
errors << {
|
|
251
|
+
'code' => SUMMARY_MISMATCH,
|
|
252
|
+
'field' => 'summary',
|
|
253
|
+
'message' => DEFAULT_ERROR_MESSAGES[SUMMARY_MISMATCH],
|
|
254
|
+
'expected' => req['summary'],
|
|
255
|
+
'actual' => source_description
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
else
|
|
259
|
+
checks['summary'] = {
|
|
260
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
261
|
+
'message' => 'Article description or excerpt could not be established from source metadata.'
|
|
262
|
+
}
|
|
263
|
+
errors << {
|
|
264
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
265
|
+
'field' => 'summary',
|
|
266
|
+
'message' => 'Article description or excerpt could not be established from source metadata.'
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# 8. Blog content length / word count check
|
|
271
|
+
min_words = req['min_word_count'] || 500
|
|
272
|
+
actual_word_count = src['word_count']
|
|
273
|
+
if actual_word_count.is_a?(Numeric)
|
|
274
|
+
if actual_word_count >= min_words
|
|
275
|
+
checks['contentLength'] = {
|
|
276
|
+
'status' => CheckStatus::PASSED,
|
|
277
|
+
'wordCount' => actual_word_count,
|
|
278
|
+
'minRequired' => min_words,
|
|
279
|
+
'message' => "Blog length passed (#{actual_word_count} words >= #{min_words} words)."
|
|
280
|
+
}
|
|
281
|
+
else
|
|
282
|
+
checks['contentLength'] = {
|
|
283
|
+
'status' => CheckStatus::FAILED,
|
|
284
|
+
'wordCount' => actual_word_count,
|
|
285
|
+
'minRequired' => min_words,
|
|
286
|
+
'message' => "Blog length failed: article has #{actual_word_count} words, which is below the required #{min_words} words."
|
|
287
|
+
}
|
|
288
|
+
errors << {
|
|
289
|
+
'code' => INSUFFICIENT_CONTENT_LENGTH,
|
|
290
|
+
'field' => 'contentLength',
|
|
291
|
+
'message' => "The blog content does not meet the minimum required length of #{min_words} words.",
|
|
292
|
+
'expected' => min_words,
|
|
293
|
+
'actual' => actual_word_count
|
|
294
|
+
}
|
|
295
|
+
end
|
|
296
|
+
else
|
|
297
|
+
checks['contentLength'] = {
|
|
298
|
+
'status' => CheckStatus::UNVERIFIED,
|
|
299
|
+
'message' => 'Blog content length could not be established from source metadata.'
|
|
300
|
+
}
|
|
301
|
+
errors << {
|
|
302
|
+
'code' => SOURCE_METADATA_UNAVAILABLE,
|
|
303
|
+
'field' => 'contentLength',
|
|
304
|
+
'message' => 'Blog content length could not be established from source metadata.'
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
aggregate_status(checks, errors, req)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Aggregates individual check statuses into valid, invalid, or unverified response
|
|
312
|
+
def aggregate_status(checks, errors, req)
|
|
313
|
+
check_statuses = checks.values.map { |c| c['status'] }
|
|
314
|
+
has_failed = check_statuses.include?(CheckStatus::FAILED)
|
|
315
|
+
has_unverified = check_statuses.include?(CheckStatus::UNVERIFIED)
|
|
316
|
+
|
|
317
|
+
if has_failed
|
|
318
|
+
overall_status = VerificationStatus::INVALID
|
|
319
|
+
msg = 'Blog verification failed.'
|
|
320
|
+
elsif has_unverified
|
|
321
|
+
overall_status = VerificationStatus::UNVERIFIED
|
|
322
|
+
msg = 'Blog verification could not be completed.'
|
|
323
|
+
else
|
|
324
|
+
overall_status = VerificationStatus::VALID
|
|
325
|
+
msg = 'Blog verification completed successfully.'
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
res = {
|
|
329
|
+
'success' => true,
|
|
330
|
+
'status' => overall_status,
|
|
331
|
+
'message' => msg,
|
|
332
|
+
'data' => {
|
|
333
|
+
'platform' => req['platform'],
|
|
334
|
+
'url' => req['url'],
|
|
335
|
+
'checks' => checks
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
res['errors'] = errors unless errors.empty?
|
|
340
|
+
res
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Builds a structured response when upstream source fails
|
|
344
|
+
def build_source_failure_response(request_data, upstream_error)
|
|
345
|
+
req = normalize_request_data(request_data)
|
|
346
|
+
has_supplied_tags = req['tags'].is_a?(Array) && !req['tags'].empty?
|
|
347
|
+
is_not_found = upstream_error.respond_to?(:error_code) &&
|
|
348
|
+
upstream_error.error_code == ARTICLE_NOT_FOUND
|
|
349
|
+
|
|
350
|
+
if is_not_found
|
|
351
|
+
return {
|
|
352
|
+
'success' => true,
|
|
353
|
+
'status' => VerificationStatus::INVALID,
|
|
354
|
+
'message' => 'Blog verification failed.',
|
|
355
|
+
'errors' => [
|
|
356
|
+
{
|
|
357
|
+
'code' => ARTICLE_NOT_FOUND,
|
|
358
|
+
'field' => 'url',
|
|
359
|
+
'message' => upstream_error.message || DEFAULT_ERROR_MESSAGES[ARTICLE_NOT_FOUND]
|
|
360
|
+
}
|
|
361
|
+
],
|
|
362
|
+
'data' => {
|
|
363
|
+
'platform' => req['platform'],
|
|
364
|
+
'url' => req['url'],
|
|
365
|
+
'checks' => {
|
|
366
|
+
'url' => {
|
|
367
|
+
'status' => CheckStatus::FAILED,
|
|
368
|
+
'message' => upstream_error.message || DEFAULT_ERROR_MESSAGES[ARTICLE_NOT_FOUND]
|
|
369
|
+
},
|
|
370
|
+
'username' => { 'status' => CheckStatus::UNVERIFIED },
|
|
371
|
+
'publishedAt' => { 'status' => CheckStatus::UNVERIFIED },
|
|
372
|
+
'quarter' => { 'status' => CheckStatus::UNVERIFIED },
|
|
373
|
+
'tags' => has_supplied_tags ? { 'status' => CheckStatus::UNVERIFIED } : { 'status' => CheckStatus::SKIPPED, 'message' => 'Tag validation was skipped because tags were not provided.' },
|
|
374
|
+
'topic' => { 'status' => CheckStatus::UNVERIFIED },
|
|
375
|
+
'summary' => { 'status' => CheckStatus::UNVERIFIED },
|
|
376
|
+
'contentLength' => { 'status' => CheckStatus::UNVERIFIED }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
err_code = upstream_error.respond_to?(:error_code) ? upstream_error.error_code : SOURCE_UNAVAILABLE
|
|
383
|
+
{
|
|
384
|
+
'success' => true,
|
|
385
|
+
'status' => VerificationStatus::UNVERIFIED,
|
|
386
|
+
'message' => 'Blog verification could not be completed.',
|
|
387
|
+
'errors' => [
|
|
388
|
+
{
|
|
389
|
+
'code' => err_code,
|
|
390
|
+
'field' => nil,
|
|
391
|
+
'message' => upstream_error.message
|
|
392
|
+
}
|
|
393
|
+
],
|
|
394
|
+
'data' => {
|
|
395
|
+
'platform' => req['platform'],
|
|
396
|
+
'url' => req['url'],
|
|
397
|
+
'checks' => {
|
|
398
|
+
'url' => { 'status' => CheckStatus::UNVERIFIED },
|
|
399
|
+
'username' => { 'status' => CheckStatus::UNVERIFIED },
|
|
400
|
+
'publishedAt' => { 'status' => CheckStatus::UNVERIFIED },
|
|
401
|
+
'quarter' => { 'status' => CheckStatus::UNVERIFIED },
|
|
402
|
+
'tags' => has_supplied_tags ? { 'status' => CheckStatus::UNVERIFIED } : { 'status' => CheckStatus::SKIPPED, 'message' => 'Tag validation was skipped because tags were not provided.' },
|
|
403
|
+
'topic' => { 'status' => CheckStatus::UNVERIFIED },
|
|
404
|
+
'summary' => { 'status' => CheckStatus::UNVERIFIED },
|
|
405
|
+
'contentLength' => { 'status' => CheckStatus::UNVERIFIED }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
private
|
|
412
|
+
|
|
413
|
+
def normalize_request_data(data)
|
|
414
|
+
h = {}
|
|
415
|
+
data.each do |k, v|
|
|
416
|
+
key_str = k.to_s
|
|
417
|
+
case key_str
|
|
418
|
+
when 'platform' then h['platform'] = v.to_s.downcase.strip
|
|
419
|
+
when 'url' then h['url'] = v.to_s.strip
|
|
420
|
+
when 'username' then h['username'] = v.to_s.strip
|
|
421
|
+
when 'publishedAt', 'published_at' then h['published_at'] = v.to_s.strip
|
|
422
|
+
when 'tags' then h['tags'] = v
|
|
423
|
+
when 'topic' then h['topic'] = v.to_s.strip
|
|
424
|
+
when 'summary' then h['summary'] = v.to_s.strip
|
|
425
|
+
when 'minWordCount', 'min_word_count' then h['min_word_count'] = v.to_i
|
|
426
|
+
when 'referenceDate', 'reference_date' then h['reference_date'] = v.to_s.strip
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
h
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def normalize_source_metadata(metadata)
|
|
433
|
+
return {} unless metadata.is_a?(Hash)
|
|
434
|
+
|
|
435
|
+
h = {}
|
|
436
|
+
metadata.each do |k, v|
|
|
437
|
+
key_str = k.to_s
|
|
438
|
+
case key_str
|
|
439
|
+
when 'canonical_url', 'canonicalUrl' then h['canonical_url'] = v
|
|
440
|
+
when 'platform' then h['platform'] = v
|
|
441
|
+
when 'author'
|
|
442
|
+
if v.is_a?(Hash)
|
|
443
|
+
author_hash = {}
|
|
444
|
+
v.each do |ak, av|
|
|
445
|
+
ak_str = ak.to_s
|
|
446
|
+
if %w[username].include?(ak_str)
|
|
447
|
+
author_hash['username'] = av
|
|
448
|
+
elsif %w[displayName display_name name].include?(ak_str)
|
|
449
|
+
author_hash['display_name'] = av
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
h['author'] = author_hash
|
|
453
|
+
end
|
|
454
|
+
when 'published_at', 'publishedAt' then h['published_at'] = v
|
|
455
|
+
when 'updated_at', 'updatedAt' then h['updated_at'] = v
|
|
456
|
+
when 'tags' then h['tags'] = v
|
|
457
|
+
when 'topic' then h['topic'] = v
|
|
458
|
+
when 'description' then h['description'] = v
|
|
459
|
+
when 'content' then h['content'] = v
|
|
460
|
+
when 'word_count', 'wordCount' then h['word_count'] = v
|
|
461
|
+
when 'raw_metadata', 'rawMetadata' then h['raw_metadata'] = v
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
h
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|