|
|
import 'dart:convert';
|
|
|
import 'package:dio/dio.dart';
|
|
|
import 'package:isekai_wiki/api/base_api.dart';
|
|
|
import 'package:isekai_wiki/api/response/csrf_token.dart';
|
|
|
|
|
|
import 'package:isekai_wiki/global.dart';
|
|
|
import 'package:json_annotation/json_annotation.dart';
|
|
|
|
|
|
part 'mw_api.g.dart';
|
|
|
|
|
|
@JsonSerializable()
|
|
|
class MWApiErrorException implements Exception {
|
|
|
String code;
|
|
|
|
|
|
String? info;
|
|
|
|
|
|
@JsonKey(name: "docref")
|
|
|
String? detail;
|
|
|
|
|
|
MWApiErrorException({required this.code, this.info, this.detail});
|
|
|
|
|
|
factory MWApiErrorException.fromJson(Map<String, dynamic> json) =>
|
|
|
_$MWApiErrorExceptionFromJson(json);
|
|
|
|
|
|
Map<String, dynamic> toJson() => _$MWApiErrorExceptionToJson(this);
|
|
|
|
|
|
@override
|
|
|
String toString() {
|
|
|
return "MediaWiki Api Error: ${info ?? code}";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
class MWApiEmptyBodyException implements Exception {
|
|
|
dynamic resData;
|
|
|
|
|
|
MWApiEmptyBodyException(this.resData);
|
|
|
|
|
|
@override
|
|
|
String toString() {
|
|
|
return "MediaWiki Api Response body is empty";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
class CannotFetchCSRFTokenException implements Exception {
|
|
|
String? message;
|
|
|
CannotFetchCSRFTokenException([this.message]);
|
|
|
|
|
|
@override
|
|
|
String toString() {
|
|
|
if (message != null) {
|
|
|
return message!;
|
|
|
} else {
|
|
|
return "Cannot fetch CSRF token from MediaWiki API";
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
class MWResponse<T> {
|
|
|
Map<String, String>? warnings;
|
|
|
T data;
|
|
|
Map<String, String>? continueInfo;
|
|
|
|
|
|
MWResponse(this.data, {this.warnings, this.continueInfo});
|
|
|
|
|
|
MWResponse<N> replaceData<N>(N data) {
|
|
|
return MWResponse<N>(
|
|
|
data,
|
|
|
warnings: warnings,
|
|
|
continueInfo: continueInfo,
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
class MWApi {
|
|
|
static Future<MWResponse<Map<String, dynamic>>> get(String action,
|
|
|
{Map<String, dynamic>? params, bool returnRoot = false}) async {
|
|
|
Map<String, String> paramsStr =
|
|
|
params?.map((key, value) => MapEntry(key, value.toString())) ?? {};
|
|
|
paramsStr.addAll({
|
|
|
"action": action,
|
|
|
"format": "json",
|
|
|
"formatversion": "2",
|
|
|
"uselang": Global.wikiLang,
|
|
|
});
|
|
|
if (Global.webOrigin != null) {
|
|
|
paramsStr["origin"] = Global.webOrigin!;
|
|
|
}
|
|
|
|
|
|
var resText = "";
|
|
|
try {
|
|
|
var apiBaseUri = Uri.parse(Global.siteConfig.apiUrl);
|
|
|
resText = await BaseApi.get(apiBaseUri, search: paramsStr);
|
|
|
} on DioError catch (err) {
|
|
|
if (err.type == DioErrorType.response) {
|
|
|
if (err.response != null) {
|
|
|
var response = err.response!;
|
|
|
var contentType = response.headers[Headers.contentTypeHeader];
|
|
|
if (contentType != null &&
|
|
|
contentType.contains("application/json") &&
|
|
|
response.data is String) {
|
|
|
resText = response.data as String;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (resText.isEmpty) {
|
|
|
// 没有捕获到服务器返回的错误,则抛给上一层
|
|
|
rethrow;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return parseMWResponse(action, resText, returnRoot);
|
|
|
}
|
|
|
|
|
|
static Future<MWResponse<Map<String, dynamic>>> post<T>(String action,
|
|
|
{Map<String, dynamic>? params, String? withToken, bool returnRoot = false}) async {
|
|
|
params ??= {};
|
|
|
params.addAll({
|
|
|
"action": action,
|
|
|
"format": "json",
|
|
|
"formatversion": 2,
|
|
|
"uselang": Global.wikiLang,
|
|
|
});
|
|
|
if (Global.webOrigin != null) {
|
|
|
params["origin"] = Global.webOrigin!;
|
|
|
}
|
|
|
|
|
|
var resText = "";
|
|
|
try {
|
|
|
if (withToken != null) {
|
|
|
// 获取CSRF Token
|
|
|
params["token"] = await getToken(type: withToken);
|
|
|
}
|
|
|
|
|
|
var apiBaseUri = Uri.parse(Global.siteConfig.apiUrl);
|
|
|
|
|
|
resText = await BaseApi.post(apiBaseUri, data: params);
|
|
|
} on DioError catch (err) {
|
|
|
if (err.type == DioErrorType.response) {
|
|
|
if (err.response != null) {
|
|
|
var response = err.response!;
|
|
|
var contentType = response.headers[Headers.contentTypeHeader];
|
|
|
if (contentType != null &&
|
|
|
contentType.contains("application/json") &&
|
|
|
response.data is String) {
|
|
|
resText = response.data as String;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (resText.isEmpty) {
|
|
|
// 没有捕获到服务器返回的错误,则抛给上一层
|
|
|
rethrow;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return parseMWResponse(action, resText, returnRoot);
|
|
|
}
|
|
|
|
|
|
static MWResponse<Map<String, dynamic>> parseMWResponse(
|
|
|
String action, String resJson, bool returnRoot) {
|
|
|
var resData = jsonDecode(resJson);
|
|
|
if (resData is! Map<String, dynamic>) {
|
|
|
throw MWApiEmptyBodyException(resData);
|
|
|
}
|
|
|
|
|
|
// 处理请求错误
|
|
|
if (resData.containsKey("error")) {
|
|
|
throw MWApiErrorException.fromJson(resData["error"]!);
|
|
|
}
|
|
|
|
|
|
// 请求结果
|
|
|
MWResponse<Map<String, dynamic>> mwRes;
|
|
|
|
|
|
if (returnRoot) {
|
|
|
var filteredResData = <String, dynamic>{}..addAll(resData);
|
|
|
filteredResData
|
|
|
..remove("error")
|
|
|
..remove("warnings")
|
|
|
..remove("batchcomplete");
|
|
|
mwRes = MWResponse(filteredResData);
|
|
|
} else {
|
|
|
if (!resData.containsKey(action) || resData[action] is! Map<String, dynamic>) {
|
|
|
throw MWApiEmptyBodyException(resData);
|
|
|
}
|
|
|
mwRes = MWResponse(resData[action] as Map<String, dynamic>);
|
|
|
}
|
|
|
|
|
|
// warnings
|
|
|
if (resData.containsKey("warnings") && resData["warnings"] is Map<String, dynamic>) {
|
|
|
var warnings = resData["warnings"] as Map<String, dynamic>;
|
|
|
Map<String, String> warningMap = {};
|
|
|
warnings.forEach((key, value) {
|
|
|
if (value is Map<String, String> && value.containsKey("*")) {
|
|
|
var valueStr = value["*"] as String;
|
|
|
warningMap[key] = valueStr;
|
|
|
}
|
|
|
});
|
|
|
if (warningMap.isNotEmpty) {
|
|
|
mwRes.warnings = warningMap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// continue参数
|
|
|
var batchcomplete = resData["batchcomplete"];
|
|
|
if (batchcomplete is bool && batchcomplete) {
|
|
|
var continueInfo = resData["continue"];
|
|
|
if (continueInfo is Map<String, dynamic>) {
|
|
|
mwRes.continueInfo = {};
|
|
|
continueInfo.forEach((key, value) {
|
|
|
var keyStr = key;
|
|
|
mwRes.continueInfo![keyStr] = value.toString();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return mwRes;
|
|
|
}
|
|
|
|
|
|
/// 获取Token,Token可能为空
|
|
|
static Future<MWResponse<String?>> getTokenRaw({String type = "csrf"}) async {
|
|
|
var query = {
|
|
|
"meta": "tokens",
|
|
|
"type": type,
|
|
|
};
|
|
|
|
|
|
var mwRes = await MWApi.get("query", params: query);
|
|
|
|
|
|
var data = CSRFTokenResponse.fromJson(mwRes.data);
|
|
|
var tokenKey = "${type}token";
|
|
|
var token = data.tokens[tokenKey];
|
|
|
|
|
|
return mwRes.replaceData(token);
|
|
|
}
|
|
|
|
|
|
/// 获取Token,并在无法获取时抛出异常
|
|
|
static Future<String> getToken({String type = "csrf"}) async {
|
|
|
var tokenRes = await getTokenRaw(type: type);
|
|
|
if (tokenRes.data == null) {
|
|
|
if (tokenRes.warnings != null && tokenRes.warnings!.containsKey("tokens")) {
|
|
|
throw CannotFetchCSRFTokenException(tokenRes.warnings!["tokens"]);
|
|
|
} else {
|
|
|
throw CannotFetchCSRFTokenException();
|
|
|
}
|
|
|
}
|
|
|
return tokenRes.data!;
|
|
|
}
|
|
|
}
|