jekyll-webmention_io 4.1.0 → 4.2.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jekyll/assets/webmention_loader.js +4 -2
  3. data/lib/jekyll/caches.rb +98 -0
  4. data/lib/jekyll/commands/webmention.rb +62 -61
  5. data/lib/jekyll/config.rb +336 -0
  6. data/lib/jekyll/generators/compile_js.rb +40 -26
  7. data/lib/jekyll/generators/gather_webmentions.rb +50 -101
  8. data/lib/jekyll/generators/queue_webmentions.rb +74 -139
  9. data/lib/jekyll/network_client.rb +109 -0
  10. data/lib/jekyll/tags/bookmarks.rb +2 -2
  11. data/lib/jekyll/tags/count.rb +5 -5
  12. data/lib/jekyll/tags/likes.rb +2 -2
  13. data/lib/jekyll/tags/links.rb +2 -2
  14. data/lib/jekyll/tags/posts.rb +2 -2
  15. data/lib/jekyll/tags/replies.rb +2 -2
  16. data/lib/jekyll/tags/reposts.rb +2 -2
  17. data/lib/jekyll/tags/rsvps.rb +2 -2
  18. data/lib/jekyll/tags/webmention.rb +31 -44
  19. data/lib/jekyll/tags/webmention_type.rb +1 -1
  20. data/lib/jekyll/tags/webmentions.rb +2 -2
  21. data/lib/jekyll/tags/webmentions_head.rb +21 -17
  22. data/lib/jekyll/tags/webmentions_js.rb +1 -1
  23. data/lib/jekyll/templates/bookmarks.html +7 -8
  24. data/lib/jekyll/templates/likes.html +6 -7
  25. data/lib/jekyll/templates/links.html +7 -8
  26. data/lib/jekyll/templates/posts.html +7 -8
  27. data/lib/jekyll/templates/replies.html +7 -8
  28. data/lib/jekyll/templates/reposts.html +6 -7
  29. data/lib/jekyll/templates/rsvps.html +6 -7
  30. data/lib/jekyll/templates/webmentions.html +10 -9
  31. data/lib/jekyll/templates.rb +59 -0
  32. data/lib/jekyll/webmention_io/js_handler.rb +8 -47
  33. data/lib/jekyll/webmention_io/version.rb +1 -1
  34. data/lib/jekyll/webmention_io/webmention_item.rb +46 -46
  35. data/lib/jekyll/webmention_io.rb +26 -549
  36. data/lib/jekyll/webmention_policy.rb +195 -0
  37. data/lib/jekyll/webmentions.rb +155 -0
  38. data/lib/jekyll-webmention_io.rb +2 -2
  39. metadata +130 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc9d31a7e443e2a0b12a6d4b5ce65fff675e8302113f751eff6b5bbde3d090e3
4
- data.tar.gz: 3c57fc8d14d3345def7d2bd432745fbcd9551d9931adc7ea870d7ae7ed638f6f
3
+ metadata.gz: eefc9862bc9a4e40c060cc0cbf7bd549e834e427b710b7340a8bb0b3bf6e20ad
4
+ data.tar.gz: 68506fdb42fb4e79639baedbfabc44c3aeefd2b2e9bbd5db7824a3948b7d04f6
5
5
  SHA512:
6
- metadata.gz: a650de9ca6309c29e09f5f9533ac5ca62dddec88f8dca8302efb9aee4254fb7445c86f68f4bc06f070189e6cf47f04efcaa6942e9cdd523536e2bc319b70c531
7
- data.tar.gz: afd4a141acc8cb97e78e27f0bfcac62f4d8d3d355a4362b7ef94789ba3e5710023f3813bb04a7a9d6de27af0e84167524440d88181aaceb3d18469362deae115
6
+ metadata.gz: 4badbaedab8e97b0d310586104c8179a8b0f8952214bc58de83c1df1ff97603ceba6321e18932be494eb5956b3457a197d76092dbe041c1d0ea674d50f6c3264
7
+ data.tar.gz: 505e06d78be628a3962a7885ac731c4f7aaab0b0079a4b40fc3a9c19ed77039c0460e468d8c5ceb3254c84f1b9eba6d30ca39e06743dd18db56fdca4b75de642
@@ -24,10 +24,12 @@
24
24
  redirects = false;
25
25
  }
26
26
 
