feat: add initial Flask web app for Discord server listing

Introduces backend with server retrieval and API integration, front-end UI with templates and styles, and configuration files for deployment.
This commit is contained in:
2025-09-03 05:18:39 -04:00
parent 334782fe5c
commit ba286278cf
17 changed files with 2329 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
{
"view_server": {
"rule": "/server/<serverID>",
"strict_slashes": false
},
"view_server_default": {
"rule": "/server/",
"strict_slashes": false
}
}

198
src/ver/0.0.2/main.py Normal file
View File

@@ -0,0 +1,198 @@
# main.py
import os
import json
import logging
import humanfriendly
import requests
from flask import Flask, render_template, redirect, url_for, abort, request
from pymongo import MongoClient
# --- Flask App Initialization ---
app = Flask(__name__)
# --- Logging Configuration ---
if app.debug:
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
app.logger.addHandler(handler)
app.logger.setLevel(logging.DEBUG)
else:
app.logger.setLevel(logging.INFO)
# --- Configuration ---
try:
base_dir = os.path.dirname(os.path.abspath(__file__))
settings_path = os.path.join(base_dir, 'settings.json')
app.logger.info(f"Looking for settings.json at: {settings_path}")
with open(settings_path, 'r', encoding='utf-8') as f:
settings = json.load(f)
app.logger.info("settings.json loaded successfully.")
except (FileNotFoundError, json.JSONDecodeError) as e:
app.logger.error(f"Error loading settings.json: {e}")
settings = {
"bot_token": "YOUR_BOT_TOKEN_HERE",
"mongo": "mongodb://localhost:27017/",
"site_name": "My Server List",
"client_id": "YOUR_CLIENT_ID_HERE"
}
bot_token = settings.get("bot_token")
if not bot_token or bot_token == "YOUR_BOT_TOKEN_HERE":
raise ValueError("Bot Token is not configured in your settings.json file.")
mongo_uri = settings.get("mongo")
if not mongo_uri:
raise ValueError("MongoDB connection URI ('mongo') is not configured in your settings.json file.")
# --- Database Connection (MongoDB) ---
client = MongoClient(mongo_uri)
db = client.BytesBump
servers_collection = db.servers
app.logger.info("Successfully connected to MongoDB.")
@app.context_processor
def inject_globals():
"""Injects global variables into all templates."""
client_id = settings.get("client_id")
bot_invite_url = None
if client_id and client_id != "YOUR_CLIENT_ID_HERE":
bot_invite_url = f"https://discord.com/api/oauth2/authorize?client_id={client_id}&permissions=537152577&scope=bot"
return dict(
bot_invite_url=bot_invite_url,
site_name=settings.get("site_name", "Server List")
)
def get_all_servers_from_db():
"""Fetches all server documents from the MongoDB collection."""
return list(servers_collection.find({}))
# --- RESTORED HELPER FUNCTIONS ---
DISCORD_API_URL = "https://discord.com/api/v10"
def get_discord_guild_data(server_id):
"""Fetches guild (server) information from the Discord API."""
headers = {"Authorization": f"Bot {bot_token}"}
url = f"{DISCORD_API_URL}/guilds/{server_id}?with_counts=true"
app.logger.debug(f"Requesting guild data from Discord API for ID: {server_id}")
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
app.logger.error(f"Error fetching guild data from Discord API: {e}")
return None
def get_discord_invite_data(invite_code):
"""Fetches invite information from the Discord API."""
url = f"{DISCORD_API_URL}/invites/{invite_code}"
app.logger.debug(f"Requesting invite data from Discord API for code: {invite_code}")
try:
response = requests.get(url)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
app.logger.error(f"Error fetching invite data from Discord API: {e}")
return None
# --- Flask Routes ---
@app.errorhandler(404)
def page_not_found(err):
"""Custom 404 error handler."""
return render_template('404.html'), 404
@app.route("/")
def index():
"""Renders the home page with a list of all servers."""
servers_from_db = get_all_servers_from_db()
servers_for_template = []
for server_in_db in servers_from_db:
server_id = server_in_db.get('_id')
if not server_id:
continue
discord_data = get_discord_guild_data(str(server_id))
if not discord_data:
continue
icon_hash = discord_data.get('icon')
server_name = discord_data.get('name', 'Unknown')
server_info = {
'_id': server_id,
'server_name': server_name,
'description': discord_data.get('description') or "No description available.",
'tags': server_in_db.get('tags', 'No tags set'),
'icon_url': f"https://cdn.discordapp.com/icons/{server_id}/{icon_hash}.png" if icon_hash else f"https://placehold.co/512x512/2c2f33/ffffff?text={server_name[0]}"
}
servers_for_template.append(server_info)
template_context = {
"servers": servers_for_template,
"len": len,
"human": humanfriendly,
}
return render_template('index.html', **template_context)
@app.route("/server/<string:server_name>")
def view_server(server_name):
"""Renders the page for a specific server using its name."""
original_server_name = server_name.replace('-', ' ')
server_db_data = servers_collection.find_one({"server_name": original_server_name})
if not server_db_data:
abort(404)
server_id = server_db_data.get('_id')
discord_guild_data = get_discord_guild_data(server_id)
if not discord_guild_data:
abort(503)
invite_code = server_db_data.get('invite_code')
if invite_code:
discord_guild_data["invite"] = f"https://discord.gg/{invite_code}"
else:
vanity_code = discord_guild_data.get('vanity_url_code')
discord_guild_data["invite"] = f"https://discord.gg/{vanity_code}" if vanity_code else None
template_context = {
"server": server_db_data,
"discord": discord_guild_data,
"len": len,
"human": humanfriendly,
}
return render_template('view_server.html', **template_context)
@app.route('/browse/', defaults={'tag_name': None})
@app.route('/browse/<string:tag_name>')
def browse(tag_name):
"""Renders a page for browsing servers by tag."""
all_servers = get_all_servers_from_db()
all_tags = set()
for server in all_servers:
tags_string = server.get('tags')
if isinstance(tags_string, str):
tags_list = [tag.strip() for tag in tags_string.split(',') if tag.strip()]
all_tags.update(tags_list)
sorted_tags = sorted(list(all_tags), key=str.lower)
if tag_name:
query = {"tags": {"$regex": tag_name, "$options": "i"}}
filtered_servers = list(servers_collection.find(query))
else:
filtered_servers = all_servers
template_context = {
"all_tags": sorted_tags,
"servers": filtered_servers,
"current_tag": tag_name,
}
return render_template('browse.html', **template_context)
if __name__ == "__main__":
app.run(debug=True, port=5000)

