6 Commits
Author SHA1 Message Date
Elias Wendland 96a8c9ea8c Release 0.2.3
CI and release / Detect release commit (push) Successful in 17s
CI and release / Run tests (push) Successful in 4m23s
CI and release / Build and publish container (push) Successful in 8m9s
CI and release / Create release (push) Successful in 20s
2026-08-30 22:40:41 +02:00
Elias Wendland 39fdfd3cde Fix new channels not being seen if your session hasn't expired yet
CI and release / Detect release commit (push) Successful in 19s
CI and release / Run tests (push) Successful in 3m32s
CI and release / Build and publish container (push) Successful in 6m55s
CI and release / Create release (push) Skipped
2026-08-30 22:40:16 +02:00
Elias Wendland 9e01b515e9 Fix a test that didn't pass due to to fixing the git link
CI and release / Detect release commit (push) Successful in 20s
CI and release / Run tests (push) Successful in 3m7s
CI and release / Build and publish container (push) Successful in 5m57s
CI and release / Create release (push) Skipped
2026-08-30 18:57:44 +02:00
Elias Wendland 5ffc8122d0 Make storage stats consistent
CI and release / Detect release commit (push) Successful in 11s
CI and release / Run tests (push) Failing after 3m7s
CI and release / Build and publish container (push) Successful in 5m47s
CI and release / Create release (push) Skipped
2026-08-30 18:42:30 +02:00
Elias Wendland 0ca1f9d916 Release 0.2.2
CI and release / Detect release commit (push) Successful in 29s
CI and release / Run tests (push) Failing after 4m57s
CI and release / Build and publish container (push) Successful in 8m45s
CI and release / Create release (push) Successful in 25s
2026-08-30 16:59:00 +02:00
Elias Wendland afc9c16484 Fixed incorrect git link
CI and release / Detect release commit (push) Successful in 42s
CI and release / Run tests (push) Failing after 4m59s
CI and release / Build and publish container (push) Successful in 7m59s
CI and release / Create release (push) Skipped
2026-08-30 16:58:38 +02:00
4 changed files with 103 additions and 31 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tg-archive-bot"
version = "0.2.1"
version = "0.2.3"
edition = "2024"
license = "GPL-3.0-or-later"
description = "Bot for archiving messages in TeenGovernment and related servers"
+1 -1
View File
@@ -12,7 +12,7 @@ pub async fn stats(ctx: Context<'_>) -> Result<(), Error> {
);
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: {}",
"# Archive Statistics\n## Messages\nArchived messages: {}\nArchived users: {}\nChannels: {}\nServers: {}\n## Storage usage\nArchived data: {}\nMessage content: {}\nAttachments: {}",
stats.messages,
stats.users,
stats.channels,
+99 -27
View File
@@ -12,6 +12,7 @@ use rand::{Rng, distr::Alphanumeric};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::{HashMap, HashSet};
use std::time::{SystemTime, UNIX_EPOCH};
use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer, cookie::time::Duration};
use tracing::{error, info};
@@ -21,6 +22,7 @@ tokio::task_local! {
const ITEMS_PER_PAGE: i64 = 100;
const SHOWN_PAGES: i64 = 10;
const CHANNEL_ACCESS_TTL_SECONDS: u64 = 5 * 60;
#[derive(Clone)]
struct WebData {
@@ -38,6 +40,8 @@ struct WebUser {
username: String,
channel_ids: Vec<i64>,
channel_access: Vec<ChannelAccess>,
#[serde(default)]
channel_access_refreshed_at: u64,
}
#[derive(Clone, Deserialize, Serialize)]
@@ -603,7 +607,26 @@ async fn require_user(
next: Next,
) -> Response {
match session.get::<WebUser>("user").await {
Ok(Some(user)) => {
Ok(Some(mut user)) => {
if channel_access_needs_refresh(&user, request.uri().path()) {
let channel_access = match accessible_channels(&data, user.id).await {
Ok(channel_access) => channel_access,
Err(error) => {
error!("Discord permission refresh failed: {}", error);
return StatusCode::BAD_GATEWAY.into_response();
}
};
user.channel_ids = channel_access
.iter()
.map(|access| access.channel_id)
.collect();
user.channel_access = channel_access;
user.channel_access_refreshed_at = unix_timestamp();
if let Err(error) = session.insert("user", &user).await {
error!("Could not update refreshed session permissions: {}", error);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
}
if !request_is_allowed(&data.pool, &user, request.uri().path()).await {
return StatusCode::NOT_FOUND.into_response();
}
@@ -618,6 +641,34 @@ async fn require_user(
}
}
fn unix_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn channel_access_needs_refresh_at(user: &WebUser, path: &str, now: u64) -> bool {
let expired =
now.saturating_sub(user.channel_access_refreshed_at) >= CHANNEL_ACCESS_TTL_SECONDS;
let requested_channel_is_missing = path
.trim_matches('/')
.split('/')
.collect::<Vec<_>>()
.as_slice()
.get(0..2)
.and_then(|parts| match parts {
["channels", id] => id.parse::<i64>().ok(),
_ => None,
})
.is_some_and(|channel_id| !user.channel_ids.contains(&channel_id));
expired || requested_channel_is_missing
}
fn channel_access_needs_refresh(user: &WebUser, path: &str) -> bool {
channel_access_needs_refresh_at(user, path, unix_timestamp())
}
async fn request_is_allowed(pool: &PgPool, user: &WebUser, path: &str) -> bool {
let parts = path.trim_matches('/').split('/').collect::<Vec<_>>();
let channel_id = match parts.as_slice() {
@@ -801,6 +852,7 @@ async fn discord_callback(
username: user.username,
channel_ids,
channel_access,
channel_access_refreshed_at: unix_timestamp(),
};
if session.cycle_id().await.is_err() {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
@@ -1072,32 +1124,14 @@ pub async fn archive_stats(pool: &PgPool) -> Result<ArchiveStats, sqlx::Error> {
(SELECT COUNT(*) FROM discord_users),
(SELECT COUNT(*) FROM guilds),
(SELECT COUNT(*) FROM channels),
pg_total_relation_size('guilds')
+ pg_total_relation_size('channels')
+ pg_total_relation_size('discord_users')
+ pg_total_relation_size('discord_roles')
+ pg_total_relation_size('messages')
+ pg_total_relation_size('message_versions')
+ pg_total_relation_size('attachments')
+ pg_total_relation_size('embeds')
+ pg_total_relation_size('embed_fields')
+ pg_total_relation_size('guild_history')
+ pg_total_relation_size('channel_history')
+ pg_total_relation_size('discord_user_history')
+ pg_total_relation_size('guild_users'),
pg_total_relation_size('messages')
+ pg_total_relation_size('message_versions')
+ pg_total_relation_size('guilds')
+ pg_total_relation_size('channels')
+ pg_total_relation_size('discord_users')
+ pg_total_relation_size('discord_roles')
+ pg_total_relation_size('embeds')
+ pg_total_relation_size('embed_fields')
+ pg_total_relation_size('guild_history')
+ pg_total_relation_size('channel_history')
+ pg_total_relation_size('discord_user_history')
+ pg_total_relation_size('guild_users'),
pg_total_relation_size('attachments');",
(SELECT COALESCE(SUM(OCTET_LENGTH(content)), 0)
FROM message_versions)
+ (SELECT COALESCE(SUM(OCTET_LENGTH(data)), 0)
FROM attachments),
(SELECT COALESCE(SUM(OCTET_LENGTH(content)), 0)
FROM message_versions),
(SELECT COALESCE(SUM(OCTET_LENGTH(data)), 0)
FROM attachments);",
)
.fetch_one(pool)
.await?;
@@ -2655,6 +2689,44 @@ mod tests {
assert_eq!(Theme::from_cookie(Some("unknown")).as_str(), "white");
}
#[test]
fn refreshes_expired_or_legacy_channel_access() {
let mut user = WebUser {
id: 1,
username: "user".into(),
channel_ids: vec![10],
channel_access: Vec::new(),
channel_access_refreshed_at: 1_000,
};
assert!(!channel_access_needs_refresh_at(&user, "/channels", 1_299));
assert!(channel_access_needs_refresh_at(&user, "/channels", 1_300));
user.channel_access_refreshed_at = 0;
assert!(channel_access_needs_refresh_at(&user, "/channels", 1_000));
}
#[test]
fn refreshes_when_a_directly_requested_channel_is_not_cached() {
let user = WebUser {
id: 1,
username: "user".into(),
channel_ids: vec![10],
channel_access: Vec::new(),
channel_access_refreshed_at: 1_000,
};
assert!(!channel_access_needs_refresh_at(
&user,
"/channels/10/messages",
1_001
));
assert!(channel_access_needs_refresh_at(
&user,
"/channels/20/messages",
1_001
));
}
#[test]
fn shared_page_contains_the_footer_and_theme_switcher() {
let html = render_page("Archive", "Content", true);
+2 -2
View File
@@ -2,8 +2,8 @@
<p>Archived {{message_count}} messages from {{user_count}} users across {{channel_count}} channels on {{server_count}} servers.</p>
<h3>Storage usage</h3>
<dl>
<dt>Total archive</dt><dd>{{total_storage}}</dd>
<dt>Messages and metadata</dt><dd>{{message_storage}}</dd>
<dt>Archived data</dt><dd>{{total_storage}}</dd>
<dt>Message content</dt><dd>{{message_storage}}</dd>
<dt>Attachments</dt><dd>{{attachment_storage}}</dd>
</dl>