about summary refs log tree commit diff
path: root/app/lib
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2020-03-08 19:38:53 +0100
committerThibaut Girka <thib@sitedethib.com>2020-03-08 19:38:53 +0100
commitc790ecb14d8b06c6242886ff4d2cdf06e70c5cac (patch)
treedff0bfefe5a1922c7227ea1ec0236b92e11db699 /app/lib
parent13ef4d5fb0dbb66074f42df7989ae40509a4724f (diff)
parent764b89939fe2fcb8c4389738af8685949104c144 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- `app/controllers/api/v1/statuses_controller.rb`:
  Conflict due to upstream adding a new parameter (with_rate_limit),
  too close to glitch-soc's own additional parameter (content_type).
  Added upstream's parameter.
- `app/services/post_status_service.rb`:
  Conflict due to upstream adding a new parameter (rate_limit),
  too close to glitch-soc's own additional parameter (content_type).
  Added upstream's parameter.
- `app/views/settings/preferences/appearance/show.html.haml`:
  Conflict due to us not exposing theme settings here (as we have
  a different flavour/skin menu).
  Took upstream change, while still not exposing theme settings.
- `config/webpack/shared.js`:
  Coding style fixes for a part we have rewritten.
  Discarded upstream changes.
Diffstat (limited to 'app/lib')
-rw-r--r--app/lib/exceptions.rb1
-rw-r--r--app/lib/rate_limiter.rb64
2 files changed, 65 insertions, 0 deletions
diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb
index 01346bfe5..3362576b0 100644
--- a/app/lib/exceptions.rb
+++ b/app/lib/exceptions.rb
@@ -8,6 +8,7 @@ module Mastodon
   class LengthValidationError < ValidationError; end
   class DimensionsValidationError < ValidationError; end
   class RaceConditionError < Error; end
+  class RateLimitExceededError < Error; end
 
   class UnexpectedResponseError < Error
     def initialize(response = nil)
diff --git a/app/lib/rate_limiter.rb b/app/lib/rate_limiter.rb
new file mode 100644
index 000000000..68dae9add
--- /dev/null
+++ b/app/lib/rate_limiter.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+class RateLimiter
+  include Redisable
+
+  FAMILIES = {
+    follows: {
+      limit: 400,
+      period: 24.hours.freeze,
+    }.freeze,
+
+    statuses: {
+      limit: 300,
+      period: 3.hours.freeze,
+    }.freeze,
+
+    media: {
+      limit: 30,
+      period: 30.minutes.freeze,
+    }.freeze,
+  }.freeze
+
+  def initialize(by, options = {})
+    @by     = by
+    @family = options[:family]
+    @limit  = FAMILIES[@family][:limit]
+    @period = FAMILIES[@family][:period].to_i
+  end
+
+  def record!
+    count = redis.get(key)
+
+    if count.nil?
+      redis.set(key, 0)
+      redis.expire(key, (@period - (last_epoch_time % @period) + 1).to_i)
+    end
+
+    raise Mastodon::RateLimitExceededError if count.present? && count.to_i >= @limit
+
+    redis.incr(key)
+  end
+
+  def rollback!
+    redis.decr(key)
+  end
+
+  def to_headers(now = Time.now.utc)
+    {
+      'X-RateLimit-Limit' => @limit.to_s,
+      'X-RateLimit-Remaining' => (@limit - (redis.get(key) || 0).to_i).to_s,
+      'X-RateLimit-Reset' => (now + (@period - now.to_i % @period)).iso8601(6),
+    }
+  end
+
+  private
+
+  def key
+    @key ||= "rate_limit:#{@by.id}:#{@family}:#{(last_epoch_time / @period).to_i}"
+  end
+
+  def last_epoch_time
+    @last_epoch_time ||= Time.now.to_i
+  end
+end