2020-02-13 17:25:37 +08:00
|
|
|
#![allow(dead_code)]
|
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
use http::StatusCode;
|
2020-01-27 15:52:36 +08:00
|
|
|
use serde_json::Value;
|
2020-01-22 21:30:09 +08:00
|
|
|
use std::time::Duration;
|
2020-01-16 00:11:27 +08:00
|
|
|
|
2020-01-28 01:27:42 +08:00
|
|
|
use async_std::io::prelude::*;
|
|
|
|
use async_std::task::{block_on, sleep};
|
2020-01-22 21:30:09 +08:00
|
|
|
use http_service::Body;
|
|
|
|
use http_service_mock::{make_server, TestBackend};
|
2020-01-16 00:11:27 +08:00
|
|
|
use meilisearch_http::data::Data;
|
|
|
|
use meilisearch_http::option::Opt;
|
|
|
|
use meilisearch_http::routes;
|
2020-01-22 21:30:09 +08:00
|
|
|
use serde_json::json;
|
|
|
|
use tempdir::TempDir;
|
|
|
|
use tide::server::Service;
|
2020-01-16 00:11:27 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
pub struct Server {
|
|
|
|
uid: String,
|
|
|
|
mock: TestBackend<Service<Data>>,
|
2020-01-16 00:11:27 +08:00
|
|
|
}
|
2020-01-22 21:30:09 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
impl Server {
|
|
|
|
pub fn with_uid(uid: &str) -> Server {
|
|
|
|
let tmp_dir = TempDir::new("meilisearch").unwrap();
|
|
|
|
|
|
|
|
let opt = Opt {
|
|
|
|
db_path: tmp_dir.path().to_str().unwrap().to_string(),
|
|
|
|
http_addr: "127.0.0.1:7700".to_owned(),
|
|
|
|
master_key: None,
|
|
|
|
env: "development".to_owned(),
|
|
|
|
no_analytics: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
let data = Data::new(opt.clone());
|
|
|
|
let mut app = tide::with_state(data);
|
|
|
|
routes::load_routes(&mut app);
|
|
|
|
let http_server = app.into_http_service();
|
|
|
|
let mock = make_server(http_server).unwrap();
|
|
|
|
|
|
|
|
Server {
|
|
|
|
uid: uid.to_string(),
|
|
|
|
mock,
|
|
|
|
}
|
|
|
|
}
|
2020-01-22 21:30:09 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
fn wait_update_id(&mut self, update_id: u64) {
|
|
|
|
loop {
|
|
|
|
let req = http::Request::get(format!("/indexes/{}/updates/{}", self.uid, update_id))
|
|
|
|
.body(Body::empty())
|
|
|
|
.unwrap();
|
2020-01-22 21:30:09 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
let res = self.mock.simulate(req).unwrap();
|
|
|
|
assert_eq!(res.status(), 200);
|
2020-01-22 21:30:09 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
let mut buf = Vec::new();
|
|
|
|
block_on(res.into_body().read_to_end(&mut buf)).unwrap();
|
|
|
|
let response: Value = serde_json::from_slice(&buf).unwrap();
|
2020-01-22 21:30:09 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
if response["status"] == "processed" {
|
2020-03-05 22:05:04 +08:00
|
|
|
eprintln!("{:#?}", response);
|
2020-03-04 21:18:07 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
block_on(sleep(Duration::from_secs(1)));
|
|
|
|
}
|
|
|
|
}
|
2020-02-13 17:25:37 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
// // Global Http request GET/POST/DELETE async or sync
|
2020-02-13 17:25:37 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
fn get_request(&mut self, url: &str) -> (Value, StatusCode) {
|
|
|
|
eprintln!("get_request: {}", url);
|
|
|
|
let req = http::Request::get(url)
|
|
|
|
.body(Body::empty())
|
|
|
|
.unwrap();
|
|
|
|
let res = self.mock.simulate(req).unwrap();
|
|
|
|
let status_code = res.status().clone();
|
2020-01-22 21:30:09 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
let mut buf = Vec::new();
|
|
|
|
block_on(res.into_body().read_to_end(&mut buf)).unwrap();
|
|
|
|
let response = serde_json::from_slice(&buf).unwrap_or_default();
|
|
|
|
(response, status_code)
|
|
|
|
}
|
2020-01-24 01:33:23 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
fn post_request(&mut self, url: &str, body: Value) -> (Value, StatusCode) {
|
|
|
|
eprintln!("post_request: {}", url);
|
|
|
|
let body_bytes = body.to_string().into_bytes();
|
2020-01-24 01:33:23 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
let req = http::Request::post(url)
|
|
|
|
.body(Body::from(body_bytes))
|
|
|
|
.unwrap();
|
|
|
|
let res = self.mock.simulate(req).unwrap();
|
|
|
|
let status_code = res.status().clone();
|
2020-01-24 01:33:23 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
let mut buf = Vec::new();
|
|
|
|
block_on(res.into_body().read_to_end(&mut buf)).unwrap();
|
|
|
|
let response = serde_json::from_slice(&buf).unwrap_or_default();
|
|
|
|
(response, status_code)
|
|
|
|
}
|
2020-01-24 01:33:23 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
fn post_request_async(&mut self, url: &str, body: Value) -> (Value, StatusCode) {
|
|
|
|
eprintln!("post_request_async: {}", url);
|
|
|
|
let (response, status_code) = self.post_request(url, body);
|
|
|
|
assert_eq!(status_code, 202);
|
|
|
|
assert!(response["updateId"].as_u64().is_some());
|
|
|
|
self.wait_update_id(response["updateId"].as_u64().unwrap());
|
|
|
|
(response, status_code)
|
|
|
|
}
|
2020-01-24 01:33:23 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
fn put_request(&mut self, url: &str, body: Value) -> (Value, StatusCode) {
|
|
|
|
eprintln!("put_request: {}", url);
|
|
|
|
let body_bytes = body.to_string().into_bytes();
|
2020-01-24 01:33:23 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
let req = http::Request::put(url)
|
|
|
|
.body(Body::from(body_bytes))
|
|
|
|
.unwrap();
|
|
|
|
let res = self.mock.simulate(req).unwrap();
|
|
|
|
let status_code = res.status().clone();
|
2020-02-13 17:25:37 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
let mut buf = Vec::new();
|
|
|
|
block_on(res.into_body().read_to_end(&mut buf)).unwrap();
|
|
|
|
let response = serde_json::from_slice(&buf).unwrap_or_default();
|
|
|
|
(response, status_code)
|
|
|
|
}
|
2020-02-13 17:25:37 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
fn put_request_async(&mut self, url: &str, body: Value) -> (Value, StatusCode) {
|
|
|
|
eprintln!("put_request_async: {}", url);
|
|
|
|
let (response, status_code) = self.put_request(url, body);
|
|
|
|
assert!(response["updateId"].as_u64().is_some());
|
|
|
|
assert_eq!(status_code, 202);
|
|
|
|
self.wait_update_id(response["updateId"].as_u64().unwrap());
|
|
|
|
(response, status_code)
|
|
|
|
}
|
2020-02-13 17:25:37 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
fn delete_request(&mut self, url: &str) -> (Value, StatusCode) {
|
|
|
|
eprintln!("delete_request: {}", url);
|
|
|
|
let req = http::Request::delete(url)
|
2020-02-13 17:25:37 +08:00
|
|
|
.body(Body::empty())
|
|
|
|
.unwrap();
|
2020-03-04 21:18:07 +08:00
|
|
|
let res = self.mock.simulate(req).unwrap();
|
|
|
|
let status_code = res.status().clone();
|
2020-02-13 17:25:37 +08:00
|
|
|
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
block_on(res.into_body().read_to_end(&mut buf)).unwrap();
|
2020-03-04 21:18:07 +08:00
|
|
|
let response = serde_json::from_slice(&buf).unwrap_or_default();
|
|
|
|
(response, status_code)
|
|
|
|
}
|
2020-02-13 17:25:37 +08:00
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
fn delete_request_async(&mut self, url: &str) -> (Value, StatusCode) {
|
|
|
|
eprintln!("delete_request_async: {}", url);
|
|
|
|
let (response, status_code) = self.delete_request(url);
|
|
|
|
assert!(response["updateId"].as_u64().is_some());
|
|
|
|
assert_eq!(status_code, 202);
|
|
|
|
self.wait_update_id(response["updateId"].as_u64().unwrap());
|
|
|
|
(response, status_code)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// // All Routes
|
|
|
|
|
|
|
|
pub fn list_indexes(&mut self) -> (Value, StatusCode) {
|
|
|
|
self.get_request("/indexes")
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn create_index(&mut self, body: Value) -> (Value, StatusCode) {
|
|
|
|
self.post_request("/indexes", body)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn search_multi_index(&mut self, query: &str) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/search?{}", query);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_index(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_index(&mut self, body: Value) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}", self.uid);
|
|
|
|
self.put_request(&url, body)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn delete_index(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}", self.uid);
|
|
|
|
self.delete_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn search(&mut self, query: &str) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/search?{}", self.uid, query);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_all_updates_status(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/updates", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_update_status(&mut self, update_id: u64) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/updates/{}", self.uid, update_id);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_all_documents(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/documents", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_or_replace_multiple_documents(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/documents", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_or_update_multiple_documents(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/documents", self.uid);
|
|
|
|
self.put_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn clear_all_documents(&mut self) {
|
|
|
|
let url = format!("/indexes/{}/documents", self.uid);
|
|
|
|
self.delete_request_async(&url);
|
|
|
|
}
|
|
|
|
|
2020-03-04 22:06:16 +08:00
|
|
|
pub fn get_document(&mut self, document_id: impl ToString) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/documents/{}", self.uid, document_id.to_string());
|
2020-03-04 21:18:07 +08:00
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
2020-03-04 22:06:16 +08:00
|
|
|
pub fn delete_document(&mut self, document_id: impl ToString) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/documents/{}", self.uid, document_id.to_string());
|
|
|
|
self.delete_request_async(&url)
|
2020-03-04 21:18:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn delete_multiple_documents(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/documents/delete-batch", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_all_settings(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings", self.uid);
|
|
|
|
self.get_request(&url)
|
2020-02-13 17:25:37 +08:00
|
|
|
}
|
2020-03-04 21:18:07 +08:00
|
|
|
|
|
|
|
pub fn update_all_settings(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/settings", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn delete_all_settings(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings", self.uid);
|
|
|
|
self.delete_request_async(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_ranking_rules(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/ranking-rules", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_ranking_rules(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/settings/ranking-rules", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
2020-03-05 22:18:19 +08:00
|
|
|
pub fn update_ranking_rules_sync(&mut self, body: Value) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/ranking-rules", self.uid);
|
|
|
|
self.post_request(&url, body)
|
|
|
|
}
|
|
|
|
|
2020-03-04 21:18:07 +08:00
|
|
|
pub fn delete_ranking_rules(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/ranking-rules", self.uid);
|
|
|
|
self.delete_request_async(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_distinct_attribute(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/distinct-attribute", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_distinct_attribute(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/settings/distinct-attribute", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn delete_distinct_attribute(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/distinct-attribute", self.uid);
|
|
|
|
self.delete_request_async(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_identifier(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/identifier", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_searchable_attributes(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/searchable-attributes", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_searchable_attributes(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/settings/searchable-attributes", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn delete_searchable_attributes(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/searchable-attributes", self.uid);
|
|
|
|
self.delete_request_async(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_displayed_attributes(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/displayed-attributes", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_displayed_attributes(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/settings/displayed-attributes", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn delete_displayed_attributes(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/displayed-attributes", self.uid);
|
|
|
|
self.delete_request_async(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_accept_new_fields(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/accept-new-fields", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_accept_new_fields(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/settings/accept-new-fields", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_synonyms(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/synonyms", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_synonyms(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/settings/synonyms", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn delete_synonyms(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/synonyms", self.uid);
|
|
|
|
self.delete_request_async(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_stop_words(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/stop-words", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_stop_words(&mut self, body: Value) {
|
|
|
|
let url = format!("/indexes/{}/settings/stop-words", self.uid);
|
|
|
|
self.post_request_async(&url, body);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn delete_stop_words(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/settings/stop-words", self.uid);
|
|
|
|
self.delete_request_async(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_index_stats(&mut self) -> (Value, StatusCode) {
|
|
|
|
let url = format!("/indexes/{}/stats", self.uid);
|
|
|
|
self.get_request(&url)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn list_keys(&mut self) -> (Value, StatusCode) {
|
|
|
|
self.get_request("/keys")
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_health(&mut self) -> (Value, StatusCode) {
|
|
|
|
self.get_request("/health")
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_health(&mut self, body: Value) -> (Value, StatusCode) {
|
|
|
|
self.put_request("/health", body)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_version(&mut self) -> (Value, StatusCode) {
|
|
|
|
self.get_request("/version")
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_sys_info(&mut self) -> (Value, StatusCode) {
|
|
|
|
self.get_request("/sys-info")
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_sys_info_pretty(&mut self) -> (Value, StatusCode) {
|
|
|
|
self.get_request("/sys-info/pretty")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate routes
|
|
|
|
|
|
|
|
pub fn populate_movies(&mut self) {
|
|
|
|
let body = json!({
|
|
|
|
"uid": "movies",
|
|
|
|
"identifier": "id",
|
|
|
|
});
|
|
|
|
self.create_index(body);
|
|
|
|
|
|
|
|
let body = json!({
|
|
|
|
"rankingRules": [
|
|
|
|
"typo",
|
|
|
|
"words",
|
|
|
|
"proximity",
|
|
|
|
"attribute",
|
|
|
|
"wordsPosition",
|
|
|
|
"desc(popularity)",
|
|
|
|
"exactness",
|
|
|
|
"desc(vote_average)",
|
|
|
|
],
|
|
|
|
"searchableAttributes": [
|
|
|
|
"title",
|
|
|
|
"tagline",
|
|
|
|
"overview",
|
|
|
|
"cast",
|
|
|
|
"director",
|
|
|
|
"producer",
|
|
|
|
"production_companies",
|
|
|
|
"genres",
|
|
|
|
],
|
|
|
|
"displayedAttributes": [
|
|
|
|
"title",
|
|
|
|
"director",
|
|
|
|
"producer",
|
|
|
|
"tagline",
|
|
|
|
"genres",
|
|
|
|
"id",
|
|
|
|
"overview",
|
|
|
|
"vote_count",
|
|
|
|
"vote_average",
|
|
|
|
"poster_path",
|
|
|
|
"popularity",
|
|
|
|
],
|
|
|
|
"acceptNewFields": false,
|
|
|
|
});
|
|
|
|
|
|
|
|
self.update_all_settings(body);
|
|
|
|
|
|
|
|
let dataset = include_bytes!("assets/movies.json");
|
|
|
|
|
|
|
|
let body: Value = serde_json::from_slice(dataset).unwrap();
|
|
|
|
|
|
|
|
self.add_or_replace_multiple_documents(body);
|
|
|
|
}
|
|
|
|
|
2020-01-24 01:33:23 +08:00
|
|
|
}
|