about summary refs log tree commit diff
path: root/kittybox-rs/src/bin
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-05-24 17:18:30 +0300
committerVika <vika@fireburn.ru>2022-05-24 17:18:30 +0300
commit5610a5f0bf1a9df02bd3d5b55e2cdebef2440360 (patch)
tree8394bcf1dcc204043d7adeb8dde2e2746977606e /kittybox-rs/src/bin
parent2f93873122b47e42f7ee1c38f1f04d052a63599c (diff)
downloadkittybox-5610a5f0bf1a9df02bd3d5b55e2cdebef2440360.tar.zst
flake.nix: reorganize
 - Kittybox's source code is moved to a subfolder
   - This improves build caching by Nix since it doesn't take changes
     to other files into account
 - Package and test definitions were spun into separate files
   - This makes my flake.nix much easier to navigate
   - This also makes it somewhat possible to use without flakes (but
     it is still not easy, so use flakes!)
 - Some attributes were moved in compliance with Nix 2.8's changes to
   flake schema
Diffstat (limited to 'kittybox-rs/src/bin')
-rw-r--r--kittybox-rs/src/bin/kittybox_bulk_import.rs66
-rw-r--r--kittybox-rs/src/bin/kittybox_database_converter.rs106
-rw-r--r--kittybox-rs/src/bin/pyindieblog_to_kittybox.rs68
3 files changed, 240 insertions, 0 deletions
diff --git a/kittybox-rs/src/bin/kittybox_bulk_import.rs b/kittybox-rs/src/bin/kittybox_bulk_import.rs
new file mode 100644
index 0000000..7e1f6af
--- /dev/null
+++ b/kittybox-rs/src/bin/kittybox_bulk_import.rs
@@ -0,0 +1,66 @@
+use anyhow::{anyhow, bail, Context, Result};
+use std::fs::File;
+use std::io;
+
+#[async_std::main]
+async fn main() -> Result<()> {
+    let args = std::env::args().collect::<Vec<String>>();
+    if args.iter().skip(1).any(|s| s == "--help") {
+        println!("Usage: {} <url> [file]", args[0]);
+        println!("\nIf launched with no arguments, reads from stdin.");
+        println!(
+            "\nUse KITTYBOX_AUTH_TOKEN environment variable to authorize to the Micropub endpoint."
+        );
+        std::process::exit(0);
+    }
+
+    let token = std::env::var("KITTYBOX_AUTH_TOKEN")
+        .map_err(|_| anyhow!("No auth token found! Use KITTYBOX_AUTH_TOKEN env variable."))?;
+    let data: Vec<serde_json::Value> = (if args.len() == 2 || (args.len() == 3 && args[2] == "-") {
+        serde_json::from_reader(io::stdin())
+    } else if args.len() == 3 {
+        serde_json::from_reader(File::open(&args[2]).with_context(|| "Error opening input file")?)
+    } else {
+        bail!("See `{} --help` for usage.", args[0]);
+    })
+    .with_context(|| "Error while loading the input file")?;
+
+    let url = surf::Url::parse(&args[1])?;
+    let client = surf::Client::new();
+
+    let iter = data.into_iter();
+
+    for post in iter {
+        println!(
+            "Processing {}...",
+            post["properties"]["url"][0]
+                .as_str()
+                .or_else(|| post["properties"]["published"][0]
+                    .as_str()
+                    .or_else(|| post["properties"]["name"][0]
+                        .as_str()
+                        .or(Some("<unidentified post>"))))
+                .unwrap()
+        );
+        match client
+            .post(&url)
+            .body(surf::http::Body::from_string(serde_json::to_string(&post)?))
+            .header("Content-Type", "application/json")
+            .header("Authorization", format!("Bearer {}", &token))
+            .send()
+            .await
+        {
+            Ok(mut response) => {
+                if response.status() == 201 || response.status() == 202 {
+                    println!("Posted at {}", response.header("location").unwrap().last());
+                } else {
+                    println!("Error: {:?}", response.body_string().await);
+                }
+            }
+            Err(err) => {
+                println!("{}", err);
+            }
+        }
+    }
+    Ok(())
+}
diff --git a/kittybox-rs/src/bin/kittybox_database_converter.rs b/kittybox-rs/src/bin/kittybox_database_converter.rs
new file mode 100644
index 0000000..bc355c9
--- /dev/null
+++ b/kittybox-rs/src/bin/kittybox_database_converter.rs
@@ -0,0 +1,106 @@
+use anyhow::{anyhow, Context};
+use kittybox::database::FileStorage;
+use kittybox::database::Storage;
+use redis::{self, AsyncCommands};
+use std::collections::HashMap;
+
+/// Convert from a Redis storage to a new storage new_storage.
+async fn convert_from_redis<S: Storage>(from: String, new_storage: S) -> anyhow::Result<()> {
+    let db = redis::Client::open(from).context("Failed to open the Redis connection")?;
+
+    let mut conn = db
+        .get_async_std_connection()
+        .await
+        .context("Failed to connect to Redis")?;
+
+    // Rebinding to convince the borrow checker we're not smuggling stuff outta scope
+    let storage = &new_storage;
+
+    let mut stream = conn.hscan::<_, String>("posts").await?;
+
+    while let Some(key) = stream.next_item().await {
+        let value = serde_json::from_str::<serde_json::Value>(
+            &stream
+                .next_item()
+                .await
+                .ok_or(anyhow!("Failed to find a corresponding value for the key"))?,
+        )?;
+
+        println!("{}, {:?}", key, value);
+
+        if value["see_other"].is_string() {
+            continue;
+        }
+
+        let user = &(url::Url::parse(value["properties"]["uid"][0].as_str().unwrap())
+            .unwrap()
+            .origin()
+            .ascii_serialization()
+            .clone()
+            + "/");
+        if let Err(err) = storage.clone().put_post(&value, user).await {
+            eprintln!("Error saving post: {}", err);
+        }
+    }
+
+    let mut stream: redis::AsyncIter<String> = conn.scan_match("settings_*").await?;
+    while let Some(key) = stream.next_item().await {
+        let mut conn = db
+            .get_async_std_connection()
+            .await
+            .context("Failed to connect to Redis")?;
+        let user = key.strip_prefix("settings_").unwrap();
+        match conn
+            .hgetall::<&str, HashMap<String, String>>(&key)
+            .await
+            .context(format!("Failed getting settings from key {}", key))
+        {
+            Ok(settings) => {
+                for (k, v) in settings.iter() {
+                    if let Err(e) = storage
+                        .set_setting(k, user, v)
+                        .await
+                        .with_context(|| format!("Failed setting {} for {}", k, user))
+                    {
+                        eprintln!("{}", e);
+                    }
+                }
+            }
+            Err(e) => {
+                eprintln!("{}", e);
+            }
+        }
+    }
+
+    Ok(())
+}
+
+#[async_std::main]
+async fn main() -> anyhow::Result<()> {
+    let mut args = std::env::args();
+    args.next(); // skip argv[0]
+    let old_uri = args
+        .next()
+        .ok_or_else(|| anyhow!("No import source is provided."))?;
+    let new_uri = args
+        .next()
+        .ok_or_else(|| anyhow!("No import destination is provided."))?;
+
+    let storage = if new_uri.starts_with("file:") {
+        let folder = new_uri.strip_prefix("file://").unwrap();
+        let path = std::path::PathBuf::from(folder);
+        Box::new(
+            FileStorage::new(path)
+                .await
+                .context("Failed to construct the file storage")?,
+        )
+    } else {
+        anyhow::bail!("Cannot construct the storage abstraction for destination storage. Check the storage type?");
+    };
+
+    if old_uri.starts_with("redis") {
+        convert_from_redis(old_uri, *storage).await?
+    }
+
+    Ok(())
+}
diff --git a/kittybox-rs/src/bin/pyindieblog_to_kittybox.rs b/kittybox-rs/src/bin/pyindieblog_to_kittybox.rs
new file mode 100644
index 0000000..38590c3
--- /dev/null
+++ b/kittybox-rs/src/bin/pyindieblog_to_kittybox.rs
@@ -0,0 +1,68 @@
+use anyhow::{anyhow, Context, Result};
+
+use redis::AsyncCommands;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::fs::File;
+
+#[derive(Default, Serialize, Deserialize)]
+struct PyindieblogData {
+    posts: Vec<serde_json::Value>,
+    cards: Vec<serde_json::Value>,
+}
+
+#[async_std::main]
+async fn main() -> Result<()> {
+    let mut args = std::env::args();
+    args.next(); // skip argv[0] which is the name
+    let redis_uri = args
+        .next()
+        .ok_or_else(|| anyhow!("No Redis URI provided"))?;
+    let client = redis::Client::open(redis_uri.as_str())
+        .with_context(|| format!("Failed to construct Redis client on {}", redis_uri))?;
+
+    let filename = args
+        .next()
+        .ok_or_else(|| anyhow!("No filename provided for export"))?;
+
+    let mut data: Vec<serde_json::Value>;
+
+    let file = File::create(filename)?;
+
+    let mut conn = client
+        .get_async_std_connection()
+        .await
+        .with_context(|| "Failed to connect to the Redis server")?;
+
+    data = conn
+        .hgetall::<&str, HashMap<String, String>>("posts")
+        .await?
+        .values()
+        .map(|s| {
+            serde_json::from_str::<serde_json::Value>(s)
+                .with_context(|| format!("Failed to parse the following entry: {:?}", s))
+        })
+        .collect::<std::result::Result<Vec<serde_json::Value>, anyhow::Error>>()
+        .with_context(|| "Failed to export h-entries from pyindieblog")?;
+    data.extend(
+        conn.hgetall::<&str, HashMap<String, String>>("hcards")
+            .await?
+            .values()
+            .map(|s| {
+                serde_json::from_str::<serde_json::Value>(s)
+                    .with_context(|| format!("Failed to parse the following card: {:?}", s))
+            })
+            .collect::<std::result::Result<Vec<serde_json::Value>, anyhow::Error>>()
+            .with_context(|| "Failed to export h-cards from pyindieblog")?,
+    );
+
+    data.sort_by_key(|v| {
+        v["properties"]["published"][0]
+            .as_str()
+            .map(|s| s.to_string())
+    });
+
+    serde_json::to_writer(file, &data)?;
+
+    Ok(())
+}