Skip to content

Commit

Permalink
feat: add FlaggedUser.AgeRanges to flag users based on birthday
Browse files Browse the repository at this point in the history
  • Loading branch information
warriordog committed Aug 10, 2024
1 parent 5f72f2d commit cb4fcbe
Show file tree
Hide file tree
Showing 8 changed files with 514 additions and 6 deletions.
154 changes: 154 additions & 0 deletions ModShark.Tests/Utils/AgeRangeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using FluentAssertions;
using ModShark.Utils;

namespace ModShark.Tests.Utils;

public class AgeRangeTests
{
[Test]
public void GetStartDate_ShouldReturnStartDate_WhenSet()
{
var birthday = new DateTime(2024, 1, 1);
var start = new Age(1, 2, 3);
var expected = start.ToDate(birthday);

var range = new AgeRange(start, null);
var actual = range.GetStartDate(birthday);

actual.Should().Be(expected);
}

[Test]
public void GetStartDate_ShouldReturnMinDate_WhenNotSet()
{
var birthday = new DateTime(2024, 1, 1);

var range = new AgeRange(null, null);
var actual = range.GetStartDate(birthday);

actual.Should().Be(DateTime.MinValue);
}

[Test]
public void GetEndDate_ShouldReturnEndDate_WhenSet()
{
var birthday = new DateTime(2024, 1, 1);
var end = new Age(1, 2, 3);
var expected = end.ToDate(birthday);

var range = new AgeRange(null, end);
var actual = range.GetEndDate(birthday);

actual.Should().Be(expected);
}

[Test]
public void GetEndDate_ShouldReturnMaxDate_WhenNotSet()
{
var birthday = new DateTime(2024, 1, 1);

var range = new AgeRange(null, null);
var actual = range.GetEndDate(birthday);

actual.Should().Be(DateTime.MaxValue);
}

[TestCase("1y2m3d - 4y5m6d", 1, 2, 3, 4, 5, 6)]
[TestCase("1y - 2y", 1, 0, 0, 2, 0, 0)]
[TestCase("1m - 1y", 0, 1, 0, 1, 0, 0)]
[TestCase("123y - 456y", 123, 0, 0, 456, 0, 0)]
[TestCase(" 1y - 3y ", 1, 0, 0, 3, 0, 0)]
public void Parse_ShouldParseStandardInputs(string input, int y1, int m1, int d1, int y2, int m2, int d2)
{
var start = new Age(y1, m1, d1);
var end = new Age(y2, m2, d2);
var expected = new AgeRange(start, end);

var actual = AgeRange.Parse(input);

actual.Should().Be(expected);
}

[Test]
public void Parse_ShouldAllowEndToBeExcluded()
{
var start = new Age(1, 0, 0);

var actual = AgeRange.Parse("1y");

actual.Start.Should().Be(start);
actual.End.Should().BeNull();
}

[TestCase("")]
[TestCase(" ")]
[TestCase("1z")]
[TestCase("Xy")]
[TestCase("1y2m3d4h")]
[TestCase("-1y")]
[TestCase("1 z")]
[TestCase("1y 2m 3d")]
public void Parse_ShouldThrow_WhenInputIsInvalid(string input)
{
Assert.Throws<ArgumentException>(() =>
{
Age.Parse(input);
});
}

[TestCase(true, 18, 0, 0)]
[TestCase(true, 19, 0, 0)]
[TestCase(true, 18, 1, 0)]
[TestCase(true, 18, 0, 1)]
[TestCase(false, 25, 0, 0)]
[TestCase(false, 17, 11, 30)]
[TestCase(false, 17, 0, 30)]
[TestCase(false, 17, 11, 0)]
[TestCase(false, 17, 1, 1)]
[TestCase(false, 26, 0, 0)]
[TestCase(false, 25, 1, 0)]
[TestCase(false, 25, 0, 1)]
public void IsInRange_Age_ShouldReturnTrueWhenAgeIsInRange(bool expected, int y, int m, int d)
{
var range = new AgeRange(new Age(18, 0, 0), new Age(25, 0, 0));
var age = new Age(y, m, d);

var actual = range.IsInRange(age);

actual.Should().Be(expected);
}

[TestCase(true, 18, 0, 0)]
[TestCase(true, 19, 0, 0)]
[TestCase(true, 18, 1, 0)]
[TestCase(true, 18, 0, 1)]
[TestCase(false, 25, 0, 0)]
[TestCase(false, 17, 11, 30)]
[TestCase(false, 17, 0, 30)]
[TestCase(false, 17, 11, 0)]
[TestCase(false, 17, 1, 1)]
[TestCase(false, 26, 0, 0)]
[TestCase(false, 25, 1, 0)]
[TestCase(false, 25, 0, 1)]
public void IsInRange_Date_ShouldReturnTrueWhenInRange(bool expected, int y, int m, int d)
{
var range = new AgeRange(new Age(18, 0, 0), new Age(25, 0, 0));
var birthday = new DateTime(2000, 1, 1);
var today = birthday.AddYears(y).AddMonths(m).AddDays(d);

var actual = range.IsInRange(birthday, today);

actual.Should().Be(expected);
}

[Test]
public void IsInRange_Date_ShouldThrow_WhenBirthdayIsFuture()
{
var range = new AgeRange(null, null);

Assert.Throws<ArgumentOutOfRangeException>(() =>
{
range.IsInRange(new DateTime(2024, 12, 31), new DateTime(2024, 1, 1));
});
}
}
110 changes: 110 additions & 0 deletions ModShark.Tests/Utils/AgeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using FluentAssertions;
using ModShark.Utils;

