Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions logic/service/src/clock_in.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use cache::CACHE;
use protos::Service::{ClockInRequest, ClockInResponse};

use crate::{
auth::current_uid, error::ServiceResult, fetch::fetch_package, utils::server_today_string,
auth::current_uid, error::ServiceResult, fetch::fetch_json_value, utils::server_today_string,
};

fn clock_in_key() -> String {
Expand All @@ -23,7 +23,7 @@ pub async fn clock_in(_request: ClockInRequest) -> ServiceResult<ClockInResponse
};

if !clocked_in_today()? {
let _package = fetch_package(
let _value = fetch_json_value(
"nuke.php",
vec![("__lib", "check_in"), ("__act", "check_in")],
vec![],
Expand Down
65 changes: 64 additions & 1 deletion logic/service/src/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
},
error::{ServiceError, ServiceResult},
request,
utils::extract_error,
utils::{extract_error, sanitize_json_control_chars_in_strings},
};
use dashmap::DashMap;
use itertools::Itertools;
Expand Down Expand Up @@ -462,18 +462,52 @@ mod json {
static ref RE: Regex = Regex::new(r"([{,}]\s*)(\d+)(:)").unwrap();
}

fn json_error(value: &serde_json::Value) -> Option<ServiceError> {
let error = value.get("error")?.as_object()?;

let code = error
.get("code")
.and_then(|v| match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
_ => None,
})
.unwrap_or_else(|| "?".to_owned());
let info = error
.iter()
.filter(|(k, _)| *k != "code")
.map(|(_, v)| v)
.find_map(|v| match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
_ => None,
})
Comment thread
BugenZhao marked this conversation as resolved.
.unwrap_or_default();

Some(ServiceError::Nga(protos::DataModel::ErrorMessage {
code,
info,
..Default::default()
}))
}

impl ResponseFormat for serde_json::Value {
fn query_pairs() -> &'static [(&'static str, &'static str)] {
&[("__output", "8")]
}

fn parse_response(response: String) -> ServiceResult<Self> {
let response = sanitize_json_control_chars_in_strings(&response);

let mut value = serde_json::from_str::<serde_json::Value>(&response).or_else(|_| {
let fixed_response = RE.replace_all(&response, r#"$1"$2"$3"#);
#[cfg(test)]
println!("fixed json: {}", fixed_response);
serde_json::from_str(&fixed_response)
})?;
if let Some(error) = json_error(&value) {
return Err(error);
}
let value = value["data"].take();
Ok(value)
}
Expand All @@ -490,13 +524,42 @@ mod json {
#[cfg(test)]
mod test {
use super::*;
use crate::error::ServiceError;

#[test]
fn test_int_key() {
let s = r#"{"data": { 1: "233", 2: "233" }}"#.to_owned();
let v = serde_json::Value::parse_response(s).unwrap();
println!("{:#?}", v);
}

#[test]
fn test_parse_json_error_without_code() {
let s = r#"{"error":{"0":"找不到用户"},"time":1774012796}"#.to_owned();
let err = serde_json::Value::parse_response(s).unwrap_err();

match err {
ServiceError::Nga(e) => {
assert_eq!(e.code, "?");
assert_eq!(e.info, "找不到用户");
}
other => panic!("unexpected error: {other:?}"),
}
}

#[test]
fn test_parse_json_error_with_code() {
let s = r#"{"error":{"code":403,"0":"帖子不存在"},"time":1774012796}"#.to_owned();
let err = serde_json::Value::parse_response(s).unwrap_err();

match err {
ServiceError::Nga(e) => {
assert_eq!(e.code, "403");
assert_eq!(e.info, "帖子不存在");
}
other => panic!("unexpected error: {other:?}"),
}
}
}
}

Expand Down
86 changes: 61 additions & 25 deletions logic/service/src/forum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::iter::once;
use crate::{
constants::{FORUM_ICON_PATH, MNGA_ICON_PATH},
error::ServiceResult,
fetch_package,
utils::{extract_kv, extract_nodes, extract_nodes_rel},
fetch::{fetch_json_value, fetch_package},
utils::{extract_kv, extract_nodes, json_object_values, json_string},
};
use protos::{
DataModel::{Category, Forum, ForumId, ForumId_oneof_id},
Expand All @@ -15,6 +15,7 @@ use protos::{
SubforumFilterRequest_Operation, SubforumFilterResponse,
},
};
use serde_json::Value;
use sxd_xpath::nodeset::Node;

#[inline]
Expand Down Expand Up @@ -85,34 +86,55 @@ pub fn make_minimal_forum(id: ForumId, name: String) -> Forum {
}
}

