Compare commits
40 Commits
76b3a8cdd7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 72cca44db0 | |||
| 806e7e5fcc | |||
| 213c53f080 | |||
| daae47405c | |||
| bf3c5debae | |||
| 825de10df7 | |||
| 5b5a23ab0b | |||
| 109c85379b | |||
| 76a8c7dc2e | |||
| 85cfeefbfa | |||
| d8b1592f9f | |||
| e2075110f2 | |||
| 33a4ea74af | |||
| da2f051709 | |||
| 6f2afe13da | |||
| 1c21a26f47 | |||
| 229c4bd713 | |||
| 42d20a95c0 | |||
| d75d8b58c4 | |||
| a0a95e0f8f | |||
| 1316f9cc7b | |||
| 24e0c968ee | |||
| 86deb61ba0 | |||
| 0a67fe2264 | |||
| 65bf2c478b | |||
| dbbbb66d22 | |||
| 0448e22fbc | |||
| 6a6df0a84d | |||
| 8a77749d12 | |||
| 4a991d673f | |||
| 77a6bf8eed | |||
| d28933318f | |||
| 9c0ef51de0 | |||
| 2ace431951 | |||
| b047e9c6af | |||
| 3c35209dc3 | |||
| b46631bf92 | |||
| 28d0da15df | |||
| b6588af80f | |||
| 2c9e3796a7 |
17
Cargo.toml
17
Cargo.toml
@@ -1,16 +1,23 @@
|
||||
[package]
|
||||
name = "dingtalk"
|
||||
version = "0.2.0"
|
||||
version = "2.0.1"
|
||||
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "DingTalk Util"
|
||||
description = "DingTalk Robot Util, Send text/markdown/link messages using DingTalk robot, 钉钉机器人"
|
||||
keywords = ["DingTalk", "Robot", "Message"]
|
||||
readme = "README.md"
|
||||
repository = "https://git.hatter.ink/hatter/dingtalk"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.11.0"
|
||||
rust-crypto = "0.2.36"
|
||||
reqwest = "0.9.22"
|
||||
reqwest = "0.10.0"
|
||||
urlencoding = "1.0.0"
|
||||
json = "0.11.14"
|
||||
futures = "0.3.1"
|
||||
hmac = "0.7.1"
|
||||
sha2 = "0.8.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.2.0"
|
||||
|
||||
132
README.md
132
README.md
@@ -1,21 +1,145 @@
|
||||
# dingtalk
|
||||
|
||||
DingTalk util
|
||||
DingTalk Robot Util, Send text/markdown/link messages using DingTalk robot
|
||||
|
||||
钉钉机器人 Rust SDK
|
||||
|
||||
NOTE: From version 1.1.0 dingtalk uses reqwest 0.10.0's `async`/`.await` API.
|
||||
|
||||
> Official reference: https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq/0fa88adc
|
||||
|
||||
|
||||
Sample 1:
|
||||
```rust
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use dingtalk::DingTalk;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dt = DingTalk::new("<token>", "");
|
||||
dt.send_text("Hello world!")?;
|
||||
dt.send_text("Hello world!").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Need use crate: `tokio = { version = "0.2.6", features = ["full"] }`.
|
||||
|
||||
Sample 2 (Read token from file):
|
||||
```rust
|
||||
#[tokio::main]
|
||||
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dt = DingTalk::from_file("~/.dingtalk-token.json")?;
|
||||
dt.send_text("Hello world!").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Sample, send markdown message:
|
||||
```rust
|
||||
dt.send_markdown("markdown title 001", r#"# markdown content 001
|
||||
* line 0
|
||||
* line 1
|
||||
* line 2"#).await?;
|
||||
```
|
||||
|
||||
Sample, send link message:
|
||||
```rust
|
||||
dt.send_link("link title 001", "link content 001", "https://hatter.ink/favicon.png", "https://hatter.ink/").await?;
|
||||
```
|
||||
|
||||
Sample, send feed card message:
|
||||
```rust
|
||||
dt.send_message(DingTalkMessage::new_feed_card()
|
||||
.add_feed_card_link(DingTalkMessageFeedCardLink{
|
||||
title: "test feed card title 001".into(),
|
||||
message_url: "https://hatter.ink/".into(),
|
||||
pic_url: "https://hatter.ink/favicon.png".into(),
|
||||
})
|
||||
.add_feed_card_link(DingTalkMessageFeedCardLink{
|
||||
title: "test feed card title 002".into(),
|
||||
message_url: "https://hatter.ink/".into(),
|
||||
pic_url: "https://hatter.ink/favicon.png".into(),
|
||||
})
|
||||
).await?;
|
||||
```
|
||||
|
||||
Sample, send action card message(single btn):
|
||||
```rust
|
||||
dt.send_message(DingTalkMessage::new_action_card("action card 001", "action card text 001")
|
||||
.set_action_card_signle_btn(DingTalkMessageActionCardBtn{
|
||||
title: "test signle btn title".into(),
|
||||
action_url: "https://hatter.ink/".into(),
|
||||
})
|
||||
).await?;
|
||||
```
|
||||
|
||||
Sample, send action card message(multi btns):
|
||||
```rust
|
||||
dt.send_message(DingTalkMessage::new_action_card("action card 002", "action card text 002")
|
||||
.add_action_card_btn(DingTalkMessageActionCardBtn{
|
||||
title: "test signle btn title 01".into(),
|
||||
action_url: "https://hatter.ink/".into(),
|
||||
})
|
||||
.add_action_card_btn(DingTalkMessageActionCardBtn{
|
||||
title: "test signle btn title 02".into(),
|
||||
action_url: "https://hatter.ink/".into(),
|
||||
})
|
||||
).await?;
|
||||
```
|
||||
|
||||
#### JSON Config
|
||||
|
||||
DingTalk config:
|
||||
```json
|
||||
{
|
||||
"access_token": "<access token>",
|
||||
"sec_token": "<sec token>"
|
||||
}
|
||||
```
|
||||
|
||||
WeChat Work config:
|
||||
```json
|
||||
{
|
||||
"type": "wechat",
|
||||
"access_token": "<token>"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Changelog
|
||||
|
||||
* v2.0.0
|
||||
* Remove `'a` life cycle
|
||||
* v1.3.2
|
||||
* Add `DingTalk::from_token`
|
||||
* v1.3.1
|
||||
* Add `DingTalk::new_wechat`
|
||||
* v1.3.0
|
||||
* Suports WeChat Work now, add type `"type": "wechat"`, supports method `DingTalk::send_text`
|
||||
* v1.2.1
|
||||
* Remove `maplit` crate
|
||||
* v1.2.0
|
||||
* Use `serde` and `serde_json` crates, replace `json` crate
|
||||
* v1.1.2
|
||||
* Use `hmac` and `sha2` crates, replace `rust-crypto` crate
|
||||
* v1.1.1
|
||||
* `DingTalk::from_json` add `direct_url`
|
||||
* Fix problems by clippy
|
||||
* v1.1.0
|
||||
* Change fn to async/.await
|
||||
* v1.0.1
|
||||
* Change two fn names
|
||||
* Add readme sample codes
|
||||
* v1.0.0
|
||||
* `TEXT` -> `Text` ..., change enum caps
|
||||
* Add `ActionCard` message, send action card message type
|
||||
* Add `direct_url` for `DingTalk`, for outgoing robot
|
||||
* Implemented almost the functions listed on https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq/0fa88adc
|
||||
* v0.3.0
|
||||
* Add `FeedCard` message, send feed card message type
|
||||
* v0.2.1
|
||||
* Add `Dingtalk::from_json`, read token from JSON string
|
||||
* v0.2.0
|
||||
* Add `DingTalk::from_file`, read token from file
|
||||
* v0.1.2
|
||||
@@ -25,5 +149,5 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
* v0.1.0
|
||||
* Add `DingTalk::send_link(...)`, send link message
|
||||
* v0.0.3
|
||||
* Add `DingTalkMessage` , can set at_all, at_mobiles now
|
||||
* Add `DingTalkMessage` , can set `at_all`, `at_mobiles` now
|
||||
|
||||
|
||||
490
src/lib.rs
490
src/lib.rs
@@ -1,21 +1,19 @@
|
||||
#[macro_use]
|
||||
extern crate json;
|
||||
use std::{ fs, env, path::PathBuf, time::SystemTime, io::{ Error, ErrorKind } };
|
||||
use serde_json::Value;
|
||||
use sha2::Sha256;
|
||||
use hmac::{ Hmac, Mac };
|
||||
|
||||
use std::fs;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
use std::io::{
|
||||
Error,
|
||||
ErrorKind,
|
||||
};
|
||||
use crypto::{
|
||||
mac::{
|
||||
Mac,
|
||||
MacResult,
|
||||
},
|
||||
hmac::Hmac,
|
||||
sha2::Sha256,
|
||||
mod msg;
|
||||
use msg::*;
|
||||
|
||||
pub use msg:: {
|
||||
DingTalkType,
|
||||
DingTalkMessage,
|
||||
DingTalkMessageType,
|
||||
DingTalkMessageActionCardHideAvatar,
|
||||
DingTalkMessageActionCardBtnOrientation,
|
||||
DingTalkMessageActionCardBtn,
|
||||
DingTalkMessageFeedCardLink,
|
||||
};
|
||||
|
||||
pub type XResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
@@ -23,115 +21,152 @@ pub type XResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
const CONTENT_TYPE: &str = "Content-Type";
|
||||
const APPLICATION_JSON_UTF8: &str = "application/json; charset=utf-8";
|
||||
|
||||
const DEFAULT_DINGTALK_ROBOT_URL: &str = "https://oapi.dingtalk.com/robot/send?access_token=";
|
||||
const DEFAULT_DINGTALK_ROBOT_URL: &str = "https://oapi.dingtalk.com/robot/send";
|
||||
const DEFAULT_WECHAT_WORK_ROBOT_URL: &str = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send";
|
||||
|
||||
|
||||
/// `DingTalk` is a simple SDK for DingTalk webhook robot
|
||||
///
|
||||
/// Document https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq
|
||||
///
|
||||
/// Sample code:
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// let dt = DingTalk::new("<token>", "");
|
||||
/// dt.send_text("Hello world!")?;
|
||||
/// ```
|
||||
///
|
||||
/// At all sample:
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// dt.send_message(&DingTalkMessage::new_text("Hello World!").at_all())?;
|
||||
/// ```
|
||||
pub struct DingTalk<'a> {
|
||||
pub default_webhook_url: &'a str,
|
||||
pub access_token: &'a str,
|
||||
pub sec_token: &'a str,
|
||||
#[derive(Default)]
|
||||
pub struct DingTalk {
|
||||
pub dingtalk_type: DingTalkType,
|
||||
pub default_webhook_url: String,
|
||||
pub access_token: String,
|
||||
pub sec_token: String,
|
||||
pub direct_url: String,
|
||||
}
|
||||
|
||||
/// DingTalk message type
|
||||
/// * TEXT - text message
|
||||
/// * MARKDONW - markdown message
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum DingTalkMessageType {
|
||||
TEXT,
|
||||
LINK,
|
||||
MARKDOWN,
|
||||
// ACTION_CARD, todo!()
|
||||
}
|
||||
|
||||
/// Default DingTalkMessageType is TEXT
|
||||
impl Default for DingTalkMessageType {
|
||||
|
||||
fn default() -> Self { DingTalkMessageType::TEXT }
|
||||
}
|
||||
|
||||
/// DingTalk message
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DingTalkMessage<'a> {
|
||||
pub message_type: DingTalkMessageType,
|
||||
pub text_content: &'a str,
|
||||
pub markdown_title: &'a str,
|
||||
pub markdown_content: &'a str,
|
||||
pub link_text: &'a str,
|
||||
pub link_title: &'a str,
|
||||
pub link_pic_url: &'a str,
|
||||
pub link_message_url: &'a str,
|
||||
pub at_all: bool,
|
||||
pub at_mobiles: Vec<String>,
|
||||
}
|
||||
|
||||
impl <'a> DingTalkMessage<'a> {
|
||||
impl DingTalkMessage {
|
||||
|
||||
/// New text DingTalk message
|
||||
pub fn new_text(text_content: &'a str) -> Self {
|
||||
Self::new(DingTalkMessageType::TEXT).text(text_content)
|
||||
pub fn new_text(text_content: &str) -> Self {
|
||||
Self::new(DingTalkMessageType::Text).text(text_content)
|
||||
}
|
||||
|
||||
/// New markdown DingTalk message
|
||||
pub fn new_markdown(markdown_title: &'a str, markdown_content: &'a str) -> Self {
|
||||
Self::new(DingTalkMessageType::MARKDOWN).markdown(markdown_title, markdown_content)
|
||||
pub fn new_markdown(markdown_title: &str, markdown_content: &str) -> Self {
|
||||
Self::new(DingTalkMessageType::Markdown).markdown(markdown_title, markdown_content)
|
||||
}
|
||||
|
||||
/// New link DingTalk message
|
||||
pub fn new_link(link_title: &'a str, link_text: &'a str, link_pic_url: &'a str, link_message_url: &'a str) -> Self {
|
||||
Self::new(DingTalkMessageType::LINK).link(link_title, link_text, link_pic_url, link_message_url)
|
||||
pub fn new_link(link_title: &str, link_text: &str, link_pic_url: &str, link_message_url: &str) -> Self {
|
||||
Self::new(DingTalkMessageType::Link).link(link_title, link_text, link_pic_url, link_message_url)
|
||||
}
|
||||
|
||||
/// New action card DingTalk message
|
||||
pub fn new_action_card(title: &str, text: &str) -> Self {
|
||||
let mut s = Self::new(DingTalkMessageType::ActionCard);
|
||||
s.action_card_title = title.into();
|
||||
s.action_card_text = text.into();
|
||||
s
|
||||
}
|
||||
|
||||
/// New feed card DingTalk message
|
||||
pub fn new_feed_card() -> Self {
|
||||
Self::new(DingTalkMessageType::FeedCard)
|
||||
}
|
||||
|
||||
/// New DingTalk message
|
||||
pub fn new(message_type: DingTalkMessageType) -> Self {
|
||||
DingTalkMessage {
|
||||
message_type: message_type,
|
||||
message_type,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set text
|
||||
pub fn text(mut self, text_content: &'a str) -> Self {
|
||||
self.text_content = text_content;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set link
|
||||
pub fn link(mut self, link_title: &'a str, link_text: &'a str, link_pic_url: &'a str, link_message_url: &'a str) -> Self {
|
||||
self.link_title = link_title;
|
||||
self.link_text = link_text;
|
||||
self.link_pic_url = link_pic_url;
|
||||
self.link_message_url = link_message_url;
|
||||
pub fn text(mut self, text_content: &str) -> Self {
|
||||
self.text_content = text_content.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set markdown
|
||||
pub fn markdown(mut self, markdown_title: &'a str, markdown_content: &'a str) -> Self {
|
||||
self.markdown_title = markdown_title;
|
||||
self.markdown_content = markdown_content;
|
||||
pub fn markdown(mut self, markdown_title: &str, markdown_content: &str) -> Self {
|
||||
self.markdown_title = markdown_title.into();
|
||||
self.markdown_content = markdown_content.into();
|
||||
self
|
||||
}
|
||||
|
||||
// At all
|
||||
/// Set link
|
||||
pub fn link(mut self, link_title: &str, link_text: &str, link_pic_url: &str, link_message_url: &str) -> Self {
|
||||
self.link_title = link_title.into();
|
||||
self.link_text = link_text.into();
|
||||
self.link_pic_url = link_pic_url.into();
|
||||
self.link_message_url = link_message_url.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set action card show avator(default show)
|
||||
pub fn action_card_show_avatar(mut self) -> Self {
|
||||
self.action_card_hide_avatar = DingTalkMessageActionCardHideAvatar::Show;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set action card hide avator
|
||||
pub fn action_card_hide_avatar(mut self) -> Self {
|
||||
self.action_card_hide_avatar = DingTalkMessageActionCardHideAvatar::Hide;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set action card btn vertical(default vertical)
|
||||
pub fn action_card_btn_vertical(mut self) -> Self {
|
||||
self.action_card_btn_orientation = DingTalkMessageActionCardBtnOrientation::Vertical;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set action card btn landscape
|
||||
pub fn action_card_btn_landscape(mut self) -> Self {
|
||||
self.action_card_btn_orientation = DingTalkMessageActionCardBtnOrientation::Landscape;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set action card single btn
|
||||
pub fn set_action_card_signle_btn(mut self, btn: DingTalkMessageActionCardBtn) -> Self {
|
||||
self.action_card_single_btn = Some(btn);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add action card btn
|
||||
pub fn add_action_card_btn(mut self, btn: DingTalkMessageActionCardBtn) -> Self {
|
||||
self.action_card_btns.push(btn);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add feed card link
|
||||
pub fn add_feed_card_link(mut self, link: DingTalkMessageFeedCardLink) -> Self {
|
||||
self.feed_card_links.push(link);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add feed card link detail
|
||||
pub fn add_feed_card_link_detail(self, title: &str, message_url: &str, pic_url: &str) -> Self {
|
||||
self.add_feed_card_link(DingTalkMessageFeedCardLink {
|
||||
title: title.into(),
|
||||
message_url: message_url.into(),
|
||||
pic_url: pic_url.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// At all
|
||||
pub fn at_all(mut self) -> Self {
|
||||
self.at_all = true;
|
||||
self
|
||||
}
|
||||
|
||||
// At mobiles
|
||||
pub fn at_mobiles(mut self, mobiles: &Vec<String>) -> Self {
|
||||
/// At mobiles
|
||||
pub fn at_mobiles(mut self, mobiles: &[String]) -> Self {
|
||||
for m in mobiles {
|
||||
self.at_mobiles.push(m.clone());
|
||||
}
|
||||
@@ -139,16 +174,47 @@ impl <'a> DingTalkMessage<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn my_simple_test() {
|
||||
let dt = DingTalk::from_file("~/.dingtalk-token.json").unwrap();
|
||||
dt.send_text("test").ok();
|
||||
}
|
||||
impl DingTalk {
|
||||
|
||||
impl <'a> DingTalk<'a> {
|
||||
/// Create `DingTalk` from token:
|
||||
/// wechatwork:access_token
|
||||
/// dingtalk:access_token?sec_token
|
||||
pub fn from_token(token: &str) -> XResult<Self> {
|
||||
if token.starts_with("dingtalk:") {
|
||||
let token_and_or_sec = &token["dingtalk:".len()..];
|
||||
let mut token_and_or_sec_vec = token_and_or_sec.split('?');
|
||||
let access_token = match token_and_or_sec_vec.next() {
|
||||
Some(t) => t, None => token_and_or_sec,
|
||||
};
|
||||
let sec_token = match token_and_or_sec_vec.next() {
|
||||
Some(t) => t, None => "",
|
||||
};
|
||||
Ok(Self::new(access_token, sec_token))
|
||||
} else if token.starts_with("wechatwork:") {
|
||||
Ok(Self::new_wechat(&token["wechatwork:".len()..]))
|
||||
} else if token.starts_with("wecom:") {
|
||||
Ok(Self::new_wechat(&token["wecom:".len()..]))
|
||||
} else {
|
||||
Err(Box::new(Error::new(ErrorKind::Other, format!("Tokne format erorr: {}", token))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `DingTalk` from file
|
||||
///
|
||||
/// Format see `DingTalk::from_json(json: &str)`
|
||||
pub fn from_file(f: &str) -> XResult<Self> {
|
||||
let f_path_buf = if f.starts_with("~/") {
|
||||
let home = PathBuf::from(env::var("HOME")?);
|
||||
home.join(f.chars().skip(2).collect::<String>())
|
||||
} else {
|
||||
PathBuf::from(f)
|
||||
};
|
||||
let f_content = fs::read_to_string(f_path_buf)?;
|
||||
Self::from_json(&f_content)
|
||||
}
|
||||
|
||||
/// Create `DingTalk` from JSON string
|
||||
///
|
||||
/// Format:
|
||||
/// ```json
|
||||
/// {
|
||||
@@ -157,142 +223,236 @@ impl <'a> DingTalk<'a> {
|
||||
/// "sec_token": "<sec token>" // option
|
||||
/// }
|
||||
/// ```
|
||||
pub fn from_file(f: &str) -> XResult<Self> {
|
||||
let f_path_buf = if f.starts_with("~/") {
|
||||
let home = PathBuf::from(env::var("HOME")?);
|
||||
home.join(f.chars().skip(2).collect::<String>())
|
||||
} else {
|
||||
PathBuf::from(f)
|
||||
};
|
||||
let f_content = fs::read_to_string(f_path_buf)?;
|
||||
let f_json_value = json::parse(&f_content)?;
|
||||
if !f_json_value.is_object() {
|
||||
return Err(Box::new(Error::new(ErrorKind::Other, format!("JSON format erorr: {}", f_content))));
|
||||
pub fn from_json(json: &str) -> XResult<Self> {
|
||||
let json_value: Value = serde_json::from_str(json)?;
|
||||
if !json_value.is_object() {
|
||||
return Err(Box::new(Error::new(ErrorKind::Other, format!("JSON format erorr: {}", json))));
|
||||
}
|
||||
let type_str = json_value["type"].as_str().unwrap_or_default().to_lowercase();
|
||||
let dingtalk_type = match type_str.as_str() {
|
||||
"wechat" | "wechatwork" | "wecom" => DingTalkType::WeChatWork,
|
||||
_ => DingTalkType::DingTalk,
|
||||
};
|
||||
|
||||
let default_webhook_url: &'a str = Self::string_to_a_str(f_json_value["default_webhook_url"].as_str().unwrap_or(DEFAULT_DINGTALK_ROBOT_URL).to_owned());
|
||||
let access_token: &'a str = Self::string_to_a_str(f_json_value["access_token"].as_str().unwrap_or_default().to_owned());
|
||||
let sec_token: &'a str = Self::string_to_a_str(f_json_value["sec_token"].as_str().unwrap_or_default().to_owned());
|
||||
let default_webhook_url = json_value["default_webhook_url"].as_str().unwrap_or_else(
|
||||
|| match dingtalk_type {
|
||||
DingTalkType::DingTalk => DEFAULT_DINGTALK_ROBOT_URL,
|
||||
DingTalkType::WeChatWork => DEFAULT_WECHAT_WORK_ROBOT_URL,
|
||||
}
|
||||
).to_owned();
|
||||
let access_token = json_value["access_token"].as_str().unwrap_or_default().to_owned();
|
||||
let sec_token = json_value["sec_token"].as_str().unwrap_or_default().to_owned();
|
||||
let direct_url = json_value["direct_url"].as_str().unwrap_or_default().to_owned();
|
||||
|
||||
Ok(DingTalk {
|
||||
default_webhook_url: default_webhook_url,
|
||||
access_token: access_token,
|
||||
sec_token: sec_token,
|
||||
dingtalk_type,
|
||||
default_webhook_url,
|
||||
access_token,
|
||||
sec_token,
|
||||
direct_url,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create `DingTalk` from url, for outgoing robot
|
||||
pub fn from_url(direct_url: &str) -> Self {
|
||||
DingTalk {
|
||||
direct_url: direct_url.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `DingTalk`
|
||||
/// `access_token` is access token, `sec_token` can be empty `""`
|
||||
pub fn new(access_token: &'a str, sec_token: &'a str) -> Self {
|
||||
pub fn new(access_token: &str, sec_token: &str) -> Self {
|
||||
DingTalk {
|
||||
default_webhook_url: DEFAULT_DINGTALK_ROBOT_URL,
|
||||
access_token: access_token,
|
||||
sec_token: sec_token,
|
||||
default_webhook_url: DEFAULT_DINGTALK_ROBOT_URL.into(),
|
||||
access_token: access_token.into(),
|
||||
sec_token: sec_token.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `DingTalk` for WeChat Work
|
||||
pub fn new_wechat(key: &str) -> Self {
|
||||
DingTalk {
|
||||
default_webhook_url: DEFAULT_WECHAT_WORK_ROBOT_URL.into(),
|
||||
dingtalk_type: DingTalkType::WeChatWork,
|
||||
access_token: key.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set default webhook url
|
||||
pub fn set_default_webhook_url(&mut self, default_webhook_url: &'a str) {
|
||||
self.default_webhook_url = default_webhook_url;
|
||||
pub fn set_default_webhook_url(&mut self, default_webhook_url: &str) {
|
||||
self.default_webhook_url = default_webhook_url.into();
|
||||
}
|
||||
|
||||
/// Send DingTalk message
|
||||
pub fn send_message(&self, dingtalk_message: &DingTalkMessage) -> XResult<()> {
|
||||
///
|
||||
/// 1. Create DingTalk JSON message
|
||||
/// 2. POST JSON message to DingTalk server
|
||||
pub async fn send_message(&self, dingtalk_message: DingTalkMessage) -> XResult<()> {
|
||||
let mut message_json = match dingtalk_message.message_type {
|
||||
DingTalkMessageType::TEXT => object!{
|
||||
"msgtype" => "text",
|
||||
"text" => object! {
|
||||
"content" => dingtalk_message.text_content,
|
||||
DingTalkMessageType::Text => serde_json::to_value(InnerTextMessage {
|
||||
msgtype: DingTalkMessageType::Text,
|
||||
text: InnerTextMessageText {
|
||||
content: dingtalk_message.text_content,
|
||||
}
|
||||
},
|
||||
DingTalkMessageType::LINK => object!{
|
||||
"msgtype" => "link",
|
||||
"link" => object!{
|
||||
"text" => dingtalk_message.link_text,
|
||||
"title" => dingtalk_message.link_title,
|
||||
"picUrl" => dingtalk_message.link_pic_url,
|
||||
"messageUrl" => dingtalk_message.link_message_url,
|
||||
}),
|
||||
DingTalkMessageType::Link => serde_json::to_value(InnerLinkMessage {
|
||||
msgtype: DingTalkMessageType::Link,
|
||||
link: InnerLinkMessageLink {
|
||||
title: dingtalk_message.link_title,
|
||||
text: dingtalk_message.link_text,
|
||||
pic_url: dingtalk_message.link_pic_url,
|
||||
message_url: dingtalk_message.link_message_url,
|
||||
}
|
||||
},
|
||||
DingTalkMessageType::MARKDOWN => object!{
|
||||
"msgtype" => "markdown",
|
||||
"markdown" => object! {
|
||||
"title" => dingtalk_message.markdown_title,
|
||||
"text" => dingtalk_message.markdown_content,
|
||||
}),
|
||||
DingTalkMessageType::Markdown => serde_json::to_value(InnerMarkdownMessage {
|
||||
msgtype: DingTalkMessageType::Markdown,
|
||||
markdown: InnerMarkdownMessageMarkdown {
|
||||
title: dingtalk_message.markdown_title,
|
||||
text: dingtalk_message.markdown_content,
|
||||
}
|
||||
},
|
||||
};
|
||||
if dingtalk_message.at_all || dingtalk_message.at_mobiles.len() > 0 {
|
||||
let mut at_mobiles = json::JsonValue::new_object();
|
||||
for m in &dingtalk_message.at_mobiles {
|
||||
at_mobiles.push(m.clone()).ok();
|
||||
}),
|
||||
DingTalkMessageType::ActionCard => serde_json::to_value(InnerActionCardMessage {
|
||||
msgtype: DingTalkMessageType::ActionCard,
|
||||
action_card: InnerActionCardMessageActionCard {
|
||||
title: dingtalk_message.action_card_title,
|
||||
text: dingtalk_message.action_card_text,
|
||||
hide_avatar: dingtalk_message.action_card_hide_avatar,
|
||||
btn_orientation: dingtalk_message.action_card_btn_orientation,
|
||||
}
|
||||
}),
|
||||
DingTalkMessageType::FeedCard => serde_json::to_value(InnerFeedCardMessage {
|
||||
msgtype: DingTalkMessageType::FeedCard,
|
||||
feed_card: InnerFeedCardMessageFeedCard {
|
||||
links: {
|
||||
let mut links: Vec<InnerFeedCardMessageFeedCardLink> = vec![];
|
||||
for feed_card_link in &dingtalk_message.feed_card_links {
|
||||
links.push(InnerFeedCardMessageFeedCardLink {
|
||||
title: feed_card_link.title.clone(),
|
||||
message_url: feed_card_link.message_url.clone(),
|
||||
pic_url: feed_card_link.pic_url.clone(),
|
||||
});
|
||||
}
|
||||
links
|
||||
}
|
||||
}
|
||||
})
|
||||
}?;
|
||||
if DingTalkMessageType::ActionCard == dingtalk_message.message_type {
|
||||
if dingtalk_message.action_card_single_btn.is_some() {
|
||||
if let Some(single_btn) = dingtalk_message.action_card_single_btn.as_ref() {
|
||||
message_json["actionCard"]["singleTitle"] = single_btn.title.as_str().into();
|
||||
message_json["actionCard"]["singleURL"] = single_btn.action_url.as_str().into();
|
||||
};
|
||||
} else {
|
||||
let mut btns: Vec<InnerActionCardMessageBtn> = vec![];
|
||||
for action_card_btn in &dingtalk_message.action_card_btns {
|
||||
btns.push(InnerActionCardMessageBtn {
|
||||
title: action_card_btn.title.clone(),
|
||||
action_url: action_card_btn.action_url.clone(),
|
||||
});
|
||||
}
|
||||
message_json["actionCard"]["btns"] = serde_json::to_value(btns)?;
|
||||
}
|
||||
message_json["at"] = object!{
|
||||
"atMobiles" => at_mobiles,
|
||||
"isAtAll" => dingtalk_message.at_all,
|
||||
};
|
||||
}
|
||||
self.send(&json::stringify(message_json))
|
||||
if dingtalk_message.at_all || !dingtalk_message.at_mobiles.is_empty() {
|
||||
if let Some(m) = message_json.as_object_mut() {
|
||||
let mut at_mobiles: Vec<Value> = vec![];
|
||||
for m in &dingtalk_message.at_mobiles {
|
||||
at_mobiles.push(Value::String(m.clone()));
|
||||
}
|
||||
let mut at_map = serde_json::Map::new();
|
||||
at_map.insert("atMobiles".into(), Value::Array(at_mobiles));
|
||||
at_map.insert("isAtAll".into(), Value::Bool(dingtalk_message.at_all));
|
||||
|
||||
m.insert("at".into(), Value::Object(at_map));
|
||||
}
|
||||
}
|
||||
self.send(&serde_json::to_string(&message_json)?).await
|
||||
}
|
||||
|
||||
/// Send text message
|
||||
pub fn send_text(&self, text_message: &str) -> XResult<()> {
|
||||
self.send_message(&DingTalkMessage::new_text(text_message))
|
||||
pub async fn send_text(&self, text_message: &str) -> XResult<()> {
|
||||
self.send_message(DingTalkMessage::new_text(text_message)).await
|
||||
}
|
||||
|
||||
/// Send markdown message
|
||||
pub fn send_markdown(&self, title: &str, text: &str) -> XResult<()> {
|
||||
self.send_message(&DingTalkMessage::new_markdown(title, text))
|
||||
pub async fn send_markdown(&self, title: &str, text: &str) -> XResult<()> {
|
||||
self.send_message(DingTalkMessage::new_markdown(title, text)).await
|
||||
}
|
||||
|
||||
/// Send link message
|
||||
pub fn send_link(&self, link_title: &'a str, link_text: &'a str, link_pic_url: &'a str, link_message_url: &'a str) -> XResult<()> {
|
||||
self.send_message(&DingTalkMessage::new_link(link_title, link_text, link_pic_url, link_message_url))
|
||||
pub async fn send_link(&self, link_title: &str, link_text: &str, link_pic_url: &str, link_message_url: &str) -> XResult<()> {
|
||||
self.send_message(DingTalkMessage::new_link(link_title, link_text, link_pic_url, link_message_url)).await
|
||||
}
|
||||
|
||||
/// Direct send JSON message
|
||||
pub fn send(&self, json_message: &str) -> XResult<()> {
|
||||
pub async fn send(&self, json_message: &str) -> XResult<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(&self.generate_signed_url())
|
||||
let response = match client.post(&self.generate_signed_url()?)
|
||||
.header(CONTENT_TYPE, APPLICATION_JSON_UTF8)
|
||||
.body(json_message.as_bytes().to_vec())
|
||||
.send()?;
|
||||
.send().await {
|
||||
Ok(r) => r, Err(e) => {
|
||||
return Err(Box::new(Error::new(ErrorKind::Other, format!("Unknown error: {}", e))) as Box<dyn std::error::Error>);
|
||||
},
|
||||
};
|
||||
|
||||
match response.status().as_u16() {
|
||||
200_u16 => Ok(()),
|
||||
_ => Err(Box::new(Error::new(ErrorKind::Other, format!("Unknown status: {}", response.status().as_u16())))),
|
||||
_ => Err(Box::new(Error::new(ErrorKind::Other, format!("Unknown status: {}", response.status().as_u16()))) as Box<dyn std::error::Error>),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate signed dingtalk webhook URL
|
||||
pub fn generate_signed_url(&self) -> String {
|
||||
pub fn generate_signed_url(&self) -> XResult<String> {
|
||||
if !self.direct_url.is_empty() {
|
||||
return Ok(self.direct_url.clone());
|
||||
}
|
||||
let mut signed_url = String::with_capacity(1024);
|
||||
signed_url.push_str(self.default_webhook_url);
|
||||
signed_url.push_str(&urlencoding::encode(self.access_token));
|
||||
signed_url.push_str(&self.default_webhook_url);
|
||||
|
||||
if self.sec_token != "" {
|
||||
if self.default_webhook_url.ends_with('?') {
|
||||
// Just Ok
|
||||
} else if self.default_webhook_url.contains('?') {
|
||||
if !self.default_webhook_url.ends_with('&') {
|
||||
signed_url.push('&');
|
||||
}
|
||||
} else {
|
||||
signed_url.push('?');
|
||||
}
|
||||
|
||||
match self.dingtalk_type {
|
||||
DingTalkType::DingTalk => signed_url.push_str("access_token="),
|
||||
DingTalkType::WeChatWork => signed_url.push_str("key="),
|
||||
}
|
||||
signed_url.push_str(&urlencoding::encode(&self.access_token));
|
||||
|
||||
if !self.sec_token.is_empty() {
|
||||
let timestamp = &format!("{}", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis());
|
||||
let timestamp_and_secret = &format!("{}\n{}", timestamp, self.sec_token);
|
||||
let hmac_sha256 = base64::encode(calc_hmac_sha256(self.sec_token.as_bytes(), timestamp_and_secret.as_bytes()).code());
|
||||
let hmac_sha256 = base64::encode(&calc_hmac_sha256(self.sec_token.as_bytes(), timestamp_and_secret.as_bytes())?[..]);
|
||||
|
||||
signed_url.push_str("×tamp=");
|
||||
signed_url.push_str(timestamp);
|
||||
signed_url.push_str("&sign=");
|
||||
signed_url.push_str(&urlencoding::encode(&hmac_sha256));
|
||||
}
|
||||
|
||||
signed_url
|
||||
}
|
||||
|
||||
// SAFE? cause memory leak?
|
||||
fn string_to_a_str(s: String) -> &'a str {
|
||||
Box::leak(s.into_boxed_str())
|
||||
Ok(signed_url)
|
||||
}
|
||||
}
|
||||
|
||||
/// calc hma_sha256 digest
|
||||
fn calc_hmac_sha256(key: &[u8], message: &[u8]) -> MacResult {
|
||||
let mut hmac = Hmac::new(Sha256::new(), key);
|
||||
hmac.input(message);
|
||||
hmac.result()
|
||||
fn calc_hmac_sha256(key: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
|
||||
let mut mac = match Hmac::<Sha256>::new_varkey(key) {
|
||||
Ok(m) => m, Err(e) => {
|
||||
return Err(Box::new(Error::new(ErrorKind::Other, format!("Hmac error: {}", e))));
|
||||
},
|
||||
};
|
||||
mac.input(message);
|
||||
Ok(mac.result().code().to_vec())
|
||||
}
|
||||
|
||||
190
src/msg.rs
Normal file
190
src/msg.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use serde::{ Serialize, Deserialize };
|
||||
|
||||
/// Send Dingtalk or WeChatWork message
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum DingTalkType {
|
||||
/// DingTalk
|
||||
DingTalk,
|
||||
/// WeChatWork
|
||||
WeChatWork,
|
||||
}
|
||||
|
||||
/// Default DingTalkType is DingTalk
|
||||
impl Default for DingTalkType {
|
||||
fn default() -> Self { DingTalkType::DingTalk }
|
||||
}
|
||||
|
||||
/// DingTalk message type
|
||||
/// * Text - text message
|
||||
/// * Markdown - markdown message
|
||||
/// * Link - link message
|
||||
/// * ActionCard - action card message
|
||||
/// * FeedCard - feed card message
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum DingTalkMessageType {
|
||||
#[serde(rename = "text")]
|
||||
Text,
|
||||
#[serde(rename = "markdown")]
|
||||
Markdown,
|
||||
#[serde(rename = "link")]
|
||||
Link,
|
||||
#[serde(rename = "actionCard")]
|
||||
ActionCard,
|
||||
#[serde(rename = "feedCard")]
|
||||
FeedCard,
|
||||
}
|
||||
|
||||
/// Default DingTalkMessageType is Text
|
||||
impl Default for DingTalkMessageType {
|
||||
fn default() -> Self { DingTalkMessageType::Text }
|
||||
}
|
||||
|
||||
/// DingTalk messge action card avatar
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum DingTalkMessageActionCardHideAvatar {
|
||||
#[serde(rename = "1")]
|
||||
Hide,
|
||||
#[serde(rename = "0")]
|
||||
Show,
|
||||
}
|
||||
|
||||
// default value
|
||||
impl Default for DingTalkMessageActionCardHideAvatar {
|
||||
fn default() -> Self { DingTalkMessageActionCardHideAvatar::Show }
|
||||
}
|
||||
|
||||
/// DingTalk message action card orientation
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub enum DingTalkMessageActionCardBtnOrientation {
|
||||
#[serde(rename = "0")]
|
||||
Vertical,
|
||||
#[serde(rename = "1")]
|
||||
Landscape,
|
||||
}
|
||||
|
||||
/// default value
|
||||
impl Default for DingTalkMessageActionCardBtnOrientation {
|
||||
fn default() -> Self { DingTalkMessageActionCardBtnOrientation::Vertical }
|
||||
}
|
||||
|
||||
/// DingTalk message action card btn
|
||||
#[derive(Debug)]
|
||||
pub struct DingTalkMessageActionCardBtn {
|
||||
pub title: String,
|
||||
pub action_url: String,
|
||||
}
|
||||
|
||||
/// DingTalk message feed card link
|
||||
#[derive(Debug)]
|
||||
pub struct DingTalkMessageFeedCardLink {
|
||||
pub title: String,
|
||||
pub message_url: String,
|
||||
pub pic_url: String,
|
||||
}
|
||||
|
||||
/// DingTalk message
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DingTalkMessage {
|
||||
pub message_type: DingTalkMessageType,
|
||||
pub text_content: String,
|
||||
pub markdown_title: String,
|
||||
pub markdown_content: String,
|
||||
pub link_text: String,
|
||||
pub link_title: String,
|
||||
pub link_pic_url: String,
|
||||
pub link_message_url: String,
|
||||
pub action_card_title: String,
|
||||
pub action_card_text: String,
|
||||
pub action_card_hide_avatar: DingTalkMessageActionCardHideAvatar,
|
||||
pub action_card_btn_orientation: DingTalkMessageActionCardBtnOrientation,
|
||||
pub action_card_single_btn: Option<DingTalkMessageActionCardBtn>,
|
||||
pub action_card_btns: Vec<DingTalkMessageActionCardBtn>,
|
||||
pub feed_card_links: Vec<DingTalkMessageFeedCardLink>,
|
||||
pub at_all: bool,
|
||||
pub at_mobiles: Vec<String>,
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct InnerTextMessageText {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct InnerTextMessage {
|
||||
pub msgtype: DingTalkMessageType,
|
||||
pub text: InnerTextMessageText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InnerLinkMessageLink {
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
pub pic_url: String,
|
||||
pub message_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct InnerLinkMessage {
|
||||
pub msgtype: DingTalkMessageType,
|
||||
pub link: InnerLinkMessageLink,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InnerMarkdownMessageMarkdown {
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct InnerMarkdownMessage {
|
||||
pub msgtype: DingTalkMessageType,
|
||||
pub markdown: InnerMarkdownMessageMarkdown,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InnerActionCardMessageActionCard {
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
pub hide_avatar: DingTalkMessageActionCardHideAvatar,
|
||||
pub btn_orientation: DingTalkMessageActionCardBtnOrientation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct InnerActionCardMessageBtn {
|
||||
pub title: String,
|
||||
#[serde(rename = "actionURL")]
|
||||
pub action_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InnerActionCardMessage {
|
||||
pub msgtype: DingTalkMessageType,
|
||||
pub action_card: InnerActionCardMessageActionCard,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct InnerFeedCardMessageFeedCardLink {
|
||||
pub title: String,
|
||||
#[serde(rename = "messageURL")]
|
||||
pub message_url: String,
|
||||
#[serde(rename = "picURL")]
|
||||
pub pic_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct InnerFeedCardMessageFeedCard {
|
||||
pub links: Vec<InnerFeedCardMessageFeedCardLink>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InnerFeedCardMessage {
|
||||
pub msgtype: DingTalkMessageType,
|
||||
pub feed_card: InnerFeedCardMessageFeedCard,
|
||||
}
|
||||
52
tests/test.rs
Normal file
52
tests/test.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use dingtalk::*;
|
||||
|
||||
#[test]
|
||||
fn run_all_tests() {
|
||||
tokio_test::block_on(_test_send()).unwrap();
|
||||
}
|
||||
|
||||
async fn _test_send() -> XResult<()> {
|
||||
let dt = DingTalk::from_file("~/.dingtalk-token.json")?;
|
||||
dt.send_text("test message 001 ---------------------").await?;
|
||||
|
||||
dt.send_markdown("markdown title 001", r#"# markdown content 001
|
||||
* line 0
|
||||
* line 1
|
||||
* line 2"#).await?;
|
||||
|
||||
dt.send_link("link title 001", "link content 001", "https://hatter.ink/favicon.png", "https://hatter.ink/").await?;
|
||||
|
||||
dt.send_message(DingTalkMessage::new_feed_card()
|
||||
.add_feed_card_link(DingTalkMessageFeedCardLink{
|
||||
title: "test feed card title 001".into(),
|
||||
message_url: "https://hatter.ink/".into(),
|
||||
pic_url: "https://hatter.ink/favicon.png".into(),
|
||||
})
|
||||
.add_feed_card_link(DingTalkMessageFeedCardLink{
|
||||
title: "test feed card title 002".into(),
|
||||
message_url: "https://hatter.ink/".into(),
|
||||
pic_url: "https://hatter.ink/favicon.png".into(),
|
||||
})
|
||||
).await?;
|
||||
|
||||
dt.send_message(DingTalkMessage::new_action_card("action card 001", "action card text 001")
|
||||
.set_action_card_signle_btn(DingTalkMessageActionCardBtn{
|
||||
title: "test signle btn title".into(),
|
||||
action_url: "https://hatter.ink/".into(),
|
||||
})
|
||||
).await?;
|
||||
|
||||
dt.send_message(DingTalkMessage::new_action_card("action card 002", "action card text 002")
|
||||
.add_action_card_btn(DingTalkMessageActionCardBtn{
|
||||
title: "test signle btn title 01".into(),
|
||||
action_url: "https://hatter.ink/".into(),
|
||||
})
|
||||
.add_action_card_btn(DingTalkMessageActionCardBtn{
|
||||
title: "test signle btn title 02".into(),
|
||||
action_url: "https://hatter.ink/".into(),
|
||||
})
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
13
tests/test_wechatwork.rs
Normal file
13
tests/test_wechatwork.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use dingtalk::*;
|
||||
|
||||
#[test]
|
||||
fn run_all_tests_wechat_work() {
|
||||
tokio_test::block_on(_test_send_wechat_work()).unwrap();
|
||||
}
|
||||
|
||||
async fn _test_send_wechat_work() -> XResult<()> {
|
||||
let dt = DingTalk::from_file("~/.wechat-work-token.json")?;
|
||||
dt.send_text("test message 001 ---------------------").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user