feat: comment antifraud (#193)

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
dom
2025-02-02 21:19:26 +08:00
committed by bggRGjQaUbCoE
parent ca16551917
commit 54e90bd986
10 changed files with 292 additions and 35 deletions

View File

@@ -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/grpc/grpc_repo.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../models/video/reply/data.dart'; import '../models/video/reply/data.dart';
import '../models/video/reply/emote.dart'; import '../models/video/reply/emote.dart';
import 'api.dart'; import 'api.dart';
import 'constants.dart';
import 'init.dart'; import 'init.dart';
class ReplyHttp { class ReplyHttp {
@@ -29,7 +27,7 @@ class ReplyHttp {
: null; : null;
var res = !isLogin var res = !isLogin
? await Request().get( ? await Request().get(
'${HttpString.apiBaseUrl}${Api.replyList}/main', '${Api.replyList}/main',
queryParameters: { queryParameters: {
'oid': oid, 'oid': oid,
'type': type, 'type': type,
@@ -40,7 +38,7 @@ class ReplyHttp {
options: options, options: options,
) )
: await Request().get( : await Request().get(
'${HttpString.apiBaseUrl}${Api.replyList}', Api.replyList,
queryParameters: { queryParameters: {
'oid': oid, 'oid': oid,
'type': type, 'type': type,
@@ -134,26 +132,27 @@ class ReplyHttp {
} }
static Future<LoadingState> replyReplyList({ static Future<LoadingState> replyReplyList({
required bool isLogin,
required int oid, required int oid,
required int root, required int root,
required int pageNum, required int pageNum,
required int type, required int type,
int sort = 1,
required String banWordForReply, required String banWordForReply,
bool? isCheck,
}) async { }) async {
Options? options = GStorage.userInfo.get('userInfoCache') == null Options? options = isLogin.not
? Options( ? Options(
headers: {HttpHeaders.cookieHeader: "buvid3= ; b_nut= ; sid= "}) headers: {HttpHeaders.cookieHeader: "buvid3= ; b_nut= ; sid= "})
: null; : null;
var res = await Request().get( var res = await Request().get(
'${HttpString.apiBaseUrl}${Api.replyReplyList}', Api.replyReplyList,
queryParameters: { queryParameters: {
'oid': oid, 'oid': oid,
'root': root, 'root': root,
'pn': pageNum, 'pn': pageNum,
'type': type, 'type': type,
'sort': 1, 'sort': 1,
'csrf': await Request.getCsrf(), if (isLogin) 'csrf': await Request.getCsrf(),
}, },
options: options, options: options,
); );
@@ -168,7 +167,11 @@ class ReplyHttp {
} }
return LoadingState.success(replyData); return LoadingState.success(replyData);
} else { } else {
return LoadingState.error(res.data['message']); return LoadingState.error(
isCheck == true
? '${res.data['code']}${res.data['message']}'
: res.data['message'],
);
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart'; import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/loading_state.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/common/reply_type.dart';
import 'package:PiliPlus/models/video/reply/data.dart'; import 'package:PiliPlus/models/video/reply/data.dart';
import 'package:PiliPlus/pages/common/common_controller.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:PiliPlus/utils/utils.dart';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:PiliPlus/models/common/reply_sort_type.dart'; import 'package:PiliPlus/models/common/reply_sort_type.dart';
import 'package:PiliPlus/models/video/reply/item.dart'; import 'package:PiliPlus/models/video/reply/item.dart';
import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:get/get_navigation/src/dialog/dialog_route.dart'; import 'package:get/get_navigation/src/dialog/dialog_route.dart';
import 'package:fixnum/fixnum.dart' as $fixnum;
abstract class ReplyController extends CommonController { abstract class ReplyController extends CommonController {
String nextOffset = ''; String nextOffset = '';
@@ -27,10 +30,11 @@ abstract class ReplyController extends CommonController {
late final bool isLogin = GStorage.userInfo.get('userInfoCache') != null; late final bool isLogin = GStorage.userInfo.get('userInfoCache') != null;
CursorReply? cursor; CursorReply? cursor;
late Mode mode = Mode.MAIN_LIST_HOT; late Rx<Mode> mode = Mode.MAIN_LIST_HOT.obs;
late bool hasUpTop = false; late bool hasUpTop = false;
late final banWordForReply = GStorage.banWordForReply; late final banWordForReply = GStorage.banWordForReply;
late final enableCommAntifraud = GStorage.enableCommAntifraud;
@override @override
void onInit() { void onInit() {
@@ -43,7 +47,7 @@ abstract class ReplyController extends CommonController {
} }
sortType.value = ReplySortType.values[defaultReplySortIndex]; sortType.value = ReplySortType.values[defaultReplySortIndex];
if (sortType.value == ReplySortType.time) { 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; hasUpTop = true;
} }
} }
if (response.response.topReplies != null) { if ((response.response.topReplies as List?)?.isNotEmpty == true) {
replies.insertAll(0, response.response.topReplies); replies.insertAll(0, response.response.topReplies);
hasUpTop = true; hasUpTop = true;
} }
@@ -117,11 +121,11 @@ abstract class ReplyController extends CommonController {
switch (sortType.value) { switch (sortType.value) {
case ReplySortType.time: case ReplySortType.time:
sortType.value = ReplySortType.like; sortType.value = ReplySortType.like;
mode = Mode.MAIN_LIST_HOT; mode.value = Mode.MAIN_LIST_HOT;
break; break;
case ReplySortType.like: case ReplySortType.like:
sortType.value = ReplySortType.time; sortType.value = ReplySortType.time;
mode = Mode.MAIN_LIST_TIME; mode.value = Mode.MAIN_LIST_TIME;
break; break;
} }
nextOffset = ''; nextOffset = '';
@@ -204,21 +208,44 @@ abstract class ReplyController extends CommonController {
} }
count.value += 1; count.value += 1;
loadingState.value = LoadingState.success(response); 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 { } else {
ReplyData response = loadingState.value is Success ReplyData response = loadingState.value is Success
? (loadingState.value as Success).response ? (loadingState.value as Success).response
: ReplyData(); : ReplyData();
response.replies ??= <ReplyItemModel>[]; response.replies ??= <ReplyItemModel>[];
ReplyItemModel replyInfo = ReplyItemModel.fromJson(res, '');
if (oid != null) { if (oid != null) {
response.replies response.replies?.insert(hasUpTop ? 1 : 0, replyInfo);
?.insert(hasUpTop ? 1 : 0, ReplyItemModel.fromJson(res, ''));
} else { } else {
response.replies?[index].replies ??= <ReplyItemModel>[]; response.replies?[index].replies ??= <ReplyItemModel>[];
response.replies?[index].replies response.replies?[index].replies?.add(replyInfo);
?.add(ReplyItemModel.fromJson(res, ''));
} }
count.value += 1; count.value += 1;
loadingState.value = LoadingState.success(response); 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); 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',
);
}
}
}
} }

View File

@@ -45,7 +45,7 @@ class DynamicDetailController extends ReplyController {
oid: oid!, oid: oid!,
cursor: CursorReq( cursor: CursorReq(
next: cursor?.next ?? $fixnum.Int64(0), next: cursor?.next ?? $fixnum.Int64(0),
mode: mode, mode: mode.value,
), ),
banWordForReply: banWordForReply, banWordForReply: banWordForReply,
) )

View File

@@ -55,7 +55,7 @@ class HtmlRenderController extends ReplyController {
oid: oid.value, oid: oid.value,
cursor: CursorReq( cursor: CursorReq(
next: cursor?.next ?? $fixnum.Int64(0), next: cursor?.next ?? $fixnum.Int64(0),
mode: mode, mode: mode.value,
), ),
banWordForReply: banWordForReply, banWordForReply: banWordForReply,
) )

View File

@@ -1937,6 +1937,24 @@ List<SettingsModel> get extraSettings => [
setKey: SettingBoxKey.showDmChart, setKey: SettingBoxKey.showDmChart,
defaultVal: false, 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( SettingsModel(
settingsType: SettingsType.sw1tch, settingsType: SettingsType.sw1tch,
enableFeedback: true, enableFeedback: true,

View File

@@ -25,7 +25,7 @@ class VideoReplyController extends ReplyController {
oid: aid!, oid: aid!,
cursor: CursorReq( cursor: CursorReq(
next: cursor?.next ?? $fixnum.Int64(0), next: cursor?.next ?? $fixnum.Int64(0),
mode: mode, mode: mode.value,
), ),
banWordForReply: banWordForReply, banWordForReply: banWordForReply,
) )

View File

@@ -1,7 +1,7 @@
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart'; import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/video/reply/item.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/global_data.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.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:PiliPlus/models/common/reply_type.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class VideoReplyReplyController extends CommonController class VideoReplyReplyController extends ReplyController
with GetTickerProviderStateMixin { with GetTickerProviderStateMixin {
VideoReplyReplyController({ VideoReplyReplyController({
required this.hasRoot, required this.hasRoot,
@@ -32,9 +32,6 @@ class VideoReplyReplyController extends CommonController
int? rpid; int? rpid;
ReplyType replyType; // = ReplyType.video; ReplyType replyType; // = ReplyType.video;
CursorReply? cursor;
Rx<Mode> mode = Mode.MAIN_LIST_TIME.obs;
RxInt count = (-1).obs;
int? upMid; int? upMid;
dynamic firstFloor; dynamic firstFloor;
@@ -45,8 +42,6 @@ class VideoReplyReplyController extends CommonController
late final horizontalPreview = GStorage.horizontalPreview; late final horizontalPreview = GStorage.horizontalPreview;
late final banWordForReply = GStorage.banWordForReply;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@@ -203,6 +198,7 @@ class VideoReplyReplyController extends CommonController
banWordForReply: banWordForReply, banWordForReply: banWordForReply,
) )
: ReplyHttp.replyReplyList( : ReplyHttp.replyReplyList(
isLogin: isLogin,
oid: oid!, oid: oid!,
root: rpid!, root: rpid!,
pageNum: currentPage, pageNum: currentPage,
@@ -210,6 +206,7 @@ class VideoReplyReplyController extends CommonController
banWordForReply: banWordForReply, banWordForReply: banWordForReply,
); );
@override
queryBySort() { queryBySort() {
mode.value = mode.value == Mode.MAIN_LIST_HOT mode.value = mode.value == Mode.MAIN_LIST_HOT
? Mode.MAIN_LIST_TIME ? Mode.MAIN_LIST_TIME

View File

@@ -27,7 +27,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.dialog, this.dialog,
this.firstFloor, this.firstFloor,
this.source, this.source,
this.replyType, required this.replyType,
this.isDialogue = false, this.isDialogue = false,
this.isTop = false, this.isTop = false,
this.onViewImage, this.onViewImage,
@@ -39,7 +39,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
final int? dialog; final int? dialog;
final dynamic firstFloor; final dynamic firstFloor;
final String? source; final String? source;
final ReplyType? replyType; final ReplyType replyType;
final bool isDialogue; final bool isDialogue;
final bool isTop; final bool isTop;
final VoidCallback? onViewImage; final VoidCallback? onViewImage;
@@ -73,7 +73,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
oid: widget.oid, oid: widget.oid,
rpid: widget.rpid, rpid: widget.rpid,
dialog: widget.dialog, dialog: widget.dialog,
replyType: widget.replyType!, replyType: widget.replyType,
isDialogue: widget.isDialogue, isDialogue: widget.isDialogue,
), ),
tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}', tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}',
@@ -352,15 +352,36 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
_videoReplyReplyController.count.value += 1; _videoReplyReplyController.count.value += 1;
_videoReplyReplyController.loadingState.value = _videoReplyReplyController.loadingState.value =
LoadingState.success(list); LoadingState.success(list);
if (_videoReplyReplyController.enableCommAntifraud && mounted) {
_videoReplyReplyController.checkReply(
context,
oid,
root,
widget.replyType.index,
replyInfo.id.toInt(),
replyInfo.content.message,
);
}
} else { } else {
List list = _videoReplyReplyController.loadingState.value is Success List list = _videoReplyReplyController.loadingState.value is Success
? (_videoReplyReplyController.loadingState.value as Success) ? (_videoReplyReplyController.loadingState.value as Success)
.response .response
: <ReplyItemModel>[]; : <ReplyItemModel>[];
list.insert(index + 1, ReplyItemModel.fromJson(res, '')); ReplyItemModel replyInfo = ReplyItemModel.fromJson(res, '');
list.insert(index + 1, replyInfo);
_videoReplyReplyController.count.value += 1; _videoReplyReplyController.count.value += 1;
_videoReplyReplyController.loadingState.value = _videoReplyReplyController.loadingState.value =
LoadingState.success(list); LoadingState.success(list);
if (_videoReplyReplyController.enableCommAntifraud && mounted) {
_videoReplyReplyController.checkReply(
context,
oid,
root,
widget.replyType.index,
replyInfo.rpid ?? 0,
replyInfo.content?.message ?? '',
);
}
} }
} }
}); });

View File

@@ -54,7 +54,6 @@ import '../pages/setting/style_setting.dart';
import '../pages/subscription/index.dart'; import '../pages/subscription/index.dart';
import '../pages/subscription_detail/index.dart'; import '../pages/subscription_detail/index.dart';
import '../pages/video/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/index.dart';
import '../pages/whisper_detail/index.dart'; import '../pages/whisper_detail/index.dart';
@@ -99,8 +98,8 @@ class Routes {
CustomGetPage(name: '/member', page: () => const MemberPageNew()), CustomGetPage(name: '/member', page: () => const MemberPageNew()),
CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()), CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()),
// 二级回复 // 二级回复
CustomGetPage( // CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()), // name: '/replyReply', page: () => const VideoReplyReplyPanel()),
// 推荐流设置 // 推荐流设置
CustomGetPage( CustomGetPage(
name: '/recommendSetting', page: () => const RecommendSetting()), name: '/recommendSetting', page: () => const RecommendSetting()),

View File

@@ -361,6 +361,9 @@ class GStorage {
static bool get showDmChart => static bool get showDmChart =>
GStorage.setting.get(SettingBoxKey.showDmChart, defaultValue: false); GStorage.setting.get(SettingBoxKey.showDmChart, defaultValue: false);
static bool get enableCommAntifraud => GStorage.setting
.get(SettingBoxKey.enableCommAntifraud, defaultValue: false);
static List<double> get dynamicDetailRatio => List<double>.from(setting static List<double> get dynamicDetailRatio => List<double>.from(setting
.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0])); .get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]));
@@ -591,6 +594,7 @@ class SettingBoxKey {
enableLivePhoto = 'enableLivePhoto', enableLivePhoto = 'enableLivePhoto',
showSeekPreview = 'showSeekPreview', showSeekPreview = 'showSeekPreview',
showDmChart = 'showDmChart', showDmChart = 'showDmChart',
enableCommAntifraud = 'enableCommAntifraud',
// Sponsor Block // Sponsor Block
enableSponsorBlock = 'enableSponsorBlock', enableSponsorBlock = 'enableSponsorBlock',