Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96a8c9ea8c | ||
|
|
39fdfd3cde | ||
|
|
9e01b515e9 | ||
|
|
5ffc8122d0 |
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tg-archive-bot"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "Bot for archiving messages in TeenGovernment and related servers"
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ pub async fn git(ctx: Context<'_>) -> Result<(), Error> {
|
||||
|
||||
let response = ctx.say("Processing...").await?;
|
||||
|
||||
let msg = "Git repo: https://git.ewenlau.net/ewenlau/tg-archive\nIf you'd like to contribute, contact <@1389325880853270569> to get an account.";
|
||||
let msg = "Git repo: https://git.ewenlau.net/ewenlau/tg-archive-bot\nIf you'd like to contribute, contact <@1389325880853270569> to get an account.";
|
||||
edit_response_message(&response, ctx, msg, true).await?;
|
||||
trace_message(
|
||||
msg,
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
{{ body|safe }}
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<p class="footer-legal">Source code available <a href="https://git.ewenlau.net/ewenlau/tg-archive">here</a>.<br>
|
||||
<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">
|
||||
|
||||
Reference in new issue
Block a user