255 lines
7.9 KiB
Dart
255 lines
7.9 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
class SpamProtectionService extends ChangeNotifier {
|
|
// Vote limits per user
|
|
static const int maxVotesPerHour = 20;
|
|
static const int maxVotesPerMinute = 5;
|
|
static const int maxSuggestionsPerHour = 10;
|
|
static const int maxSuggestionsPerMinute = 3;
|
|
|
|
// Cooldown periods (in seconds)
|
|
static const int voteCooldown = 2;
|
|
static const int suggestionCooldown = 10;
|
|
|
|
// User activity tracking
|
|
final Map<String, List<DateTime>> _userVotes = {};
|
|
final Map<String, List<DateTime>> _userSuggestions = {};
|
|
final Map<String, DateTime> _lastVoteTime = {};
|
|
final Map<String, DateTime> _lastSuggestionTime = {};
|
|
final Map<String, int> _consecutiveActions = {};
|
|
|
|
// Blocked users (temporary)
|
|
final Map<String, DateTime> _blockedUsers = {};
|
|
|
|
String getCurrentUserId() {
|
|
// In a real app, this would come from authentication
|
|
return 'current_user_${DateTime.now().millisecondsSinceEpoch ~/ 1000000}';
|
|
}
|
|
|
|
// Check if user can vote
|
|
bool canVote(String userId) {
|
|
// Check if user is temporarily blocked
|
|
if (_isUserBlocked(userId)) {
|
|
return false;
|
|
}
|
|
|
|
// Allow first vote without cooldown for smooth user experience
|
|
final votes = _userVotes[userId] ?? [];
|
|
if (votes.isEmpty) {
|
|
return true; // First vote is always allowed
|
|
}
|
|
|
|
// Check cooldown for subsequent votes
|
|
if (_isOnCooldown(userId, _lastVoteTime, voteCooldown)) {
|
|
return false;
|
|
}
|
|
|
|
// Check rate limits
|
|
if (!_checkRateLimit(userId, _userVotes, maxVotesPerMinute, maxVotesPerHour)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Check if user can suggest songs
|
|
bool canSuggest(String userId) {
|
|
// Check if user is temporarily blocked
|
|
if (_isUserBlocked(userId)) {
|
|
return false;
|
|
}
|
|
|
|
// Allow first suggestion without cooldown for smooth user experience
|
|
final suggestions = _userSuggestions[userId] ?? [];
|
|
if (suggestions.isEmpty) {
|
|
return true; // First suggestion is always allowed
|
|
}
|
|
|
|
// Check cooldown for subsequent suggestions
|
|
if (_isOnCooldown(userId, _lastSuggestionTime, suggestionCooldown)) {
|
|
return false;
|
|
}
|
|
|
|
// Check rate limits
|
|
if (!_checkRateLimit(userId, _userSuggestions, maxSuggestionsPerMinute, maxSuggestionsPerHour)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Record a vote action
|
|
void recordVote(String userId) {
|
|
final now = DateTime.now();
|
|
|
|
// Add to vote history
|
|
_userVotes.putIfAbsent(userId, () => []).add(now);
|
|
_lastVoteTime[userId] = now;
|
|
|
|
// Track consecutive actions for spam detection
|
|
_incrementConsecutiveActions(userId);
|
|
|
|
// Clean old entries
|
|
_cleanOldEntries(_userVotes[userId]!);
|
|
|
|
// Check for suspicious behavior
|
|
_checkForSpam(userId);
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
// Record a suggestion action
|
|
void recordSuggestion(String userId) {
|
|
final now = DateTime.now();
|
|
|
|
// Add to suggestion history
|
|
_userSuggestions.putIfAbsent(userId, () => []).add(now);
|
|
_lastSuggestionTime[userId] = now;
|
|
|
|
// Track consecutive actions for spam detection
|
|
_incrementConsecutiveActions(userId);
|
|
|
|
// Clean old entries
|
|
_cleanOldEntries(_userSuggestions[userId]!);
|
|
|
|
// Check for suspicious behavior
|
|
_checkForSpam(userId);
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
// Get remaining cooldown time in seconds
|
|
int getVoteCooldownRemaining(String userId) {
|
|
final lastVote = _lastVoteTime[userId];
|
|
if (lastVote == null) return 0;
|
|
|
|
final elapsed = DateTime.now().difference(lastVote).inSeconds;
|
|
return (voteCooldown - elapsed).clamp(0, voteCooldown);
|
|
}
|
|
|
|
int getSuggestionCooldownRemaining(String userId) {
|
|
final lastSuggestion = _lastSuggestionTime[userId];
|
|
if (lastSuggestion == null) return 0;
|
|
|
|
final elapsed = DateTime.now().difference(lastSuggestion).inSeconds;
|
|
return (suggestionCooldown - elapsed).clamp(0, suggestionCooldown);
|
|
}
|
|
|
|
// Get user activity stats
|
|
Map<String, int> getUserStats(String userId) {
|
|
final now = DateTime.now();
|
|
final hourAgo = now.subtract(const Duration(hours: 1));
|
|
final minuteAgo = now.subtract(const Duration(minutes: 1));
|
|
|
|
final votesThisHour = _userVotes[userId]?.where((time) => time.isAfter(hourAgo)).length ?? 0;
|
|
final votesThisMinute = _userVotes[userId]?.where((time) => time.isAfter(minuteAgo)).length ?? 0;
|
|
final suggestionsThisHour = _userSuggestions[userId]?.where((time) => time.isAfter(hourAgo)).length ?? 0;
|
|
final suggestionsThisMinute = _userSuggestions[userId]?.where((time) => time.isAfter(minuteAgo)).length ?? 0;
|
|
|
|
return {
|
|
'votesThisHour': votesThisHour,
|
|
'votesThisMinute': votesThisMinute,
|
|
'suggestionsThisHour': suggestionsThisHour,
|
|
'suggestionsThisMinute': suggestionsThisMinute,
|
|
'maxVotesPerHour': maxVotesPerHour,
|
|
'maxVotesPerMinute': maxVotesPerMinute,
|
|
'maxSuggestionsPerHour': maxSuggestionsPerHour,
|
|
'maxSuggestionsPerMinute': maxSuggestionsPerMinute,
|
|
};
|
|
}
|
|
|
|
// Check if user is blocked
|
|
bool _isUserBlocked(String userId) {
|
|
final blockTime = _blockedUsers[userId];
|
|
if (blockTime == null) return false;
|
|
|
|
// Unblock after 5 minutes
|
|
if (DateTime.now().difference(blockTime).inMinutes >= 5) {
|
|
_blockedUsers.remove(userId);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Check cooldown
|
|
bool _isOnCooldown(String userId, Map<String, DateTime> lastActionTime, int cooldownSeconds) {
|
|
final lastAction = lastActionTime[userId];
|
|
if (lastAction == null) return false;
|
|
|
|
return DateTime.now().difference(lastAction).inSeconds < cooldownSeconds;
|
|
}
|
|
|
|
// Check rate limits
|
|
bool _checkRateLimit(String userId, Map<String, List<DateTime>> userActions, int maxPerMinute, int maxPerHour) {
|
|
final actions = userActions[userId] ?? [];
|
|
final now = DateTime.now();
|
|
|
|
// Count actions in the last minute
|
|
final actionsLastMinute = actions.where((time) =>
|
|
now.difference(time).inMinutes < 1).length;
|
|
if (actionsLastMinute >= maxPerMinute) return false;
|
|
|
|
// Count actions in the last hour
|
|
final actionsLastHour = actions.where((time) =>
|
|
now.difference(time).inHours < 1).length;
|
|
if (actionsLastHour >= maxPerHour) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Clean old entries (older than 1 hour)
|
|
void _cleanOldEntries(List<DateTime> entries) {
|
|
final oneHourAgo = DateTime.now().subtract(const Duration(hours: 1));
|
|
entries.removeWhere((time) => time.isBefore(oneHourAgo));
|
|
}
|
|
|
|
// Track consecutive actions for spam detection
|
|
void _incrementConsecutiveActions(String userId) {
|
|
_consecutiveActions[userId] = (_consecutiveActions[userId] ?? 0) + 1;
|
|
|
|
// Reset after some time of inactivity
|
|
Timer(const Duration(seconds: 30), () {
|
|
_consecutiveActions[userId] = 0;
|
|
});
|
|
}
|
|
|
|
// Check for spam behavior and block if necessary
|
|
void _checkForSpam(String userId) {
|
|
final consecutive = _consecutiveActions[userId] ?? 0;
|
|
|
|
// Block user if too many consecutive actions
|
|
if (consecutive > 15) {
|
|
_blockedUsers[userId] = DateTime.now();
|
|
if (kDebugMode) {
|
|
print('User $userId temporarily blocked for spam behavior');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get block status message
|
|
String? getBlockMessage(String userId) {
|
|
final blockTime = _blockedUsers[userId];
|
|
if (blockTime == null) return null;
|
|
|
|
final remaining = 5 - DateTime.now().difference(blockTime).inMinutes;
|
|
if (remaining <= 0) {
|
|
_blockedUsers.remove(userId);
|
|
return null;
|
|
}
|
|
|
|
return 'You are temporarily blocked for $remaining more minutes due to excessive activity.';
|
|
}
|
|
|
|
// Clear all data (for testing)
|
|
void clearAllData() {
|
|
_userVotes.clear();
|
|
_userSuggestions.clear();
|
|
_lastVoteTime.clear();
|
|
_lastSuggestionTime.clear();
|
|
_consecutiveActions.clear();
|
|
_blockedUsers.clear();
|
|
notifyListeners();
|
|
}
|
|
}
|