use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use reqwest::Method;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::error::{Error, Result};
use crate::types::*;
pub const DEFAULT_API: &str = "https://zynko.org/api";
#[derive(Debug, Clone)]
pub struct FileInput {
pub name: String,
pub data: Vec<u8>,
pub content_type: Option<String>,
}
impl FileInput {
pub fn new(name: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
Self {
name: name.into(),
data: data.into(),
content_type: None,
}
}
pub async fn from_path(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let data = tokio::fs::read(path).await?;
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("arquivo")
.to_string();
Ok(Self::new(name, data))
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
self.content_type = Some(content_type.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct SendOptions {
pub content: String,
pub reply_to: Option<Id>,
pub files: Vec<FileInput>,
pub nonce: Option<String>,
pub cards: Vec<Card>,
pub components: Components,
}
impl SendOptions {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
..Default::default()
}
}
pub fn reply_to(mut self, id: impl Into<Id>) -> Self {
self.reply_to = Some(id.into());
self
}
pub fn file(mut self, file: FileInput) -> Self {
self.files.push(file);
self
}
pub fn nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
pub fn card(mut self, card: Card) -> Self {
self.cards.push(card);
self
}
pub fn row(mut self, row: Vec<Button>) -> Self {
self.components.push(row);
self
}
}
impl From<&str> for SendOptions {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for SendOptions {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&String> for SendOptions {
fn from(s: &String) -> Self {
Self::new(s.clone())
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ReplyOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cards: Option<Vec<Card>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Components>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub ephemeral: bool,
}
impl ReplyOptions {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: Some(content.into()),
..Default::default()
}
}
pub fn ephemeral(mut self) -> Self {
self.ephemeral = true;
self
}
pub fn card(mut self, card: Card) -> Self {
self.cards.get_or_insert_with(Vec::new).push(card);
self
}
pub fn row(mut self, row: Vec<Button>) -> Self {
self.components.get_or_insert_with(Vec::new).push(row);
self
}
}
impl From<&str> for ReplyOptions {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for ReplyOptions {
fn from(s: String) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct EditOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cards: Option<Vec<Card>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub components: Option<Components>,
}
impl EditOptions {
pub fn new() -> Self {
Self::default()
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
pub fn cards(mut self, cards: Vec<Card>) -> Self {
self.cards = Some(cards);
self
}
pub fn components(mut self, components: Components) -> Self {
self.components = Some(components);
self
}
}
impl From<&str> for EditOptions {
fn from(s: &str) -> Self {
Self::new().content(s)
}
}
impl From<String> for EditOptions {
fn from(s: String) -> Self {
Self::new().content(s)
}
}
#[derive(Debug, Clone, Default)]
pub struct MessagesQuery {
pub before: Option<Id>,
pub after: Option<Id>,
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct NewRole {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hoist: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct NewChannel {
pub name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub kind: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topic: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ChannelEdit {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topic: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<Option<Id>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rate_limit_per_user: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct InviteOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_age_secs: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_uses: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum CallbackType {
Message,
Update,
}
enum Body<'a> {
None,
Json(&'a Value),
File(&'a FileInput),
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct ApiErrorBody {
code: Option<String>,
message: Option<String>,
retry_after_ms: Option<u64>,
}
#[derive(Clone)]
pub struct Rest {
inner: Arc<RestInner>,
}
struct RestInner {
api: String,
auth: String,
retries: u32,
http: reqwest::Client,
}
impl std::fmt::Debug for Rest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Rest")
.field("api", &self.inner.api)
.finish_non_exhaustive()
}
}
fn enc(s: &str) -> String {
let mut out = String::new();
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}
impl Rest {
pub fn new(token: &str, api: Option<&str>, retries: u32) -> Self {
crate::install_crypto();
let api = api.unwrap_or(DEFAULT_API).trim_end_matches('/').to_string();
let http = reqwest::Client::builder()
.user_agent(concat!("zynko-rust/", env!("CARGO_PKG_VERSION")))
.build()
.expect("cliente HTTP");
Self {
inner: Arc::new(RestInner {
api,
auth: format!("Bot {token}"),
retries,
http,
}),
}
}
pub fn api(&self) -> &str {
&self.inner.api
}
pub fn base(&self) -> &str {
self.inner
.api
.strip_suffix("/api")
.unwrap_or(&self.inner.api)
}
pub async fn request<T: DeserializeOwned>(
&self,
method: Method,
path: &str,
body: Option<Value>,
) -> Result<T> {
match &body {
Some(v) => self.send_req(method, path, Body::Json(v)).await,
None => self.send_req(method, path, Body::None).await,
}
}
async fn send_req<T: DeserializeOwned>(
&self,
method: Method,
path: &str,
body: Body<'_>,
) -> Result<T> {
let url = format!("{}{}", self.inner.api, path);
let mut attempt = 0;
loop {
let mut req = self
.inner
.http
.request(method.clone(), &url)
.header("Authorization", &self.inner.auth);
req = match &body {
Body::None => req,
Body::Json(v) => req.json(v),
Body::File(f) => {
let mut part =
reqwest::multipart::Part::bytes(f.data.clone()).file_name(f.name.clone());
if let Some(ct) = &f.content_type {
part = part.mime_str(ct)?;
}
req.multipart(reqwest::multipart::Form::new().part("file", part))
}
};
let res = req.send().await?;
let status = res.status();
if status.is_success() {
let text = res.text().await?;
let value = if text.trim().is_empty() {
Value::Null
} else {
serde_json::from_str(&text)?
};
return Ok(serde_json::from_value(value)?);
}
let header_wait = res
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<f64>().ok());
let reason = status.canonical_reason().unwrap_or("").to_string();
let text = res.text().await.unwrap_or_default();
let err: ApiErrorBody = serde_json::from_str(&text).unwrap_or_default();
if status.as_u16() == 429
&& attempt < self.inner.retries
&& err.code.as_deref() == Some("rate_limited")
{
let ms = err
.retry_after_ms
.unwrap_or_else(|| (header_wait.unwrap_or(1.0) * 1000.0) as u64);
tokio::time::sleep(Duration::from_millis(ms + 50)).await;
attempt += 1;
continue;
}
return Err(Error::Api {
status: status.as_u16(),
code: err
.code
.unwrap_or_else(|| format!("http_{}", status.as_u16())),
message: err.message.unwrap_or(reason),
});
}
}
pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
self.request(Method::GET, path, None).await
}
pub async fn post<T: DeserializeOwned>(&self, path: &str, body: Option<Value>) -> Result<T> {
self.request(Method::POST, path, Some(body.unwrap_or_else(|| json!({}))))
.await
}
pub async fn patch<T: DeserializeOwned>(&self, path: &str, body: Value) -> Result<T> {
self.request(Method::PATCH, path, Some(body)).await
}
pub async fn put<T: DeserializeOwned>(&self, path: &str, body: Option<Value>) -> Result<T> {
self.request(Method::PUT, path, body).await
}
pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
self.request(Method::DELETE, path, None).await
}
pub async fn send(
&self,
channel_id: &str,
message: impl Into<SendOptions>,
) -> Result<MessageData> {
let opts = message.into();
let mut attachment_ids = Vec::new();
for f in &opts.files {
attachment_ids.push(self.upload(channel_id, f).await?.id);
}
let mut body = json!({ "content": opts.content });
if let Some(r) = &opts.reply_to {
body["reply_to"] = json!(r);
}
if let Some(n) = &opts.nonce {
body["nonce"] = json!(n);
}
if !attachment_ids.is_empty() {
body["attachment_ids"] = json!(attachment_ids);
}
if !opts.cards.is_empty() {
body["cards"] = serde_json::to_value(&opts.cards)?;
}
if !opts.components.is_empty() {
body["components"] = serde_json::to_value(&opts.components)?;
}
self.post(&format!("/channels/{channel_id}/messages"), Some(body))
.await
}
pub async fn upload(&self, channel_id: &str, file: &FileInput) -> Result<Attachment> {
self.send_req(
Method::POST,
&format!("/channels/{channel_id}/attachments"),
Body::File(file),
)
.await
}
pub async fn edit_message(
&self,
channel_id: &str,
message_id: &str,
edit: impl Into<EditOptions>,
) -> Result<MessageData> {
let body = serde_json::to_value(edit.into())?;
self.patch(
&format!("/channels/{channel_id}/messages/{message_id}"),
body,
)
.await
}
pub async fn delete_message(&self, channel_id: &str, message_id: &str) -> Result<()> {
self.delete::<Value>(&format!("/channels/{channel_id}/messages/{message_id}"))
.await
.map(drop)
}
pub async fn messages(&self, channel_id: &str, q: MessagesQuery) -> Result<Vec<MessageData>> {
let mut params = Vec::new();
if let Some(b) = &q.before {
params.push(format!("before={}", enc(b)));
}
if let Some(a) = &q.after {
params.push(format!("after={}", enc(a)));
}
if let Some(l) = q.limit {
params.push(format!("limit={l}"));
}
let qs = if params.is_empty() {
String::new()
} else {
format!("?{}", params.join("&"))
};
self.get(&format!("/channels/{channel_id}/messages{qs}"))
.await
}
pub async fn react(&self, channel_id: &str, message_id: &str, emoji: &str) -> Result<()> {
self.put::<Value>(
&format!(
"/channels/{channel_id}/messages/{message_id}/reactions/{}",
enc(emoji)
),
None,
)
.await
.map(drop)
}
pub async fn unreact(&self, channel_id: &str, message_id: &str, emoji: &str) -> Result<()> {
self.delete::<Value>(&format!(
"/channels/{channel_id}/messages/{message_id}/reactions/{}",
enc(emoji)
))
.await
.map(drop)
}
pub async fn typing(&self, channel_id: &str) -> Result<()> {
self.post::<Value>(&format!("/channels/{channel_id}/typing"), None)
.await
.map(drop)
}
pub async fn me(&self) -> Result<User> {
self.get("/users/@me").await
}
pub async fn profile(&self, user_id: &str) -> Result<Profile> {
self.get(&format!("/users/{user_id}/profile")).await
}
pub async fn dm(&self, user_id: &str) -> Result<Channel> {
self.post(
"/users/@me/channels",
Some(json!({ "recipient_id": user_id })),
)
.await
}
pub async fn members(&self, guild_id: &str) -> Result<Vec<Member>> {
self.get(&format!("/guilds/{guild_id}/members")).await
}
pub async fn kick(&self, guild_id: &str, user_id: &str) -> Result<()> {
self.delete::<Value>(&format!("/guilds/{guild_id}/members/{user_id}"))
.await
.map(drop)
}
pub async fn ban(&self, guild_id: &str, user_id: &str) -> Result<()> {
self.put::<Value>(&format!("/guilds/{guild_id}/bans/{user_id}"), None)
.await
.map(drop)
}
pub async fn unban(&self, guild_id: &str, user_id: &str) -> Result<()> {
self.delete::<Value>(&format!("/guilds/{guild_id}/bans/{user_id}"))
.await
.map(drop)
}
pub async fn add_role(&self, guild_id: &str, user_id: &str, role_id: &str) -> Result<()> {
self.put::<Value>(
&format!("/guilds/{guild_id}/members/{user_id}/roles/{role_id}"),
None,
)
.await
.map(drop)
}
pub async fn remove_role(&self, guild_id: &str, user_id: &str, role_id: &str) -> Result<()> {
self.delete::<Value>(&format!(
"/guilds/{guild_id}/members/{user_id}/roles/{role_id}"
))
.await
.map(drop)
}
pub async fn create_role(&self, guild_id: &str, role: &NewRole) -> Result<Role> {
self.post(
&format!("/guilds/{guild_id}/roles"),
Some(serde_json::to_value(role)?),
)
.await
}
pub async fn create_channel(&self, guild_id: &str, channel: &NewChannel) -> Result<Channel> {
self.post(
&format!("/guilds/{guild_id}/channels"),
Some(serde_json::to_value(channel)?),
)
.await
}
pub async fn edit_channel(&self, channel_id: &str, edit: &ChannelEdit) -> Result<Channel> {
self.patch(
&format!("/channels/{channel_id}"),
serde_json::to_value(edit)?,
)
.await
}
pub async fn delete_channel(&self, channel_id: &str) -> Result<()> {
self.delete::<Value>(&format!("/channels/{channel_id}"))
.await
.map(drop)
}
pub async fn create_invite(&self, guild_id: &str, opts: &InviteOptions) -> Result<Invite> {
self.post(
&format!("/guilds/{guild_id}/invites"),
Some(serde_json::to_value(opts)?),
)
.await
}
pub async fn set_commands(&self, commands: &[Command]) -> Result<Vec<Command>> {
self.put("/users/@me/commands", Some(serde_json::to_value(commands)?))
.await
}
pub async fn commands(&self) -> Result<Vec<Command>> {
self.get("/users/@me/commands").await
}
pub async fn interaction_callback(
&self,
id: &str,
token: &str,
kind: CallbackType,
body: impl Serialize,
) -> Result<MessageData> {
let mut v = serde_json::to_value(body)?;
if !v.is_object() {
v = json!({});
}
v["type"] = json!(kind);
self.post(&format!("/interactions/{id}/{token}/callback"), Some(v))
.await
}
pub async fn voice_token(&self, channel_id: &str) -> Result<VoiceJoin> {
self.post(&format!("/channels/{channel_id}/voice"), None)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_emoji_segments() {
assert_eq!(enc("abc_1"), "abc_1");
assert_eq!(enc("👍"), "%F0%9F%91%8D");
assert_eq!(enc("gato:123"), "gato%3A123");
}
#[test]
fn base_strips_api() {
let r = Rest::new("x", Some("http://127.0.0.1:8080/api/"), 3);
assert_eq!(r.api(), "http://127.0.0.1:8080/api");
assert_eq!(r.base(), "http://127.0.0.1:8080");
}
#[test]
fn edit_options_send_only_what_is_set() {
let v = serde_json::to_value(EditOptions::new().components(vec![])).unwrap();
assert_eq!(v, json!({ "components": [] }));
let v = serde_json::to_value(ReplyOptions::new("oi").ephemeral()).unwrap();
assert_eq!(v, json!({ "content": "oi", "ephemeral": true }));
}
}