Making a Discord Webhook API Proxy (With ratelimit-compliant queue system) In Python!

Making a Discord Webhook API Proxy!

SKIP TO THE CODE


Information

sup, im spiral. i am an independent web developer and game programmer. today, we are going to breakdown the exact process of making a python-based discord webhook proxy

hopefully this post can help some beginner web developers get a grasp on the field!

you need some prior knowledge on terminal (linux) usage for this tutorial to make sense.

this tutorial uses a queue system, allowing you to send as many requests as you wish. this is especially good for large-scale games! it does come at a cost however: if your json is invalid it won’t return an error. so make sure your webhook json data is valid and functioning!


How is it made?

For this system, we are going to use the following:

THIS TUTORIAL WILL NOT TEACH YOU THE SPECIFICS OF HOSTING THIS SYSTEM. EXTERNAL RESOURCES WILL BE LINKED.


Installing necessary resources

Installation is easy. Once you download Python (3.10+), ensuring you install pip through the installer menu, you can open a new terminal and run the following:

pip install redis-py requests flask validators uwsgi uwsgidecorators
What if I wish to use a python environment! (Recommended) If you are seeking a python environment (good on you) then you can create one easily. Navigate to your directory of your project and run "python3 -m venv virtual-environment-name". Then activate it by running "source virtual-environment-name/bin/activate" for Linux or "virtual-environment-name\Scripts\activate" for Windows!

Cut to the chase already!

alr, alr. I am sure you are so very eager to actually make the script! I am bursting at the seams with excitement myself!

First off, you will have to start a redis server on a separate port. I wont explain this process, so here is a guide on how to do it!

Now, we are going to set up imported the needed modules that we installed earlier!

# //IMPORTS
from flask import Flask, request, jsonify
import redis
import requests
import validators
from urllib.parse import urlparse
from threading import Thread
import json
import re
from time import sleep
import uwsgidecorators # opt

Afterwards, we are going to define our variables!

# //Variables
app = Flask(__name__)
client = redis.Redis() # you must setup your own redis server! https://developer.redis.com/develop/python/#step-1-run-a-redis-server
RobloxIPs = ["103.140.28.0", "128.116.0.0", "128.116.2.0", "128.116.3.0", "128.116.4.0", "128.116.6.0", "128.116.8.0", "128.116.11.0", "128.116.13.0", "128.116.14.0", "128.116.15.0", "128.116.16.0", "128.116.17.0", "128.116.18.0", "128.116.19.0", "128.116.20.0", "128.116.23.0", "128.116.24.0", "128.116.25.0", "128.116.26.0", "128.116.27.0", "128.116.28.0", "128.116.29.0", "128.116.34.0", "128.116.35.0", "128.116.36.0", "128.116.37.0", "128.116.38.0", "128.116.39.0", "128.116.40.0", "128.116.41.0", "128.116.42.0", "128.116.43.0", "128.116.44.0", "128.116.45.0", "128.116.46.0", "128.116.49.0", "128.116.50.0", "128.116.51.0", "128.116.58.0", "128.116.59.0", "128.116.60.0", "128.116.62.0", "128.116.65.0", "128.116.67.0", "128.116.69.0", "128.116.70.0", "128.116.71.0", "128.116.72.0", "128.116.73.0", "128.116.74.0", "128.116.75.0", "128.116.76.0", "128.116.77.0", "128.116.78.0", "128.116.80.0", "128.116.81.0", "128.116.82.0", "128.116.83.0", "128.116.84.0", "128.116.85.0", "128.116.87.0", "128.116.88.0", "128.116.89.0", "128.116.95.0", "128.116.97.0", "128.116.99.0", "128.116.101.0", "128.116.102.0", "128.116.104.0", "128.116.105.0", "128.116.112.0", "128.116.114.0", "128.116.115.0", "128.116.116.0", "128.116.117.0", "128.116.118.0", "128.116.119.0", "128.116.120.0", "128.116.121.0", "128.116.122.0", "128.116.123.0", "128.116.124.0", "128.116.126.0", "128.116.127.0", "141.193.3.0", "205.201.62.0", "209.206.40.0", "209.206.40.0", "209.206.42.0", "209.206.43.0", "209.206.44.0"] # Pulled from https://bgp.he.net/AS22697#_prefixes

We will use these Roblox IPs to make sure your requests are only from a Roblox game and no external sources are using your proxy. This is really not necessary for a majority of you but I have added it anyways.

Now, We define our functions.

# //FUNCTIONS
def detect_special_character(pass_string):  #pulled from stackoverflow
  regex= re.compile('[@_!#$%^&*()<>?/\|}{~:]') 
  if(regex.search(pass_string) == None): 
    res = False
  else: 
    res = True
  return(res)

def processQueue():
  while True:
    task = client.blpop("discord_queue")
    
    data = json.loads(task[1])
    webhook_data = data["value"]
    webhook_url = data["webhook_url"]
    try:
      req = requests.post(webhook_url, json=webhook_data)
      if req.status_code == 200:
        return jsonify({"response": "Success."}), 200
    except requests.exceptions.RequestException as error:
      return jsonify({"response": error}), 500
    sleep(1/30) # complies with discord ratelimits

These functions will aid us in validating requests and processing the queue itself.

And now for the main course, the functionality!

# //APP FUNCTIONALITY
@app.route('/api', defaults={'path': ''})
@app.route('/<path:path>', methods=["POST", "GET"]) # this allows any request starting with IP:PORT/api/
def proxy(path):
    webhookUrl = "https://discord.com/" + path
    
    if not validators.url(webhookUrl) or detect_special_character(webhookUrl) and not urlparse(webhookUrl).hostname == "discord.com" and urlparse(webhookUrl).scheme: #Here, we validate the URL and make sure it is a valid discord webhook url.
      jsonify({"response": "Webhook URL Invalid."}), 400
    
    if not request.remote_addr in RobloxIPs: # This will allow only Roblox server IP's!
      return jsonify({"response": "Access denied. Make sure you are using this service on a ROBLOX server."}), 401
    
    if request.method == "POST": # Checks if the request type is POST
      # Below we are going to process the data and add it to our queue
      data2 = {}
      data = request.get_json()

      data2["webhook_url"] = webhookUrl
      data2["value"] = data

      if data2 and "webhook_url" in data2:
        client.rpush("discord_queue", json.dumps(data2)) # adds data to queue.

      return webhookUrl
    elif request.method == "GET":  # Checks if the request type is POST
      try:
        resp = requests.get(webhookUrl)
        return resp.json()  # returns webhook info
      except: 
        return jsonify({"response": "Error returning webhook info."}), 500

And the finishing touches: starting the queue looping thread and app itself!

# //STARTUP 
queue_thread = Thread(target=processQueue)
queue_thread.start()

app.run("0.0.0.0", 1050) #you can change your port here!

View the full script!


Hosting information!

We have a working prototype of our script above, but it won’t work in an actual deployment because we aren’t actually hosting it yet. To host your script, you will have to study and use a VPS. Personally I use Vultr as I found it is the best for good quality for low cost.

Steps For hosting, [I suggest this guide](https://www.vultr.com/docs/deploy-a-flask-website-on-nginx-with-uwsgi/) for an entry-level look at nginx and uwsgi.

When hosting using uwsgi, you can’t use the Threading module. To combat this, we can use the uwsgidecorators module to achieve a similar effect. Replace the code for your queue thread with the following:

@uwsgidecorators.postfork
@uwsgidecorators.thread
def processQueue():
  while True:
    task = client.blpop("discord_queue")
    
    data = json.loads(task[1])
    webhook_data = data["value"]
    webhook_url = data["webhook_url"]
    try:
      req = requests.post(webhook_url, json=webhook_data)
      if req.status_code == 200:
        return jsonify({"response": "Success."}), 200
    except requests.exceptions.RequestException as error:
      return jsonify({"response": error}), 500
    sleep(1/30)

Usage example!

So, we made our proxy, how do we use it? It’s very simple. Once we have our server up and running, you can access your proxy at a url similar to http://0.0.0.0:8080/api/webhooks/----------/----------------------------------
To send data from your roblox game, your script might look similar to the below:

local HttpService = game:GetService("HttpService")
local data =
	{
		["contents"] = "",
		["embeds"] = {{
			["title"]= "Testing",
			["description"] = "This is an example of what you would be sending to your webhook.",
			["type"]= "rich",
			["color"]= tonumber(0x6AA84F),
		}}
	}
HttpService:PostAsync("http://0.0.0.0:8080/api/webhooks/----------/----------------------------------", HttpService:JSONEncode(data))

thats all! Thank you so much for reading, I hope this at the very least taught you something about web development!