twtxt

a client for twtxt, the "decentralised, minimalist microblogging service for hackers"
Log | Files | Refs

commit 64f56a123ab612e2e3aa2d9ebbd779a1823deff5
Author: Chris Johns <chris@ter0.net>
Date:   Thu, 30 Apr 2020 09:53:21 +0100

Initial commit

Diffstat:
A.gitignore | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACargo.toml | 16++++++++++++++++
Asrc/cli.yml | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main.rs | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 372 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,143 @@ + +# Created by https://www.gitignore.io/api/vim,rust,linux,macos,windows,sublimetext +# Edit at https://www.gitignore.io/?templates=vim,rust,linux,macos,windows,sublimetext + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +### SublimeText ### +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist + +# Auto-generated tag files +tags + +# Persistent undo +[._]*.un~ + +# Coc configuration directory +.vim + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/vim,rust,linux,macos,windows,sublimetext + diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "twtxt" +version = "0.1.0" +authors = ["Chris Johns <chris@ter0.net>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { git = "https://github.com/clap-rs/clap/", features = ["yaml"]} +dirs = "2.0.2" +rust-ini = "0.15.2" +chrono = "0.4.11" +reqwest = { version = "0.10", features = ["blocking", "json"] } +regex = "1"+ \ No newline at end of file diff --git a/src/cli.yml b/src/cli.yml @@ -0,0 +1,50 @@ +name: twtxt +version: "1.0" +author: Chris J. <chris@ter0.net> +about: Decentralised, minimalist microblogging service for hackers. +settings: + - ArgRequiredElseHelp +args: + - config: + short: c + long: config + value_name: FILE + help: Specify a custom config file location. + takes_value: true +subcommands: + - follow: + about: Add a new source to your followings. + args: + - nick: + help: the nickname of the user + required: true + index: 1 + - source: + help: the url of the twtxt file + required: true + index: 2 + - following: + about: Return the list of sources you’re following + - timeline: + about: Retrieve your personal timeline + - tweet: + about: Append a new tweet to your twtxt file + args: + - message: + help: the message to tweet + required: true + index: 1 + - unfollow: + about: Remove an existing source from your list of sources + args: + - nick: + help: the nick of the source to remove + required: true + index: 1 + - view: + about: Show feed of given source + args: + - source: + help: the source to view + required: true + index: 1+ \ No newline at end of file diff --git a/src/main.rs b/src/main.rs @@ -0,0 +1,162 @@ +use chrono; +use clap::{load_yaml, App}; +use dirs; +use ini; +use regex::Regex; +use std::fs; +use std::io; +use std::io::Write; +use std::path; +use std::process; + +struct Config { + ini: ini::Ini, + file_path: path::PathBuf, +} + +impl Config { + fn new() -> Config { + let config_dir = dirs::config_dir().unwrap().join("twtxt"); + let config_file = config_dir.join("config"); + + let ini_file = ini::Ini::load_from_file(&config_file).unwrap_or_else(|error| match error { + ini::ini::Error::Io(_e) => { + fs::create_dir_all(&config_dir).unwrap(); + ini::Ini::new() + } + _ => { + eprintln!("unusable config file"); + process::exit(1) + } + }); + + Config { + ini: ini_file, + file_path: config_file, + } + } + + fn write(&self) { + self.ini.write_to_file(&self.file_path).unwrap(); + } +} + +struct Tweet<'a> { + message: &'a str, + timestamp: chrono::DateTime<chrono::Local>, +} + +impl<'a> Tweet<'a> { + fn new(message: &'a str) -> Tweet<'a> { + Tweet { + message: &message, + timestamp: chrono::Local::now(), + } + } + + fn write(&self) { + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("/Users/ter0/twtxt.txt") + .unwrap(); + if let Err(e) = writeln!(file, "{}\t{}", self.timestamp.to_rfc3339(), self.message) { + eprintln!("Couldn't write to file: {}", e); + } + } +} + +fn follow(nick: &str, source: &str) { + let mut cfg = Config::new(); + + match cfg.ini.get_from(Some("following"), nick) { + Some(_source) => { + print!("➤ You’re already following {}. Overwrite? [y/N]: ", nick); + io::stdout().flush().unwrap(); + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + if input.trim().to_lowercase() == "y" { + cfg.ini.with_section(Some("following")).set(nick, source); + println!("✓ You’re now following {}.", nick); + } + } + None => { + cfg.ini.with_section(Some("following")).set(nick, source); + println!("✓ You’re now following {}.", nick); + } + } + + cfg.write(); +} + +fn following() { + let cfg = Config::new(); + let following = cfg.ini.section(Some("following")).unwrap(); + for (nick, source) in following.iter() { + println!("➤ {} @ {} [0B] (200)", nick, source); + } +} + +fn timeline() { + println!("timeline") +} + +fn unfollow(nick: &str) { + let mut cfg = Config::new(); + + match cfg.ini.get_from(Some("following"), nick) { + Some(_source) => { + cfg.ini.delete_from(Some("following"), nick); + println!("✓ You’ve unfollowed {}.", nick); + } + None => { + println!("✗ You’re not following {}.", nick); + } + } + + cfg.write(); +} + +fn view(source: &str) { + let res = reqwest::blocking::get(source).unwrap(); + let body = res.text().unwrap(); + let regex = Regex::new(r"(?m)^(.*?)\t(.*)$").unwrap(); + + for caps in regex.captures_iter(&body) { + println!("➤ {} ({}):", source, caps.get(1).unwrap().as_str()); + println!("{}\n", caps.get(2).unwrap().as_str()); + } +} + +fn main() { + let yaml = load_yaml!("cli.yml"); + let matches = App::from(yaml).get_matches(); + + match matches.subcommand() { + ("follow", Some(follow_match)) => { + let nick = follow_match.value_of("nick").unwrap(); + let source = follow_match.value_of("source").unwrap(); + follow(&nick, &source); + } + ("following", Some(_following_match)) => { + following(); + } + ("timeline", Some(_timeline_match)) => { + timeline(); + } + ("tweet", Some(tweet_match)) => { + let message = tweet_match.value_of("message").unwrap(); + let tweet = Tweet::new(message); + tweet.write(); + } + ("unfollow", Some(unfollow_match)) => { + let nick = unfollow_match.value_of("nick").unwrap(); + unfollow(&nick); + } + ("view", Some(view_match)) => { + let source = view_match.value_of("source").unwrap(); + view(&source); + } + _ => unreachable!(), + } +}