root/src/main.rs

// main.rs
//
// Copyright 2022 nee <nee-git@patchouli.garden>
// SPDX-License-Identifier: AGPL-3.0-or-later
// use chrono::*;
mod locale_to_unicode_flag;
use locale_to_unicode_flag::locale_str_to_flag_sequence;

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::env;
use std::fs::File;
use std::io::BufReader;

// const FILE: &str = "latest.json";
const URL: &str = "https://odrs.gnome.org/1.0/reviews/api/app/";
// const APP: &str = "org.gnome.Podcasts";

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Review {
    app_id: String,
    date_created: f64,
    description: String,
    distro: String,
    karma_down: u64,
    karma_up: u64,
    locale: String, // "en_US.UTF-8"
    rating: i32,    // 0 - 100
    reported: i32,
    review_id: u64,
    summary: String,
    user_display: Option<String>,
    user_hash: Option<String>,
    version: String,
}

impl Review {
    fn date(&self) -> String {
        let naive = chrono::NaiveDateTime::from_timestamp(self.date_created as i64, 0);
        let datetime: chrono::DateTime<chrono::Utc> =
            chrono::DateTime::from_utc(naive, chrono::Utc);
        datetime.format("%Y-%m-%d %H:%M:%S").to_string()
    }
    fn rating_stars(&self) -> &str {
        if self.rating >= 100 {
            "★★★★★"
        } else if self.rating >= 80 {
            "★★★★☆"
        } else if self.rating >= 60 {
            "★★★☆☆"
        } else if self.rating >= 40 {
            "★★☆☆☆"
        } else if self.rating >= 20 {
            "★☆☆☆☆"
        } else {
            "☆☆☆☆☆"
        }
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.iter().any(|i| i == "--help") {
        eprintln!("gnome-review-notification-bot [options] app-id-slug");
        eprintln!("Example: gnome-review-notification-bot org.gnome.Podcasts");
        eprintln!("");
        eprintln!("Available options: --help --version");
        return;
    }

    if args.iter().any(|i| i == "--version") {
        eprintln!("gnome-review-notification-bot v1.0.0");
        return;
    }

    match args.get(1) {
        Some(app) => {
            if let Err(e) = run_main(app) {
                eprintln!("{:#?}", e);
            }
        }
        _ => {
            eprintln!("Must be run with an app id as first parameter.");
            eprintln!("Example: gnome-review-notification-bot org.gnome.Podcasts");
        }
    }
}

fn run_main(app: &str) -> Result<()> {
    // curl https://odrs.gnome.org/1.0/reviews/api/app/org.gnome.Podcasts?limit=1
    let old_json = read_json(app).context("failed to read old reviews json")?;
    let new_json = update_json(app).context("failed to get new json from the web")?;
    let new_entries = find_new_entries(&old_json, &new_json);
    post_about_it(&new_entries);
    write_json(&new_json, app).context("failed to write the reviews json")?;
    Ok(())
}

fn read_json(app: &str) -> Result<Vec<Review>> {
    let filepath = vec![app, "-reviews.json"].join("");
    if let Ok(file) = File::open(filepath) {
        let reader = BufReader::new(file);
        let reviews = serde_json::from_reader(reader)?;
        Ok(reviews)
    } else {
        Ok(vec![])
    }
}

fn update_json(app: &str) -> Result<Vec<Review>> {
    // "?limit=1"
    // TODO no idea why i had limit in this? Just removed this, maybe put it back and leave a comment here.
    let url = vec![URL, app].join("");
    let body = reqwest::blocking::get(url)?.text()?;
    let parsed = serde_json::from_str(&body)?;
    Ok(parsed)
}

fn find_new_entries(old: &[Review], new: &Vec<Review>) -> Vec<Review> {
    let mut found = Vec::new();
    for i in new {
        let exists = old
            .iter()
            .find(|&x| x.review_id == i.review_id && x.date_created == i.date_created);
        if exists.is_none() {
            found.push(i.clone());
        }
    }
    found
}

fn post_about_it(entries: &Vec<Review>) {
    let anon_name: String = "Anonymous".to_string();
    for e in entries {
        let locale = locale_str_to_flag_sequence(e.locale.to_string()).unwrap_or("".to_string());
        let user_name = e.user_display.as_ref().unwrap_or(&anon_name);
        println!("\"{}\" - by {} {}", e.summary, user_name, locale);
        println!("{}", e.description);
        println!(
            "{} - {} on {} - v{}\n",
            e.date(),
            e.rating_stars(),
            e.distro,
            e.version
        );
    }
}

fn write_json(new: &Vec<Review>, app: &str) -> Result<()> {
    let filepath = vec![app, "-reviews.json"].join("");
    let buffer = File::create(filepath)?;
    serde_json::to_writer(buffer, new)?;
    Ok(())
}