Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions migrations/021_update_notifications.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Remembers which mod updates have already been announced, so a restart (or the
-- next poll) does not re-notify about the same version.
CREATE TABLE IF NOT EXISTS update_notifications (
mod_db_id INTEGER NOT NULL,
version TEXT NOT NULL,
notified_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (mod_db_id, version),
FOREIGN KEY (mod_db_id) REFERENCES installed_mods(id) ON DELETE CASCADE
);
3 changes: 2 additions & 1 deletion src/cli/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,10 @@ pub async fn drain_all(ctx: &CliContext) -> Result<usize> {
&ctx.dirs,
&ctx.config,
installed.id,
version_id,
Some(version_id),
&version_str,
archive,
None,
) {
let remaining = pending.len() - applied - 1;
eprintln!("\n Error: {e:#}");
Expand Down
6 changes: 2 additions & 4 deletions src/cli/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ mod tests {

// Create minimum SPT structure at root (legacy layout)
std::fs::create_dir_all(spt_dir.join("SPT")).unwrap();
std::fs::write(spt_dir.join("SPT/SPT.Server.exe"), b"").unwrap();
std::fs::write(spt_dir.join("SPT/SPT.Server.deps.json"), b"{}").unwrap();
let configs_dir = spt_dir.join("SPT/SPT_Data/configs");
std::fs::create_dir_all(&configs_dir).unwrap();
std::fs::write(
Expand All @@ -707,7 +707,6 @@ mod tests {
)
.unwrap();
std::fs::create_dir_all(spt_dir.join("SPT/user/mods")).unwrap();
std::fs::create_dir_all(spt_dir.join("BepInEx/plugins")).unwrap();

let state = classify_directory(spt_dir).unwrap();
assert!(matches!(state, DirState::ExistingSptLegacy));
Expand All @@ -721,7 +720,7 @@ mod tests {

// Create minimum SPT structure in spt-server/ subdir (new layout)
std::fs::create_dir_all(spt_dir.join("SPT")).unwrap();
std::fs::write(spt_dir.join("SPT/SPT.Server.exe"), b"").unwrap();
std::fs::write(spt_dir.join("SPT/SPT.Server.deps.json"), b"{}").unwrap();
let configs_dir = spt_dir.join("SPT/SPT_Data/configs");
std::fs::create_dir_all(&configs_dir).unwrap();
std::fs::write(
Expand All @@ -730,7 +729,6 @@ mod tests {
)
.unwrap();
std::fs::create_dir_all(spt_dir.join("SPT/user/mods")).unwrap();
std::fs::create_dir_all(spt_dir.join("BepInEx/plugins")).unwrap();

let state = classify_directory(quma_root).unwrap();
assert!(matches!(state, DirState::ExistingSptNew));
Expand Down
3 changes: 2 additions & 1 deletion src/cli/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,10 @@ pub async fn apply_update_by_version(
&ctx.dirs,
&ctx.config,
installed.id,
target_version_id,
Some(target_version_id),
&version_info.version,
&archive_path,
None,
)?;

let file_count = ctx.db.get_files_for_mod(installed.id)?.len();
Expand Down
18 changes: 18 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ fn default_session_secret() -> String {
String::new()
}

fn default_update_notify_interval() -> u64 {
1800
}

fn default_update_check_interval() -> u64 {
300
}
Expand Down Expand Up @@ -935,6 +939,16 @@ pub struct Config {
#[serde(default = "default_update_check_interval")]
pub update_check_interval: u64,

/// Discord webhook URL for announcing newly available mod updates.
/// Unset = the background update poller does not run at all.
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub discord_webhook_url: Option<String>,

/// How often the poller checks Forge and GitHub for updates, in seconds.
#[serde(default = "default_update_notify_interval")]
pub update_notify_interval: u64,

#[serde(default)]
pub update_disabled_mods: bool,

Expand Down Expand Up @@ -1029,6 +1043,8 @@ impl Default for Config {
web_port: 9190,
web_workers: None,
update_check_interval: 300,
discord_webhook_url: None,
update_notify_interval: 1800,
update_disabled_mods: false,
forge_cache_ttl: Some(86400),
headless: None,
Expand Down Expand Up @@ -1217,6 +1233,8 @@ impl Config {
env_override!(opt_parse: self.server_port, "QUMA_SERVER_PORT", u16);
env_override!(parse: self.container_stop_timeout, "QUMA_CONTAINER_STOP_TIMEOUT", u64);
env_override!(parse: self.update_check_interval, "QUMA_UPDATE_CHECK_INTERVAL", u64);
env_override!(opt_str: self.discord_webhook_url, "QUMA_DISCORD_WEBHOOK_URL");
env_override!(parse: self.update_notify_interval, "QUMA_UPDATE_NOTIFY_INTERVAL", u64);
env_override!(opt_parse: self.forge_cache_ttl, "QUMA_FORGE_CACHE_TTL", u64);
env_override!(bool: self.auto_start_server, "QUMA_AUTO_START_SERVER");
env_override!(parse: self.on_exit, "QUMA_ON_EXIT", OnExit);
Expand Down
34 changes: 34 additions & 0 deletions src/db/mods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,40 @@ impl Database {
)
}

/// Has this mod-version already been announced? Keeps the update poller from
/// repeating itself on every tick, and across restarts.
pub fn was_update_notified(&self, mod_db_id: i64, version: &str) -> rusqlite::Result<bool> {
self.conn
.query_row(
"SELECT 1 FROM update_notifications WHERE mod_db_id = ?1 AND version = ?2",
params![mod_db_id, version],
|_| Ok(()),
)
.optional()
.map(|hit| hit.is_some())
}

pub fn mark_update_notified(&self, mod_db_id: i64, version: &str) -> rusqlite::Result<usize> {
self.conn.execute(
"INSERT OR IGNORE INTO update_notifications (mod_db_id, version) VALUES (?1, ?2)",
params![mod_db_id, version],
)
}

/// Record a new version for a mod that came from a URL rather than Forge.
pub fn update_mod_source(
&self,
id: i64,
version: &str,
source_url: &str,
) -> rusqlite::Result<usize> {
self.conn.execute(
"UPDATE installed_mods SET version = ?1, source_url = ?2, updated_at = datetime('now')
WHERE id = ?3",
params![version, source_url, id],
)
}

pub fn delete_mod(&self, id: i64) -> rusqlite::Result<usize> {
self.conn
.execute("DELETE FROM installed_mods WHERE id = ?1", params![id])
Expand Down
15 changes: 5 additions & 10 deletions src/dirs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,7 @@ mod tests {
let dirs = QumaDirs::from_root(tmp.path().to_path_buf());
std::fs::create_dir_all(dirs.spt_server.join("SPT/SPT_Data/configs")).unwrap();
std::fs::create_dir_all(dirs.spt_server.join("SPT/user/mods")).unwrap();
std::fs::create_dir_all(dirs.spt_server.join("BepInEx/plugins")).unwrap();
std::fs::write(dirs.spt_server.join("SPT/SPT.Server.exe"), "").unwrap();
std::fs::write(dirs.spt_server.join("SPT/SPT.Server.deps.json"), "{}").unwrap();
std::fs::write(dirs.spt_server.join("SPT/SPT_Data/configs/core.json"), "{}").unwrap();

assert!(crate::spt::detect::validate_spt_dir(&dirs.spt_server).is_ok());
Expand All @@ -494,8 +493,7 @@ mod tests {
let spt = root.join("spt-server");
std::fs::create_dir_all(spt.join("SPT/SPT_Data/configs")).unwrap();
std::fs::create_dir_all(spt.join("SPT/user/mods")).unwrap();
std::fs::create_dir_all(spt.join("BepInEx/plugins")).unwrap();
std::fs::write(spt.join("SPT/SPT.Server.exe"), "").unwrap();
std::fs::write(spt.join("SPT/SPT.Server.deps.json"), "{}").unwrap();
std::fs::write(spt.join("SPT/SPT_Data/configs/core.json"), "{}").unwrap();

let dirs = QumaDirs::detect(Some(root), None).unwrap();
Expand All @@ -509,8 +507,7 @@ mod tests {
let root = tmp.path();
std::fs::create_dir_all(root.join("SPT/SPT_Data/configs")).unwrap();
std::fs::create_dir_all(root.join("SPT/user/mods")).unwrap();
std::fs::create_dir_all(root.join("BepInEx/plugins")).unwrap();
std::fs::write(root.join("SPT/SPT.Server.exe"), "").unwrap();
std::fs::write(root.join("SPT/SPT.Server.deps.json"), "{}").unwrap();
std::fs::write(root.join("SPT/SPT_Data/configs/core.json"), "{}").unwrap();

let dirs = QumaDirs::detect(Some(root), None).unwrap();
Expand All @@ -526,8 +523,7 @@ mod tests {
let spt = root.join("spt-server");
std::fs::create_dir_all(spt.join("SPT/SPT_Data/configs")).unwrap();
std::fs::create_dir_all(spt.join("SPT/user/mods")).unwrap();
std::fs::create_dir_all(spt.join("BepInEx/plugins")).unwrap();
std::fs::write(spt.join("SPT/SPT.Server.exe"), "").unwrap();
std::fs::write(spt.join("SPT/SPT.Server.deps.json"), "{}").unwrap();
std::fs::write(spt.join("SPT/SPT_Data/configs/core.json"), "{}").unwrap();

temp_env::with_vars(
Expand Down Expand Up @@ -587,8 +583,7 @@ mod tests {
let root = tmp.path();
std::fs::create_dir_all(root.join("SPT/SPT_Data/configs")).unwrap();
std::fs::create_dir_all(root.join("SPT/user/mods")).unwrap();
std::fs::create_dir_all(root.join("BepInEx/plugins")).unwrap();
std::fs::write(root.join("SPT/SPT.Server.exe"), "").unwrap();
std::fs::write(root.join("SPT/SPT.Server.deps.json"), "{}").unwrap();
std::fs::write(root.join("SPT/SPT_Data/configs/core.json"), "{}").unwrap();

temp_env::with_vars(
Expand Down
Loading
Loading