use error_stack::{report, Result};
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use imghdr;
use indicatif::ProgressIterator;
use reqwest;
use reqwest::Url;
use tempdir::TempDir;
use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize};
use walkdir::DirEntry;
use walkdir::WalkDir;
use zip::write::FileOptions;
use env_logger;
use log::{debug, error, warn};
use chrono::Local;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Local {
#[arg(short = 'o', long = "output", default_value_t = String::from("generated_emojis"))]
outputFilepath: String,
#[arg(short, long)]
folder: String,
#[arg(short = 'h', long = "host", default_value_t = String::from("https://git.joinfirefish.org/firefish/emoji-gen"))]
originHost: String,
#[arg(short, long, default_value_t = ("Custom").to_string())]
group: String,
},
Crawl {
#[arg(short = 'o', long = "output", default_value_t = String::from("generated_emojis"))]
outputFilepath: String,
#[arg(short, long)]
host: String,
},
}
#[derive(Serialize, Deserialize)]
struct Meta {
metaVersion: i8,
host: String,
exportedAt: String,
emojis: Vec<Emoji>,
}
#[derive(Serialize, Deserialize)]
struct EmojiResponse {
shortcode: Option<String>,
url: Option<String>,
static_url: String,
category: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct Emoji {
downloaded: bool,
fileName: String,
emoji: EmojiData,
}
#[derive(Serialize, Deserialize)]
struct EmojiData {
name: String,
category: String,
aliases: Vec<String>,
}
fn getTypename(typeEnum: imghdr::Type) -> &'static str {
return match typeEnum {
imghdr::Type::Bgp => "bgp",
imghdr::Type::Bmp => "bmp",
imghdr::Type::Exr => "exr",
imghdr::Type::Flif => "flif",
imghdr::Type::Gif => "gif",
imghdr::Type::Ico => "ico",
imghdr::Type::Jpeg => "jpg",
imghdr::Type::Pbm => "pbm",
imghdr::Type::Pgm => "pgm",
imghdr::Type::Png => "png",
imghdr::Type::Ppm => "ppm",
imghdr::Type::Rast => "rast",
imghdr::Type::Rgb => "rgb",
imghdr::Type::Rgbe => "rgbe",
imghdr::Type::Tiff => "tiff",
imghdr::Type::Webp => "webp",
imghdr::Type::Xbm => "xbm",
};
}
#[derive(Debug)]
enum EmojiGenError {
ZipCreationFailed,
EmojiFetchFailed,
MetadataGenerationFailed,
ImageFetchingFailed,
}
impl fmt::Display for EmojiGenError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt.write_str("Error processing emojis: Could not create the emoji zip bundle.")
}
}
impl Error for EmojiGenError {}
fn main() {
env_logger::init();
let args = Cli::parse();
match process(args.command) {
Ok(()) => {}
Err(err) => {
error!("The process could not be completed. Quiting.");
error!("\n{err:?}");
}
};
}
fn process(command: Command) -> Result<(), EmojiGenError> {
let tmpDir: TempDir = TempDir::new("emoji-gen").unwrap();
let tmpFolder: &Path = tmpDir.path();
let zipFilepath: PathBuf;
let emojis: Vec<Emoji>;
let hostUrl: String;
match command {
Command::Local {
outputFilepath,
folder,
originHost,
group,
} => {
zipFilepath = prepare_zip_filepath(outputFilepath)?;
emojis = get_local_emojis(Path::new(folder.as_str()), group, tmpFolder)?;
hostUrl = originHost;
}
Command::Crawl {
outputFilepath,
host,
} => {
zipFilepath = prepare_zip_filepath(outputFilepath)?;
emojis = get_host_emojis(
&Url::parse(&host).map_err(|_| {
report!(EmojiGenError::EmojiFetchFailed)
.attach_printable(format!("Url '{}' is invalid.", host))
})?,
tmpFolder,
)?;
hostUrl = host;
}
}
generate_meta(hostUrl, emojis, tmpFolder)?;
zip(tmpFolder, &zipFilepath)?;
drop(tmpFolder);
println!(
"✅ Done! Importable ZIP file under '{}'",
zipFilepath.display()
);
Ok(())
}
fn prepare_zip_filepath(outputFilepath: String) -> Result<PathBuf, EmojiGenError> {
let mut zipFilepath = PathBuf::from(outputFilepath.as_str());
zipFilepath.set_extension("zip");
if zipFilepath.exists() {
return Err(
report!(EmojiGenError::ZipCreationFailed).attach_printable(format!(
"File '{}' exists. Please choose another name.",
zipFilepath.display()
)),
);
}
Ok(zipFilepath)
}
fn get_host_emojis(host: &Url, tmpFolder: &Path) -> Result<Vec<Emoji>, EmojiGenError> {
println!(
"Getting all the fine emojis from Url '{}'...",
host.as_str()
);
let hostUrl = &host.join("/api/v1/custom_emojis").unwrap();
let emojis = match reqwest::blocking::get(hostUrl.clone()) {
Ok(response) => match response.json::<Vec<EmojiResponse>>() {
Ok(emojiRes) => {
let emojos: Vec<EmojiResponse> = emojiRes;
let iter = emojos.iter();
Ok(iter
.progress_count(emojos.len() as u64)
.map(|res| {
get_host_emoji_data(
Url::parse(res.url.as_ref().unwrap().as_str().clone()).unwrap(),
res.shortcode.clone().unwrap_or_default(),
res.category.clone().unwrap_or_default(),
tmpFolder,
)
})
.filter_map(|r| r.ok())
.collect::<Vec<Emoji>>())
}
Err(_) => Err(
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not get emoji list from url '{}'.",
hostUrl.as_str()
)),
),
},
Err(_) => Err(
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not get response from url '{}'.",
hostUrl.as_str()
)),
),
}?;
Ok(emojis)
}
fn get_host_emoji_data(
fileUrl: Url,
name: String,
category: String,
tmpFolder: &Path,
) -> Result<Emoji, EmojiGenError> {
debug!("{}", fileUrl.to_string());
let newFilename = get_image_from_url(&fileUrl, tmpFolder, name.clone())?;
let data: EmojiData = EmojiData {
name: name,
category: category.to_string(),
aliases: Vec::<String>::new(),
};
Ok(Emoji {
downloaded: true,
fileName: newFilename,
emoji: data,
})
}
fn get_image_from_url(
fileUrl: &Url,
tmpFolder: &Path,
filename: String,
) -> Result<String, EmojiGenError> {
let img_bytes = &reqwest::blocking::get(fileUrl.clone())
.map_err(|_| {
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not get image file from url '{}'.",
fileUrl.as_str()
))
})?
.bytes()
.unwrap();
let mut tmpFilepath: PathBuf = tmpFolder.join(filename);
match imghdr::from_bytes(img_bytes) {
Some(extension) => tmpFilepath.set_extension(getTypename(extension)),
None => tmpFilepath.set_extension("xxx"),
};
println!("Creating image file at path '{}'...", tmpFilepath.display());
let mut imageFile = File::create(tmpFilepath.as_os_str()).map_err(|_| {
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not create image file at temporary path '{}'.",
tmpFilepath.display()
))
})?;
imageFile.write_all(img_bytes).map_err(|_| {
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not write image at temporary path '{}'.",
tmpFilepath.display()
))
})?;
Ok(String::from(
tmpFilepath.file_name().unwrap().to_str().unwrap(),
))
}
fn get_local_emojis(
folder: &Path,
group: String,
tmpFolder: &Path,
) -> Result<Vec<Emoji>, EmojiGenError> {
match folder.canonicalize() {
Ok(f) => {
if !f.is_dir() {
Err(
report!(EmojiGenError::EmojiFetchFailed).attach_printable(format!(
"Folder path '{}' is not a directory.",
folder.display()
)),
)
} else {
Ok(f)
}
}
Err(_) => Err(report!(EmojiGenError::EmojiFetchFailed)
.attach_printable(format!("Folder '{}' does not exist.", folder.display()))),
}?;
println!(
"Getting all the fine emojis from folder '{}'...",
folder.display()
);
let mut emojis = Vec::<Emoji>::new();
let iter = WalkDir::new(folder).into_iter();
let count = WalkDir::new(folder).into_iter().count() as u64;
for result in iter.progress_count(count) {
if let Err(_) = result {
continue;
}
let opt_file = result.ok();
if opt_file.is_none() {
continue;
}
let file = opt_file.unwrap();
if !file.metadata().unwrap().is_file() {
continue;
}
let filename = file.path().file_name().unwrap();
println!("Checking file '{}'...", filename.to_string_lossy());
let image = imghdr::from_file(file.path()).map_err(|_| {
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not get image at path '{}'.",
file.path().display()
))
})?;
if image.is_none() {
if filename.to_ascii_uppercase() == "LICENSE"
|| filename.to_ascii_uppercase() == "LICENSE.md"
{
get_image_from_path(&file.path(), tmpFolder)
.map_err(|err| warn!("{}", err))
.unwrap();
}
continue;
}
match get_local_emoji_data(file, group.clone().into(), tmpFolder) {
Ok(emoji) => {
emojis.push(emoji);
}
Err(err) => {
warn!("{}", err)
}
}
}
Ok(emojis)
}
fn get_local_emoji_data(
file: DirEntry,
original_category: Rc<String>,
tmpFolder: &Path,
) -> Result<Emoji, EmojiGenError> {
debug!("{}", file.path().display());
let fileName = String::from(file.file_name().to_str().unwrap());
let name = String::from(file.path().file_stem().unwrap().to_str().unwrap())
.replace(&[' ', '-'][..], "_");
get_image_from_path(&file.path(), tmpFolder)?;
let data = EmojiData {
name: name,
category: original_category.to_string(),
aliases: Vec::<String>::new(),
};
Ok(Emoji {
downloaded: true,
fileName,
emoji: data,
})
}
fn get_image_from_path(filePath: &Path, tmpFolder: &Path) -> Result<(), EmojiGenError> {
let filename = filePath.file_name().unwrap();
let img_data = std::fs::read(filePath.as_os_str()).map_err(|_| {
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not read image file at path '{}'.",
filePath.display()
))
})?;
let img_bytes = img_data.as_slice();
let imageFilePath = &tmpFolder.join(filename);
let mut imageFile = File::create(imageFilePath).map_err(|_| {
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not create image to temporary path '{}'.",
imageFilePath.display()
))
})?;
imageFile.write_all(&img_bytes).map_err(|_| {
report!(EmojiGenError::ImageFetchingFailed).attach_printable(format!(
"Could not write image to temporary path '{}'.",
imageFilePath.display()
))
})?;
Ok(())
}
fn generate_meta(host: String, emojis: Vec<Emoji>, tmpFolder: &Path) -> Result<(), EmojiGenError> {
let meta = Meta {
metaVersion: 1,
host: host,
exportedAt: Local::now().to_rfc3339(),
emojis: emojis,
};
let json = serde_json::to_string(&meta).map_err(|_| {
report!(EmojiGenError::MetadataGenerationFailed)
.attach_printable(format!("Could not generate metadata for 'meta.json'."))
})?;
let metaFilepath: &PathBuf = &tmpFolder.join("meta.json");
println!(
"Creating file 'meta.json' at path '{}'...",
metaFilepath.to_str().unwrap()
);
let mut file = File::create(metaFilepath).map_err(|_| {
report!(EmojiGenError::MetadataGenerationFailed).attach_printable(format!(
"Could not create file '{}'.",
metaFilepath.display()
))
})?;
write!(file, "{}", json).map_err(|_| {
report!(EmojiGenError::MetadataGenerationFailed).attach_printable(format!(
"Could not write metadata to file '{}'.",
metaFilepath.display()
))
})?;
Ok(())
}
fn zip(src_dir: &Path, dst_file: &Path) -> Result<(), EmojiGenError> {
if !std::path::Path::new(src_dir).is_dir() {
return Err(report!(EmojiGenError::ZipCreationFailed)
.attach_printable(format!("Could not find folder '{}'.", src_dir.display())));
}
println!("Creating zip file at path '{}'...", dst_file.display());
let zipFile = &File::create(dst_file).map_err(|_| {
report!(EmojiGenError::ZipCreationFailed)
.attach_printable(format!("Could not create file '{}'.", dst_file.display()))
})?;
let mut zip = zip::ZipWriter::new(zipFile);
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755);
let iter = WalkDir::new(src_dir).into_iter();
let count = WalkDir::new(src_dir).into_iter().count() as u64;
let mut buffer = Vec::new();
for entryRes in iter.progress_count(count) {
let entry = entryRes.map_err(|_| {
report!(EmojiGenError::ZipCreationFailed).attach_printable(format!(
"Could get path to a file in folder '{}'.",
src_dir.display()
))
})?;
let path = entry.path();
let name = path.strip_prefix(src_dir).map_err(|_| {
report!(EmojiGenError::ZipCreationFailed).attach_printable(format!(
"Could not strip prefix on file path '{}'.",
path.display()
))
})?;
if path.is_file() {
debug!("adding file {:?} as {:?} ...", path, name);
#[allow(deprecated)]
zip.start_file_from_path(name, options).map_err(|_| {
report!(EmojiGenError::ZipCreationFailed).attach_printable(format!(
"Could not create file '{}' in zip file '{:?}'.",
path.display(),
zipFile
))
})?;
let mut f = File::open(path).map_err(|_| {
report!(EmojiGenError::ZipCreationFailed)
.attach_printable(format!("Could not read file '{}'.", path.display()))
})?;
f.read_to_end(&mut buffer).map_err(|_| {
report!(EmojiGenError::ZipCreationFailed)
.attach_printable(format!("Could not read file '{}'.", path.display()))
})?;
zip.write_all(&*buffer).map_err(|_| {
report!(EmojiGenError::ZipCreationFailed)
.attach_printable(format!("Could not write data to zip file '{:?}'.", zipFile))
})?;
buffer.clear();
} else if !name.as_os_str().is_empty() {
debug!("adding dir {:?} as {:?} ...", path, name);
#[allow(deprecated)]
zip.add_directory_from_path(name, options).map_err(|_| {
report!(EmojiGenError::ZipCreationFailed).attach_printable(format!(
"Could not create folder '{}' in zip file '{:?}'.",
path.display(),
zipFile
))
})?;
}
}
zip.finish().map_err(|_| {
report!(EmojiGenError::ZipCreationFailed)
.attach_printable(format!("Could not close zip file '{:?}'.", zipFile))
})?;
Ok(())
}