1
0
Fork 0

Initial commit

This commit is contained in:
ChaotiCryptidz 2022-08-22 16:39:05 +01:00
commit c56c423491
No known key found for this signature in database
13 changed files with 2675 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/target
dist
build
result
.direnv

1671
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

17
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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())
}
}