<commit_before>/*
 *     Copyright 2015-2016 <NAME> & <NAME>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package net.dv8tion.jda.core.requests.ratelimit;

import com.mashape.unirest.http.Headers;
import com.mashape.unirest.http.HttpResponse;
import net.dv8tion.jda.core.requests.RateLimiter;
import net.dv8tion.jda.core.requests.Request;
import net.dv8tion.jda.core.requests.Requester;
import net.dv8tion.jda.core.requests.Route.CompiledRoute;
import net.dv8tion.jda.core.utils.SimpleLog;
import org.json.JSONObject;

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;

public class BotRateLimiter extends RateLimiter
{
    volatile Long timeOffset = null;
    volatile Long globalCooldown = null;

    public BotRateLimiter(Requester requester, int poolSize)
    {
        super(requester, poolSize);
    }

    @Override
    public Long getRateLimit(CompiledRoute route)
    {
        Bucket bucket = getBucket(route.getRatelimitRoute());
        synchronized (bucket)
        {
            if (globalCooldown != null) //Are we on global cooldown?
            {
                long now = getNow();
                if (now > globalCooldown)   //Verify that we should still be on cooldown.
                {
                    globalCooldown = null;  //If we are done cooling down, reset the globalCooldown and continue.
                } else
                {
                    return globalCooldown - now;    //If we should still be on cooldown, return when we can go again.
                }
            }
            if (bucket.routeUsageRemaining <= 0)
            {
                if (getNow() > bucket.resetTime)
                {
                    bucket.routeUsageRemaining = bucket.routeUsageLimit;
                    bucket.resetTime = 0;
                }
            }
            if (bucket.routeUsageRemaining > 0)
                return null;
            else
                return bucket.resetTime - getNow();
        }
    }

    @Override
    protected void queueRequest(Request request)
    {
        if (isShutdown)
            throw new RejectedExecutionException("Cannot queue a request after shutdown");
        Bucket bucket = getBucket(request.getRoute().getRatelimitRoute());
        synchronized (bucket)
        {
            bucket.addToQueue(request);
        }
    }

    @Override
    protected Long handleResponse(CompiledRoute route, HttpResponse<String> response)
    {
        Bucket bucket = getBucket(route.getRatelimitRoute());
        synchronized (bucket)
        {
            Headers headers = response.getHeaders();
            int code = response.getStatus();
            if (timeOffset == null)
                setTimeOffset(headers);

            if (code == 429)
            {
                String global = headers.getFirst("X-RateLimit-Global");
                String retry = headers.getFirst("Retry-After");
                if (retry == null || retry.isEmpty())
                {
                    JSONObject limitObj = new JSONObject(response.getBody());
                    retry = limitObj.get("retry_after").toString();
                }
                long retryAfter = Long.parseLong(retry);
                if (!Boolean.parseBoolean(global))  //Not global ratelimit
                {
                    updateBucket(bucket, headers);
                }
                else
                {
                    //If it is global, lock down the threads.
                    globalCooldown = getNow() + retryAfter;
                }
                return retryAfter;
            }
            else
            {
                updateBucket(bucket, headers);
                return null;
            }
        }

    }

    private Bucket getBucket(String route)
    {
        Bucket bucket = (Bucket) buckets.get(route);
        if (bucket == null)
        {
            synchronized (buckets)
            {
                bucket = (Bucket) buckets.get(route);
                if (bucket == null)
                {
                    bucket = new Bucket(route);
                    buckets.put(route, bucket);
                }
            }
        }
        return bucket;
    }

    public long getNow()
    {
        return System.currentTimeMillis() + getTimeOffset();
    }

    public long getTimeOffset()
    {
        return timeOffset == null ? 0 : timeOffset;
    }

    private void setTimeOffset(Headers headers)
    {
        //Store as soon as possible to get the most accurate time difference;
        long time = System.currentTimeMillis();
        if (timeOffset == null)
        {
            //Get the date header provided by Discord.
            //Format:  "date" : "Fri, 16 Sep 2016 05:49:36 GMT"
            String date = headers.getFirst("date");
            if (date != null)
            {
                OffsetDateTime tDate = OffsetDateTime.parse(date, DateTimeFormatter.RFC_1123_DATE_TIME);
                long lDate = tDate.toEpochSecond() * 1000;             //We want to work in milliseconds, not seconds
                timeOffset = Math.floorDiv(lDate - time, 1000) * 1000; //Get offset, convert to seconds, round it down, convert to milliseconds.
            }
        }
    }

    private void updateBucket(Bucket bucket, Headers headers)<commit_msg>fixed Date header perform less math for more accurate values<commit_after>/*
 *     Copyright 2015-2016 <NAME> & <NAME>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package net.dv8tion.jda.core.requests.ratelimit;

import com.mashape.unirest.http.Headers;
import com.mashape.unirest.http.HttpResponse;
import net.dv8tion.jda.core.requests.RateLimiter;
import net.dv8tion.jda.core.requests.Request;
import net.dv8tion.jda.core.requests.Requester;
import net.dv8tion.jda.core.requests.Route.CompiledRoute;
import net.dv8tion.jda.core.utils.SimpleLog;
import org.json.JSONObject;

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;

public class BotRateLimiter extends RateLimiter
{
    volatile Long timeOffset = null;
    volatile Long globalCooldown = null;

    public BotRateLimiter(Requester requester, int poolSize)
    {
        super(requester, poolSize);
    }

    @Override
    public Long getRateLimit(CompiledRoute route)
    {
        Bucket bucket = getBucket(route.getRatelimitRoute());
        synchronized (bucket)
        {
            if (globalCooldown != null) //Are we on global cooldown?
            {
                long now = getNow();
                if (now > globalCooldown)   //Verify that we should still be on cooldown.
                {
                    globalCooldown = null;  //If we are done cooling down, reset the globalCooldown and continue.
                } else
                {
                    return globalCooldown - now;    //If we should still be on cooldown, return when we can go again.
                }
            }
            if (bucket.routeUsageRemaining <= 0)
            {
                if (getNow() > bucket.resetTime)
                {
                    bucket.routeUsageRemaining = bucket.routeUsageLimit;
                    bucket.resetTime = 0;
                }
            }
            if (bucket.routeUsageRemaining > 0)
                return null;
            else
                return bucket.resetTime - getNow();
        }
    }

    @Override
    protected void queueRequest(Request request)
    {
        if (isShutdown)
            throw new RejectedExecutionException("Cannot queue a request after shutdown");
        Bucket bucket = getBucket(request.getRoute().getRatelimitRoute());
        synchronized (bucket)
        {
            bucket.addToQueue(request);
        }
    }

    @Override
    protected Long handleResponse(CompiledRoute route, HttpResponse<String> response)
    {
        Bucket bucket = getBucket(route.getRatelimitRoute());
        synchronized (bucket)
        {
            Headers headers = response.getHeaders();
            int code = response.getStatus();
            if (timeOffset == null)
                setTimeOffset(headers);

            if (code == 429)
            {
                String global = headers.getFirst("X-RateLimit-Global");
                String retry = headers.getFirst("Retry-After");
                if (retry == null || retry.isEmpty())
                {
                    JSONObject limitObj = new JSONObject(response.getBody());
                    retry = limitObj.get("retry_after").toString();
                }
                long retryAfter = Long.parseLong(retry);
                if (!Boolean.parseBoolean(global))  //Not global ratelimit
                {
                    updateBucket(bucket, headers);
                }
                else
                {
                    //If it is global, lock down the threads.
                    globalCooldown = getNow() + retryAfter;
                }
                return retryAfter;
            }
            else
            {
                updateBucket(bucket, headers);
                return null;
            }
        }

    }

    private Bucket getBucket(String route)
    {
        Bucket bucket = (Bucket) buckets.get(route);
        if (bucket == null)
        {
            synchronized (buckets)
            {
                bucket = (Bucket) buckets.get(route);
                if (bucket == null)
                {
                    bucket = new Bucket(route);
                    buckets.put(route, bucket);
                }
            }
        }
        return bucket;
    }

    public long getNow()
    {
        return System.currentTimeMillis() + getTimeOffset();
    }

    public long getTimeOffset()
    {
        return timeOffset == null ? 0 : timeOffset;
    }

    private void setTimeOffset(Headers headers)
    {
        //Store as soon as possible to get the most accurate time difference;
        long time = System.currentTimeMillis();
        if (timeOffset == null)
        {
            //Get the date header provided by Discord.
            //Format:  "date" : "Fri, 16 Sep 2016 05:49:36 GMT"
            String date = headers.getFirst("Date");
            if (date != null)
            {
                OffsetDateTime tDate = OffsetDateTime.parse(date, DateTimeFormatter.RFC_1123_DATE_TIME);
                long lDate = tDate.toInstant().toEpochMilli(); //We want to work in milliseconds, not seconds
                timeOffset = lDate - time; //Get offset in milliseconds.
            }
        }
    }

    private void updateBucket(Bucket bucket, Headers headers)
{'fraction_non_alphanumeric': 0.0861620987343894, 'fraction_numerical': 0.007878635487385802, 'mean_word_length': 1.762037037037037, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2771559', 'n_tokens_mistral': 3197, 'n_tokens_neox': 3011, 'n_words': 850}