From a8e5d51a3aef61aa47010af9440fda599d1b6585 Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Mon, 9 Jan 2023 15:19:02 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=87=BA=E7=9A=84=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/base_api.dart | 34 +++++++- lib/api/mw/list.dart | 46 ++++------- lib/api/mw/mw_api.dart | 130 +++++++++++++++++++++++++++---- lib/api/mw/token.dart | 1 - lib/api/mw/user.dart | 17 ++-- lib/api/response/csrf_token.dart | 19 +---- lib/api/response/page_info.dart | 70 +++++++++++------ lib/models/user.dart | 15 ++++ 8 files changed, 235 insertions(+), 97 deletions(-) delete mode 100644 lib/api/mw/token.dart diff --git a/lib/api/base_api.dart b/lib/api/base_api.dart index 87f7352..1c70d22 100644 --- a/lib/api/base_api.dart +++ b/lib/api/base_api.dart @@ -61,8 +61,7 @@ class BaseApi { // 自定义Header dio.interceptors.add( InterceptorsWrapper(onRequest: (options, handler) { - options.headers["X-IsekaiWikiApp-Version"] = - Global.packageInfo?.version ?? "unknow"; + options.headers["X-IsekaiWikiApp-Version"] = Global.packageInfo?.version ?? "unknow"; options.headers["User-Agent"] = ""; return handler.next(options); }), @@ -91,8 +90,7 @@ class BaseApi { ); if (res.statusCode != null && res.statusCode != 200) { - throw HttpResponseException(res.statusCode!, - statusText: res.statusMessage!); + throw HttpResponseException(res.statusCode!, statusText: res.statusMessage!); } return res.data ?? ""; @@ -108,4 +106,32 @@ class BaseApi { return {}; } } + + static Future post(Uri uri, {Map? search, dynamic data}) async { + var client = await getClient(); + + var res = await client.post( + uri.toString(), + queryParameters: search, + data: data, + options: Options(responseType: ResponseType.plain), + ); + + if (res.statusCode != null && res.statusCode != 200) { + throw HttpResponseException(res.statusCode!, statusText: res.statusMessage!); + } + + return res.data ?? ""; + } + + static Future postJson(Uri uri, {Map? search, dynamic data}) async { + var resText = await post(uri, search: search, data: data); + var resData = jsonDecode(resText); + + if (resData is Map) { + return resData; + } else { + return {}; + } + } } diff --git a/lib/api/mw/list.dart b/lib/api/mw/list.dart index 87bd5a1..52e1bda 100755 --- a/lib/api/mw/list.dart +++ b/lib/api/mw/list.dart @@ -4,10 +4,8 @@ import 'package:isekai_wiki/api/response/recent_changes.dart'; class MWApiList { /// 获取最近更改列表 - static Future>> getRecentChanges( - String type, - {int? limit, - Map? continueInfo}) async { + static Future>> getRecentChanges(String type, + {int? limit, Map? continueInfo}) async { var query = { "list": "recentchanges", "rctype": type, @@ -18,7 +16,7 @@ class MWApiList { query["rccontinue"] = continueInfo["rccontinue"]; } - var mwRes = await MWApi.get("query", query: query); + var mwRes = await MWApi.get("query", params: query); var rcRes = RecentChangesResponse.fromJson(mwRes.data); return mwRes.replaceData(rcRes.recentchanges); @@ -50,12 +48,10 @@ class MWApiList { var rcResList = await Future.wait([ ignoreRcNew ? Future.value(MWResponse>([])) - : getRecentChanges("new", - limit: limit, continueInfo: continueInfoNew), + : getRecentChanges("new", limit: limit, continueInfo: continueInfoNew), ignoreRcEdit ? Future.value(MWResponse>([])) - : getRecentChanges("edit", - limit: limit, continueInfo: continueInfoEdit), + : getRecentChanges("edit", limit: limit, continueInfo: continueInfoEdit), ]); var rcNewRes = rcResList[0]; @@ -65,35 +61,25 @@ class MWApiList { mergedList.sort((a, b) { // 为新页面添加7天的保护期 - var timeA = a.type == "new" - ? a.timestamp.add(const Duration(days: 7)) - : a.timestamp; - var timeB = b.type == "new" - ? b.timestamp.add(const Duration(days: 7)) - : b.timestamp; + var timeA = a.type == "new" ? a.timestamp.add(const Duration(days: 7)) : a.timestamp; + var timeB = b.type == "new" ? b.timestamp.add(const Duration(days: 7)) : b.timestamp; return timeB.compareTo(timeA); }); List uniqueMergedList = []; for (var page in mergedList) { - if (uniqueMergedList - .indexWhere((element) => element.pageid == page.pageid) == - -1) { + if (uniqueMergedList.indexWhere((element) => element.pageid == page.pageid) == -1) { uniqueMergedList.add(page); } } Map mergedContinueInfo = {}; - if (rcNewRes.continueInfo != null && - rcNewRes.continueInfo!.containsKey("rccontinue")) { - mergedContinueInfo["rcnewcontinue"] = - rcNewRes.continueInfo!["rccontinue"]!; + if (rcNewRes.continueInfo != null && rcNewRes.continueInfo!.containsKey("rccontinue")) { + mergedContinueInfo["rcnewcontinue"] = rcNewRes.continueInfo!["rccontinue"]!; } - if (rcEditRes.continueInfo != null && - rcEditRes.continueInfo!.containsKey("rccontinue")) { - mergedContinueInfo["rceditcontinue"] = - rcEditRes.continueInfo!["rccontinue"]!; + if (rcEditRes.continueInfo != null && rcEditRes.continueInfo!.containsKey("rccontinue")) { + mergedContinueInfo["rceditcontinue"] = rcEditRes.continueInfo!["rccontinue"]!; } return MWResponse( @@ -139,14 +125,13 @@ class MWApiList { query.addAll(extraParams); } - var mwRes = await MWApi.get("query", query: query); + var mwRes = await MWApi.get("query", params: query); var pagesRes = PagesResponse.fromJson(mwRes.data); var pageList = pagesRes.pages.map((pageInfo) { if (pageInfo.description != null) { - pageInfo.description = - pageInfo.description!.replaceAll(RegExp(r"\n\n"), "\n"); + pageInfo.description = pageInfo.description!.replaceAll(RegExp(r"\n\n"), "\n"); } if (pageInfo.title.contains("/")) { var splitPos = pageInfo.title.lastIndexOf("/"); @@ -171,8 +156,7 @@ class MWApiList { if (titles != null) { for (var title in titles) { - var index = - pagesRes.pages.indexWhere((element) => element.title == title); + var index = pagesRes.pages.indexWhere((element) => element.title == title); if (index != -1) { sortedPages.add(pagesRes.pages[index]); } diff --git a/lib/api/mw/mw_api.dart b/lib/api/mw/mw_api.dart index befefa4..588287c 100755 --- a/lib/api/mw/mw_api.dart +++ b/lib/api/mw/mw_api.dart @@ -1,6 +1,7 @@ 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'; @@ -40,17 +41,31 @@ class MWApiEmptyBodyException implements Exception { } } +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 { - List? warnList; + Map? warnings; T data; Map? continueInfo; - MWResponse(this.data, {this.warnList, this.continueInfo}); + MWResponse(this.data, {this.warnings, this.continueInfo}); MWResponse replaceData(N data) { return MWResponse( data, - warnList: warnList, + warnings: warnings, continueInfo: continueInfo, ); } @@ -60,22 +75,66 @@ class MWApi { static Uri apiBaseUri = Uri.parse(Global.wikiApiUrl); static Future>> get(String action, - {Map? query}) async { - Map queryStr = - query?.map((key, value) => MapEntry(key, value.toString())) ?? {}; - queryStr.addAll({ + {Map? params}) 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 { + 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); + } + + static Future>> post(String action, + {Map? params, String? withToken}) 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) { - queryStr["origin"] = Global.webOrigin!; + paramsStr["origin"] = Global.webOrigin!; } var resText = ""; try { - resText = await BaseApi.get(apiBaseUri, search: queryStr); + if (withToken != null) { + // 获取CSRF Token + paramsStr["token"] = await getToken(type: withToken); + } + + resText = await BaseApi.post(apiBaseUri, data: paramsStr); } on DioError catch (err) { if (err.type == DioErrorType.response) { if (err.response != null) { @@ -98,8 +157,7 @@ class MWApi { return parseMWResponse(action, resText); } - static MWResponse> parseMWResponse( - String action, String resJson) { + static MWResponse> parseMWResponse(String action, String resJson) { var resData = jsonDecode(resJson); if (resData is! Map) { throw MWApiEmptyBodyException(resData); @@ -111,13 +169,26 @@ class MWApi { } // 请求结果 - if (!resData.containsKey(action) || - resData[action] is! Map) { + if (!resData.containsKey(action) || resData[action] is! Map) { throw MWApiEmptyBodyException(resData); } - MWResponse> mwRes = - MWResponse(resData[action] as Map); + MWResponse> 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"]; @@ -134,4 +205,33 @@ class MWApi { 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!; + } } diff --git a/lib/api/mw/token.dart b/lib/api/mw/token.dart deleted file mode 100644 index 0f7802d..0000000 --- a/lib/api/mw/token.dart +++ /dev/null @@ -1 +0,0 @@ -class MWApiToken {} diff --git a/lib/api/mw/user.dart b/lib/api/mw/user.dart index f602893..3deb3eb 100644 --- a/lib/api/mw/user.dart +++ b/lib/api/mw/user.dart @@ -9,21 +9,20 @@ class MWApiUser { "method": "startauth", }; - var mwRes = await MWApi.get("mugenapp", query: query); + var mwRes = await MWApi.get("mugenapp", params: query); var data = MugenAppStartAuthResponse.fromJson(mwRes.data); return mwRes.replaceData(data.startauth); } - static Future> attemptAuth( - String loginRequestKey) async { + static Future> attemptAuth(String loginRequestKey) async { var query = { "method": "attemptauth", "requestkey": loginRequestKey, }; - var mwRes = await MWApi.get("mugenapp", query: query); + var mwRes = await MWApi.get("mugenapp", params: query); var data = MugenAppAttemptAuthResponse.fromJson(mwRes.data); @@ -36,10 +35,18 @@ class MWApiUser { "uiprop": "blockinfo|groups|rights|options|email|realname|latestcontrib", }; - var mwRes = await MWApi.get("query", query: query); + var mwRes = await MWApi.get("query", params: query); var data = MetaUserInfoResponse.fromJson(mwRes.data); return mwRes.replaceData(data); } + + static Future logout() async { + try { + await MWApi.post("logout", withToken: "csrf"); + } on MWApiEmptyBodyException catch (_) { + // 因为必定是空返回,忽略这个错误 + } + } } diff --git a/lib/api/response/csrf_token.dart b/lib/api/response/csrf_token.dart index 6b4052e..f241c1e 100644 --- a/lib/api/response/csrf_token.dart +++ b/lib/api/response/csrf_token.dart @@ -3,26 +3,9 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'csrf_token.freezed.dart'; part 'csrf_token.g.dart'; -@freezed -class CSRFTokenInfo with _$CSRFTokenInfo { - const factory CSRFTokenInfo({ - String? csrftoken, - String? logintoken, - String? createaccounttoken, - String? patroltoken, - String? rollbacktoken, - String? userrightstoken, - String? watchtoken, - }) = _CSRFTokenInfo; - - factory CSRFTokenInfo.fromJson(Map json) => - _$CSRFTokenInfoFromJson(json); -} - @freezed class CSRFTokenResponse with _$CSRFTokenResponse { - const factory CSRFTokenResponse({required CSRFTokenInfo tokens}) = - _CSRFTokenInfoResponse; + const factory CSRFTokenResponse({required Map tokens}) = _CSRFTokenInfoResponse; factory CSRFTokenResponse.fromJson(Map json) => _$CSRFTokenResponseFromJson(json); diff --git a/lib/api/response/page_info.dart b/lib/api/response/page_info.dart index 44722ad..5e3e0a1 100644 --- a/lib/api/response/page_info.dart +++ b/lib/api/response/page_info.dart @@ -5,29 +5,51 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'page_info.freezed.dart'; part 'page_info.g.dart'; -@freezed -class PageInfo with _$PageInfo { - const PageInfo._(); - - factory PageInfo({ - required int pageid, - required int ns, - required String title, - String? subtitle, - String? displayTitle, - @JsonKey(name: "extract") String? description, - String? contentmodel, - String? pagelanguage, - String? pagelanguagehtmlcode, - String? pagelanguagedir, - bool? inwatchlist, - @JsonKey(name: "touched") DateTime? updatedTime, - int? lastrevid, - int? length, - String? fullurl, - String? editurl, - String? canonicalurl, - }) = _PageInfo; +@JsonSerializable() +class PageInfo { + int pageid; + int ns; + String title; + String? subtitle; + String? displayTitle; + + @JsonKey(name: "extract") + String? description; + + String? contentmodel; + String? pagelanguage; + String? pagelanguagehtmlcode; + String? pagelanguagedir; + bool? inwatchlist; + + @JsonKey(name: "touched") + DateTime? updatedTime; + + int? lastrevid; + int? length; + String? fullurl; + String? editurl; + String? canonicalurl; + + PageInfo({ + required this.pageid, + required this.ns, + required this.title, + this.subtitle, + this.displayTitle, + this.description, + this.contentmodel, + this.pagelanguage, + this.pagelanguagehtmlcode, + this.pagelanguagedir, + this.inwatchlist, + this.updatedTime, + this.lastrevid, + this.length, + this.fullurl, + this.editurl, + this.canonicalurl, + }); String get mainTitle { return displayTitle ?? title; @@ -38,6 +60,8 @@ class PageInfo with _$PageInfo { } factory PageInfo.fromJson(Map json) => _$PageInfoFromJson(json); + + Map toJson() => _$PageInfoToJson(this); } @freezed diff --git a/lib/models/user.dart b/lib/models/user.dart index bbe2c99..e6b5d2d 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -235,6 +235,19 @@ class UserController extends GetxController { } Future logout({bool logoutRemote = true}) async { + authProcessing.value = true; + + try { + await MWApiUser.logout(); + } catch (err, stack) { + alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误"); + if (kDebugMode) { + print("Exception in logout: $err"); + stack.printError(); + } + return; + } + // 清除Cookie await BaseApi.clearCookie(); @@ -244,6 +257,8 @@ class UserController extends GetxController { nickName.value = ""; avatarUrlSet.clear(); + authProcessing.value = true; + saveToStorage(); } }