Initial commit: working single-post application
This commit is contained in:
		
						commit
						7e61db4f3c
					
				| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
/target
 | 
			
		||||
.vscode
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "izzilis"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
authors = ["Emilis <grinding@graduate.org>"]
 | 
			
		||||
edition = "2018"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
elefren = { version = "0.22.0", features = ["toml"] }
 | 
			
		||||
rand = "0.8.4"
 | 
			
		||||
serde = "1.0.126"
 | 
			
		||||
serde_json = "1.0.64"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,106 @@
 | 
			
		|||
use rand::Rng;
 | 
			
		||||
use std::{error::Error, io};
 | 
			
		||||
 | 
			
		||||
use crate::{generator, model, publish};
 | 
			
		||||
 | 
			
		||||
pub struct IzzilisBot<T: model::SampleModel, U: publish::Publisher> {
 | 
			
		||||
    generator: generator::Generator<T>,
 | 
			
		||||
    publisher: U, // One day I'll figure out how to make this a vector with differing Publisher types
 | 
			
		||||
    loaded_samples: Vec<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T, U> IzzilisBot<T, U>
 | 
			
		||||
where
 | 
			
		||||
    T: model::SampleModel,
 | 
			
		||||
    U: publish::Publisher,
 | 
			
		||||
{
 | 
			
		||||
    pub fn new(generator: generator::Generator<T>, publisher: U) -> IzzilisBot<T, U> {
 | 
			
		||||
        Self {
 | 
			
		||||
            generator,
 | 
			
		||||
            publisher,
 | 
			
		||||
            loaded_samples: Vec::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn generate_samples(&mut self) -> Option<io::Error> {
 | 
			
		||||
        let lines_result = self.generator.generate_sample_lines();
 | 
			
		||||
        let lines = match lines_result {
 | 
			
		||||
            Ok(res) => res,
 | 
			
		||||
            Err(err) => return Some(err),
 | 
			
		||||
        };
 | 
			
		||||
        self.loaded_samples = lines; // wtf happens to the original self.loaded_samples???????
 | 
			
		||||
 | 
			
		||||
        None
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn publish(&mut self) -> Option<Box<dyn Error>> {
 | 
			
		||||
        if self.loaded_samples.len() < 5 {
 | 
			
		||||
            // Refresh samples. Either none have been generated so far,
 | 
			
		||||
            // or generated ones are stale.
 | 
			
		||||
            //
 | 
			
		||||
            // This is a shit solution, but I'm going with it for v1
 | 
			
		||||
            // purely because I don't know the language well enough to be
 | 
			
		||||
            // confident in doing this via threads. Yet.
 | 
			
		||||
            self.generate_samples();
 | 
			
		||||
        }
 | 
			
		||||
        let sample_index = rand::thread_rng().gen_range(0..self.loaded_samples.len() - 1);
 | 
			
		||||
        let content = self.loaded_samples[sample_index].clone();
 | 
			
		||||
        self.loaded_samples.remove(sample_index);
 | 
			
		||||
 | 
			
		||||
        match self.publisher.publish(content) {
 | 
			
		||||
            Some(err) => Some(err),
 | 
			
		||||
            None => None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(tests)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use std::io::{self, ErrorKind};
 | 
			
		||||
 | 
			
		||||
    use crate::{generator, model, publish};
 | 
			
		||||
 | 
			
		||||
    struct fake_sampler {
 | 
			
		||||
        should_ok: bool,
 | 
			
		||||
        ok_str: String,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    struct fake_publisher {
 | 
			
		||||
        should_ok: bool,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl model::SampleModel for fake_sampler {
 | 
			
		||||
        fn get_sample(&self) -> Result<String, io::Error> {
 | 
			
		||||
            if self.should_ok {
 | 
			
		||||
                return Ok(self.ok_str.clone());
 | 
			
		||||
            }
 | 
			
		||||
            Err(io::Error::new(ErrorKind::NotFound, "error"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl publish::Publisher for fake_publisher {
 | 
			
		||||
        fn publish(&self, content: String) -> Option<Box<dyn std::error::Error>> {
 | 
			
		||||
            if self.should_ok {
 | 
			
		||||
                return None;
 | 
			
		||||
            }
 | 
			
		||||
            Some(Box::new(io::Error::new(ErrorKind::NotFound, "error")))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn generate_samples_populates() {
 | 
			
		||||
        let model_ok_string = String::from("model_ok");
 | 
			
		||||
        let model = fake_sampler {
 | 
			
		||||
            should_ok: true,
 | 
			
		||||
            ok_str: model_ok_string,
 | 
			
		||||
        };
 | 
			
		||||
        let gen = generator::Generator::new(model);
 | 
			
		||||
        let publish = fake_publisher { should_ok: true };
 | 
			
		||||
        let mut bot = super::IzzilisBot::new(gen, publish);
 | 
			
		||||
 | 
			
		||||
        match bot.publish() {
 | 
			
		||||
            Some(_) => panic!("publish failed"),
 | 
			
		||||
            None => (),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
use std::error::Error;
 | 
			
		||||
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize)]
 | 
			
		||||
pub struct Config {
 | 
			
		||||
    python_path: String,
 | 
			
		||||
    model_name: String,
 | 
			
		||||
    temperature: String,
 | 
			
		||||
    top_k: String,
 | 
			
		||||
    gpt_code_path: String,
 | 
			
		||||
    fediverse_base_url: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Config {
 | 
			
		||||
    pub fn default() -> Config {
 | 
			
		||||
        Config {
 | 
			
		||||
            python_path: String::from("/usr/bin/python3"),
 | 
			
		||||
            model_name: String::from("117M"),
 | 
			
		||||
            temperature: String::from("1"),
 | 
			
		||||
            top_k: String::from("40"),
 | 
			
		||||
            gpt_code_path: String::from("."),
 | 
			
		||||
            fediverse_base_url: String::from("https://lain.com"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from(path: String) -> Result<Config, Box<dyn Error>> {
 | 
			
		||||
        let file_bytes = std::fs::read(path)?;
 | 
			
		||||
        match serde_json::from_slice(&file_bytes) {
 | 
			
		||||
            Ok(res) => Ok(res),
 | 
			
		||||
            Err(err) => Err(Box::new(err)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn save(&self, path: String) -> Option<Box<dyn Error>> {
 | 
			
		||||
        let cfg_json = match serde_json::to_vec(self) {
 | 
			
		||||
            Ok(res) => res,
 | 
			
		||||
            Err(err) => return Some(Box::new(err)),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        match std::fs::write(path, &cfg_json) {
 | 
			
		||||
            Ok(_) => None,
 | 
			
		||||
            Err(err) => Some(Box::new(err)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get a reference to the config's python path.
 | 
			
		||||
    pub fn python_path(&self) -> String {
 | 
			
		||||
        self.python_path.clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get a reference to the config's model name.
 | 
			
		||||
    pub fn model_name(&self) -> String {
 | 
			
		||||
        self.model_name.clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get a reference to the config's temperature.
 | 
			
		||||
    pub fn temperature(&self) -> String {
 | 
			
		||||
        self.temperature.clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get a reference to the config's top k.
 | 
			
		||||
    pub fn top_k(&self) -> String {
 | 
			
		||||
        self.top_k.clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get a reference to the config's gpt code path.
 | 
			
		||||
    pub fn gpt_code_path(&self) -> String {
 | 
			
		||||
        self.gpt_code_path.clone()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn fediverse_base_url(&self) -> String {
 | 
			
		||||
        self.fediverse_base_url.clone()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
use std::io;
 | 
			
		||||
 | 
			
		||||
use crate::model;
 | 
			
		||||
const SAMPLE_SPLIT_WORD: &str = "<|endoftext|>";
 | 
			
		||||
const SAMPLE_SAMPLE_LINE: &str =
 | 
			
		||||
    "======================================== SAMPLE 1 ========================================";
 | 
			
		||||
pub struct Generator<T: model::SampleModel> {
 | 
			
		||||
    model: T,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Why did this fucking shit take so long to sort out??
 | 
			
		||||
impl<T> Generator<T>
 | 
			
		||||
where
 | 
			
		||||
    T: model::SampleModel,
 | 
			
		||||
{
 | 
			
		||||
    pub fn new(model: T) -> Generator<T> {
 | 
			
		||||
        Self { model }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn generate_sample_lines(&self) -> Result<Vec<String>, io::Error> {
 | 
			
		||||
        let unwashed_sample = self.model.get_sample()?;
 | 
			
		||||
        // Cursed, I just wanted a Select
 | 
			
		||||
        let mut washed_sample: Vec<String> = Vec::new();
 | 
			
		||||
        unwashed_sample
 | 
			
		||||
            .replace(SAMPLE_SAMPLE_LINE, "")
 | 
			
		||||
            .split(SAMPLE_SPLIT_WORD)
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .for_each(|elem| washed_sample.push(elem.trim().to_string()));
 | 
			
		||||
 | 
			
		||||
        Ok(washed_sample)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
use std::{error::Error, process};
 | 
			
		||||
 | 
			
		||||
mod bot;
 | 
			
		||||
mod config;
 | 
			
		||||
mod generator;
 | 
			
		||||
mod model;
 | 
			
		||||
mod publish;
 | 
			
		||||
 | 
			
		||||
const CONFIG_PATH: &str = "bot_config.json";
 | 
			
		||||
 | 
			
		||||
fn main() -> Result<(), Box<dyn Error>> {
 | 
			
		||||
    let cfg = match config::Config::from(CONFIG_PATH.to_string()) {
 | 
			
		||||
        Ok(cfg) => cfg,
 | 
			
		||||
        Err(_) => {
 | 
			
		||||
            println!(
 | 
			
		||||
                "Failed reading config at [{}], writing default",
 | 
			
		||||
                CONFIG_PATH
 | 
			
		||||
            );
 | 
			
		||||
            match config::Config::default().save(CONFIG_PATH.to_string()) {
 | 
			
		||||
                Some(err) => println!("Failed writing file to {}: {}", CONFIG_PATH, err),
 | 
			
		||||
                None => (),
 | 
			
		||||
            }
 | 
			
		||||
            process::exit(1);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let gpt_model = model::GPTSampleModel::new(
 | 
			
		||||
        cfg.python_path(),
 | 
			
		||||
        cfg.gpt_code_path(),
 | 
			
		||||
        vec![
 | 
			
		||||
            "generate_unconditional_samples.py".to_string(),
 | 
			
		||||
            "--model_name".to_string(),
 | 
			
		||||
            cfg.model_name(),
 | 
			
		||||
            "--temperature".to_string(),
 | 
			
		||||
            cfg.temperature(),
 | 
			
		||||
            "--top_k".to_string(),
 | 
			
		||||
            cfg.top_k(),
 | 
			
		||||
            "--nsamples".to_string(),
 | 
			
		||||
            "1".to_string(),
 | 
			
		||||
        ],
 | 
			
		||||
    );
 | 
			
		||||
    let publisher = publish::FediversePublisher::new(cfg.fediverse_base_url())?;
 | 
			
		||||
    let gen = generator::Generator::new(gpt_model);
 | 
			
		||||
    let mut bot = bot::IzzilisBot::new(gen, publisher);
 | 
			
		||||
 | 
			
		||||
    match bot.publish() {
 | 
			
		||||
        Some(err) => Err(err),
 | 
			
		||||
        None => Ok(()),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
use std::{io, process::Command};
 | 
			
		||||
 | 
			
		||||
pub trait SampleModel {
 | 
			
		||||
    fn get_sample(&self) -> Result<String, io::Error>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct GPTSampleModel {
 | 
			
		||||
    python_command: String,
 | 
			
		||||
    command_working_path: String,
 | 
			
		||||
    command_args: Vec<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl SampleModel for GPTSampleModel {
 | 
			
		||||
    fn get_sample(&self) -> Result<String, io::Error> {
 | 
			
		||||
        let cmd_output = Command::new(&self.python_command)
 | 
			
		||||
            .current_dir(&self.command_working_path)
 | 
			
		||||
            .args(&self.command_args)
 | 
			
		||||
            .output()?;
 | 
			
		||||
 | 
			
		||||
        Ok(String::from_utf8_lossy(&cmd_output.stdout).to_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GPTSampleModel {
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        python_command: String,
 | 
			
		||||
        command_working_path: String,
 | 
			
		||||
        command_args: Vec<String>,
 | 
			
		||||
    ) -> GPTSampleModel {
 | 
			
		||||
        Self {
 | 
			
		||||
            python_command: python_command,
 | 
			
		||||
            command_working_path: command_working_path,
 | 
			
		||||
            command_args: command_args,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
use std::error::Error;
 | 
			
		||||
 | 
			
		||||
use elefren::{
 | 
			
		||||
    helpers::{cli, toml},
 | 
			
		||||
    scopes::Scopes,
 | 
			
		||||
    status_builder::Visibility,
 | 
			
		||||
    Language, Mastodon, MastodonClient, Registration, StatusBuilder,
 | 
			
		||||
};
 | 
			
		||||
pub trait Publisher {
 | 
			
		||||
    fn publish(&self, content: String) -> Option<Box<dyn Error>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct FediversePublisher {
 | 
			
		||||
    client: Mastodon,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl FediversePublisher {
 | 
			
		||||
    pub fn new(fedi_url: String) -> Result<FediversePublisher, Box<dyn Error>> {
 | 
			
		||||
        let fedi = if let Ok(data) = toml::from_file("fediverse.toml") {
 | 
			
		||||
            Mastodon::from(data)
 | 
			
		||||
        } else {
 | 
			
		||||
            register(fedi_url)?
 | 
			
		||||
        };
 | 
			
		||||
        Ok(Self { client: fedi })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Publisher for FediversePublisher {
 | 
			
		||||
    fn publish(&self, content: String) -> Option<Box<dyn Error>> {
 | 
			
		||||
        let status_build_result = StatusBuilder::new()
 | 
			
		||||
            .status(&content)
 | 
			
		||||
            // .visibility(Visibility::Direct)
 | 
			
		||||
            .visibility(Visibility::Public)
 | 
			
		||||
            .sensitive(false)
 | 
			
		||||
            .language(Language::Eng)
 | 
			
		||||
            .build();
 | 
			
		||||
 | 
			
		||||
        let status = match status_build_result {
 | 
			
		||||
            Ok(status) => status,
 | 
			
		||||
            Err(err) => return Some(Box::new(err)),
 | 
			
		||||
        };
 | 
			
		||||
        println!("Posting status [{}] to fediverse", &content);
 | 
			
		||||
 | 
			
		||||
        match self.client.new_status(status) {
 | 
			
		||||
            Ok(_) => None,
 | 
			
		||||
            Err(err) => Some(Box::new(err)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn register(fedi_url: String) -> Result<Mastodon, Box<dyn Error>> {
 | 
			
		||||
    let registration = Registration::new(fedi_url)
 | 
			
		||||
        .client_name("izzilis")
 | 
			
		||||
        .scopes(Scopes::write_all())
 | 
			
		||||
        .build()?;
 | 
			
		||||
    let fediverse = cli::authenticate(registration)?;
 | 
			
		||||
 | 
			
		||||
    // Save app data for using on the next run.
 | 
			
		||||
    toml::to_file(&*fediverse, "fediverse.toml")?;
 | 
			
		||||
 | 
			
		||||
    Ok(fediverse)
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue