Initial commit
This commit is contained in:
commit
c56c423491
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/target
|
||||
dist
|
||||
build
|
||||
result
|
||||
.direnv
|
1671
Cargo.lock
generated
Normal file
1671
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "stickerdeploy"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9.9"
|
||||
serde_with = "1.3.1"
|
||||
serenity = { version = "0.11" }
|
||||
indexmap = "1.9.1"
|
||||
reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls-native-roots", "json", "multipart"] }
|
||||
clap = { version = "3.2.17", features = ["derive"] }
|
6
default.nix
Normal file
6
default.nix
Normal file
|
@ -0,0 +1,6 @@
|
|||
(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in fetchTarball {
|
||||
url =
|
||||
"https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}) { src = ./.; }).defaultNix
|
60
flake.lock
Normal file
60
flake.lock
Normal file
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1650374568,
|
||||
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1660862418,
|
||||
"narHash": "sha256-/TT9ETUgMREkW8p1hco/yWkVQ6sGfaXrDswhmJpFYqE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d7a4a9397f769980f5a87f3e2d23acd434c295e3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "release-22.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"utils": "utils"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
56
flake.nix
Normal file
56
flake.nix
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
description = "A tool for deploying sticker packs";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/release-22.05";
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, utils, ... }:
|
||||
{
|
||||
overlay = final: prev:
|
||||
let system = final.system;
|
||||
in {
|
||||
stickerdeploy = final.rustPlatform.buildRustPackage rec {
|
||||
pname = "stickerdeploy";
|
||||
version = "latest";
|
||||
|
||||
src = ./.;
|
||||
cargoLock = { lockFile = ./Cargo.lock; };
|
||||
|
||||
doCheck = false;
|
||||
nativeBuildInputs = with final.pkgs; [ pkg-config rustc cargo ];
|
||||
};
|
||||
};
|
||||
} // utils.lib.eachSystem (utils.lib.defaultSystems) (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ self.overlay ];
|
||||
};
|
||||
in {
|
||||
defaultPackage = self.packages."${system}".stickerdeploy;
|
||||
packages.stickerdeploy = pkgs.stickerdeploy;
|
||||
|
||||
apps = rec {
|
||||
stickerdeploy = {
|
||||
type = "app";
|
||||
program = "${self.defaultPackage.${system}}/bin/stickerdeploy";
|
||||
};
|
||||
default = stickerdeploy;
|
||||
};
|
||||
|
||||
defaultApp = self.apps."${system}".stickerdeploy;
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
|
||||
buildInputs = with pkgs; [ rustc cargo rust-analyzer rustfmt clippy ];
|
||||
};
|
||||
|
||||
lib = pkgs.stickerdeploy.lib;
|
||||
});
|
||||
}
|
7
src/creds.rs
Normal file
7
src/creds.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Creds {
|
||||
pub discord_bot_token: String,
|
||||
pub telegram_bot_token: String,
|
||||
}
|
220
src/deploy_discord.rs
Normal file
220
src/deploy_discord.rs
Normal file
|
@ -0,0 +1,220 @@
|
|||
use crate::creds::Creds;
|
||||
use crate::sticker_config::{DiscordDeployLocation, Sticker, StickerConfig, StickerType};
|
||||
use indexmap::IndexMap;
|
||||
use serenity::http::client::Http as DiscordClient;
|
||||
//use serenity::cache::Cache as DiscordCache;
|
||||
|
||||
use serenity::model::prelude::Emoji;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
fn split_by(
|
||||
pack_contents: &[String],
|
||||
sticker_type: StickerType,
|
||||
deploy_locations: &[DiscordDeployLocation],
|
||||
pack_emojis: &HashMap<String, Sticker>,
|
||||
) -> IndexMap<String, Vec<(String, Sticker)>> {
|
||||
let pack_type_emoji_count = pack_emojis
|
||||
.iter()
|
||||
.filter(|emoji| emoji.1.r#type == sticker_type)
|
||||
.count();
|
||||
|
||||
let max_type_emoji_total: u64 = deploy_locations
|
||||
.iter()
|
||||
.map(|loc| {
|
||||
return match sticker_type {
|
||||
StickerType::Regular => loc.max_regular_emoji,
|
||||
StickerType::Gif => loc.max_animated_emoji,
|
||||
_ => unreachable!("wrong sticker type for discord"),
|
||||
};
|
||||
})
|
||||
.sum();
|
||||
|
||||
if pack_type_emoji_count > max_type_emoji_total.try_into().unwrap() {
|
||||
panic!("not enough space in servers for emoji, please add more servers to deploy across")
|
||||
}
|
||||
|
||||
let mut pack_type_emojis: VecDeque<(String, Sticker)> = pack_contents
|
||||
.iter()
|
||||
.filter(|emoji| {
|
||||
return pack_emojis.get(*emoji).unwrap().r#type == sticker_type;
|
||||
})
|
||||
.map(|emoji| {
|
||||
return (emoji.clone(), pack_emojis.get(emoji).unwrap().clone());
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut emojis_per_server: IndexMap<String, Vec<(String, Sticker)>> = IndexMap::new();
|
||||
for deploy_location in deploy_locations.iter() {
|
||||
emojis_per_server.insert(deploy_location.deploy_name.clone(), Vec::new());
|
||||
}
|
||||
|
||||
'outer: for deploy_location in deploy_locations.iter() {
|
||||
let mut sticker_count = 0;
|
||||
|
||||
let max_type_emoji = match sticker_type {
|
||||
StickerType::Regular => deploy_location.max_regular_emoji,
|
||||
StickerType::Gif => deploy_location.max_animated_emoji,
|
||||
_ => unreachable!("wrong sticker type for discord"),
|
||||
};
|
||||
|
||||
while sticker_count < max_type_emoji {
|
||||
let emoji = pack_type_emojis.pop_front();
|
||||
if emoji.is_none() {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
emojis_per_server
|
||||
.get_mut(&deploy_location.deploy_name)
|
||||
.unwrap()
|
||||
.push(emoji.unwrap());
|
||||
sticker_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
emojis_per_server
|
||||
}
|
||||
|
||||
pub async fn deploy_discord(
|
||||
deploy_id: String,
|
||||
sticker_config: StickerConfig,
|
||||
creds: Creds,
|
||||
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
|
||||
.iter()
|
||||
.map(|loc| (loc.deploy_name.clone(), loc))
|
||||
.collect();
|
||||
|
||||
let pack_contents = sticker_config.sticker_sets.get(&deploy_where.pack_id).unwrap();
|
||||
let pack_emojis: HashMap<String, Sticker> = pack_contents
|
||||
.iter()
|
||||
.map(|emoji_name| {
|
||||
return (
|
||||
emoji_name.clone(),
|
||||
sticker_config.stickers.get(emoji_name).unwrap().clone(),
|
||||
);
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut emojis_per_server: IndexMap<String, Vec<(String, Sticker)>> = IndexMap::new();
|
||||
for deploy_location in deploy_locations.iter() {
|
||||
emojis_per_server.insert(deploy_location.deploy_name.clone(), Vec::new());
|
||||
}
|
||||
|
||||
// block only so can hide in IDE
|
||||
{
|
||||
let regular_emoji_per_server = split_by(
|
||||
pack_contents,
|
||||
StickerType::Regular,
|
||||
&deploy_locations,
|
||||
&pack_emojis,
|
||||
);
|
||||
|
||||
for loc in regular_emoji_per_server.into_iter() {
|
||||
emojis_per_server
|
||||
.get_mut(&loc.0)
|
||||
.unwrap()
|
||||
.extend(loc.1.into_iter());
|
||||
}
|
||||
|
||||
let gif_emoji_per_server = split_by(
|
||||
pack_contents,
|
||||
StickerType::Gif,
|
||||
&deploy_locations,
|
||||
&pack_emojis,
|
||||
);
|
||||
|
||||
for loc in gif_emoji_per_server.into_iter() {
|
||||
emojis_per_server
|
||||
.get_mut(&loc.0)
|
||||
.unwrap()
|
||||
.extend(loc.1.into_iter());
|
||||
}
|
||||
}
|
||||
|
||||
for server_k_v in emojis_per_server.iter() {
|
||||
let deploy_name = server_k_v.0.to_owned();
|
||||
let deploy_emojis = server_k_v.1.to_owned();
|
||||
|
||||
println!("Deploying to {}", deploy_name);
|
||||
|
||||
let deploy_location = deploy_locations_map.get(&deploy_name).unwrap();
|
||||
|
||||
let deploy_emoji_names: Vec<String> = deploy_emojis
|
||||
.iter()
|
||||
.map(|deploy_emoji| deploy_emoji.0.clone())
|
||||
.collect();
|
||||
|
||||
let discord_emojis = discord_client
|
||||
.get_emojis(deploy_location.id)
|
||||
.await
|
||||
.expect("could not fetch discord emoji");
|
||||
let discord_emojis_names: Vec<String> = discord_emojis
|
||||
.iter()
|
||||
.map(|emoji| emoji.name.clone())
|
||||
.collect();
|
||||
|
||||
let invalid_emojis: Vec<Emoji> = discord_emojis
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|emoji| !deploy_emoji_names.contains(&emoji.name))
|
||||
.collect();
|
||||
|
||||
if !invalid_emojis.is_empty() {
|
||||
for emoji in invalid_emojis.iter() {
|
||||
println!("Removing Emoji {}", &emoji.name);
|
||||
discord_client
|
||||
.delete_emoji(deploy_location.id, emoji.id.0)
|
||||
.await
|
||||
.expect("could not delete emoji")
|
||||
}
|
||||
}
|
||||
|
||||
let missing_emojis: Vec<String> = deploy_emoji_names
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|emoji| !discord_emojis_names.contains(emoji))
|
||||
.collect();
|
||||
|
||||
if !missing_emojis.is_empty() {
|
||||
for emoji in missing_emojis.iter() {
|
||||
println!("Uploading Emoji {}", &emoji);
|
||||
let emoji_data = pack_emojis.get(emoji).unwrap();
|
||||
|
||||
let image_path = std::path::PathBuf::from(&base_stickerdb_path)
|
||||
.join(std::path::PathBuf::from(emoji_data.file.clone()));
|
||||
let image_data =
|
||||
serenity::utils::read_image(image_path).expect("could not open emoji file");
|
||||
|
||||
discord_client
|
||||
.create_emoji(
|
||||
deploy_location.id,
|
||||
&json!({
|
||||
"name": emoji,
|
||||
"image": image_data,
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("could not upload emoji");
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"Missing: {:#?}\nInvalid: {:#?}",
|
||||
missing_emojis, invalid_emojis
|
||||
);
|
||||
}
|
||||
|
||||
//println!("{:#?}", emojis_per_server);
|
||||
|
||||
//println!("{} {} {:?}", max_regular_emoji_total, max_animated_emoji_total, deploy_locations);
|
||||
}
|
280
src/deploy_telegram.rs
Normal file
280
src/deploy_telegram.rs
Normal file
|
@ -0,0 +1,280 @@
|
|||
use std::io::Read;
|
||||
use std::ops::Index;
|
||||
use std::{collections::HashMap, error::Error};
|
||||
|
||||
use crate::{
|
||||
creds::Creds,
|
||||
sticker_config::{Sticker, StickerConfig, StickerType},
|
||||
tg_api::{TelegramAPI, TelegramSticker},
|
||||
};
|
||||
|
||||
struct TelegramStickerIDState {
|
||||
// File ID to Sticker Name
|
||||
data: HashMap<String, String>,
|
||||
base_stickerdb_path: String,
|
||||
}
|
||||
|
||||
impl TelegramStickerIDState {
|
||||
pub fn new(base_stickerdb_path: &str) -> Self {
|
||||
TelegramStickerIDState {
|
||||
data: HashMap::new(),
|
||||
base_stickerdb_path: base_stickerdb_path.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
let path = std::path::PathBuf::from(&self.base_stickerdb_path).join("telegram_state.yml");
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.open(path)?;
|
||||
let data: HashMap<String, String> = serde_yaml::from_reader(file)?;
|
||||
self.data.extend(data.into_iter());
|
||||
Ok(())
|
||||
}
|
||||
pub fn save(&self) -> Result<(), Box<dyn Error>> {
|
||||
let path = std::path::PathBuf::from(&self.base_stickerdb_path).join("telegram_state.yml");
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(path)?;
|
||||
serde_yaml::to_writer(file, &self.data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, file_id: &String) -> Option<&String> {
|
||||
self.data.get(file_id)
|
||||
}
|
||||
|
||||
pub fn set(&mut self, file_id: &str, sticker_name: &str) {
|
||||
self.data.insert(file_id.to_owned(), sticker_name.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn deploy_telegram(
|
||||
deploy_id: String,
|
||||
sticker_config: StickerConfig,
|
||||
creds: Creds,
|
||||
base_stickerdb_path: String,
|
||||
) {
|
||||
let mut tg_state = TelegramStickerIDState::new(&base_stickerdb_path);
|
||||
tg_state.load().expect("could not load sticker state");
|
||||
|
||||
let deploy_where = sticker_config.deploy_where.get(&deploy_id).unwrap();
|
||||
|
||||
let deploy_location = deploy_where.telegram.as_ref().unwrap();
|
||||
|
||||
if deploy_location.r#type == StickerType::Gif {
|
||||
panic!("gif stickers not supported on telegram, bodge it into video")
|
||||
}
|
||||
|
||||
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 contains_invalid_stickers = pack_stickers
|
||||
.iter()
|
||||
.find(|sticker| sticker.1.r#type != deploy_location.r#type);
|
||||
|
||||
if contains_invalid_stickers.is_some() {
|
||||
panic!("pack contains a invalid type of emoji for pack type")
|
||||
}
|
||||
|
||||
let tg_bot = TelegramAPI::new(creds.telegram_bot_token);
|
||||
let tg_sticker_set = tg_bot
|
||||
.get_sticker_set(&deploy_location.name)
|
||||
.await
|
||||
.expect("could not get sticker pack");
|
||||
|
||||
let mut tg_stickers: Vec<(String, TelegramSticker)> = Vec::new();
|
||||
|
||||
// Delete stickers that arent in state
|
||||
// add valid stickers to tg_stickers
|
||||
{
|
||||
let mut tg_sticker_set_iter = tg_sticker_set.stickers.iter();
|
||||
// Skip the first sticker
|
||||
tg_sticker_set_iter.next();
|
||||
for sticker in tg_sticker_set_iter {
|
||||
match tg_state.get(&sticker.file_unique_id) {
|
||||
Some(sticker_name) => tg_stickers.push((sticker_name.clone(), sticker.clone())),
|
||||
None => {
|
||||
println!(
|
||||
"Deleting sticker with unique_id {} as is not in state",
|
||||
&sticker.file_unique_id
|
||||
);
|
||||
tg_bot
|
||||
.delete_sticker(&sticker.file_id)
|
||||
.await
|
||||
.expect("could not delete sticker");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete stickers that have a name but are no longer in the new pack
|
||||
{
|
||||
let invalid_stickers: Vec<TelegramSticker> = tg_stickers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|sticker| !pack_contents.contains(&sticker.0))
|
||||
.map(|sticker| sticker.1)
|
||||
.collect();
|
||||
|
||||
for invalid_sticker in invalid_stickers.iter() {
|
||||
println!(
|
||||
"Deleting sticker with name {} as is not in pack",
|
||||
&invalid_sticker.file_unique_id
|
||||
);
|
||||
tg_bot
|
||||
.delete_sticker(&invalid_sticker.file_id)
|
||||
.await
|
||||
.expect("could not delete sticker");
|
||||
|
||||
tg_stickers.retain(|sticker| {
|
||||
// keep if true
|
||||
sticker.1.file_unique_id != invalid_sticker.file_unique_id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// +1 for skipping the first sticker
|
||||
let new_sticker_position_base = tg_stickers.len() + 1;
|
||||
let mut new_stickers: Vec<String> = Vec::new();
|
||||
|
||||
// add stickers that arent in tg_stickers
|
||||
{
|
||||
let missing_stickers: Vec<String> = pack_contents
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|sticker| {
|
||||
for tg_sticker in tg_stickers.iter() {
|
||||
if tg_sticker.0 == sticker.clone() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
for sticker_name in missing_stickers.iter() {
|
||||
println!("Uploading Sticker: {}", sticker_name);
|
||||
let sticker_data = pack_stickers.get(sticker_name).unwrap();
|
||||
|
||||
let file_path = std::path::PathBuf::from(&base_stickerdb_path).join(&sticker_data.file);
|
||||
let mut file = std::fs::File::open(file_path).unwrap();
|
||||
let mut file_data: Vec<u8> = Vec::new();
|
||||
file.read_to_end(&mut file_data)
|
||||
.expect("could not read file");
|
||||
|
||||
let mut png_sticker: Option<Vec<u8>> = None;
|
||||
let mut tgs_sticker: Option<Vec<u8>> = None;
|
||||
let mut webm_sticker: Option<Vec<u8>> = None;
|
||||
|
||||
match deploy_location.r#type {
|
||||
StickerType::Regular => {
|
||||
png_sticker = Some(file_data);
|
||||
}
|
||||
StickerType::TelegramVideo => {
|
||||
webm_sticker = Some(file_data);
|
||||
}
|
||||
StickerType::TelegramAnimated => {
|
||||
tgs_sticker = Some(file_data);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
tg_bot
|
||||
.upload_sticker(
|
||||
deploy_location.user_id,
|
||||
deploy_location.name.clone(),
|
||||
Some(sticker_data.emojis.concat()),
|
||||
png_sticker,
|
||||
tgs_sticker,
|
||||
webm_sticker,
|
||||
)
|
||||
.await
|
||||
.expect("could not upload sticker");
|
||||
|
||||
new_stickers.push(sticker_name.clone());
|
||||
}
|
||||
|
||||
println!("{:#?}", missing_stickers);
|
||||
}
|
||||
|
||||
// Update tg_stickers with current sticker contents
|
||||
{
|
||||
let tg_new_sticker_set = tg_bot
|
||||
.get_sticker_set(&deploy_location.name)
|
||||
.await
|
||||
.expect("could not get sticker pack");
|
||||
|
||||
for (i, sticker_name) in new_stickers.iter().enumerate() {
|
||||
let new_sticker = tg_new_sticker_set
|
||||
.stickers
|
||||
.index(new_sticker_position_base + i)
|
||||
.clone();
|
||||
|
||||
tg_state.set(&new_sticker.file_unique_id, sticker_name);
|
||||
|
||||
tg_stickers.push((
|
||||
sticker_name.clone(),
|
||||
tg_new_sticker_set
|
||||
.stickers
|
||||
.index(new_sticker_position_base + i)
|
||||
.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
tg_state.save().expect("could not save state");
|
||||
|
||||
// Reorder stickers using order of pack_contents
|
||||
{
|
||||
for (correct_sticker_i, sticker_name) in pack_contents.iter().enumerate() {
|
||||
let pack_sticker = sticker_name.clone();
|
||||
let tg_sticker = tg_stickers.index(correct_sticker_i).0.clone();
|
||||
if pack_sticker != tg_sticker {
|
||||
println!(
|
||||
"Sticker in position {} is the wrong sticker",
|
||||
correct_sticker_i
|
||||
);
|
||||
|
||||
let mut sticker_incorrect_pos: Option<usize> = None;
|
||||
for (sticker_i, sticker) in tg_stickers.iter().enumerate() {
|
||||
if sticker.0 == pack_sticker {
|
||||
sticker_incorrect_pos = Some(sticker_i);
|
||||
}
|
||||
}
|
||||
if sticker_incorrect_pos.is_none() {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
println!(
|
||||
"Moving sticker at position {} to {}",
|
||||
sticker_incorrect_pos.unwrap(),
|
||||
correct_sticker_i
|
||||
);
|
||||
|
||||
let sticker = tg_stickers.remove(sticker_incorrect_pos.unwrap());
|
||||
tg_stickers.insert(correct_sticker_i, sticker.clone());
|
||||
|
||||
tg_bot
|
||||
.set_sticker_position(sticker.1.file_id.clone(), correct_sticker_i + 1)
|
||||
.await
|
||||
.expect("could not move sticker");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/main.rs
Normal file
57
src/main.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
pub mod creds;
|
||||
pub mod deploy_discord;
|
||||
pub mod deploy_telegram;
|
||||
pub mod sticker_config;
|
||||
pub mod tg_api;
|
||||
|
||||
use creds::Creds;
|
||||
use deploy_discord::deploy_discord;
|
||||
use deploy_telegram::deploy_telegram;
|
||||
use sticker_config::StickerConfig;
|
||||
|
||||
use clap::Parser;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap()]
|
||||
pub struct CLIArgs {
|
||||
pub folder: String,
|
||||
pub deploy_id: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = CLIArgs::parse();
|
||||
|
||||
let base_path = PathBuf::from(&args.folder);
|
||||
let config_path = base_path.join("stickers.yml");
|
||||
let creds_path = base_path.join("../stickerdb/creds.yml");
|
||||
|
||||
let config_file = File::open(config_path).expect("could not open stickers.yml");
|
||||
let creds_file = File::open(creds_path).expect("could not open creds.yml");
|
||||
|
||||
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");
|
||||
|
||||
match config
|
||||
.deploy_where
|
||||
.get(&args.deploy_id)
|
||||
.expect("no deploy config with id specified found")
|
||||
.deploy_to
|
||||
.as_str()
|
||||
{
|
||||
"discord" => {
|
||||
println!("deploying {} to discord", &args.deploy_id);
|
||||
deploy_discord(args.deploy_id, config, creds, args.folder).await;
|
||||
}
|
||||
"telegram" => {
|
||||
println!("deploying {} to telegram", &args.deploy_id);
|
||||
deploy_telegram(args.deploy_id, config, creds, args.folder).await;
|
||||
}
|
||||
_ => {
|
||||
panic!("deploy_to not set")
|
||||
}
|
||||
}
|
||||
}
|
99
src/sticker_config.rs
Normal file
99
src/sticker_config.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct StickerConfig {
|
||||
pub stickers: HashMap<String, Sticker>,
|
||||
pub credits: HashMap<String, Credit>,
|
||||
pub deploy_where: HashMap<String, DeployWhere>,
|
||||
pub sticker_sets: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DeployWhere {
|
||||
pub pack_id: String,
|
||||
pub deploy_to: String,
|
||||
pub discord: Option<Vec<DiscordDeployLocation>>,
|
||||
pub telegram: Option<TelegramDeployLocation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct TelegramDeployLocation {
|
||||
pub deploy_name: String,
|
||||
pub r#type: StickerType,
|
||||
pub name: String,
|
||||
pub user_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DiscordDeployLocation {
|
||||
pub deploy_name: String,
|
||||
pub id: u64,
|
||||
#[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_max_regular_emoji_default() -> u64 {
|
||||
50
|
||||
}
|
||||
|
||||
fn discord_max_animated_emoji_default() -> u64 {
|
||||
50
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Credit {
|
||||
pub artist: String,
|
||||
pub source: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Sticker {
|
||||
pub file: String,
|
||||
pub emojis: Vec<String>,
|
||||
pub alt_text: Option<String>,
|
||||
pub credit: Option<String>,
|
||||
#[serde(default)]
|
||||
pub r#type: StickerType,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde_with::DeserializeFromStr)]
|
||||
pub enum StickerType {
|
||||
Regular,
|
||||
Gif,
|
||||
TelegramVideo,
|
||||
TelegramAnimated,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for StickerType {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"regular" => Ok(Self::Regular),
|
||||
"gif" => Ok(Self::Gif),
|
||||
"telegram_video" => Ok(Self::TelegramVideo),
|
||||
"telegram_animated" => Ok(Self::TelegramAnimated),
|
||||
_ => Err(String::from("invalid sticker type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StickerType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match *self {
|
||||
StickerType::Regular => write!(f, "regular"),
|
||||
StickerType::Gif => write!(f, "gif"),
|
||||
StickerType::TelegramVideo => write!(f, "telegram_video"),
|
||||
StickerType::TelegramAnimated => write!(f, "telegram_animated"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StickerType {
|
||||
fn default() -> Self {
|
||||
StickerType::Regular
|
||||
}
|
||||
}
|
196
src/tg_api.rs
Normal file
196
src/tg_api.rs
Normal file
|
@ -0,0 +1,196 @@
|
|||
use std::{collections::HashMap, error::Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const API_BASE: &str = "https://api.telegram.org";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TelegramAPIResponse<T> {
|
||||
pub ok: bool,
|
||||
pub error_code: Option<u16>,
|
||||
pub description: Option<String>,
|
||||
pub result: Option<T>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TelegramStickerSet {
|
||||
pub name: String,
|
||||
pub title: String,
|
||||
pub is_animated: bool,
|
||||
pub is_video: bool,
|
||||
pub stickers: Vec<TelegramSticker>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TelegramSticker {
|
||||
pub file_id: String,
|
||||
pub file_unique_id: String,
|
||||
pub is_animated: bool,
|
||||
pub is_video: bool,
|
||||
pub emoji: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TelegramError {
|
||||
error_code: u16,
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl TelegramError {
|
||||
fn new(error_code: u16, description: String) -> TelegramError {
|
||||
TelegramError {
|
||||
error_code,
|
||||
description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TelegramError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{} {}", self.error_code, self.description)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for TelegramError {
|
||||
fn description(&self) -> &str {
|
||||
self.description.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TelegramAPI {
|
||||
token: String,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl TelegramAPI {
|
||||
pub fn new(token: String) -> Self {
|
||||
Self {
|
||||
token,
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_sticker_set(
|
||||
&self,
|
||||
pack_name: &str,
|
||||
) -> Result<TelegramStickerSet, Box<dyn Error>> {
|
||||
let url = format!("{}/bot{}/getStickerSet", API_BASE, self.token);
|
||||
let res = self
|
||||
.client
|
||||
.get(url)
|
||||
.form(&HashMap::from([("name", pack_name)]))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let res_json = res
|
||||
.json::<TelegramAPIResponse<TelegramStickerSet>>()
|
||||
.await?;
|
||||
|
||||
if !res_json.ok {
|
||||
return Err(Box::new(TelegramError::new(
|
||||
res_json.error_code.unwrap(),
|
||||
res_json.description.unwrap(),
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(res_json.result.unwrap())
|
||||
}
|
||||
|
||||
pub async fn delete_sticker(&self, sticker_file_id: &str) -> Result<bool, Box<dyn Error>> {
|
||||
let url = format!("{}/bot{}/deleteStickerFromSet", API_BASE, self.token);
|
||||
let res = self
|
||||
.client
|
||||
.get(url)
|
||||
.form(&HashMap::from([("sticker", sticker_file_id)]))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let res_json = res.json::<TelegramAPIResponse<bool>>().await?;
|
||||
|
||||
if !res_json.ok {
|
||||
return Err(Box::new(TelegramError::new(
|
||||
res_json.error_code.unwrap(),
|
||||
res_json.description.unwrap(),
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(res_json.result.unwrap())
|
||||
}
|
||||
|
||||
pub async fn upload_sticker(
|
||||
&self,
|
||||
user_id: u64,
|
||||
sticker_pack_name: String,
|
||||
emojis: Option<String>,
|
||||
png_sticker: Option<Vec<u8>>,
|
||||
tgs_sticker: Option<Vec<u8>>,
|
||||
webm_sticker: Option<Vec<u8>>,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
let url = format!("{}/bot{}/addStickerToSet", API_BASE, self.token);
|
||||
|
||||
let mut form = reqwest::multipart::Form::new();
|
||||
form = form.text("user_id", user_id.to_string());
|
||||
form = form.text("name", sticker_pack_name);
|
||||
if emojis.is_some() {
|
||||
form = form.text("emojis", emojis.unwrap());
|
||||
}
|
||||
if png_sticker.is_some() {
|
||||
form = form.part(
|
||||
"png_sticker",
|
||||
reqwest::multipart::Part::bytes(png_sticker.unwrap()).file_name(""),
|
||||
);
|
||||
}
|
||||
if tgs_sticker.is_some() {
|
||||
form = form.part(
|
||||
"tgs_sticker",
|
||||
reqwest::multipart::Part::bytes(tgs_sticker.unwrap()),
|
||||
);
|
||||
}
|
||||
if webm_sticker.is_some() {
|
||||
form = form.part(
|
||||
"webm_sticker",
|
||||
reqwest::multipart::Part::bytes(webm_sticker.unwrap()),
|
||||
);
|
||||
}
|
||||
|
||||
let res = self.client.get(url).multipart(form).send().await?;
|
||||
|
||||
let res_json = res.json::<TelegramAPIResponse<bool>>().await?;
|
||||
|
||||
if !res_json.ok {
|
||||
return Err(Box::new(TelegramError::new(
|
||||
res_json.error_code.unwrap(),
|
||||
res_json.description.unwrap(),
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(res_json.result.unwrap())
|
||||
}
|
||||
|
||||
pub async fn set_sticker_position(
|
||||
&self,
|
||||
sticker_file_id: String,
|
||||
sticker_position: usize,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
let url = format!("{}/bot{}/setStickerPositionInSet", API_BASE, self.token);
|
||||
let res = self
|
||||
.client
|
||||
.get(url)
|
||||
.form(&HashMap::from([
|
||||
("sticker", sticker_file_id),
|
||||
("position", sticker_position.to_string()),
|
||||
]))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let res_json = res.json::<TelegramAPIResponse<bool>>().await?;
|
||||
|
||||
if !res_json.ok {
|
||||
return Err(Box::new(TelegramError::new(
|
||||
res_json.error_code.unwrap(),
|
||||
res_json.description.unwrap(),
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(res_json.result.unwrap())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue