├── README.md ├── client ├── example_1.jpg ├── example_1.json └── python.py ├── server ├── _config.py ├── database.db ├── database.py ├── pages │ ├── 404.html │ └── embed.html ├── utils.py └── web.py └── technicals.html /README.md: -------------------------------------------------------------------------------- 1 | ## Discord Embed API 2 | API that allows users to create urls that have a custom embed displayed on social media's (like Discord). 3 | 4 | Feel free to host, use, steal, change, or take inspiration from this code. 5 | Technical details about how to display embeds with html is in `technicals.html`. 6 | 7 | ### Example 8 | The payload you'd send to the API for this embed is in the client folder. 9 | 10 | ![Example 1](https://raw.githubusercontent.com/itschasa/e.chasa.wtf/main/client/example_1.jpg) 11 | 12 | ### Deployment 13 | This server is not the most efficient or cleanest setup, but it will work out of the box. 14 | 15 | You will need to host the API on a server, with a domain pointing to it. 16 | 17 | The default port is 8080, but this can be changed at the bottom of `web.py`. 18 | 19 | In `_config.py`, change `url` and `url2` with the appropriate domain you are hosting on. 20 | You can also change the rate limits if you want to. 21 | 22 | By default, the server will run as a development server. You can use waitress, or another deployment server to host efficiently. -------------------------------------------------------------------------------- /client/example_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itschasa/Discord-Embed-API/9c4e8901ebe30da4aa7608b886b57666f67b75a3/client/example_1.jpg -------------------------------------------------------------------------------- /client/example_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Want to advertise YOUR SHOP?", 3 | "description": "💜 - Mass DM on Discord to MILLIONS of users!\n💸 - Tokens starting from $0.02!\n🤩 - Get MORE SALES + MORE PROFIT on your shop!\n\nGet Started Today!", 4 | "redirect": "https://discord.gg/invite", 5 | "color": "#7289da", 6 | "provider": { 7 | "name": "Click here to view our sellix!", 8 | "url": "https://shop.sellix.io/" 9 | }, 10 | "image": { 11 | "thumbnail": false, 12 | "url": "https://i.imgur.com/7v2sRvV.jpg" 13 | } 14 | } -------------------------------------------------------------------------------- /client/python.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | embed_data = { 4 | "title": "Want to advertise YOUR SHOP?", 5 | "description": "💜 - Mass DM on Discord to MILLIONS of users!\n💸 - Tokens starting from $0.02!\n🤩 - Get MORE SALES + MORE PROFIT on your shop!\n\nGet Started Today!", 6 | "redirect": "https://discord.gg/invite", 7 | "color": "#7289da", 8 | "provider": { 9 | "name": "Click here to view our sellix!", 10 | "url": "https://shop.sellix.io/" 11 | }, 12 | "image": { 13 | "thumbnail": False, 14 | "url": "https://i.imgur.com/7v2sRvV.jpg" 15 | } 16 | } 17 | 18 | response = requests.post(f"http://localhost:8080/api/v1/embed", json=embed_data) 19 | 20 | if response.status_code == 200: 21 | print("Embed already existed") 22 | print(response.json()['link']) 23 | 24 | elif response.status_code == 201: 25 | print("Created Embed") 26 | print(response.json()['link']) 27 | 28 | elif response.status_code == 403: 29 | print("You've been blacklisted") 30 | print(response.json()['error']) 31 | print(response.json()['message']) 32 | print(response.json()['reason']) 33 | print("Doesn't Expire" if response.json()['expires'] is False else f"Does Expire: {response.json()['expires']}") 34 | 35 | else: 36 | print(f"Error Code: {response.status_code}") 37 | print(response.json()['error']) 38 | print(response.json()['message']) 39 | -------------------------------------------------------------------------------- /server/_config.py: -------------------------------------------------------------------------------- 1 | # x embed creation allowed every y seconds 2 | post_rate_limit_per_min = 5 3 | post_rate_limit_per_hour = 20 4 | 5 | # x embed GETs allowed every y seconds 6 | get_rate_limit_per_min = 10 7 | get_rate_limit_per_hour = 250 8 | 9 | db_name = "database.db" 10 | 11 | id_length = 7 12 | 13 | url = "https://e.chasa.wtf/e/" # embed url here 14 | url2 = "https://e.chasa.wtf/o/" # o data url here -------------------------------------------------------------------------------- /server/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itschasa/Discord-Embed-API/9c4e8901ebe30da4aa7608b886b57666f67b75a3/server/database.db -------------------------------------------------------------------------------- /server/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | class DataBase(): 4 | "Used to access a SQLite3 database, using the filename provided." 5 | 6 | def __init__(self, name): 7 | self.conn = sqlite3.connect(name) 8 | self.cursor = self.conn.cursor() 9 | 10 | def insert(self, table : str, values : list) -> None: 11 | """Insert data into a table. 12 | 13 | ---------- 14 | table: :class:`str` 15 | Name of table to insert in. 16 | values: :class:`list` 17 | List of values to be added to the table, in order of columns. 18 | 19 | ---------- 20 | `DataBase.insert("tablename", ["value1", "value2"])` 21 | """ 22 | 23 | data = "" 24 | for _ in range(len(values)): data += "?," 25 | data = data[:-1] 26 | 27 | newvalues = [] 28 | for item in values: 29 | newvalues.append(str(item)) 30 | 31 | self.cursor.execute(f"INSERT INTO {table} VALUES ({data})", newvalues) 32 | self.conn.commit() 33 | 34 | def query(self, table : str, columns : list, where : dict={}, fetchOne : bool=True, orOrAnd="AND"): 35 | """Querys the database. Returns `tuple`, `list`, `[]` or `None`. 36 | 37 | ---------- 38 | table: :class:`str` 39 | Name of table to query in. 40 | columns: :class:`list` 41 | List of column names to gather data from. 42 | where: :class:`dict = {}` 43 | List of columm names used to filter data. 44 | fetchOne: :class:`bool = True` 45 | Fetch the first value which matched the arguements. 46 | Or to fetch all data from the table that matches. 47 | orOrAnd: :class:`str` 48 | Whether to use the "AND" or "OR" statement when using "WHERE". 49 | 50 | ---------- 51 | `DataBase.query("tablename", ["column1", "column2"], {"column1": "value1"}, False)` 52 | 53 | "column1"'s value has to equal "value1"'s value to be valid and to not be filtered out. 54 | """ 55 | 56 | if len(columns) == 0: 57 | raise TypeError("columns can't be empty (len(columns) != 0)") 58 | 59 | columnsdata = "" 60 | for column in columns: 61 | columnsdata += f"{column}, " 62 | columnsdata = columnsdata[:-2] 63 | 64 | values = [] 65 | if len(where) != 0: 66 | wheredata = " WHERE " 67 | for key, value in where.items(): 68 | wheredata += f"{key} = ? {orOrAnd} " 69 | values.append(str(value)) 70 | if orOrAnd == "AND": wheredata = wheredata[:-5] 71 | else: wheredata = wheredata[:-4] 72 | else: wheredata = "" 73 | 74 | cur = self.conn.execute(f"SELECT rowid, {columnsdata} FROM {table}{wheredata}", values) 75 | if fetchOne == True: 76 | res = cur.fetchone() 77 | else: 78 | res = cur.fetchall() 79 | cur.close() 80 | 81 | return res 82 | 83 | def edit(self, table : str, newvalues : dict, where : dict, orOrAnd="AND") -> None: 84 | """Edits an entry in the database. 85 | 86 | ---------- 87 | table: :class:`str` 88 | Name of table to edit in. 89 | newvalues: :class:`dict` 90 | Dict of the new data to be edited. 91 | where: :class:`dict` 92 | Dict of the columns and values to be used. 93 | orOrAnd: :class:`str` 94 | Whether to use the "AND" or "OR" statement when using "WHERE". 95 | 96 | ---------- 97 | `DataBase.edit("tablename", {"column1": "newvalue1"}, {"column2": "value2"})` 98 | 99 | "column2"'s value has to equal "value2"'s value to be valid and to not be filtered out. 100 | """ 101 | 102 | values = [] 103 | 104 | setData = "" 105 | for key, value in newvalues.items(): 106 | setData += f"{key} = ?, " 107 | values.append(str(value)) 108 | setData = setData[:-2] 109 | 110 | wheredata = "" 111 | for key, value in where.items(): 112 | wheredata += f"{key} = ? {orOrAnd} " 113 | values.append(str(value)) 114 | if orOrAnd == "AND": wheredata = wheredata[:-5] 115 | else: wheredata = wheredata[:-4] 116 | 117 | self.conn.execute(f"UPDATE {table} SET {setData} WHERE {wheredata}", values) 118 | self.conn.commit() 119 | 120 | def delete(self, table : str, where : dict, orOrAnd="AND", whereOverRide=None, valuesOverRide=None) -> None: 121 | """Deletes an entry in the database. 122 | 123 | ---------- 124 | table: :class:`str` 125 | Name of table to edit in. 126 | where: :class:`dict` 127 | Dict of the columns and values to be used. 128 | 129 | 130 | ---------- 131 | `DataBase.delete("tablename", {"column2": "value2"})` 132 | 133 | "column2"'s value has to equal "value2"'s value to be valid and to not be filtered out. 134 | """ 135 | 136 | values = [] 137 | 138 | if whereOverRide == None: 139 | wheredata = "" 140 | for key, value in where.items(): 141 | wheredata += f"{key} = ? {orOrAnd} " 142 | values.append(str(value)) 143 | if orOrAnd == "AND": wheredata = wheredata[:-5] 144 | else: wheredata = wheredata[:-4] 145 | 146 | self.cursor.execute(f"DELETE FROM {table} WHERE {wheredata}", values) 147 | self.conn.commit() 148 | else: 149 | self.cursor.execute(f"DELETE FROM {table} WHERE {whereOverRide}", valuesOverRide) 150 | self.conn.commit() 151 | 152 | def close(self) -> None: 153 | """Closes the database connection. 154 | 155 | ---------- 156 | `DataBase.close()` 157 | """ 158 | 159 | self.cursor.close() 160 | self.conn.close() -------------------------------------------------------------------------------- /server/pages/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Who took my chips? | e.chasa.wtf 6 | 7 | 34 | 35 |
36 |
37 | 38 |

