diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 137599db..1d57ae66 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -4,13 +4,11 @@ import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart'; import 'package:PiliPlus/grpc/grpc_repo.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/utils/extension.dart'; -import 'package:PiliPlus/utils/storage.dart'; import 'package:dio/dio.dart'; import '../models/video/reply/data.dart'; import '../models/video/reply/emote.dart'; import 'api.dart'; -import 'constants.dart'; import 'init.dart'; class ReplyHttp { @@ -29,7 +27,7 @@ class ReplyHttp { : null; var res = !isLogin ? await Request().get( - '${HttpString.apiBaseUrl}${Api.replyList}/main', + '${Api.replyList}/main', queryParameters: { 'oid': oid, 'type': type, @@ -40,7 +38,7 @@ class ReplyHttp { options: options, ) : await Request().get( - '${HttpString.apiBaseUrl}${Api.replyList}', + Api.replyList, queryParameters: { 'oid': oid, 'type': type, @@ -134,26 +132,27 @@ class ReplyHttp { } static Future replyReplyList({ + required bool isLogin, required int oid, required int root, required int pageNum, required int type, - int sort = 1, required String banWordForReply, + bool? isCheck, }) async { - Options? options = GStorage.userInfo.get('userInfoCache') == null + Options? options = isLogin.not ? Options( headers: {HttpHeaders.cookieHeader: "buvid3= ; b_nut= ; sid= "}) : null; var res = await Request().get( - '${HttpString.apiBaseUrl}${Api.replyReplyList}', + Api.replyReplyList, queryParameters: { 'oid': oid, 'root': root, 'pn': pageNum, 'type': type, 'sort': 1, - 'csrf': await Request.getCsrf(), + if (isLogin) 'csrf': await Request.getCsrf(), }, options: options, ); @@ -168,7 +167,11 @@ class ReplyHttp { } return LoadingState.success(replyData); } else { - return LoadingState.error(res.data['message']); + return LoadingState.error( + isCheck == true + ? '${res.data['code']}${res.data['message']}' + : res.data['message'], + ); } } diff --git a/lib/pages/common/reply_controller.dart b/lib/pages/common/reply_controller.dart index 451deee1..b67ae6dd 100644 --- a/lib/pages/common/reply_controller.dart +++ b/lib/pages/common/reply_controller.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/reply.dart'; import 'package:PiliPlus/models/common/reply_type.dart'; import 'package:PiliPlus/models/video/reply/data.dart'; import 'package:PiliPlus/pages/common/common_controller.dart'; @@ -9,12 +10,14 @@ import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/models/common/reply_sort_type.dart'; import 'package:PiliPlus/models/video/reply/item.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:get/get_navigation/src/dialog/dialog_route.dart'; +import 'package:fixnum/fixnum.dart' as $fixnum; abstract class ReplyController extends CommonController { String nextOffset = ''; @@ -27,10 +30,11 @@ abstract class ReplyController extends CommonController { late final bool isLogin = GStorage.userInfo.get('userInfoCache') != null; CursorReply? cursor; - late Mode mode = Mode.MAIN_LIST_HOT; + late Rx mode = Mode.MAIN_LIST_HOT.obs; late bool hasUpTop = false; late final banWordForReply = GStorage.banWordForReply; + late final enableCommAntifraud = GStorage.enableCommAntifraud; @override void onInit() { @@ -43,7 +47,7 @@ abstract class ReplyController extends CommonController { } sortType.value = ReplySortType.values[defaultReplySortIndex]; if (sortType.value == ReplySortType.time) { - mode = Mode.MAIN_LIST_TIME; + mode.value = Mode.MAIN_LIST_TIME; } } @@ -95,7 +99,7 @@ abstract class ReplyController extends CommonController { hasUpTop = true; } } - if (response.response.topReplies != null) { + if ((response.response.topReplies as List?)?.isNotEmpty == true) { replies.insertAll(0, response.response.topReplies); hasUpTop = true; } @@ -117,11 +121,11 @@ abstract class ReplyController extends CommonController { switch (sortType.value) { case ReplySortType.time: sortType.value = ReplySortType.like; - mode = Mode.MAIN_LIST_HOT; + mode.value = Mode.MAIN_LIST_HOT; break; case ReplySortType.like: sortType.value = ReplySortType.time; - mode = Mode.MAIN_LIST_TIME; + mode.value = Mode.MAIN_LIST_TIME; break; } nextOffset = ''; @@ -204,21 +208,44 @@ abstract class ReplyController extends CommonController { } count.value += 1; loadingState.value = LoadingState.success(response); + if (enableCommAntifraud && context.mounted) { + checkReply( + context, + oid ?? replyItem.oid.toInt(), + replyItem?.id.toInt(), + replyItem?.type.toInt() ?? + replyType?.index ?? + ReplyType.video.index, + replyInfo.id.toInt(), + replyInfo.content.message, + ); + } } else { ReplyData response = loadingState.value is Success ? (loadingState.value as Success).response : ReplyData(); response.replies ??= []; + ReplyItemModel replyInfo = ReplyItemModel.fromJson(res, ''); if (oid != null) { - response.replies - ?.insert(hasUpTop ? 1 : 0, ReplyItemModel.fromJson(res, '')); + response.replies?.insert(hasUpTop ? 1 : 0, replyInfo); } else { response.replies?[index].replies ??= []; - response.replies?[index].replies - ?.add(ReplyItemModel.fromJson(res, '')); + response.replies?[index].replies?.add(replyInfo); } count.value += 1; loadingState.value = LoadingState.success(response); + if (enableCommAntifraud && context.mounted) { + checkReply( + context, + oid ?? replyItem.oid, + replyItem?.rpid, + replyItem?.type.toInt() ?? + replyType?.index ?? + ReplyType.video.index, + replyInfo.rpid ?? 0, + replyInfo.content?.message ?? '', + ); + } } } }, @@ -262,4 +289,192 @@ abstract class ReplyController extends CommonController { loadingState.value = LoadingState.success(response); } } + + // ref https://github.com/freedom-introvert/biliSendCommAntifraud + void checkReply( + BuildContext context, + dynamic oid, + dynamic rpid, + int replyType, + int replyId, + String message, + ) async { + void showReplyCheckResult(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('评论检查结果'), + content: SelectableText(message), + ), + ); + } + + await Future.delayed(const Duration(seconds: 5)); + if (context.mounted.not) return; + // root reply + if (rpid == null) { + // no cookie check + dynamic res = await ReplyHttp.replyList( + isLogin: false, + oid: oid, + nextOffset: '', + type: replyType, + sort: ReplySortType.time.index, + page: 1, + banWordForReply: '', + ); + if (context.mounted.not) return; + if (res is Error) { + SmartDialog.showToast('获取评论主列表时发生错误:${res.errMsg}'); + return; + } else if (res is Success) { + ReplyData replies = res.response; + int index = + replies.replies?.indexWhere((item) => item.rpid == replyId) ?? -1; + if (index != -1) { + // found + if (context.mounted) { + showReplyCheckResult( + context, + '无账号状态下找到了你的评论,评论正常!\n\n你的评论:$message', + ); + } + } else { + // not found + if (context.mounted.not) return; + // cookie check + dynamic res1 = await ReplyHttp.replyReplyList( + isLogin: isLogin, + oid: oid, + root: rpid, + pageNum: 1, + type: replyType, + banWordForReply: '', + ); + if (context.mounted.not) return; + if (res1 is Error) { + // not found + if (context.mounted) { + showReplyCheckResult( + context, + '无法找到你的评论。\n\n你的评论:$message', + ); + } + } else if (res1 is Success) { + // found + if (context.mounted.not) return; + // no cookie check + dynamic res2 = await ReplyHttp.replyReplyList( + isLogin: false, + oid: oid, + root: rpid, + pageNum: 1, + type: replyType, + banWordForReply: '', + isCheck: true, + ); + if (context.mounted.not) return; + if (res2 is Error) { + // not found + if (context.mounted) { + showReplyCheckResult( + context, + res2.errMsg.startsWith('12022') + ? '你的评论被shadow ban(仅自己可见)!\n\n你的评论: $message' + : '评论不可见(${res2.errMsg}): $message', + ); + } + } else if (res2 is Success) { + // found + if (context.mounted) { + showReplyCheckResult(context, ''' +你评论状态有点可疑,虽然无账号翻找评论区获取不到你的评论,但是无账号可通过 +https://api.bilibili.com/x/v2/reply/reply?oid=$oid&pn=1&ps=20&root=$rpid&type=$replyType +获取你的评论,疑似评论区被戒严或者这是你的视频。 + +你的评论:$message'''); + } + } + } + } + } + } else { + for (int i = 1; true; i++) { + if (context.mounted.not) return; + dynamic res3 = await ReplyHttp.replyReplyList( + isLogin: false, + oid: oid, + root: rpid, + pageNum: i, + type: replyType, + banWordForReply: '', + isCheck: true, + ); + if (res3 is Error) { + break; + } else if (res3 is Success) { + ReplyReplyData data = res3.response; + if (data.replies.isNullOrEmpty) { + break; + } + int index = + data.replies?.indexWhere((item) => item.rpid == replyId) ?? -1; + if (index == -1) { + // not found + } else { + // found + if (context.mounted) { + showReplyCheckResult( + context, + '无账号状态下找到了你的评论,评论正常!\n\n你的评论:$message', + ); + } + return; + } + } + } + + for (int i = 1; true; i++) { + if (context.mounted.not) return; + dynamic res4 = await ReplyHttp.replyReplyList( + isLogin: true, + oid: oid, + root: rpid, + pageNum: i, + type: replyType, + banWordForReply: '', + isCheck: true, + ); + if (res4 is Error) { + break; + } else if (res4 is Success) { + ReplyReplyData data = res4.response; + if (data.replies.isNullOrEmpty) { + break; + } + int index = + data.replies?.indexWhere((item) => item.rpid == replyId) ?? -1; + if (index == -1) { + // not found + } else { + // found + if (context.mounted) { + showReplyCheckResult( + context, + '你的评论被shadow ban(仅自己可见)!\n\n你的评论: $message', + ); + } + return; + } + } + } + + if (context.mounted) { + showReplyCheckResult( + context, + '评论不可见: $message', + ); + } + } + } } diff --git a/lib/pages/dynamics/detail/controller.dart b/lib/pages/dynamics/detail/controller.dart index cbc4016b..4b3c67d4 100644 --- a/lib/pages/dynamics/detail/controller.dart +++ b/lib/pages/dynamics/detail/controller.dart @@ -45,7 +45,7 @@ class DynamicDetailController extends ReplyController { oid: oid!, cursor: CursorReq( next: cursor?.next ?? $fixnum.Int64(0), - mode: mode, + mode: mode.value, ), banWordForReply: banWordForReply, ) diff --git a/lib/pages/html/controller.dart b/lib/pages/html/controller.dart index 24b7a0ec..af2d125e 100644 --- a/lib/pages/html/controller.dart +++ b/lib/pages/html/controller.dart @@ -55,7 +55,7 @@ class HtmlRenderController extends ReplyController { oid: oid.value, cursor: CursorReq( next: cursor?.next ?? $fixnum.Int64(0), - mode: mode, + mode: mode.value, ), banWordForReply: banWordForReply, ) diff --git a/lib/pages/setting/widgets/model.dart b/lib/pages/setting/widgets/model.dart index a8346cc9..a6c94ae6 100644 --- a/lib/pages/setting/widgets/model.dart +++ b/lib/pages/setting/widgets/model.dart @@ -1937,6 +1937,24 @@ List get extraSettings => [ setKey: SettingBoxKey.showDmChart, defaultVal: false, ), + SettingsModel( + settingsType: SettingsType.sw1tch, + title: '发评反诈', + subtitle: '发送评论后检查评论是否可见', + leading: Stack( + alignment: Alignment.center, + children: [ + const Icon(Icons.shield), + Icon( + Icons.reply, + size: 16, + color: Theme.of(Get.context!).colorScheme.surface, + ), + ], + ), + setKey: SettingBoxKey.enableCommAntifraud, + defaultVal: false, + ), SettingsModel( settingsType: SettingsType.sw1tch, enableFeedback: true, diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index 2839cfdf..eea97bf6 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -25,7 +25,7 @@ class VideoReplyController extends ReplyController { oid: aid!, cursor: CursorReq( next: cursor?.next ?? $fixnum.Int64(0), - mode: mode, + mode: mode.value, ), banWordForReply: banWordForReply, ) diff --git a/lib/pages/video/detail/reply_reply/controller.dart b/lib/pages/video/detail/reply_reply/controller.dart index cc71ef96..78318787 100644 --- a/lib/pages/video/detail/reply_reply/controller.dart +++ b/lib/pages/video/detail/reply_reply/controller.dart @@ -1,7 +1,7 @@ import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/video/reply/item.dart'; -import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:PiliPlus/pages/common/reply_controller.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:flutter/material.dart'; @@ -10,7 +10,7 @@ import 'package:PiliPlus/http/reply.dart'; import 'package:PiliPlus/models/common/reply_type.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -class VideoReplyReplyController extends CommonController +class VideoReplyReplyController extends ReplyController with GetTickerProviderStateMixin { VideoReplyReplyController({ required this.hasRoot, @@ -32,9 +32,6 @@ class VideoReplyReplyController extends CommonController int? rpid; ReplyType replyType; // = ReplyType.video; - CursorReply? cursor; - Rx mode = Mode.MAIN_LIST_TIME.obs; - RxInt count = (-1).obs; int? upMid; dynamic firstFloor; @@ -45,8 +42,6 @@ class VideoReplyReplyController extends CommonController late final horizontalPreview = GStorage.horizontalPreview; - late final banWordForReply = GStorage.banWordForReply; - @override void onInit() { super.onInit(); @@ -203,6 +198,7 @@ class VideoReplyReplyController extends CommonController banWordForReply: banWordForReply, ) : ReplyHttp.replyReplyList( + isLogin: isLogin, oid: oid!, root: rpid!, pageNum: currentPage, @@ -210,6 +206,7 @@ class VideoReplyReplyController extends CommonController banWordForReply: banWordForReply, ); + @override queryBySort() { mode.value = mode.value == Mode.MAIN_LIST_HOT ? Mode.MAIN_LIST_TIME diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index 71b19a10..9ac28143 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -27,7 +27,7 @@ class VideoReplyReplyPanel extends StatefulWidget { this.dialog, this.firstFloor, this.source, - this.replyType, + required this.replyType, this.isDialogue = false, this.isTop = false, this.onViewImage, @@ -39,7 +39,7 @@ class VideoReplyReplyPanel extends StatefulWidget { final int? dialog; final dynamic firstFloor; final String? source; - final ReplyType? replyType; + final ReplyType replyType; final bool isDialogue; final bool isTop; final VoidCallback? onViewImage; @@ -73,7 +73,7 @@ class _VideoReplyReplyPanelState extends State oid: widget.oid, rpid: widget.rpid, dialog: widget.dialog, - replyType: widget.replyType!, + replyType: widget.replyType, isDialogue: widget.isDialogue, ), tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}', @@ -352,15 +352,36 @@ class _VideoReplyReplyPanelState extends State _videoReplyReplyController.count.value += 1; _videoReplyReplyController.loadingState.value = LoadingState.success(list); + if (_videoReplyReplyController.enableCommAntifraud && mounted) { + _videoReplyReplyController.checkReply( + context, + oid, + root, + widget.replyType.index, + replyInfo.id.toInt(), + replyInfo.content.message, + ); + } } else { List list = _videoReplyReplyController.loadingState.value is Success ? (_videoReplyReplyController.loadingState.value as Success) .response : []; - list.insert(index + 1, ReplyItemModel.fromJson(res, '')); + ReplyItemModel replyInfo = ReplyItemModel.fromJson(res, ''); + list.insert(index + 1, replyInfo); _videoReplyReplyController.count.value += 1; _videoReplyReplyController.loadingState.value = LoadingState.success(list); + if (_videoReplyReplyController.enableCommAntifraud && mounted) { + _videoReplyReplyController.checkReply( + context, + oid, + root, + widget.replyType.index, + replyInfo.rpid ?? 0, + replyInfo.content?.message ?? '', + ); + } } } }); diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 58e11902..c8ed0c67 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -54,7 +54,6 @@ import '../pages/setting/style_setting.dart'; import '../pages/subscription/index.dart'; import '../pages/subscription_detail/index.dart'; import '../pages/video/detail/index.dart'; -import '../pages/video/detail/reply_reply/index.dart'; import '../pages/whisper/index.dart'; import '../pages/whisper_detail/index.dart'; @@ -99,8 +98,8 @@ class Routes { CustomGetPage(name: '/member', page: () => const MemberPageNew()), CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()), // 二级回复 - CustomGetPage( - name: '/replyReply', page: () => const VideoReplyReplyPanel()), + // CustomGetPage( + // name: '/replyReply', page: () => const VideoReplyReplyPanel()), // 推荐流设置 CustomGetPage( name: '/recommendSetting', page: () => const RecommendSetting()), diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 166095c3..2112c415 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -361,6 +361,9 @@ class GStorage { static bool get showDmChart => GStorage.setting.get(SettingBoxKey.showDmChart, defaultValue: false); + static bool get enableCommAntifraud => GStorage.setting + .get(SettingBoxKey.enableCommAntifraud, defaultValue: false); + static List get dynamicDetailRatio => List.from(setting .get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0])); @@ -591,6 +594,7 @@ class SettingBoxKey { enableLivePhoto = 'enableLivePhoto', showSeekPreview = 'showSeekPreview', showDmChart = 'showDmChart', + enableCommAntifraud = 'enableCommAntifraud', // Sponsor Block enableSponsorBlock = 'enableSponsorBlock',