import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/grpc/im.dart'; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/validate.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/login/model.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/media.dart'; import 'package:PiliPlus/models_new/later/list.dart'; import 'package:PiliPlus/pages/common/multi_select/base.dart'; import 'package:PiliPlus/pages/dynamics_tab/controller.dart'; import 'package:PiliPlus/pages/fav_detail/controller.dart' show BaseFavController; import 'package:PiliPlus/pages/group_panel/view.dart'; import 'package:PiliPlus/pages/login/geetest/geetest_webview_dialog.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; abstract final class RequestUtils { static Future syncHistoryStatus() async { final account = Accounts.history; if (!account.isLogin) { return; } final res = await UserHttp.historyStatus(account: account); if (res case Success(:final response)) { GStorage.localCache.put(LocalCacheKey.historyPause, response); } } // 1:小视频(已弃用) // 2:相簿 // 3:纯文字 // 4:直播(此类型不常用,见分享其他内容消息) // 5:视频 // 6:专栏 // 7:番剧(id 为 season_id) // 8:音乐 // 9:国产动画(id 为 AV 号) // 10:图片 // 11:动态 // 16:番剧(id 为 epid) // 17:番剧 // https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/message/private_msg_content.md static Future pmShare({ required int receiverId, required Map content, String? message, }) async { final ownerMid = Accounts.main.mid; final contentRes = await ImGrpc.sendMsg( senderUid: ownerMid, receiverId: receiverId, content: jsonEncode(content), msgType: content['source'] is String ? MsgType.EN_MSG_TYPE_COMMON_SHARE_CARD : MsgType.EN_MSG_TYPE_SHARE_V2, ); if (contentRes.isSuccess) { if (message?.isNotEmpty == true) { final msgRes = await ImGrpc.sendMsg( senderUid: ownerMid, receiverId: receiverId, content: jsonEncode({"content": message}), msgType: MsgType.EN_MSG_TYPE_TEXT, ); return msgRes.isSuccess; } else { return true; } } else { return false; } } static Future actionRelationMod({ required BuildContext context, required dynamic mid, required bool isFollow, required ValueChanged? afterMod, Map? followStatus, }) async { if (mid == null) { return; } feedBack(); if (!isFollow) { final res = await VideoHttp.relationMod( mid: mid, act: 1, reSrc: 11, ); if (res.isSuccess) { SmartDialog.showToast('关注成功'); afterMod?.call(2); } else { res.toast(); } } else { if (followStatus?['tag'] == null) { final res = await UserHttp.hasFollow(mid); if (res case Success(:final response)) { followStatus = response; } else { res.toast(); return; } } if (context.mounted) { bool isSpecialFollowed = followStatus!['special'] == 1; String text = isSpecialFollowed ? '移除特别关注' : '加入特别关注'; showDialog( context: context, builder: (context) => AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( dense: true, onTap: () async { Get.back(); final res = await MemberHttp.specialAction( fid: mid, isAdd: !isSpecialFollowed, ); if (res.isSuccess) { SmartDialog.showToast('$text成功'); afterMod?.call(isSpecialFollowed ? 2 : -10); } else { res.toast(); } }, title: Text( text, style: const TextStyle(fontSize: 14), ), ), ListTile( dense: true, onTap: () async { Get.back(); final result = await showModalBottomSheet>( context: context, useSafeArea: true, isScrollControlled: true, constraints: BoxConstraints( maxWidth: min(640, context.mediaQueryShortestSide), ), builder: (BuildContext context) { final maxChildSize = PlatformUtils.isMobile && !context.mediaQuerySize.isPortrait ? 1.0 : 0.7; return DraggableScrollableSheet( minChildSize: 0, maxChildSize: 1, snap: true, expand: false, snapSizes: [maxChildSize], initialChildSize: maxChildSize, builder: ( BuildContext context, ScrollController scrollController, ) { return GroupPanel( mid: mid, tags: followStatus!['tag'], scrollController: scrollController, ); }, ); }, ); followStatus!['tag'] = result?.toList(); if (result != null) { afterMod?.call(result.contains(-10) ? -10 : 2); } }, title: const Text( '设置分组', style: TextStyle(fontSize: 14), ), ), ListTile( dense: true, onTap: () async { Get.back(); final res = await VideoHttp.relationMod( mid: mid, act: 2, reSrc: 11, ); if (res.isSuccess) { SmartDialog.showToast('取消关注成功'); afterMod?.call(0); } else { res.toast(); } }, title: const Text( '取消关注', style: TextStyle(fontSize: 14), ), ), ], ), ), ); } } } static ReplyInfo replyCast(Map res) { Map? emote = res['content']['emote']; emote?.forEach((key, value) { value['size'] = value['meta']['size']; }); return ReplyInfo.create()..mergeFromProto3Json( res ..['content'].remove('members') ..['id'] = res['rpid'] ..['member']['name'] = res['member']['uname'] ..['member']['face'] = res['member']['avatar'] ..['member']['level'] = res['member']['level_info']['current_level'] ..['member']['vipStatus'] = res['member']['vip']['vipStatus'] ..['member']['vipType'] = res['member']['vip']['vipType'] ..['member']['officialVerifyType'] = res['member']['official_verify']['type'] ..['content']['emotes'] = emote, ignoreUnknownFields: true, ); } // static Future getWwebid(mid) async { // try { // final response = await Request().get( // '${HttpString.spaceBaseUrl}/$mid/dynamic', // options: Options( // extra: {'account': AnonymousAccount()}, // ), // ); // dom.Document document = html_parser.parse(response.data); // dom.Element? scriptElement = // document.querySelector('script#__RENDER_DATA__'); // return jsonDecode( // Uri.decodeComponent(scriptElement?.text ?? ''))['access_id']; // } catch (e) { // if (kDebugMode) debugPrint('failed to get wwebid: $e'); // return null; // } // } static Future insertCreatedDyn(dynamic id) async { if (id != null) { try { await Future.delayed(const Duration(milliseconds: 450)); final res = await DynamicsHttp.dynamicDetail(id: id); if (res case final Success e) { final ctr = Get.find(tag: 'all'); if (ctr.loadingState.value case Success(:final response?)) { response.insert(0, e.response); ctr.loadingState.refresh(); return; } ctr.loadingState.value = Success([e.response]); } } catch (e) { if (kDebugMode) debugPrint('create dyn $e'); } } } static Future checkCreatedDyn({ dynamic id, String? dynText, bool isManual = false, }) async { if (isManual || Pref.enableCreateDynAntifraud) { try { if (id != null) { if (!isManual) { await Future.delayed(const Duration(seconds: 5)); } final res = await DynamicsHttp.dynamicDetail( id: id, clearCookie: true, ); final isSuccess = res.isSuccess; final actions = [ if (!isSuccess) TextButton( onPressed: () { Get.back(); Utils.copyText('https://www.bilibili.com/opus/$id'); Get.toNamed( '/webview', parameters: { 'url': 'https://www.bilibili.com/h5/comment/appeal?${Utils.themeUrl(Get.isDarkMode)}', }, ); }, child: const Text('申诉'), ), if (!isManual) TextButton( onPressed: Get.back, child: Text( '关闭', style: TextStyle(color: Get.theme.colorScheme.outline), ), ), ]; showDialog( context: Get.context!, barrierDismissible: isManual, builder: (context) => AlertDialog( title: const Text('动态检查结果'), content: SelectableText( '${isSuccess ? '无账号状态下找到了你的动态,动态正常!' : '你的动态被shadow ban(仅自己可见)!'}${dynText != null ? ' \n\n动态内容: $dynText' : ''}', ), actions: actions.isEmpty ? null : actions, ), ); } } catch (e) { if (kDebugMode) debugPrint('check dyn error: $e'); } } } // 动态点赞 static Future onLikeDynamic( DynamicItemModel item, bool uiStatus, VoidCallback onSuccess, ) async { feedBack(); final like = item.modules.moduleStat?.like; final status = like?.status ?? false; if (status ^ uiStatus) { SmartDialog.showToast(status ? '点赞成功' : '取消赞'); onSuccess(); return; } final res = await DynamicsHttp.thumbDynamic( dynamicId: item.idStr!, up: status ? 2 : 1, // 1 已点赞 2 不喜欢 0 未操作 ); if (res.isSuccess) { SmartDialog.showToast(status ? '取消赞' : '点赞成功'); like ?..count = (like.count ?? 0) + (status ? -1 : 1) ..status = !status; onSuccess(); } else { res.toast(); } } static void onCopyOrMove({ required BuildContext context, required bool isCopy, required CommonMultiSelectMixin ctr, required dynamic mediaId, required dynamic mid, }) { FavHttp.allFavFolders(mid).then((res) { if (!context.mounted) return; if (res case Success(:final response)) { final list = response.list; if (list == null || list.isEmpty) return; int? checkedId; showDialog( context: context, builder: (context) { return AlertDialog( title: Text('${isCopy ? '复制' : '移动'}到'), contentPadding: const EdgeInsets.only(top: 5), content: SingleChildScrollView( child: RadioGroup( onChanged: (value) { checkedId = value; (context as Element).markNeedsBuild(); }, groupValue: checkedId, child: Column( children: list.map((item) { return RadioListTile( dense: true, title: Text(item.title), value: item.id, ); }).toList(), ), ), ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: Theme.of(context).colorScheme.outline, ), ), ), TextButton( onPressed: () { if (checkedId != null) { final removeList = ctr.allChecked.toSet(); SmartDialog.showLoading(); FavHttp.copyOrMoveFav( isCopy: isCopy, isFav: ctr is BaseFavController, srcMediaId: mediaId, tarMediaId: checkedId, resources: removeList .map( (e) => switch (e) { LaterItemModel _ => e.aid, FavDetailItemModel _ => '${e.id}:${e.type}', _ => throw UnsupportedError(e.toString()), }, ) .join(','), mid: isCopy ? mid : null, ).then((res) { if (res.isSuccess) { ctr.handleSelect(checked: false); if (!isCopy) { ctr.loadingState ..value.data!.removeWhere(removeList.contains) ..refresh(); } SmartDialog.dismiss(); SmartDialog.showToast('${isCopy ? '复制' : '移动'}成功'); Get.back(); } else { SmartDialog.dismiss(); res.toast(); } }); } }, child: const Text('确认'), ), ], ); }, ); } else { res.toast(); } }); } static Future validate( String vVoucher, ValueChanged onSuccess, ) async { if (Platform.isLinux) { return; } final res = await ValidateHttp.gaiaVgateRegister(vVoucher); if (!res.isSuccess) { res.toast(); return; } final resData = res.data; if (resData == null) { SmartDialog.showToast("null data"); return; } CaptchaDataModel captchaData = CaptchaDataModel(); final geetest = resData['geetest']; String? gt = geetest?['gt']; String? challenge = geetest?['challenge']; captchaData.token = resData['token']; bool isGeeArgumentValid() { return gt?.isNotEmpty == true && challenge?.isNotEmpty == true && captchaData.token?.isNotEmpty == true; } if (!isGeeArgumentValid()) { SmartDialog.showToast("参数为空"); return; } Future gaiaVgateValidate() async { final res = await ValidateHttp.gaiaVgateValidate( challenge: captchaData.geetest?.challenge, seccode: captchaData.seccode, token: captchaData.token, validate: captchaData.validate, ); if (res case Success(:final response?)) { if (response['is_valid'] == 1) { final griskId = response['grisk_id']; if (griskId is String) { onSuccess(griskId); } } else { SmartDialog.showToast('invalid'); } } else { res.toast(); } } if (PlatformUtils.isDesktop) { final json = await showDialog>( context: Get.context!, builder: (context) => GeetestWebviewDialog(gt!, challenge!), ); if (json != null) { captchaData ..validate = json['geetest_validate'] ..seccode = json['geetest_seccode'] ..geetest = GeetestData( challenge: json['geetest_challenge'], gt: gt!, ); gaiaVgateValidate(); } return; } final registerData = Gt3RegisterData( challenge: challenge, gt: gt, success: true, ); Gt3FlutterPlugin() ..addEventHandler( onClose: (Map message) { SmartDialog.showToast('关闭验证'); }, onResult: (Map message) { if (kDebugMode) debugPrint("Captcha result: $message"); String code = message["code"]; if (code == "1") { // 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询 SmartDialog.showToast('验证成功'); final result = message['result']; captchaData ..validate = result?['geetest_validate'] ..seccode = result?['geetest_seccode'] ..geetest = GeetestData( challenge: result?['geetest_challenge'], gt: gt!, ); gaiaVgateValidate(); } else { // 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried. if (kDebugMode) debugPrint("Captcha result code : $code"); } }, onError: (Map message) { SmartDialog.showToast("Captcha onError: $message"); String code = message["code"]; // 处理验证中返回的错误 Handling errors returned in verification if (Platform.isAndroid) { // Android 平台 if (code == "-2") { // Dart 调用异常 Call exception } else if (code == "-1") { // Gt3RegisterData 参数不合法 Parameter is invalid } else if (code == "201") { // 网络无法访问 Network inaccessible } else if (code == "202") { // Json 解析错误 Analysis error } else if (code == "204") { // WebView 加载超时,请检查是否混淆极验 SDK Load timed out } else if (code == "204_1") { // WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log } else if (code == "204_2") { // WebView 加载 SSLError } else if (code == "206") { // gettype 接口错误或返回为 null API error or return null } else if (code == "207") { // getphp 接口错误或返回为 null API error or return null } else if (code == "208") { // ajax 接口错误或返回为 null API error or return null } else { // 更多错误码参考开发文档 More error codes refer to the development document // https://docs.geetest.com/sensebot/apirefer/errorcode/android } } if (Platform.isIOS) { // iOS 平台 if (code == "-1009") { // 网络无法访问 Network inaccessible } else if (code == "-1004") { // 无法查找到 HOST Unable to find HOST } else if (code == "-1002") { // 非法的 URL Illegal URL } else if (code == "-1001") { // 网络超时 Network timeout } else if (code == "-999") { // 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation } else if (code == "-21") { // 使用了重复的 challenge Duplicate challenges are used // 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached } else if (code == "-20") { // 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again } else if (code == "-10") { // 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification } else if (code == "-2") { // Dart 调用异常 Call exception } else if (code == "-1") { // Gt3RegisterData 参数不合法 Parameter is invalid } else { // 更多错误码参考开发文档 More error codes refer to the development document // https://docs.geetest.com/sensebot/apirefer/errorcode/ios } } }, ) ..startCaptcha(registerData); } static Future showUserRealName(String mid) async { final res = await UserHttp.getUserRealName(mid); if (res case Success(:final response)) { final show = !response.name.isNullOrEmpty; showDialog( context: Get.context!, builder: (context) => AlertDialog( title: SelectableText( show ? response.name! : response.rejectPage?.title ?? '', ), content: show ? null : Text(response.rejectPage?.text ?? ''), actions: [ TextButton( onPressed: Get.back, child: const Text('关闭'), ), ], ), ); } else { res.toast(); } } }