use crate::decode::{decode_event, decode_event_batch};
use crate::encode::encode_segments;
use crate::wire::{Inbound, RespJson};
use async_trait::async_trait;
use dashmap::DashMap;
use futures_util::{SinkExt, StreamExt};
use nagisa_core::adapter::{ActionInvoker, MilkyActions, OneBotActions};
use nagisa_core::wire::Envelope;
use nagisa_core::{EventSource, ImplInfo, ShutdownToken};
use nagisa_types::event::{RawEvent, ReactionKind, RequestTokenInner};
use nagisa_types::prelude::*;
use serde_json::{json, Map, Value};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use tokio::sync::{mpsc, oneshot};
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::tungstenite::Message as WsMessage;
mod onebot;
mod api;
mod config;
mod inbound;
pub(crate) use api::*;
pub(crate) use config::encode_query;
pub use config::{LLOneBotEventMode, OneBotConfig, OneBotTransport, QuickOpFn};
pub(crate) use inbound::{dispatch_event, log_wire, prepare_inbound};
const ACTION_TIMEOUT: Duration = Duration::from_secs(10);
const IDLE_TIMEOUT: Duration = Duration::from_secs(90);
const PING_INTERVAL: Duration = Duration::from_secs(30);
const PROTO: Protocol = Protocol::OneBot11;
type PendingMap = Arc<DashMap<String, oneshot::Sender<RespJson>>>;
pub struct OneBotAdapter {
config: OneBotConfig,
outbound: mpsc::Sender<String>,
outbound_rx: std::sync::Mutex<Option<mpsc::Receiver<String>>>,
pending: PendingMap,
seq: AtomicU64,
impl_info: OnceLock<ImplInfo>,
vendor: OnceLock<nagisa_types::vendor::Vendor>,
pub(crate) server_outbound: std::sync::Mutex<Option<mpsc::Sender<String>>>,
http_api: std::sync::Mutex<Option<(reqwest::Client, String)>>,
pub(crate) quick_op: std::sync::Mutex<Option<QuickOpFn>>,
}
impl OneBotAdapter {
pub fn new(config: OneBotConfig) -> Arc<Self> {
let (tx, rx) = mpsc::channel::<String>(256);
Arc::new(OneBotAdapter {
config,
outbound: tx,
outbound_rx: std::sync::Mutex::new(Some(rx)),
pending: Arc::new(DashMap::new()),
seq: AtomicU64::new(1),
impl_info: OnceLock::new(),
vendor: OnceLock::new(),
server_outbound: std::sync::Mutex::new(None),
http_api: std::sync::Mutex::new(None),
quick_op: std::sync::Mutex::new(None),
})
}
pub fn impl_info(&self) -> Option<&ImplInfo> {
self.impl_info.get()
}
pub fn with_quick_op(
self: Arc<Self>,
f: impl Fn(&nagisa_types::event::Event) -> Option<Value> + Send + Sync + 'static,
) -> Arc<Self> {
*self.quick_op.lock().unwrap() = Some(Arc::new(f));
self
}
pub(crate) fn quick_op_response(&self, event: &nagisa_types::event::Event) -> Option<Value> {
self.quick_op.lock().unwrap().as_ref().and_then(|f| f(event))
}
fn next_echo(&self) -> String {
format!("nagisa-{}", self.seq.fetch_add(1, Ordering::Relaxed))
}
async fn call(&self, action: &str, params: Value) -> Result<Value> {
if matches!(
self.config.mode,
OneBotTransport::Http { .. } | OneBotTransport::HttpApi { .. } | OneBotTransport::LLOneBotHttp { .. }
) {
return self.call_http(action, params).await;
}
let echo = self.next_echo();
let frame = json!({ "action": action, "params": params, "echo": echo });
let frame = serde_json::to_string(&frame).map_err(Error::Decode)?;
log_wire("out", &frame);
let (tx, rx) = oneshot::channel::<RespJson>();
self.pending.insert(echo.clone(), tx);
let sent = match &self.config.mode {
OneBotTransport::Forward { .. } => self.outbound.send(frame).await.is_ok(),
_ => {
let sender = self.server_outbound.lock().expect("server_outbound poisoned").clone();
match sender {
Some(s) => s.send(frame).await.is_ok(),
None => false,
}
}
};
if !sent {
self.pending.remove(&echo);
return Err(Error::ConnectionClosed);
}
match tokio::time::timeout(ACTION_TIMEOUT, rx).await {
Ok(Ok(resp)) => map_response(action, resp),
Ok(Err(_)) => {
self.pending.remove(&echo);
Err(Error::ConnectionClosed)
}
Err(_) => {
self.pending.remove(&echo);
Err(Error::Timeout)
}
}
}
async fn call_alias(&self, primary: &str, alt: &str, params: Value) -> Result<Value> {
match self.call(primary, params.clone()).await {
Err(e) if e.is_unsupported() => self.call(alt, params).await,
other => other,
}
}
async fn call_http(&self, action: &str, params: Value) -> Result<Value> {
let (client, api_url) = {
let guard = self.http_api.lock().expect("http_api poisoned");
match guard.as_ref() {
Some((c, u)) => (c.clone(), u.clone()),
None => return Err(Error::ConnectionClosed),
}
};
let url = format!("{}/{}", api_url.trim_end_matches('/'), action);
tracing::debug!(target: "nagisa::wire", dir = "out", action = %action, "{params}");
let mut req = client.post(&url).json(¶ms);
if let Some(token) = &self.config.access_token {
req = req.bearer_auth(token);
}
let resp = req.send().await.map_err(|e| Error::Transport(TransportError::Http(e.to_string())))?;
let status = resp.status();
if !status.is_success() && status != reqwest::StatusCode::NOT_FOUND {
return Err(Error::Action {
retcode: status.as_u16() as i64,
message: format!("HTTP {status}"),
kind: match status {
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN => ActionErrorKind::AuthFailed,
reqwest::StatusCode::TOO_MANY_REQUESTS => ActionErrorKind::RateLimited,
_ => ActionErrorKind::Other,
},
});
}
let body = resp.text().await.map_err(|e| Error::Transport(TransportError::Http(e.to_string())))?;
log_wire("in", &body); nagisa_core::wire::http_action_envelope(action, status.as_u16(), &body, |body| {
let resp: RespJson = serde_json::from_str(body)?;
let ok = resp.status == "ok" || resp.status == "async" || resp.retcode == 0;
Ok(if ok {
Envelope {
status: "ok".into(),
retcode: 0,
data: Some(resp.data),
message: None,
classify: ActionErrorKind::Other,
}
} else {
let message = resp.message.unwrap_or_else(|| format!("retcode {}", resp.retcode));
Envelope {
classify: classify_retcode(resp.retcode),
status: resp.status,
retcode: resp.retcode,
data: Some(resp.data),
message: Some(message),
}
})
})
}
pub(crate) async fn handle_inbound_public(self: &Arc<Self>, txt: &str, sink: &mpsc::Sender<Event>) -> bool {
self.handle_inbound(txt, sink).await
}
pub(crate) fn clear_pending_public(&self) {
self.pending.clear();
}
pub(crate) fn access_token(&self) -> Option<&str> {
self.config.access_token.as_deref()
}
pub(crate) fn install_http_api(&self, client: reqwest::Client, api_url: String) {
*self.http_api.lock().expect("http_api poisoned") = Some((client, api_url));
}
pub(crate) async fn dispatch_and_decode(self: &Arc<Self>, txt: &str, sink: &mpsc::Sender<Event>) -> Option<Event> {
match serde_json::from_str::<Inbound>(txt) {
Ok(Inbound::Event(ev)) => {
log_wire("in", txt);
let event = prepare_inbound(decode_event(*ev), self.vendor())?;
if sink.send(event.clone()).await.is_err() {
tracing::warn!("event sink closed");
}
Some(event)
}
_ => {
self.handle_inbound(txt, sink).await;
None
}
}
}
pub(crate) async fn dispatch_sse_event(&self, payload: &str, sink: &mpsc::Sender<Event>) -> bool {
let value = serde_json::from_str::<Value>(payload).unwrap_or(Value::Null);
if value.is_null() && payload.trim().is_empty() {
return false;
}
log_wire("in", payload); dispatch_event(sink, crate::decode::decode_event_value(value), self.vendor()).await
}
}
impl OneBotAdapter {
fn encode_forward_nodes(nodes: &[ForwardNode]) -> Value {
Value::Array(nodes.iter().map(crate::encode::encode_forward_node).collect())
}
fn build_request(&self) -> Result<tokio_tungstenite::tungstenite::handshake::client::Request> {
let base_url = match &self.config.mode {
OneBotTransport::Forward { url } => url.clone(),
_ => String::new(),
};
let url = match &self.config.access_token {
Some(token) => {
let sep = if base_url.contains('?') { '&' } else { '?' };
format!("{}{}access_token={}", base_url, sep, encode_query(token))
}
None => base_url.clone(),
};
let mut req = url
.as_str()
.into_client_request()
.map_err(|e| Error::Transport(TransportError::WebSocket(e.to_string())))?;
if let Some(token) = &self.config.access_token {
let val = format!("Bearer {token}");
if let Ok(hv) = val.parse() {
req.headers_mut().insert("Authorization", hv);
}
}
Ok(req)
}
async fn run_once(
self: &Arc<Self>,
sink: &mpsc::Sender<Event>,
outbound_rx: &mut mpsc::Receiver<String>,
shutdown: &ShutdownToken,
) -> std::result::Result<(), TransportError> {
let req = self.build_request().map_err(|e| TransportError::WebSocket(e.to_string()))?;
let (ws, _resp) = connect_async(req).await.map_err(|e| TransportError::WebSocket(e.to_string()))?;
let url = match &self.config.mode {
OneBotTransport::Forward { url } => url.as_str(),
_ => "",
};
tracing::info!(%url, "onebot ws connected");
let _ = sink.try_send(Event::Meta(nagisa_types::event::Meta::Connect));
let (mut write, mut read) = ws.split();
{
let adapter = Arc::clone(self);
tokio::spawn(async move {
if let Ok(data) = adapter.call("get_version_info", serde_json::json!({})).await {
let name = data.get("app_name").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
let version = data.get("app_version").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
let vendor = nagisa_types::vendor::Vendor::from_app_name(&name);
tracing::info!(impl_name = %name, impl_version = %version, ?vendor, "onebot impl info");
let _ = adapter.vendor.set(vendor);
let _ = adapter.impl_info.set(ImplInfo {
name,
version,
qq_protocol_version: None,
qq_protocol_type: None,
milky_version: None,
});
}
});
}
let idle = tokio::time::sleep(IDLE_TIMEOUT);
tokio::pin!(idle);
let mut ping_iv = tokio::time::interval(PING_INTERVAL);
ping_iv.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
ping_iv.tick().await;
loop {
tokio::select! {
biased;
_ = shutdown.cancelled() => {
let _ = write.close().await;
return Ok(());
}
_ = &mut idle => {
tracing::warn!(timeout = ?IDLE_TIMEOUT, "onebot ws idle (no inbound); forcing reconnect");
return Err(TransportError::Closed);
}
_ = ping_iv.tick() => {
let _ = write.send(WsMessage::Ping(Default::default())).await;
}
maybe_frame = outbound_rx.recv() => {
match maybe_frame {
Some(frame) => {
if let Err(e) = write.send(WsMessage::text(frame)).await {
return Err(TransportError::WebSocket(e.to_string()));
}
}
None => return Ok(()), }
}
maybe_msg = read.next() => {
idle.as_mut().reset(tokio::time::Instant::now() + IDLE_TIMEOUT);
match maybe_msg {
Some(Ok(WsMessage::Text(txt))) => {
if self.handle_inbound(txt.as_str(), sink).await {
return Ok(());
}
}
Some(Ok(WsMessage::Binary(bin))) => {
if let Ok(txt) = std::str::from_utf8(&bin) {
if self.handle_inbound(txt, sink).await {
return Ok(());
}
}
}
Some(Ok(WsMessage::Ping(payload))) => {
let _ = write.send(WsMessage::Pong(payload)).await;
}
Some(Ok(WsMessage::Close(_))) | None => return Err(TransportError::Closed),
Some(Ok(_)) => {} Some(Err(e)) => return Err(TransportError::WebSocket(e.to_string())),
}
}
}
}
}
async fn handle_inbound(self: &Arc<Self>, txt: &str, sink: &mpsc::Sender<Event>) -> bool {
log_wire("in", txt);
match serde_json::from_str::<Inbound>(txt) {
Ok(Inbound::Resp(env)) => {
let key = echo_key(&env.echo);
if let Some((_, tx)) = self.pending.remove(&key) {
let resp: RespJson = serde_json::from_str(txt).unwrap_or_default();
let _ = tx.send(resp);
} else {
tracing::debug!(echo = %key, "response for unknown/expired echo");
}
false
}
Ok(Inbound::Event(ev)) => dispatch_event(sink, decode_event(*ev), self.vendor()).await,
Err(_) => {
let raw = serde_json::from_str::<Value>(txt).unwrap_or(Value::Null);
let event = Event::Raw(RawEvent { protocol: PROTO, kind: "undecodable".to_string(), raw });
sink.send(event).await.is_err()
}
}
}
}
#[async_trait]
impl EventSource for OneBotAdapter {
async fn run(self: Arc<Self>, sink: mpsc::Sender<Event>, shutdown: ShutdownToken) -> Result<()> {
match self.config.mode.clone() {
OneBotTransport::Forward { .. } => {}
OneBotTransport::ReverseWs { bind, path } => {
return crate::reverse_ws::run_reverse_ws(self, bind, path, sink, shutdown).await;
}
OneBotTransport::Http { api_url, post_bind, post_path, secret } => {
return crate::http_post::run_http_post(self, api_url, post_bind, post_path, secret, sink, shutdown)
.await;
}
OneBotTransport::HttpApi { api_url } => {
let _ = &sink;
self.install_http_api(reqwest::Client::new(), api_url);
shutdown.cancelled().await;
return Ok(());
}
OneBotTransport::LLOneBotHttp { api_url, events } => {
return crate::http_post::run_llonebot_http(self, api_url, events, sink, shutdown).await;
}
}
{
let guard = self.outbound_rx.lock().expect("outbound_rx mutex poisoned");
assert!(guard.is_some(), "EventSource::run may only be called once");
}
let backoff = nagisa_core::reconnect::Backoff::new(Duration::from_millis(500), Duration::from_secs(30));
let me = &self;
let sink_ref = &sink;
let shutdown_ref = &shutdown;
nagisa_core::reconnect::run(
&shutdown,
backoff,
|backoff_ms| me.seq.load(Ordering::Relaxed) % (backoff_ms / 4 + 1),
move || async move {
let mut outbound_rx = me
.outbound_rx
.lock()
.expect("outbound_rx mutex poisoned")
.take()
.expect("outbound_rx taken concurrently");
let result = me.run_once(sink_ref, &mut outbound_rx, shutdown_ref).await;
*me.outbound_rx.lock().expect("outbound_rx mutex poisoned") = Some(outbound_rx);
match result {
Ok(()) => nagisa_core::reconnect::Step::Stop, Err(e) => {
me.pending.clear();
if shutdown_ref.is_cancelled() {
return nagisa_core::reconnect::Step::Stop;
}
let _ = sink_ref.try_send(Event::Meta(nagisa_types::event::Meta::Disconnect {
reason: Some(e.to_string()),
}));
tracing::warn!(error = %e, "onebot ws disconnected; reconnecting");
nagisa_core::reconnect::Step::Reconnect
}
}
},
)
.await
}
}
#[async_trait]
impl ActionInvoker for OneBotAdapter {
fn protocol(&self) -> Protocol {
PROTO
}
fn vendor(&self) -> nagisa_types::vendor::Vendor {
self.vendor.get().copied().unwrap_or_default()
}
fn supports(&self, cap: Capability) -> bool {
if matches!(
cap,
Capability::GroupMute
| Capability::GroupAdmin
| Capability::GroupKick
| Capability::HandleRequest
| Capability::Essence
| Capability::Reaction
| Capability::Forward
| Capability::FileOps
| Capability::Nudge
| Capability::ProfileLike
| Capability::SelfProfile
| Capability::MessageHistory
| Capability::Cookies
| Capability::Ocr
) {
return true;
}
if cap == Capability::Ai {
return matches!(
self.vendor(),
nagisa_types::vendor::Vendor::LagrangeOneBot
| nagisa_types::vendor::Vendor::NapCat
| nagisa_types::vendor::Vendor::LLOneBot
);
}
false
}
async fn send(&self, peer: &Peer, message: &[Segment]) -> Result<MessageId> {
if let [Segment::Forward(Forward::Nodes { nodes, .. })] = message {
let messages = Self::encode_forward_nodes(nodes);
let (action, params) = match peer.scene {
Scene::Group => ("send_group_forward_msg", json!({ "group_id": peer.id.0, "messages": messages })),
Scene::Friend | Scene::Temp => {
("send_private_forward_msg", json!({ "user_id": peer.id.0, "messages": messages }))
}
};
let data = self.call(action, params).await?;
let onebot_id = data_i64(&data, "message_id").map(|v| v as i32);
return Ok(MessageId { peer: *peer, seq: 0, onebot_id });
}
let wire = encode_segments(message);
let segs = serde_json::to_value(&wire).map_err(Error::Decode)?;
let (action, params) = match peer.scene {
Scene::Group => ("send_group_msg", json!({ "group_id": peer.id.0, "message": segs })),
Scene::Friend | Scene::Temp => ("send_private_msg", json!({ "user_id": peer.id.0, "message": segs })),
};
let data = self.call(action, params).await?;
let onebot_id = data_i64(&data, "message_id").map(|v| v as i32);
Ok(MessageId { peer: *peer, seq: 0, onebot_id })
}
async fn recall(&self, id: &MessageId) -> Result<()> {
let mid = id
.onebot_id
.ok_or_else(|| Error::action_kind(ActionErrorKind::BadParams, "message id has no onebot_id"))?;
self.call("delete_msg", json!({ "message_id": mid })).await?;
Ok(())
}
async fn get_message(&self, id: &MessageId) -> Result<MessageEvent> {
let mid = id.onebot_id.ok_or_else(|| Error::Unsupported("get_message".into()))?;
let data = self.call("get_msg", json!({ "message_id": mid })).await?;
let mut obj = match data {
Value::Object(m) => m,
other => {
let mut m = Map::new();
m.insert("message".into(), other);
m
}
};
obj.insert("post_type".into(), Value::String("message".into()));
obj.entry("message_type".to_string()).or_insert(Value::String("private".into()));
let ev: crate::wire::RawEventJson = serde_json::from_value(Value::Object(obj)).map_err(Error::Decode)?;
match decode_event(ev) {
Event::Message(m) => Ok(*m),
_ => Err(Error::action("get_msg response did not decode to a message")),
}
}
async fn get_login_info(&self) -> Result<(Uin, String)> {
let data = self.call("get_login_info", json!({})).await?;
let uin = Uin(data_i64(&data, "user_id").unwrap_or(0));
let nick = data_str(&data, "nickname").unwrap_or_default();
Ok((uin, nick))
}
async fn get_group_info(&self, group: Uin, no_cache: bool) -> Result<GroupInfo> {
let data = self.call("get_group_info", json!({ "group_id": group.0, "no_cache": no_cache })).await?;
Ok(group_info_from(&data))
}
async fn get_group_list(&self, no_cache: bool) -> Result<Vec<GroupInfo>> {
let data = self.call("get_group_list", json!({ "no_cache": no_cache })).await?;
Ok(data.as_array().map(|a| a.iter().map(group_info_from).collect()).unwrap_or_default())
}
async fn get_group_member_info(&self, group: Uin, user: Uin, no_cache: bool) -> Result<MemberInfo> {
let data = self
.call("get_group_member_info", json!({ "group_id": group.0, "user_id": user.0, "no_cache": no_cache }))
.await?;
Ok(member_info_from(&data))
}
async fn get_group_member_list(&self, group: Uin, no_cache: bool) -> Result<Vec<MemberInfo>> {
let data = self.call("get_group_member_list", json!({ "group_id": group.0, "no_cache": no_cache })).await?;
Ok(data.as_array().map(|a| a.iter().map(member_info_from).collect()).unwrap_or_default())
}
async fn get_friend_list(&self, no_cache: bool) -> Result<Vec<FriendInfo>> {
let data = self.call("get_friend_list", json!({ "no_cache": no_cache })).await?;
Ok(data.as_array().map(|a| a.iter().map(friend_info_from).collect()).unwrap_or_default())
}
async fn set_group_member_mute(&self, group: Uin, user: Uin, duration: u32) -> Result<()> {
self.call("set_group_ban", json!({ "group_id": group.0, "user_id": user.0, "duration": duration })).await?;
Ok(())
}
async fn set_group_whole_mute(&self, group: Uin, enable: bool) -> Result<()> {
self.call("set_group_whole_ban", json!({ "group_id": group.0, "enable": enable })).await?;
Ok(())
}
async fn set_group_admin(&self, group: Uin, user: Uin, enable: bool) -> Result<()> {
self.call("set_group_admin", json!({ "group_id": group.0, "user_id": user.0, "enable": enable })).await?;
Ok(())
}
async fn set_group_member_card(&self, group: Uin, user: Uin, card: &str) -> Result<()> {
self.call("set_group_card", json!({ "group_id": group.0, "user_id": user.0, "card": card })).await?;
Ok(())
}
async fn set_group_name(&self, group: Uin, name: &str) -> Result<()> {
self.call("set_group_name", json!({ "group_id": group.0, "group_name": name })).await?;
Ok(())
}
async fn kick_group_member(&self, group: Uin, user: Uin, reject_add: bool) -> Result<()> {
self.call(
"set_group_kick",
json!({ "group_id": group.0, "user_id": user.0, "reject_add_request": reject_add }),
)
.await?;
Ok(())
}
async fn handle_request(&self, token: &RequestToken, approve: bool, reason: Option<&str>) -> Result<()> {
let RequestTokenInner::OneBotFlag(flag) = &token.0 else {
return Err(Error::action_kind(ActionErrorKind::BadParams, "request token is not an OneBot flag"));
};
let looks_group = flag.matches('-').count() >= 2;
if looks_group {
let sub_type = flag
.split('-')
.nth(2)
.and_then(|s| s.parse::<i64>().ok())
.map(|t| if t == 2 { "invite" } else { "add" })
.unwrap_or("add");
let mut params = json!({
"flag": flag,
"sub_type": sub_type,
"approve": approve,
});
if let (false, Some(r)) = (approve, reason) {
params["reason"] = Value::String(r.to_string());
}
self.call("set_group_add_request", params).await?;
} else {
let mut params = json!({ "flag": flag, "approve": approve });
if let Some(r) = reason {
params["remark"] = Value::String(r.to_string());
}
self.call("set_friend_add_request", params).await?;
}
Ok(())
}
async fn send_reaction(
&self,
group: Uin,
seq: i64,
face_id: &str,
_kind: ReactionKind,
is_add: bool,
) -> Result<()> {
self.call(
"set_group_reaction",
json!({
"group_id": group.0,
"message_id": seq,
"code": face_id,
"is_add": is_add,
}),
)
.await?;
Ok(())
}
async fn send_nudge(&self, peer: &Peer, target: Uin) -> Result<()> {
let (action, params) = match peer.scene {
Scene::Group => ("group_poke", json!({ "group_id": peer.id.0, "user_id": target.0 })),
_ => ("friend_poke", json!({ "user_id": target.0 })),
};
self.call(action, params).await?;
Ok(())
}
async fn upload_group_file(
&self,
group: Uin,
src: ResourceSource,
name: &str,
parent_folder_id: Option<&str>,
) -> Result<String> {
let file = crate::encode::encode_source(&src);
let mut params = serde_json::json!({ "group_id": group.0, "file": file, "name": name });
if let Some(folder) = parent_folder_id {
params["folder"] = serde_json::Value::String(folder.to_string());
params["folder_id"] = serde_json::Value::String(folder.to_string());
}
let data = self.call("upload_group_file", params).await?;
Ok(data_str(&data, "file_id").unwrap_or_default())
}
async fn upload_private_file(&self, user: Uin, src: ResourceSource, name: &str) -> Result<String> {
let file = crate::encode::encode_source(&src);
let data = self.call("upload_private_file", json!({ "user_id": user.0, "file": file, "name": name })).await?;
Ok(data_str(&data, "file_id").unwrap_or_default())
}
async fn get_user_info(&self, user: Uin, no_cache: bool) -> Result<UserInfo> {
let data = self.call("get_stranger_info", json!({ "user_id": user.0, "no_cache": no_cache })).await?;
Ok(user_info_from(&data))
}
async fn get_message_history(
&self,
peer: &Peer,
start: Option<&MessageId>,
count: u32,
) -> Result<Vec<MessageEvent>> {
let anchor = start.and_then(|m| m.onebot_id).unwrap_or(0);
let (action, params, is_group) = match peer.scene {
Scene::Group => {
("get_group_msg_history", json!({ "group_id": peer.id.0, "message_id": anchor, "count": count }), true)
}
Scene::Friend | Scene::Temp => {
("get_friend_msg_history", json!({ "user_id": peer.id.0, "message_id": anchor, "count": count }), false)
}
};
let data = self.call(action, params).await?;
let arr = data.get("messages").and_then(|v| v.as_array()).cloned().unwrap_or_default();
let mut out = Vec::with_capacity(arr.len());
for entry in arr {
let mut obj = match entry {
Value::Object(m) => m,
_ => continue,
};
obj.insert("post_type".into(), Value::String("message".into()));
obj.entry("message_type".to_string()).or_insert(Value::String(if is_group {
"group".into()
} else {
"private".into()
}));
let Ok(ev) = serde_json::from_value::<crate::wire::RawEventJson>(Value::Object(obj)) else {
continue;
};
if let Event::Message(m) = decode_event(ev) {
out.push(*m);
}
}
Ok(out)
}
async fn leave_group(&self, group: Uin, dismiss: bool) -> Result<()> {
self.call("set_group_leave", json!({ "group_id": group.0, "is_dismiss": dismiss })).await?;
Ok(())
}
async fn set_group_member_special_title(&self, group: Uin, user: Uin, title: &str, duration: i64) -> Result<()> {
self.call(
"set_group_special_title",
json!({ "group_id": group.0, "user_id": user.0, "special_title": title, "duration": duration }),
)
.await?;
Ok(())
}
async fn mark_message_as_read(&self, _peer: &Peer, id: &MessageId) -> Result<()> {
let mid = onebot_id_of(id)?;
self.call("mark_msg_as_read", json!({ "message_id": mid })).await?;
Ok(())
}
async fn set_essence(&self, _group: Uin, id: &MessageId, enable: bool) -> Result<()> {
let mid = onebot_id_of(id)?;
let action = if enable { "set_essence_msg" } else { "delete_essence_msg" };
self.call(action, json!({ "message_id": mid })).await?;
Ok(())
}
async fn get_group_file_download_url(&self, group: Uin, file_id: &str) -> Result<String> {
let data = self.call("get_group_file_url", json!({ "group_id": group.0, "file_id": file_id })).await?;
Ok(data_str(&data, "url").unwrap_or_default())
}
async fn delete_group_file(&self, group: Uin, file_id: &str) -> Result<()> {
self.call("delete_group_file", json!({ "group_id": group.0, "file_id": file_id })).await?;
Ok(())
}
async fn get_group_requests(&self) -> Result<Vec<Request>> {
let data = self.call("get_group_requests", json!({})).await?;
let arr = data.as_array().cloned().unwrap_or_default();
let mut out = Vec::with_capacity(arr.len());
for entry in &arr {
let group = Uin(data_i64(entry, "group_id").unwrap_or(0));
let initiator = Uin(data_i64(entry, "user_id").unwrap_or(0));
let comment = data_str(entry, "comment").unwrap_or_default();
let flag = data_str(entry, "flag").unwrap_or_default();
let token = RequestToken::onebot_flag(flag);
let req = if entry.get("sub_type").and_then(|v| v.as_str()) == Some("invite") {
Request::GroupInvite { group, initiator, comment, source_group: None, token }
} else {
Request::GroupJoin {
group,
initiator,
comment,
invitor: data_i64(entry, "invitor_id").map(Uin),
is_filtered: false,
token,
}
};
out.push(req);
}
Ok(out)
}
async fn get_cookies(&self, domain: Option<&str>) -> Result<String> {
let data = self.call("get_cookies", json!({ "domain": domain.unwrap_or("") })).await?;
Ok(data_str(&data, "cookies").unwrap_or_default())
}
async fn get_forward_messages(&self, forward_id: &str) -> Result<Vec<ForwardNode>> {
let data = self.call("get_forward_msg", json!({ "id": forward_id, "message_id": forward_id })).await?;
let arr = data
.get("messages")
.or_else(|| data.get("message"))
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut out = Vec::with_capacity(arr.len());
for node in &arr {
let body = node.get("data").unwrap_or(node);
let user = Uin(data_i64(body, "user_id").or_else(|| data_i64(body, "uin")).unwrap_or(0));
let name = data_str(body, "nickname").or_else(|| data_str(body, "name")).unwrap_or_default();
let content_val = body.get("content").or_else(|| body.get("message"));
let content =
content_val.map(|c| crate::decode::decode_message_value(c, Peer::group(0))).unwrap_or_default();
let time = body.get("time").and_then(Value::as_i64);
out.push(ForwardNode { user, name, content, time });
}
Ok(out)
}
async fn delete_friend(&self, user: Uin) -> Result<()> {
self.call("delete_friend", json!({ "user_id": user.0 })).await?;
Ok(())
}
async fn send_profile_like(&self, user: Uin, count: u32) -> Result<()> {
self.call("send_like", json!({ "user_id": user.0, "times": count })).await?;
Ok(())
}
async fn send_group_announcement(
&self,
group: Uin,
content: &str,
image: Option<ResourceSource>,
) -> Result<String> {
let mut params = json!({ "group_id": group.0, "content": content });
if let Some(src) = image {
let file = crate::encode::encode_source(&src);
params["image"] = Value::String(file);
}
let data = self.call("_send_group_notice", params).await?;
let id = match &data {
Value::String(s) => s.clone(),
other => data_str(other, "notice_id").or_else(|| data_str(other, "new_fid")).unwrap_or_default(),
};
Ok(id)
}
async fn get_group_announcements(&self, group: Uin) -> Result<Vec<Announcement>> {
let data = self.call("_get_group_notice", json!({ "group_id": group.0 })).await?;
Ok(data.as_array().map(|a| a.iter().map(|v| announcement_from(group, v)).collect()).unwrap_or_default())
}
async fn delete_group_announcement(&self, group: Uin, announcement_id: &str) -> Result<()> {
self.call_alias(
"_del_group_notice",
"_delete_group_notice",
json!({ "group_id": group.0, "notice_id": announcement_id }),
)
.await?;
Ok(())
}
async fn get_essence_messages(&self, group: Uin) -> Result<Vec<EssenceMessage>> {
let data = self.call("get_essence_msg_list", json!({ "group_id": group.0 })).await?;
Ok(data.as_array().map(|a| a.iter().map(|v| essence_from(group, v)).collect()).unwrap_or_default())
}
async fn get_group_files(&self, group: Uin, folder_id: Option<&str>) -> Result<GroupFileList> {
let (action, params) = match folder_id {
None => ("get_group_root_files", json!({ "group_id": group.0 })),
Some(fid) => ("get_group_files_by_folder", json!({ "group_id": group.0, "folder_id": fid })),
};
let data = self.call(action, params).await?;
let files = data
.get("files")
.and_then(|v| v.as_array())
.map(|a| a.iter().map(group_file_from).collect())
.unwrap_or_default();
let folders = data
.get("folders")
.and_then(|v| v.as_array())
.map(|a| a.iter().map(group_folder_from).collect())
.unwrap_or_default();
Ok(GroupFileList { files, folders })
}
async fn get_private_file_download_url(&self, user: Uin, file_id: &str, hash: Option<&str>) -> Result<String> {
let mut params = json!({ "user_id": user.0, "file_id": file_id });
if let Some(h) = hash {
params["file_hash"] = Value::String(h.to_string());
}
let data = self.call("get_private_file_url", params).await?;
Ok(data_str(&data, "url").unwrap_or_default())
}
async fn create_group_folder(&self, group: Uin, name: &str) -> Result<String> {
let data = self.call("create_group_file_folder", json!({ "group_id": group.0, "name": name })).await?;
let id = data_str(&data, "folder_id")
.or_else(|| data.get("groupItem").and_then(|gi| data_str(gi, "folderId")))
.or_else(|| data.get("groupItem").and_then(|gi| data_str(gi, "folder_id")))
.unwrap_or_default();
Ok(id)
}
async fn rename_group_folder(&self, group: Uin, folder_id: &str, new_name: &str) -> Result<()> {
self.call(
"rename_group_file_folder",
json!({ "group_id": group.0, "folder_id": folder_id, "new_folder_name": new_name }),
)
.await?;
Ok(())
}
async fn delete_group_folder(&self, group: Uin, folder_id: &str) -> Result<()> {
self.call_alias(
"delete_group_folder",
"delete_group_file_folder",
json!({ "group_id": group.0, "folder_id": folder_id }),
)
.await?;
Ok(())
}
async fn move_group_file(
&self,
group: Uin,
file_id: &str,
source_folder_id: Option<&str>,
target_folder_id: Option<&str>,
) -> Result<()> {
let source = source_folder_id.unwrap_or("/");
let target = target_folder_id.unwrap_or("/");
self.call(
"move_group_file",
json!({
"group_id": group.0,
"file_id": file_id,
"parent_directory": source,
"target_directory": target,
"current_parent_directory": source,
"target_parent_directory": target,
}),
)
.await?;
Ok(())
}
async fn rename_group_file(
&self,
group: Uin,
file_id: &str,
source_folder_id: Option<&str>,
new_name: &str,
) -> Result<()> {
let source = source_folder_id.unwrap_or("/");
self.call(
"rename_group_file",
json!({
"group_id": group.0,
"file_id": file_id,
"current_parent_directory": source,
"new_name": new_name,
}),
)
.await?;
Ok(())
}
async fn set_group_avatar(&self, group: Uin, src: ResourceSource) -> Result<()> {
let file = crate::encode::encode_source(&src);
self.call("set_group_portrait", json!({ "group_id": group.0, "file": file })).await?;
Ok(())
}
async fn set_self_avatar(&self, src: ResourceSource) -> Result<()> {
let file = crate::encode::encode_source(&src);
self.call("set_qq_avatar", json!({ "file": file })).await?;
Ok(())
}
async fn set_self_nickname(&self, name: &str) -> Result<()> {
self.call("set_qq_profile", json!({ "nickname": name })).await?;
Ok(())
}
async fn set_self_bio(&self, bio: &str) -> Result<()> {
let params = match self.get_login_info().await {
Ok((_, nick)) => json!({ "nickname": nick, "personal_note": bio }),
Err(_) => json!({ "personal_note": bio }),
};
self.call("set_qq_profile", params).await?;
Ok(())
}
async fn get_csrf_token(&self) -> Result<String> {
let data = self.call("get_csrf_token", json!({})).await?;
Ok(csrf_token_from_data(&data))
}
async fn call_raw(&self, action: &str, params: Value) -> Result<Value> {
let params = if params.is_null() { Value::Object(Map::new()) } else { params };
self.call(action, params).await
}
async fn get_credentials(&self, domain: Option<&str>) -> Result<(String, String)> {
let data = self.call("get_credentials", json!({ "domain": domain.unwrap_or("") })).await?;
let cookies = data_str(&data, "cookies").unwrap_or_default();
let csrf = csrf_token_from_data(&data);
Ok((cookies, csrf))
}
}
impl MilkyActions for OneBotAdapter {}