Making a Discord Webhook API Proxy!
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:
- Python (3.10+)
-
Python Modules: *Optional
- redis-py
- Requests
- Flask
- Validators
- Numerous built-in modules.
- uWSGI*
- uwsgidecorators*
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!