use http::Method;
use serde::{Deserialize, Serialize};
use crate::core::operation::{Operation, encode_path_segment, json_body, push_opt};
use crate::core::pagination::{DEFAULT_PAGE_SIZE, Listing, Page, Pagination, Paginator};
use crate::error::Result;
use crate::types::{
Chat, ChatBackgroundResponse, Json, LinkPreview, Message, MessageDetail, MessageStatus,
ReactionResponse, ReadResponse, SendMessageResponse, TypingResponse,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Text {
One(String),
Many(Vec<String>),
}
impl From<&str> for Text {
fn from(s: &str) -> Self {
Text::One(s.to_owned())
}
}
impl From<String> for Text {
fn from(s: String) -> Self {
Text::One(s)
}
}
impl From<Vec<String>> for Text {
fn from(v: Vec<String>) -> Self {
Text::Many(v)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Attachment {
Url(String),
Named {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
}
impl From<&str> for Attachment {
fn from(s: &str) -> Self {
Attachment::Url(s.to_owned())
}
}
impl From<String> for Attachment {
fn from(s: String) -> Self {
Attachment::Url(s)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Default, Serialize)]
pub struct MessagePart {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mention: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub link_preview: Option<LinkPreview>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ListChatsResponse {
pub chats: Vec<Chat>,
pub pagination: Option<Pagination>,
}
impl Listing for ListChatsResponse {
type Item = Chat;
fn into_page(self) -> Page<Self::Item> {
Page {
items: self.chats,
pagination: self.pagination,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ListChatMessagesResponse {
pub chat_id: Option<String>,
pub messages: Vec<Message>,
pub pagination: Option<Pagination>,
}
impl Listing for ListChatMessagesResponse {
type Item = Message;
fn into_page(self) -> Page<Self::Item> {
Page {
items: self.messages,
pagination: self.pagination,
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct SendPollResponse {
pub poll_id: Option<String>,
pub chat_id: Option<String>,
pub poll: Option<Json>,
pub sent_at: Option<f64>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct PollOptionResult {
pub text: Option<String>,
pub votes: Option<i64>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct PollResults {
pub poll_id: Option<String>,
pub chat_id: Option<String>,
pub title: Option<String>,
pub options: Option<Vec<PollOptionResult>>,
pub total_votes: Option<i64>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct ShareContactCardResponse {
pub success: Option<bool>,
pub chat_id: Option<String>,
pub message: Option<String>,
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Default)]
pub struct ListChats {
pub limit: Option<u32>,
pub offset: Option<u32>,
pub q: Option<String>,
pub sort: Option<String>,
}
impl Operation for ListChats {
type Output = ListChatsResponse;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
"/chats".into()
}
fn query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
push_opt(&mut q, "limit", self.limit);
push_opt(&mut q, "offset", self.offset);
push_opt(&mut q, "q", self.q.as_ref());
push_opt(&mut q, "sort", self.sort.as_ref());
q
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct GetChat {
pub chat_id: String,
}
impl Operation for GetChat {
type Output = Chat;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!("/chats/{}", encode_path_segment(&self.chat_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct ListChatMessages {
pub chat_id: String,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub sort: Option<String>,
pub direction: Option<String>,
pub since: Option<i64>,
pub until: Option<i64>,
}
impl ListChatMessages {
pub fn new(chat_id: impl Into<String>) -> Self {
ListChatMessages {
chat_id: chat_id.into(),
limit: None,
offset: None,
sort: None,
direction: None,
since: None,
until: None,
}
}
}
impl Operation for ListChatMessages {
type Output = ListChatMessagesResponse;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!("/chats/{}/messages", encode_path_segment(&self.chat_id))
}
fn query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
push_opt(&mut q, "limit", self.limit);
push_opt(&mut q, "offset", self.offset);
push_opt(&mut q, "sort", self.sort.as_ref());
push_opt(&mut q, "direction", self.direction.as_ref());
push_opt(&mut q, "since", self.since);
push_opt(&mut q, "until", self.until);
q
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct SendMessage {
#[serde(skip)]
pub chat_id: String,
#[serde(skip)]
pub idempotency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<Text>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<Attachment>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_typing_indicator: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub share_contact: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parts: Option<Vec<MessagePart>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub link_preview: Option<LinkPreview>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effect: Option<String>,
}
impl SendMessage {
pub fn new(chat_id: impl Into<String>) -> Self {
SendMessage {
chat_id: chat_id.into(),
idempotency_key: None,
text: None,
attachments: None,
use_typing_indicator: None,
from_number: None,
share_contact: None,
parts: None,
link_preview: None,
effect: None,
}
}
#[must_use]
pub fn text(mut self, text: impl Into<Text>) -> Self {
self.text = Some(text.into());
self
}
#[must_use]
pub fn attachments(mut self, attachments: Vec<Attachment>) -> Self {
self.attachments = Some(attachments);
self
}
#[must_use]
pub fn parts(mut self, parts: Vec<MessagePart>) -> Self {
self.parts = Some(parts);
self
}
#[must_use]
pub fn effect(mut self, effect: impl Into<String>) -> Self {
self.effect = Some(effect.into());
self
}
#[must_use]
pub fn from_number(mut self, number: impl Into<String>) -> Self {
self.from_number = Some(number.into());
self
}
#[must_use]
pub fn link_preview(mut self, preview: LinkPreview) -> Self {
self.link_preview = Some(preview);
self
}
#[must_use]
pub fn use_typing_indicator(mut self, yes: bool) -> Self {
self.use_typing_indicator = Some(yes);
self
}
#[must_use]
pub fn share_contact(mut self, yes: bool) -> Self {
self.share_contact = Some(yes);
self
}
#[must_use]
pub fn idempotency_key(mut self, key: impl Into<String>) -> Self {
self.idempotency_key = Some(key.into());
self
}
}
impl Operation for SendMessage {
type Output = SendMessageResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!("/chats/{}/messages", encode_path_segment(&self.chat_id))
}
fn headers(&self) -> Vec<(&'static str, String)> {
let key = self
.idempotency_key
.clone()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
vec![("Idempotency-Key", key)]
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct GetMessage {
pub chat_id: String,
pub message_id: String,
}
impl Operation for GetMessage {
type Output = MessageDetail;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!(
"/chats/{}/messages/{}",
encode_path_segment(&self.chat_id),
encode_path_segment(&self.message_id)
)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct GetMessageStatus {
pub chat_id: String,
pub message_id: String,
}
impl Operation for GetMessageStatus {
type Output = MessageStatus;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!(
"/chats/{}/messages/{}/status",
encode_path_segment(&self.chat_id),
encode_path_segment(&self.message_id)
)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct AddReaction {
#[serde(skip)]
pub chat_id: String,
#[serde(skip)]
pub message_id: String,
pub reaction: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub direction: Option<String>,
}
impl Operation for AddReaction {
type Output = ReactionResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!(
"/chats/{}/messages/{}/reactions",
encode_path_segment(&self.chat_id),
encode_path_segment(&self.message_id)
)
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct SendPoll {
#[serde(skip)]
pub chat_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub options: Vec<String>,
}
impl Operation for SendPoll {
type Output = SendPollResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!("/chats/{}/polls", encode_path_segment(&self.chat_id))
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct GetPollResults {
pub chat_id: String,
pub poll_id: String,
}
impl Operation for GetPollResults {
type Output = PollResults;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!(
"/chats/{}/polls/{}",
encode_path_segment(&self.chat_id),
encode_path_segment(&self.poll_id)
)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct StartTyping {
pub chat_id: String,
}
impl Operation for StartTyping {
type Output = TypingResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!("/chats/{}/typing", encode_path_segment(&self.chat_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct StopTyping {
pub chat_id: String,
}
impl Operation for StopTyping {
type Output = TypingResponse;
const METHOD: Method = Method::DELETE;
fn path(&self) -> String {
format!("/chats/{}/typing", encode_path_segment(&self.chat_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct MarkChatRead {
pub chat_id: String,
}
impl Operation for MarkChatRead {
type Output = ReadResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!("/chats/{}/read", encode_path_segment(&self.chat_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct ShareContactCard {
pub chat_id: String,
}
impl Operation for ShareContactCard {
type Output = ShareContactCardResponse;
const METHOD: Method = Method::POST;
fn path(&self) -> String {
format!("/chats/{}/contact-card", encode_path_segment(&self.chat_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct GetChatBackground {
pub chat_id: String,
}
impl Operation for GetChatBackground {
type Output = ChatBackgroundResponse;
const METHOD: Method = Method::GET;
fn path(&self) -> String {
format!("/chats/{}/background", encode_path_segment(&self.chat_id))
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Serialize)]
pub struct SetChatBackground {
#[serde(skip)]
pub chat_id: String,
pub background: String,
}
impl Operation for SetChatBackground {
type Output = ChatBackgroundResponse;
const METHOD: Method = Method::PUT;
fn path(&self) -> String {
format!("/chats/{}/background", encode_path_segment(&self.chat_id))
}
fn body(&self) -> Result<Option<Vec<u8>>> {
json_body(self)
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
pub struct RemoveChatBackground {
pub chat_id: String,
}
impl Operation for RemoveChatBackground {
type Output = ChatBackgroundResponse;
const METHOD: Method = Method::DELETE;
fn path(&self) -> String {
format!("/chats/{}/background", encode_path_segment(&self.chat_id))
}
}
#[derive(Debug)]
pub struct Chats<'c, C> {
pub(crate) client: &'c C,
}
#[derive(Debug)]
pub struct Chat_<'c, C> {
pub(crate) client: &'c C,
pub(crate) chat_id: String,
}
#[cfg(feature = "async")]
impl crate::Client {
pub fn chats(&self) -> Chats<'_, crate::Client> {
Chats { client: self }
}
pub fn chat(&self, chat_id: impl Into<String>) -> Chat_<'_, crate::Client> {
Chat_ {
client: self,
chat_id: chat_id.into(),
}
}
}
#[cfg(feature = "sync")]
impl crate::BlockingClient {
pub fn chats(&self) -> Chats<'_, crate::BlockingClient> {
Chats { client: self }
}
pub fn chat(&self, chat_id: impl Into<String>) -> Chat_<'_, crate::BlockingClient> {
Chat_ {
client: self,
chat_id: chat_id.into(),
}
}
}
#[cfg(feature = "async")]
impl<'c> Chats<'c, crate::Client> {
pub async fn list(&self) -> Result<ListChatsResponse> {
self.client.send(ListChats::default()).await
}
pub async fn list_with(&self, query: ListChats) -> Result<ListChatsResponse> {
self.client.send(query).await
}
pub fn list_all(
&self,
) -> Paginator<'c, crate::Client, impl Fn(u32, u32) -> ListChats + use<'c>, ListChats> {
Paginator::new(self.client, DEFAULT_PAGE_SIZE, |offset, limit| ListChats {
offset: Some(offset),
limit: Some(limit),
..Default::default()
})
}
}
#[cfg(feature = "sync")]
impl<'c> Chats<'c, crate::BlockingClient> {
pub fn list(&self) -> Result<ListChatsResponse> {
self.client.send(ListChats::default())
}
pub fn list_with(&self, query: ListChats) -> Result<ListChatsResponse> {
self.client.send(query)
}
pub fn list_all(
&self,
) -> Paginator<'c, crate::BlockingClient, impl Fn(u32, u32) -> ListChats + use<'c>, ListChats>
{
Paginator::new(self.client, DEFAULT_PAGE_SIZE, |offset, limit| ListChats {
offset: Some(offset),
limit: Some(limit),
..Default::default()
})
}
}
#[cfg(feature = "async")]
impl<'c> Chat_<'c, crate::Client> {
pub async fn get(&self) -> Result<Chat> {
self.client
.send(GetChat {
chat_id: self.chat_id.clone(),
})
.await
}
pub async fn list_messages(&self) -> Result<ListChatMessagesResponse> {
self.client
.send(ListChatMessages::new(self.chat_id.clone()))
.await
}
pub async fn list_messages_with(
&self,
query: ListChatMessages,
) -> Result<ListChatMessagesResponse> {
self.client.send(query).await
}
pub fn list_messages_all(
&self,
) -> Paginator<
'c,
crate::Client,
impl Fn(u32, u32) -> ListChatMessages + use<'c>,
ListChatMessages,
> {
let chat_id = self.chat_id.clone();
Paginator::new(self.client, DEFAULT_PAGE_SIZE, move |offset, limit| {
ListChatMessages {
offset: Some(offset),
limit: Some(limit),
..ListChatMessages::new(chat_id.clone())
}
})
}
pub fn message(&self) -> SendMessage {
SendMessage::new(self.chat_id.clone())
}
pub async fn send(&self, message: SendMessage) -> Result<SendMessageResponse> {
self.client.send(message).await
}
pub async fn send_text(&self, text: impl Into<Text>) -> Result<SendMessageResponse> {
self.client.send(self.message().text(text)).await
}
pub async fn get_message(&self, message_id: impl Into<String>) -> Result<MessageDetail> {
self.client
.send(GetMessage {
chat_id: self.chat_id.clone(),
message_id: message_id.into(),
})
.await
}
pub async fn message_status(&self, message_id: impl Into<String>) -> Result<MessageStatus> {
self.client
.send(GetMessageStatus {
chat_id: self.chat_id.clone(),
message_id: message_id.into(),
})
.await
}
pub async fn add_reaction(
&self,
message_id: impl Into<String>,
reaction: impl Into<String>,
direction: Option<String>,
) -> Result<ReactionResponse> {
self.client
.send(AddReaction {
chat_id: self.chat_id.clone(),
message_id: message_id.into(),
reaction: reaction.into(),
direction,
})
.await
}
pub async fn send_poll(
&self,
title: Option<String>,
options: Vec<String>,
) -> Result<SendPollResponse> {
self.client
.send(SendPoll {
chat_id: self.chat_id.clone(),
title,
options,
})
.await
}
pub async fn poll_results(&self, poll_id: impl Into<String>) -> Result<PollResults> {
self.client
.send(GetPollResults {
chat_id: self.chat_id.clone(),
poll_id: poll_id.into(),
})
.await
}
pub async fn start_typing(&self) -> Result<TypingResponse> {
self.client
.send(StartTyping {
chat_id: self.chat_id.clone(),
})
.await
}
pub async fn stop_typing(&self) -> Result<TypingResponse> {
self.client
.send(StopTyping {
chat_id: self.chat_id.clone(),
})
.await
}
pub async fn mark_read(&self) -> Result<ReadResponse> {
self.client
.send(MarkChatRead {
chat_id: self.chat_id.clone(),
})
.await
}
pub async fn share_contact_card(&self) -> Result<ShareContactCardResponse> {
self.client
.send(ShareContactCard {
chat_id: self.chat_id.clone(),
})
.await
}
pub async fn background(&self) -> Result<ChatBackgroundResponse> {
self.client
.send(GetChatBackground {
chat_id: self.chat_id.clone(),
})
.await
}
pub async fn set_background(
&self,
background: impl Into<String>,
) -> Result<ChatBackgroundResponse> {
self.client
.send(SetChatBackground {
chat_id: self.chat_id.clone(),
background: background.into(),
})
.await
}
pub async fn remove_background(&self) -> Result<ChatBackgroundResponse> {
self.client
.send(RemoveChatBackground {
chat_id: self.chat_id.clone(),
})
.await
}
}
#[cfg(feature = "sync")]
impl<'c> Chat_<'c, crate::BlockingClient> {
pub fn get(&self) -> Result<Chat> {
self.client.send(GetChat {
chat_id: self.chat_id.clone(),
})
}
pub fn list_messages(&self) -> Result<ListChatMessagesResponse> {
self.client
.send(ListChatMessages::new(self.chat_id.clone()))
}
pub fn list_messages_with(&self, query: ListChatMessages) -> Result<ListChatMessagesResponse> {
self.client.send(query)
}
pub fn list_messages_all(
&self,
) -> Paginator<
'c,
crate::BlockingClient,
impl Fn(u32, u32) -> ListChatMessages + use<'c>,
ListChatMessages,
> {
let chat_id = self.chat_id.clone();
Paginator::new(self.client, DEFAULT_PAGE_SIZE, move |offset, limit| {
ListChatMessages {
offset: Some(offset),
limit: Some(limit),
..ListChatMessages::new(chat_id.clone())
}
})
}
pub fn message(&self) -> SendMessage {
SendMessage::new(self.chat_id.clone())
}
pub fn send(&self, message: SendMessage) -> Result<SendMessageResponse> {
self.client.send(message)
}
pub fn send_text(&self, text: impl Into<Text>) -> Result<SendMessageResponse> {
self.client.send(self.message().text(text))
}
pub fn get_message(&self, message_id: impl Into<String>) -> Result<MessageDetail> {
self.client.send(GetMessage {
chat_id: self.chat_id.clone(),
message_id: message_id.into(),
})
}
pub fn message_status(&self, message_id: impl Into<String>) -> Result<MessageStatus> {
self.client.send(GetMessageStatus {
chat_id: self.chat_id.clone(),
message_id: message_id.into(),
})
}
pub fn add_reaction(
&self,
message_id: impl Into<String>,
reaction: impl Into<String>,
direction: Option<String>,
) -> Result<ReactionResponse> {
self.client.send(AddReaction {
chat_id: self.chat_id.clone(),
message_id: message_id.into(),
reaction: reaction.into(),
direction,
})
}
pub fn send_poll(
&self,
title: Option<String>,
options: Vec<String>,
) -> Result<SendPollResponse> {
self.client.send(SendPoll {
chat_id: self.chat_id.clone(),
title,
options,
})
}
pub fn poll_results(&self, poll_id: impl Into<String>) -> Result<PollResults> {
self.client.send(GetPollResults {
chat_id: self.chat_id.clone(),
poll_id: poll_id.into(),
})
}
pub fn start_typing(&self) -> Result<TypingResponse> {
self.client.send(StartTyping {
chat_id: self.chat_id.clone(),
})
}
pub fn stop_typing(&self) -> Result<TypingResponse> {
self.client.send(StopTyping {
chat_id: self.chat_id.clone(),
})
}
pub fn mark_read(&self) -> Result<ReadResponse> {
self.client.send(MarkChatRead {
chat_id: self.chat_id.clone(),
})
}
pub fn share_contact_card(&self) -> Result<ShareContactCardResponse> {
self.client.send(ShareContactCard {
chat_id: self.chat_id.clone(),
})
}
pub fn background(&self) -> Result<ChatBackgroundResponse> {
self.client.send(GetChatBackground {
chat_id: self.chat_id.clone(),
})
}
pub fn set_background(&self, background: impl Into<String>) -> Result<ChatBackgroundResponse> {
self.client.send(SetChatBackground {
chat_id: self.chat_id.clone(),
background: background.into(),
})
}
pub fn remove_background(&self) -> Result<ChatBackgroundResponse> {
self.client.send(RemoveChatBackground {
chat_id: self.chat_id.clone(),
})
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stdout,
clippy::unreadable_literal
)]
mod tests {
use super::*;
use crate::core::operation::Operation;
#[test]
fn text_serializes_untagged() {
let one = serde_json::to_string(&Text::One("hi".into())).unwrap();
assert_eq!(one, "\"hi\"");
let many = serde_json::to_string(&Text::Many(vec!["a".into(), "b".into()])).unwrap();
assert_eq!(many, "[\"a\",\"b\"]");
}
#[test]
fn auto_idempotency_key_is_generated() {
let msg = SendMessage::new("chat1").text("hello");
let headers = msg.headers();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0, "Idempotency-Key");
assert_eq!(headers[0].1.len(), 36);
}
#[test]
fn explicit_idempotency_key_is_used() {
let msg = SendMessage::new("chat1")
.text("hi")
.idempotency_key("my-key");
assert_eq!(msg.headers()[0].1, "my-key");
}
#[test]
fn send_body_only_includes_set_fields() {
let msg = SendMessage::new("chat1").text("hi");
let body = msg.body().unwrap().unwrap();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v, serde_json::json!({ "text": "hi" }));
}
}