Skip to content

Commit 7cd6254

Browse files
committed
basic
1 parent 89cc131 commit 7cd6254

20 files changed

Lines changed: 2403 additions & 0 deletions

deploy.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SRC="$(dirname "$(realpath "$0")")"
5+
WWW="/var/www/crygup"
6+
API="/opt/avatar-lookup/avatar_api.py"
7+
8+
echo "=== Deploying static files -> $WWW"
9+
sudo cp "$SRC"/*.html "$WWW"/
10+
sudo cp "$SRC"/*.js "$WWW"/
11+
sudo cp "$SRC"/*.css "$WWW"/
12+
sudo cp -r "$SRC"/images/* "$WWW"/images/ 2>/dev/null || true
13+
sudo nginx -t && sudo systemctl reload nginx
14+
15+
echo "=== Deploying avatar API -> $API"
16+
sudo cp "$SRC"/server/avatar_api.py "$API"
17+
sudo systemctl restart avatar-lookup.service
18+
19+
echo "=== Restarting bot"
20+
sudo systemctl restart fish.service
21+
22+
echo "Done — nginx reloaded, avatar API + bot restarted."

discord.css

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#discord-panel { max-width: 720px; }
2+
3+
#search-form {
4+
display: flex; gap: 0.5rem; margin-bottom: 0.75rem;
5+
}
6+
#search-input {
7+
flex: 1;
8+
padding: 0.6rem 1rem; font-size: 0.9rem; font-family: inherit;
9+
color: #e2e8f0; background: #243133; border: none; border-radius: 0.25rem; outline: none;
10+
}
11+
#search-input::placeholder { color: #64748b; }
12+
#search-form button {
13+
padding: 0.6rem 1.25rem; font-size: 0.9rem; font-family: inherit;
14+
color: #cbd5e1; background: #336058; border: none; border-radius: 0.25rem;
15+
cursor: pointer; transition: background 0.15s;
16+
}
17+
#search-form button:hover { background: #2a5048; }
18+
19+
#login-section { display: flex; justify-content: center; margin-bottom: 0.5rem; }
20+
.login-status { text-align: center; width: 100%; font-size: 0.85rem; color: #94a3b8; margin: 0 0 0.75rem; }
21+
.login-status strong { color: #e2e8f0; }
22+
.small-btn {
23+
padding: 0.25rem 0.75rem; font-size: 0.8rem; font-family: inherit;
24+
color: #94a3b8; background: #332b37; border: none; border-radius: 0.2rem;
25+
cursor: pointer; margin-left: 0.5rem; transition: background 0.15s, color 0.15s;
26+
}
27+
.small-btn:hover { background: #232b2e; color: #cbd5e1; }
28+
.small-login-btn {
29+
display: inline-block; padding: 0.4rem 1.25rem; background: #5865f2; color: #fff;
30+
text-decoration: none; border-radius: 0.25rem; font-size: 0.85rem; font-weight: 600;
31+
font-family: inherit; transition: background 0.15s;
32+
}
33+
.small-login-btn:hover { background: #4752c4; }
34+
.invite-banner { text-align: center; font-size: 0.85rem; color: #94a3b8; margin: 0.35rem 0 0; }
35+
.invite-banner a { color: #ffffff; text-decoration: none; }
36+
.invite-banner a:hover { text-decoration: underline; }
37+
38+
#results-grid.avatar-grid {
39+
display: grid;
40+
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
41+
gap: 0.4rem; max-width: 500px; margin: 0 auto;
42+
}
43+
.avatar-cell {
44+
aspect-ratio: 1; border-radius: 0.25rem; overflow: hidden;
45+
cursor: pointer; background: #243133; transition: transform 0.1s;
46+
}
47+
.avatar-cell:hover { transform: scale(1.04); }
48+
.avatar-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
49+
50+
.text-list { display: flex; flex-direction: column; gap: 0.4rem; max-width: 480px; margin: 0 auto; }
51+
.text-row {
52+
display: flex; flex-direction: column; align-items: center;
53+
padding: 0.6rem 0.85rem; background: #243133; border-radius: 0.25rem;
54+
font-size: 0.85rem; gap: 0.2rem; position: relative;
55+
}
56+
.text-value { color: #e2e8f0; word-break: break-all; text-align: center; }
57+
.text-date { color: #64748b; font-size: 0.78rem; }
58+
59+
.delete-btn {
60+
position: absolute; top: 0.25rem; right: 0.5rem;
61+
padding: 0 0.35rem; font-size: 0.9rem; line-height: 1.2;
62+
color: #ef4444; background: none; border: none; cursor: pointer;
63+
font-family: inherit; transition: color 0.15s;
64+
}
65+
.delete-btn:hover { color: #f87171; }
66+
.modal-del { position: static; font-size: 0.8rem; padding: 0.2rem 0.5rem; color: #ef4444; background: #3b1a1a; border: none; border-radius: 0.2rem; cursor: pointer; margin-left: 0.5rem; }
67+
.modal-del:hover { background: #4b2323; }
68+
69+
#pagination {
70+
display: flex; gap: 0.5rem; align-items: center; justify-content: center; margin: 0.75rem 0 0.5rem;
71+
}
72+
#pagination.hidden { display: none; }
73+
#pagination button {
74+
padding: 0.4rem 1rem; font-size: 0.85rem; font-family: inherit;
75+
color: #94a3b8; background: #332b37; border: none; border-radius: 0.25rem;
76+
cursor: pointer; transition: background 0.15s, color 0.15s;
77+
}
78+
#pagination button:hover { background: #232b2e; color: #cbd5e1; }
79+
#pagination button:disabled { opacity: 0.4; cursor: default; }
80+
#pagination span { font-size: 0.85rem; color: #64748b; }
81+
#status { text-align: center; font-size: 0.85rem; color: #94a3b8; min-height: 1.25rem; }
82+
83+
#modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; }
84+
#modal.hidden { display: none; }
85+
.modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.7); }
86+
.modal-content { position: relative; max-width: 90vw; max-height: 90vh; }
87+
.modal-content img { max-width: 512px; max-height: 512px; width: 100%; object-fit: cover; display: block; border-radius: 0.25rem 0.25rem 0 0; margin: 0; }
88+
.modal-close { position: absolute; top: -2rem; right: 0; background: none; border: none; color: #94a3b8; font-size: 1.5rem; cursor: pointer; }
89+
.modal-close:hover { color: #fff; }
90+
#modal-key { text-align: center; color: #94a3b8; font-size: 0.8rem; padding: 0.4rem 0.5rem 0.2rem; background: #1a1d20; max-width: 512px; width: 100%; box-sizing: border-box; margin: 0; border-radius: 0 0 0.25rem 0.25rem; }
91+
#modal-date { text-align: center; color: #64748b; font-size: 0.75rem; padding: 0 0.5rem 0.4rem; background: #1a1d20; max-width: 512px; width: 100%; box-sizing: border-box; margin: 0; }

discord.html

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>crygup · discord</title>
7+
<link rel="stylesheet" href="style.css?s=9">
8+
<link rel="stylesheet" href="discord.css?s=1">
9+
<link rel="icon" type="image/x-icon" href="images/pfp.ico">
10+
</head>
11+
<body>
12+
<header>
13+
<h1>Discord Tools</h1>
14+
</header>
15+
<main>
16+
<nav class="tab-bar" id="discord-tabs">
17+
<a href="/" class="tab-btn">Home</a>
18+
<button class="tab-btn active" data-tab="avatars">Avatars</button>
19+
<button class="tab-btn" data-tab="usernames">Usernames</button>
20+
<button class="tab-btn" data-tab="display-names">Display Names</button>
21+
<button class="tab-btn" data-tab="discrims">Discrims</button>
22+
</nav>
23+
24+
<section class="tab-panel" id="discord-panel">
25+
<form id="search-form">
26+
<input type="text" id="search-input" placeholder="Discord ID or username…" autofocus>
27+
<button type="submit">Search</button>
28+
</form>
29+
<div id="login-section"></div>
30+
<p id="status"></p>
31+
<div id="results-grid"></div>
32+
<div id="pagination" class="hidden"></div>
33+
34+
<p class="invite-banner">
35+
Want to track your avatars (and more)? Join the
36+
<a href="https://discord.gg/rM9u4MRFBE" target="_blank" rel="noopener"> Discord server</a>
37+
or invite the
38+
<a href="https://discord.com/oauth2/authorize?client_id=876391494485950504&scope=bot+applications.commands&permissions=138513074240" target="_blank" rel="noopener"> Discord bot</a>
39+
</p>
40+
</section>
41+
</main>
42+
43+
<div id="modal" class="hidden">
44+
<div class="modal-backdrop"></div>
45+
<div class="modal-content">
46+
<button class="modal-close">&times;</button>
47+
<img id="modal-img" src="" alt="">
48+
<p id="modal-key"></p>
49+
<p id="modal-date"></p>
50+
</div>
51+
</div>
52+
53+
<footer>
54+
<p>&copy; <span id="year"></span></p>
55+
</footer>
56+
57+
<script src="script.js?s=3"></script>
58+
<script src="discord.js?s=2"></script>
59+
</body>
60+
</html>

discord.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
const FISHIE_API = "https://api.crygup.com/fishie";
2+
const CLIENT_ID = "876391494485950504";
3+
4+
let currentTab = "avatars";
5+
let currentPage = 1;
6+
let currentQuery = "";
7+
let loggedInUser = JSON.parse(localStorage.getItem("discord_user") || "null");
8+
let accessToken = localStorage.getItem("discord_token") || null;
9+
let currentUserId = null;
10+
11+
const grid = document.getElementById("results-grid");
12+
const pagination = document.getElementById("pagination");
13+
const statusEl = document.getElementById("status");
14+
const input = document.getElementById("search-input");
15+
const loginSection = document.getElementById("login-section");
16+
const tabs = document.querySelectorAll("#discord-tabs .tab-btn");
17+
18+
tabs.forEach(btn => {
19+
btn.addEventListener("click", () => {
20+
tabs.forEach(b => b.classList.remove("active"));
21+
btn.classList.add("active");
22+
currentTab = btn.dataset.tab;
23+
currentPage = 1;
24+
updateDeleteAllLabel();
25+
if (currentQuery) fetchData();
26+
});
27+
});
28+
29+
function tabLabel() {
30+
return currentTab === "avatars" ? "Avatars" : currentTab === "usernames" ? "Usernames" : currentTab === "display-names" ? "Display Names" : "Discrims";
31+
}
32+
33+
function renderLogin() {
34+
if (loggedInUser) {
35+
loginSection.innerHTML = `<p class="login-status">Logged in as <strong>${escapeHtml(loggedInUser.global_name || loggedInUser.username)}</strong> <button id="logout-btn" class="small-btn">Logout</button> <button id="delete-all-btn" class="small-btn danger" style="display:none">Delete All ${tabLabel()}</button></p>`;
36+
document.getElementById("logout-btn").addEventListener("click", () => {
37+
localStorage.removeItem("discord_user"); localStorage.removeItem("discord_token");
38+
loggedInUser = null; accessToken = null; renderLogin();
39+
});
40+
document.getElementById("delete-all-btn").addEventListener("click", deleteAll);
41+
} else {
42+
const params = new URLSearchParams(window.location.search);
43+
const code = params.get("code");
44+
if (code) { window.history.replaceState({}, "", "/discord"); exchangeCode(code); }
45+
loginSection.innerHTML = `<a class="small-login-btn" href="https://discord.com/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent("https://crygup.com/discord")}&response_type=code&scope=identify">Login with Discord</a>`;
46+
}
47+
}
48+
49+
function updateDeleteAllLabel() {
50+
const btn = document.getElementById("delete-all-btn");
51+
if (btn) btn.textContent = `Delete All ${tabLabel()}`;
52+
}
53+
54+
async function exchangeCode(code) {
55+
loginSection.innerHTML = '<p class="login-status">Logging in…</p>';
56+
try {
57+
const res = await fetch(`${FISHIE_API}/oauth/exchange?code=${encodeURIComponent(code)}`, { method: "POST" });
58+
if (!res.ok) throw new Error("Login failed");
59+
const data = await res.json();
60+
localStorage.setItem("discord_user", JSON.stringify(data.user));
61+
localStorage.setItem("discord_token", data.access_token);
62+
loggedInUser = data.user; accessToken = data.access_token;
63+
renderLogin();
64+
} catch { loginSection.innerHTML = '<p class="login-status">Login failed. Refresh to try again.</p>'; }
65+
}
66+
67+
document.getElementById("search-form").addEventListener("submit", e => {
68+
e.preventDefault();
69+
currentQuery = input.value.trim();
70+
currentPage = 1;
71+
if (currentQuery) fetchData();
72+
});
73+
74+
const ENDPOINTS = {
75+
avatars: (id, page) => `https://api.crygup.com/avatars?q=${id}&page=${page}&per_page=80`,
76+
usernames: (id, page) => `${FISHIE_API}/usernames/${id}?page=${page}&per_page=80`,
77+
"display-names": (id, page) => `${FISHIE_API}/display-names/${id}?page=${page}&per_page=80`,
78+
discrims: (id, page) => `${FISHIE_API}/discrims/${id}?page=${page}&per_page=80`,
79+
};
80+
const TABLE_MAP = { usernames: "username_logs", "display-names": "display_name_logs", discrims: "discrim_logs", avatars: "avatars" };
81+
82+
const canDelete = () => accessToken && loggedInUser && currentUserId === String(loggedInUser.id);
83+
84+
async function fetchData() {
85+
grid.innerHTML = "";
86+
statusEl.textContent = "Loading…";
87+
pagination.classList.add("hidden");
88+
try {
89+
let id = currentQuery;
90+
if (!/^\d+$/.test(currentQuery)) {
91+
const r = await fetch(`${FISHIE_API}/resolve?q=${encodeURIComponent(currentQuery)}`);
92+
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || "Could not resolve user"); }
93+
id = (await r.json()).user_id;
94+
}
95+
currentUserId = id;
96+
const res = await fetch(ENDPOINTS[currentTab](id, currentPage));
97+
if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.detail || "Not found"); }
98+
const data = await res.json();
99+
if (currentTab === "avatars") renderAvatars(data.avatars);
100+
else renderTextItems(data.items);
101+
renderPagination(data.page, data.pages);
102+
statusEl.textContent = data.total ? `${data.total} found` : "No results.";
103+
const delBtn = document.getElementById("delete-all-btn");
104+
if (delBtn) { updateDeleteAllLabel(); delBtn.style.display = canDelete() ? "" : "none"; }
105+
} catch (err) { statusEl.textContent = err.message; }
106+
}
107+
108+
function renderAvatars(avatars) {
109+
grid.innerHTML = "";
110+
grid.className = "avatar-grid";
111+
for (const av of avatars) {
112+
const div = document.createElement("div");
113+
div.className = "avatar-cell";
114+
const img = document.createElement("img");
115+
img.src = av.url; img.alt = av.avatar_key; img.loading = "lazy";
116+
img.onerror = () => { img.src = ""; };
117+
div.appendChild(img);
118+
div.addEventListener("click", () => openModal(av));
119+
grid.appendChild(div);
120+
}
121+
}
122+
123+
function renderTextItems(items) {
124+
grid.innerHTML = "";
125+
grid.className = "";
126+
if (!items.length) return;
127+
const list = document.createElement("div");
128+
list.className = "text-list";
129+
for (const item of items) {
130+
const row = document.createElement("div");
131+
row.className = "text-row";
132+
const key = item.id || item.value;
133+
const delBtn = canDelete() ? `<button class="delete-btn" data-key="${escapeHtml(key)}">×</button>` : "";
134+
row.innerHTML = `${delBtn}<span class="text-value">${escapeHtml(item.value)}</span><span class="text-date">${new Date(item.created_at).toLocaleDateString()}</span>`;
135+
if (canDelete()) row.querySelector(".delete-btn").addEventListener("click", e => { e.stopPropagation(); deleteItem(TABLE_MAP[currentTab], key); });
136+
list.appendChild(row);
137+
}
138+
grid.appendChild(list);
139+
}
140+
141+
async function deleteItem(table, key) {
142+
if (!confirm("Delete this entry?")) return;
143+
try {
144+
const res = await fetch(`${FISHIE_API}/item/${table}/${currentUserId}?key=${encodeURIComponent(key)}`, { method: "DELETE", headers: { Authorization: `Bearer ${accessToken}` } });
145+
if (!res.ok) throw new Error("Delete failed");
146+
closeModal();
147+
alert("Deleted.");
148+
fetchData();
149+
} catch { alert("Delete failed."); }
150+
}
151+
152+
async function deleteAll() {
153+
const table = TABLE_MAP[currentTab];
154+
if (!confirm(`Delete ALL your ${tabLabel().toLowerCase()}? This cannot be undone!`)) return;
155+
if (!confirm("Are you sure? This data will be permanently deleted.")) return;
156+
try {
157+
const res = await fetch(`${FISHIE_API}/user/${currentUserId}?table=${table}`, { method: "DELETE", headers: { Authorization: `Bearer ${accessToken}` } });
158+
if (!res.ok) throw new Error("Delete all failed");
159+
alert(`${tabLabel()} deleted.`);
160+
fetchData();
161+
} catch { alert("Delete failed."); }
162+
}
163+
164+
function escapeHtml(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
165+
166+
function renderPagination(page, pages) {
167+
if (pages <= 1) { pagination.classList.add("hidden"); return; }
168+
pagination.classList.remove("hidden");
169+
pagination.innerHTML = `<button ${page<=1?"disabled":""} id="prev-btn">← Prev</button><span>${page} / ${pages}</span><button ${page>=pages?"disabled":""} id="next-btn">Next →</button>`;
170+
document.getElementById("prev-btn")?.addEventListener("click", () => { currentPage--; fetchData(); });
171+
document.getElementById("next-btn")?.addEventListener("click", () => { currentPage++; fetchData(); });
172+
}
173+
174+
const modal = document.getElementById("modal");
175+
const modalImg = document.getElementById("modal-img");
176+
const modalKey = document.getElementById("modal-key");
177+
const modalDate = document.getElementById("modal-date");
178+
function openModal(av) {
179+
modalImg.src = av.url;
180+
modalKey.innerHTML = `${escapeHtml(av.avatar_key)} ${canDelete() ? `<button class="modal-del" data-key="${escapeHtml(av.avatar_key)}">Delete</button>` : ""}`;
181+
modalDate.textContent = new Date(av.created_at).toLocaleString("en-US", { year:"numeric", month:"short", day:"numeric", hour:"numeric", minute:"2-digit" });
182+
if (canDelete()) modalKey.querySelector(".modal-del").addEventListener("click", () => deleteItem("avatars", av.avatar_key));
183+
modal.classList.remove("hidden");
184+
}
185+
function closeModal() { modal.classList.add("hidden"); modalImg.src = ""; }
186+
document.querySelector(".modal-backdrop")?.addEventListener("click", closeModal);
187+
document.querySelector(".modal-close")?.addEventListener("click", closeModal);
188+
document.addEventListener("keydown", e => { if (e.key==="Escape" && !modal.classList.contains("hidden")) closeModal(); });
189+
190+
renderLogin();
191+
const qp = new URLSearchParams(window.location.search);
192+
const q = qp.get("q");
193+
const tabParam = qp.get("tab");
194+
if (tabParam && document.querySelector(`#discord-tabs [data-tab="${tabParam}"]`)) {
195+
document.querySelectorAll("#discord-tabs .tab-btn").forEach(b => b.classList.remove("active"));
196+
document.querySelector(`#discord-tabs [data-tab="${tabParam}"]`).classList.add("active");
197+
currentTab = tabParam;
198+
}
199+
if (q) { input.value = q; currentQuery = q; fetchData(); }
200+
else if (loggedInUser) { input.value = loggedInUser.username; currentQuery = String(loggedInUser.id); fetchData(); }

0 commit comments

Comments
 (0)