2 Commits
Author SHA1 Message Date
Elias Wendland b5bca1c59a Release 0.2.1
CI and release / Detect release commit (push) Successful in 29s
CI and release / Run tests (push) Successful in 4m27s
CI and release / Build and publish container (push) Successful in 10m33s
CI and release / Create release (push) Successful in 28s
2026-08-30 16:55:00 +02:00
Elias Wendland 48408cba61 Release 0.2.0
CI and release / Run tests (push) Failing after 33s
CI and release / Detect release commit (push) Successful in 36s
CI and release / Build and publish container (push) Successful in 6m30s
CI and release / Create release (push) Successful in 15s
2026-08-25 21:58:43 +02:00
44 changed files with 1875 additions and 515 deletions

No files matched your search

+7 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tg-archive-bot" name = "tg-archive-bot"
version = "0.1.0" version = "0.2.1"
edition = "2024" edition = "2024"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
description = "Bot for archiving messages in TeenGovernment and related servers" description = "Bot for archiving messages in TeenGovernment and related servers"
@@ -10,11 +10,17 @@ repository = "https://git.ewenlau.net/ewenlau/tg-archive-bot"
homepage = "https://git.ewenlau.net/ewenlau/tg-archive-bot" homepage = "https://git.ewenlau.net/ewenlau/tg-archive-bot"
[dependencies] [dependencies]
askama = "0.14"
axum = { version = "0.8.4", features = ["form"] } axum = { version = "0.8.4", features = ["form"] }
axum-extra = { version = "0.12.6", features = ["cookie"] }
dotenv = "0.15.0" dotenv = "0.15.0"
poise = "0.6.2" poise = "0.6.2"
rand = "0.9"
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.9.0", features = ["postgres", "runtime-tokio", "macros"] } sqlx = { version = "0.9.0", features = ["postgres", "runtime-tokio", "macros"] }
tokio = { version = "1.52.3", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
tower-sessions = "0.15"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
urlencoding = "2"
+2
View File
@@ -5,3 +5,5 @@ This is the bot meant for archiving messages in TeenGovernment and related serve
The bot archives new messages and edits, including attachments and embeds. Messages stay in the archive if they are deleted from Discord. The bot archives new messages and edits, including attachments and embeds. Messages stay in the archive if they are deleted from Discord.
An image is available at git.ewenlau.net/ewenlau/tg-archive-bot:latest for the stable version and git.ewenlau.net/ewenlau/tg-archive-bot:dev for the development (or testing) version. An image is available at git.ewenlau.net/ewenlau/tg-archive-bot:latest for the stable version and git.ewenlau.net/ewenlau/tg-archive-bot:dev for the development (or testing) version.
The web archive uses Discord OAuth2 with the `identify` scope. Add `/login/discord/callback` as a redirect in the Discord developer portal, then set `TG_BOT_DISCORD_CLIENT_ID`, `TG_BOT_DISCORD_CLIENT_SECRET`, and `TG_BOT_DISCORD_REDIRECT_URI`.
+2
View File
@@ -0,0 +1,2 @@
[general]
dirs = ["static"]
+3
View File
@@ -31,6 +31,9 @@ services:
- postgres - postgres
environment: environment:
TG_BOT_DISCORD_TOKEN: YOUR_DISCORD_TOKEN_HERE TG_BOT_DISCORD_TOKEN: YOUR_DISCORD_TOKEN_HERE
TG_BOT_DISCORD_CLIENT_ID: YOUR_DISCORD_CLIENT_ID_HERE
TG_BOT_DISCORD_CLIENT_SECRET: YOUR_DISCORD_CLIENT_SECRET_HERE
TG_BOT_DISCORD_REDIRECT_URI: http://localhost:3000/login/discord/callback
TG_BOT_DATABASE_URL: postgres://postgres:YOUR_POSTGRES_PASSWORD_HERE@postgres:5432/tg_archive_bot TG_BOT_DATABASE_URL: postgres://postgres:YOUR_POSTGRES_PASSWORD_HERE@postgres:5432/tg_archive_bot
TG_BOT_LOG: info TG_BOT_LOG: info
TG_BOT_WEB_ADDRESS: 0.0.0.0:3000 TG_BOT_WEB_ADDRESS: 0.0.0.0:3000
+2
View File
@@ -1,7 +1,9 @@
pub mod git; pub mod git;
pub mod ping; pub mod ping;
pub mod stats;
pub mod version; pub mod version;
pub use git::git; pub use git::git;
pub use ping::ping; pub use ping::ping;
pub use stats::stats;
pub use version::version; pub use version::version;
+33
View File
@@ -0,0 +1,33 @@
use crate::messaging::trace_message;
use crate::{Context, Error, web};
use tracing::{debug, trace};
#[doc = "Show archive statistics"]
#[poise::command(slash_command, prefix_command)]
pub async fn stats(ctx: Context<'_>) -> Result<(), Error> {
trace!(
"stats command called by user {} in guild {}",
ctx.author().id.get(),
ctx.guild_id().unwrap().get()
);
let stats = web::archive_stats(&ctx.data().pool).await?;
let msg = format!(
"# Archive Statistics\n## Messages\nArchived messages: {}\nArchived users: {}\nChannels: {}\nServers: {}\n## Storage usage\nTotal archive: {}\nMessages and metadata: {}\nAttachments: {}",
stats.messages,
stats.users,
stats.channels,
stats.servers,
web::format_bytes(stats.total_storage),
web::format_bytes(stats.message_storage),
web::format_bytes(stats.attachment_storage),
);
trace_message(
&msg,
ctx.channel_id().to_string(),
ctx.guild_id().unwrap().to_string(),
)
.await;
ctx.say(msg).await?;
debug!("Stats command performed for user {}", ctx.author().name);
Ok(())
}
+29 -6
View File
@@ -12,7 +12,6 @@ pub struct Data {
pub pool: sqlx::PgPool, pub pool: sqlx::PgPool,
} }
// Define standard types for Poise context, commands, and errors.
pub type Error = Box<dyn std::error::Error + Send + Sync>; pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Context<'a> = poise::Context<'a, Data, Error>; pub type Context<'a> = poise::Context<'a, Data, Error>;
@@ -37,7 +36,6 @@ async fn main() {
trace!("Env loaded"); trace!("Env loaded");
// Grab envs
trace!("Loading database url"); trace!("Loading database url");
let database_url = let database_url =
env::var("TG_BOT_DATABASE_URL").expect("Expected a database url in the environment"); env::var("TG_BOT_DATABASE_URL").expect("Expected a database url in the environment");
@@ -51,9 +49,15 @@ async fn main() {
let web_address = env::var("TG_BOT_WEB_ADDRESS").unwrap_or_else(|_| "0.0.0.0:3000".to_string()); let web_address = env::var("TG_BOT_WEB_ADDRESS").unwrap_or_else(|_| "0.0.0.0:3000".to_string());
trace!("Web address loaded"); trace!("Web address loaded");
let discord_client_id = env::var("TG_BOT_DISCORD_CLIENT_ID")
.expect("Expected a Discord client ID in the environment");
let discord_client_secret = env::var("TG_BOT_DISCORD_CLIENT_SECRET")
.expect("Expected a Discord client secret in the environment");
let discord_redirect_uri = env::var("TG_BOT_DISCORD_REDIRECT_URI")
.expect("Expected a Discord redirect URI in the environment");
info!("Starting bot..."); info!("Starting bot...");
// Initialize database
debug!("Initalizing database"); debug!("Initalizing database");
let pool = connect_database(&database_url) let pool = connect_database(&database_url)
.await .await
@@ -71,7 +75,14 @@ async fn main() {
let web_listener = tokio::net::TcpListener::bind(&web_address) let web_listener = tokio::net::TcpListener::bind(&web_address)
.await .await
.expect("Failed to bind web server"); .expect("Failed to bind web server");
tokio::spawn(web::run(web_listener, pool.clone())); tokio::spawn(web::run(
web_listener,
pool.clone(),
token.clone(),
discord_client_id,
discord_client_secret,
discord_redirect_uri,
));
debug!("Web server started"); debug!("Web server started");
trace!("Loading intents"); trace!("Loading intents");
@@ -102,7 +113,12 @@ async fn main() {
} }
}) })
}, },
commands: vec![commands::ping(), commands::version(), commands::git()], commands: vec![
commands::ping(),
commands::version(),
commands::git(),
commands::stats(),
],
..Default::default() ..Default::default()
}) })
.setup(|ctx, _ready, framework| { .setup(|ctx, _ready, framework| {
@@ -171,5 +187,12 @@ async fn connect_database(database_url: &str) -> Result<sqlx::PgPool, Error> {
} }
fn quote_identifier(identifier: &str) -> String { fn quote_identifier(identifier: &str) -> String {
format!("\"{}\"", identifier.replace('"', "\"\"")) let mut escaped = String::with_capacity(identifier.len());
for character in identifier.chars() {
escaped.push(character);
if character == '"' {
escaped.push(character);
}
}
format!("\"{}\"", escaped)
} }
+1449 -395
View File
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -1,5 +1,6 @@
<form method="post" action="$${{action}}"> <form method="post" action="{{action}}">
<input type="hidden" name="search" value="$${{search}}"> <input type="hidden" name="search" value="{{search}}">
$${{additional_fields}} <label>Page <input type="number" name="page" min="1" max="$${{total_pages}}" required></label> {% if let Some(search_by) = search_by %} <input type="hidden" name="search_by" value="{{search_by}}">
{% endif %} <label>Page <input type="number" name="page" min="1" max="{{total_pages}}" required></label>
<button type="submit">Go</button> <button type="submit">Go</button>
</form> </form>
-1
View File
@@ -1 +0,0 @@
— $${{value}}
+1 -1
View File
@@ -1 +1 @@
<li><a href="/attachments/$${{attachment_id}}?version=$${{message_version}}" target="_blank">View $${{filename}}</a> — $${{size}} bytes$${{content_type}}$${{description}}</li> <li><a href="/attachments/{{attachment_id}}?version={{message_version}}" target="_blank">View {{filename}}</a> - {{size}} bytes{% if let Some(value) = content_type %} - {{value}}{% endif %}{% if let Some(value) = description %} - {{value}}{% endif %}</li>
+13 -12
View File
@@ -1,22 +1,23 @@
<h2>$${{channel_name}}</h2> <h2>{{channel_name}}</h2>
<p>You can see this channel because {{access_reason}}.</p>
<dl> <dl>
<dt>Channel ID</dt><dd>$${{channel_id}}</dd> <dt>Channel ID</dt><dd>{{channel_id}}</dd>
<dt>Server</dt><dd><a href="/servers/$${{server_id}}">$${{server_name}}</a></dd> <dt>Server</dt><dd><a href="/servers/{{server_id}}">{{server_name}}</a></dd>
<dt>Messages</dt><dd>$${{message_count}}</dd> <dt>Messages</dt><dd>{{message_count}}</dd>
<dt>Observed users</dt><dd>$${{user_count}}</dd> <dt>Observed users</dt><dd>{{user_count}}</dd>
<dt>Message versions</dt><dd>$${{version_count}}</dd> <dt>Message versions</dt><dd>{{version_count}}</dd>
<dt>Archived data</dt><dd>$${{total_storage}}</dd> <dt>Archived data</dt><dd>{{total_storage}}</dd>
<dt>Message content</dt><dd>$${{message_storage}}</dd> <dt>Message content</dt><dd>{{message_storage}}</dd>
<dt>Attachments</dt><dd>$${{attachment_storage}}</dd> <dt>Attachments</dt><dd>{{attachment_storage}}</dd>
</dl> </dl>
<form method="get" action="/channels/$${{channel_id}}/messages"> <form method="get" action="/channels/{{channel_id}}/messages">
<button type="submit">View messages in this channel</button> <button type="submit">View messages in this channel</button>
</form> </form>
<h3>Known names</h3> <h3>Known names</h3>
<ul> <ul>
$${{names}} {{names|safe}}
</ul> </ul>
<h3>Observed users</h3> <h3>Observed users</h3>
<ul> <ul>
$${{users}} {{users|safe}}
</ul> </ul>
+1 -1
View File
@@ -1 +1 @@
<li><a href="/channels/$${{channel_id}}"><strong>$${{channel_name}}</strong> ($${{channel_id}})</a> in <a href="/servers/$${{server_id}}">$${{server_name}}</a></li> <li><a href="/channels/{{channel_id}}"><strong>{{channel_name}}</strong> ({{channel_id}})</a> in <a href="/servers/{{server_id}}">{{server_name}}</a></li>
+1 -1
View File
@@ -1 +1 @@
<strong>$${{page_number}}</strong> <strong>{{page_number}}</strong>
-1
View File
@@ -1 +0,0 @@
<pre>$${{description}}</pre>
-1
View File
@@ -1 +0,0 @@
<strong>$${{title}}</strong>
-1
View File
@@ -1 +0,0 @@
<a href="$${{url}}">$${{url}}</a>
+1 -1
View File
@@ -1 +1 @@
<li>$${{title}}$${{description}}$${{url}}</li> <li>{% if let Some(value) = title %}<strong>{{value}}</strong>{% endif %}{% if let Some(value) = description %}<pre>{{value}}</pre>{% endif %}{% if let Some(value) = url %}<a href="{{value}}">{{value}}</a>{% endif %}</li>
+1 -1
View File
@@ -1,2 +1,2 @@
<h2>Error</h2> <h2>Error</h2>
<p>$${{message}}</p> <p>{{message}}</p>
+6 -4
View File
@@ -1,8 +1,10 @@
<h2>Archive</h2> <h2>Archive</h2>
<p>Archived $${{message_count}} messages from $${{user_count}} users across $${{channel_count}} channels on $${{server_count}} servers.</p> <p>Archived {{message_count}} messages from {{user_count}} users across {{channel_count}} channels on {{server_count}} servers.</p>
<h3>Storage usage</h3> <h3>Storage usage</h3>
<dl> <dl>
<dt>Total archive</dt><dd>$${{total_storage}}</dd> <dt>Total archive</dt><dd>{{total_storage}}</dd>
<dt>Messages and metadata</dt><dd>$${{message_storage}}</dd> <dt>Messages and metadata</dt><dd>{{message_storage}}</dd>
<dt>Attachments</dt><dd>$${{attachment_storage}}</dd> <dt>Attachments</dt><dd>{{attachment_storage}}</dd>
</dl> </dl>
<p>If you cannot see any messages, try and send a message on a server the bot is in.</p>
+5 -5
View File
@@ -1,7 +1,7 @@
<h2>$${{title}}</h2> <h2>{{title}}</h2>
$${{search_form}} {{search_form|safe}}
<p>$${{item_count}} $${{item_name}} found.</p> <p>{{item_count}} {{item_name}} found.</p>
<ul> <ul>
$${{items}} {{items|safe}}
</ul> </ul>
$${{pagination}} {{pagination|safe}}
+6
View File
@@ -0,0 +1,6 @@
<h2>Log in</h2>
<p>It is required to log in with Discord to view the archive.</p>
<p>The login happens on the official Discord website. This site does not have access to your email or password.</p>
<form method="get" action="/login/discord">
<button type="submit">Log in with Discord</button>
</form>
+1 -1
View File
@@ -1 +1 @@
<li><a href="/messages/$${{message_id}}">Message $${{message_id}}</a> by <a href="/users/$${{author_id}}"><strong>$${{author}}</strong></a> in <a href="/servers/$${{server_id}}">$${{server}}</a> / <a href="/channels/$${{channel_id}}">$${{channel}}</a> at $${{timestamp}} — $${{attachment_count}} attachments, $${{embed_count}} embeds</li> <li><a href="/messages/{{message_id}}">Message {{message_id}}</a> by <a href="/users/{{author_id}}"><strong>{{author}}</strong></a> in <a href="/servers/{{server_id}}">{{server}}</a> / <a href="/channels/{{channel_id}}">{{channel}}</a> at {{timestamp}} - {{attachment_count}} attachments, {{embed_count}} embeds</li>
+5 -5
View File
@@ -1,7 +1,7 @@
<h2>$${{title}}</h2> <h2>{{title}}</h2>
$${{search_form}} {{search_form|safe}}
<p>$${{item_count}} archived messages found.</p> <p>{{item_count}} archived messages found.</p>
<ol> <ol>
$${{items}} {{items|safe}}
</ol> </ol>
$${{pagination}} {{pagination|safe}}
+4 -4
View File
@@ -1,9 +1,9 @@
<form method="post" action="$${{action}}"> <form method="post" action="{{action}}">
<label>Search messages: <input type="search" name="search" value="$${{search}}"></label> <label>Search messages: <input type="search" name="search" value="{{search}}"></label>
<label>Search by: <label>Search by:
<select name="search_by"> <select name="search_by">
<option value="content"$${{content_selected}}>Content</option> <option value="content"{{content_selected}}>Content</option>
<option value="timestamp"$${{timestamp_selected}}>Timestamp</option> <option value="timestamp"{{timestamp_selected}}>Timestamp</option>
</select> </select>
</label> </label>
<button type="submit">Search</button> <button type="submit">Search</button>
+2 -2
View File
@@ -1,3 +1,3 @@
<form method="get" action="/messages/$${{message_id}}"> <form method="get" action="/messages/{{message_id}}">
<button type="submit" name="version" value="$${{version}}">$${{label}}</button> <button type="submit" name="version" value="{{version}}">{{label}}</button>
</form> </form>
+4 -4
View File
@@ -1,9 +1,9 @@
<nav aria-label="Message versions"> <nav aria-label="Message versions">
<p>Version $${{current_version}} of $${{version_count}} — archived $${{archived_at}}</p> <p>Version {{current_version}} of {{version_count}} - archived {{archived_at}}</p>
<p> <p>
$${{previous_version}}$${{next_version}} </p> {{previous_version|safe}}{{next_version|safe}} </p>
<form method="get" action="/messages/$${{message_id}}"> <form method="get" action="/messages/{{message_id}}">
<label>Version <input type="number" name="version" min="1" max="$${{version_count}}" value="$${{current_version}}" required></label> <label>Version <input type="number" name="version" min="1" max="{{version_count}}" value="{{current_version}}" required></label>
<button type="submit">View version</button> <button type="submit">View version</button>
</form> </form>
</nav> </nav>
+10 -9
View File
@@ -1,18 +1,19 @@
<h2>Message $${{message_id}}</h2> <h2>Message {{message_id}}</h2>
$${{version_navigation}} <p>You can see this message because {{access_reason}}.</p>
{{version_navigation|safe}}
<dl> <dl>
<dt>Author</dt><dd><a href="/users/$${{author_id}}">$${{author}} ($${{author_id}})</a></dd> <dt>Author</dt><dd><a href="/users/{{author_id}}">{{author}} ({{author_id}})</a></dd>
<dt>Server</dt><dd><a href="/servers/$${{server_id}}">$${{server}}</a></dd> <dt>Server</dt><dd><a href="/servers/{{server_id}}">{{server}}</a></dd>
<dt>Channel</dt><dd><a href="/channels/$${{channel_id}}">$${{channel}}</a></dd> <dt>Channel</dt><dd><a href="/channels/{{channel_id}}">{{channel}}</a></dd>
<dt>Timestamp</dt><dd>$${{timestamp}}</dd> <dt>Timestamp</dt><dd>{{timestamp}}</dd>
</dl> </dl>
<h3>Content</h3> <h3>Content</h3>
<pre>$${{content}}</pre> <pre>{{content}}</pre>
<h3>Attachments</h3> <h3>Attachments</h3>
<ul> <ul>
$${{attachments}} {{attachments|safe}}
</ul> </ul>
<h3>Embeds</h3> <h3>Embeds</h3>
<ol> <ol>
$${{embeds}} {{embeds|safe}}
</ol> </ol>
+4 -3
View File
@@ -1,4 +1,5 @@
<form method="post" action="$${{action}}"> <form method="post" action="{{action}}">
<input type="hidden" name="search" value="$${{search}}"> <input type="hidden" name="search" value="{{search}}">
$${{additional_fields}} <button type="submit" name="page" value="$${{page_number}}">$${{label}}</button> {% if let Some(search_by) = search_by %} <input type="hidden" name="search_by" value="{{search_by}}">
{% endif %} <button type="submit" name="page" value="{{page_number}}">{{label}}</button>
</form> </form>
+26 -9
View File
@@ -1,23 +1,40 @@
<!doctype html> <!doctype html>
<html lang="en"> <html class="theme-{{theme}}" lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>$${{title}} — TG Archive</title> <title>{{ title }} - TG Archive</title>
<link rel="stylesheet" href="/theme.css">
</head> </head>
<body> <body>
<header> <header class="site-header">
<h1>TG Archive</h1> <strong class="site-brand">TG Archive</strong>
<nav> {% if logged_in %} <nav class="site-navigation">
<a href="/">Home</a> | <a href="/">Home</a> |
<a href="/servers">Servers</a> | <a href="/servers">Servers</a> |
<a href="/channels">Channels</a> | <a href="/channels">Channels</a> |
<a href="/users">Users</a> | <a href="/users">Users</a> |
<a href="/messages">Messages</a> <a href="/messages">Messages</a> |
</nav> <form method="post" action="/logout"><button type="submit">Log out</button></form>
</nav>{% endif %}
</header> </header>
<main> <main class="page-content">
$${{body}} {{ body|safe }}
</main> </main>
<footer class="site-footer">
<p class="footer-legal">Source code available <a href="https://git.ewenlau.net/ewenlau/tg-archive-bot">here</a>.<br>
Copyright © 2026 Ewi and contributors<br>
This content is licensed under GPL-3.0.</p>
<form class="footer-theme" method="post" action="/theme">
<label>Theme
<select name="theme">
<option value="white"{% if theme == "white" %} selected{% endif %}>White</option>
<option value="black"{% if theme == "black" %} selected{% endif %}>Black</option>
<option value="oled"{% if theme == "oled" %} selected{% endif %}>OLED</option>
</select>
</label>
<button type="submit">Apply</button>
</form>
</footer>
</body> </body>
</html> </html>
+4 -4
View File
@@ -1,10 +1,10 @@
<nav aria-label="Pages"> <nav aria-label="Pages">
<p> <p>
$${{first_pages}} {{first_pages|safe}}
</p> </p>
$${{arbitrary_page}} {{arbitrary_page|safe}}
<p> <p>
$${{final_pages}} {{final_pages|safe}}
</p> </p>
<p>Page $${{current_page}} of $${{total_pages}}</p> <p>Page {{current_page}} of {{total_pages}}</p>
</nav> </nav>
+2 -2
View File
@@ -1,4 +1,4 @@
<form method="post" action="$${{action}}"> <form method="post" action="{{action}}">
<label>$${{label}}: <input type="search" name="search" value="$${{search}}"></label> <label>{{label}}: <input type="search" name="search" value="{{search}}"></label>
<button type="submit">Search</button> <button type="submit">Search</button>
</form> </form>
+1 -1
View File
@@ -1 +1 @@
— <a href="$${{icon_url}}">icon</a> - <a href="{{icon_url}}">icon</a>
+14 -14
View File
@@ -1,31 +1,31 @@
<h2>$${{server_name}}</h2> <h2>{{server_name}}</h2>
<dl> <dl>
<dt>Server ID</dt><dd>$${{server_id}}</dd> <dt>Server ID</dt><dd>{{server_id}}</dd>
<dt>Messages</dt><dd>$${{message_count}}</dd> <dt>Messages</dt><dd>{{message_count}}</dd>
<dt>Archived users</dt><dd>$${{user_count}}</dd> <dt>Archived users</dt><dd>{{user_count}}</dd>
<dt>Archived channels</dt><dd>$${{channel_count}}</dd> <dt>Archived channels</dt><dd>{{channel_count}}</dd>
<dt>Message versions</dt><dd>$${{version_count}}</dd> <dt>Message versions</dt><dd>{{version_count}}</dd>
<dt>Archived data</dt><dd>$${{total_storage}}</dd> <dt>Archived data</dt><dd>{{total_storage}}</dd>
<dt>Message content</dt><dd>$${{message_storage}}</dd> <dt>Message content</dt><dd>{{message_storage}}</dd>
<dt>Attachments</dt><dd>$${{attachment_storage}}</dd> <dt>Attachments</dt><dd>{{attachment_storage}}</dd>
</dl> </dl>
<form method="get" action="/servers/$${{server_id}}/messages"> <form method="get" action="/servers/{{server_id}}/messages">
<button type="submit">View messages in this server</button> <button type="submit">View messages in this server</button>
</form> </form>
<h3>Known names</h3> <h3>Known names</h3>
<ul> <ul>
$${{names}} {{names|safe}}
</ul> </ul>
<h3>Known icons</h3> <h3>Known icons</h3>
<ul> <ul>
$${{icons}} {{icons|safe}}
</ul> </ul>
<h3>Channels</h3> <h3>Channels</h3>
<ul> <ul>
$${{channels}} {{channels|safe}}
</ul> </ul>
<h3>Observed users</h3> <h3>Observed users</h3>
<p>Users are included after one of their messages is archived in this server.</p> <p>Users are included after one of their messages is archived in this server.</p>
<ul> <ul>
$${{users}} {{users|safe}}
</ul> </ul>
+1 -1
View File
@@ -1 +1 @@
<li><a href="/servers/$${{guild_id}}"><strong>$${{guild_name}}</strong> ($${{guild_id}})</a>$${{icon}}</li> <li><a href="/servers/{{guild_id}}"><strong>{{guild_name}}</strong> ({{guild_id}})</a>{{icon|safe}}</li>
+1 -1
View File
@@ -1 +1 @@
<li><a href="$${{avatar_url}}"><img src="$${{avatar_url}}" alt="Archived user avatar" width="64" height="64"></a> — first seen $${{first_seen}}, last seen $${{last_seen}}</li> <li><a href="{{avatar_url}}"><img src="{{avatar_url}}" alt="Archived user avatar" width="64" height="64"></a> - first seen {{first_seen}}, last seen {{last_seen}}</li>
+1 -1
View File
@@ -1 +1 @@
<li><a href="/channels/$${{channel_id}}"><strong>$${{channel_name}}</strong></a> — $${{message_count}} messages</li> <li><a href="/channels/{{channel_id}}"><strong>{{channel_name}}</strong></a> - {{message_count}} messages</li>
+1 -1
View File
@@ -1 +1 @@
<li><a href="$${{icon_url}}"><img src="$${{icon_url}}" alt="Archived server icon" width="64" height="64"></a> — first seen $${{first_seen}}, last seen $${{last_seen}}</li> <li><a href="{{icon_url}}"><img src="{{icon_url}}" alt="Archived server icon" width="64" height="64"></a> - first seen {{first_seen}}, last seen {{last_seen}}</li>
+1 -1
View File
@@ -1 +1 @@
<li><strong>$${{name}}</strong> — first seen $${{first_seen}}, last seen $${{last_seen}}</li> <li><strong>{{name}}</strong> - first seen {{first_seen}}, last seen {{last_seen}}</li>
+1 -1
View File
@@ -1 +1 @@
<li><a href="/servers/$${{server_id}}"><strong>$${{server_name}}</strong></a> — $${{message_count}} messages</li> <li><a href="/servers/{{server_id}}"><strong>{{server_name}}</strong></a> - {{message_count}} messages</li>
+1 -1
View File
@@ -1 +1 @@
<li><a href="/users/$${{user_id}}"><strong>$${{username}}</strong></a> — $${{message_count}} messages</li> <li><a href="/users/{{user_id}}"><strong>{{username}}</strong></a> - {{message_count}} messages</li>
+209
View File
@@ -0,0 +1,209 @@
:root {
color-scheme: light;
font-family: Arial, sans-serif;
}
html {
background: #f5f5f5;
color: #202020;
}
html.theme-black {
background: #181818;
color: #e6e6e6;
color-scheme: dark;
}
html.theme-oled {
background: #000;
color: #e6e6e6;
color-scheme: dark;
}
body {
box-sizing: border-box;
display: flex;
flex-direction: column;
line-height: 1.45;
margin: 0 auto;
max-width: 1100px;
min-height: 100vh;
padding: 0 18px;
}
a {
color: #245b91;
}
a:hover {
color: #123f6b;
}
.theme-black a,
.theme-oled a {
color: #8ebbea;
}
.theme-black a:hover,
.theme-oled a:hover {
color: #b8d8f5;
}
.site-header,
.site-footer {
padding: 18px 0;
}
.site-header {
align-items: center;
display: flex;
gap: 24px;
justify-content: space-between;
}
.site-brand {
font-size: 1.2rem;
white-space: nowrap;
}
.site-navigation {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.site-navigation form {
display: inline;
}
.page-content {
background: #fff;
border: 1px solid #d8d8d8;
flex: 1;
padding: 24px;
}
.theme-black .page-content {
background: #222;
border-color: #444;
}
.theme-oled .page-content {
background: #000;
border-color: #444;
}
button,
input,
select {
border: 1px solid #999;
border-radius: 3px;
box-sizing: border-box;
font: inherit;
padding: 7px;
}
button {
background: #f8f8f8;
color: #202020;
cursor: pointer;
}
button:hover {
background: #e9e9e9;
}
.theme-black button,
.theme-black input,
.theme-black select,
.theme-oled button,
.theme-oled input,
.theme-oled select {
background: #292929;
border-color: #666;
color: #eee;
}
.theme-black button:hover,
.theme-oled button:hover {
background: #3d3d3d;
}
form {
margin: 12px 0;
}
li {
margin: 8px 0;
}
dt {
font-weight: bold;
margin-top: 12px;
}
dd {
margin-left: 0;
}
pre {
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.site-footer {
display: flex;
gap: 24px;
justify-content: space-between;
}
.site-footer > * {
margin: 0;
}
.footer-legal {
text-align: left;
}
.footer-theme {
text-align: right;
}
@media (max-width: 700px) {
body {
padding: 0 10px;
}
.site-header,
.site-footer {
align-items: stretch;
flex-direction: column;
gap: 10px;
padding: 12px 0;
}
.page-content {
padding: 16px;
}
.site-navigation {
line-height: 1.8;
}
.footer-theme {
text-align: left;
}
input,
select {
max-width: 100%;
}
img,
video,
iframe {
height: auto;
max-width: 100%;
}
}
+15 -14
View File
@@ -1,30 +1,31 @@
<h2>$${{username}}</h2> <h2>{{username}}</h2>
<p>You can see this user because they have messages in a channel where {{access_reason}}.</p>
<dl> <dl>
<dt>User ID</dt><dd>$${{user_id}}</dd> <dt>User ID</dt><dd>{{user_id}}</dd>
<dt>Messages</dt><dd>$${{message_count}}</dd> <dt>Messages</dt><dd>{{message_count}}</dd>
<dt>Servers</dt><dd>$${{server_count}}</dd> <dt>Servers</dt><dd>{{server_count}}</dd>
<dt>Channels</dt><dd>$${{channel_count}}</dd> <dt>Channels</dt><dd>{{channel_count}}</dd>
<dt>Message versions</dt><dd>$${{version_count}}</dd> <dt>Message versions</dt><dd>{{version_count}}</dd>
<dt>Archived data</dt><dd>$${{total_storage}}</dd> <dt>Archived data</dt><dd>{{total_storage}}</dd>
<dt>Message content</dt><dd>$${{message_storage}}</dd> <dt>Message content</dt><dd>{{message_storage}}</dd>
<dt>Attachments</dt><dd>$${{attachment_storage}}</dd> <dt>Attachments</dt><dd>{{attachment_storage}}</dd>
</dl> </dl>
<form method="get" action="/users/$${{user_id}}/messages"> <form method="get" action="/users/{{user_id}}/messages">
<button type="submit">View messages by this user</button> <button type="submit">View messages by this user</button>
</form> </form>
<h3>Known usernames</h3> <h3>Known usernames</h3>
<ul> <ul>
$${{names}} {{names|safe}}
</ul> </ul>
<h3>Known avatars</h3> <h3>Known avatars</h3>
<ul> <ul>
$${{avatars}} {{avatars|safe}}
</ul> </ul>
<h3>Servers</h3> <h3>Servers</h3>
<ul> <ul>
$${{servers}} {{servers|safe}}
</ul> </ul>
<h3>Channels</h3> <h3>Channels</h3>
<ul> <ul>
$${{channels}} {{channels|safe}}
</ul> </ul>
+1 -1
View File
@@ -1 +1 @@
<li><a href="/users/$${{discord_id}}"><strong>$${{discord_username}}</strong> ($${{discord_id}})</a></li> <li><a href="/users/{{discord_id}}"><strong>{{discord_username}}</strong> ({{discord_id}})</a></li>