use super::*;
pub fn decode_segments(segs: &[WireSegment], peer: Peer) -> Vec<Segment> {
segs.iter().map(|s| decode_segment(s, peer)).collect()
}
pub fn decode_message_value(v: &Value, peer: Peer) -> Message {
match v {
Value::Array(_) => serde_json::from_value::<Vec<WireSegment>>(v.clone())
.map(|segs| decode_segments(&segs, peer))
.unwrap_or_default(),
Value::String(s) => decode_cq_string(s, peer),
_ => Vec::new(),
}
}
fn raw_segment(seg: &WireSegment) -> Segment {
Segment::Raw { protocol: PROTO, kind: seg.kind.clone(), data: seg.data.clone() }
}
fn decode_segment(seg: &WireSegment, peer: Peer) -> Segment {
match seg.kind.as_str() {
"text" => Segment::Text(seg.str_field("text").unwrap_or_default()),
"at" => match seg.str_field("qq").as_deref() {
Some("all") | Some("0") => Segment::MentionAll,
Some(s) => match s.parse::<i64>() {
Ok(uin) => Segment::Mention {
user: Uin(uin),
name: seg.str_field("name"),
},
Err(_) => raw_segment(seg),
},
None => raw_segment(seg),
},
"face" => Segment::Face {
id: seg.str_field("id").unwrap_or_default(),
large: seg.bool_field("large").unwrap_or(false),
result_id: seg.str_field("resultId"),
chain_count: seg.i64_field("chainCount").map(|v| v as i32),
sub_type: seg.i64_field("sub_type").map(|v| v as i32),
},
"image" => Segment::Image {
res: media_from_recv(seg),
sub_type: if seg.str_field("type").as_deref() == Some("flash") {
ImageSubType::Flash
} else {
match seg.i64_field("subType") {
Some(0) | None => ImageSubType::Normal,
Some(_) => ImageSubType::Sticker,
}
},
hints: hints_from_recv(seg),
},
"record" => decode_record(seg),
"video" => Segment::Video {
res: media_from_recv(seg),
hints: hints_from_recv(seg),
thumb: seg
.str_field("thumb")
.or_else(|| seg.str_field("cover"))
.filter(|s| !s.is_empty())
.map(ResourceSource::url),
},
"reply" => decode_reply(seg, peer),
"forward" => Segment::Forward(Forward::Ref {
id: seg.str_field("id").unwrap_or_default(),
title: None,
preview: Vec::new(),
summary: None,
}),
"file" => Segment::File {
id: seg
.str_field("file_id")
.or_else(|| seg.str_field("file"))
.or_else(|| seg.str_field("id"))
.or_else(|| seg.str_field("path"))
.unwrap_or_default(),
name: seg.str_field("file_name").or_else(|| seg.str_field("name")).unwrap_or_default(),
size: seg.i64_field("file_size").or_else(|| seg.i64_field("size")).unwrap_or(0) as u64,
hash: seg.str_field("file_hash"),
url: seg.str_field("url"),
},
"json" => Segment::LightApp { app_name: None, payload: seg.str_field("data").unwrap_or_default() },
"xml" => Segment::Xml {
service_id: seg.i64_field("service_id").map(|v| v as i32),
payload: seg.str_field("data").unwrap_or_default(),
},
"mface" => Segment::MarketFace {
package_id: seg.i64_field("emoji_package_id").unwrap_or(0) as i32,
emoji_id: seg.str_field("emoji_id").unwrap_or_default(),
key: seg.str_field("key").unwrap_or_default(),
summary: seg.str_field("summary"),
url: seg.str_field("url"),
},
"poke" => Segment::Poke {
kind: seg.i64_field("type").unwrap_or(0) as i32,
id: seg.i64_field("id").unwrap_or(-1) as i32,
strength: seg.i64_field("strength").map(|v| v as i32),
name: seg.str_field("name"),
},
"contact" => match seg.str_field("type").as_deref() {
Some("qq") => Segment::Contact { kind: ContactKind::Friend, id: Uin(seg.i64_field("id").unwrap_or(0)) },
Some("group") => Segment::Contact { kind: ContactKind::Group, id: Uin(seg.i64_field("id").unwrap_or(0)) },
_ => raw_segment(seg),
},
"location" => Segment::Location {
lat: seg.str_field("lat").and_then(|s| s.parse().ok()).unwrap_or(0.0),
lon: seg.str_field("lon").and_then(|s| s.parse().ok()).unwrap_or(0.0),
title: seg.str_field("title").filter(|s| !s.is_empty()),
content: seg.str_field("content").filter(|s| !s.is_empty()),
},
"share" => Segment::Share {
url: seg.str_field("url").unwrap_or_default(),
title: seg.str_field("title").unwrap_or_default(),
content: seg.str_field("content").filter(|s| !s.is_empty()),
image: seg.str_field("image").filter(|s| !s.is_empty()),
},
"rps" => Segment::Rps { result: seg.i64_field("result").map(|v| v as i32) },
"dice" => Segment::Dice { result: seg.i64_field("result").map(|v| v as i32) },
"shake" => Segment::Shake,
"anonymous" => Segment::Anonymous { ignore: seg.bool_field("ignore") },
"node" => {
let user = Uin(seg.i64_field("user_id").or_else(|| seg.i64_field("uin")).unwrap_or(0));
let name = seg.str_field("nickname").or_else(|| seg.str_field("name")).unwrap_or_default();
let content =
seg.data.get("content").map(|v| crate::decode::decode_message_value(v, peer)).unwrap_or_default();
let time = seg.i64_field("time");
Segment::Forward(Forward::Nodes {
nodes: vec![ForwardNode { user, name, content, time }],
title: None,
summary: seg.str_field("summary"),
prompt: seg.str_field("prompt"),
news: news_lines(seg.data.get("news")),
source: seg.str_field("source"),
})
}
"music" => match seg.str_field("type").as_deref() {
Some("custom") => Segment::Music(MusicShare::Custom {
url: seg.str_field("url").unwrap_or_default(),
audio: seg.str_field("audio").unwrap_or_default(),
title: seg.str_field("title").unwrap_or_default(),
content: seg.str_field("content").filter(|s| !s.is_empty()),
image: seg.str_field("image").filter(|s| !s.is_empty()),
}),
_ => Segment::Music(MusicShare::Platform {
ty: seg.str_field("type").unwrap_or_default(),
id: seg.str_field("id").unwrap_or_default(),
}),
},
"keyboard" => Segment::Keyboard { content: seg.data.get("content").cloned().unwrap_or(Value::Null) },
"markdown" => Segment::Markdown { content: seg.str_field("content").unwrap_or_default() },
"longmsg" => Segment::LongMsg { id: seg.str_field("id").unwrap_or_default() },
"flash_file" => Segment::FlashFile {
title: seg.str_field("title"),
file_set_id: seg.str_field("file_set_id").unwrap_or_default(),
scene_type: seg.i64_field("scene_type").unwrap_or(0) as i32,
},
"miniapp" => Segment::MiniApp { payload: seg.str_field("data").unwrap_or_default() },
"onlinefile" => Segment::OnlineFile {
msg_id: seg.str_field("msgId").unwrap_or_default(),
element_id: seg.str_field("elementId").unwrap_or_default(),
file_name: seg.str_field("fileName").unwrap_or_default(),
file_size: seg.str_field("fileSize").unwrap_or_default(),
is_dir: seg.bool_field("isDir").unwrap_or(false),
},
"flashtransfer" => Segment::FlashTransfer { file_set_id: seg.str_field("fileSetId").unwrap_or_default() },
_ => raw_segment(seg),
}
}
fn decode_record(seg: &WireSegment) -> Segment {
Segment::Record {
res: media_from_recv(seg),
magic: seg.i64_field("magic").map(|v| v as i32),
hints: hints_from_recv(seg),
}
}
fn hints_from_recv(seg: &WireSegment) -> nagisa_types::segment::MediaSendHints {
nagisa_types::segment::MediaSendHints {
cache: seg.bool_field("cache"),
proxy: seg.bool_field("proxy"),
timeout: seg.i64_field("timeout").map(|v| v as i32),
}
}
fn decode_reply(seg: &WireSegment, peer: Peer) -> Segment {
let onebot_id = seg.i64_field("id").map(|v| v as i32);
let id = MessageId { peer, seq: 0, onebot_id };
Segment::Reply { id, sender: seg.i64_field("user_id").map(Uin), time: None, quoted: Vec::new() }
}
fn news_lines(v: Option<&Value>) -> Vec<String> {
let Some(Value::Array(items)) = v else { return Vec::new() };
items
.iter()
.filter_map(|item| match item {
Value::String(s) => Some(s.clone()),
Value::Object(o) => o.get("text").and_then(Value::as_str).map(str::to_string),
_ => None,
})
.collect()
}
fn media_from_recv(seg: &WireSegment) -> Media {
let recv = ResourceRef {
id: seg.str_field("file").or_else(|| seg.str_field("filename")).or_else(|| seg.str_field("path")),
url: seg.str_field("url"),
raw: Value::Object(seg.data.clone()),
};
let mut media = Media::from_recv(recv);
media.summary = seg.str_field("summary");
media
}
pub fn decode_cq_string(s: &str, peer: Peer) -> Vec<Segment> {
let mut out = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
let mut text_start = 0;
while i < bytes.len() {
if bytes[i] == b'[' && s[i..].starts_with("[CQ:") {
if i > text_start {
let t = cq_unescape(&s[text_start..i]);
if !t.is_empty() {
out.push(Segment::Text(t));
}
}
if let Some(end) = s[i..].find(']') {
let body = &s[i + 4..i + end];
out.push(parse_cq_segment(body, peer));
i += end + 1;
text_start = i;
continue;
}
}
i += 1;
}
if text_start < bytes.len() {
let t = cq_unescape(&s[text_start..]);
if !t.is_empty() {
out.push(Segment::Text(t));
}
}
out
}
fn parse_cq_segment(body: &str, peer: Peer) -> Segment {
let mut parts = body.split(',');
let kind = parts.next().unwrap_or("").to_string();
let mut data = Map::new();
for kv in parts {
if let Some((k, v)) = kv.split_once('=') {
data.insert(k.to_string(), Value::String(cq_unescape(v)));
}
}
decode_segment(&WireSegment::new(kind, data), peer)
}
fn cq_unescape(s: &str) -> String {
s.replace(",", ",").replace("[", "[").replace("]", "]").replace("&", "&")
}