Add Imgur rate limiting

This commit is contained in:
JonnyWong16 2018-03-25 13:47:49 -07:00
parent dec5931fd4
commit 80df2b0fad
4 changed files with 105 additions and 6 deletions

View file

@ -977,7 +977,7 @@
<p class="help-block"> <p class="help-block">
Enter your Imgur API client ID in order to upload posters. Enter your Imgur API client ID in order to upload posters.
You can register a new application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>. You can register a new application <a href="${anon_url('https://api.imgur.com/oauth2/addclient')}" target="_blank">here</a>.
</p>> </p>
</div> </div>
</div> </div>
<div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}"> <div id="self_host_image_options" style="overlfow: hidden; display: ${'none' if config['notify_upload_posters'] != 2 else 'block'}">
@ -2543,7 +2543,7 @@ $(document).ready(function() {
}); });
function newsletterUploadEnabled() { function newsletterUploadEnabled() {
if ($('#notify_upload_posters').is(':checked') || $('#newsletter_self_hosted').is(':checked')) { if ($('#notify_upload_posters').val() === '1' || $('#newsletter_self_hosted').is(':checked')) {
$('#newsletter_upload_warning').hide(); $('#newsletter_upload_warning').hide();
} else { } else {
$('#newsletter_upload_warning').show(); $('#newsletter_upload_warning').show();

59
lib/ratelimit/__init__.py Normal file
View file

@ -0,0 +1,59 @@
from math import floor
import time
import sys
import threading
import functools
def clamp(value):
'''
Clamp integer between 1 and max
There must be at least 1 method invocation
made over the time period. Make sure the
value passed is at least 1 and is not a
fraction of an invocation.
:param float value: The number of method invocations.
:return: Clamped number of invocations.
:rtype: int
'''
return max(1, min(sys.maxsize, floor(value)))
class RateLimitDecorator:
def __init__(self, period=1, every=1.0):
self.frequency = abs(every) / float(clamp(period))
self.last_called = 0.0
self.lock = threading.RLock()
def __call__(self, func):
'''
Extend the behaviour of the following
function, forwarding method invocations
if the time window hes elapsed.
:param function func: The function to decorate.
:return: Decorated function.
:rtype: function
'''
@functools.wraps(func)
def wrapper(*args, **kwargs):
'''Decorator wrapper function'''
with self.lock:
elapsed = time.time() - self.last_called
left_to_wait = self.frequency - elapsed
if left_to_wait > 0:
time.sleep(left_to_wait)
self.last_called = time.time()
return func(*args, **kwargs)
return wrapper
rate_limited = RateLimitDecorator
__all__ = [
'rate_limited'
]

9
lib/ratelimit/version.py Normal file
View file

@ -0,0 +1,9 @@
class Version(object):
'''Version of the package'''
def __setattr__(self, *args):
raise TypeError('cannot modify immutable instance')
__delattr__ = __setattr__
def __init__(self, num):
super(Version, self).__setattr__('number', num)

View file

@ -28,6 +28,7 @@ import math
import maxminddb import maxminddb
from operator import itemgetter from operator import itemgetter
import os import os
from ratelimit import rate_limited
import re import re
import socket import socket
import sys import sys
@ -76,6 +77,7 @@ def addtoapi(*dargs, **dkwargs):
return rd return rd
def multikeysort(items, columns): def multikeysort(items, columns):
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns] comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
@ -161,6 +163,7 @@ def convert_milliseconds(ms):
return minutes return minutes
def convert_milliseconds_to_minutes(ms): def convert_milliseconds_to_minutes(ms):
if str(ms).isdigit(): if str(ms).isdigit():
@ -171,6 +174,7 @@ def convert_milliseconds_to_minutes(ms):
return 0 return 0
def convert_seconds(s): def convert_seconds(s):
gmtime = time.gmtime(s) gmtime = time.gmtime(s)
@ -181,6 +185,7 @@ def convert_seconds(s):
return minutes return minutes
def convert_seconds_to_minutes(s): def convert_seconds_to_minutes(s):
if str(s).isdigit(): if str(s).isdigit():
@ -201,6 +206,7 @@ def now():
now = datetime.datetime.now() now = datetime.datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S") return now.strftime("%Y-%m-%d %H:%M:%S")
def human_duration(s, sig='dhms'): def human_duration(s, sig='dhms'):
hd = '' hd = ''
@ -233,6 +239,7 @@ def human_duration(s, sig='dhms'):
return hd return hd
def get_age(date): def get_age(date):
try: try:
@ -385,6 +392,7 @@ def split_string(mystring, splitvar=','):
mylist.append(each_word.strip()) mylist.append(each_word.strip())
return mylist return mylist
def create_https_certificates(ssl_cert, ssl_key): def create_https_certificates(ssl_cert, ssl_key):
""" """
Create a self-signed HTTPS certificate and store in it in Create a self-signed HTTPS certificate and store in it in
@ -424,12 +432,14 @@ def cast_to_int(s):
except (ValueError, TypeError): except (ValueError, TypeError):
return 0 return 0
def cast_to_float(s): def cast_to_float(s):
try: try:
return float(s) return float(s)
except (ValueError, TypeError): except (ValueError, TypeError):
return 0 return 0
def convert_xml_to_json(xml): def convert_xml_to_json(xml):
o = xmltodict.parse(xml) o = xmltodict.parse(xml)
return json.dumps(o) return json.dumps(o)
@ -452,12 +462,14 @@ def get_percent(value1, value2):
return math.trunc(percent) return math.trunc(percent)
def hex_to_int(hex): def hex_to_int(hex):
try: try:
return int(hex, 16) return int(hex, 16)
except (ValueError, TypeError): except (ValueError, TypeError):
return 0 return 0
def parse_xml(unparsed=None): def parse_xml(unparsed=None):
if unparsed: if unparsed:
try: try:
@ -473,10 +485,11 @@ def parse_xml(unparsed=None):
logger.warn("XML parse request made but no data received.") logger.warn("XML parse request made but no data received.")
return [] return []
"""
Validate xml keys to make sure they exist and return their attribute value, return blank value is none found
"""
def get_xml_attr(xml_key, attribute, return_bool=False, default_return=''): def get_xml_attr(xml_key, attribute, return_bool=False, default_return=''):
"""
Validate xml keys to make sure they exist and return their attribute value, return blank value is none found
"""
if xml_key.getAttribute(attribute): if xml_key.getAttribute(attribute):
if return_bool: if return_bool:
return True return True
@ -488,6 +501,7 @@ def get_xml_attr(xml_key, attribute, return_bool=False, default_return=''):
else: else:
return default_return return default_return
def process_json_kwargs(json_kwargs): def process_json_kwargs(json_kwargs):
params = {} params = {}
if json_kwargs: if json_kwargs:
@ -495,18 +509,21 @@ def process_json_kwargs(json_kwargs):
return params return params
def sanitize(string): def sanitize(string):
if string: if string:
return unicode(string).replace('<','&lt;').replace('>','&gt;') return unicode(string).replace('<','&lt;').replace('>','&gt;')
else: else:
return '' return ''
def is_public_ip(host): def is_public_ip(host):
ip = is_valid_ip(get_ip(host)) ip = is_valid_ip(get_ip(host))
if ip and ip.iptype() == 'PUBLIC': if ip and ip.iptype() == 'PUBLIC':
return True return True
return False return False
def get_ip(host): def get_ip(host):
ip_address = '' ip_address = ''
if is_valid_ip(host): if is_valid_ip(host):
@ -519,6 +536,7 @@ def get_ip(host):
logger.error(u"IP Checker :: Bad IP or hostname provided.") logger.error(u"IP Checker :: Bad IP or hostname provided.")
return ip_address return ip_address
def is_valid_ip(address): def is_valid_ip(address):
try: try:
return IP(address) return IP(address)
@ -527,6 +545,7 @@ def is_valid_ip(address):
except ValueError: except ValueError:
return False return False
def install_geoip_db(): def install_geoip_db():
maxmind_url = 'http://geolite.maxmind.com/download/geoip/database/' maxmind_url = 'http://geolite.maxmind.com/download/geoip/database/'
geolite2_gz = 'GeoLite2-City.mmdb.gz' geolite2_gz = 'GeoLite2-City.mmdb.gz'
@ -587,6 +606,7 @@ def install_geoip_db():
return True return True
def uninstall_geoip_db(): def uninstall_geoip_db():
logger.debug(u"Tautulli Helpers :: Uninstalling the GeoLite2 database...") logger.debug(u"Tautulli Helpers :: Uninstalling the GeoLite2 database...")
try: try:
@ -600,6 +620,7 @@ def uninstall_geoip_db():
logger.debug(u"Tautulli Helpers :: GeoLite2 database uninstalled successfully.") logger.debug(u"Tautulli Helpers :: GeoLite2 database uninstalled successfully.")
return True return True
def geoip_lookup(ip_address): def geoip_lookup(ip_address):
if not plexpy.CONFIG.GEOIP_DB: if not plexpy.CONFIG.GEOIP_DB:
return 'GeoLite2 database not installed. Please install from the ' \ return 'GeoLite2 database not installed. Please install from the ' \
@ -638,6 +659,7 @@ def geoip_lookup(ip_address):
return geo_info return geo_info
def whois_lookup(ip_address): def whois_lookup(ip_address):
nets = [] nets = []
@ -674,6 +696,7 @@ def whois_lookup(ip_address):
return whois_info return whois_info
# Taken from SickRage # Taken from SickRage
def anon_url(*url): def anon_url(*url):
""" """
@ -682,6 +705,7 @@ def anon_url(*url):
return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url)) return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url))
@rate_limited(450, 3600)
def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''): def upload_to_imgur(img_data, img_title='', rating_key='', fallback=''):
""" Uploads an image to Imgur """ """ Uploads an image to Imgur """
client_id = plexpy.CONFIG.IMGUR_CLIENT_ID client_id = plexpy.CONFIG.IMGUR_CLIENT_ID
@ -769,6 +793,7 @@ def cache_image(url, image=None):
return imagefile, imagetype return imagefile, imagetype
def build_datatables_json(kwargs, dt_columns, default_sort_col=None): def build_datatables_json(kwargs, dt_columns, default_sort_col=None):
""" Builds datatables json data """ Builds datatables json data
@ -793,6 +818,7 @@ def build_datatables_json(kwargs, dt_columns, default_sort_col=None):
} }
return json.dumps(json_data) return json.dumps(json_data)
def humanFileSize(bytes, si=False): def humanFileSize(bytes, si=False):
if str(bytes).isdigit(): if str(bytes).isdigit():
bytes = int(bytes) bytes = int(bytes)
@ -816,6 +842,7 @@ def humanFileSize(bytes, si=False):
return "{0:.1f} {1}".format(bytes, units[u]) return "{0:.1f} {1}".format(bytes, units[u])
def parse_condition_logic_string(s, num_cond=0): def parse_condition_logic_string(s, num_cond=0):
""" Parse a logic string into a nested list """ Parse a logic string into a nested list
Based on http://stackoverflow.com/a/23185606 Based on http://stackoverflow.com/a/23185606
@ -900,6 +927,7 @@ def parse_condition_logic_string(s, num_cond=0):
return stack.pop() return stack.pop()
def nested_list_to_string(l): def nested_list_to_string(l):
for i, x in enumerate(l): for i, x in enumerate(l):
if isinstance(x, list): if isinstance(x, list):
@ -907,6 +935,7 @@ def nested_list_to_string(l):
s = '(' + ' '.join(l) + ')' s = '(' + ' '.join(l) + ')'
return s return s
def eval_logic_groups_to_bool(logic_groups, eval_conds): def eval_logic_groups_to_bool(logic_groups, eval_conds):
first_cond = logic_groups[0] first_cond = logic_groups[0]
@ -928,6 +957,7 @@ def eval_logic_groups_to_bool(logic_groups, eval_conds):
return result return result
def get_plexpy_url(hostname=None): def get_plexpy_url(hostname=None):
if plexpy.CONFIG.ENABLE_HTTPS: if plexpy.CONFIG.ENABLE_HTTPS:
scheme = 'https' scheme = 'https'
@ -961,6 +991,7 @@ def get_plexpy_url(hostname=None):
return scheme + '://' + hostname + port + root return scheme + '://' + hostname + port + root
def momentjs_to_arrow(format, duration=False): def momentjs_to_arrow(format, duration=False):
invalid_formats = ['Mo', 'DDDo', 'do'] invalid_formats = ['Mo', 'DDDo', 'do']
if duration: if duration: