Compare commits

..

No commits in common. "69a02e0e15a367800db85d70d2ee1660190b8536" and "7246c982df991331ee5eafca0957848b6fa92675" have entirely different histories.

11 changed files with 86 additions and 190 deletions

View file

@ -2,22 +2,6 @@
*a,c,f,r,o* *a,c,f,r,o*
## [6.0.14]
### Added
- Added `.notify <channel> nicecatch <message>` event
- It will show all rare fish/trash and all max star fish caught on any server
- You can use `.notifyphs nicecatch` to see the list of placeholders you can use while setting a message
- Example: `.notify #fishfeed nicecatch %user% just caught a %event.fish.stars% %event.fish.name% %event.fish.emoji%`
### Changed
- .notify commands now require Manage Messages permission
- .notify will now let you know if you can't set a notify message due to a missing channel
### Fixed
- Fixed `.antispamignore` restart persistence
- Fixed `.notify` events. Only levelup used to work
## [6.0.13] - 23.03.2025 ## [6.0.13] - 23.03.2025
### Added ### Added

View file

@ -21,6 +21,5 @@ public enum NotifyType
Protection = 1, Prot = 1, Protection = 1, Prot = 1,
AddRoleReward = 2, AddRoleReward = 2,
RemoveRoleReward = 3, RemoveRoleReward = 3,
NiceCatch = 4,
// BigWin = 4, // BigWin = 4,
} }

View file

@ -1,42 +0,0 @@
#nullable disable
using EllieBot.Db.Models;
using EllieBot.Modules.Games;
namespace EllieBot.Modules.Administration.Services;
public record struct NiceCatchNotifyModel(
ulong UserId,
FishData Fish,
string Stars
) : INotifyModel<NiceCatchNotifyModel>
{
public static string KeyName
=> "notify.nicecatch";
public static NotifyType NotifyType
=> NotifyType.NiceCatch;
public const string PH_EMOJI = "fish.emoji";
public const string PH_IMAGE = "fish.image";
public const string PH_NAME = "fish.name";
public const string PH_STARS = "fish.stars";
public const string PH_FLUFF = "fish.fluff";
public bool TryGetUserId(out ulong userId)
{
userId = UserId;
return true;
}
public static IReadOnlyList<NotifyModelPlaceholderData<NiceCatchNotifyModel>> GetReplacements()
{
return
[
new(PH_EMOJI, static (data, _) => data.Fish.Emoji),
new(PH_IMAGE, static (data, _) => data.Fish.Image),
new(PH_NAME, static (data, _) => data.Fish.Name),
new(PH_STARS, static (data, _) => data.Stars),
new(PH_FLUFF, static (data, _) => data.Fish.Fluff),
];
}
}

View file

@ -8,7 +8,7 @@ public partial class Administration
public class NotifyCommands : EllieModule<NotifyService> public class NotifyCommands : EllieModule<NotifyService>
{ {
[Cmd] [Cmd]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.Administrator)]
public async Task Notify() public async Task Notify()
{ {
await Response() await Response()
@ -38,12 +38,11 @@ public partial class Administration
NotifyType.Protection => strs.notify_desc_protection, NotifyType.Protection => strs.notify_desc_protection,
NotifyType.AddRoleReward => strs.notify_desc_addrolerew, NotifyType.AddRoleReward => strs.notify_desc_addrolerew,
NotifyType.RemoveRoleReward => strs.notify_desc_removerolerew, NotifyType.RemoveRoleReward => strs.notify_desc_removerolerew,
NotifyType.NiceCatch => strs.notify_desc_nicecatch,
_ => strs.notify_desc_not_found _ => strs.notify_desc_not_found
}; };
[Cmd] [Cmd]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.Administrator)]
public async Task Notify(NotifyType nType) public async Task Notify(NotifyType nType)
{ {
// show msg // show msg
@ -77,12 +76,12 @@ public partial class Administration
} }
[Cmd] [Cmd]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.Administrator)]
public async Task Notify(NotifyType nType, [Leftover] string message) public async Task Notify(NotifyType nType, [Leftover] string message)
=> await NotifyInternalAsync(nType, null, message); => await NotifyInternalAsync(nType, null, message);
[Cmd] [Cmd]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.Administrator)]
public async Task Notify(NotifyType nType, IMessageChannel channel, [Leftover] string message) public async Task Notify(NotifyType nType, IMessageChannel channel, [Leftover] string message)
=> await NotifyInternalAsync(nType, channel, message); => await NotifyInternalAsync(nType, channel, message);
@ -90,14 +89,6 @@ public partial class Administration
{ {
var result = await _service.EnableAsync(ctx.Guild.Id, channel?.Id, nType, message); var result = await _service.EnableAsync(ctx.Guild.Id, channel?.Id, nType, message);
if(!result)
{
await Response()
.Error(strs.notify_cant_set)
.SendAsync();
return;
}
var outChannel = channel is null ? "origin" : $"<#{channel.Id}>"; var outChannel = channel is null ? "origin" : $"<#{channel.Id}>";
await Response() await Response()
.Confirm(strs.notify_on(outChannel, Format.Bold(nType.ToString()))) .Confirm(strs.notify_on(outChannel, Format.Bold(nType.ToString())))
@ -105,7 +96,7 @@ public partial class Administration
} }
[Cmd] [Cmd]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.Administrator)]
public async Task NotifyPhs(NotifyType nType) public async Task NotifyPhs(NotifyType nType)
{ {
var data = _service.GetRegisteredModel(nType); var data = _service.GetRegisteredModel(nType);
@ -120,7 +111,7 @@ public partial class Administration
} }
[Cmd] [Cmd]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.Administrator)]
public async Task NotifyList(int page = 1) public async Task NotifyList(int page = 1)
{ {
if (--page < 0) if (--page < 0)
@ -148,7 +139,7 @@ public partial class Administration
} }
[Cmd] [Cmd]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.Administrator)]
public async Task NotifyClear(NotifyType nType) public async Task NotifyClear(NotifyType nType)
{ {
await _service.DisableAsync(ctx.Guild.Id, nType); await _service.DisableAsync(ctx.Guild.Id, nType);

View file

@ -42,11 +42,12 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
RegisterModel<ProtectionNotifyModel>(); RegisterModel<ProtectionNotifyModel>();
RegisterModel<AddRoleRewardNotifyModel>(); RegisterModel<AddRoleRewardNotifyModel>();
RegisterModel<RemoveRoleRewardNotifyModel>(); RegisterModel<RemoveRoleRewardNotifyModel>();
RegisterModel<NiceCatchNotifyModel>();
} }
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
RegisterModels();
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
_events = (await uow.GetTable<Notify>() _events = (await uow.GetTable<Notify>()
.Where(x => Queries.GuildOnShard(x.GuildId, .Where(x => Queries.GuildOnShard(x.GuildId,
@ -56,8 +57,9 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
.GroupBy(x => x.Type) .GroupBy(x => x.Type)
.ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent()) .ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent())
.ToConcurrent(); .ToConcurrent();
RegisterModels();
await SubscribeToEvent<LevelUpNotifyModel>();
} }
private async Task SubscribeToEvent<T>() private async Task SubscribeToEvent<T>()
@ -73,7 +75,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
{ {
if (isShardLocal) if (isShardLocal)
{ {
_ = Task.Run(async () => await OnEvent(data)); await OnEvent(data);
return; return;
} }
@ -281,10 +283,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
var data = new NotifyModelData(T.NotifyType, var data = new NotifyModelData(T.NotifyType,
T.SupportsOriginTarget, T.SupportsOriginTarget,
T.GetReplacements().Map(x => x.Name)); T.GetReplacements().Map(x => x.Name));
_models[T.NotifyType] = data; _models[T.NotifyType] = data;
_pubSub.Sub<T>(new(T.KeyName), async (data) => await OnEvent(data));
} }
public NotifyModelData GetRegisteredModel(NotifyType nType) public NotifyModelData GetRegisteredModel(NotifyType nType)

View file

@ -391,7 +391,7 @@ public class ProtectionService : IReadyExecutor, IEService
{ {
var obj = new AntiSpamIgnore var obj = new AntiSpamIgnore
{ {
ChannelId = channelId, ChannelId = channelId
}; };
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
@ -529,18 +529,16 @@ public class ProtectionService : IReadyExecutor, IEService
}; };
} }
var spamConfigs = await uow.Set<AntiSpamSetting>() var spamConfigs = await uow.GetTable<AntiSpamSetting>()
.AsNoTracking() .AsNoTracking()
.Where(x => gids.Contains(x.GuildId)) .Where(x => Queries.GuildOnShard(x.GuildId, _shardData.TotalShards, _shardData.ShardId))
.Include(x => x.IgnoredChannels) .ToListAsyncLinqToDB();
.ToListAsyncEF();
foreach (var config in spamConfigs) foreach (var config in spamConfigs)
{ {
_antiSpamGuilds[config.GuildId] = new() _antiSpamGuilds[config.GuildId] = new()
{ {
AntiSpamSettings = config, AntiSpamSettings = config,
UserStats = new()
}; };
} }

View file

@ -105,7 +105,7 @@ public partial class Games
.WithOkColor() .WithOkColor()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithDescription(desc) .WithDescription(desc)
.AddField(GetText(strs.fish_quality), fs.GetStarText(res.Stars, res.Fish.Stars), true) .AddField(GetText(strs.fish_quality), GetStarText(res.Stars, res.Fish.Stars), true)
.AddField(GetText(strs.desc), res.Fish.Fluff, true) .AddField(GetText(strs.desc), res.Fish.Fluff, true)
.WithThumbnailUrl(res.Fish.Image)) .WithThumbnailUrl(res.Fish.Image))
.SendAsync(); .SendAsync();
@ -150,7 +150,7 @@ public partial class Games
.Items(fishes) .Items(fishes)
.PageSize(9) .PageSize(9)
.CurrentPage(page) .CurrentPage(page)
.Page((fishes, i) => .Page((fs, i) =>
{ {
var eb = CreateEmbed() var eb = CreateEmbed()
.WithDescription($"🧠 **Skill:** {skill} / {maxSkill}") .WithDescription($"🧠 **Skill:** {skill} / {maxSkill}")
@ -158,7 +158,7 @@ public partial class Games
.WithTitle(GetText(strs.fish_list_title)) .WithTitle(GetText(strs.fish_list_title))
.WithOkColor(); .WithOkColor();
foreach (var f in fishes) foreach (var f in fs)
{ {
if (catchDict.TryGetValue(f.Id, out var c)) if (catchDict.TryGetValue(f.Id, out var c))
{ {
@ -169,14 +169,14 @@ public partial class Games
+ GetTodEmoji(f.Time) + GetTodEmoji(f.Time)
+ GetWeatherEmoji(f.Weather) + GetWeatherEmoji(f.Weather)
+ "\n" + "\n"
+ fs.GetStarText(c.MaxStars, f.Stars) + GetStarText(c.MaxStars, f.Stars)
+ "\n" + "\n"
+ Format.Italics(f.Fluff), + Format.Italics(f.Fluff),
true); true);
} }
else else
{ {
eb.AddField("?", GetFishEmoji(null, 0) + "\n" + fs.GetStarText(0, f.Stars), true); eb.AddField("?", GetFishEmoji(null, 0) + "\n" + GetStarText(0, f.Stars), true);
} }
} }
@ -225,8 +225,31 @@ public partial class Games
_ => "" _ => ""
}; };
private string GetStarText(int resStars, int fishStars)
{
if (resStars == fishStars)
{
return MultiplyStars(fcs.Data.StarEmojis[^1], fishStars);
}
var c = fcs.Data;
var starsp1 = MultiplyStars(c.StarEmojis[resStars], resStars);
var starsp2 = MultiplyStars(c.StarEmojis[0], fishStars - resStars);
return starsp1 + starsp2;
}
private string MultiplyStars(string starEmoji, int count)
{
var sb = new StringBuilder();
for (var i = 0; i < count; i++)
{
sb.Append(starEmoji);
}
return sb.ToString();
}
} }
} }

View file

@ -7,12 +7,6 @@ public sealed class FishResult
public bool IsSkillUp { get; set; } public bool IsSkillUp { get; set; }
public int Skill { get; set; } public int Skill { get; set; }
public int MaxSkill { get; set; } public int MaxSkill { get; set; }
public bool IsMaxStar()
=> Stars == Fish.Stars;
public bool IsRare()
=> Fish.Chance <= 15;
} }
public readonly record struct AlreadyFishing; public readonly record struct AlreadyFishing;

View file

@ -1,19 +1,10 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Modules.Administration;
using EllieBot.Modules.Administration.Services;
namespace EllieBot.Modules.Games.Fish; namespace EllieBot.Modules.Games.Fish;
public sealed class FishService( public sealed class FishService(FishConfigService fcs, IBotCache cache, DbService db) : IEService
FishConfigService fcs,
IBotCache cache,
DbService db,
INotifySubscriber notify
)
: IEService
{ {
private const double MAX_SKILL = 100; private const double MAX_SKILL = 100;
@ -24,7 +15,7 @@ public sealed class FishService(
public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId) public async Task<OneOf.OneOf<Task<FishResult?>, AlreadyFishing>> FishAsync(ulong userId, ulong channelId)
{ {
var duration = _rng.Next(3, 6); var duration = _rng.Next(5, 9);
if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false)) if (!await cache.AddAsync(FishingKey(userId), true, TimeSpan.FromSeconds(duration), overwrite: false))
{ {
@ -78,18 +69,6 @@ public sealed class FishService(
} }
} }
// notification system
if (result is not null)
{
if (result.IsMaxStar() || result.IsRare())
{
await notify.NotifyAsync(new NiceCatchNotifyModel(
userId,
result.Fish,
GetStarText(result.Stars, result.Fish.Stars)
));
}
}
return result; return result;
} }
@ -106,21 +85,21 @@ public sealed class FishService(
var maxSkill = (int)MAX_SKILL; var maxSkill = (int)MAX_SKILL;
await ctx.GetTable<UserFishStats>() await ctx.GetTable<UserFishStats>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
UserId = userId, UserId = userId,
Skill = 1, Skill = 1,
}, },
(old) => new() (old) => new()
{ {
UserId = userId, UserId = userId,
Skill = old.Skill > maxSkill ? maxSkill : old.Skill + 1 Skill = old.Skill > maxSkill ? maxSkill : old.Skill + 1
}, },
() => new() () => new()
{ {
UserId = userId, UserId = userId,
Skill = playerSkill Skill = playerSkill
}); });
return true; return true;
} }
@ -144,9 +123,9 @@ public sealed class FishService(
await using var ctx = db.GetDbContext(); await using var ctx = db.GetDbContext();
var skill = await ctx.GetTable<UserFishStats>() var skill = await ctx.GetTable<UserFishStats>()
.Where(x => x.UserId == userId) .Where(x => x.UserId == userId)
.Select(x => x.Skill) .Select(x => x.Skill)
.FirstOrDefaultAsyncLinqToDB(); .FirstOrDefaultAsyncLinqToDB();
return (skill, (int)MAX_SKILL); return (skill, (int)MAX_SKILL);
} }
@ -209,23 +188,23 @@ public sealed class FishService(
await using var uow = db.GetDbContext(); await using var uow = db.GetDbContext();
await uow.GetTable<FishCatch>() await uow.GetTable<FishCatch>()
.InsertOrUpdateAsync(() => new FishCatch() .InsertOrUpdateAsync(() => new FishCatch()
{ {
UserId = userId, UserId = userId,
FishId = caught.Fish.Id, FishId = caught.Fish.Id,
MaxStars = caught.Stars, MaxStars = caught.Stars,
Count = 1 Count = 1
}, },
(old) => new FishCatch() (old) => new FishCatch()
{ {
Count = old.Count + 1, Count = old.Count + 1,
MaxStars = Math.Max(old.MaxStars, caught.Stars), MaxStars = Math.Max(old.MaxStars, caught.Stars),
}, },
() => new() () => new()
{ {
FishId = caught.Fish.Id, FishId = caught.Fish.Id,
UserId = userId UserId = userId
}); });
return caught; return caught;
} }
@ -413,35 +392,9 @@ public sealed class FishService(
await using var ctx = db.GetDbContext(); await using var ctx = db.GetDbContext();
var catches = await ctx.GetTable<FishCatch>() var catches = await ctx.GetTable<FishCatch>()
.Where(x => x.UserId == userId) .Where(x => x.UserId == userId)
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
return catches; return catches;
} }
public string GetStarText(int resStars, int fishStars)
{
if (resStars == fishStars)
{
return MultiplyStars(fcs.Data.StarEmojis[^1], fishStars);
}
var c = fcs.Data;
var starsp1 = MultiplyStars(c.StarEmojis[resStars], resStars);
var starsp2 = MultiplyStars(c.StarEmojis[0], fishStars - resStars);
return starsp1 + starsp2;
}
private string MultiplyStars(string starEmoji, int count)
{
var sb = new StringBuilder();
for (var i = 0; i < count; i++)
{
sb.Append(starEmoji);
}
return sb.ToString();
}
} }

View file

@ -88,7 +88,6 @@ public partial class Utility : EllieModule
.Text(message) .Text(message)
.Channel(channel) .Channel(channel)
.UserBasedMentions() .UserBasedMentions()
.NoReply()
.SendAsync(); .SendAsync();
} }

View file

@ -1154,7 +1154,6 @@
"notify_desc_protection": "Triggers when antialt, antispam or antiraid is triggered.", "notify_desc_protection": "Triggers when antialt, antispam or antiraid is triggered.",
"notify_desc_addrolerew": "Triggers when a user gets a role as a reward for reaching a level (xprew).", "notify_desc_addrolerew": "Triggers when a user gets a role as a reward for reaching a level (xprew).",
"notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).", "notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).",
"notify_desc_nicecatch": "Triggers when a user catches quality fish.",
"notify_desc_not_found": "No description found for this notify event. Please report this.", "notify_desc_not_found": "No description found for this notify event. Please report this.",
"notify_placeholders": "Placeholders for '{0}' notify event", "notify_placeholders": "Placeholders for '{0}' notify event",
"winlb": "Biggest Wins Leaderboard", "winlb": "Biggest Wins Leaderboard",
@ -1239,6 +1238,5 @@
"linkfix_list_none": "No link fixes have been configured for this server.", "linkfix_list_none": "No link fixes have been configured for this server.",
"linkfix_list_title": "Link Fixes", "linkfix_list_title": "Link Fixes",
"linkfix_removed": "Link fix for {0} has been removed.", "linkfix_removed": "Link fix for {0} has been removed.",
"linkfix_not_found": "No link fix found for {0}.", "linkfix_not_found": "No link fix found for {0}."
"notify_cant_set": "This event doesn't support origin channel, Please specify a channel"
} }