mod auth;
mod client;
mod login;
mod messages;
mod transport;
mod types;
mod updates;
mod url_policy;
use std::sync::Arc;
use crate::{BootError, Module, ProviderDefinition, ProviderToken, Result as BootResult};
pub use auth::{ClientVersionError, SecretValue, SecretValueError};
pub use client::{IlinkAuth, IlinkError};
pub use transport::{IlinkClient, IlinkLoginTransport, IlinkMessagingTransport};
pub use types::{
CreateQrResponse, GetConfigResponse, GetUpdatesResponse, NotifyResponse, PollQrResponse,
QrCodeStatus, SendMessageResponse, SendTypingResponse, WeixinMessage, MESSAGE_STATE_FINISH,
MESSAGE_TYPE_USER,
};
pub use url_policy::{IlinkUrlError, ValidatedBaseUrl};
pub const WEIXIN_ILINK_APP_ID: &str = "bot";
pub const WEIXIN_ILINK_BOT_TYPE: &str = "3";
pub const WEIXIN_ILINK_PROTOCOL_VERSION: &str = "2.4.6";
pub const WEIXIN_ILINK_BASE_URL: &str = "https://ilinkai.weixin.qq.com";
const PRIMARY_ILINK_HOST: &str = "ilinkai.weixin.qq.com";
const TENCENT_ILINK_HOST_SUFFIX: &str = "qq.com";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IlinkClientOptions {
app_id: String,
bot_type: String,
protocol_version: String,
bot_agent: String,
base_url: String,
allowed_hosts: Vec<String>,
allowed_host_suffixes: Vec<String>,
}
impl IlinkClientOptions {
pub fn weixin(bot_agent: impl Into<String>) -> Self {
Self {
app_id: WEIXIN_ILINK_APP_ID.to_string(),
bot_type: WEIXIN_ILINK_BOT_TYPE.to_string(),
protocol_version: WEIXIN_ILINK_PROTOCOL_VERSION.to_string(),
bot_agent: bot_agent.into(),
base_url: WEIXIN_ILINK_BASE_URL.to_string(),
allowed_hosts: vec![PRIMARY_ILINK_HOST.to_string()],
allowed_host_suffixes: vec![TENCENT_ILINK_HOST_SUFFIX.to_string()],
}
}
pub fn with_allowed_host(mut self, host: impl Into<String>) -> Self {
let host = host.into();
if !self
.allowed_hosts
.iter()
.any(|current| current.eq_ignore_ascii_case(&host))
{
self.allowed_hosts.push(host);
}
self
}
}
impl IlinkClient {
pub fn from_options(options: IlinkClientOptions) -> Result<Self, IlinkError> {
let identity = client::IlinkClientIdentity::new(
options.app_id,
options.bot_type,
&options.protocol_version,
options.bot_agent,
)?;
let host_policy = url_policy::IlinkHostPolicy::production_with_suffixes(
&options.allowed_hosts,
&options.allowed_host_suffixes,
)?;
Self::new(identity, host_policy, &options.base_url)
}
pub fn weixin(bot_agent: impl Into<String>) -> Result<Self, IlinkError> {
Self::from_options(IlinkClientOptions::weixin(bot_agent))
}
}
#[derive(Clone, Debug)]
pub struct IlinkModule {
options: IlinkClientOptions,
}
impl IlinkModule {
pub fn new(options: IlinkClientOptions) -> Self {
Self { options }
}
pub fn weixin(bot_agent: impl Into<String>) -> Self {
Self::new(IlinkClientOptions::weixin(bot_agent))
}
}
impl Module for IlinkModule {
fn name(&self) -> &'static str {
"a3s-boot-ilink"
}
fn providers(&self) -> BootResult<Vec<ProviderDefinition>> {
let client = IlinkClient::from_options(self.options.clone()).map_err(|error| {
BootError::Internal(format!("failed to configure iLink client: {error}"))
})?;
Ok(vec![ProviderDefinition::from_arc(Arc::new(client))])
}
fn exports(&self) -> BootResult<Vec<ProviderToken>> {
Ok(vec![ProviderToken::of::<IlinkClient>()])
}
}
#[cfg(test)]
mod tests;