27
- // Load up any unpublished webmentions on load
27
+ // Load up any unpublished webmentions on load. The API base is injected at
28
+ // build time (see CompileJS#add_config); fall back to webmention.io.
28
29
  $script = document.createElement('script');
29
30
  $script.async = true;
30
- $script.src = 'https://webmention.io/api/mentions?' +
31
+ $script.src = ( window.JekyllWebmentionIO.api_base || 'https://webmention.io/api' ) +
32
+ '/mentions?' +
31
33
  'jsonp=window.JekyllWebmentionIO.processWebmentions&target[]=' +
32
34
  targets.join( '&target[]=' );
33
35
  document.querySelector('head').appendChild( $script );
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'fileutils'
5
+
6
+ module Jekyll
7
+ module WebmentionIO
8
+ # The Caches class is a utility service that provides access to the cache files used
9
+ # by this plugin.
10
+ #
11
+ # It is initialized with a config object and creates a folder in the configured cache folder
12
+ # to store the cache files.
13
+ #
14
+ # The class is a singleton and the instance is accessed via the WebmentionIO.caches method.
15
+ class Caches
16
+ def initialize(config)
17
+ @config = config
18
+
19
+ FileUtils.makedirs(@config.cache_folder)
20
+ end
21
+
22
+ def incoming_webmentions
23
+ @@incoming_webmentions ||= Cache.new(cache_file_path('incoming'))
24
+ end
25
+
26
+ def outgoing_webmentions
27
+ @@outgoing_webmentions ||= Cache.new(cache_file_path('outgoing'))
28
+ end
29
+
30
+ def bad_uris
31
+ @@bad_uris ||= Cache.new(cache_file_path('bad_uris'))
32
+ end
33
+
34
+ def site_lookups
35
+ @@site_lookups ||= Cache.new(cache_file_path('lookups'))
36
+ end
37
+
38
+ # Resets all singleton cache instances by clearing the class variables.
39
+ # This is useful for testing to ensure clean state between tests.
40
+ def self.reset
41
+ @@incoming_webmentions = nil
42
+ @@outgoing_webmentions = nil
43
+ @@bad_uris = nil
44
+ @@site_lookups = nil
45
+ end
46
+
47
+ private
48
+
49
+ def cache_file_path(name)
50
+ Jekyll.sanitized_path(@config.cache_folder, "webmention_io_#{name}.yml")
51
+ end
52
+
53
+ # A class that represents a single cache file. The initalizer takes a full path
54
+ # where the cache data will be stored and retrieved.
55
+ #
56
+ # Upon initialization the current contents of the cache are loaded. Writes are not
57
+ # saved until the `write` method is called.
58
+ class Cache
59
+ extend Forwardable
60
+
61
+ attr_reader :path
62
+
63
+ def_delegator :@data, :each
64
+ def_delegator :@data, :key?
65
+ def_delegator :@data, :delete
66
+ def_delegator :@data, :dig
67
+ def_delegator :@data, :[]
68
+ def_delegator :@data, :[]=
69
+ def_delegator :@data, :empty?
70
+ def_delegator :@data, :map
71
+
72
+ def initialize(path)
73
+ # NOTE: This is a deviation from the old code! Previously if the configured
74
+ # cache folder had the word 'webmention' in it, the 'webmention_io' prefix
75
+ # would be removed, but the extra complexity wasn't worth replicating.
76
+ @path = path
77
+
78
+ begin
79
+ @data = SafeYAML.load_file(path)
80
+ rescue StandardError
81
+ @data = {}
82
+ end
83
+ end
84
+
85
+ def write
86
+ File.open(@path, 'wb') { |f| f.puts YAML.dump(@data) }
87
+ end
88
+
89
+ def clear
90
+ @data = {}
91
+ FileUtils.rm_f(@path)
92
+ end
93
+ end
94
+
95
+ private_constant :Cache
96
+ end
97
+ end
98
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require 'json'
4
4
 
5
5
  module Jekyll
6
6
  module WebmentionIO
@@ -8,8 +8,8 @@ module Jekyll
8
8
  class WebmentionCommand < Command
9
9
  def self.init_with_program(prog)
10
10
  prog.command(:webmention) do |c|
11
- c.syntax "webmention"
12
- c.description "Sends queued webmentions"
11
+ c.syntax 'webmention'
12
+ c.description 'Sends queued webmentions'
13
13
 