fn extract_category(node: Node) -> Option<Category> {
use super::macros::get;
let map = extract_kv(node);

let forums = extract_nodes_rel(node, "./groups/item/forums/item", |ns| {
ns.into_iter().filter_map(extract_forum).collect()
fn extract_forum_json(value: &Value) -> Option<Forum> {
let icon_id = json_string(value, "id")
.or_else(|| json_string(value, "fid"))
.unwrap_or_default();
let fid = json_string(value, "fid").and_then(make_fid);
let stid = json_string(value, "stid").and_then(make_stid);

Some(Forum {
id: stid.or(fid).into(),
name: json_string(value, "name")?,
info: json_string(value, "info").unwrap_or_default(),
icon_url: format!("{}{}.png", FORUM_ICON_PATH, icon_id),
topped_topic_id: json_string(value, "topped_topic").unwrap_or_default(),
..Default::default()
})
.ok()?;
}

let category = Category {
id: get!(map, "_id")?,
name: get!(map, "name")?,
fn extract_category_json(value: &Value) -> Option<Category> {
let forums: Vec<_> = value
.get("groups")?
.as_object()?
.values()
.flat_map(|group| {
group
.get("forums")
.and_then(Value::as_object)
.into_iter()
.flat_map(|forums| forums.values())
})
.filter_map(extract_forum_json)
.collect();

Some(Category {
id: json_string(value, "_id")?,
name: json_string(value, "name")?,
forums: forums.into(),
..Default::default()
};

Some(category)
})
}

pub async fn get_forum_list(_request: ForumListRequest) -> ServiceResult<ForumListResponse> {
let package = fetch_package(
let value = fetch_json_value(
"app_api.php",
vec![("__lib", "home"), ("__act", "category")],
vec![],
)
.await?;

let categories = extract_nodes(&package, "/root/data/item", |ns| {
let categories: Vec<_> = {
// todo: dynamic
let mnga_category = Category {
id: "mnga".to_owned(),
Expand All @@ -128,9 +150,9 @@ pub async fn get_forum_list(_request: ForumListRequest) -> ServiceResult<ForumLi
};

once(mnga_category)
.chain(ns.into_iter().filter_map(extract_category))
.chain(json_object_values(&value).filter_map(extract_category_json))
.collect()
})?;
};

Ok(ForumListResponse {
categories: categories.into(),
Expand All @@ -145,7 +167,7 @@ pub async fn set_subforum_filter(
SubforumFilterRequest_Operation::SHOW => "del",
SubforumFilterRequest_Operation::BLOCK => "add",
};
let _package = fetch_package(
let _value = fetch_json_value(
"nuke.php",
vec![
("__lib", "user_option"),
Expand Down Expand Up @@ -181,16 +203,20 @@ pub async fn search_forum(request: ForumSearchRequest) -> ServiceResult<ForumSea
pub async fn get_favorite_forum_list(
_request: FavoriteForumListRequest,
) -> ServiceResult<FavoriteForumListResponse> {
let package = fetch_package(
let value = fetch_json_value(
"nuke.php",
vec![("__lib", "forum_favor2"), ("__act", "forum_favor")],
vec![("action", "get")],
)
.await?;

let forums = extract_nodes(&package, "/root/data/item/item", |ns| {
ns.into_iter().filter_map(extract_forum).collect()
})?;
let forums: Vec<_> = value
.get("0")
.and_then(Value::as_object)
.into_iter()
.flat_map(|forums| forums.values())
.filter_map(extract_forum_json)
.collect();

Ok(FavoriteForumListResponse {
forums: forums.into(),
Expand Down Expand Up @@ -218,7 +244,7 @@ pub async fn modify_favorite_forum(
));
};

let _package = fetch_package(
let _value = fetch_json_value(
"nuke.php",
vec![("__lib", "forum_favor2"), ("__act", "forum_favor")],
vec![("action", action), ("fid", &id)],
Expand Down Expand Up @@ -303,6 +329,16 @@ mod test {
Ok(())
}

#[ignore = "manual: requires network or mutable external state"]
#[tokio::test]
async fn test_get_favorite_forum_list() -> ServiceResult<()> {
let response = get_favorite_forum_list(FavoriteForumListRequest::new()).await?;

println!("response: {:?}", response);

Ok(())
}

#[ignore = "manual: requires network or mutable external state"]
#[tokio::test]
async fn test_favorite_forum() -> ServiceResult<()> {
Expand Down
Loading
Loading