use crate::error::{ApiError, BotError, Result};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::borrow::Cow;
use std::fmt::*;
use std::time::Duration;
#[cfg(feature = "templates")]
use tera::{Context, Tera};
use tracing::debug;
pub const VKTEAMS_BOT_API_URL: &str = "VKTEAMS_BOT_API_URL";
pub const VKTEAMS_BOT_API_TOKEN: &str = "VKTEAMS_BOT_API_TOKEN";
pub const VKTEAMS_PROXY: &str = "VKTEAMS_PROXY";
pub const POLL_TIME: u64 = 30;
pub const POLL_DURATION: &Duration = &Duration::from_secs(POLL_TIME + 10);
pub const SERVICE_NAME: &str = "BOT";
#[derive(Debug)]
pub enum APIVersionUrl {
V1,
}
#[derive(Debug, Default)]
pub enum HTTPMethod {
#[default]
GET,
POST,
}
#[derive(Debug, Default)]
pub enum HTTPBody {
MultiPart(MultipartName),
#[default]
None,
}
pub trait BotRequest {
type Args;
const METHOD: &'static str;
const HTTP_METHOD: HTTPMethod = HTTPMethod::GET;
type RequestType: Serialize + Debug + Default;
type ResponseType: Serialize + DeserializeOwned + Debug + Default;
fn get_multipart(&self) -> &MultipartName;
fn new(args: Self::Args) -> Self;
fn get_chat_id(&self) -> Option<&ChatId>;
}
pub type EventId = u32;
#[derive(Serialize, Clone, Debug)]
pub enum MessageTextFormat {
Plain(String),
Bold(String),
Italic(String),
Underline(String),
Strikethrough(String),
Link(String, String),
Mention(ChatId),
Code(String),
Pre(String, Option<String>),
OrderedList(Vec<String>),
UnOrderedList(Vec<String>),
Quote(String),
None,
}
#[derive(Default, Clone, Debug)]
pub struct MessageTextParser {
pub text: Vec<MessageTextFormat>,
#[cfg(feature = "templates")]
pub ctx: Context,
#[cfg(feature = "templates")]
pub name: String,
#[cfg(feature = "templates")]
pub tmpl: Tera,
pub parse_mode: ParseMode,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ButtonKeyboard {
pub text: String, #[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<ButtonStyle>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Keyboard {
pub buttons: Vec<Vec<ButtonKeyboard>>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum ButtonStyle {
Primary,
Attention,
#[default]
Base,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ParseMode {
MarkdownV2,
#[default]
HTML,
#[cfg(feature = "templates")]
Template,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventMessage {
pub event_id: EventId,
#[serde(flatten)]
pub event_type: EventType,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type", content = "payload")]
pub enum EventType {
NewMessage(Box<EventPayloadNewMessage>),
EditedMessage(Box<EventPayloadEditedMessage>),
DeleteMessage(Box<EventPayloadDeleteMessage>),
PinnedMessage(Box<EventPayloadPinnedMessage>),
UnpinnedMessage(Box<EventPayloadUnpinnedMessage>),
NewChatMembers(Box<EventPayloadNewChatMembers>),
LeftChatMembers(Box<EventPayloadLeftChatMembers>),
CallbackQuery(Box<EventPayloadCallbackQuery>),
#[default]
None,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventPayloadNewMessage {
pub msg_id: MsgId,
#[serde(default)]
pub text: String,
pub chat: Chat,
pub from: From,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<MessageFormat>,
#[serde(default)]
pub parts: Vec<MessageParts>,
pub timestamp: Timestamp,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventPayloadEditedMessage {
pub msg_id: MsgId,
pub text: String,
pub timestamp: Timestamp,
pub chat: Chat,
pub from: From,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<MessageFormat>,
pub edited_timestamp: Timestamp,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventPayloadDeleteMessage {
pub msg_id: MsgId,
pub chat: Chat,
pub timestamp: Timestamp,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventPayloadPinnedMessage {
pub msg_id: MsgId,
pub chat: Chat,
pub from: From,
#[serde(default)]
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<MessageFormat>,
#[serde(default)]
pub parts: Vec<MessageParts>,
pub timestamp: Timestamp,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventPayloadUnpinnedMessage {
pub msg_id: MsgId,
pub chat: Chat,
pub timestamp: Timestamp,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventPayloadNewChatMembers {
pub chat: Chat,
pub new_members: Vec<From>,
pub added_by: From,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventPayloadLeftChatMembers {
pub chat: Chat,
pub left_members: Vec<From>,
#[serde(skip_serializing_if = "Option::is_none")]
pub removed_by: Option<From>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EventPayloadCallbackQuery {
pub query_id: QueryId,
pub from: From,
#[serde(default)]
pub chat: Chat,
pub message: EventPayloadNewMessage,
pub callback_data: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessageParts {
#[serde(rename = "type", flatten)]
pub part_type: MessagePartsType,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type", content = "payload")]
pub enum MessagePartsType {
Sticker(MessagePartsPayloadSticker),
Mention(MessagePartsPayloadMention),
Voice(MessagePartsPayloadVoice),
File(Box<MessagePartsPayloadFile>),
Forward(Box<MessagePartsPayloadForward>),
Reply(Box<MessagePartsPayloadReply>),
InlineKeyboardMarkup(Vec<Vec<MessagePartsPayloadInlineKeyboard>>),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessagePartsPayloadSticker {
pub file_id: FileId,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessagePartsPayloadMention {
#[serde(flatten)]
pub user_id: From,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessagePartsPayloadVoice {
pub file_id: FileId,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessagePartsPayloadFile {
pub file_id: FileId,
#[serde(rename = "type", default)]
pub file_type: String,
#[serde(default)]
pub caption: String,
#[serde(default)]
pub format: MessageFormat,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessagePartsPayloadForward {
message: MessagePayload,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessagePartsPayloadReply {
message: MessagePayload,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessagePartsPayloadInlineKeyboard {
#[serde(default)]
pub callback_data: String,
pub style: ButtonStyle,
pub text: String,
#[serde(default)]
pub url: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessageFormat {
#[serde(skip_serializing_if = "Option::is_none")]
pub bold: Option<Vec<MessageFormatStruct>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub italic: Option<Vec<MessageFormatStruct>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub underline: Option<Vec<MessageFormatStruct>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strikethrough: Option<Vec<MessageFormatStruct>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub link: Option<Vec<MessageFormatStructLink>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mention: Option<Vec<MessageFormatStruct>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inline_code: Option<Vec<MessageFormatStruct>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre: Option<Vec<MessageFormatStructPre>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ordered_list: Option<Vec<MessageFormatStruct>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quote: Option<Vec<MessageFormatStruct>>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessageFormatStruct {
pub offset: i32,
pub length: i32,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessageFormatStructLink {
pub offset: i32,
pub length: i32,
pub url: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessageFormatStructPre {
pub offset: i32,
pub length: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessagePayload {
pub from: From,
pub msg_id: MsgId,
#[serde(default)]
pub text: String,
pub timestamp: u64,
#[serde(default)]
pub parts: Vec<MessageParts>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
pub struct ChatId(pub Cow<'static, str>);
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
pub struct MsgId(pub String);
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
pub struct UserId(pub String);
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
pub struct FileId(pub String);
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
pub struct QueryId(pub String);
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Hash, Eq)]
pub struct Timestamp(pub u32);
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Chat {
pub chat_id: ChatId,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(rename = "type")]
pub chat_type: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct From {
pub first_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_name: Option<String>, pub user_id: UserId,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum Languages {
#[default]
Ru,
En,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub enum ChatType {
#[default]
Private,
Group,
Channel,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum ChatActions {
Looking,
#[default]
Typing,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub enum MultipartName {
FilePath(String),
ImagePath(String),
FileContent {
filename: String,
content: Vec<u8>,
},
ImageContent {
filename: String,
content: Vec<u8>,
},
#[default]
None,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Admin {
pub user_id: UserId,
#[serde(skip_serializing_if = "Option::is_none")]
pub creator: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Users {
pub user_id: UserId,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Member {
pub user_id: UserId,
#[serde(skip_serializing_if = "Option::is_none")]
pub creator: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub admin: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Sn {
pub sn: String,
pub user_id: UserId,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PhotoUrl {
pub url: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum ApiResponseWrapper<T> {
PayloadWithOk {
ok: bool,
#[serde(flatten)]
payload: T,
},
PayloadOnly(T),
Error {
ok: bool,
description: String,
},
}
impl<T> std::convert::From<ApiResponseWrapper<T>> for Result<T>
where
T: Default + Serialize + DeserializeOwned,
{
fn from(wrapper: ApiResponseWrapper<T>) -> Self {
match wrapper {
ApiResponseWrapper::PayloadWithOk { ok, payload } => {
if ok {
debug!("Answer is ok, payload received");
Ok(payload)
} else {
debug!("Answer is not ok, but description is not provided");
Err(BotError::Api(ApiError {
description: "Unspecified error".to_string(),
}))
}
}
ApiResponseWrapper::PayloadOnly(payload) => {
debug!("Answer is ok, payload received");
Ok(payload)
}
ApiResponseWrapper::Error { ok, description } => {
if ok {
debug!("Answer is ok, BUT error description is provided");
} else {
debug!("Answer is NOT ok and error description is provided");
}
Err(BotError::Api(ApiError { description }))
}
}
}
}
impl Display for ChatId {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::convert::From<String> for ChatId {
fn from(s: String) -> Self {
ChatId(Cow::Owned(s))
}
}
impl std::convert::From<&'static str> for ChatId {
fn from(s: &'static str) -> Self {
ChatId(Cow::Borrowed(s))
}
}
impl std::convert::From<Cow<'static, str>> for ChatId {
fn from(cow: Cow<'static, str>) -> Self {
ChatId(cow)
}
}
impl AsRef<str> for ChatId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl ChatId {
pub fn from_static(s: &'static str) -> Self {
ChatId::from(s)
}
pub fn from_borrowed_str(s: &str) -> Self {
ChatId(Cow::Owned(s.to_string()))
}
pub fn from_owned(s: String) -> Self {
ChatId(Cow::Owned(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0.into_owned()
}
}
impl Display for APIVersionUrl {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
APIVersionUrl::V1 => write!(f, "bot/v1/"),
}
}
}
impl Display for MultipartName {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
MultipartName::FilePath(..) | MultipartName::FileContent { .. } => write!(f, "file"),
MultipartName::ImagePath(..) | MultipartName::ImageContent { .. } => write!(f, "image"),
_ => write!(f, ""),
}
}
}
impl Default for Keyboard {
fn default() -> Self {
Self {
buttons: vec![vec![]],
}
}
}
impl std::fmt::Display for UserId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chat_id_display() {
let id = ChatId::from("test_id");
assert_eq!(format!("{id}"), "test_id");
}
#[test]
fn test_chat_id_from_implementations() {
let static_id = ChatId::from("static_chat_id");
assert_eq!(static_id.as_str(), "static_chat_id");
let dynamic_string = format!("dynamic_{}", 123);
let dynamic_id = ChatId::from_borrowed_str(&dynamic_string);
assert_eq!(dynamic_id.as_str(), "dynamic_123");
let owned_id = ChatId::from("owned_string".to_string());
assert_eq!(owned_id.as_str(), "owned_string");
let static_method_id = ChatId::from_static("static_method");
assert_eq!(static_method_id.as_str(), "static_method");
let static_literal = ChatId::from("literal");
match static_literal.0 {
Cow::Borrowed(_) => (), Cow::Owned(_) => panic!("Expected Cow::Borrowed for static string literal"),
}
let dynamic = ChatId::from_borrowed_str("not_static");
match dynamic.0 {
Cow::Owned(_) => (), Cow::Borrowed(_) => panic!("Expected Cow::Owned for dynamic string"),
}
}
#[test]
fn test_apiversionurl_display() {
assert_eq!(format!("{}", APIVersionUrl::V1), "bot/v1/");
}
#[test]
fn test_multipartname_display() {
let f = MultipartName::FilePath("file.txt".to_string());
let i = MultipartName::ImagePath("img.png".to_string());
let n = MultipartName::None;
assert_eq!(format!("{f}"), "file");
assert_eq!(format!("{i}"), "image");
assert_eq!(format!("{n}"), "");
}
#[test]
fn test_keyboard_default() {
let kb = Keyboard::default();
assert_eq!(kb.buttons, vec![vec![]]);
}
#[test]
fn test_userid_display() {
let id = UserId("u123".to_string());
assert_eq!(format!("{id}"), "u123");
}
#[test]
fn test_parsemode_default_and_eq() {
assert_eq!(ParseMode::default(), ParseMode::HTML);
assert_eq!(ParseMode::HTML, ParseMode::HTML);
assert_ne!(ParseMode::HTML, ParseMode::MarkdownV2);
}
#[test]
fn test_buttonstyle_default_and_eq() {
assert_eq!(ButtonStyle::default(), ButtonStyle::Base);
assert_eq!(ButtonStyle::Primary, ButtonStyle::Primary);
assert_ne!(ButtonStyle::Primary, ButtonStyle::Attention);
}
#[test]
fn test_apiresponsewrapper_from_payloadonly() {
let wrap = ApiResponseWrapper::PayloadOnly(42);
let res: Result<i32> = wrap.into();
assert_eq!(res.unwrap(), 42);
}
#[test]
fn test_apiresponsewrapper_from_payloadwithok() {
let wrap = ApiResponseWrapper::PayloadWithOk {
ok: true,
payload: 7,
};
let res: Result<i32> = wrap.into();
assert_eq!(res.unwrap(), 7);
let wrap = ApiResponseWrapper::PayloadWithOk {
ok: false,
payload: 0,
};
let res: Result<i32> = wrap.into();
assert!(res.is_err());
}
#[test]
fn test_apiresponsewrapper_from_error() {
let wrap = ApiResponseWrapper::<i32>::Error {
ok: false,
description: "fail".to_string(),
};
let res: Result<i32> = wrap.into();
assert!(res.is_err());
}
#[test]
fn test_message_text_format_variants() {
let _ = MessageTextFormat::Plain("text".to_string());
let _ = MessageTextFormat::Bold("b".to_string());
let _ = MessageTextFormat::Italic("i".to_string());
let _ = MessageTextFormat::Underline("u".to_string());
let _ = MessageTextFormat::Strikethrough("s".to_string());
let _ = MessageTextFormat::Link("t".to_string(), "url".to_string());
let _ = MessageTextFormat::Mention(ChatId::from("cid"));
let _ = MessageTextFormat::Code("c".to_string());
let _ = MessageTextFormat::Pre("p".to_string(), Some("lang".to_string()));
let _ = MessageTextFormat::OrderedList(vec!["1".to_string()]);
let _ = MessageTextFormat::UnOrderedList(vec!["2".to_string()]);
let _ = MessageTextFormat::Quote("q".to_string());
let _ = MessageTextFormat::None;
}
#[test]
fn test_eventtype_default() {
assert_eq!(EventType::default(), EventType::None);
}
}