feat: account manager (#468)

* feat: account manager

* remove dep

* some fixes

* migrate accounts

* reimplement clearCookie
This commit is contained in:
My-Responsitories
2025-03-19 13:19:32 +08:00
committed by GitHub
parent 94fa0652ac
commit b15fdfa2ff
47 changed files with 1233 additions and 800 deletions

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2018 Wen Du (wendux)
Copyright (c) 2022 The CFUG Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,90 @@
# dio_cookie_manager
[![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg)](https://pub.dev/packages/dio_cookie_manager)
A cookie manager combines cookie_jar and dio, based on the interceptor algorithm.
## Getting Started
### Install
Add the `dio_cookie_manager` package to your
[pubspec dependencies](https://pub.dev/packages/dio_cookie_manager/install).
### Usage
```dart
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
void main() async {
final dio = Dio();
final cookieJar = CookieJar();
dio.interceptors.add(CookieManager(cookieJar));
// First request, and save cookies (CookieManager do it).
await dio.get("https://dart.dev");
// Print cookies
print(await cookieJar.loadForRequest(Uri.parse("https://dart.dev")));
// Second request with the cookies
await dio.get('https://dart.dev');
}
```
## Cookie Manager
`CookieManager` Interceptor can help us manage the request/response cookies automatically.
`CookieManager` depends on the `cookie_jar` package:
> The dio_cookie_manager manage API is based on the withdrawn
> [cookie_jar](https://github.com/flutterchina/cookie_jar).
You can create a `CookieJar` or `PersistCookieJar` to manage cookies automatically,
and dio use the `CookieJar` by default, which saves the cookies **in RAM**.
If you want to persists cookies, you can use the `PersistCookieJar` class, for example:
```dart
dio.interceptors.add(CookieManager(PersistCookieJar()))
```
`PersistCookieJar` persists the cookies in files,
so if the application exit, the cookies always exist unless call `delete` explicitly.
> Note: In flutter, the path passed to `PersistCookieJar` must be valid (exists in phones and with write access).
> Use [path_provider](https://pub.dev/packages/path_provider) package to get the right path.
In flutter:
```dart
Future<void> prepareJar() async {
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String appDocPath = appDocDir.path;
final jar = PersistCookieJar(
ignoreExpires: true,
storage: FileStorage(appDocPath + "/.cookies/"),
);
dio.interceptors.add(CookieManager(jar));
}
```
## Handling Cookies with redirect requests
Redirect requests require extra configuration to parse cookies correctly.
In shortly:
- Set `followRedirects` to `false`.
- Allow `statusCode` from `300` to `399` responses predicated as succeed.
- Make further requests using the `HttpHeaders.locationHeader`.
For example:
```dart
final cookieJar = CookieJar();
final dio = Dio()
..interceptors.add(CookieManager(cookieJar))
..options.followRedirects = false
..options.validateStatus =
(status) => status != null && status >= 200 && status < 400;
final redirected = await dio.get('/redirection');
final response = await dio.get(
redirected.headers.value(HttpHeaders.locationHeader)!,
);
```

View File

@@ -0,0 +1,258 @@
// edit from package:dio_cookie_manager
import 'dart:async';
import 'dart:io';
import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../account.dart';
final _setCookieReg = RegExp('(?<=)(,)(?=[^;]+?=)');
class AccountManager extends Interceptor {
static final Map<AccountType, Set<String>> apiTypeSet = {
AccountType.heartbeat: {
Api.videoUrl,
Api.videoIntro,
Api.relatedList,
Api.replyList,
Api.replyReplyList,
Api.searchSuggest,
Api.searchByType,
Api.heartBeat,
Api.ab2c,
Api.bangumiInfo,
Api.liveRoomInfo,
Api.onlineTotal,
Api.dynamicDetail,
Api.aiConclusion,
Api.getSeasonDetailApi,
Api.liveRoomDmToken,
Api.liveRoomDmPrefetch,
},
AccountType.recommend: {
Api.recommendListWeb,
Api.recommendListApp,
Api.feedDislike,
Api.feedDislikeCancel,
Api.hotList,
Api.hotSearchList, // 不同账号搜索结果可能不一样
Api.searchDefault,
Api.searchSuggest,
Api.searchByType
},
AccountType.video: {Api.videoUrl, Api.bangumiVideoUrl}
};
static final loginApi = {
Api.getTVCode,
Api.qrcodePoll,
Api.getCaptcha,
Api.getWebKey,
Api.appSmsCode,
Api.loginByPwdApi,
Api.logInByAppSms,
Api.safeCenterGetInfo,
Api.preCapture,
Api.safeCenterSmsCode,
Api.safeCenterSmsVerify,
Api.oauth2AccessToken,
};
const AccountManager();
static String getCookies(List<Cookie> cookies) {
// Sort cookies by path (longer path first).
cookies.sort((a, b) {
if (a.path == null && b.path == null) {
return 0;
} else if (a.path == null) {
return -1;
} else if (b.path == null) {
return 1;
} else {
return b.path!.length.compareTo(a.path!.length);
}
});
return cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
}
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final path = options.path;
if (path.startsWith(GStorage.blockServer)) return handler.next(options);
final Account account = options.extra['account'] ?? _findAccount(path);
if (account.isLogin) options.headers.addAll(account.headers);
// app端不需要管理cookie
if (path.startsWith(HttpString.appBaseUrl)) {
// debugPrint('is app: ${options.path}');
// bytes是grpc响应
if (options.responseType != ResponseType.bytes) {
final dataPtr = (options.method == 'POST' && options.data is Map
? options.data as Map
: options.queryParameters)
.cast<String, dynamic>();
if (dataPtr.isNotEmpty) {
if (!account.accessKey.isNullOrEmpty) {
dataPtr['access_key'] = account.accessKey!;
}
dataPtr['ts'] ??=
(DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
Utils.appSign(dataPtr);
// debugPrint(dataPtr.toString());
}
}
return handler.next(options);
} else {
account.cookieJar.loadForRequest(options.uri).then((cookies) {
final previousCookies =
options.headers[HttpHeaders.cookieHeader] as String?;
final newCookies = getCookies([
...?previousCookies
?.split(';')
.where((e) => e.isNotEmpty)
.map((c) => Cookie.fromSetCookieValue(c)),
...cookies,
]);
options.headers[HttpHeaders.cookieHeader] =
newCookies.isNotEmpty ? newCookies : null;
handler.next(options);
}).catchError((dynamic e, StackTrace s) {
final err = DioException(
requestOptions: options,
error: e,
stackTrace: s,
);
handler.reject(err, true);
});
}
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final path = response.requestOptions.path;
if (path.startsWith(HttpString.appBaseUrl) ||
path.startsWith(GStorage.blockServer)) {
return handler.next(response);
} else {
_saveCookies(response).then((_) => handler.next(response)).catchError(
(dynamic e, StackTrace s) {
final error = DioException(
requestOptions: response.requestOptions,
error: e,
stackTrace: s,
);
handler.reject(error, true);
},
);
}
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
String url = err.requestOptions.uri.toString();
debugPrint('🌹🌹ApiInterceptor: $url');
if (url.contains('heartbeat') ||
url.contains('seg.so') ||
url.contains('online/total') ||
url.contains('github') ||
(url.contains('skipSegments') && err.requestOptions.method == 'GET')) {
// skip
} else {
dioError(err).then((res) => SmartDialog.showToast(res + url));
}
if (err.response != null &&
!err.response!.requestOptions.path.startsWith(HttpString.appBaseUrl)) {
_saveCookies(err.response!).then((_) => handler.next(err)).catchError(
(dynamic e, StackTrace s) {
final error = DioException(
requestOptions: err.response!.requestOptions,
error: e,
stackTrace: s,
);
handler.next(error);
},
);
} else {
handler.next(err);
}
}
Future<void> _saveCookies(Response response) async {
final account = (response.requestOptions.extra['account'] as Account? ??
_findAccount(response.requestOptions.path));
final setCookies = response.headers[HttpHeaders.setCookieHeader];
if (setCookies == null || setCookies.isEmpty) {
return;
}
final List<Cookie> cookies = setCookies
.map((str) => str.split(_setCookieReg))
.expand((cookie) => cookie)
.where((cookie) => cookie.isNotEmpty)
.map((str) => Cookie.fromSetCookieValue(str))
.toList();
final statusCode = response.statusCode ?? 0;
final locations = response.headers[HttpHeaders.locationHeader] ?? [];
final isRedirectRequest = statusCode >= 300 && statusCode < 400;
final originalUri = response.requestOptions.uri;
final realUri = originalUri.resolveUri(response.realUri);
await account.cookieJar.saveFromResponse(realUri, cookies);
if (isRedirectRequest && locations.isNotEmpty) {
final originalUri = response.realUri;
await Future.wait(
locations.map(
(location) => account.cookieJar.saveFromResponse(
// Resolves the location based on the current Uri.
originalUri.resolve(location),
cookies,
),
),
);
}
await account.onChange();
}
Account _findAccount(String path) => loginApi.contains(path)
? AnonymousAccount()
: Accounts.get(AccountType.values.firstWhere(
(i) => apiTypeSet[i]?.contains(path) == true,
orElse: () => AccountType.main));
static Future<String> dioError(DioException error) async {
switch (error.type) {
case DioExceptionType.badCertificate:
return '证书有误!';
case DioExceptionType.badResponse:
return '服务器异常,请稍后重试!';
case DioExceptionType.cancel:
return '请求已被取消,请重新请求';
case DioExceptionType.connectionError:
return '连接错误,请检查网络设置';
case DioExceptionType.connectionTimeout:
return '网络连接超时,请检查网络设置';
case DioExceptionType.receiveTimeout:
return '响应超时,请稍后重试!';
case DioExceptionType.sendTimeout:
return '发送请求超时,请检查网络设置';
case DioExceptionType.unknown:
final String res =
(await Connectivity().checkConnectivity()).first.title;
return '$res网络异常 ${error.error}';
}
}
}
extension _ConnectivityResultExt on ConnectivityResult {
String get title => const ['蓝牙', 'Wi-Fi', '局域', '流量', '', '代理', '其他'][index];
}