Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96a8c9ea8c | ||
|
|
39fdfd3cde | ||
|
|
9e01b515e9 | ||
|
|
5ffc8122d0 | ||
|
|
0ca1f9d916 | ||
|
|
afc9c16484 | ||
|
|
b5bca1c59a | ||
|
|
48408cba61 |
No files matched your search
+7
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tg-archive-bot"
|
name = "tg-archive-bot"
|
||||||
version = "0.1.0"
|
version = "0.2.3"
|
||||||
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"
|
||||||
@@ -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`.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[general]
|
||||||
|
dirs = ["static"]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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\nArchived data: {}\nMessage content: {}\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
@@ -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)
|
||||||
}
|
}
|
||||||
+1547
-421
File diff suppressed because it is too large.
Load diff
@@ -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 +0,0 @@
|
|||||||
— $${{value}}
|
|
||||||
@@ -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
@@ -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
@@ -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 @@
|
|||||||
<strong>$${{page_number}}</strong>
|
<strong>{{page_number}}</strong>
|
||||||
@@ -1 +0,0 @@
|
|||||||
<pre>$${{description}}</pre>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<strong>$${{title}}</strong>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<a href="$${{url}}">$${{url}}</a>
|
|
||||||
+1
-1
@@ -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
@@ -1,2 +1,2 @@
|
|||||||
<h2>Error</h2>
|
<h2>Error</h2>
|
||||||
<p>$${{message}}</p>
|
<p>{{message}}</p>
|
||||||
+6
-4
@@ -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>Archived data</dt><dd>{{total_storage}}</dd>
|
||||||
<dt>Messages and metadata</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>
|
||||||
|
|
||||||
|
<p>If you cannot see any messages, try and send a message on a server the bot is in.</p>
|
||||||
+5
-5
@@ -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}}
|
||||||
@@ -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 @@
|
|||||||
<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>
|
||||||
@@ -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}}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -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>
|
||||||
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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 @@
|
|||||||
— <a href="$${{icon_url}}">icon</a>
|
- <a href="{{icon_url}}">icon</a>
|
||||||
+14
-14
@@ -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
@@ -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 @@
|
|||||||
<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 @@
|
|||||||
<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 @@
|
|||||||
<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 @@
|
|||||||
<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 @@
|
|||||||
<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 @@
|
|||||||
<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>
|
||||||
@@ -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
@@ -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
@@ -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>
|
||||||
Reference in new issue
Block a user