14
14
  c.action { |args, options| process args, options }
15
15
  end
@@ -17,68 +17,69 @@ module Jekyll
17
17
 
18
18
  def self.process(_args = [], options = {})
19
19
  options = configuration_from_options(options)
20
- WebmentionIO.bootstrap(Jekyll::Site.new(options))
20
+ site = Jekyll::Site.new(options)
21
21
 
22
- if File.exist? WebmentionIO.cache_file("sent.yml")
23
- WebmentionIO.log "error", "Your outgoing webmentions queue needs to be upgraded. Please re-build your project."
24
- end
22
+ WebmentionIO.bootstrap(site)
23
+
24
+ send_webmentions
25
+ end
25
26
 
26
- WebmentionIO.log "msg", "Getting ready to send webmentions (this may take a while)."
27
+ def self.send_webmentions
28
+ WebmentionIO.log 'msg', 'Getting ready to send webmentions (this may take a while).'
27
29
 
28
30
  count = 0
29
- max_attempts = WebmentionIO.max_attempts()
30
- cached_outgoing = WebmentionIO.get_cache_file_path "outgoing"
31
- if File.exist?(cached_outgoing)
32
- outgoing = WebmentionIO.load_yaml(cached_outgoing)
33
- outgoing.each do |source, targets|
34
- targets.each do |target, response|
35
- # skip ones we’ve handled
36
- next unless response == false or response.instance_of? Integer
37
-
38
- # skip protocol-less links, we'll need to revisit this again later
39
- next if target.index("//").zero?
40
-
41
- # produce an escaped version of the target (in case of special
42
- # characters, etc).
43
- escaped = URI::Parser.new.escape(target);
44
-
45
- # skip bad URLs
46
- next unless WebmentionIO.uri_ok?(escaped)
47
-
48
- # give up if we've attempted this too many times
49
- response = (response || 0) + 1
50
-
51
- if ! max_attempts.nil? and response > max_attempts
52
- outgoing[source][target] = ""
53
- WebmentionIO.log "msg", "Giving up sending from #{source} to #{target}."
54
- next
55
- else
56
- outgoing[source][target] = response
57
- end
58
-
59
- # get the endpoint
60
- endpoint = WebmentionIO.get_webmention_endpoint(escaped)
61
- next unless endpoint
62
-
63
- # get the response
64
- response = WebmentionIO.webmention(source, target)
65
- next unless response
66
-
67
- # capture JSON responses in case site wants to do anything with them
68
- begin
69
- response = JSON.parse response
70
- rescue JSON::ParserError
71
- response = ""
72
- end
31
+ max_attempts = WebmentionIO.config.max_attempts
32
+ outgoing = WebmentionIO.caches.outgoing_webmentions
33
+
34
+ return if outgoing.empty?
35
+
36
+ outgoing.each do |source, targets|
37
+ targets.each do |target, response|
38
+ # skip ones we’ve handled
39
+ next unless response == false || response.instance_of?(Integer)
40
+
41
+ # skip protocol-less links, we'll need to revisit this again later
42
+ idx = target.index('//')
43
+ next if idx.nil? || idx.zero?
44
+
45
+ # produce an escaped version of the target (in case of special
46
+ # characters, etc).
47
+ escaped = URI::Parser.new.escape(target);
48
+
49
+ # skip bad URLs
50
+ next unless WebmentionIO.policy.uri_ok?(escaped)
51
+
52
+ # give up if we've attempted this too many times
53
+ response = (response || 0) + 1
54
+
55
+ if !max_attempts.nil? && response > max_attempts
56
+ outgoing[source][target] = ''
57
+ WebmentionIO.log 'msg', "Giving up sending from #{source} to #{target}."
58
+ next
59
+ else
73
60
  outgoing[source][target] = response
74
- count += 1
75
61
  end
62
+
63
+ # get the response
64
+ response = WebmentionIO.webmentions.send_webmention(source, target)
65
+ next unless response
66
+
67
+ # capture JSON responses in case site wants to do anything with them
68
+ begin
69
+ response = JSON.parse response
70
+ rescue JSON::ParserError
71
+ response = ''
72
+ end
73
+
74
+ outgoing[source][target] = response
75
+ count += 1
76
76
  end
