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 json) => _$MWApiErrorExceptionFromJson(json); Map 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 { Map? warnings; T data; Map? continueInfo; MWResponse(this.data, {this.warnings, this.continueInfo}); MWResponse replaceData(N data) { return MWResponse( data, warnings: warnings, continueInfo: continueInfo, ); } } class MWApi { static Future>> get(String action, {Map? params, bool returnRoot = false}) async { Map 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>> post(String action, {Map? 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> parseMWResponse( String action, String resJson, bool returnRoot) { var resData = jsonDecode(resJson); if (resData is! Map) { throw MWApiEmptyBodyException(resData); } // 处理请求错误 if (resData.containsKey("error")) { throw MWApiErrorException.fromJson(resData["error"]!); } // 请求结果 MWResponse> mwRes; if (returnRoot) { var filteredResData = {}..addAll(resData); filteredResData ..remove("error") ..remove("warnings") ..remove("batchcomplete"); mwRes = MWResponse(filteredResData); } else { if (!resData.containsKey(action) || resData[action] is! Map) { throw MWApiEmptyBodyException(resData); } mwRes = MWResponse(resData[action] as Map); } // warnings if (resData.containsKey("warnings") && resData["warnings"] is Map) { var warnings = resData["warnings"] as Map; Map warningMap = {}; warnings.forEach((key, value) { if (value is Map && 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) { mwRes.continueInfo = {}; continueInfo.forEach((key, value) { var keyStr = key; mwRes.continueInfo![keyStr] = value.toString(); }); } } return mwRes; } /// 获取Token,Token可能为空 static Future> 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 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!; } }