commit 64f56a123ab612e2e3aa2d9ebbd779a1823deff5
Author: Chris Johns <chris@ter0.net>
Date: Thu, 30 Apr 2020 09:53:21 +0100
Initial commit
Diffstat:
A | .gitignore | | | 143 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | Cargo.toml | | | 16 | ++++++++++++++++ |
A | src/cli.yml | | | 51 | +++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/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!(),
+ }
+}