From 12fc3effaa7562ed260a0d091ab78f8d556bad62 Mon Sep 17 00:00:00 2001
From: sommerfeld <sommerfeld@sommerfeld.dev>
Date: Tue, 30 Apr 2024 15:10:01 +0100
Subject: Improve default ntfy configuration

---
 README.md           | 29 +++++++++++-------
 src/actions/ntfy.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 96 insertions(+), 18 deletions(-)

diff --git a/README.md b/README.md
index cff420e..7955571 100644
--- a/README.md
+++ b/README.md
@@ -136,31 +136,38 @@ accounts for some reason).
 This is the best straightforward way to get push notifications on a smartphone.
 
 1. Install the android/iOS app following the relevant links in https://ntfy.sh
-2. If you don't run your own ntfy self-hosted server, create an account at
-   ntfy.sh
-3. Open the app, give it the needed permissions and configure your account
-   credentials
-4. Click on the `+` button and create a "topic", preferably named `sentrum`
-   since that's what will be used by default.
+2. Open the app, give it the needed permissions
 
 Then you just need to add the relevant configuration:
 
 ```toml
 [[actions]]
 type =  "ntfy"
-# Credentials (required if you use a public server like the default one)
-credentials.username = "<YOUR USERNAME HERE>"
-credentials.password = "<YOUR PASSWORD HERE>"
+#
+# EVERYTHING BELOW IS OPTIONAL
+#
+# Credentials (optional, relevant for self-hosted instances or paid reserved topics)
+#credentials.username = "<YOUR USERNAME HERE>"
+#credentials.password = "<YOUR PASSWORD HERE>"
 # ntfy server (optional)
 #url = "https://ntfy.sh"
-# notification channel name (optional)
-#topic = "sentrum"
+# notification channel name (optional, defaults to random string for security)
+#topic = "<RANDOM TOPIC NAME>"
 # Proxy used to connect (optional, defaults to None)
 #proxy = "socks5://127.0.0.1:9050"
 # Priority ("max", "high", "default", "low", "min") (optional)
 #priority = "default"
 ```
 
+If you don't set a `topic = `, `sentrum` will auto-generate one for you randomly
+(since topic names are kind of like a password for the public default ntfy.sh
+server). When you later run `sentrum`, it will print out the topic name it's
+using.
+
+Open the ntfy app, click on the `+` button, create a "topic" and set the same
+equal to the one should be the `sentrum` logs.
+
+
 ### nostr
 
 Get notified by a nostr [NIP04 encrypted
diff --git a/src/actions/ntfy.rs b/src/actions/ntfy.rs
index badee70..98b630a 100644
--- a/src/actions/ntfy.rs
+++ b/src/actions/ntfy.rs
@@ -1,15 +1,76 @@
-use super::Action;
-use crate::message::MessageConfig;
-use crate::message::MessageFormat;
-use crate::message::MessageParams;
+use std::fs::File;
+use std::io::BufReader;
+use std::io::Write;
+use std::path::PathBuf;
+
+use anyhow::Context;
 use anyhow::Result;
 use async_trait::async_trait;
+use log::info;
 use ntfy::Auth;
 use ntfy::Dispatcher;
 use ntfy::Payload;
 use ntfy::Priority;
 use ntfy::Url;
+use rand::distributions::Alphanumeric;
+use rand::thread_rng;
+use rand::Rng;
 use serde::Deserialize;
+use serde::Serialize;
+use serde_json::from_reader;
+use serde_json::to_string;
+
+use super::Action;
+use crate::message::MessageConfig;
+use crate::message::MessageFormat;
+use crate::message::MessageParams;
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+struct NtfyData {
+    topic: String,
+}
+
+fn get_random_topic() -> String {
+    thread_rng()
+        .sample_iter(&Alphanumeric)
+        .take(16)
+        .map(char::from)
+        .collect()
+}
+
+impl Default for NtfyData {
+    fn default() -> Self {
+        NtfyData {
+            topic: get_random_topic(),
+        }
+    }
+}
+
+fn get_ntfy_data_filepath() -> PathBuf {
+    dirs::cache_dir()
+        .unwrap_or(PathBuf::from("cache"))
+        .join(env!("CARGO_PKG_NAME"))
+        .join("ntfy.json")
+}
+
+fn get_ntfy_topic() -> Result<String> {
+    let path = get_ntfy_data_filepath();
+    Ok(match File::open(&path) {
+        Ok(file) => {
+            let reader = BufReader::new(file);
+            from_reader(reader)
+                .with_context(|| format!("cannot read ntfy data from '{}'", path.display()))
+        }
+        Err(_) => {
+            let ntfy_data = NtfyData::default();
+            let mut file = File::create(&path)?;
+            file.write_all(to_string(&ntfy_data)?.as_bytes())
+                .with_context(|| format!("could not write ntfy data to '{}'", path.display()))?;
+            Ok(ntfy_data)
+        }
+    }?
+    .topic)
+}
 
 #[derive(Deserialize, Debug)]
 #[serde(remote = "Priority")]
@@ -49,8 +110,8 @@ impl NtfyConfig {
         self.url.as_deref().unwrap_or("https://ntfy.sh")
     }
 
-    pub fn topic(&self) -> &str {
-        self.topic.as_deref().unwrap_or(env!("CARGO_PKG_NAME"))
+    pub fn topic(&self) -> Option<&str> {
+        self.topic.as_deref()
     }
 }
 
@@ -71,7 +132,17 @@ impl<'a> NtfyAction<'a> {
             dispatcher_builder = dispatcher_builder.proxy(proxy);
         }
 
-        let mut payload = Payload::new(ntfy_config.topic())
+        let topic = ntfy_config
+            .topic()
+            .unwrap_or(&get_ntfy_topic()?)
+            .to_string();
+        info!(
+            "[ntfy] using topic '{}', connect to {}/{}",
+            topic,
+            ntfy_config.url(),
+            topic
+        );
+        let mut payload = Payload::new(&topic)
             .markdown(match message_config.format() {
                 MessageFormat::Plain => false,
                 MessageFormat::Markdown => true,
-- 
cgit v1.2.3-70-g09d2