1
0
Fork 0

add mastoapi deploy support and change a bunch of things

This commit is contained in:
chaos 2023-08-21 12:38:54 +01:00
parent c4869d1447
commit ea80ad1e9a
No known key found for this signature in database
10 changed files with 376 additions and 29 deletions

4
Cargo.lock generated
View file

@ -584,9 +584,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
[[package]]
name = "libc"
version = "0.2.132"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "lock_api"

View file

@ -13,6 +13,7 @@ pub async fn check_missing(args: CheckMissingArgs) {
.sticker_sets
.iter()
.map(|pack_id| {
println!("{}", pack_id);
return (pack_id, config.sticker_sets.get(pack_id).unwrap());
})
.collect();

View file

@ -1,8 +1,9 @@
use crate::creds::Creds;
use crate::deploy_discord::deploy_discord;
use crate::deploy_mastodon::deploy_mastodon;
use crate::deploy_telegram::deploy_telegram;
use crate::sticker_config::StickerConfig;
use std::collections::HashMap;
use std::fs::File;
use std::path::PathBuf;
@ -16,9 +17,16 @@ pub async fn deploy(args: crate::args::DeployArgs) {
let config: StickerConfig =
serde_yaml::from_reader(config_file).expect("could not parse stickers.yml");
let creds: Creds = serde_yaml::from_reader(creds_file).expect("could not parse creds.yml");
let creds: HashMap<String, String> =
serde_yaml::from_reader(creds_file).expect("could not parse creds.yml");
for deploy_id in args.deploy_ids.into_iter() {
let mut deploy_ids = args.deploy_ids;
if deploy_ids.is_empty() {
deploy_ids = config.deploy_where.keys().cloned().collect::<Vec<String>>();
}
for deploy_id in deploy_ids.into_iter() {
let sticker_config = config.clone();
match sticker_config
@ -48,6 +56,16 @@ pub async fn deploy(args: crate::args::DeployArgs) {
)
.await;
}
"mastodon" => {
println!("deploying {} to mastodon", &deploy_id);
deploy_mastodon(
deploy_id,
sticker_config,
creds.clone(),
args.folder.clone(),
)
.await;
}
_ => {
panic!("deploy_to not set")
}

View file

@ -1,7 +0,0 @@
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Creds {
pub discord_bot_token: String,
pub telegram_bot_token: String,
}

View file

@ -1,4 +1,3 @@
use crate::creds::Creds;
use crate::sticker_config::{DiscordDeployLocation, Sticker, StickerConfig, StickerType};
use indexmap::IndexMap;
use serenity::http::client::Http as DiscordClient;
@ -79,16 +78,12 @@ fn split_by(
pub async fn deploy_discord(
deploy_id: String,
sticker_config: StickerConfig,
creds: Creds,
creds: HashMap<String, String>,
base_stickerdb_path: String,
) {
let discord_token = creds.discord_bot_token.as_str();
let discord_client = DiscordClient::new(discord_token);
//let discord_cache = DiscordCache::new()
let deploy_where = sticker_config.deploy_where.get(&deploy_id).unwrap();
let deploy_locations = deploy_where.discord.as_ref().unwrap().clone();
let deploy_locations_map: HashMap<_, _> = deploy_locations
let deploy_locations_map: HashMap<String, &DiscordDeployLocation> = deploy_locations
.iter()
.map(|loc| (loc.deploy_name.clone(), loc))
.collect();
@ -152,6 +147,9 @@ pub async fn deploy_discord(
let deploy_location = deploy_locations_map.get(&deploy_name).unwrap();
let discord_token = creds.get(&deploy_location.credential_name).unwrap();
let discord_client = DiscordClient::new(discord_token);
let deploy_emoji_names: Vec<String> = deploy_emojis
.iter()
.map(|deploy_emoji| deploy_emoji.0.clone())

142
src/deploy_mastodon.rs Normal file
View file

@ -0,0 +1,142 @@
use std::{collections::HashMap, io::Read, path::PathBuf, process::Command};
use crate::{
mastodon_api::{AdminEmoji, MastodonAPI},
sticker_config::{Sticker, StickerConfig},
};
fn convert_to_png(path: PathBuf) -> Vec<u8> {
let output = Command::new("convert")
.args([
"-format",
"png",
"-resize",
"128x128",
path.as_os_str().to_str().unwrap(),
"png:-",
])
.output()
.expect("failed to execute process");
if !output.status.success() {
panic!(
"failed to run convert, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
output.stdout
}
pub async fn deploy_mastodon(
deploy_id: String,
sticker_config: StickerConfig,
creds: HashMap<String, String>,
base_stickerdb_path: String,
) {
let deploy_where = sticker_config.deploy_where.get(&deploy_id).unwrap();
let deploy_location = deploy_where.mastodon.as_ref().unwrap();
let pack_contents = sticker_config
.sticker_sets
.get(&deploy_where.pack_id)
.unwrap();
let pack_stickers: HashMap<String, Sticker> = pack_contents
.iter()
.map(|sticker_name| {
return (
sticker_name.clone(),
sticker_config.stickers.get(sticker_name).unwrap().clone(),
);
})
.collect();
let token = creds.get(&deploy_location.credential_name).unwrap();
let mastodon_api = MastodonAPI::new(token.to_string(), deploy_location.api_base.clone());
let filter = vec!["domain:local".to_string()];
let mut mastodon_emojis = mastodon_api
.get_custom_emoji(&filter)
.await
.expect("could not get custom emojis");
// Delete emojis which have been removed from list
{
let invalid_emojis: Vec<AdminEmoji> = mastodon_emojis
.clone()
.into_iter()
.filter(|emoji| {
!(pack_contents.contains(&emoji.shortcode)
|| emoji.category != deploy_location.category)
})
.collect();
for invalid_emoji in invalid_emojis.iter() {
println!("Deleting emoji with name {}", &invalid_emoji.shortcode);
mastodon_api
.delete_custom_emoji(&invalid_emoji.id)
.await
.expect("could not delete emoji");
}
}
if let Some(category) = &deploy_location.category {
mastodon_emojis = mastodon_emojis
.into_iter()
.filter(|emoji| emoji.category.is_some())
.filter(|emoji| emoji.category == Some(category.to_string()))
.collect();
} else {
mastodon_emojis = mastodon_emojis
.into_iter()
.filter(|emoji| emoji.category.is_none())
.collect()
}
// add emoji that arent in mastodon_emojis
{
let missing_emoji: Vec<String> = pack_contents
.clone()
.into_iter()
.filter(|emoji_name| {
for emoji in mastodon_emojis.iter() {
if &emoji.shortcode == emoji_name {
return false;
}
}
true
})
.collect();
for emoji_name in missing_emoji.iter() {
println!("Uploading Emoji: {}", emoji_name);
let sticker_data = pack_stickers.get(emoji_name).unwrap();
let file_path = PathBuf::from(&base_stickerdb_path).join(&sticker_data.file);
let mut file_data: Vec<u8>;
if file_path.extension().unwrap() != "png" {
file_data = convert_to_png(file_path);
} else {
file_data = Vec::new();
let mut file = std::fs::File::open(file_path).unwrap();
file.read_to_end(&mut file_data)
.expect("could not read file");
}
mastodon_api
.upload_emoji(
emoji_name.clone(),
deploy_location.category.clone(),
file_data,
)
.await
.expect("could not upload emoji");
}
}
}

View file

@ -3,7 +3,6 @@ use std::ops::Index;
use std::{collections::HashMap, error::Error};
use crate::{
creds::Creds,
sticker_config::{Sticker, StickerConfig, StickerType},
tg_api::{TelegramAPI, TelegramSticker},
};
@ -56,7 +55,7 @@ impl TelegramStickerIDState {
pub async fn deploy_telegram(
deploy_id: String,
sticker_config: StickerConfig,
creds: Creds,
creds: HashMap<String, String>,
base_stickerdb_path: String,
) {
let mut tg_state = TelegramStickerIDState::new(&base_stickerdb_path);
@ -106,7 +105,9 @@ pub async fn deploy_telegram(
panic!("too many stickers in pack");
}
let tg_bot = TelegramAPI::new(creds.telegram_bot_token);
let token = creds.get(&deploy_location.credential_name).unwrap();
let tg_bot = TelegramAPI::new(token.to_string());
let tg_sticker_set = tg_bot
.get_sticker_set(&deploy_location.name)
.await
@ -241,7 +242,7 @@ pub async fn deploy_telegram(
tg_state.set(&new_sticker.file_unique_id, sticker_name);
tg_stickers.push((
sticker_name.clone(),
sticker_name.to_string(),
tg_new_sticker_set
.stickers
.index(new_sticker_position_base + i)

View file

@ -2,9 +2,10 @@ pub mod args;
pub mod cmd_check_missing;
pub mod cmd_createtg;
pub mod cmd_deploy;
pub mod creds;
pub mod deploy_discord;
pub mod deploy_mastodon;
pub mod deploy_telegram;
pub mod mastodon_api;
pub mod sticker_config;
pub mod tg_api;
@ -21,11 +22,12 @@ async fn main() {
if args.debug {
let subscriber = tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.compact()
.finish();
.with_max_level(tracing::Level::DEBUG)
.pretty()
.finish();
tracing::subscriber::set_global_default(subscriber).expect("setting tracing default failed");
tracing::subscriber::set_global_default(subscriber)
.expect("setting tracing default failed");
}
match args.command {

168
src/mastodon_api.rs Normal file
View file

@ -0,0 +1,168 @@
use reqwest::multipart::Part;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, error::Error};
// API docs taken from https://github.com/superseriousbusiness/gotosocial/blob/main/docs/api/swagger.yaml
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MastodonErrorResp {
pub error: String,
}
impl MastodonErrorResp {
fn to_error(&self, error_code: u16) -> MastodonAPIError {
MastodonAPIError::new(self.error.clone(), error_code)
}
}
#[derive(Debug)]
struct MastodonAPIError {
error: String,
error_code: u16,
}
impl MastodonAPIError {
fn new(error: String, error_code: u16) -> MastodonAPIError {
MastodonAPIError { error, error_code }
}
}
impl std::fmt::Display for MastodonAPIError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}: {}", self.error_code, self.error)
}
}
impl Error for MastodonAPIError {
fn description(&self) -> &str {
self.error.as_str()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminEmoji {
pub id: String,
pub category: Option<String>,
pub shortcode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Emoji {
pub category: Option<String>,
pub shortcode: String,
}
pub struct MastodonAPI {
token: String,
api_base: String,
client: reqwest::Client,
}
impl MastodonAPI {
pub fn new(token: String, api_base: String) -> Self {
Self {
token,
api_base,
client: reqwest::Client::builder()
.user_agent("stickerdeploy")
.build()
.unwrap(),
}
}
pub async fn get_custom_emoji(
&self,
filter: &[String],
) -> Result<Vec<AdminEmoji>, Box<dyn Error>> {
let url = format!("{}/api/v1/admin/custom_emojis", self.api_base);
let mut query: HashMap<String, String> = HashMap::new();
query.insert("filter".to_string(), filter.join(","));
query.insert("limit".to_string(), "0".to_string());
let res = self
.client
.get(url)
.header(
"Authorization".to_string(),
format!("Bearer {}", self.token).to_string(),
)
.query(&query)
.send()
.await?;
let status = res.status();
if status.is_success() {
let json = res.json::<Vec<AdminEmoji>>().await?;
Ok(json)
} else {
let json = res.json::<MastodonErrorResp>().await?;
Err(Box::new(json.to_error(status.as_u16())))
}
}
pub async fn delete_custom_emoji(&self, id: &String) -> Result<(), Box<dyn Error>> {
let url = format!("{}/api/v1/admin/custom_emojis/{}", self.api_base, id);
let res = self
.client
.delete(url)
.header(
"Authorization".to_string(),
format!("Bearer {}", self.token).to_string(),
)
.send()
.await?;
let status = res.status();
if !status.is_success() {
let json = res.json::<MastodonErrorResp>().await?;
Err(Box::new(json.to_error(status.as_u16())))
} else {
Ok(())
}
}
pub async fn upload_emoji(
&self,
shortcode: String,
category: Option<String>,
file_data: Vec<u8>,
) -> Result<Emoji, Box<dyn Error>> {
let url = format!("{}/api/v1/admin/custom_emojis", self.api_base);
let mut form = reqwest::multipart::Form::new();
form = form.text("shortcode", shortcode.clone());
if let Some(category) = category {
form = form.text("category", category);
} else {
form = form.text("category", "");
}
form = form.part("image", Part::bytes(file_data).file_name("test.png"));
let res = self
.client
.post(url)
.header(
"Authorization".to_string(),
format!("Bearer {}", self.token).to_string(),
)
.multipart(form)
.send()
.await?;
let status = res.status();
if status.is_success() {
let json = res.json::<Emoji>().await?;
Ok(json)
} else {
let json = res.json::<MastodonErrorResp>().await?;
Err(Box::new(json.to_error(status.as_u16())))
}
}
}

View file

@ -15,26 +15,50 @@ pub struct DeployWhere {
pub deploy_to: String,
pub discord: Option<Vec<DiscordDeployLocation>>,
pub telegram: Option<TelegramDeployLocation>,
pub mastodon: Option<MastodonDeployLocation>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TelegramDeployLocation {
pub deploy_name: String,
#[serde(default = "telegram_credential_name_default")]
pub credential_name: String,
pub r#type: StickerType,
pub name: String,
pub user_id: u64,
}
fn telegram_credential_name_default() -> String {
"telegram".to_string()
}
#[derive(Debug, Clone, Deserialize)]
pub struct MastodonDeployLocation {
pub api_base: String,
#[serde(default = "mastodon_credential_name_default")]
pub credential_name: String,
pub category: Option<String>,
}
fn mastodon_credential_name_default() -> String {
"mastodon".to_string()
}
#[derive(Debug, Clone, Deserialize)]
pub struct DiscordDeployLocation {
pub deploy_name: String,
pub id: u64,
#[serde(default = "discord_credential_name_default")]
pub credential_name: String,
#[serde(default = "discord_max_regular_emoji_default")]
pub max_regular_emoji: u64,
#[serde(default = "discord_max_animated_emoji_default")]
pub max_animated_emoji: u64,
}
fn discord_credential_name_default() -> String {
"discord".to_string()
}
fn discord_max_regular_emoji_default() -> u64 {
50
}