about summary refs log tree commit diff
path: root/app/lib
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2020-03-08 15:17:39 +0100
committerGitHub <noreply@github.com>2020-03-08 15:17:39 +0100
commit339ce1c4e90605b736745b1f04493a247b2627ec (patch)
treebf6f6c697648416c0578fbc0e11132403a85b27c /app/lib
parent503eab1c1f101e92f163ed4f8457cac9a6193ffc (diff)
Add specific rate limits for posting and following (#13172)
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