77
- WebmentionIO.dump_yaml(cached_outgoing, outgoing)
78
- WebmentionIO.log "msg", "#{count} webmentions sent."
79
- end # file exists (outgoing)
80
- end # def process
81
- end # WebmentionCommand
82
- end # Commands
83
- end # WebmentionIO
84
- end # Jekyll
77
+ end
78
+
79
+ outgoing.write
80
+ WebmentionIO.log 'msg', "#{count} webmentions sent."
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Jekyll
6
+ module WebmentionIO
7
+ class Config
8
+ module HtmlProofer
9
+ NONE = 'none'
10
+ ALL = 'all'
11
+ TEMPLATES = 'templates'
12
+
13
+ def self.get_const(val)
14
+ constants.find { |sym| const_get(sym) == val }
15
+ end
16
+ end
17
+
18
+ module UriPolicy
19
+ BAN = 'ban'
20
+ IGNORE = 'ignore'
21
+ RETRY = 'retry'
22
+ end
23
+
24
+ TIMEFRAMES = {
25
+ 'last_week' => 'weekly',
26
+ 'last_month' => 'monthly',
27
+ 'last_year' => 'yearly',
28
+ }.freeze
29
+
30
+ attr_accessor :html_proofer_ignore, :max_attempts,
31
+ :templates, :bad_uri_policy, :throttle_lookups, :cache_folder,
32
+ :legacy_domains, :pause_lookups, :site_url, :syndication, :js,
33
+ :username, :debug, :api_url
34
+
35
+ # The scheme://host[:port] origin and bare host of the configured API,
36
+ # derived from api_url. Used to build the service links emitted into the
37
+ # page head (and the JS) so they track a custom endpoint instead of being
38
+ # hard-coded to webmention.io.
39
+ attr_reader :api_origin, :api_host
40
+
41
+ # The default base URL for the Webmention.io API. Exposed as a config key
42
+ # so the endpoint can be pointed elsewhere (e.g. a local stand-in during
43
+ # integration testing) instead of being hard-coded in the network layer.
44
+ DEFAULT_API_URL = 'https://webmention.io/api'
45
+
46
+ # Resolves a webmention API URL to a URI, falling back to the default
47
+ # endpoint when the configured value has no host.
48
+ def self.api_uri(url)
49
+ uri = URI.parse(url)
50
+ uri.host ? uri : URI.parse(DEFAULT_API_URL)
51
+ end
52
+
53
+ # The host of a parsed API URI, plus the port when it isn't the scheme's
54
+ # default (so custom/local endpoints still match the full URL).
55
+ def self.authority(uri)
56
+ uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}"
57
+ end
58
+
59
+ def initialize(site = nil)
60
+ @site = site
61
+
62
+ if site.nil?
63
+ parse
64
+ else
65
+ parse(@site.config['webmentions'], @site.config['url'].to_s, @site.config['baseurl'].to_s)
66
+ end
67
+ end
68
+
69
+ def parse(config = nil, site_url = '', base_url = '')
70
+ config ||= {}
71
+
72
+ @site_url = site_url
73
+ @username = config['username']
74
+ @debug = config['debug']
75
+ @api_url = config['api_url'] || DEFAULT_API_URL
76
+ api_uri = self.class.api_uri(@api_url)
77
+ @api_host = api_uri.host
78
+ @api_origin = "#{api_uri.scheme}://#{self.class.authority(api_uri)}"
79
+
80
+ @pause_lookups =
81
+ if !@site.nil? && @site.config['serving']
82
+ WebmentionIO.log 'msg', 'Webmentions won’t be gathered when running `jekyll serve`.'
83
+
84
+ true
85
+ elsif !@site.nil? && @site_url.include?('localhost')
86
+ WebmentionIO.log 'msg', 'Webmentions won’t be gathered on localhost.'
87
+
88
+ true
89
+ else
90
+ config['pause_lookups']
91
+ end
92
+
93
+ @cache_folder = config['cache_folder'] || '.jekyll-cache'
94
+ @cache_folder = @site.in_source_dir(@cache_folder) if !@site.nil?
95
+
96
+ @pages = config['pages']
97
+ @collections = config['collections'] || {}
98
+ @templates = config['templates'] || {}
99
+
100
+ @js = JsConfig.new(base_url, config['js'] || false)
101
+
102
+ @html_proofer_ignore = HtmlProofer.get_const(
103
+ config['html_proofer_ignore'] ||
104
+ (config['html_proofer'] ? 'templates' : nil) ||
105
+ 'none'
106
+ )
107
+
108
+ @max_attempts = config['max_attempts']
109
+
110
+ @bad_uri_policy = BadUriPolicy.new(config)
111
+
112
+ @throttle_lookups = config['throttle_lookups'] || {}
113
+
114
+ @legacy_domains = config['legacy_domains'] || []
115
+
116
+ @syndication = (config['syndication'] || {}).transform_values { |entry| SyndicationRule.new(entry) }
117
+ end
118
+
119
+ # The next lookup date has to be before this date to be allowed to
120
+ # request webmentions again.
121
+ def last_lookup_threshold(date)
122
+ age = get_timeframe_from_date(date)
123
+
124
+ throttle = @throttle_lookups[age]
125
+
126
+ throttle.nil? ? nil : get_date_from_string(throttle)
127
+ end
128
+
129
+ # Given a webmention endpoint, find the corresponding syndication rule
130
+ # Yes, this is a kind of reverse lookup so we can figure out of a given
131
+ # queued webmention was a result of a syndication rule.
132
+ def syndication_rule_for_uri(uri)
133
+ @syndication.values.detect { |rule| rule.endpoint == uri }
134
+ end
135
+
136
+ # Based on the specified configuration, return the list of documents for
137
+ # the site that should be processed.
138
+ def documents
139
+ documents = @site.posts.docs.clone
140
+
141
+ if @pages == true
142
+ WebmentionIO.log 'info', 'Including site pages.'
143
+ documents.concat @site.pages.clone
144
+ end
145
+
146
+ if @collections.empty?
147
+ WebmentionIO.log 'info', 'Adding collections.'
148
+
149
+ @site.collections.each do |name, collection|
150
+ # skip _posts
151
+ next if name == 'posts'
152
+
153
+ if collections.include?(name)
154
+ documents.concat collection.docs.clone
155
+ end
156
+ end
157
+ end
158
+
159
+ documents
160
+ end
161
+
162
+ def collections
163
+ @site.collections
164
+ end
165
+
166
+ class BadUriPolicy
167
+ BadUriPolicyEntry = Struct.new(:policy, :max_attempts, :retry_delay)
168
+
169
+ attr_reader :whitelist, :blacklist
170
+
171
+ def initialize(site_config)
172
+ @bad_uri_policy = site_config['bad_uri_policy'] || {}
173
+
174
+ @bad_uri_policy['whitelist'] ||= []
175
+ @bad_uri_policy['blacklist'] ||= []
176
+
177
+ # We always want to collect webmentions from the configured API host,
178
+ # so we explicitly whitelist it. This way a transient service outage
179
+ # won't get the endpoint banned by the bad-URI policy. Derived from the
180
+ # configured api_url (default webmention.io) so a custom endpoint gets
181
+ # the same protection.
182
+ @bad_uri_policy['whitelist'].insert(-1, api_host_pattern(site_config))
183
+
184
+ @whitelist = @bad_uri_policy['whitelist'].map { |expr| Regexp.new(expr) }
185
+ @blacklist = @bad_uri_policy['blacklist'].map { |expr| Regexp.new(expr) }
186
+ end
187
+
188
+ def set_policy(state, policy, max_attempts = nil, retry_delay = nil)
189
+ @bad_uri_policy[state] = {
190
+ 'policy' => policy,
191
+ 'max_attempts' => max_attempts,
192
+ 'retry_delay' => retry_delay
193
+ }
194
+ end
195
+
196
+ # Given the provided state value (see WebmentionPolicy::State),
197
+ # retrieve the policy entry. If no entry exists, return a new default
198
+ # entry that indicates unlimited retries.
199
+ def for_state(state)
200
+ default_policy = { 'policy' => UriPolicy::RETRY }
201
+
202
+ # Retrieve the policy entry, the default entry, or the canned default
203
+ policy_entry = @bad_uri_policy[state] || @bad_uri_policy['default'] || default_policy
204
+
205
+ # Convert shorthand entry to full policy record
206
+ if policy_entry.instance_of? String
207
+ policy_entry = { 'policy' => policy_entry }
208
+ end
209
+
210
+ if policy_entry['policy'] == UriPolicy::RETRY && !policy_entry.key?('retry_delay')
211
+ # If this is a retry policy and no delay is set, set up the default
212
+ # delay policy. This inherits from the legacy cache_bad_uris_for
213
+ # setting to enable backward compatibility with older configurations.
214
+ #
215
+ # We do this here to make the rule enforcement logic a little tidier.
216
+
217
+ policy_entry['retry_delay'] = [(@bad_uri_policy['cache_bad_uris_for'] || 1) * 24]
218
+ end
219
+
220
+ # Now finally convert into a proper policy entry structure
221
+ BadUriPolicyEntry.new(
222
+ policy_entry['policy'],
223
+ policy_entry['max_attempts'],
224
+ policy_entry['retry_delay']
225
+ )
226
+ end
227
+
228
+ private
229
+
230
+ # Builds an anchored host pattern for the configured webmention API so it
231
+ # is always exempt from the bad-URI policy, mirroring the api_url read in
232
+ # Config#parse. A non-default port is included so custom/local endpoints
233
+ # still match the full URL the network layer checks.
234
+ def api_host_pattern(site_config)
235
+ uri = Config.api_uri(site_config['api_url'] || DEFAULT_API_URL)
236
+ "^https?://#{Regexp.escape(Config.authority(uri))}/"
237
+ end
238
+ end
239
+
240
+ class SyndicationRule
241
+ attr_reader :endpoint, :response_mapping, :shorturl, :fragment
242
+
243
+ def initialize(entry)
244
+ @endpoint = entry['endpoint']
245
+ @shorturl = entry['shorturl']
246
+ @fragment = entry['fragment']
247
+ @response_mapping = {}
248
+
249
+ return unless entry.key?('response_mapping')
250
+
251
+ entry['response_mapping'].each do |key, pattern|
252
+ @response_mapping[key] = JsonPath.new(pattern)
253
+ rescue StandardError => e
254
+ WebmentionIO.log 'error', "Ignoring invalid JsonPath expression #{pattern}: #{e}"
255
+ end
256
+ end
257
+ end
258
+
259
+ class JsConfig
260
+ attr_reader :destination, :resource_name, :resource_url
261
+
262
+ def initialize(base_url, js_config)
263
+ if js_config == false
264
+ @disabled = true
265
+ return
266
+ end
267
+
268
+ @disabled = false
269
+ @destination = js_config['destination'] || 'js'
270
+
271
+ # rubocop:disable Style/RedundantCondition
272
+ # Apparently this cop is broken...
273
+ @deploy = js_config['deploy'].nil? ? true : js_config['deploy']
274
+ @source = js_config['source'].nil? ? true : js_config['source']
275
+ @uglify = js_config['uglify'].nil? ? true : js_config['uglify']
276
+ # rubocop:enable Style/RedundantCondition
277
+
278
+ @resource_name = 'JekyllWebmentionIO.js'
279
+ @resource_url = File.join('', base_url, @destination, @resource_name)
280
+ end
281
+
282
+ def disabled?; @disabled; end
283
+
284
+ def source?; @source; end
285
+
286
+ def deploy?; @deploy; end
287
+
288
+ def uglify?; @uglify; end
289
+ end
290
+
291
+ private
292
+
293
+ def get_timeframe_from_date(time)
294
+ date = time.to_date
295
+
296
+ timeframe = nil
297
+
298
+ TIMEFRAMES.each do |key, value|
299
+ if date.to_date > get_date_from_string(value)
300
+ timeframe = key
301
+ break
302
+ end
303
+ end
304
+
305
+ timeframe ||= 'older'
306
+ end
307
+
308
+ def get_date_from_string(text)
309
+ today = Date.today
310
+ pattern = /every\s(?:(\d+)\s)?(day|week|month|year)s?/
311
+ matches = text.match(pattern)
312
+
313
+ unless matches
314
+ text = if text == 'daily'
315
+ 'every 1 day'
316
+ else
317
+ "every 1 #{text.sub('ly', '')}"
318
+ end
319
+ matches = text.match(pattern)
320
+ end
321
+
322
+ n = matches[1] ? matches[1].to_i : 1
323
+ unit = matches[2]
324
+
325
+ # weeks aren't natively supported in Ruby
326
+ if unit == 'week'
327
+ n *= 7
328
+ unit = 'day'
329
+ end
330
+
331
+ # dynamic method call
332
+ today.send "prev_#{unit}", n
333
+ end
334
+ end
335
+ end
336
+ end