namespace ModShark.Tests.Utils;

public class AgeTests
{
[Test]
public void Comparisons_ShouldRespectMemberPriority()
{
var first = new Age(0, 4, 5);
var second = new Age(1, 2, 3);
var third = new Age(1, 3, 3);
var fourth = new Age(1, 3, 4);
var fourth2 = new Age(1, 3, 4);

first.Should().BeLessThan(second);
second.Should().BeLessThan(third);
third.Should().BeLessThan(fourth);

fourth.Should().BeGreaterThan(third);
third.Should().BeGreaterThan(second);
second.Should().BeGreaterThan(first);

fourth2.Should().Be(fourth);
}

[Test]
public void ToDate_ShouldAddYears()
{
var birthday = new DateTime(2024, 8, 1);
var age = new Age(1);

var result = age.ToDate(birthday);

var expected = new DateTime(2025, 8, 1);
result.Should().Be(expected);
}

[Test]
public void ToDate_ShouldAddMonths()
{
var birthday = new DateTime(2024, 8, 1);
var age = new Age(0, 1);

var result = age.ToDate(birthday);

var expected = new DateTime(2024, 9, 1);
result.Should().Be(expected);
}

[Test]
public void ToDate_ShouldAddDays()
{
var birthday = new DateTime(2024, 8, 1);
var age = new Age(0, 0, 1);

var result = age.ToDate(birthday);

var expected = new DateTime(2024, 8, 2);
result.Should().Be(expected);
}

[Test]
public void ToDate_ShouldAddAllValues()
{
var birthday = new DateTime(2024, 8, 1);
var age = new Age(1, 2, 3);

var result = age.ToDate(birthday);

var expected = new DateTime(2025, 10, 4);
result.Should().Be(expected);
}

[TestCase("1y2m3d", 1, 2, 3)]
[TestCase("1y", 1, 0, 0)]
[TestCase("1m", 0, 1, 0)]
[TestCase("1d", 0, 0, 1)]
[TestCase("1y1m", 1, 1, 0)]
[TestCase("1m1d", 0, 1, 1)]
[TestCase("1y1d", 1, 0, 1)]
[TestCase("123y", 123, 0, 0)]
[TestCase("12y34m56d", 12, 34, 56)]
[TestCase(" 1y ", 1, 0, 0)]
public void Parse_ShouldParseValidInputs(string input, int years, int months, int days)
{
var expected = new Age(years, months, days);

var actual = Age.Parse(input);

actual.Should().Be(expected);
}

[TestCase("")]
[TestCase(" ")]
[TestCase("1z")]
[TestCase("Xy")]
[TestCase("1y2m3d4h")]
[TestCase("-1y")]
[TestCase("1 z")]
[TestCase("1y 2m 3d")]
public void Parse_ShouldThrow_WhenInputIsInvalid(string input)
{
Assert.Throws<ArgumentException>(() =>
{
Age.Parse(input);
});
}
}
55 changes: 50 additions & 5 deletions ModShark/Rules/FlaggedUserRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public class FlaggedUserConfig : QueuedRuleConfig