View File

@@ -0,0 +1,5 @@
{
"name": "kServers",
"version": "1.0.0",
"description": "BytesBump Rehash"
}

View File

@@ -0,0 +1,11 @@
humanfriendly
Python.js
discord
discord.py
colorama
psycopg2-binary
dnspython
PyYAML
requests
flask
flask_dance

View File

@@ -0,0 +1,7 @@
{
"site_name": "kServers",
"mongo": "mongodb://HOST:PORT/DATABASE",
"bot_token": "",
"client_id": "1234567891234567891",
"bot_invite_url": "https://discord.com/oauth2/authorize?client_id=1234567891234567891&permissions=536955969&integration_type=0&scope=bot"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,133 @@
/* -----------------------------------------------
/* How to use? : Check the GitHub README
/* ----------------------------------------------- */
/* To load a config file (particles.json) you need to host this demo (MAMP/WAMP/local)... */
/*
particlesJS.load('particles-js', 'particles.json', function() {
console.log('particles.js loaded - callback');
});
*/
/* Otherwise just put the config content (json): */
particlesJS('particles-js',
{
"particles": {
"number": {
"value": 80,
"density": {
"enable": true,
"value_area": 2000
}
},
"color": {
"value": "random"
},
"shape": {
"type": "circle",
"stroke": {
"width": 0,
"color": "#000000"
},
"polygon": {
"nb_sides": 5
},
"image": {
"src": "img/github.svg",
"width": 100,
"height": 100
}
},
"opacity": {
"value": 0.5,
"random": false,
"anim": {
"enable": false,
"speed": 1,
"opacity_min": 0.1,
"sync": false
}
},
"size": {
"value": 5,
"random": true,
"anim": {
"enable": false,
"speed": 40,
"size_min": 0.1,
"sync": false
}
},
"line_linked": {
"enable": true,
"distance": 150,
"color": "#ffffff",
"opacity": 0.4,
"width": 1
},
"move": {
"enable": true,
"speed": 6,
"direction": "none",
"random": false,
"straight": false,
"out_mode": "out",
"attract": {
"enable": false,
"rotateX": 600,
"rotateY": 1200
}
}
},
"interactivity": {
"detect_on": "canvas",
"events": {
"onhover": {
"enable": true,
"mode": "grab"
},
"onclick": {
"enable": true,
"mode": "push"
},
"resize": true
},
"modes": {
"grab": {
"distance": 400,
"line_linked": {
"opacity": 1
}
},
"bubble": {
"distance": 400,
"size": 40,
"duration": 2,
"opacity": 8,
"speed": 3
},
"repulse": {
"distance": 200
},
"push": {
"particles_nb": 4
},
"remove": {
"particles_nb": 2
}
}
},
"retina_detect": true,
"config_demo": {
"hide_card": false,
"background_color": "#b61924",
"background_image": "",
"background_position": "50% 50%",
"background_repeat": "no-repeat",
"background_size": "cover"
}
}
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
html, body {
overflow-y: scroll;
margin: 0; /* This removes the extra space */
}
body {
background-color: #000;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
code {
margin: 0; /* This removes the extra space */
}
/* This is the updated rule */
pre {
white-space: pre-wrap;
word-break: break-all;
}
#particles-js {
z-index: -1;
position: fixed;
height: 100%;
width: 100%;
top: 0;
}
.invite-button {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.list-group-item.active {
font-weight: bold;
}
.list-group-item.active::after {
content: ' «';
font-size: 1.2em;
}
@media (max-width: 767px) {
.col-md-3 {
margin-bottom: 2rem;
}
}

View File

@@ -0,0 +1,15 @@
.list-group-item.active {
font-weight: bold;
}
.list-group-item.active::after {
content: ' «';
font-size: 1.2em;
}
/* Example: Add a bit more space above the tag list when on mobile devices */
@media (max-width: 767px) {
.col-md-3 {
margin-bottom: 2rem;
}
}

View File

@@ -0,0 +1,35 @@
.servers {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center; /* This keeps the cards centered */
opacity: 0%;
padding: 10px; /* This adds some nice spacing around the cards */
}
.card {
background-color: rgba(211, 211, 211, 0.55) !important;
}
.invite-button {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.list-group-item.active {
font-weight: bold;
}
.list-group-item.active::after {
content: ' «';
font-size: 1.2em;
}
/* Example: Add a bit more space above the tag list when on mobile devices */
@media (max-width: 767px) {
.col-md-3 {
margin-bottom: 2rem;
}
}

View File

@@ -0,0 +1,48 @@
.container {
margin-top : 10px;
background: rgba(211, 211, 211, 0.55);
}
#serverName {
text-align: center;
font-size: 20px;
font-weight: 600;
margin: 10px;
}
.info {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.info-column {
display: flex;
flex-direction: column;
flex-wrap: wrap;
height: 100%;
margin-left: 30px;
width: 80%;
margin-top: 20px;
}
.info > img {
height: 200px;
margin-bottom: 10px;
}
.info-column > a {
height: 100%;
}
.info-column > .card-body {
margin-top: 10px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
visibility: 55%;
}
.card-body > a {
margin-right: 10px;
}

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
<title>Not Found | {{site_name}}</title>
<style>
.center-container {
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="center-container">
<div class="card shadow-lg p-4 text-light" style="background-color: rgba(44, 47, 51, 0.8);">
<h1 class="display-1">404</h1>
<h2>Page Not Found</h2>
<p class="lead">Sorry, the page you are looking for does not exist.</p>
<a href="{{ url_for('index') }}" class="btn btn-primary mt-3">Go Home</a>
<hr class="my-4">
<p class="text-muted small">
<strong>Note for server owners:</strong> If you're seeing this page for your server, its name may have recently changed on Discord. You can fix the link by running the `{p}sync` command in your server.
</p>
</div>
</div>
</div>
<div id="particles-js"></div>
<script src="{{url_for('static', filename='js/particles.js')}}"></script>
<script src="{{url_for('static', filename='js/app.js')}}"></script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
{% block links %}{% endblock %}
<title>{% block title %}{{ site_name }}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
<div id="particles-js"></div>
<script src="{{url_for('static', filename='js/particles.js')}}"></script>
<script src="{{url_for('static', filename='js/app.js')}}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,80 @@
{% extends "base.html" %}
{% block title %}Browse Servers | {{ site_name }}{% endblock %}
{% block links %}
<link rel="stylesheet" href="{{url_for('static', filename='styles/index.css')}}">
{% endblock %}
{% block content %}
<div class="container my-4">
<div class="d-flex justify-content-between align-items-center">
<a href="{{ url_for('index') }}" class="btn btn-outline-light">&larr; Back to Home</a>
<h1 class="text-light">Browse Servers</h1>
</div>
<hr class="text-light">
</div>
<div class="container">
<div class="row">
<div class="col-md-3">
<h4 class="text-light">Tags</h4>
<div class="list-group">
<a href="{{ url_for('browse') }}" class="list-group-item list-group-item-action {% if not current_tag %}active{% endif %}">
All Servers
</a>
{% for tag in all_tags %}
<a href="{{ url_for('browse', tag_name=tag) }}" class="list-group-item list-group-item-action {% if tag == current_tag %}active{% endif %}">
{{ tag }}
</a>
{% endfor %}
</div>
</div>
<div class="col-md-9">
<div class="servers">
{% if servers %}
{% for server in servers %}
<div class="card shadow-lg" style="width: 18rem; margin:5px;">
<img class="card-img-top" src="{{server['icon_url']}}">
<div class="card-body">
<div class="card-header bg-danger bg-gradient text-light" style="margin-bottom:5px;">
<strong>{{server['server_name']}}</strong>
</div>
<p class="card-text server-description">{{ server['description'] }}</p>
<p class="card-text">
<strong>Tags:</strong> {{ server['tags'] }}
</p>
<a class="btn btn-outline-success d-block mx-auto" href="/server/{{ server['server_name']|replace(' ', '-') }}">View Server</a>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-light text-center mt-5">
<h4>No servers found with the tag "{{ current_tag }}".</h4>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(() => {
// This is the part that renders markdown
const md = window.markdownit();
const elements = document.getElementsByClassName("server-description");
for (var i = 0; i < elements.length; i++) {
elements[i].innerHTML = md.render(elements[i].innerHTML);
}
// --- ADD THIS ANIMATION ---
// This fades in the server list, making it visible
$(".servers").animate({
opacity: "100%"
}, 600)
})
</script>
{% endblock %}

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='styles/index.css')}}">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<title>Home | {{site_name}}</title>
</head>
<body>}
<div class="container text-center my-4">
<h1><a href="{{ url_for('index') }}" class="text-light text-decoration-none">{{ site_name }}</a></h1>
<a href="{{ url_for('browse') }}" class="btn btn-secondary mt-2">Browse by Tag</a>
<a href="{{ bot_invite_url }}" class="btn btn-primary invite-button" target="https://discord.com/oauth2/authorize?client_id=1285545069591527466&permissions=537012969&integration_type=0&scope=bot">🤖 Invite Bot</a>
</div>
<div class="servers">
{% for server in servers %}
<div class="card shadow-lg" style="width: 18rem; margin:5px;">
<img class="card-img-top" src="{{server['icon_url']}}">
<div class="card-body">
<div class="card-header bg-danger bg-gradient text-light" style="margin-bottom:5px;">
<strong>{{server['server_name']}}</strong>
</div>
<p class="card-text server-description">{{ server['description'] }}</p>
<p class="card-text">
<strong>Tags:</strong> {{ server['tags'] }}
</p>
<a class="btn btn-success d-block mx-auto" href="/server/{{ server['server_name']|replace(' ', '-') }}">View Server</a>
</div>
</div>
{% endfor %}
</div>
<div id="particles-js"></div>
<script src="{{url_for('static', filename='js/particles.js')}}"></script>
<script src="{{url_for('static', filename='js/app.js')}}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script>
$(document).ready(() => {
const md = window.markdownit();
const elements = document.getElementsByClassName("server-description");
for (var i = 0; i < elements.length; i++) {
elements[i].innerHTML = md.render(elements[i].innerHTML);
}
$(".servers").animate({
opacity: "100%"
}, 600)
})
</script>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha341-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='styles/index.css')}}">
<title>{{ discord['name'] }} | {{site_name}}</title>
</head>
<body>
<div class="container my-4">
<a href="{{ url_for('index') }}" class="btn btn-outline-light">&larr; Back to Server List</a>
</div>
<div class="container">
<div class="card shadow-lg text-light">
<div class="card-header bg-danger bg-gradient text-light text-center h3">
{{ discord.name }}
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 text-center">
{% if discord.icon %}
<img src="https://cdn.discordapp.com/icons/{{ discord.id }}/{{ discord.icon }}.png?size=256" class="img-fluid rounded-circle border border-5 border-danger mb-3">
{% else %}
<img src="https://placehold.co/256x256/2c2f33/ffffff?text={{ discord.name[0] }}" class="img-fluid rounded-circle border border-5 border-danger mb-3">
{% endif %}
{% if discord.invite %}
<a href="{{ discord.invite }}" class="btn btn-success btn-lg my-3" target="_blank">Join Server</a>
{% endif %}
</div>
<div class="col-md-8">
<h4>Description</h4>
<p id="description-text">{{ server.description or "No description available." }}</p>
<hr>
<h4>Tags</h4>
<p>{{ server.tags or "No tags set." }}</p>
<hr>
<h4>Server Stats</h4>
<p>
<span class="badge bg-primary me-2">Members: {{ human.format_number(discord.approximate_member_count) }}</span>
<span class="badge bg-secondary me-2">Roles: {{ discord.roles | length }}</span>
<span class="badge bg-warning text-dark me-2">Boosts: {{ discord.premium_subscription_count }} (Tier {{ discord.premium_tier }})</span>
</p>
</div>
</div>
</div>
</div>
</div>
<div id="particles-js"></div>
<script src="{{url_for('static', filename='js/particles.js')}}"></script>
<script src="{{url_for('static', filename='js/app.js')}}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
<script>
// Render markdown for the description
const md = window.markdownit();
const descriptionElement = document.getElementById("description-text");
if (descriptionElement) {
descriptionElement.innerHTML = md.render(descriptionElement.innerHTML);
}
</script>
</body>
</html>