Embed Not Found

39 |

40 | This Embed either doesn't exist, or was deleted. Sorry :/ 41 |

42 |

43 | Embeds provided with <3 by 44 | e.chasa.wtf 45 |

46 |
47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /server/pages/embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% if image != "false" %} 12 | 13 | {% endif %} 14 | 15 | {% if thumbnail != "false" %} 16 | 17 | {% endif %} 18 | 19 | {% if color != "false" %} 20 | 21 | {% endif %} 22 | 23 | 50 | 51 |
52 |
53 | 54 |

You will be redirected in 4 seconds...

55 |

56 | Click here if you aren't being redirected. 57 |

58 |

59 | Embeds provided with <3 by 60 | e.chasa.wtf 61 |

62 |
63 |
64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /server/utils.py: -------------------------------------------------------------------------------- 1 | import random, json 2 | from flask import request, render_template 3 | 4 | def randChars(length): 5 | x = "" 6 | for _ in range(length): 7 | x += random.choice("QWERTYUIOPLKJHGFDSAZXCVBNM1234567890qwertyuioplkjhgfdsazxcvbnm") 8 | return x 9 | 10 | def returnIP(): 11 | return request.access_route[0] 12 | 13 | def returnJSON(js): 14 | return json.dumps(js) -------------------------------------------------------------------------------- /server/web.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import request 3 | from flask import render_template 4 | from flask import send_from_directory, request 5 | import json 6 | import flask_limiter 7 | import flask 8 | import os 9 | import base64 10 | import time 11 | 12 | browsers = ['chrome', 'windows', 'firefox'] 13 | 14 | # local imports 15 | import utils, _config 16 | from database import DataBase 17 | 18 | template_dir = os.path.abspath(f'{os.getcwd()}/pages/') 19 | app = Flask(__name__, template_folder=template_dir) 20 | limiter = flask_limiter.Limiter( 21 | app, 22 | key_func=utils.returnIP, 23 | default_limits=["3 per second", "90 per minute"], 24 | ) 25 | 26 | def parse_embed(data) -> dict: 27 | "0th = embed struct, 1st = oembed struct" 28 | embed = {} 29 | try: 30 | embed["title"] = str(data["title"]) 31 | embed["description"] = str(data["description"]).replace(r"\n", "\n") 32 | embed["redirect"] = str(data["redirect"]) 33 | except: 34 | return None 35 | 36 | try: 37 | if "#" in data["color"]: 38 | embed["color"] = str(data["color"]) 39 | else: 40 | embed["color"] = "#" + hex(int(data["color"])).replace("0x", "") 41 | except: 42 | embed["color"] = None 43 | 44 | try: 45 | tmp = {} 46 | tmp["name"] = str(data["author"]["name"]) 47 | tmp["url"] = str(data["author"]["url"]) 48 | embed["author"] = tmp 49 | except: 50 | embed["author"] = None 51 | 52 | try: 53 | tmp = {} 54 | tmp["name"] = str(data["provider"]["name"]) 55 | tmp["url"] = str(data["provider"]["url"]) 56 | embed["provider"] = tmp 57 | except: 58 | embed["provider"] = None 59 | 60 | try: 61 | tmp = {} 62 | tmp["thumbnail"] = bool(data["image"]["thumbnail"]) 63 | tmp["url"] = str(data["image"]["url"]) 64 | embed["image"] = tmp 65 | except: 66 | embed["image"] = None 67 | 68 | return embed 69 | 70 | def blacklist_check(ip): 71 | exc_time = time.time() 72 | db = DataBase(_config.db_name) 73 | blacklists = db.query("blacklist", ["ip", "reason", "timestamp", "expire"], {"ip": ip}, False) 74 | db.close() 75 | for bl in blacklists: 76 | if bl[4] == "0": 77 | return utils.returnJSON({"error": "ERROR_BLACKLIST", "message": "You are permanently blacklisted from this service.", "reason": bl[2], "expires": False}) 78 | 79 | else: 80 | expires = float(bl[3]) + float(bl[4]) 81 | if expires > exc_time: 82 | return utils.returnJSON({"error": "ERROR_BLACKLIST", "message": "You are blacklisted from this service.", "reason": bl[2], "expires": expires}) 83 | return False 84 | 85 | @app.route('/api/v1/embed', methods=["GET"]) 86 | @limiter.limit(f"{_config.get_rate_limit_per_min}/minute") 87 | @limiter.limit(f"{_config.get_rate_limit_per_hour}/hour") 88 | def fetch_embed(): 89 | ip = utils.returnIP() 90 | blacklist = blacklist_check(ip) 91 | if blacklist != False: return blacklist, 403 92 | 93 | id_req = request.args.get("id") 94 | if id_req == None: 95 | return utils.returnJSON({"error": "ERROR_ID_NOT_PROVIDED", "message": "No Embed ID was found/provided."}), 400 96 | 97 | db = DataBase(_config.db_name) 98 | data = db.query("embeds", ["data", "id", "timestamp"], {"id": id_req}) 99 | if data == None: 100 | return utils.returnJSON({"error": "ERROR_DATA_NOT_FOUND", "message": "Embed not found using provided ID."}), 404 101 | 102 | return utils.returnJSON( 103 | { 104 | "id": data[2], 105 | "link": f"{_config.url}{data[2]}", 106 | "timestamp": str(data[3]), 107 | "embed": json.loads(base64.b64decode(data[1].encode("utf-8")).decode("utf-8")) 108 | } 109 | ), 200 110 | 111 | 112 | @app.route('/api/v1/embed', methods=["POST"]) 113 | @limiter.limit(f"{_config.post_rate_limit_per_min}/minute", deduct_when=lambda response: response.status_code == 201 or response.status_code == 200) 114 | @limiter.limit(f"{_config.post_rate_limit_per_hour}/hour", deduct_when=lambda response: response.status_code == 201 or response.status_code == 200) 115 | def create_embed(): 116 | ip = utils.returnIP() 117 | req_time = time.time() 118 | blacklist = blacklist_check(ip) 119 | if blacklist != False: return blacklist, 403 120 | 121 | ctx = request.get_json() 122 | if ctx == None: 123 | return utils.returnJSON({"error": "ERROR_FETCHING_DATA", "message": "Invalid JSON, check headers and/or content."}), 400 124 | 125 | embed = parse_embed(ctx) 126 | if embed == None: 127 | return utils.returnJSON({"error": "ERROR_PARSING_DATA", "message": "Invalid JSON, check content."}), 400 128 | 129 | db = DataBase(_config.db_name) 130 | rand_id = None 131 | for _ in range(5): 132 | rand_id = utils.randChars(_config.id_length) 133 | if db.query("embeds", ["id"], {"id": rand_id}) == None: 134 | break 135 | if rand_id == None: 136 | db.close() 137 | return utils.returnJSON({"error": "ERROR_RANDOM_ID", "message": "Internal Error, send request again."}), 508 138 | 139 | b64embed = base64.b64encode(json.dumps(embed, separators=(",", ":")).encode("utf-8")).decode("utf-8") 140 | exist_check = db.query("embeds", ["data", "id", "timestamp"], {"data": b64embed}) 141 | if exist_check != None: 142 | db.close() 143 | return utils.returnJSON( 144 | { 145 | "id": exist_check[2], 146 | "link": f"{_config.url}{exist_check[2]}", 147 | "timestamp": str(exist_check[3]), 148 | "embed": embed 149 | } 150 | ), 200 151 | 152 | 153 | db.insert("embeds", [ip, base64.b64encode(json.dumps(embed, separators=(",", ":")).encode("utf-8")).decode("utf-8"), rand_id, str(req_time)]) 154 | db.close() 155 | 156 | return utils.returnJSON( 157 | { 158 | "id": rand_id, 159 | "link": f"{_config.url}{rand_id}", 160 | "timestamp": str(req_time), 161 | "embed": embed 162 | } 163 | ), 201 164 | 165 | 166 | @app.route('/api/v1/ping') 167 | @limiter.exempt 168 | def ping(): 169 | return utils.returnJSON({}), 200 170 | 171 | @app.route('/e/') 172 | def open_embed(path): 173 | path = path.replace("/", "") 174 | db = DataBase(_config.db_name) 175 | data = db.query("embeds", ["data", "id", "timestamp"], {"id": path}) 176 | if data == None: 177 | return render_template("404.html"), 404 178 | 179 | embed = json.loads(base64.b64decode(data[1].encode("utf-8")).decode("utf-8")) 180 | 181 | image = False 182 | try: image = embed["image"] 183 | except: pass 184 | if image == False or image == None: 185 | image = {"url": False, "thumbnail": False} 186 | 187 | color = False 188 | try: color = embed["color"] 189 | except: pass 190 | if color == False or color == None: 191 | color = False 192 | 193 | ua = request.headers.get('User-Agent').lower() 194 | 195 | for header in browsers: 196 | if header in ua: 197 | res = flask.make_response() 198 | res.headers['location'] = embed["redirect"] 199 | return res, 302 200 | 201 | return render_template("embed.html", 202 | new_url = embed["redirect"], 203 | title = embed["title"], 204 | description = embed["description"], 205 | o_url = _config.url2, 206 | image = image["url"], 207 | thumbnail = image["thumbnail"], 208 | color = color, 209 | id = path 210 | ), 200 211 | 212 | @app.route('/o/') 213 | def oembed_json(path): 214 | path = path.replace(".json", "") 215 | 216 | db = DataBase(_config.db_name) 217 | data = db.query("embeds", ["data", "id", "timestamp"], {"id": path}) 218 | if data == None: 219 | return render_template("404.html"), 404 220 | 221 | embed = json.loads(base64.b64decode(data[1].encode("utf-8")).decode("utf-8")) 222 | 223 | data = {} 224 | 225 | try: 226 | data["author_name"] = embed["author"]["name"] 227 | data["author_url"] = embed["author"]["url"] 228 | except: pass 229 | try: 230 | data["provider_name"] = embed["provider"]["name"] 231 | data["provider_url"] = embed["provider"]["url"] 232 | except: pass 233 | 234 | return utils.returnJSON(data), 200 235 | 236 | 237 | if __name__ == '__main__': 238 | app.run('0.0.0.0',8080) -------------------------------------------------------------------------------- /technicals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 32 | 33 | 34 | 35 | --------------------------------------------------------------------------------