public bool IncludeBlockedInstance { get; set; }
public bool IncludeSilencedInstance { get; set; }


public List<string> AgeRanges { get; set; } = [];
public List<string> UsernamePatterns { get; set; } = [];
public List<string> DisplayNamePatterns { get; set; } = [];
public List<string> BioPatterns { get; set; } = [];
Expand All @@ -36,10 +37,12 @@ public class FlaggedUserRule(ILogger<FlaggedUserRule> logger, FlaggedUserConfig
private Regex UsernamePattern { get; } = PatternUtils.CreateMatcher(config.UsernamePatterns, config.Timeout, ignoreCase: true);
private Regex DisplayNamePattern { get; } = PatternUtils.CreateMatcher(config.DisplayNamePatterns, config.Timeout, ignoreCase: true);
private Regex BioPattern { get; } = PatternUtils.CreateMatcher(config.BioPatterns, config.Timeout, ignoreCase: true);

private List<AgeRange> AgeRanges { get; } = ParseAgeRanges(config.AgeRanges);

private bool HasUsernamePatterns => config.UsernamePatterns.Count > 0;
private bool HasDisplayNamePatterns => config.DisplayNamePatterns.Count > 0;
private bool HasBioPatterns => config.BioPatterns.Count > 0;
private bool HasAgeRanges => AgeRanges.Count > 0;

protected override Task<bool> CanRun(CancellationToken stoppingToken)
{
Expand Down Expand Up @@ -109,9 +112,7 @@ protected override async Task RunQueuedRule(Report report, int maxId, Cancellati
continue;
}

// For better use of database resources, we handle pattern matching in application code.
// This also gives us .NET's faster and more powerful regex engine.
if (!HasFlaggedUsername(user) && !HasFlaggedDisplayName(user) && !HasFlaggedBio(user))
if (!IsFlagged(user))
continue;

report.UserReports.Add(new UserReport
Expand All @@ -128,6 +129,45 @@ protected override async Task RunQueuedRule(Report report, int maxId, Cancellati
}
}

private bool IsFlagged(User user)
{
// We check for age first, since it's a faster comparison.
if (HasFlaggedAge(user))
return true;

// For better use of database resources, we handle pattern matching in application code.
// This also gives us .NET's faster and more powerful regex engine.
if (HasFlaggedUsername(user))
return true;
if (HasFlaggedDisplayName(user))
return true;
if (HasFlaggedBio(user))
return true;

return false;
}

private bool HasFlaggedAge(User user)
{
if (!HasAgeRanges)
return false;

if (!user.HasProfile)
return false;

if (!user.Profile.HasBirthday)
return false;

var today = DateTime.Now;
var birthday = user.Profile.Birthday.Value;
if (birthday > today)
return false;

return AgeRanges.Any(r =>
r.IsInRange(birthday, today)
);
}

private bool HasFlaggedUsername(User user)
{
if (!HasUsernamePatterns)
Expand Down Expand Up @@ -160,4 +200,9 @@ private bool HasFlaggedBio(User user)

return BioPattern.IsMatch(user.Profile.Description);
}

private static List<AgeRange> ParseAgeRanges(IEnumerable<string> rangePatterns) =>
rangePatterns
.Select(AgeRange.Parse)
.ToList();
}
Loading

0 comments on commit cb4fcbe

Please sign in to comment.