Compare commits

...

55 commits
5.1.20 ... v5

Author SHA1 Message Date
c574956d94
adding a role to a sar group which already exists in another group will simply move it, instead of reporting success but not doing anything 2024-12-27 20:50:11 +13:00
2bf2d4465d
postgresql will skip the sar migration, users will have to re-do them 2024-12-27 20:46:50 +13:00
2a3fe1f45c
Updated release url with the new repo url 2024-12-22 19:52:47 +13:00
6841c3969a
fixed iam with exclusive roles (in some cases?) being broken 2024-12-20 23:44:49 +13:00
5d4e730f35
updated changelog, updated version
.notify commands are no longer owner only, they now require Admin permissions
.notify messages can now mention anyone
2024-12-16 17:47:04 +13:00
29822cd0cb
.banner fixed, will now show guild banner if available, otherwise global banner, if available 2024-12-15 20:34:04 +13:00
75f5cfde29
.dmcmd will now correctly block commands in dms, not globally
timely will no longer require guild context, as dmcmd .timely will do the same thing
updated changelog, updated version
2024-12-13 22:51:14 +13:00
756555a061
bannersize fixed, honeypot will now put 'Honeypot' as a ban reason 2024-12-13 22:35:53 +13:00
e05792639c
.sclr will now correctly show the color without alpha prefixed 2024-12-12 19:43:33 +13:00
4283a939b7
.banner will now show a 'no banner' error, and will use image embed instead of thumbnail to show the banner. Only user server banners will be shown for now, global user banners will still not work. 2024-12-12 19:40:14 +13:00
a7529496ce
winlb now has 9 items per page to look not broken 2024-12-12 19:33:44 +13:00
a943a2a4ec
winlb now has a title 2024-12-12 19:29:53 +13:00
afc1541865
added role icon to .inrole, .winlb will now show userids when user can't be found 2024-12-11 12:35:17 +13:00
4910e53854
winlb embed fields are now inline to use less space 2024-12-11 01:25:16 +13:00
46ac468b1d
Updated commandlist 2024-12-10 22:40:21 +13:00
b03b32436b
.translate will now use 2 embeds 2024-12-10 22:32:08 +13:00
b3714d3c9e
.sar ex had an outdated description 2024-12-10 22:27:41 +13:00
688c1572f8
.winlb looks better when there are no items 2024-12-10 22:23:12 +13:00
d743bd563b
I really hate database migrations
Also fixed tests
2024-12-08 20:20:50 +13:00
e1cc500a3a
Updated changelog.md 2024-12-08 19:49:08 +13:00
29bac7739d
added addrolereward and removerolereward events for .notify
added .notify with no params showing events with descriptions
added .winlb
updated discord.net, redid migrations
2024-12-08 19:37:22 +13:00
f8eb585093
Added .notify and migrations, added levelup and protection events for notify, removed xpnotify completely 2024-12-08 18:51:31 +13:00
6bc55cd97f
notify, minesweeper, migrations
renames, refactors
remind optimized wait
2024-12-08 17:07:17 +13:00
204db02cd9
Queueing a song after the queue is finished will restart the playback 2024-12-08 15:53:59 +13:00
dc9ec2dafe
Added .dmmod and .dmcmd to disable modules and commands in bot DMs 2024-12-08 15:45:08 +13:00
4a723b7c1c
Added .xplevelset
removed awardedxp from database.
.sclr show will now show hex
.awardxp will now add directly to user's real xp
2024-12-08 15:27:28 +13:00
dfd5b7a823
.setstream and .setactivity will now pause .ropl 2024-12-01 00:43:53 +13:00
a21c7d2ab8
Finished giveaway will now reply to the giveaway message and ping a winner 2024-11-29 23:09:46 +13:00
04a69b9ddd
fixed giveaway load broken in the last patch 2024-11-29 23:06:51 +13:00
9c58465959
added ending date for givaway as a timestamp tag
fixed an issue with flag translates
2024-11-29 22:49:19 +13:00
81064efb57
Fixed a build warning in SarGroup.cs
This was actually meant to be pushed two versions ago
2024-11-29 20:23:05 +13:00
57d32184ff
fixed .sclr again, again, and fixed .iamn 2024-11-29 20:12:01 +13:00
cd1c461690
fixed .iam
fixed .sclr not being respected on many different commands
.rps now also has the amount bet
2024-11-29 19:22:10 +13:00
ae54270d58
Updated changelog 2024-11-29 18:41:39 +13:00
cbe1f49083
fixed .sclr again 2024-11-29 18:14:24 +13:00
786646218c
Fixed many issues with 5.2.0 2024-11-29 18:12:21 +13:00
861b1251a5
fixed .sclr again 2024-11-29 17:57:33 +13:00
73f2312a1c
updated changelog 2024-11-28 23:48:36 +13:00
6be8403ac6
fixed sar migration 2024-11-28 23:20:37 +13:00
8cab4094d2
updated commandlist 2024-11-28 20:09:41 +13:00
ef03c6c3fe
MessageXpCooldown is not in seconds 2024-11-28 20:05:33 +13:00
e127133612
.gc will now remove previous plants with the same pw length from the channel and add them to itself. This is to avoid missed gcs. The amount text will be wrong however, as it will only show how much flowers spawned now. The user will get full amount of all gcs previously 2024-11-28 19:38:28 +13:00
ae338db294
gambling commands now show amount bet. Slightly changed the layout. Updated some gambling strings
added .btr excl
2024-11-28 19:12:37 +13:00
97871f3e47
Merge branch '520' into v5 2024-11-28 01:17:16 +13:00
7b2adbf9bf
new method in rotating service 2024-11-28 01:10:50 +13:00
b411e8cb25
.btr and .sclr added, cleanup 2024-11-28 01:06:01 +13:00
97094dbe5d
fixed .sinfo for guilds on other shards 2024-11-27 22:45:01 +13:00
3532554a13
sar rework, improved 2024-11-27 22:38:06 +13:00
af71e88985
fixed time conversion 2024-11-22 20:47:43 +13:00
4af3a9086f
re-added a missing extension method 2024-11-18 17:21:00 +13:00
2d806119b4
fixed nullref in blacklist 2024-11-18 16:55:56 +13:00
f68ff81dce
.bsreset price reduced 10x 2024-11-13 18:56:05 +13:00
3e5bccd215
fixed update usernames query 2024-11-13 18:54:55 +13:00
e851c01c91
fixed command description for betroll 2024-11-13 18:53:12 +13:00
b81de1f103
fixed newline missing in .timely 2024-11-13 18:20:54 +13:00
190 changed files with 33591 additions and 6777 deletions

View file

@ -2,14 +2,182 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o
## [5.3.3] - 16.12.2024
## Fixed
- `.notify` commands are no longer owner only, they now require Admin permissions
- `.notify` messages can now mention anyone
## [5.3.2] - 14.12.2024
## Fixed
- `.banner` should be working properly now with both server and global user banners
## [5.3.1] - 13.12.2024
## Changed
- `.translate` will now use 2 embeds, to allow for longer messages
- Added role icon to `.inrole`, if it exists
- `.honeypot` will now add a 'Honeypot' as a ban reason.
## Fixed
- `.winlb` looks better, has a title, shows 9 entries now
- `.sar ex` help updated
- `.banner` partially fixed, it still can't show global banners, but it will show guild ones correctly, in a good enough size
- `.sclr` will now show correct color hexes without alpha
- `.dmcmd` will now correctly block commands in dms, not globally
## [5.3.0] - 10.12.2024
## Added
- Added `.minesweeper` / `.mw` command - spoiler-based minesweeper minigame. Just for fun
- Added `.temprole` command - add a role to a user for a certain amount of time, after which the role will be removed
- Added `.xplevelset` - you can now set a level for a user in your server
- Added `.winlb` command - leaderboard of top gambling wins
- Added `.notify` command
- Specify an event to be notified about, and the bot will post the specified message in the current channel when the
event occurs
- A few events supported right now:
- `UserLevelUp` when user levels up in the server
- `AddRoleReward` when a role is added to a user through .xpreward system
- `RemoveRoleReward` when a role is removed from a user through .xpreward system
- `Protection` when antialt, antiraid or antispam protection is triggered
- Added `.banner` command to see someone's banner
- Selfhosters:
- Added `.dmmod` and `.dmcmd` - you can now disable or enable whether commands or modules can be executed in bot's
DMs
## Changed
- Giveaway improvements
- Now mentions winners in a separate message
- Shows the timestamp of when the giveaway ends
- Xp Changes
- Removed awarded xp (the number in the brackets on the xp card)
- Awarded xp, (or the new level set) now directly apply to user's real xp
- Server xp notifications are now set by the server admin/manager in a specified channel
- `.sclr show` will now show hex code of the current color
- Queueing a song will now restart the playback if the queue is on the last track and stopped (there were no more tracks
to play)
- `.translate` will now use 2 embeds instead of 1
## Fixed
- .setstream and .setactivity will now pause .ropl (rotating statuses)
- Fixed `.sar ex` help description
## Removed
- `.xpnotify` command, superseded by `.notify`, although as of right now you can't post user's level up in the same
channel user last typed, because you have to specify a channel where the notify messages will be posted
## [5.2.4] - 29.11.2024
## Fixed
- More fixes for .sclr
- `.iamn` fixed
## [5.2.3] - 29.11.2024
### Fixed
- `.iam` Fixed
- `.sclr` will now properly change color on many commands it didn't work previously
### Changed
- `.rps` now also has bet amount in the result, like other gambling commands
## [5.2.2] - 29.11.2024
### Changed
- Button roles are now non-exclusive by default
### Fixed
- Fixed sar migration, again (this time correctly)
- Fixed `.sclr` not updating unless bot is restarted, the changes should be immediate now for warn and error
- Fixed group buttons exclusivity message always saying groups are exclusive
## [5.2.1] - 28.11.2024
### Fixed
- Fixed old self assigned missing
## [5.2.0] - 28.11.2024
### Added
- Added `.todo undone` command to unmark a todo as done
- Added Button Roles!
- `.btr a` to add a button role to the specified message
- `.btr list` to list all button roles on the server
- `.btr rm` to remove a button role from the specified message
- `.btr rma` to remove all button roles on the specified message
- `.btr excl` to toggle exclusive button roles (only 1 role per message or any number)
- Use `.h btr` for more info
- Added `.wrongsong` which will delete the last queued song.
- Useful in case you made a mistake, or the bot queued a wrong song
- It will reset after a shuffle or fairplay toggle, or similar events.
- Added Server color Commands!
- Every Server can now set their own colors for ok/error/pending embed (the default green/red/yellow color on the
left side of the message the bot sends)
- Use `.h .sclr` to see the list of commands
- `.sclr show` will show the current server colors
- `.sclr ok <color hex>` to set ok color
- `.sclr warn <color hex>` to set warn color
- `.sclr error <color hex>` to set error color
### Changed
- Self Assigned Roles reworked! Use `.h .sar` for the list of commands
- `.sar autodel`
- Toggles the automatic deletion of the user's message and Nadeko's confirmations for .iam and .iamn commands.
- `.sar ad`
- Adds a role to the list of self-assignable roles. You can also specify a group.
- If 'Exclusive self-assignable roles' feature is enabled (.sar exclusive), users will be able to pick one role
per group.
- `.sar groupname`
- Sets a self assignable role group name. Provide no name to remove.
- `.sar remove`
- Removes a specified role from the list of self-assignable roles.
- `.sar list`
- Lists self-assignable roles. Shows 20 roles per page.
- `.sar exclusive`
- Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role
per group.
- `.sar rolelvlreq`
- Set a level requirement on a self-assignable role.
- `.sar grouprolereq`
- Set a role that users have to have in order to assign a self-assignable role from the specified group.
- `.sar groupdelete`
- Deletes a self-assignable role group
- `.iam` and `.iamn` are unchanged
- Removed patron limits from Reaction Roles. Anyone can have as many reros as they like.
- `.timely` captcha made stronger and cached per user.
- `.bsreset` price reduced by 90%
### Fixed
- Fixed `.sinfo` for servers on other shard
## [5.1.20] - 13.11.2024 ## [5.1.20] - 13.11.2024
### Added ### Added
- Added `.rakeback` command, get a % of house edge back as claimable currency - Added `.rakeback` command, get a % of house edge back as claimable currency
- Added `.snipe` command to quickly get a copy of a posted message as an embed - Added `.snipe` command to quickly get a copy of a posted message as an embed
- You can reply to a message to snipe that message - You can reply to a message to snipe that message
- Or just type .snipe and the bot will snipe the last message in the channel with content or image - Or just type .snipe and the bot will snipe the last message in the channel with content or image
- Added `.betstatsreset` / `.bsreset` command to reset your stats for a fee - Added `.betstatsreset` / `.bsreset` command to reset your stats for a fee
- Added `.gamblestatsreset` / `.gsreset` owner-only command to reset bot stats for all games - Added `.gamblestatsreset` / `.gsreset` owner-only command to reset bot stats for all games
- Added `.waifuclaims` command which lists all of your claimed waifus - Added `.waifuclaims` command which lists all of your claimed waifus
@ -20,8 +188,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except da
- `.divorce` no longer has a cooldown - `.divorce` no longer has a cooldown
- `.betroll` has a 2% better payout - `.betroll` has a 2% better payout
- `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins, - `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins,
- now has a new symbol, wheat - now has a new symbol, wheat
- worse around 1% in total (now shares the top spot with .bf) - worse around 1% in total (now shares the top spot with .bf)
## [5.1.19] - 05.11.2024 ## [5.1.19] - 05.11.2024
@ -40,7 +208,7 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except da
- `.plant/pick` password font size will be slightly bigger - `.plant/pick` password font size will be slightly bigger
- `.race` will now have 82-94% payout rate based on the number of players playing (1-12, x0.01 per player). - `.race` will now have 82-94% payout rate based on the number of players playing (1-12, x0.01 per player).
- Any player over 12 won't increase payout - Any player over 12 won't increase payout
### Fixed ### Fixed

View file

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net.Core" Version="3.15.3" /> <PackageReference Include="Discord.Net.Core" Version="3.16.0" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="YamlDotNet" Version="15.1.4" /> <PackageReference Include="YamlDotNet" Version="15.1.4" />
</ItemGroup> </ItemGroup>

View file

@ -356,3 +356,5 @@ resharper_arrange_redundant_parentheses_highlighting = hint
# IDE0011: Add braces # IDE0011: Add braces
dotnet_diagnostic.IDE0011.severity = warning dotnet_diagnostic.IDE0011.severity = warning
resharper_arrange_type_member_modifiers_highlighting = hint

View file

@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Administration.Services;
// ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable UnusedAutoPropertyAccessor.Global
@ -14,7 +15,6 @@ public abstract class EllieContext : DbContext
public DbSet<Quote> Quotes { get; set; } public DbSet<Quote> Quotes { get; set; }
public DbSet<Reminder> Reminders { get; set; } public DbSet<Reminder> Reminders { get; set; }
public DbSet<SelfAssignedRole> SelfAssignableRoles { get; set; }
public DbSet<MusicPlaylist> MusicPlaylists { get; set; } public DbSet<MusicPlaylist> MusicPlaylists { get; set; }
public DbSet<EllieExpression> Expressions { get; set; } public DbSet<EllieExpression> Expressions { get; set; }
public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; } public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; }
@ -74,6 +74,87 @@ public abstract class EllieContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
#region Notify
modelBuilder.Entity<Notify>(e =>
{
e.HasAlternateKey(x => new
{
x.GuildId,
Event = x.Type
});
});
#endregion
#region TempRoles
modelBuilder.Entity<TempRole>(e =>
{
e.HasAlternateKey(x => new
{
x.GuildId,
x.UserId,
x.RoleId
});
e.HasIndex(x => x.ExpiresAt);
});
#endregion
#region GuildColors
modelBuilder.Entity<GuildColors>()
.HasIndex(x => x.GuildId)
.IsUnique(true);
#endregion
#region Button Roles
modelBuilder.Entity<ButtonRole>(br =>
{
br.HasIndex(x => x.GuildId)
.IsUnique(false);
br.HasAlternateKey(x => new
{
x.RoleId,
x.MessageId,
});
});
#endregion
#region New Sar
modelBuilder.Entity<SarGroup>(sg =>
{
sg.HasAlternateKey(x => new
{
x.GuildId,
x.GroupNumber
});
sg.HasMany(x => x.Roles)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Sar>()
.HasAlternateKey(x => new
{
x.GuildId,
x.RoleId
});
modelBuilder.Entity<SarAutoDelete>()
.HasIndex(x => x.GuildId)
.IsUnique();
#endregion
#region Rakeback #region Rakeback
modelBuilder.Entity<Rakeback>() modelBuilder.Entity<Rakeback>()
@ -83,17 +164,29 @@ public abstract class EllieContext : DbContext
#region UserBetStats #region UserBetStats
modelBuilder.Entity<UserBetStats>() modelBuilder.Entity<UserBetStats>(ubs =>
.HasIndex(x => new { x.UserId, x.Game }) {
.IsUnique(); ubs.HasIndex(x => new
{
x.UserId,
x.Game
})
.IsUnique();
ubs.HasIndex(x => x.MaxWin)
.IsUnique(false);
});
#endregion #endregion
#region Flag Translate #region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>() modelBuilder.Entity<FlagTranslateChannel>()
.HasIndex(x => new { x.GuildId, x.ChannelId }) .HasIndex(x => new
{
x.GuildId,
x.ChannelId
})
.IsUnique(); .IsUnique();
#endregion #endregion
@ -286,11 +379,6 @@ public abstract class EllieContext : DbContext
.HasForeignKey(x => x.GuildConfigId) .HasForeignKey(x => x.GuildConfigId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GuildConfig>()
.HasMany(x => x.SelfAssignableRoleGroupNames)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<FeedSub>() modelBuilder.Entity<FeedSub>()
.HasAlternateKey(x => new .HasAlternateKey(x => new
{ {
@ -319,21 +407,6 @@ public abstract class EllieContext : DbContext
#endregion #endregion
#region Self Assignable Roles
var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>();
selfassignableRolesEntity.HasIndex(s => new
{
s.GuildId,
s.RoleId
})
.IsUnique();
selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0);
#endregion
#region MusicPlaylists #region MusicPlaylists
var musicPlaylistEntity = modelBuilder.Entity<MusicPlaylist>(); var musicPlaylistEntity = modelBuilder.Entity<MusicPlaylist>();
@ -401,16 +474,15 @@ public abstract class EllieContext : DbContext
var xps = modelBuilder.Entity<UserXpStats>(); var xps = modelBuilder.Entity<UserXpStats>();
xps.HasIndex(x => new xps.HasIndex(x => new
{ {
x.UserId, x.UserId,
x.GuildId x.GuildId
}) })
.IsUnique(); .IsUnique();
xps.HasIndex(x => x.UserId); xps.HasIndex(x => x.UserId);
xps.HasIndex(x => x.GuildId); xps.HasIndex(x => x.GuildId);
xps.HasIndex(x => x.Xp); xps.HasIndex(x => x.Xp);
xps.HasIndex(x => x.AwardedXp);
#endregion #endregion
@ -450,9 +522,9 @@ public abstract class EllieContext : DbContext
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
ci.HasIndex(x => new ci.HasIndex(x => new
{ {
x.Name x.Name
}) })
.IsUnique(); .IsUnique();
#endregion #endregion
@ -516,23 +588,6 @@ public abstract class EllieContext : DbContext
#endregion #endregion
#region GroupName
modelBuilder.Entity<GroupName>()
.HasIndex(x => new
{
x.GuildConfigId,
x.Number
})
.IsUnique();
modelBuilder.Entity<GroupName>()
.HasOne(x => x.GuildConfig)
.WithMany(x => x.SelfAssignableRoleGroupNames)
.IsRequired();
#endregion
#region BanTemplate #region BanTemplate
modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique(); modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique();
@ -571,10 +626,10 @@ public abstract class EllieContext : DbContext
.IsUnique(false); .IsUnique(false);
rr2.HasIndex(x => new rr2.HasIndex(x => new
{ {
x.MessageId, x.MessageId,
x.Emote x.Emote
}) })
.IsUnique(); .IsUnique();
}); });
@ -649,11 +704,11 @@ public abstract class EllieContext : DbContext
{ {
// user can own only one of each item // user can own only one of each item
x.HasIndex(model => new x.HasIndex(model => new
{ {
model.UserId, model.UserId,
model.ItemType, model.ItemType,
model.ItemKey model.ItemKey
}) })
.IsUnique(); .IsUnique();
}); });
@ -678,10 +733,10 @@ public abstract class EllieContext : DbContext
#region Sticky Roles #region Sticky Roles
modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new
{ {
x.GuildId, x.GuildId,
x.UserId x.UserId
}) })
.IsUnique()); .IsUnique());
#endregion #endregion
@ -726,10 +781,10 @@ public abstract class EllieContext : DbContext
modelBuilder modelBuilder
.Entity<GreetSettings>(gs => gs.HasIndex(x => new .Entity<GreetSettings>(gs => gs.HasIndex(x => new
{ {
x.GuildId, x.GuildId,
x.GreetType x.GreetType
}) })
.IsUnique()); .IsUnique());
modelBuilder.Entity<GreetSettings>(gs => modelBuilder.Entity<GreetSettings>(gs =>

View file

@ -1,22 +0,0 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models;
namespace EllieBot.Db;
public static class SelfAssignableRolesExtensions
{
public static bool DeleteByGuildAndRoleId(this DbSet<SelfAssignedRole> roles, ulong guildId, ulong roleId)
{
var role = roles.FirstOrDefault(s => s.GuildId == guildId && s.RoleId == roleId);
if (role is null)
return false;
roles.Remove(role);
return true;
}
public static IReadOnlyCollection<SelfAssignedRole> GetFromGuild(this DbSet<SelfAssignedRole> roles, ulong guildId)
=> roles.AsQueryable().Where(s => s.GuildId == guildId).ToArray();
}

View file

@ -1,5 +1,4 @@
#nullable disable using LinqToDB;
using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models; using EllieBot.Db.Models;
@ -8,6 +7,9 @@ namespace EllieBot.Db;
public static class UserXpExtensions public static class UserXpExtensions
{ {
public static async Task<UserXpStats?> GetGuildUserXp(this ITable<UserXpStats> table, ulong guildId, ulong userId)
=> await table.FirstOrDefaultAsyncLinqToDB(x => x.GuildId == guildId && x.UserId == userId);
public static UserXpStats GetOrCreateUserXpStats(this DbContext ctx, ulong guildId, ulong userId) public static UserXpStats GetOrCreateUserXpStats(this DbContext ctx, ulong guildId, ulong userId)
{ {
var usr = ctx.Set<UserXpStats>().FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId); var usr = ctx.Set<UserXpStats>().FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId);
@ -18,7 +20,6 @@ public static class UserXpExtensions
{ {
Xp = 0, Xp = 0,
UserId = userId, UserId = userId,
NotifyOnLevelUp = XpNotificationLocation.None,
GuildId = guildId GuildId = guildId
}); });
} }
@ -29,17 +30,17 @@ public static class UserXpExtensions
public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count) public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
=> await xps.ToLinqToDBTable() => await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp) .OrderByDescending(x => x.Xp)
.Take(count) .Take(count)
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
public static async Task<int> GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId) public static async Task<int> GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
=> await xps.ToLinqToDBTable() => await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId .Where(x => x.GuildId == guildId
&& x.Xp + x.AwardedXp && x.Xp
> xps.AsQueryable() > xps.AsQueryable()
.Where(y => y.UserId == userId && y.GuildId == guildId) .Where(y => y.UserId == userId && y.GuildId == guildId)
.Select(y => y.Xp + y.AwardedXp) .Select(y => y.Xp)
.FirstOrDefault()) .FirstOrDefault())
.CountAsyncLinqToDB() .CountAsyncLinqToDB()
+ 1; + 1;
@ -51,6 +52,6 @@ public static class UserXpExtensions
=> await userXp => await userXp
.Where(x => x.GuildId == guildId && x.UserId == userId) .Where(x => x.GuildId == guildId && x.UserId == userId)
.FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs .FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs
? new(uxs.Xp + uxs.AwardedXp) ? new(uxs.Xp)
: new(0); : new(0);
} }

View file

@ -3,38 +3,28 @@ namespace EllieBot.Db;
public readonly struct LevelStats public readonly struct LevelStats
{ {
public const int XP_REQUIRED_LVL_1 = 36;
public long Level { get; } public long Level { get; }
public long LevelXp { get; } public long LevelXp { get; }
public long RequiredXp { get; } public long RequiredXp { get; }
public long TotalXp { get; } public long TotalXp { get; }
public LevelStats(long xp) public LevelStats(long totalXp)
{ {
if (xp < 0) if (totalXp < 0)
xp = 0; totalXp = 0;
TotalXp = xp; TotalXp = totalXp;
Level = GetLevelByTotalXp(totalXp);
const int baseXp = XP_REQUIRED_LVL_1; LevelXp = totalXp - GetTotalXpReqForLevel(Level);
RequiredXp = (9 * (Level + 1)) + 27;
var required = baseXp;
var totalXp = 0;
var lvl = 1;
while (true)
{
required = (int)(baseXp + (baseXp / 4.0 * (lvl - 1)));
if (required + totalXp > xp)
break;
totalXp += required;
lvl++;
}
Level = lvl - 1;
LevelXp = xp - totalXp;
RequiredXp = required;
} }
public static LevelStats CreateForLevel(long level)
=> new(GetTotalXpReqForLevel(level));
public static long GetTotalXpReqForLevel(long level)
=> ((9 * level * level) + (63 * level)) / 2;
public static long GetLevelByTotalXp(long totalXp)
=> (long)((-7.0 / 2) + (1 / 6.0 * Math.Sqrt((8 * totalXp) + 441)));
} }

View file

@ -9,7 +9,8 @@ public class FollowedStream : DbEntity
Picarto = 3, Picarto = 3,
Youtube = 4, Youtube = 4,
Facebook = 5, Facebook = 5,
Trovo = 6 Trovo = 6,
Kick = 7,
} }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }

View file

@ -1,11 +0,0 @@
#nullable disable
namespace EllieBot.Db.Models;
public class GroupName : DbEntity
{
public int GuildConfigId { get; set; }
public GuildConfig GuildConfig { get; set; }
public int Number { get; set; }
public string Name { get; set; }
}

View file

@ -5,14 +5,15 @@ namespace EllieBot.Db.Models;
public class GuildColors public class GuildColors
{ {
[Key] [Key]
public int Id { get; set; }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }
[Length(0, 9)] [MaxLength(9)]
public string? OkColor { get; set; } public string? OkColor { get; set; }
[Length(0, 9)] [MaxLength(9)]
public string? ErrorColor { get; set; } public string? ErrorColor { get; set; }
[Length(0, 9)] [MaxLength(9)]
public string? PendingColor { get; set; } public string? PendingColor { get; set; }
} }

View file

@ -13,28 +13,11 @@ public class GuildConfig : DbEntity
public string AutoAssignRoleIds { get; set; } public string AutoAssignRoleIds { get; set; }
// //greet stuff //todo FUTURE: DELETE, UNUSED
// public int AutoDeleteGreetMessagesTimer { get; set; } = 30;
// public int AutoDeleteByeMessagesTimer { get; set; } = 30;
//
// public ulong GreetMessageChannelId { get; set; }
// public ulong ByeMessageChannelId { get; set; }
//
// public bool SendDmGreetMessage { get; set; }
// public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
//
// public bool SendChannelGreetMessage { get; set; }
// public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
//
// public bool SendChannelByeMessage { get; set; }
// public string ChannelByeMessageText { get; set; } = "%user% has left!";
// public bool SendBoostMessage { get; set; }
// pulic int BoostMessageDeleteAfter { get; set; }
//self assignable roles
public bool ExclusiveSelfAssignedRoles { get; set; } public bool ExclusiveSelfAssignedRoles { get; set; }
public bool AutoDeleteSelfAssignedRoleMessages { get; set; } public bool AutoDeleteSelfAssignedRoleMessages { get; set; }
//stream notifications //stream notifications
public HashSet<FollowedStream> FollowedStreams { get; set; } = new(); public HashSet<FollowedStream> FollowedStreams { get; set; } = new();
@ -53,29 +36,37 @@ public class GuildConfig : DbEntity
public HashSet<FilterChannelId> FilterInvitesChannelIds { get; set; } = new(); public HashSet<FilterChannelId> FilterInvitesChannelIds { get; set; } = new();
public HashSet<FilterLinksChannelId> FilterLinksChannelIds { get; set; } = new(); public HashSet<FilterLinksChannelId> FilterLinksChannelIds { get; set; } = new();
//public bool FilterLinks { get; set; }
//public HashSet<FilterLinksChannelId> FilterLinksChannels { get; set; } = new HashSet<FilterLinksChannelId>();
public bool FilterWords { get; set; } public bool FilterWords { get; set; }
public HashSet<FilteredWord> FilteredWords { get; set; } = new(); public HashSet<FilteredWord> FilteredWords { get; set; } = new();
public HashSet<FilterWordsChannelId> FilterWordsChannelIds { get; set; } = new(); public HashSet<FilterWordsChannelId> FilterWordsChannelIds { get; set; } = new();
// mute
public HashSet<MutedUserId> MutedUsers { get; set; } = new(); public HashSet<MutedUserId> MutedUsers { get; set; } = new();
public string MuteRoleName { get; set; } public string MuteRoleName { get; set; }
// chatterbot
public bool CleverbotEnabled { get; set; } public bool CleverbotEnabled { get; set; }
// protection
public AntiRaidSetting AntiRaidSetting { get; set; } public AntiRaidSetting AntiRaidSetting { get; set; }
public AntiSpamSetting AntiSpamSetting { get; set; } public AntiSpamSetting AntiSpamSetting { get; set; }
public AntiAltSetting AntiAltSetting { get; set; } public AntiAltSetting AntiAltSetting { get; set; }
// time
public string Locale { get; set; } public string Locale { get; set; }
public string TimeZoneId { get; set; } public string TimeZoneId { get; set; }
// timers
public HashSet<UnmuteTimer> UnmuteTimers { get; set; } = new(); public HashSet<UnmuteTimer> UnmuteTimers { get; set; } = new();
public HashSet<UnbanTimer> UnbanTimer { get; set; } = new(); public HashSet<UnbanTimer> UnbanTimer { get; set; } = new();
public HashSet<UnroleTimer> UnroleTimer { get; set; } = new(); public HashSet<UnroleTimer> UnroleTimer { get; set; } = new();
// vcrole
public HashSet<VcRoleInfo> VcRoleInfos { get; set; } public HashSet<VcRoleInfo> VcRoleInfos { get; set; }
// aliases
public HashSet<CommandAlias> CommandAliases { get; set; } = new(); public HashSet<CommandAlias> CommandAliases { get; set; } = new();
public bool WarningsInitialized { get; set; } public bool WarningsInitialized { get; set; }
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; } public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
@ -91,15 +82,10 @@ public class GuildConfig : DbEntity
public List<FeedSub> FeedSubs { get; set; } = new(); public List<FeedSub> FeedSubs { get; set; } = new();
public bool NotifyStreamOffline { get; set; } public bool NotifyStreamOffline { get; set; }
public bool DeleteStreamOnlineMessage { get; set; } public bool DeleteStreamOnlineMessage { get; set; }
public List<GroupName> SelfAssignableRoleGroupNames { get; set; }
public int WarnExpireHours { get; set; } public int WarnExpireHours { get; set; }
public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear; public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear;
public bool DisableGlobalExpressions { get; set; } = false; public bool DisableGlobalExpressions { get; set; } = false;
#region Boost Message
public bool StickyRoles { get; set; } public bool StickyRoles { get; set; }
#endregion
} }

View file

@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models;
public class Notify
{
[Key]
public int Id { get; set; }
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
public NotifyType Type { get; set; }
[MaxLength(10_000)]
public string Message { get; set; } = string.Empty;
}
public enum NotifyType
{
LevelUp = 0,
Protection = 1, Prot = 1,
AddRoleReward = 2,
RemoveRoleReward = 3,
}

View file

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models;
public sealed class SarGroup
{
[Key]
public int Id { get; set; }
public int GroupNumber { get; set; }
public ulong GuildId { get; set; }
public ulong? RoleReq { get; set; }
public ICollection<Sar> Roles { get; set; } = [];
public bool IsExclusive { get; set; }
[MaxLength(100)]
public string? Name { get; set; }
}

View file

@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models;
public sealed class ButtonRole
{
[Key]
public int Id { get; set; }
[MaxLength(200)]
public string ButtonId { get; set; } = string.Empty;
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
public ulong MessageId { get; set; }
public int Position { get; set; }
public ulong RoleId { get; set; }
[MaxLength(100)]
public string Emote { get; set; } = string.Empty;
[MaxLength(50)]
public string Label { get; set; } = string.Empty;
public bool Exclusive { get; set; }
}

View file

@ -1,11 +1,24 @@
#nullable disable using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
public class SelfAssignedRole : DbEntity public sealed class Sar
{ {
[Key]
public int Id { get; set; }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }
public ulong RoleId { get; set; } public ulong RoleId { get; set; }
public int Group { get; set; } public int SarGroupId { get; set; }
public int LevelRequirement { get; set; } public int LevelReq { get; set; }
}
public sealed class SarAutoDelete
{
[Key]
public int Id { get; set; }
public ulong GuildId { get; set; }
public bool IsEnabled { get; set; } = false;
} }

View file

@ -0,0 +1,12 @@
namespace EllieBot.Db.Models;
public class TempRole
{
public int Id { get; set; }
public ulong GuildId { get; set; }
public bool Remove { get; set; }
public ulong RoleId { get; set; }
public ulong UserId { get; set; }
public DateTime ExpiresAt { get; set; }
}

View file

@ -6,6 +6,4 @@ public class UserXpStats : DbEntity
public ulong UserId { get; set; } public ulong UserId { get; set; }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }
public long Xp { get; set; } public long Xp { get; set; }
public long AwardedXp { get; set; }
public XpNotificationLocation NotifyOnLevelUp { get; set; }
} }

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.20</Version> <Version>5.3.3</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -29,7 +29,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" /> <PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" />
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.15.3" /> <PackageReference Include="Discord.Net" Version="3.16.0" />
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246" /> <PackageReference Include="CoreCLR-NCalc" Version="3.1.246" />
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" /> <PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414" /> <PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414" />

View file

@ -5,9 +5,52 @@ namespace EllieBot.Migrations;
public static class MigrationQueries public static class MigrationQueries
{ {
public static void MergeAwardedXp(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
UPDATE UserXpStats
SET Xp = AwardedXp + Xp,
AwardedXp = 0
WHERE AwardedXp > 0;
""");
}
public static void MigrateSar(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
return;
migrationBuilder.Sql("""
INSERT INTO GroupName (Number, GuildConfigId)
SELECT DISTINCT "Group", GC.Id
FROM SelfAssignableRoles as SAR
INNER JOIN GuildConfigs as GC
ON SAR.GuildId = GC.GuildId
WHERE SAR.GuildId not in (SELECT GuildConfigs.GuildId from GroupName LEFT JOIN GuildConfigs ON GroupName.GuildConfigId = GuildConfigs.Id);
INSERT INTO SarGroup (Id, GroupNumber, Name, IsExclusive, GuildId)
SELECT GN.Id, GN.Number, GN.Name, GC.ExclusiveSelfAssignedRoles, GC.GuildId
FROM GroupName as GN
INNER JOIN GuildConfigs as GC ON GN.GuildConfigId = GC.Id;
INSERT INTO Sar (GuildId, RoleId, SarGroupId, LevelReq)
SELECT SAR.GuildId, SAR.RoleId, (SELECT Id FROM SarGroup WHERE SG.Number = SarGroup.GroupNumber AND SG.GuildId = SarGroup.GuildId), MIN(SAR.LevelRequirement)
FROM SelfAssignableRoles as SAR
INNER JOIN (SELECT GuildId, gn.Number FROM GroupName as gn
INNER JOIN GuildConfigs as gc ON gn.GuildConfigId =gc.Id
) as SG
ON SG.GuildId = SAR.GuildId
WHERE SG.Number IN (SELECT GroupNumber FROM SarGroup WHERE Sar.GuildId = SarGroup.GuildId)
GROUP BY SAR.GuildId, SAR.RoleId;
INSERT INTO SarAutoDelete (GuildId, IsEnabled)
SELECT GuildId, AutoDeleteSelfAssignedRoleMessages FROM GuildConfigs WHERE AutoDeleteSelfAssignedRoleMessages = TRUE;
""");
}
public static void UpdateUsernames(MigrationBuilder migrationBuilder) public static void UpdateUsernames(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' + Username WHERE Discriminator = '????';"); migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' || Username WHERE Discriminator = '????';");
} }
public static void MigrateRero(MigrationBuilder migrationBuilder) public static void MigrateRero(MigrationBuilder migrationBuilder)

View file

@ -12,8 +12,6 @@ namespace EllieBot.Migrations.PostgreSql
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
MigrationQueries.UpdateUsernames(migrationBuilder);
migrationBuilder.DropColumn( migrationBuilder.DropColumn(
name: "discriminator", name: "discriminator",
table: "discorduser"); table: "discorduser");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class btnroles_guildcolors : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "buttonrole",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
buttonid = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
position = table.Column<int>(type: "integer", nullable: false),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
emote = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
label = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
exclusive = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_buttonrole", x => x.id);
table.UniqueConstraint("ak_buttonrole_roleid_messageid", x => new { x.roleid, x.messageid });
});
migrationBuilder.CreateTable(
name: "guildcolors",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
okcolor = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true),
errorcolor = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true),
pendingcolor = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_guildcolors", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_buttonrole_guildid",
table: "buttonrole",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_guildcolors_guildid",
table: "guildcolors",
column: "guildid",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "buttonrole");
migrationBuilder.DropTable(
name: "guildcolors");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,153 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class sarrework : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "groupname");
migrationBuilder.DropTable(
name: "selfassignableroles");
migrationBuilder.CreateTable(
name: "sarautodelete",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
isenabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_sarautodelete", x => x.id);
});
migrationBuilder.CreateTable(
name: "sargroup",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
groupnumber = table.Column<int>(type: "integer", nullable: false),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
rolereq = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
isexclusive = table.Column<bool>(type: "boolean", nullable: false),
name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_sargroup", x => x.id);
table.UniqueConstraint("ak_sargroup_guildid_groupnumber", x => new { x.guildid, x.groupnumber });
});
migrationBuilder.CreateTable(
name: "sar",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
sargroupid = table.Column<int>(type: "integer", nullable: false),
levelreq = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_sar", x => x.id);
table.UniqueConstraint("ak_sar_guildid_roleid", x => new { x.guildid, x.roleid });
table.ForeignKey(
name: "fk_sar_sargroup_sargroupid",
column: x => x.sargroupid,
principalTable: "sargroup",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_sar_sargroupid",
table: "sar",
column: "sargroupid");
migrationBuilder.CreateIndex(
name: "ix_sarautodelete_guildid",
table: "sarautodelete",
column: "guildid",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "sar");
migrationBuilder.DropTable(
name: "sarautodelete");
migrationBuilder.DropTable(
name: "sargroup");
migrationBuilder.CreateTable(
name: "groupname",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildconfigid = table.Column<int>(type: "integer", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
name = table.Column<string>(type: "text", nullable: true),
number = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_groupname", x => x.id);
table.ForeignKey(
name: "fk_groupname_guildconfigs_guildconfigid",
column: x => x.guildconfigid,
principalTable: "guildconfigs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "selfassignableroles",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
group = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
levelrequirement = table.Column<int>(type: "integer", nullable: false),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_selfassignableroles", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_groupname_guildconfigid_number",
table: "groupname",
columns: new[] { "guildconfigid", "number" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_selfassignableroles_guildid_roleid",
table: "selfassignableroles",
columns: new[] { "guildid", "roleid" },
unique: true);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,109 @@
using System;
using EllieBot.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class awardedxptemprolenotify : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "ix_userxpstats_awardedxp",
table: "userxpstats");
MigrationQueries.MergeAwardedXp(migrationBuilder);
migrationBuilder.DropColumn(
name: "awardedxp",
table: "userxpstats");
migrationBuilder.DropColumn(
name: "notifyonlevelup",
table: "userxpstats");
migrationBuilder.CreateTable(
name: "notify",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
message = table.Column<string>(type: "character varying(10000)", maxLength: 10000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_notify", x => x.id);
table.UniqueConstraint("ak_notify_guildid_type", x => new { x.guildid, x.type });
});
migrationBuilder.CreateTable(
name: "temprole",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
remove = table.Column<bool>(type: "boolean", nullable: false),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
expiresat = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_temprole", x => x.id);
table.UniqueConstraint("ak_temprole_guildid_userid_roleid", x => new { x.guildid, x.userid, x.roleid });
});
migrationBuilder.CreateIndex(
name: "ix_userbetstats_maxwin",
table: "userbetstats",
column: "maxwin");
migrationBuilder.CreateIndex(
name: "ix_temprole_expiresat",
table: "temprole",
column: "expiresat");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "notify");
migrationBuilder.DropTable(
name: "temprole");
migrationBuilder.DropIndex(
name: "ix_userbetstats_maxwin",
table: "userbetstats");
migrationBuilder.AddColumn<long>(
name: "awardedxp",
table: "userxpstats",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<int>(
name: "notifyonlevelup",
table: "userxpstats",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "ix_userxpstats_awardedxp",
table: "userxpstats",
column: "awardedxp");
}
}
}

View file

@ -11,8 +11,6 @@ namespace EllieBot.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
MigrationQueries.UpdateUsernames(migrationBuilder);
migrationBuilder.DropColumn( migrationBuilder.DropColumn(
name: "Discriminator", name: "Discriminator",
table: "DiscordUser"); table: "DiscordUser");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class btnroles_guildcolors : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ButtonRole",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ButtonId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
Position = table.Column<int>(type: "INTEGER", nullable: false),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
Emote = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Label = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
Exclusive = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ButtonRole", x => x.Id);
table.UniqueConstraint("AK_ButtonRole_RoleId_MessageId", x => new { x.RoleId, x.MessageId });
});
migrationBuilder.CreateTable(
name: "GuildColors",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
OkColor = table.Column<string>(type: "TEXT", maxLength: 9, nullable: true),
ErrorColor = table.Column<string>(type: "TEXT", maxLength: 9, nullable: true),
PendingColor = table.Column<string>(type: "TEXT", maxLength: 9, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GuildColors", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ButtonRole_GuildId",
table: "ButtonRole",
column: "GuildId");
migrationBuilder.CreateIndex(
name: "IX_GuildColors_GuildId",
table: "GuildColors",
column: "GuildId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ButtonRole");
migrationBuilder.DropTable(
name: "GuildColors");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,152 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class sarrework : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GroupName");
migrationBuilder.DropTable(
name: "SelfAssignableRoles");
migrationBuilder.CreateTable(
name: "SarAutoDelete",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SarAutoDelete", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SarGroup",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GroupNumber = table.Column<int>(type: "INTEGER", nullable: false),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
RoleReq = table.Column<ulong>(type: "INTEGER", nullable: true),
IsExclusive = table.Column<bool>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SarGroup", x => x.Id);
table.UniqueConstraint("AK_SarGroup_GuildId_GroupNumber", x => new { x.GuildId, x.GroupNumber });
});
migrationBuilder.CreateTable(
name: "Sar",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
SarGroupId = table.Column<int>(type: "INTEGER", nullable: false),
LevelReq = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Sar", x => x.Id);
table.UniqueConstraint("AK_Sar_GuildId_RoleId", x => new { x.GuildId, x.RoleId });
table.ForeignKey(
name: "FK_Sar_SarGroup_SarGroupId",
column: x => x.SarGroupId,
principalTable: "SarGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Sar_SarGroupId",
table: "Sar",
column: "SarGroupId");
migrationBuilder.CreateIndex(
name: "IX_SarAutoDelete_GuildId",
table: "SarAutoDelete",
column: "GuildId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Sar");
migrationBuilder.DropTable(
name: "SarAutoDelete");
migrationBuilder.DropTable(
name: "SarGroup");
migrationBuilder.CreateTable(
name: "GroupName",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
Number = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GroupName", x => x.Id);
table.ForeignKey(
name: "FK_GroupName_GuildConfigs_GuildConfigId",
column: x => x.GuildConfigId,
principalTable: "GuildConfigs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SelfAssignableRoles",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
Group = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
LevelRequirement = table.Column<int>(type: "INTEGER", nullable: false),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SelfAssignableRoles", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_GroupName_GuildConfigId_Number",
table: "GroupName",
columns: new[] { "GuildConfigId", "Number" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_SelfAssignableRoles_GuildId_RoleId",
table: "SelfAssignableRoles",
columns: new[] { "GuildId", "RoleId" },
unique: true);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,107 @@
using System;
using EllieBot.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class awardedxptemprolenotify : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_UserXpStats_AwardedXp",
table: "UserXpStats");
MigrationQueries.MergeAwardedXp(migrationBuilder);
migrationBuilder.DropColumn(
name: "AwardedXp",
table: "UserXpStats");
migrationBuilder.DropColumn(
name: "NotifyOnLevelUp",
table: "UserXpStats");
migrationBuilder.CreateTable(
name: "Notify",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", maxLength: 10000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Notify", x => x.Id);
table.UniqueConstraint("AK_Notify_GuildId_Type", x => new { x.GuildId, x.Type });
});
migrationBuilder.CreateTable(
name: "TempRole",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
Remove = table.Column<bool>(type: "INTEGER", nullable: false),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TempRole", x => x.Id);
table.UniqueConstraint("AK_TempRole_GuildId_UserId_RoleId", x => new { x.GuildId, x.UserId, x.RoleId });
});
migrationBuilder.CreateIndex(
name: "IX_UserBetStats_MaxWin",
table: "UserBetStats",
column: "MaxWin");
migrationBuilder.CreateIndex(
name: "IX_TempRole_ExpiresAt",
table: "TempRole",
column: "ExpiresAt");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Notify");
migrationBuilder.DropTable(
name: "TempRole");
migrationBuilder.DropIndex(
name: "IX_UserBetStats_MaxWin",
table: "UserBetStats");
migrationBuilder.AddColumn<long>(
name: "AwardedXp",
table: "UserXpStats",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddColumn<int>(
name: "NotifyOnLevelUp",
table: "UserXpStats",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_UserXpStats_AwardedXp",
table: "UserXpStats",
column: "AwardedXp");
}
}
}

View file

@ -46,7 +46,7 @@ public partial class Administration : EllieModule<AdministrationService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageGuild)] [BotPerm(GuildPerm.ManageGuild)]
public async Task ImageOnlyChannel(StoopidTime time = null) public async Task ImageOnlyChannel(ParsedTimespan timespan = null)
{ {
var newValue = await _somethingOnly.ToggleImageOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id); var newValue = await _somethingOnly.ToggleImageOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
if (newValue) if (newValue)
@ -59,7 +59,7 @@ public partial class Administration : EllieModule<AdministrationService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageGuild)] [BotPerm(GuildPerm.ManageGuild)]
public async Task LinkOnlyChannel(StoopidTime time = null) public async Task LinkOnlyChannel(ParsedTimespan timespan = null)
{ {
var newValue = await _somethingOnly.ToggleLinkOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id); var newValue = await _somethingOnly.ToggleLinkOnlyChannelAsync(ctx.Guild.Id, ctx.Channel.Id);
if (newValue) if (newValue)
@ -72,10 +72,10 @@ public partial class Administration : EllieModule<AdministrationService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageChannels)] [UserPerm(ChannelPerm.ManageChannels)]
[BotPerm(ChannelPerm.ManageChannels)] [BotPerm(ChannelPerm.ManageChannels)]
public async Task Slowmode(StoopidTime time = null) public async Task Slowmode(ParsedTimespan timespan = null)
{ {
var seconds = (int?)time?.Time.TotalSeconds ?? 0; var seconds = (int?)timespan?.Time.TotalSeconds ?? 0;
if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6))) if (timespan is not null && (timespan.Time < TimeSpan.FromSeconds(0) || timespan.Time > TimeSpan.FromHours(6)))
return; return;
await ((ITextChannel)ctx.Channel).ModifyAsync(tcp => await ((ITextChannel)ctx.Channel).ModifyAsync(tcp =>
@ -96,7 +96,7 @@ public partial class Administration : EllieModule<AdministrationService>
var guild = (SocketGuild)ctx.Guild; var guild = (SocketGuild)ctx.Guild;
var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id); var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id);
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.server_delmsgoncmd)) .WithTitle(GetText(strs.server_delmsgoncmd))
.WithDescription(enabled ? "✅" : "❌"); .WithDescription(enabled ? "✅" : "❌");
@ -298,18 +298,18 @@ public partial class Administration : EllieModule<AdministrationService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)] [UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)] [BotPerm(ChannelPerm.ManageMessages)]
public Task Delete(ulong messageId, StoopidTime time = null) public Task Delete(ulong messageId, ParsedTimespan timespan = null)
=> Delete((ITextChannel)ctx.Channel, messageId, time); => Delete((ITextChannel)ctx.Channel, messageId, timespan);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null) public async Task Delete(ITextChannel channel, ulong messageId, ParsedTimespan timespan = null)
=> await InternalMessageAction(channel, messageId, time, msg => msg.DeleteAsync()); => await InternalMessageAction(channel, messageId, timespan, msg => msg.DeleteAsync());
private async Task InternalMessageAction( private async Task InternalMessageAction(
ITextChannel channel, ITextChannel channel,
ulong messageId, ulong messageId,
StoopidTime time, ParsedTimespan timespan,
Func<IMessage, Task> func) Func<IMessage, Task> func)
{ {
var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel); var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel);
@ -334,13 +334,13 @@ public partial class Administration : EllieModule<AdministrationService>
return; return;
} }
if (time is null) if (timespan is null)
await msg.DeleteAsync(); await msg.DeleteAsync();
else if (time.Time <= TimeSpan.FromDays(7)) else if (timespan.Time <= TimeSpan.FromDays(7))
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
await Task.Delay(time.Time); await Task.Delay(timespan.Time);
await msg.DeleteAsync(); await msg.DeleteAsync();
}); });
} }
@ -450,7 +450,8 @@ public partial class Administration : EllieModule<AdministrationService>
public async Task SetServerBanner([Leftover] string img = null) public async Task SetServerBanner([Leftover] string img = null)
{ {
// Tier2 or higher is required to set a banner. // Tier2 or higher is required to set a banner.
if (ctx.Guild.PremiumTier is PremiumTier.Tier1 or PremiumTier.None) return; if (ctx.Guild.PremiumTier is PremiumTier.Tier1 or PremiumTier.None)
return;
var result = await _service.SetServerBannerAsync(ctx.Guild, img); var result = await _service.SetServerBannerAsync(ctx.Guild, img);

View file

@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Administration._common.results; using EllieBot.Modules.Administration._common.results;
namespace EllieBot.Modules.Administration.Services; namespace EllieBot.Modules.Administration;
public class AdministrationService : IEService public class AdministrationService : IEService
{ {

View file

@ -136,7 +136,7 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
await using var linqCtx = ctx.CreateLinqToDBContext(); await using var linqCtx = ctx.CreateLinqToDBContext();
await using var tempTable = linqCtx.CreateTempTable<CleanupId>(); await using var tempTable = linqCtx.CreateTempTable<CleanupId>();
foreach (var chunk in allIds.Chunk(20000)) foreach (var chunk in allIds.Chunk(10000))
{ {
await tempTable.BulkCopyAsync(chunk.Select(x => new CleanupId() await tempTable.BulkCopyAsync(chunk.Select(x => new CleanupId()
{ {
@ -187,13 +187,6 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete ignored users
await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null
&& !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId.Value))
.DeleteAsync();
// delete perm overrides // delete perm overrides
await ctx.GetTable<DiscordPermOverride>() await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null .Where(x => x.GuildId != null
@ -219,6 +212,54 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete sar
await ctx.GetTable<SarGroup>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete warnings
await ctx.GetTable<Warning>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete warn punishments
await ctx.GetTable<WarningPunishment>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete sticky roles
await ctx.GetTable<StickyRole>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete at channels
await ctx.GetTable<AutoTranslateChannel>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete ban templates
await ctx.GetTable<BanTemplate>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete reminders
await ctx.GetTable<Reminder>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.ServerId))
.DeleteAsync();
// delete button roles
await ctx.GetTable<ButtonRole>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
return new() return new()
{ {
GuildCount = guildIds.Keys.Count, GuildCount = guildIds.Keys.Count,

View file

@ -42,9 +42,9 @@ public partial class Administration
.Page((items, _) => .Page((items, _) =>
{ {
if (!items.Any()) if (!items.Any())
return _sender.CreateEmbed().WithErrorColor().WithFooter(sql).WithDescription("-"); return CreateEmbed().WithErrorColor().WithFooter(sql).WithDescription("-");
return _sender.CreateEmbed() return CreateEmbed()
.WithOkColor() .WithOkColor()
.WithFooter(sql) .WithFooter(sql)
.WithTitle(string.Join(" ║ ", result.ColumnNames)) .WithTitle(string.Join(" ║ ", result.ColumnNames))
@ -99,7 +99,7 @@ public partial class Administration
{ {
try try
{ {
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithTitle(GetText(strs.sql_confirm_exec)) .WithTitle(GetText(strs.sql_confirm_exec))
.WithDescription(Format.Code(sql)); .WithDescription(Format.Code(sql));
@ -119,7 +119,7 @@ public partial class Administration
[OwnerOnly] [OwnerOnly]
public async Task PurgeUser(ulong userId) public async Task PurgeUser(ulong userId)
{ {
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithDescription(GetText(strs.purge_user_confirm(Format.Bold(userId.ToString())))); .WithDescription(GetText(strs.purge_user_confirm(Format.Bold(userId.ToString()))));
if (!await PromptUserConfirmAsync(embed)) if (!await PromptUserConfirmAsync(embed))

View file

@ -71,7 +71,7 @@ public sealed class HoneyPotService : IHoneyPotService, IReadyExecutor, IExecNoC
try try
{ {
Log.Information("Honeypot caught user {User} [{UserId}]", user, user.Id); Log.Information("Honeypot caught user {User} [{UserId}]", user, user.Id);
await user.BanAsync(pruneDays: 1); await user.BanAsync(pruneDays: 1, reason: "Honeypot");
await user.Guild.RemoveBanAsync(user.Id); await user.Guild.RemoveBanAsync(user.Id);
} }
catch (Exception e) catch (Exception e)

View file

@ -123,7 +123,7 @@ public partial class Administration
[Cmd] [Cmd]
public async Task LanguagesList() public async Task LanguagesList()
=> await Response().Embed(_sender.CreateEmbed() => await Response().Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.lang_list)) .WithTitle(GetText(strs.lang_list))
.WithDescription(string.Join("\n", .WithDescription(string.Join("\n",

View file

@ -72,18 +72,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
[Priority(1)] [Priority(1)]
public async Task Mute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") public async Task Mute(ParsedTimespan timespan, IGuildUser user, [Leftover] string reason = "")
{ {
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) if (timespan.Time < TimeSpan.FromMinutes(1) || timespan.Time > TimeSpan.FromDays(49))
return; return;
try try
{ {
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return; return;
await _service.TimedMute(user, ctx.User, time.Time, reason: reason); await _service.TimedMute(user, ctx.User, timespan.Time, reason: reason);
await Response().Confirm(strs.user_muted_time(Format.Bold(user.ToString()), await Response().Confirm(strs.user_muted_time(Format.Bold(user.ToString()),
(int)time.Time.TotalMinutes)).SendAsync(); (int)timespan.Time.TotalMinutes)).SendAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -133,18 +133,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[Priority(1)] [Priority(1)]
public async Task ChatMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") public async Task ChatMute(ParsedTimespan timespan, IGuildUser user, [Leftover] string reason = "")
{ {
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) if (timespan.Time < TimeSpan.FromMinutes(1) || timespan.Time > TimeSpan.FromDays(49))
return; return;
try try
{ {
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return; return;
await _service.TimedMute(user, ctx.User, time.Time, MuteType.Chat, reason); await _service.TimedMute(user, ctx.User, timespan.Time, MuteType.Chat, reason);
await Response().Confirm(strs.user_chat_mute_time(Format.Bold(user.ToString()), await Response().Confirm(strs.user_chat_mute_time(Format.Bold(user.ToString()),
(int)time.Time.TotalMinutes)).SendAsync(); (int)timespan.Time.TotalMinutes)).SendAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -193,18 +193,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.MuteMembers)] [UserPerm(GuildPerm.MuteMembers)]
[Priority(1)] [Priority(1)]
public async Task VoiceMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") public async Task VoiceMute(ParsedTimespan timespan, IGuildUser user, [Leftover] string reason = "")
{ {
if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) if (timespan.Time < TimeSpan.FromMinutes(1) || timespan.Time > TimeSpan.FromDays(49))
return; return;
try try
{ {
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
return; return;
await _service.TimedMute(user, ctx.User, time.Time, MuteType.Voice, reason); await _service.TimedMute(user, ctx.User, timespan.Time, MuteType.Voice, reason);
await Response().Confirm(strs.user_voice_mute_time(Format.Bold(user.ToString()), await Response().Confirm(strs.user_voice_mute_time(Format.Bold(user.ToString()),
(int)time.Time.TotalMinutes)).SendAsync(); (int)timespan.Time.TotalMinutes)).SendAsync();
} }
catch catch
{ {

View file

@ -122,7 +122,7 @@ public class MuteService : IEService
return; return;
_ = Task.Run(() => _sender.Response(user) _ = Task.Run(() => _sender.Response(user)
.Embed(_sender.CreateEmbed() .Embed(_sender.CreateEmbed(user?.GuildId)
.WithDescription($"You've been muted in {user.Guild} server") .WithDescription($"You've been muted in {user.Guild} server")
.AddField("Mute Type", type.ToString()) .AddField("Mute Type", type.ToString())
.AddField("Moderator", mod.ToString()) .AddField("Moderator", mod.ToString())
@ -140,7 +140,7 @@ public class MuteService : IEService
return; return;
_ = Task.Run(() => _sender.Response(user) _ = Task.Run(() => _sender.Response(user)
.Embed(_sender.CreateEmbed() .Embed(_sender.CreateEmbed(user.GuildId)
.WithDescription($"You've been unmuted in {user.Guild} server") .WithDescription($"You've been unmuted in {user.Guild} server")
.AddField("Unmute Type", type.ToString()) .AddField("Unmute Type", type.ToString())
.AddField("Moderator", mod.ToString()) .AddField("Moderator", mod.ToString())

View file

@ -0,0 +1,23 @@
using EllieBot.Db.Models;
using System.Collections;
namespace EllieBot.Modules.Administration;
public interface INotifyModel
{
static abstract string KeyName { get; }
static abstract NotifyType NotifyType { get; }
IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements();
public virtual bool TryGetGuildId(out ulong guildId)
{
guildId = 0;
return false;
}
public virtual bool TryGetUserId(out ulong userId)
{
userId = 0;
return false;
}
}

View file

@ -0,0 +1,7 @@
namespace EllieBot.Modules.Administration;
public interface INotifySubscriber
{
Task NotifyAsync<T>(T data, bool isShardLocal = false)
where T : struct, INotifyModel;
}

View file

@ -0,0 +1,36 @@
using EllieBot.Db.Models;
using EllieBot.Modules.Administration;
namespace EllieBot.Modules.Xp.Services;
public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel
{
public static string KeyName
=> "notify.reward.addrole";
public static NotifyType NotifyType
=> NotifyType.AddRoleReward;
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
{
var model = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() },
{ "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() },
{ "%event.level%", g => model.Level.ToString() }
};
}
public bool TryGetUserId(out ulong userId)
{
userId = UserId;
return true;
}
public bool TryGetGuildId(out ulong guildId)
{
guildId = GuildId;
return true;
}
}

View file

@ -0,0 +1,38 @@
using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration;
public record struct LevelUpNotifyModel(
ulong GuildId,
ulong ChannelId,
ulong UserId,
long Level) : INotifyModel
{
public static string KeyName
=> "notify.levelup";
public static NotifyType NotifyType
=> NotifyType.LevelUp;
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
{
var data = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%event.level%", g => data.Level.ToString() },
{ "%event.user%", g => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() },
};
}
public bool TryGetGuildId(out ulong guildId)
{
guildId = GuildId;
return true;
}
public bool TryGetUserId(out ulong userId)
{
userId = UserId;
return true;
}
}

View file

@ -0,0 +1,34 @@
#nullable disable
using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration.Services;
public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel
{
public static string KeyName
=> "notify.protection";
public static NotifyType NotifyType
=> NotifyType.Protection;
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
{
var data = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%event.type%", g => data.ProtType.ToString() },
};
}
public bool TryGetUserId(out ulong userId)
{
userId = UserId;
return true;
}
public bool TryGetGuildId(out ulong guildId)
{
guildId = GuildId;
return true;
}
}

View file

@ -0,0 +1,36 @@
using EllieBot.Db.Models;
using EllieBot.Modules.Administration;
namespace EllieBot.Modules.Xp.Services;
public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel
{
public static string KeyName
=> "notify.reward.removerole";
public static NotifyType NotifyType
=> NotifyType.RemoveRoleReward;
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
{
var model = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() },
{ "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() },
{ "%event.level%", g => model.Level.ToString() },
};
}
public bool TryGetUserId(out ulong userId)
{
userId = UserId;
return true;
}
public bool TryGetGuildId(out ulong guildId)
{
guildId = GuildId;
return true;
}
}

View file

@ -0,0 +1,114 @@
using EllieBot.Db.Models;
using System.Text;
namespace EllieBot.Modules.Administration;
public partial class Administration
{
public class NotifyCommands : EllieModule<NotifyService>
{
[Cmd]
[UserPerm(GuildPerm.Administrator)]
public async Task Notify()
{
await Response()
.Paginated()
.Items(Enum.GetValues<NotifyType>())
.PageSize(5)
.Page((items, page) =>
{
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.notify_available));
foreach (var item in items)
{
eb.AddField(item.ToString(), GetText(GetDescription(item)), false);
}
return eb;
})
.SendAsync();
}
private LocStr GetDescription(NotifyType item)
=> item switch
{
NotifyType.LevelUp => strs.notify_desc_levelup,
NotifyType.Protection => strs.notify_desc_protection,
NotifyType.AddRoleReward => strs.notify_desc_addrolerew,
NotifyType.RemoveRoleReward => strs.notify_desc_removerolerew,
_ => strs.notify_desc_not_found
};
[Cmd]
[UserPerm(GuildPerm.Administrator)]
public async Task Notify(NotifyType nType, [Leftover] string? message = null)
{
if (string.IsNullOrWhiteSpace(message))
{
// show msg
var conf = await _service.GetNotifyAsync(ctx.Guild.Id, nType);
if (conf is null)
{
await Response().Confirm(strs.notify_msg_not_set).SendAsync();
return;
}
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.notify_msg))
.WithDescription(conf.Message.TrimTo(2048))
.AddField(GetText(strs.notify_type), conf.Type.ToString(), true)
.AddField(GetText(strs.channel),
$"""
<#{conf.ChannelId}>
`{conf.ChannelId}`
""",
true);
await Response().Embed(eb).SendAsync();
return;
}
await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nType, message);
await Response().Confirm(strs.notify_on($"<#{ctx.Channel.Id}>", Format.Bold(nType.ToString()))).SendAsync();
}
[Cmd]
[UserPerm(GuildPerm.Administrator)]
public async Task NotifyList(int page = 1)
{
if (--page < 0)
return;
var notifs = await _service.GetForGuildAsync(ctx.Guild.Id);
var sb = new StringBuilder();
foreach (var notif in notifs)
{
sb.AppendLine($"""
- **{notif.Type}**
<#{notif.ChannelId}> `{notif.ChannelId}`
""");
}
if (notifs.Count == 0)
sb.AppendLine(GetText(strs.notify_none));
await Response()
.Confirm(GetText(strs.notify_list), text: sb.ToString())
.SendAsync();
}
[Cmd]
[UserPerm(GuildPerm.Administrator)]
public async Task NotifyClear(NotifyType nType)
{
await _service.DisableAsync(ctx.Guild.Id, nType);
await Response().Confirm(strs.notify_off(nType)).SendAsync();
}
}
}

View file

@ -0,0 +1,6 @@
namespace EllieBot.Modules.Administration;
public static class NotifyKeys
{
public static TypedKey<LevelUpNotifyModel> LevelUp { get; } = new("notify:levelup");
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.Modules.Administration;
public static class NotifyModelExtensions
{
public static TypedKey<T> GetTypedKey<T>(this T model)
where T : struct, INotifyModel
=> new(T.KeyName);
}

View file

@ -0,0 +1,227 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Generators;
namespace EllieBot.Modules.Administration;
public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
{
private readonly DbService _db;
private readonly IMessageSenderService _mss;
private readonly DiscordSocketClient _client;
private readonly IBotCreds _creds;
private readonly IReplacementService _repSvc;
private readonly IPubSub _pubSub;
private ConcurrentDictionary<NotifyType, ConcurrentDictionary<ulong, Notify>> _events = new();
public NotifyService(
DbService db,
IMessageSenderService mss,
DiscordSocketClient client,
IBotCreds creds,
IReplacementService repSvc,
IPubSub pubSub)
{
_db = db;
_mss = mss;
_client = client;
_creds = creds;
_repSvc = repSvc;
_pubSub = pubSub;
}
public async Task OnReadyAsync()
{
await using var uow = _db.GetDbContext();
_events = (await uow.GetTable<Notify>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
_creds.TotalShards,
_client.ShardId))
.ToListAsyncLinqToDB())
.GroupBy(x => x.Type)
.ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent())
.ToConcurrent();
await SubscribeToEvent<LevelUpNotifyModel>();
}
private async Task SubscribeToEvent<T>()
where T : struct, INotifyModel
{
await _pubSub.Sub(new TypedKey<T>(T.KeyName), async (model) => await OnEvent(model));
}
public async Task NotifyAsync<T>(T data, bool isShardLocal = false)
where T : struct, INotifyModel
{
try
{
if (isShardLocal)
{
await OnEvent(data);
return;
}
await _pubSub.Pub(data.GetTypedKey(), data);
}
catch (Exception ex)
{
Log.Warning(ex,
"Unknown error occurred while trying to triger {NotifyEvent} for {NotifyModel}",
T.KeyName,
data);
}
}
private async Task OnEvent<T>(T model)
where T : struct, INotifyModel
{
if (_events.TryGetValue(T.NotifyType, out var subs))
{
if (model.TryGetGuildId(out var gid))
{
if (!subs.TryGetValue(gid, out var conf))
return;
await HandleNotifyEvent(conf, model);
return;
}
foreach (var key in subs.Keys.ToArray())
{
if (subs.TryGetValue(key, out var notif))
{
try
{
await HandleNotifyEvent(notif, model);
}
catch (Exception ex)
{
Log.Error(ex,
"Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}",
T.NotifyType,
key,
ex.Message);
}
await Task.Delay(500);
}
}
}
}
private async Task HandleNotifyEvent(Notify conf, INotifyModel model)
{
var guild = _client.GetGuild(conf.GuildId);
var channel = guild?.GetTextChannel(conf.ChannelId);
if (guild is null || channel is null)
return;
IUser? user = null;
if (model.TryGetUserId(out var userId))
{
user = guild.GetUser(userId) ?? _client.GetUser(userId);
}
var rctx = new ReplacementContext(guild: guild, channel: channel, user: user);
var st = SmartText.CreateFrom(conf.Message);
foreach (var modelRep in model.GetReplacements())
{
rctx.WithOverride(modelRep.Key, () => modelRep.Value(guild));
}
st = await _repSvc.ReplaceAsync(st, rctx);
if (st is SmartPlainText spt)
{
await _mss.Response(channel)
.Confirm(spt.Text)
.SendAsync();
return;
}
await _mss.Response(channel)
.Text(st)
.Sanitize(false)
.SendAsync();
}
public async Task EnableAsync(
ulong guildId,
ulong channelId,
NotifyType nType,
string message)
{
await using var uow = _db.GetDbContext();
await uow.GetTable<Notify>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = channelId,
Type = nType,
Message = message,
},
(_) => new()
{
Message = message,
ChannelId = channelId
},
() => new()
{
GuildId = guildId,
Type = nType
});
var eventDict = _events.GetOrAdd(nType, _ => new());
eventDict[guildId] = new()
{
GuildId = guildId,
ChannelId = channelId,
Type = nType,
Message = message
};
}
public async Task DisableAsync(ulong guildId, NotifyType nType)
{
await using var uow = _db.GetDbContext();
var deleted = await uow.GetTable<Notify>()
.Where(x => x.GuildId == guildId && x.Type == nType)
.DeleteAsync();
if (deleted == 0)
return;
if (!_events.TryGetValue(nType, out var guildsDict))
return;
guildsDict.TryRemove(guildId, out _);
}
public async Task<IReadOnlyCollection<Notify>> GetForGuildAsync(ulong guildId, int page = 0)
{
ArgumentOutOfRangeException.ThrowIfNegative(page);
await using var ctx = _db.GetDbContext();
var list = await ctx.GetTable<Notify>()
.Where(x => x.GuildId == guildId)
.OrderBy(x => x.Type)
.Skip(page * 10)
.Take(10)
.ToListAsyncLinqToDB();
return list;
}
public async Task<Notify?> GetNotifyAsync(ulong guildId, NotifyType nType)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<Notify>()
.Where(x => x.GuildId == guildId && x.Type == nType)
.FirstOrDefaultAsyncLinqToDB();
}
}

View file

@ -36,7 +36,7 @@ public partial class Administration
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
public async Task DiscordPermOverrideReset() public async Task DiscordPermOverrideReset()
{ {
var result = await PromptUserConfirmAsync(_sender.CreateEmbed() var result = await PromptUserConfirmAsync(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(GetText(strs.perm_override_all_confirm))); .WithDescription(GetText(strs.perm_override_all_confirm)));
@ -65,7 +65,7 @@ public partial class Administration
.CurrentPage(page) .CurrentPage(page)
.Page((items, _) => .Page((items, _) =>
{ {
var eb = _sender.CreateEmbed().WithTitle(GetText(strs.perm_overrides)).WithOkColor(); var eb = CreateEmbed().WithTitle(GetText(strs.perm_overrides)).WithOkColor();
if (items.Count == 0) if (items.Count == 0)
eb.WithDescription(GetText(strs.perm_override_page_none)); eb.WithDescription(GetText(strs.perm_override_page_none));

View file

@ -6,7 +6,7 @@ namespace EllieBot.Modules.Administration;
public partial class Administration public partial class Administration
{ {
[Group] [Group]
public partial class PlayingRotateCommands : EllieModule<PlayingRotateService> public partial class PlayingRotateCommands : EllieModule<IBotActivityService>
{ {
[Cmd] [Cmd]
[OwnerOnly] [OwnerOnly]

View file

@ -28,17 +28,17 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
public async Task AntiAlt( public async Task AntiAlt(
StoopidTime minAge, ParsedTimespan minAge,
PunishmentAction action, PunishmentAction action,
[Leftover] StoopidTime punishTime = null) [Leftover] ParsedTimespan punishTimespan = null)
{ {
var minAgeMinutes = (int)minAge.Time.TotalMinutes; var minAgeMinutes = (int)minAge.Time.TotalMinutes;
var punishTimeMinutes = (int?)punishTime?.Time.TotalMinutes ?? 0; var punishTimeMinutes = (int?)punishTimespan?.Time.TotalMinutes ?? 0;
if (minAgeMinutes < 1 || punishTimeMinutes < 0) if (minAgeMinutes < 1 || punishTimeMinutes < 0)
return; return;
var minutes = (int?)punishTime?.Time.TotalMinutes ?? 0; var minutes = (int?)punishTimespan?.Time.TotalMinutes ?? 0;
if (action is PunishmentAction.TimeOut && minutes < 1) if (action is PunishmentAction.TimeOut && minutes < 1)
minutes = 1; minutes = 1;
@ -53,7 +53,7 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover] IRole role) public async Task AntiAlt(ParsedTimespan minAge, PunishmentAction action, [Leftover] IRole role)
{ {
var minAgeMinutes = (int)minAge.Time.TotalMinutes; var minAgeMinutes = (int)minAge.Time.TotalMinutes;
@ -86,8 +86,8 @@ public partial class Administration
int userThreshold, int userThreshold,
int seconds, int seconds,
PunishmentAction action, PunishmentAction action,
[Leftover] StoopidTime punishTime) [Leftover] ParsedTimespan punishTimespan)
=> InternalAntiRaid(userThreshold, seconds, action, punishTime); => InternalAntiRaid(userThreshold, seconds, action, punishTimespan);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
@ -100,7 +100,7 @@ public partial class Administration
int userThreshold, int userThreshold,
int seconds = 10, int seconds = 10,
PunishmentAction action = PunishmentAction.Mute, PunishmentAction action = PunishmentAction.Mute,
StoopidTime punishTime = null) ParsedTimespan punishTimespan = null)
{ {
if (action == PunishmentAction.AddRole) if (action == PunishmentAction.AddRole)
{ {
@ -120,13 +120,13 @@ public partial class Administration
return; return;
} }
if (punishTime is not null) if (punishTimespan is not null)
{ {
if (!_service.IsDurationAllowed(action)) if (!_service.IsDurationAllowed(action))
await Response().Error(strs.prot_cant_use_time).SendAsync(); await Response().Error(strs.prot_cant_use_time).SendAsync();
} }
var time = (int?)punishTime?.Time.TotalMinutes ?? 0; var time = (int?)punishTimespan?.Time.TotalMinutes ?? 0;
if (time is < 0 or > 60 * 24) if (time is < 0 or > 60 * 24)
return; return;
@ -170,8 +170,8 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
[Priority(1)] [Priority(1)]
public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime) public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] ParsedTimespan punishTimespan)
=> InternalAntiSpam(messageCount, action, punishTime); => InternalAntiSpam(messageCount, action, punishTimespan);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
@ -183,19 +183,19 @@ public partial class Administration
private async Task InternalAntiSpam( private async Task InternalAntiSpam(
int messageCount, int messageCount,
PunishmentAction action, PunishmentAction action,
StoopidTime timeData = null, ParsedTimespan timespanData = null,
IRole role = null) IRole role = null)
{ {
if (messageCount is < 2 or > 10) if (messageCount is < 2 or > 10)
return; return;
if (timeData is not null) if (timespanData is not null)
{ {
if (!_service.IsDurationAllowed(action)) if (!_service.IsDurationAllowed(action))
await Response().Error(strs.prot_cant_use_time).SendAsync(); await Response().Error(strs.prot_cant_use_time).SendAsync();
} }
var time = (int?)timeData?.Time.TotalMinutes ?? 0; var time = (int?)timespanData?.Time.TotalMinutes ?? 0;
if (time is < 0 or > 60 * 24) if (time is < 0 or > 60 * 24)
return; return;
@ -241,7 +241,7 @@ public partial class Administration
return; return;
} }
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.prot_active)); var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.prot_active));
if (spam is not null) if (spam is not null)
embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true); embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true);

View file

@ -22,6 +22,7 @@ public class ProtectionService : IEService
private readonly MuteService _mute; private readonly MuteService _mute;
private readonly DbService _db; private readonly DbService _db;
private readonly UserPunishService _punishService; private readonly UserPunishService _punishService;
private readonly INotifySubscriber _notifySub;
private readonly Channel<PunishQueueItem> _punishUserQueue = private readonly Channel<PunishQueueItem> _punishUserQueue =
Channel.CreateUnbounded<PunishQueueItem>(new() Channel.CreateUnbounded<PunishQueueItem>(new()
@ -35,12 +36,14 @@ public class ProtectionService : IEService
IBot bot, IBot bot,
MuteService mute, MuteService mute,
DbService db, DbService db,
UserPunishService punishService) UserPunishService punishService,
INotifySubscriber notifySub)
{ {
_client = client; _client = client;
_mute = mute; _mute = mute;
_db = db; _db = db;
_punishService = punishService; _punishService = punishService;
_notifySub = notifySub;
var ids = client.GetGuildIds(); var ids = client.GetGuildIds();
using (var uow = db.GetDbContext()) using (var uow = db.GetDbContext())
@ -175,6 +178,9 @@ public class ProtectionService : IEService
alts.RoleId, alts.RoleId,
user); user);
await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id,
ProtectionType.Alting,
user.Id));
return; return;
} }
} }
@ -194,6 +200,8 @@ public class ProtectionService : IEService
var settings = stats.AntiRaidSettings; var settings = stats.AntiRaidSettings;
await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users); await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users);
await _notifySub.NotifyAsync(
new ProtectionNotifyModel(user.Guild.Id, ProtectionType.Raiding, users[0].Id));
} }
await Task.Delay(1000 * stats.AntiRaidSettings.Seconds); await Task.Delay(1000 * stats.AntiRaidSettings.Seconds);
@ -246,6 +254,10 @@ public class ProtectionService : IEService
settings.MuteTime, settings.MuteTime,
settings.RoleId, settings.RoleId,
(IGuildUser)msg.Author); (IGuildUser)msg.Author);
await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId,
ProtectionType.Spamming,
msg.Author.Id));
} }
} }
} }

View file

@ -113,7 +113,7 @@ public partial class Administration
{ {
await progressMsg.ModifyAsync(props => await progressMsg.ModifyAsync(props =>
{ {
props.Embed = _sender.CreateEmbed() props.Embed = CreateEmbed()
.WithPendingColor() .WithPendingColor()
.WithDescription(GetText(strs.prune_progress(deleted, total))) .WithDescription(GetText(strs.prune_progress(deleted, total)))
.Build(); .Build();

View file

@ -0,0 +1,302 @@
using EllieBot.Common.TypeReaders.Models;
using EllieBot.Db.Models;
using EllieBot.Modules.Administration.Services;
using System.Text;
using ContextType = Discord.Commands.ContextType;
namespace EllieBot.Modules.Administration;
public partial class Administration
{
[Group("btr")]
public partial class ButtonRoleCommands : EllieModule<ButtonRolesService>
{
private List<ActionRowBuilder> GetActionRows(IReadOnlyList<ButtonRole> roles)
{
var rows = roles.Select((x, i) => (Index: i, ButtonRole: x))
.GroupBy(x => x.Index / 5)
.Select(x => x.Select(y => y.ButtonRole))
.Select(x =>
{
var ab = new ActionRowBuilder()
.WithComponents(x.Select(y =>
{
var curRole = ctx.Guild.GetRole(y.RoleId);
var label = string.IsNullOrWhiteSpace(y.Label)
? curRole?.ToString() ?? "?missing " + y.RoleId
: y.Label;
var btnEmote = EmoteTypeReader.TryParse(y.Emote, out var e)
? e
: null;
return new ButtonBuilder()
.WithCustomId(y.ButtonId)
.WithEmote(btnEmote)
.WithLabel(label)
.WithStyle(ButtonStyle.Secondary)
.Build() as IMessageComponent;
})
.ToList());
return ab;
})
.ToList();
return rows;
}
private async Task<MessageLink?> CreateMessageLinkAsync(ulong messageId)
{
var msg = await ctx.Channel.GetMessageAsync(messageId);
if (msg is null)
return null;
return new MessageLink(ctx.Guild, ctx.Channel, msg);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleAdd(ulong messageId, IEmote emote, [Leftover] IRole role)
{
var link = await CreateMessageLinkAsync(messageId);
if (link is null)
{
await Response().Error(strs.invalid_message_id).SendAsync();
return;
}
await BtnRoleAdd(link, emote, role);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleAdd(MessageLink link, IEmote emote, [Leftover] IRole role)
{
if (link.Message is not IUserMessage msg || !msg.IsAuthor(ctx.Client))
{
await Response().Error(strs.invalid_message_link).SendAsync();
return;
}
if (!await CheckRoleHierarchy(role))
{
await Response().Error(strs.hierarchy).SendAsync();
return;
}
var success = await _service.AddButtonRole(ctx.Guild.Id, link.Channel.Id, role.Id, link.Message.Id, emote);
if (!success)
{
await Response().Error(strs.btnrole_message_max).SendAsync();
return;
}
var roles = await _service.GetButtonRoles(ctx.Guild.Id, link.Message.Id);
var rows = GetActionRows(roles);
await msg.ModifyAsync(x => x.Components = new(new ComponentBuilder().WithRows(rows).Build()));
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleRemove(ulong messageId, IRole role)
=> BtnRoleRemove(messageId, role.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleRemove(MessageLink link, IRole role)
=> BtnRoleRemove(link.Message.Id, role.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleRemove(MessageLink link, ulong roleId)
=> BtnRoleRemove(link.Message.Id, roleId);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleRemove(ulong messageId, ulong roleId)
{
var removed = await _service.RemoveButtonRole(ctx.Guild.Id, messageId, roleId);
if (removed is null)
{
await Response().Error(strs.btnrole_not_found).SendAsync();
return;
}
var roles = await _service.GetButtonRoles(ctx.Guild.Id, messageId);
var ch = await ctx.Guild.GetTextChannelAsync(removed.ChannelId);
if (ch is null)
{
await Response().Error(strs.btnrole_removeall_not_found).SendAsync();
return;
}
var msg = await ch.GetMessageAsync(removed.MessageId) as IUserMessage;
if (msg is null)
{
await Response().Error(strs.btnrole_removeall_not_found).SendAsync();
return;
}
var rows = GetActionRows(roles);
await msg.ModifyAsync(x => x.Components = new(new ComponentBuilder().WithRows(rows).Build()));
await Response().Confirm(strs.btnrole_removed).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleRemoveAll(MessageLink link)
=> BtnRoleRemoveAll(link.Message.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleRemoveAll(ulong messageId)
{
var succ = await _service.RemoveButtonRoles(ctx.Guild.Id, messageId);
if (succ.Count == 0)
{
await Response().Error(strs.btnrole_not_found).SendAsync();
return;
}
var info = succ[0];
var ch = await ctx.Guild.GetTextChannelAsync(info.ChannelId);
if (ch is null)
{
await Response().Pending(strs.btnrole_removeall_not_found).SendAsync();
return;
}
var msg = await ch.GetMessageAsync(info.MessageId) as IUserMessage;
if (msg is null)
{
await Response().Pending(strs.btnrole_removeall_not_found).SendAsync();
return;
}
await msg.ModifyAsync(x => x.Components = new(new ComponentBuilder().Build()));
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleList()
{
var btnRoles = await _service.GetButtonRoles(ctx.Guild.Id, null);
var groups = btnRoles
.GroupBy(x => (x.ChannelId, x.MessageId))
.ToList();
await Response()
.Paginated()
.Items(groups)
.PageSize(1)
.AddFooter(false)
.Page(async (items, page) =>
{
var eb = CreateEmbed()
.WithOkColor();
var item = items.FirstOrDefault();
if (item == default)
{
eb.WithPendingColor()
.WithDescription(GetText(strs.btnrole_none));
return eb;
}
var (cid, msgId) = item.Key;
var str = new StringBuilder();
var ch = await ctx.Client.GetChannelAsync(cid) as IMessageChannel;
str.AppendLine($"Channel: {ch?.ToString() ?? cid.ToString()}");
str.AppendLine($"Message: {msgId}");
if (ch is not null)
{
var msg = await ch.GetMessageAsync(msgId);
if (msg is not null)
{
str.AppendLine(new MessageLink(ctx.Guild, ch, msg).ToString());
}
}
str.AppendLine("---");
foreach (var x in item.AsEnumerable())
{
var role = ctx.Guild.GetRole(x.RoleId);
str.AppendLine($"{x.Emote} {(role?.ToString() ?? x.RoleId.ToString())}");
}
eb.WithDescription(str.ToString());
return eb;
})
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleExclusive(MessageLink link, PermissionAction exclusive)
=> BtnRoleExclusive(link.Message.Id, exclusive);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleExclusive(ulong messageId, PermissionAction exclusive)
{
var res = await _service.SetExclusiveButtonRoles(ctx.Guild.Id, messageId, exclusive.Value);
if (!res)
{
await Response().Error(strs.btnrole_not_found).SendAsync();
return;
}
if (exclusive.Value)
{
await Response().Confirm(strs.btnrole_exclusive).SendAsync();
}
else
{
await Response().Confirm(strs.btnrole_multiple).SendAsync();
}
}
}
}

View file

@ -0,0 +1,187 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using LinqToDB.SqlQuery;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using NCalc;
namespace EllieBot.Modules.Administration.Services;
public sealed class ButtonRolesService : IEService, IReadyExecutor
{
private const string BTN_PREFIX = "n:btnrole:";
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCreds _creds;
public ButtonRolesService(IBotCreds creds, DiscordSocketClient client, DbService db)
{
_creds = creds;
_client = client;
_db = db;
}
public Task OnReadyAsync()
{
_client.InteractionCreated += OnInteraction;
return Task.CompletedTask;
}
private async Task OnInteraction(SocketInteraction inter)
{
if (inter is not SocketMessageComponent smc)
return;
if (!smc.Data.CustomId.StartsWith(BTN_PREFIX))
return;
await inter.DeferAsync();
_ = Task.Run(async () =>
{
try
{
await using var uow = _db.GetDbContext();
var buttonRole = await uow.GetTable<ButtonRole>()
.Where(x => x.ButtonId == smc.Data.CustomId && x.MessageId == smc.Message.Id)
.FirstOrDefaultAsyncLinqToDB();
if (buttonRole is null)
return;
var guild = _client.GetGuild(buttonRole.GuildId);
if (guild is null)
return;
var role = guild.GetRole(buttonRole.RoleId);
if (role is null)
return;
if (smc.User is not IGuildUser user)
return;
if (user.GetRoles().Any(x => x.Id == role.Id))
{
await user.RemoveRoleAsync(role.Id);
return;
}
if (buttonRole.Exclusive)
{
var otherRoles = await uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == smc.GuildId && x.MessageId == smc.Message.Id)
.Select(x => x.RoleId)
.ToListAsyncLinqToDB();
await user.RemoveRolesAsync(otherRoles);
}
await user.AddRoleAsync(role.Id);
}
catch (Exception ex)
{
Log.Warning(ex, "Unable to handle button role interaction for user {UserId}", inter.User.Id);
}
});
}
public async Task<bool> AddButtonRole(
ulong guildId,
ulong channelId,
ulong roleId,
ulong messageId,
IEmote emote
)
{
await using var uow = _db.GetDbContext();
// up to 25 per message
if (await uow.GetTable<ButtonRole>()
.Where(x => x.MessageId == messageId)
.CountAsyncLinqToDB()
>= 25)
return false;
var emoteStr = emote.ToString()!;
var guid = Guid.NewGuid();
await uow.GetTable<ButtonRole>()
.InsertOrUpdateAsync(() => new ButtonRole()
{
GuildId = guildId,
ChannelId = channelId,
RoleId = roleId,
MessageId = messageId,
Position =
uow
.GetTable<ButtonRole>()
.Any(x => x.MessageId == messageId)
? uow.GetTable<ButtonRole>()
.Where(x => x.MessageId == messageId)
.Max(x => x.Position)
: 1,
Emote = emoteStr,
Label = string.Empty,
ButtonId = $"{BTN_PREFIX}:{guildId}:{guid}",
Exclusive = uow.GetTable<ButtonRole>()
.Any(x => x.GuildId == guildId && x.MessageId == messageId)
&& uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.All(x => x.Exclusive)
},
_ => new()
{
Emote = emoteStr,
Label = string.Empty,
ButtonId = $"{BTN_PREFIX}:{guildId}:{guid}"
},
() => new()
{
RoleId = roleId,
MessageId = messageId,
});
return true;
}
public async Task<IReadOnlyList<ButtonRole>> RemoveButtonRoles(ulong guildId, ulong messageId)
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.DeleteWithOutputAsync();
}
public async Task<ButtonRole?> RemoveButtonRole(ulong guildId, ulong messageId, ulong roleId)
{
await using var uow = _db.GetDbContext();
var deleted = await uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId && x.RoleId == roleId)
.DeleteWithOutputAsync();
return deleted.FirstOrDefault();
}
public async Task<IReadOnlyList<ButtonRole>> GetButtonRoles(ulong guildId, ulong? messageId)
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && (messageId == null || x.MessageId == messageId))
.OrderBy(x => x.Id)
.ToListAsyncLinqToDB();
}
public async Task<bool> SetExclusiveButtonRoles(ulong guildId, ulong messageId, bool exclusive)
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.UpdateAsync((_) => new()
{
Exclusive = exclusive
}) > 0;
}
}

View file

@ -80,7 +80,7 @@ public partial class Administration
.CurrentPage(page) .CurrentPage(page)
.Page((items, _) => .Page((items, _) =>
{ {
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor(); .WithOkColor();
var content = string.Empty; var content = string.Empty;

View file

@ -1,8 +1,6 @@
#nullable disable using LinqToDB;
using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Modules.Patronage;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using OneOf.Types; using OneOf.Types;
using OneOf; using OneOf;
@ -18,18 +16,15 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache; private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new(); private readonly object _cacheLock = new();
private readonly SemaphoreSlim _assignementLock = new(1, 1); private readonly SemaphoreSlim _assignementLock = new(1, 1);
private readonly IPatronageService _ps;
public ReactionRolesService( public ReactionRolesService(
DiscordSocketClient client, DiscordSocketClient client,
IPatronageService ps,
DbService db, DbService db,
IBotCreds creds) IBotCreds creds)
{ {
_db = db; _db = db;
_client = client; _client = client;
_creds = creds; _creds = creds;
_ps = ps;
_cache = new(); _cache = new();
} }
@ -57,7 +52,7 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
var guild = _client.GetGuild(rero.GuildId); var guild = _client.GetGuild(rero.GuildId);
var role = guild?.GetRole(rero.RoleId); var role = guild?.GetRole(rero.RoleId);
if (role is null) if (guild is null || role is null)
return default; return default;
var user = guild.GetUser(userId) as IGuildUser var user = guild.GetUser(userId) as IGuildUser
@ -96,10 +91,11 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
{ {
if (user.RoleIds.Contains(role.Id)) if (user.RoleIds.Contains(role.Id))
{ {
await user.RemoveRoleAsync(role.Id, new RequestOptions() await user.RemoveRoleAsync(role.Id,
{ new RequestOptions()
AuditLogReason = $"Reaction role" {
}); AuditLogReason = $"Reaction role"
});
} }
} }
finally finally
@ -210,10 +206,11 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
} }
} }
await user.AddRoleAsync(role.Id, new() await user.AddRoleAsync(role.Id,
{ new()
AuditLogReason = "Reaction role" {
}); AuditLogReason = "Reaction role"
});
} }
} }
finally finally
@ -244,36 +241,23 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
int levelReq = 0) int levelReq = 0)
{ {
ArgumentOutOfRangeException.ThrowIfNegative(group); ArgumentOutOfRangeException.ThrowIfNegative(group);
ArgumentOutOfRangeException.ThrowIfNegative(levelReq); ArgumentOutOfRangeException.ThrowIfNegative(levelReq);
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await using var tran = await ctx.Database.BeginTransactionAsync();
var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guild.Id)
.CountAsync();
var limit = await _ps.GetUserLimit(LimitedFeatureName.ReactionRole, guild.OwnerId);
if (!_creds.IsOwner(guild.OwnerId) && (activeReactionRoles >= limit.Quota && limit.Quota >= 0))
{
return new Error();
}
await ctx.GetTable<ReactionRoleV2>() await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guild.Id, GuildId = guild.Id,
ChannelId = msg.Channel.Id, ChannelId = msg.Channel.Id,
MessageId = msg.Id, MessageId = msg.Id,
Emote = emote, Emote = emote,
RoleId = role.Id, RoleId = role.Id,
Group = group, Group = group,
LevelReq = levelReq LevelReq = levelReq
}, },
(old) => new() (old) => new()
{ {
RoleId = role.Id, RoleId = role.Id,
@ -286,8 +270,6 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
Emote = emote, Emote = emote,
}); });
await tran.CommitAsync();
var obj = new ReactionRoleV2() var obj = new ReactionRoleV2()
{ {
GuildId = guild.Id, GuildId = guild.Id,
@ -380,9 +362,9 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
var updated = ctx.GetTable<ReactionRoleV2>() var updated = ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == fromMessageId) .Where(x => x.GuildId == guildId && x.MessageId == fromMessageId)
.UpdateWithOutput(old => new() .UpdateWithOutput(old => new()
{ {
MessageId = toMessageId MessageId = toMessageId
}, },
(old, neu) => neu); (old, neu) => neu);
lock (_cacheLock) lock (_cacheLock)
{ {

View file

@ -1,4 +1,6 @@
#nullable disable #nullable disable
using Google.Protobuf.WellKnownTypes;
using EllieBot.Common.TypeReaders.Models;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
@ -13,13 +15,18 @@ public partial class Administration
Excl Excl
} }
private readonly TempRoleService _tempRoleService;
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private StickyRolesService _stickyRoleSvc; private StickyRolesService _stickyRoleSvc;
public RoleCommands(IServiceProvider services, StickyRolesService stickyRoleSvc) public RoleCommands(
IServiceProvider services,
StickyRolesService stickyRoleSvc,
TempRoleService tempRoleService)
{ {
_services = services; _services = services;
_stickyRoleSvc = stickyRoleSvc; _stickyRoleSvc = stickyRoleSvc;
_tempRoleService = tempRoleService;
} }
[Cmd] [Cmd]
@ -34,13 +41,16 @@ public partial class Administration
return; return;
try try
{ {
await targetUser.AddRoleAsync(roleToAdd, new RequestOptions() await targetUser.AddRoleAsync(roleToAdd,
{ new RequestOptions()
AuditLogReason = $"Added by [{ctx.User.Username}]" {
}); AuditLogReason = $"Added by [{ctx.User.Username}]"
});
await Response().Confirm(strs.setrole(Format.Bold(roleToAdd.Name), await Response()
Format.Bold(targetUser.ToString()))).SendAsync(); .Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
Format.Bold(targetUser.ToString())))
.SendAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -62,8 +72,10 @@ public partial class Administration
try try
{ {
await targetUser.RemoveRoleAsync(roleToRemove); await targetUser.RemoveRoleAsync(roleToRemove);
await Response().Confirm(strs.remrole(Format.Bold(roleToRemove.Name), await Response()
Format.Bold(targetUser.ToString()))).SendAsync(); .Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
Format.Bold(targetUser.ToString())))
.SendAsync();
} }
catch catch
{ {
@ -174,12 +186,11 @@ public partial class Administration
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
[Priority(0)] [Priority(0)]
public async Task RoleColor(Color color, [Leftover] IRole role) public async Task RoleColor(Rgba32 color, [Leftover] IRole role)
{ {
try try
{ {
var rgba32 = color.ToPixel<Rgba32>(); await role.ModifyAsync(r => r.Color = new Discord.Color(color.R, color.G, color.B));
await role.ModifyAsync(r => r.Color = new Discord.Color(rgba32.R, rgba32.G, rgba32.B));
await Response().Confirm(strs.rc(Format.Bold(role.Name))).SendAsync(); await Response().Confirm(strs.rc(Format.Bold(role.Name))).SendAsync();
} }
catch (Exception) catch (Exception)
@ -205,5 +216,29 @@ public partial class Administration
await Response().Confirm(strs.sticky_roles_disabled).SendAsync(); await Response().Confirm(strs.sticky_roles_disabled).SendAsync();
} }
} }
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task TempRole(ParsedTimespan timespan, IUser user, [Leftover] IRole role)
{
if (!await CheckRoleHierarchy(role))
{
await Response()
.Error(strs.hierarchy)
.SendAsync();
return;
}
await _tempRoleService.AddTempRoleAsync(ctx.Guild.Id, role.Id, user.Id, timespan.Time);
await Response()
.Confirm(strs.temp_role_added(user.Mention,
Format.Bold(role.Name),
TimestampTag.FromDateTime(DateTime.UtcNow.Add(timespan.Time), TimestampTagStyles.Relative)))
.SendAsync();
}
} }
} }

View file

@ -0,0 +1,140 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration;
public class TempRoleService : IReadyExecutor, IEService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCreds _creds;
private TaskCompletionSource<bool> _tcs = new();
public TempRoleService(
DbService db,
DiscordSocketClient client,
IBotCreds creds)
{
_db = db;
_client = client;
_creds = creds;
}
public async Task AddTempRoleAsync(
ulong guildId,
ulong roleId,
ulong userId,
TimeSpan duration)
{
if (duration == TimeSpan.Zero)
{
await using var uow = _db.GetDbContext();
await uow.GetTable<TempRole>()
.Where(x => x.GuildId == guildId && x.UserId == userId)
.DeleteAsync();
return;
}
var until = DateTime.UtcNow.Add(duration);
await using var ctx = _db.GetDbContext();
await ctx.GetTable<TempRole>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
RoleId = roleId,
UserId = userId,
Remove = false,
ExpiresAt = until
},
(old) => new()
{
ExpiresAt = until,
},
() => new()
{
GuildId = guildId,
UserId = userId,
RoleId = roleId
});
_tcs.TrySetResult(true);
}
public async Task OnReadyAsync()
{
while (true)
{
try
{
_tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
var latest = await _db.GetDbContext()
.GetTable<TempRole>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
_creds.TotalShards,
_client.ShardId))
.OrderBy(x => x.ExpiresAt)
.FirstOrDefaultAsyncLinqToDB();
if (latest == default)
{
await _tcs.Task;
continue;
}
var now = DateTime.UtcNow;
if (latest.ExpiresAt > now)
{
await Task.WhenAny(Task.Delay(latest.ExpiresAt - now), _tcs.Task);
continue;
}
var deleted = await _db.GetDbContext()
.GetTable<TempRole>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
_creds.TotalShards,
_client.ShardId)
&& x.ExpiresAt <= now)
.DeleteWithOutputAsync();
foreach (var d in deleted)
{
try
{
await RemoveRole(d);
}
catch
{
Log.Warning("Unable to remove temp role {RoleId} from user {UserId}",
d.RoleId,
d.UserId);
}
await Task.Delay(1000);
}
}
catch (Exception ex)
{
Log.Error(ex, "Unexpected error occurred in temprole loop");
await Task.Delay(1000);
}
}
}
private async Task RemoveRole(TempRole tempRole)
{
var guild = _client.GetGuild(tempRole.GuildId);
var role = guild?.GetRole(tempRole.RoleId);
if (role is null)
return;
var user = guild?.GetUser(tempRole.UserId);
if (user is null)
return;
await user.RemoveRoleAsync(role);
}
}

View file

@ -5,33 +5,131 @@ using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration.Services; namespace EllieBot.Modules.Administration.Services;
public sealed class PlayingRotateService : IEService, IReadyExecutor public sealed class BotActivityService : IBotActivityService, IReadyExecutor, IEService
{ {
private readonly BotConfigService _bss; private readonly TypedKey<ActivityPubData> _activitySetKey = new("activity.set");
private readonly SelfService _selfService;
private readonly IReplacementService _repService;
// private readonly Replacer _rep;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
public PlayingRotateService( private readonly IPubSub _pubSub;
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly IReplacementService _rep;
private readonly BotConfigService _bss;
public BotActivityService(
IPubSub pubSub,
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
BotConfigService bss, IReplacementService rep,
IEnumerable<IPlaceholderProvider> phProviders, BotConfigService bss)
SelfService selfService,
IReplacementService repService)
{ {
_db = db; _pubSub = pubSub;
_bss = bss;
_selfService = selfService;
_repService = repService;
_client = client; _client = client;
_db = db;
_rep = rep;
_bss = bss;
}
public async Task<string> RemovePlayingAsync(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
await using var uow = _db.GetDbContext();
var toRemove = await uow.Set<RotatingPlayingStatus>()
.AsQueryable()
.AsNoTracking()
.Skip(index)
.FirstOrDefaultAsync();
if (toRemove is null)
return null;
uow.Remove(toRemove);
await uow.SaveChangesAsync();
return toRemove.Status;
}
public async Task AddPlaying(ActivityType activityType, string status)
{
await using var uow = _db.GetDbContext();
var toAdd = new RotatingPlayingStatus
{
Status = status,
Type = (EllieBot.Db.DbActivityType)activityType
};
uow.Add(toAdd);
await uow.SaveChangesAsync();
}
public void DisableRotatePlaying()
{
_bss.ModifyConfig(bs => { bs.RotateStatuses = false; });
}
public bool ToggleRotatePlaying()
{
var enabled = false;
_bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; });
return enabled;
}
public IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses()
{
using var uow = _db.GetDbContext();
return uow.Set<RotatingPlayingStatus>().AsNoTracking().ToList();
}
public Task SetActivityAsync(string game, ActivityType? type)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = game,
Link = null,
Type = type
});
public Task SetStreamAsync(string name, string link)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = name,
Link = link,
Type = ActivityType.Streaming
});
private sealed class ActivityPubData
{
public string Name { get; init; }
public string Link { get; init; }
public ActivityType? Type { get; init; }
} }
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
await _pubSub.Sub(_activitySetKey,
async data =>
{
if (_client.ShardId == 0)
{
DisableRotatePlaying();
}
try
{
if (data.Type is { } activityType)
{
await _client.SetGameAsync(data.Name, data.Link, activityType);
}
else
{
await _client.SetCustomStatusAsync(data.Name);
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error setting activity");
}
});
if (_client.ShardId != 0) if (_client.ShardId != 0)
return; return;
@ -57,8 +155,8 @@ public sealed class PlayingRotateService : IEService, IReadyExecutor
? rotatingStatuses[index = 0] ? rotatingStatuses[index = 0]
: rotatingStatuses[index++]; : rotatingStatuses[index++];
var statusText = await _repService.ReplaceAsync(playingStatus.Status, new (client: _client)); var statusText = await _rep.ReplaceAsync(playingStatus.Status, new(client: _client));
await _selfService.SetActivityAsync(statusText, (ActivityType)playingStatus.Type); await SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -66,44 +164,4 @@ public sealed class PlayingRotateService : IEService, IReadyExecutor
} }
} }
} }
public async Task<string> RemovePlayingAsync(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
await using var uow = _db.GetDbContext();
var toRemove = await uow.Set<RotatingPlayingStatus>().AsQueryable().AsNoTracking().Skip(index).FirstOrDefaultAsync();
if (toRemove is null)
return null;
uow.Remove(toRemove);
await uow.SaveChangesAsync();
return toRemove.Status;
}
public async Task AddPlaying(ActivityType activityType, string status)
{
await using var uow = _db.GetDbContext();
var toAdd = new RotatingPlayingStatus
{
Status = status,
Type = (EllieBot.Db.DbActivityType)activityType
};
uow.Add(toAdd);
await uow.SaveChangesAsync();
}
public bool ToggleRotatePlaying()
{
var enabled = false;
_bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; });
return enabled;
}
public IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses()
{
using var uow = _db.GetDbContext();
return uow.Set<RotatingPlayingStatus>().AsNoTracking().ToList();
}
} }

View file

@ -19,7 +19,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private const string RELEASES_URL = "https://toastielab.dev/api/v1/repos/Emotions-stuff/elliebot/releases"; private const string RELEASES_URL = "https://toastielab.dev/api/v1/repos/EllieBotDevs/elliebot/releases";
public CheckForUpdatesService( public CheckForUpdatesService(
BotConfigService bcs, BotConfigService bcs,
@ -72,7 +72,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
UpdateLastKnownVersion(latestVersion); UpdateLastKnownVersion(latestVersion);
// pull changelog // pull changelog
var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/elliebot/raw/branch/v5/CHANGELOG.md"); var changelog = await http.GetStringAsync("https://toastielab.dev/EllieBotDevs/elliebot/raw/branch/v5/CHANGELOG.md");
var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog); var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog);
@ -95,7 +95,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
.WithOkColor() .WithOkColor()
.WithAuthor($"EllieBot v{latest} Released!") .WithAuthor($"EllieBot v{latest} Released!")
.WithTitle("Changelog") .WithTitle("Changelog")
.WithUrl("https://toastielab.dev/Emotions-stuff/elliebot/src/branch/v5/CHANGELOG.md") .WithUrl("https://toastielab.dev/EllieBotDevs/elliebot/src/branch/v5/CHANGELOG.md")
.WithDescription(thisVersionChangelog.TrimTo(4096)) .WithDescription(thisVersionChangelog.TrimTo(4096))
.WithFooter( .WithFooter(
"You may disable these messages by typing '.conf bot checkforupdates false'"); "You may disable these messages by typing '.conf bot checkforupdates false'");

View file

@ -0,0 +1,14 @@
#nullable disable
using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration.Services;
public interface IBotActivityService
{
Task SetActivityAsync(string game, ActivityType? type);
Task SetStreamAsync(string name, string link);
bool ToggleRotatePlaying();
Task AddPlaying(ActivityType statusType, string status);
Task<string> RemovePlayingAsync(int index);
IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses();
}

View file

@ -24,19 +24,22 @@ public partial class Administration
private readonly IMarmaladeLoaderService _marmaladeLoader; private readonly IMarmaladeLoaderService _marmaladeLoader;
private readonly ICoordinator _coord; private readonly ICoordinator _coord;
private readonly DbService _db; private readonly DbService _db;
private readonly IBotActivityService _bas;
public SelfCommands( public SelfCommands(
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
IBotStrings strings, IBotStrings strings,
ICoordinator coord, ICoordinator coord,
IMarmaladeLoaderService marmaladeLoader) IMarmaladeLoaderService marmaladeLoader,
IBotActivityService bas)
{ {
_client = client; _client = client;
_db = db; _db = db;
_strings = strings; _strings = strings;
_coord = coord; _coord = coord;
_marmaladeLoader = marmaladeLoader; _marmaladeLoader = marmaladeLoader;
_bas = bas;
} }
@ -62,10 +65,10 @@ public partial class Administration
var (added, updated) = await _service.RefreshUsersAsync(users); var (added, updated) = await _service.RefreshUsersAsync(users);
await message.ModifyAsync(x => await message.ModifyAsync(x =>
x.Embed = _sender.CreateEmbed() x.Embed = CreateEmbed()
.WithDescription(GetText(strs.cache_users_done(added, updated))) .WithDescription(GetText(strs.cache_users_done(added, updated)))
.WithOkColor() .WithOkColor()
.Build() .Build()
); );
} }
@ -115,14 +118,14 @@ public partial class Administration
_service.AddNewAutoCommand(cmd); _service.AddNewAutoCommand(cmd);
await Response() await Response()
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.scadd)) .WithTitle(GetText(strs.scadd))
.AddField(GetText(strs.server), .AddField(GetText(strs.server),
cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}", cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
true) true)
.AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true) .AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
.AddField(GetText(strs.command_text), cmdText)) .AddField(GetText(strs.command_text), cmdText))
.SendAsync(); .SendAsync();
} }
@ -320,16 +323,16 @@ public partial class Administration
.ToArray()); .ToArray());
var allShardStrings = statuses.Select(st => var allShardStrings = statuses.Select(st =>
{ {
var timeDiff = DateTime.UtcNow - st.LastUpdate; var timeDiff = DateTime.UtcNow - st.LastUpdate;
var stateStr = ConnectionStateToEmoji(st); var stateStr = ConnectionStateToEmoji(st);
var maxGuildCountLength = var maxGuildCountLength =
statuses.Max(x => x.GuildCount).ToString().Length; statuses.Max(x => x.GuildCount).ToString().Length;
return $"`{stateStr} " return $"`{stateStr} "
+ $"| #{st.ShardId.ToString().PadBoth(3)} " + $"| #{st.ShardId.ToString().PadBoth(3)} "
+ $"| {timeDiff:mm\\:ss} " + $"| {timeDiff:mm\\:ss} "
+ $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `"; + $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `";
}) })
.ToArray(); .ToArray();
await Response() await Response()
.Paginated() .Paginated()
@ -343,7 +346,7 @@ public partial class Administration
if (string.IsNullOrWhiteSpace(str)) if (string.IsNullOrWhiteSpace(str))
str = GetText(strs.no_shards_on_page); str = GetText(strs.no_shards_on_page);
return _sender.CreateEmbed().WithOkColor().WithDescription($"{status}\n\n{str}"); return CreateEmbed().WithOkColor().WithDescription($"{status}\n\n{str}");
}) })
.SendAsync(); .SendAsync();
} }
@ -437,7 +440,8 @@ public partial class Administration
return; return;
} }
try { await Response().Confirm(strs.restarting).SendAsync(); } try
{ await Response().Confirm(strs.restarting).SendAsync(); }
catch { } catch { }
} }
@ -496,7 +500,7 @@ public partial class Administration
// var rep = new ReplacementBuilder().WithDefault(Context).Build(); // var rep = new ReplacementBuilder().WithDefault(Context).Build();
var repCtx = new ReplacementContext(ctx); var repCtx = new ReplacementContext(ctx);
await _service.SetActivityAsync(game is null ? game : await repSvc.ReplaceAsync(game, repCtx), type); await _bas.SetActivityAsync(game is null ? game : await repSvc.ReplaceAsync(game, repCtx), type);
await Response().Confirm(strs.set_activity).SendAsync(); await Response().Confirm(strs.set_activity).SendAsync();
} }
@ -518,7 +522,7 @@ public partial class Administration
{ {
name ??= ""; name ??= "";
await _service.SetStreamAsync(name, url); await _bas.SetStreamAsync(name, url);
await Response().Confirm(strs.set_stream).SendAsync(); await Response().Confirm(strs.set_stream).SendAsync();
} }

View file

@ -28,7 +28,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
//keys //keys
private readonly TypedKey<ActivityPubData> _activitySetKey;
private readonly TypedKey<string> _guildLeaveKey; private readonly TypedKey<string> _guildLeaveKey;
public SelfService( public SelfService(
@ -51,11 +50,8 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
_bss = bss; _bss = bss;
_pubSub = pubSub; _pubSub = pubSub;
_sender = sender; _sender = sender;
_activitySetKey = new("activity.set");
_guildLeaveKey = new("guild.leave"); _guildLeaveKey = new("guild.leave");
HandleStatusChanges();
_pubSub.Sub(_guildLeaveKey, _pubSub.Sub(_guildLeaveKey,
async input => async input =>
{ {
@ -394,49 +390,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
return channelId is not null; return channelId is not null;
} }
private void HandleStatusChanges()
=> _pubSub.Sub(_activitySetKey,
async data =>
{
try
{
if (data.Type is { } activityType)
await _client.SetGameAsync(data.Name, data.Link, activityType);
else
await _client.SetCustomStatusAsync(data.Name);
}
catch (Exception ex)
{
Log.Warning(ex, "Error setting activity");
}
});
public Task SetActivityAsync(string game, ActivityType? type)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = game,
Link = null,
Type = type
});
public Task SetStreamAsync(string name, string link)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = name,
Link = link,
Type = ActivityType.Streaming
});
private sealed class ActivityPubData
{
public string Name { get; init; }
public string Link { get; init; }
public ActivityType? Type { get; init; }
}
/// <summary> /// <summary>
/// Adds the specified <paramref name="users"/> to the database. If a database user with placeholder name /// Adds the specified <paramref name="users"/> to the database. If a database user with placeholder name
/// and discriminator is present in <paramref name="users"/>, their name and discriminator get updated accordingly. /// and discriminator is present in <paramref name="users"/>, their name and discriminator get updated accordingly.

View file

@ -6,16 +6,136 @@ namespace EllieBot.Modules.Administration;
public partial class Administration public partial class Administration
{ {
[Group] public partial class SelfAssignedRolesHelpers : EllieModule<SelfAssignedRolesService>
{
private readonly SarAssignerService _sas;
public SelfAssignedRolesHelpers(SarAssignerService sas)
{
_sas = sas;
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Iam([Leftover] IRole role)
{
var guildUser = (IGuildUser)ctx.User;
var group = await _service.GetRoleGroup(ctx.Guild.Id, role.Id);
IUserMessage msg = null;
try
{
if (group is null)
{
msg = await Response().Error(strs.self_assign_not).SendAsync();
return;
}
var tcs = new TaskCompletionSource<SarAssignResult>(TaskCreationOptions.RunContinuationsAsynchronously);
await _sas.Add(new()
{
Group = group,
RoleId = role.Id,
User = guildUser,
CompletionTask = tcs
});
var res = await tcs.Task;
if (res.TryPickT0(out _, out var error))
{
msg = await Response()
.Confirm(strs.self_assign_success(Format.Bold(role.Name)))
.SendAsync();
}
else
{
var resStr = error.Match(
_ => strs.error_occured,
lvlReq => strs.self_assign_not_level(Format.Bold(lvlReq.Level.ToString())),
roleRq => strs.self_assign_role_req(Format.Bold(ctx.Guild.GetRole(roleRq.RoleId).ToString()
?? "missing role " + roleRq.RoleId),
group.Name),
_ => strs.self_assign_already(Format.Bold(role.Name)),
_ => strs.self_assign_perms);
msg = await Response().Error(resStr).SendAsync();
}
}
finally
{
var ad = _service.GetAutoDelete(ctx.Guild.Id);
if (ad)
{
msg?.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Iamnot([Leftover] IRole role)
{
var guildUser = (IGuildUser)ctx.User;
IUserMessage msg = null;
try
{
if (!guildUser.RoleIds.Contains(role.Id))
{
msg = await Response().Error(strs.self_assign_not_have(Format.Bold(role.Name))).SendAsync();
return;
}
var group = await _service.GetRoleGroup(ctx.Guild.Id, role.Id);
if (group is null || group.Roles.All(x => x.RoleId != role.Id))
{
msg = await Response().Error(strs.self_assign_not).SendAsync();
return;
}
if (role.Position >= ((SocketGuild)ctx.Guild).CurrentUser.Roles.Max(x => x.Position))
{
msg = await Response().Error(strs.self_assign_perms).SendAsync();
return;
}
await guildUser.RemoveRoleAsync(role);
msg = await Response().Confirm(strs.self_assign_remove(Format.Bold(role.Name))).SendAsync();
}
finally
{
var ad = _service.GetAutoDelete(ctx.Guild.Id);
if (ad)
{
msg?.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
}
}
}
[Group("sar")]
public partial class SelfAssignedRolesCommands : EllieModule<SelfAssignedRolesService> public partial class SelfAssignedRolesCommands : EllieModule<SelfAssignedRolesService>
{ {
private readonly SarAssignerService _sas;
public SelfAssignedRolesCommands(SarAssignerService sas)
{
_sas = sas;
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[BotPerm(GuildPerm.ManageMessages)] [BotPerm(GuildPerm.ManageMessages)]
public async Task AdSarm() public async Task SarAutoDelete()
{ {
var newVal = _service.ToggleAdSarm(ctx.Guild.Id); var newVal = await _service.ToggleAutoDelete(ctx.Guild.Id);
if (newVal) if (newVal)
await Response().Confirm(strs.adsarm_enable(prefix)).SendAsync(); await Response().Confirm(strs.adsarm_enable(prefix)).SendAsync();
@ -28,30 +148,24 @@ public partial class Administration
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
[Priority(1)] [Priority(1)]
public Task Asar([Leftover] IRole role) public Task SarAdd([Leftover] IRole role)
=> Asar(0, role); => SarAdd(0, role);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
[Priority(0)] [Priority(0)]
public async Task Asar(int group, [Leftover] IRole role) public async Task SarAdd(int group, [Leftover] IRole role)
{ {
var guser = (IGuildUser)ctx.User; if (!await CheckRoleHierarchy(role))
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
return; return;
var succ = _service.AddNew(ctx.Guild.Id, role, group); await _service.AddAsync(ctx.Guild.Id, role.Id, group);
if (succ) await Response()
{ .Confirm(strs.role_added(Format.Bold(role.Name), Format.Bold(group.ToString())))
await Response() .SendAsync();
.Confirm(strs.role_added(Format.Bold(role.Name), Format.Bold(group.ToString())))
.SendAsync();
}
else
await Response().Error(strs.role_in_list(Format.Bold(role.Name))).SendAsync();
} }
[Cmd] [Cmd]
@ -59,9 +173,9 @@ public partial class Administration
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
[Priority(0)] [Priority(0)]
public async Task Sargn(int group, [Leftover] string name = null) public async Task SarGroupName(int group, [Leftover] string name = null)
{ {
var set = await _service.SetNameAsync(ctx.Guild.Id, group, name); var set = await _service.SetGroupNameAsync(ctx.Guild.Id, group, name);
if (set) if (set)
{ {
@ -70,19 +184,19 @@ public partial class Administration
.SendAsync(); .SendAsync();
} }
else else
{
await Response().Confirm(strs.group_name_removed(Format.Bold(group.ToString()))).SendAsync(); await Response().Confirm(strs.group_name_removed(Format.Bold(group.ToString()))).SendAsync();
}
} }
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
public async Task Rsar([Leftover] IRole role) public async Task SarRemove([Leftover] IRole role)
{ {
var guser = (IGuildUser)ctx.User; var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
return;
var success = _service.RemoveSar(role.Guild.Id, role.Id); var success = await _service.RemoveAsync(role.Guild.Id, role.Id);
if (!success) if (!success)
await Response().Error(strs.self_assign_not).SendAsync(); await Response().Error(strs.self_assign_not).SendAsync();
else else
@ -91,59 +205,81 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Lsar(int page = 1) public async Task SarList(int page = 1)
{ {
if (--page < 0) if (--page < 0)
return; return;
var (exclusive, roles, groups) = _service.GetRoles(ctx.Guild); var groups = await _service.GetSarsAsync(ctx.Guild.Id);
var gDict = groups.ToDictionary(x => x.Id, x => x);
await Response() await Response()
.Paginated() .Paginated()
.Items(roles.OrderBy(x => x.Model.Group).ToList()) .Items(groups.SelectMany(x => x.Roles).ToList())
.PageSize(20) .PageSize(20)
.CurrentPage(page) .CurrentPage(page)
.Page((items, _) => .Page(async (items, _) =>
{ {
var rolesStr = new StringBuilder();
var roleGroups = items var roleGroups = items
.GroupBy(x => x.Model.Group) .GroupBy(x => x.SarGroupId)
.OrderBy(x => x.Key); .OrderBy(x => x.Key);
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.self_assign_list(groups.Sum(x => x.Roles.Count))));
foreach (var kvp in roleGroups) foreach (var kvp in roleGroups)
{ {
string groupNameText; var group = gDict[kvp.Key];
if (!groups.TryGetValue(kvp.Key, out var name))
groupNameText = Format.Bold(GetText(strs.self_assign_group(kvp.Key)));
else
groupNameText = Format.Bold($"{kvp.Key} - {name.TrimTo(25, true)}");
rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫"); var groupNameText = "";
foreach (var (model, role) in kvp.AsEnumerable())
if (!string.IsNullOrWhiteSpace(group.Name))
groupNameText += $" **{group.Name}**";
groupNameText = $"`{group.GroupNumber}` {groupNameText}";
var rolesStr = new StringBuilder();
if (group.IsExclusive)
{ {
if (role is null) rolesStr.AppendLine(Format.Italics(GetText(strs.choose_one)));
}
if (group.RoleReq is ulong rrId)
{
var rr = ctx.Guild.GetRole(rrId);
if (rr is null)
{ {
await _service.SetGroupRoleReq(group.GuildId, group.GroupNumber, null);
} }
else else
{ {
// first character is invisible space rolesStr.AppendLine(
if (model.LevelRequirement == 0) Format.Italics(GetText(strs.requires_role(Format.Bold(rr.Name)))));
rolesStr.AppendLine(" " + role.Name);
else
rolesStr.AppendLine(" " + role.Name + $" (lvl {model.LevelRequirement}+)");
} }
} }
rolesStr.AppendLine(); foreach (var sar in kvp)
{
var roleName = (ctx.Guild.GetRole(sar.RoleId)?.Name ?? (sar.RoleId + " (deleted)"));
rolesStr.Append("- " + Format.Code(roleName));
if (sar.LevelReq > 0)
{
rolesStr.Append($" *[lvl {sar.LevelReq}+]*");
}
rolesStr.AppendLine();
}
eb.AddField(groupNameText, rolesStr, false);
} }
return _sender.CreateEmbed() return eb;
.WithOkColor()
.WithTitle(Format.Bold(GetText(strs.self_assign_list(roles.Count()))))
.WithDescription(rolesStr.ToString())
.WithFooter(exclusive
? GetText(strs.self_assign_are_exclusive)
: GetText(strs.self_assign_are_not_exclusive));
}) })
.SendAsync(); .SendAsync();
} }
@ -152,25 +288,36 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
public async Task Togglexclsar() public async Task SarExclusive(int groupNumber)
{ {
var areExclusive = _service.ToggleEsar(ctx.Guild.Id); var areExclusive = await _service.SetGroupExclusivityAsync(ctx.Guild.Id, groupNumber);
if (areExclusive)
if (areExclusive is null)
{
await Response().Error(strs.sar_group_not_found).SendAsync();
return;
}
if (areExclusive is true)
{
await Response().Confirm(strs.self_assign_excl).SendAsync(); await Response().Confirm(strs.self_assign_excl).SendAsync();
}
else else
{
await Response().Confirm(strs.self_assign_no_excl).SendAsync(); await Response().Confirm(strs.self_assign_no_excl).SendAsync();
}
} }
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
public async Task RoleLevelReq(int level, [Leftover] IRole role) public async Task SarRoleLevelReq(int level, [Leftover] IRole role)
{ {
if (level < 0) if (level < 0)
return; return;
var succ = _service.SetLevelReq(ctx.Guild.Id, role, level); var succ = await _service.SetRoleLevelReq(ctx.Guild.Id, role.Id, level);
if (!succ) if (!succ)
{ {
@ -186,54 +333,35 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Iam([Leftover] IRole role) [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task SarGroupRoleReq(int groupNumber, [Leftover] IRole role)
{ {
var guildUser = (IGuildUser)ctx.User; var succ = await _service.SetGroupRoleReq(ctx.Guild.Id, groupNumber, role.Id);
var (result, autoDelete, extra) = await _service.Assign(guildUser, role); if (!succ)
IUserMessage msg;
if (result == SelfAssignedRolesService.AssignResult.ErrNotAssignable)
msg = await Response().Error(strs.self_assign_not).SendAsync();
else if (result == SelfAssignedRolesService.AssignResult.ErrLvlReq)
msg = await Response().Error(strs.self_assign_not_level(Format.Bold(extra.ToString()))).SendAsync();
else if (result == SelfAssignedRolesService.AssignResult.ErrAlreadyHave)
msg = await Response().Error(strs.self_assign_already(Format.Bold(role.Name))).SendAsync();
else if (result == SelfAssignedRolesService.AssignResult.ErrNotPerms)
msg = await Response().Error(strs.self_assign_perms).SendAsync();
else
msg = await Response().Confirm(strs.self_assign_success(Format.Bold(role.Name))).SendAsync();
if (autoDelete)
{ {
msg.DeleteAfter(3); await Response().Error(strs.sar_group_not_found).SendAsync();
ctx.Message.DeleteAfter(3); return;
} }
await Response()
.Confirm(strs.self_assign_group_role_req(
Format.Bold(groupNumber.ToString()),
Format.Bold(role.Name)))
.SendAsync();
} }
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Iamnot([Leftover] IRole role) [UserPerm(GuildPerm.ManageRoles)]
public async Task SarGroupDelete(int groupNumber)
{ {
var guildUser = (IGuildUser)ctx.User; var succ = await _service.DeleteRoleGroup(ctx.Guild.Id, groupNumber);
if (succ)
var (result, autoDelete) = await _service.Remove(guildUser, role); await Response().Confirm(strs.sar_group_deleted(Format.Bold(groupNumber.ToString()))).SendAsync();
IUserMessage msg;
if (result == SelfAssignedRolesService.RemoveResult.ErrNotAssignable)
msg = await Response().Error(strs.self_assign_not).SendAsync();
else if (result == SelfAssignedRolesService.RemoveResult.ErrNotHave)
msg = await Response().Error(strs.self_assign_not_have(Format.Bold(role.Name))).SendAsync();
else if (result == SelfAssignedRolesService.RemoveResult.ErrNotPerms)
msg = await Response().Error(strs.self_assign_perms).SendAsync();
else else
msg = await Response().Confirm(strs.self_assign_remove(Format.Bold(role.Name))).SendAsync(); await Response().Error(strs.sar_group_not_found).SendAsync();
if (autoDelete)
{
msg.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
} }
} }
} }

View file

@ -1,233 +1,335 @@
#nullable disable using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Xp.Services;
using OneOf;
using OneOf.Types;
using System.ComponentModel.DataAnnotations;
using System.Threading.Channels;
namespace EllieBot.Modules.Administration.Services; namespace EllieBot.Modules.Administration.Services;
public class SelfAssignedRolesService : IEService public class SelfAssignedRolesService : IEService, IReadyExecutor
{ {
public enum AssignResult
{
Assigned, // successfully removed
ErrNotAssignable, // not assignable (error)
ErrAlreadyHave, // you already have that role (error)
ErrNotPerms, // bot doesn't have perms (error)
ErrLvlReq // you are not required level (error)
}
public enum RemoveResult
{
Removed, // successfully removed
ErrNotAssignable, // not assignable (error)
ErrNotHave, // you don't have a role you want to remove (error)
ErrNotPerms // bot doesn't have perms (error)
}
private readonly DbService _db; private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCreds _creds;
public SelfAssignedRolesService(DbService db) private ConcurrentHashSet<ulong> _sarAds = new();
=> _db = db;
public bool AddNew(ulong guildId, IRole role, int group) public SelfAssignedRolesService(DbService db, DiscordSocketClient client, IBotCreds creds)
{ {
using var uow = _db.GetDbContext(); _db = db;
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId); _client = client;
if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id)) _creds = creds;
return false; }
uow.Set<SelfAssignedRole>().Add(new() public async Task AddAsync(ulong guildId, ulong roleId, int groupNumber)
{
await using var ctx = _db.GetDbContext();
await ctx.GetTable<SarGroup>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
GroupNumber = groupNumber,
IsExclusive = false
},
_ => new()
{
},
() => new()
{
GuildId = guildId,
GroupNumber = groupNumber
});
await ctx.GetTable<Sar>()
.InsertOrUpdateAsync(() => new()
{
RoleId = roleId,
LevelReq = 0,
GuildId = guildId,
SarGroupId = ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.Select(x => x.Id)
.First()
},
_ => new()
{
SarGroupId = ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.Select(x => x.Id)
.First()
},
() => new()
{
RoleId = roleId,
GuildId = guildId,
});
}
public async Task<bool> RemoveAsync(ulong guildId, ulong roleId)
{
await using var ctx = _db.GetDbContext();
var deleted = await ctx.GetTable<Sar>()
.Where(x => x.RoleId == roleId && x.GuildId == guildId)
.DeleteAsync();
return deleted > 0;
}
public async Task<bool> SetGroupNameAsync(ulong guildId, int groupNumber, string? name)
{
await using var ctx = _db.GetDbContext();
var changes = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.UpdateAsync(x => new()
{
Name = name
});
return changes > 0;
}
public async Task<IReadOnlyCollection<SarGroup>> GetSarsAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var sgs = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId)
.LoadWith(x => x.Roles)
.ToListAsyncLinqToDB();
return sgs;
}
public async Task<bool> SetRoleLevelReq(ulong guildId, ulong roleId, int levelReq)
{
await using var ctx = _db.GetDbContext();
var changes = await ctx.GetTable<Sar>()
.Where(x => x.GuildId == guildId && x.RoleId == roleId)
.UpdateAsync(_ => new()
{
LevelReq = levelReq,
});
return changes > 0;
}
public async Task<bool> SetGroupRoleReq(ulong guildId, int groupNumber, ulong? roleId)
{
await using var ctx = _db.GetDbContext();
var changes = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.UpdateAsync(_ => new()
{
RoleReq = roleId
});
return changes > 0;
}
public async Task<bool?> SetGroupExclusivityAsync(ulong guildId, int groupNumber)
{
await using var ctx = _db.GetDbContext();
var changes = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.UpdateWithOutputAsync(old => new()
{
IsExclusive = !old.IsExclusive
},
(o, n) => n.IsExclusive);
if (changes.Length == 0)
{ {
Group = group, return null;
RoleId = role.Id, }
GuildId = role.Guild.Id
}); return changes[0];
uow.SaveChanges(); }
public async Task<SarGroup?> GetRoleGroup(ulong guildId, ulong roleId)
{
await using var ctx = _db.GetDbContext();
var group = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.Roles.Any(x => x.RoleId == roleId))
.LoadWith(x => x.Roles)
.FirstOrDefaultAsyncLinqToDB();
return group;
}
public async Task<bool> DeleteRoleGroup(ulong guildId, int groupNumber)
{
await using var ctx = _db.GetDbContext();
var deleted = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.DeleteAsync();
return deleted > 0;
}
public async Task<bool> ToggleAutoDelete(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var delted = await ctx.GetTable<SarAutoDelete>()
.DeleteAsync(x => x.GuildId == guildId);
if (delted > 0)
{
_sarAds.TryRemove(guildId);
return false;
}
await ctx.GetTable<SarAutoDelete>()
.InsertOrUpdateAsync(() => new()
{
IsEnabled = true,
GuildId = guildId,
},
(_) => new()
{
IsEnabled = true
},
() => new()
{
GuildId = guildId
});
_sarAds.Add(guildId);
return true; return true;
} }
public bool ToggleAdSarm(ulong guildId) public bool GetAutoDelete(ulong guildId)
=> _sarAds.Contains(guildId);
public async Task OnReadyAsync()
{ {
bool newval;
using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, set => set);
newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages;
uow.SaveChanges();
return newval;
}
public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role)
{
LevelStats userLevelData;
await using (var uow = _db.GetDbContext())
{
var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id);
userLevelData = new(stats.Xp + stats.AwardedXp);
}
var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id);
var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id);
if (theRoleYouWant is null)
return (AssignResult.ErrNotAssignable, autoDelete, null);
if (theRoleYouWant.LevelRequirement > userLevelData.Level)
return (AssignResult.ErrLvlReq, autoDelete, theRoleYouWant.LevelRequirement);
if (guildUser.RoleIds.Contains(role.Id))
return (AssignResult.ErrAlreadyHave, autoDelete, null);
var roleIds = roles.Where(x => x.Group == theRoleYouWant.Group).Select(x => x.RoleId).ToArray();
if (exclusive)
{
var sameRoles = guildUser.RoleIds.Where(r => roleIds.Contains(r));
foreach (var roleId in sameRoles)
{
var sameRole = guildUser.Guild.GetRole(roleId);
if (sameRole is not null)
{
try
{
await guildUser.RemoveRoleAsync(sameRole);
await Task.Delay(300);
}
catch
{
// ignored
}
}
}
}
try
{
await guildUser.AddRoleAsync(role);
}
catch (Exception ex)
{
return (AssignResult.ErrNotPerms, autoDelete, ex);
}
return (AssignResult.Assigned, autoDelete, null);
}
public async Task<bool> SetNameAsync(ulong guildId, int group, string name)
{
var set = false;
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames)); var guilds = await uow.GetTable<SarAutoDelete>()
var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group); .Where(x => x.IsEnabled && Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
.Select(x => x.GuildId)
.ToListAsyncLinqToDB();
if (string.IsNullOrWhiteSpace(name)) _sarAds = new(guilds);
{
if (toUpdate is not null)
gc.SelfAssignableRoleGroupNames.Remove(toUpdate);
}
else if (toUpdate is null)
{
gc.SelfAssignableRoleGroupNames.Add(new()
{
Name = name,
Number = group
});
set = true;
}
else
{
toUpdate.Name = name;
set = true;
}
await uow.SaveChangesAsync();
return set;
}
public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role)
{
var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id);
if (roles.FirstOrDefault(r => r.RoleId == role.Id) is null)
return (RemoveResult.ErrNotAssignable, autoDelete);
if (!guildUser.RoleIds.Contains(role.Id))
return (RemoveResult.ErrNotHave, autoDelete);
try
{
await guildUser.RemoveRoleAsync(role);
}
catch (Exception)
{
return (RemoveResult.ErrNotPerms, autoDelete);
}
return (RemoveResult.Removed, autoDelete);
}
public bool RemoveSar(ulong guildId, ulong roleId)
{
bool success;
using var uow = _db.GetDbContext();
success = uow.Set<SelfAssignedRole>().DeleteByGuildAndRoleId(guildId, roleId);
uow.SaveChanges();
return success;
}
public (bool AutoDelete, bool Exclusive, IReadOnlyCollection<SelfAssignedRole>) GetAdAndRoles(ulong guildId)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages;
var exclusive = gc.ExclusiveSelfAssignedRoles;
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId);
return (autoDelete, exclusive, roles);
}
public bool SetLevelReq(ulong guildId, IRole role, int level)
{
using var uow = _db.GetDbContext();
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId);
var sar = roles.FirstOrDefault(x => x.RoleId == role.Id);
if (sar is not null)
{
sar.LevelRequirement = level;
uow.SaveChanges();
}
else
return false;
return true;
}
public bool ToggleEsar(ulong guildId)
{
bool areExclusive;
using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, set => set);
areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles;
uow.SaveChanges();
return areExclusive;
}
public (bool Exclusive, IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary<int, string>
GroupNames
) GetRoles(IGuild guild)
{
var exclusive = false;
IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> roles;
IDictionary<int, string> groupNames;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames));
exclusive = gc.ExclusiveSelfAssignedRoles;
groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name);
var roleModels = uow.Set<SelfAssignedRole>().GetFromGuild(guild.Id);
roles = roleModels.Select(x => (Model: x, Role: guild.GetRole(x.RoleId)))
.ToList();
uow.Set<SelfAssignedRole>().RemoveRange(roles.Where(x => x.Role is null).Select(x => x.Model).ToArray());
uow.SaveChanges();
}
return (exclusive, roles.Where(x => x.Role is not null).ToList(), groupNames);
} }
} }
public sealed class SarAssignerService : IEService, IReadyExecutor
{
private readonly XpService _xp;
private readonly DbService _db;
private readonly Channel<SarAssignerDataItem> _channel =
Channel.CreateBounded<SarAssignerDataItem>(100);
public SarAssignerService(XpService xp, DbService db)
{
_xp = xp;
_db = db;
}
public async Task OnReadyAsync()
{
var reader = _channel.Reader;
while (true)
{
var item = await reader.ReadAsync();
try
{
var sar = item.Group.Roles.First(x => x.RoleId == item.RoleId);
if (item.User.RoleIds.Contains(item.RoleId))
{
item.CompletionTask.TrySetResult(new SarAlreadyHasRole());
continue;
}
if (item.Group.RoleReq is { } rid)
{
if (!item.User.RoleIds.Contains(rid))
{
item.CompletionTask.TrySetResult(new SarRoleRequirement(rid));
continue;
}
// passed
}
// check level requirement
if (sar.LevelReq > 0)
{
await using var ctx = _db.GetDbContext();
var xpStats = await ctx.GetTable<UserXpStats>().GetGuildUserXp(sar.GuildId, item.User.Id);
var lvlData = new LevelStats(xpStats?.Xp ?? 0);
if (lvlData.Level < sar.LevelReq)
{
item.CompletionTask.TrySetResult(new SarLevelRequirement(sar.LevelReq));
continue;
}
// passed
}
if (item.Group.IsExclusive)
{
var rolesToRemove = item.Group.Roles
.Where(x => item.User.RoleIds.Contains(x.RoleId))
.Select(x => x.RoleId)
.ToArray();
if (rolesToRemove.Length > 0)
await item.User.RemoveRolesAsync(rolesToRemove);
}
await item.User.AddRoleAsync(item.RoleId);
item.CompletionTask.TrySetResult(new Success());
}
catch (Exception ex)
{
Log.Error(ex, "Unknown error ocurred in SAR runner: {Error}", ex.Message);
item.CompletionTask.TrySetResult(new Error());
}
}
}
public async Task Add(SarAssignerDataItem item)
{
await _channel.Writer.WriteAsync(item);
}
}
public sealed class SarAssignerDataItem
{
public required SarGroup Group { get; init; }
public required IGuildUser User { get; init; }
public required ulong RoleId { get; init; }
public required TaskCompletionSource<SarAssignResult> CompletionTask { get; init; }
}
[GenerateOneOf]
public sealed partial class SarAssignResult
: OneOfBase<Success, Error, SarLevelRequirement, SarRoleRequirement, SarAlreadyHasRole, SarInsuffPerms>
{
}
public record class SarLevelRequirement(int Level);
public record class SarRoleRequirement(ulong RoleId);
public record class SarAlreadyHasRole();
public record class SarInsuffPerms();

View file

@ -35,7 +35,7 @@ public partial class Administration
var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList() var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList()
?? new List<IgnoredLogItem>(); ?? new List<IgnoredLogItem>();
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.AddField(GetText(strs.log_ignored_channels), .AddField(GetText(strs.log_ignored_channels),
chs.Count == 0 chs.Count == 0

View file

@ -42,7 +42,7 @@ public partial class Administration
.Items(timezoneStrings) .Items(timezoneStrings)
.PageSize(timezonesPerPage) .PageSize(timezonesPerPage)
.CurrentPage(page) .CurrentPage(page)
.Page((items, _) => _sender.CreateEmbed() .Page((items, _) => CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.timezones_available)) .WithTitle(GetText(strs.timezones_available))
.WithDescription(string.Join("\n", items))) .WithDescription(string.Join("\n", items)))

View file

@ -23,27 +23,6 @@ public partial class Administration
_mute = mute; _mute = mute;
} }
private async Task<bool> CheckRoleHierarchy(IGuildUser target)
{
var curUser = ((SocketGuild)ctx.Guild).CurrentUser;
var ownerId = ctx.Guild.OwnerId;
var modMaxRole = ((IGuildUser)ctx.User).GetRoles().Max(r => r.Position);
var targetMaxRole = target.GetRoles().Max(r => r.Position);
var botMaxRole = curUser.GetRoles().Max(r => r.Position);
// bot can't punish a user who is higher in the hierarchy. Discord will return 403
// moderator can be owner, in which case role hierarchy doesn't matter
// otherwise, moderator has to have a higher role
if (botMaxRole <= targetMaxRole
|| (ctx.User.Id != ownerId && targetMaxRole >= modMaxRole)
|| target.Id == ownerId)
{
await Response().Error(strs.hierarchy).SendAsync();
return false;
}
return true;
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
@ -65,7 +44,7 @@ public partial class Administration
try try
{ {
await _sender.Response(user) await _sender.Response(user)
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithErrorColor() .WithErrorColor()
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString()))) .WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
.AddField(GetText(strs.moderator), ctx.User.ToString()) .AddField(GetText(strs.moderator), ctx.User.ToString())
@ -85,7 +64,7 @@ public partial class Administration
catch (Exception ex) catch (Exception ex)
{ {
Log.Warning(ex, "Exception occured while warning a user"); Log.Warning(ex, "Exception occured while warning a user");
var errorEmbed = _sender.CreateEmbed() var errorEmbed = CreateEmbed()
.WithErrorColor() .WithErrorColor()
.WithDescription(GetText(strs.cant_apply_punishment)); .WithDescription(GetText(strs.cant_apply_punishment));
@ -96,7 +75,7 @@ public partial class Administration
return; return;
} }
var embed = _sender.CreateEmbed().WithOkColor(); var embed = CreateEmbed().WithOkColor();
if (punishment is null) if (punishment is null)
embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString())))); embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString()))));
else else
@ -205,7 +184,7 @@ public partial class Administration
.Page((warnings, page) => .Page((warnings, page) =>
{ {
var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString(); var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString();
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.warnlog_for(user))); var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
if (!warnings.Any()) if (!warnings.Any())
embed.WithDescription(GetText(strs.warnings_none)); embed.WithDescription(GetText(strs.warnings_none));
@ -266,7 +245,7 @@ public partial class Administration
+ $" | {total} ({all} - {forgiven})"; + $" | {total} ({all} - {forgiven})";
}); });
return _sender.CreateEmbed() return CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.warnings_list)) .WithTitle(GetText(strs.warnings_list))
.WithDescription(string.Join("\n", ws)); .WithDescription(string.Join("\n", ws));
@ -334,7 +313,7 @@ public partial class Administration
int number, int number,
AddRole _, AddRole _,
IRole role, IRole role,
StoopidTime time = null) ParsedTimespan timespan = null)
{ {
var punish = PunishmentAction.AddRole; var punish = PunishmentAction.AddRole;
@ -345,12 +324,12 @@ public partial class Administration
return; return;
} }
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, time, role); var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, timespan, role);
if (!success) if (!success)
return; return;
if (time is null) if (timespan is null)
{ {
await Response() await Response()
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()), .Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
@ -362,7 +341,7 @@ public partial class Administration
await Response() await Response()
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()), .Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
Format.Bold(number.ToString()), Format.Bold(number.ToString()),
Format.Bold(time.Input))) Format.Bold(timespan.Input)))
.SendAsync(); .SendAsync();
} }
} }
@ -370,7 +349,7 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
public async Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null) public async Task WarnPunish(int number, PunishmentAction punish, ParsedTimespan timespan = null)
{ {
// this should never happen. Addrole has its own method with higher priority // this should never happen. Addrole has its own method with higher priority
// also disallow warn punishment for getting warned // also disallow warn punishment for getting warned
@ -378,15 +357,15 @@ public partial class Administration
return; return;
// you must specify the time for timeout // you must specify the time for timeout
if (punish is PunishmentAction.TimeOut && time is null) if (punish is PunishmentAction.TimeOut && timespan is null)
return; return;
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, time); var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, timespan);
if (!success) if (!success)
return; return;
if (time is null) if (timespan is null)
{ {
await Response() await Response()
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()), .Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
@ -398,7 +377,7 @@ public partial class Administration
await Response() await Response()
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()), .Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
Format.Bold(number.ToString()), Format.Bold(number.ToString()),
Format.Bold(time.Input))) Format.Bold(timespan.Input)))
.SendAsync(); .SendAsync();
} }
} }
@ -438,17 +417,17 @@ public partial class Administration
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)] [BotPerm(GuildPerm.BanMembers)]
[Priority(1)] [Priority(1)]
public Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null) public Task Ban(ParsedTimespan timespan, IUser user, [Leftover] string msg = null)
=> Ban(time, user.Id, msg); => Ban(timespan, user.Id, msg);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)] [BotPerm(GuildPerm.BanMembers)]
[Priority(0)] [Priority(0)]
public async Task Ban(StoopidTime time, ulong userId, [Leftover] string msg = null) public async Task Ban(ParsedTimespan timespan, ulong userId, [Leftover] string msg = null)
{ {
if (time.Time > TimeSpan.FromDays(49)) if (timespan.Time > TimeSpan.FromDays(49))
return; return;
var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
@ -465,7 +444,7 @@ public partial class Administration
{ {
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg)); var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
var smartText = var smartText =
await _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time); await _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, timespan.Time);
if (smartText is not null) if (smartText is not null)
await Response().User(guildUser).Text(smartText).SendAsync(); await Response().User(guildUser).Text(smartText).SendAsync();
} }
@ -477,14 +456,14 @@ public partial class Administration
var user = await ctx.Client.GetUserAsync(userId); var user = await ctx.Client.GetUserAsync(userId);
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune); await _mute.TimedBan(ctx.Guild, userId, timespan.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
var toSend = _sender.CreateEmbed() var toSend = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user)) .WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true) .AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
.AddField("ID", userId.ToString(), true) .AddField("ID", userId.ToString(), true)
.AddField(GetText(strs.duration), .AddField(GetText(strs.duration),
time.Time.ToPrettyStringHm(), timespan.Time.ToPrettyStringHm(),
true); true);
if (dmFailed) if (dmFailed)
@ -507,7 +486,7 @@ public partial class Administration
await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512)); await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
await Response() await Response()
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user)) .WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField("ID", userId.ToString(), true)) .AddField("ID", userId.ToString(), true))
@ -544,7 +523,7 @@ public partial class Administration
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7; var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512)); await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
var toSend = _sender.CreateEmbed() var toSend = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user)) .WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user.ToString(), true) .AddField(GetText(strs.username), user.ToString(), true)
@ -622,7 +601,7 @@ public partial class Administration
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)] [BotPerm(GuildPerm.BanMembers)]
[Priority(1)] [Priority(1)]
public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null) public Task BanMessageTest(ParsedTimespan duration, [Leftover] string reason = null)
=> InternalBanMessageTest(reason, duration.Time); => InternalBanMessageTest(reason, duration.Time);
private async Task InternalBanMessageTest(string reason, TimeSpan? duration) private async Task InternalBanMessageTest(string reason, TimeSpan? duration)
@ -740,7 +719,7 @@ public partial class Administration
{ await ctx.Guild.RemoveBanAsync(user); } { await ctx.Guild.RemoveBanAsync(user); }
catch { await ctx.Guild.RemoveBanAsync(user); } catch { await ctx.Guild.RemoveBanAsync(user); }
var toSend = _sender.CreateEmbed() var toSend = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("☣ " + GetText(strs.sb_user)) .WithTitle("☣ " + GetText(strs.sb_user))
.AddField(GetText(strs.username), user.ToString(), true) .AddField(GetText(strs.username), user.ToString(), true)
@ -795,7 +774,7 @@ public partial class Administration
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512)); await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
var toSend = _sender.CreateEmbed() var toSend = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.kicked_user)) .WithTitle(GetText(strs.kicked_user))
.AddField(GetText(strs.username), user.ToString(), true) .AddField(GetText(strs.username), user.ToString(), true)
@ -812,7 +791,7 @@ public partial class Administration
[UserPerm(GuildPerm.ModerateMembers)] [UserPerm(GuildPerm.ModerateMembers)]
[BotPerm(GuildPerm.ModerateMembers)] [BotPerm(GuildPerm.ModerateMembers)]
[Priority(2)] [Priority(2)]
public async Task Timeout(IUser globalUser, StoopidTime time, [Leftover] string msg = null) public async Task Timeout(IUser globalUser, ParsedTimespan timespan, [Leftover] string msg = null)
{ {
var user = await ctx.Guild.GetUserAsync(globalUser.Id); var user = await ctx.Guild.GetUserAsync(globalUser.Id);
@ -828,7 +807,7 @@ public partial class Administration
{ {
var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg)); var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg));
await _sender.Response(user) await _sender.Response(user)
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithPendingColor() .WithPendingColor()
.WithDescription(dmMessage)) .WithDescription(dmMessage))
.SendAsync(); .SendAsync();
@ -838,9 +817,9 @@ public partial class Administration
dmFailed = true; dmFailed = true;
} }
await user.SetTimeOutAsync(time.Time); await user.SetTimeOutAsync(timespan.Time);
var toSend = _sender.CreateEmbed() var toSend = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("⏳ " + GetText(strs.timedout_user)) .WithTitle("⏳ " + GetText(strs.timedout_user))
.AddField(GetText(strs.username), user.ToString(), true) .AddField(GetText(strs.username), user.ToString(), true)
@ -901,7 +880,7 @@ public partial class Administration
if (string.IsNullOrWhiteSpace(missStr)) if (string.IsNullOrWhiteSpace(missStr))
missStr = "-"; missStr = "-";
var toSend = _sender.CreateEmbed() var toSend = CreateEmbed()
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count))) .WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
.AddField(GetText(strs.invalid(missing.Count)), missStr) .AddField(GetText(strs.invalid(missing.Count)), missStr)
.WithPendingColor(); .WithPendingColor();
@ -921,7 +900,7 @@ public partial class Administration
} }
} }
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed() await banningMessage.ModifyAsync(x => x.Embed = CreateEmbed()
.WithDescription( .WithDescription(
GetText(strs.mass_ban_completed( GetText(strs.mass_ban_completed(
banning.Count()))) banning.Count())))
@ -949,7 +928,7 @@ public partial class Administration
//send a message but don't wait for it //send a message but don't wait for it
var banningMessageTask = Response() var banningMessageTask = Response()
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithDescription( .WithDescription(
GetText(strs.mass_kill_in_progress(bans.Count()))) GetText(strs.mass_kill_in_progress(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr) .AddField(GetText(strs.invalid(missing)), missStr)
@ -970,7 +949,7 @@ public partial class Administration
//wait for the message and edit it //wait for the message and edit it
var banningMessage = await banningMessageTask; var banningMessage = await banningMessageTask;
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed() await banningMessage.ModifyAsync(x => x.Embed = CreateEmbed()
.WithDescription( .WithDescription(
GetText(strs.mass_kill_completed(bans.Count()))) GetText(strs.mass_kill_completed(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr) .AddField(GetText(strs.invalid(missing)), missStr)

View file

@ -68,7 +68,7 @@ public partial class Administration
else else
text = GetText(strs.no_vcroles); text = GetText(strs.no_vcroles);
await Response().Embed(_sender.CreateEmbed() await Response().Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.vc_role_list)) .WithTitle(GetText(strs.vc_role_list))
.WithDescription(text)).SendAsync(); .WithDescription(text)).SendAsync();

View file

@ -34,7 +34,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
var ex = await _service.AddAsync(ctx.Guild?.Id, key, message); var ex = await _service.AddAsync(ctx.Guild?.Id, key, message);
await Response() await Response()
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.expr_new)) .WithTitle(GetText(strs.expr_new))
.WithDescription($"#{new kwum(ex.Id)}") .WithDescription($"#{new kwum(ex.Id)}")
@ -104,7 +104,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
if (ex is not null) if (ex is not null)
{ {
await Response() await Response()
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.expr_edited)) .WithTitle(GetText(strs.expr_edited))
.WithDescription($"#{id}") .WithDescription($"#{id}")
@ -159,7 +159,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
: " // " + string.Join(" ", ex.GetReactions()))) : " // " + string.Join(" ", ex.GetReactions())))
.Join('\n'); .Join('\n');
return _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.expressions)).WithDescription(desc); return CreateEmbed().WithOkColor().WithTitle(GetText(strs.expressions)).WithDescription(desc);
}) })
.SendAsync(); .SendAsync();
} }
@ -179,7 +179,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
await Response() await Response()
.Interaction(IsValidExprEditor() ? inter : null) .Interaction(IsValidExprEditor() ? inter : null)
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription($"#{id}") .WithDescription($"#{id}")
.AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024)) .AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024))
@ -224,7 +224,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
if (ex is not null) if (ex is not null)
{ {
await Response() await Response()
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.expr_deleted)) .WithTitle(GetText(strs.expr_deleted))
.WithDescription($"#{id}") .WithDescription($"#{id}")
@ -375,7 +375,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
public async Task ExprClear() public async Task ExprClear()
{ {
if (await PromptUserConfirmAsync(_sender.CreateEmbed() if (await PromptUserConfirmAsync(CreateEmbed()
.WithTitle("Expression clear") .WithTitle("Expression clear")
.WithDescription("This will delete all expressions on this server."))) .WithDescription("This will delete all expressions on this server.")))
{ {

View file

@ -6,9 +6,9 @@ namespace EllieBot.Modules.Gambling.Common.AnimalRacing;
public sealed class AnimalRace : IDisposable public sealed class AnimalRace : IDisposable
{ {
public const double BASE_MULTIPLIER = 0.82; public const double BASE_MULTIPLIER = 0.87;
public const double MAX_MULTIPLIER = 0.94; public const double MAX_MULTIPLIER = 0.94;
public const double MULTI_PER_USER = 0.01; public const double MULTI_PER_USER = 0.005;
public enum Phase public enum Phase
{ {

View file

@ -74,7 +74,7 @@ public partial class Gambling
if (race.FinishedUsers[0].Bet > 0) if (race.FinishedUsers[0].Bet > 0)
{ {
return Response() return Response()
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.animal_race)) .WithTitle(GetText(strs.animal_race))
.WithDescription(GetText(strs.animal_race_won_money( .WithDescription(GetText(strs.animal_race_won_money(
@ -132,7 +132,7 @@ public partial class Gambling
raceMessage = await Response().Confirm(text).SendAsync(); raceMessage = await Response().Confirm(text).SendAsync();
else else
{ {
await msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed() await msg.ModifyAsync(x => x.Embed = CreateEmbed()
.WithTitle(GetText(strs.animal_race)) .WithTitle(GetText(strs.animal_race))
.WithDescription(text) .WithDescription(text)
.WithOkColor() .WithOkColor()

View file

@ -59,7 +59,7 @@ public partial class Gambling
{ {
var bal = await _bank.GetBalanceAsync(ctx.User.Id); var bal = await _bank.GetBalanceAsync(ctx.User.Id);
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(GetText(strs.bank_balance(N(bal)))); .WithDescription(GetText(strs.bank_balance(N(bal))));
@ -80,7 +80,7 @@ public partial class Gambling
{ {
var bal = await _bank.GetBalanceAsync(user.Id); var bal = await _bank.GetBalanceAsync(user.Id);
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(GetText(strs.bank_balance_other(user.ToString(), N(bal)))); .WithDescription(GetText(strs.bank_balance_other(user.ToString(), N(bal))));

View file

@ -1,6 +1,7 @@
#nullable disable #nullable disable
using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common;
using EllieBot.Modules.Gambling.Services; using EllieBot.Modules.Gambling.Services;
using EllieBot.Modules.Xp.Services;
namespace EllieBot.Modules.Gambling; namespace EllieBot.Modules.Gambling;
@ -10,13 +11,19 @@ public partial class Gambling
public sealed class BetStatsCommands : GamblingModule<UserBetStatsService> public sealed class BetStatsCommands : GamblingModule<UserBetStatsService>
{ {
private readonly GamblingTxTracker _gamblingTxTracker; private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IBotCache _cache;
private readonly IUserService _userService;
public BetStatsCommands( public BetStatsCommands(
GamblingTxTracker gamblingTxTracker, GamblingTxTracker gamblingTxTracker,
GamblingConfigService gcs) GamblingConfigService gcs,
IBotCache cache,
IUserService userService)
: base(gcs) : base(gcs)
{ {
_gamblingTxTracker = gamblingTxTracker; _gamblingTxTracker = gamblingTxTracker;
_cache = cache;
_userService = userService;
} }
[Cmd] [Cmd]
@ -24,13 +31,13 @@ public partial class Gambling
{ {
var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game); var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game);
var result = await PromptUserConfirmAsync(_sender.CreateEmbed() var result = await PromptUserConfirmAsync(CreateEmbed()
.WithDescription( .WithDescription(
$""" $"""
Are you sure you want to reset your bet stats for **{GetGameName(game)}**? Are you sure you want to reset your bet stats for **{GetGameName(game)}**?
It will cost you {N(price)} It will cost you {N(price)}
""")); """));
if (!result) if (!result)
return; return;
@ -87,16 +94,16 @@ public partial class Gambling
} }
}; };
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(user) .WithAuthor(user)
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true) .AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true) .AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true) .AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true) .AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
.AddField("Payout", .AddField("Payout",
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture), (stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
true); true);
if (game == null) if (game == null)
{ {
var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount); var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount);
@ -115,23 +122,96 @@ public partial class Gambling
.SendAsync(); .SendAsync();
} }
private readonly record struct WinLbStat(
int Rank,
string User,
GamblingGame Game,
long MaxWin);
private TypedKey<List<WinLbStat>> GetWinLbKey(int page)
=> new($"winlb:{page}");
private async Task<IReadOnlyCollection<WinLbStat>> GetCachedWinLbAsync(int page)
{
return await _cache.GetOrAddAsync(GetWinLbKey(page),
async () =>
{
var items = await _service.GetWinLbAsync(page);
if (items.Count == 0)
return [];
var outputItems = new List<WinLbStat>(items.Count);
for (var i = 0; i < items.Count; i++)
{
var x = items[i];
var user = (await ctx.Client.GetUserAsync(x.UserId, CacheMode.CacheOnly))?.ToString()
?? (await _userService.GetUserAsync(x.UserId))?.Username
?? x.UserId.ToString();
if (user.StartsWith("??"))
user = x.UserId.ToString();
outputItems.Add(new WinLbStat(i + 1 + (page * 9), user, x.Game, x.MaxWin));
}
return outputItems;
},
expiry: TimeSpan.FromMinutes(5));
}
[Cmd]
public async Task WinLb(int page = 1)
{
if (--page < 0)
return;
await Response()
.Paginated()
.PageItems(p => GetCachedWinLbAsync(p))
.PageSize(9)
.Page((items, curPage) =>
{
var eb = CreateEmbed()
.WithTitle(GetText(strs.winlb))
.WithOkColor();
if (items.Count == 0)
{
eb.WithDescription(GetText(strs.empty_page));
return eb;
}
for (var i = 0; i < items.Count; i++)
{
var item = items[i];
eb.AddField($"#{item.Rank} {item.User}",
$"{N(item.MaxWin)}\n`{item.Game.ToString().ToLower()}`",
true);
}
return eb;
})
.SendAsync();
}
[Cmd] [Cmd]
public async Task GambleStats() public async Task GambleStats()
{ {
var stats = await _gamblingTxTracker.GetAllAsync(); var stats = await _gamblingTxTracker.GetAllAsync();
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor(); .WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n"; var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
str += "――――――――――――――――――――\n"; str += "――――――――――――――――――――\n";
foreach (var stat in stats) foreach (var stat in stats)
{ {
var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture); var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture);
str += $"`{stat.Feature.PadBoth(9)}`" str += $"`{stat.Feature.PadBoth(9)}`"
+ $"`{stat.Bet.ToString("N0").PadLeft(8, ' ')}`" + $"`{stat.Bet.ToString("N0").PadLeft(8, '')}`"
+ $"`{stat.PaidOut.ToString("N0").PadLeft(8, ' ')}`" + $"`{stat.PaidOut.ToString("N0").PadLeft(8, '')}`"
+ $"`{perc.PadLeft(6, ' ')}`\n"; + $"`{perc.PadLeft(6, '')}`\n";
} }
var bet = stats.Sum(x => x.Bet); var bet = stats.Sum(x => x.Bet);
@ -143,9 +223,9 @@ public partial class Gambling
var tPerc = (paidOut / bet).ToString("P2", Culture); var tPerc = (paidOut / bet).ToString("P2", Culture);
str += "――――――――――――――――――――\n"; str += "――――――――――――――――――――\n";
str += $"` {("TOTAL").PadBoth(7)}` " str += $"` {("TOTAL").PadBoth(7)}` "
+ $"**{N(bet).PadLeft(8, ' ')}**" + $"**{N(bet).PadLeft(8, '')}**"
+ $"**{N(paidOut).PadLeft(8, ' ')}**" + $"**{N(paidOut).PadLeft(8, '')}**"
+ $"`{tPerc.PadLeft(6, ' ')}`"; + $"`{tPerc.PadLeft(6, '')}`";
eb.WithDescription(str); eb.WithDescription(str);
@ -156,14 +236,14 @@ public partial class Gambling
[OwnerOnly] [OwnerOnly]
public async Task GambleStatsReset() public async Task GambleStatsReset()
{ {
if (!await PromptUserConfirmAsync(_sender.CreateEmbed() if (!await PromptUserConfirmAsync(CreateEmbed()
.WithDescription( .WithDescription(
""" """
Are you sure? Are you sure?
This will completely reset Gambling Stats. This will completely reset Gambling Stats.
This action is irreversible. This action is irreversible.
"""))) """)))
return; return;
await GambleStats(); await GambleStats();

View file

@ -95,7 +95,7 @@ public partial class Gambling
var cStr = string.Concat(c.Select(x => x[..^1] + " ")); var cStr = string.Concat(c.Select(x => x[..^1] + " "));
cStr += "\n" + string.Concat(c.Select(x => x.Last() + " ")); cStr += "\n" + string.Concat(c.Select(x => x.Last() + " "));
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("BlackJack") .WithTitle("BlackJack")
.AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr); .AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr);

View file

@ -132,7 +132,7 @@ public partial class Gambling
else else
title = GetText(strs.connect4_draw); title = GetText(strs.connect4_draw);
return msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed() return msg.ModifyAsync(x => x.Embed = CreateEmbed()
.WithTitle(title) .WithTitle(title)
.WithDescription(GetGameStateText(game)) .WithDescription(GetGameStateText(game))
.WithOkColor() .WithOkColor()
@ -142,7 +142,7 @@ public partial class Gambling
private async Task Game_OnGameStateUpdated(Connect4Game game) private async Task Game_OnGameStateUpdated(Connect4Game game)
{ {
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}") .WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}")
.WithDescription(GetGameStateText(game)) .WithDescription(GetGameStateText(game))
.WithOkColor(); .WithOkColor();

View file

@ -38,7 +38,7 @@ public partial class Gambling
var fileName = $"dice.{format.FileExtensions.First()}"; var fileName = $"dice.{format.FileExtensions.First()}";
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.AddField(GetText(strs.roll2), gen) .AddField(GetText(strs.roll2), gen)
@ -115,7 +115,7 @@ public partial class Gambling
d.Dispose(); d.Dispose();
var imageName = $"dice.{format.FileExtensions.First()}"; var imageName = $"dice.{format.FileExtensions.First()}";
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.AddField(GetText(strs.rolls), values.Select(x => Format.Code(x.ToString())).Join(' '), true) .AddField(GetText(strs.rolls), values.Select(x => Format.Code(x.ToString())).Join(' '), true)
@ -141,7 +141,7 @@ public partial class Gambling
for (var i = 0; i < n1; i++) for (var i = 0; i < n1; i++)
rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]); rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithDescription(GetText(strs.dice_rolled_num(Format.Bold(n1.ToString())))) .WithDescription(GetText(strs.dice_rolled_num(Format.Bold(n1.ToString()))))
@ -170,7 +170,7 @@ public partial class Gambling
arr[i] = rng.Next(1, n2 + 1); arr[i] = rng.Next(1, n2 + 1);
var sum = arr.Sum(); var sum = arr.Sum();
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithDescription(GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`"))) .WithDescription(GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`")))

View file

@ -56,8 +56,8 @@ public partial class Gambling
foreach (var i in images) foreach (var i in images)
i.Dispose(); i.Dispose();
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor(); .WithOkColor();
var toSend = string.Empty; var toSend = string.Empty;
if (cardObjects.Count == 5) if (cardObjects.Count == 5)
@ -171,14 +171,15 @@ public partial class Gambling
return; return;
} }
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithDescription(result.Card.GetEmoji()) .WithDescription(result.Card.GetEmoji())
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true) .AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
.AddField(GetText(strs.card), GetCardInfo(result.Card), true) .AddField(GetText(strs.card), GetCardInfo(result.Card), false)
.AddField(GetText(strs.won), N((long)result.Won), false) .AddField(GetText(strs.bet), N(amount), true)
.WithImageUrl("attachment://card.png"); .AddField(GetText(strs.won), N((long)result.Won), true)
.WithImageUrl("attachment://card.png");
using var img = await GetCardImageAsync(result.Card); using var img = await GetCardImageAsync(result.Card);
await using var imgStream = await img.ToStreamAsync(); await using var imgStream = await img.ToStreamAsync();

View file

@ -30,12 +30,12 @@ public partial class Gambling
private EmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot) private EmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot)
=> type switch => type switch
{ {
CurrencyEvent.Type.Reaction => _sender.CreateEmbed() CurrencyEvent.Type.Reaction => CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString()))) .WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetReactionDescription(opts.Amount, currentPot)) .WithDescription(GetReactionDescription(opts.Amount, currentPot))
.WithFooter(GetText(strs.event_duration_footer(opts.Hours))), .WithFooter(GetText(strs.event_duration_footer(opts.Hours))),
CurrencyEvent.Type.GameStatus => _sender.CreateEmbed() CurrencyEvent.Type.GameStatus => CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString()))) .WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetGameStatusDescription(opts.Amount, currentPot)) .WithDescription(GetGameStatusDescription(opts.Amount, currentPot))

View file

@ -84,11 +84,11 @@ public partial class Gambling
? Format.Bold(GetText(strs.heads)) ? Format.Bold(GetText(strs.heads))
: Format.Bold(GetText(strs.tails)))); : Format.Bold(GetText(strs.tails))));
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithDescription(msg) .WithDescription(msg)
.WithImageUrl($"attachment://{imgName}"); .WithImageUrl($"attachment://{imgName}");
await ctx.Channel.SendFileAsync(stream, await ctx.Channel.SendFileAsync(stream,
imgName, imgName,
@ -123,18 +123,22 @@ public partial class Gambling
var won = (long)result.Won; var won = (long)result.Won;
if (won > 0) if (won > 0)
{ {
str = Format.Bold(GetText(strs.flip_guess(N(won)))); str = Format.Bold(GetText(strs.betflip_guess));
} }
else else
{ {
str = Format.Bold(GetText(strs.better_luck)); str = Format.Bold(GetText(strs.better_luck));
} }
await Response().Embed(_sender.CreateEmbed() await Response()
.WithAuthor(ctx.User) .Embed(CreateEmbed()
.WithDescription(str) .WithAuthor(ctx.User)
.WithOkColor() .WithDescription(str)
.WithImageUrl(imageToSend.ToString())).SendAsync(); .AddField(GetText(strs.bet), N(amount), true)
.AddField(GetText(strs.won), N((long)result.Won), true)
.WithOkColor()
.WithImageUrl(imageToSend.ToString()))
.SendAsync();
} }
} }
} }

View file

@ -39,6 +39,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly GamblingTxTracker _gamblingTxTracker; private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IPatronageService _ps; private readonly IPatronageService _ps;
private readonly RakebackService _rb; private readonly RakebackService _rb;
private readonly IBotCache _cache;
public Gambling( public Gambling(
IGamblingService gs, IGamblingService gs,
@ -52,7 +53,8 @@ public partial class Gambling : GamblingModule<GamblingService>
IRemindService remind, IRemindService remind,
IPatronageService patronage, IPatronageService patronage,
GamblingTxTracker gamblingTxTracker, GamblingTxTracker gamblingTxTracker,
RakebackService rb) RakebackService rb,
IBotCache cache)
: base(configService) : base(configService)
{ {
_gs = gs; _gs = gs;
@ -63,6 +65,7 @@ public partial class Gambling : GamblingModule<GamblingService>
_remind = remind; _remind = remind;
_gamblingTxTracker = gamblingTxTracker; _gamblingTxTracker = gamblingTxTracker;
_rb = rb; _rb = rb;
_cache = cache;
_ps = patronage; _ps = patronage;
_rng = new EllieRandom(); _rng = new EllieRandom();
@ -132,7 +135,6 @@ public partial class Gambling : GamblingModule<GamblingService>
}); });
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)]
public async Task Timely() public async Task Timely()
{ {
var val = Config.Timely.Amount; var val = Config.Timely.Amount;
@ -152,40 +154,10 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
else if (Config.Timely.ProtType == TimelyProt.Captcha) else if (Config.Timely.ProtType == TimelyProt.Captcha)
{ {
var password = _service.GeneratePassword(); var password = await GetUserTimelyPassword(ctx.User.Id);
var img = GetPasswordImage(password);
var img = new Image<Rgba32>(70, 35);
var font = _fonts.NotoSans.CreateFont(30);
var outlinePen = new SolidPen(Color.Black, 1f);
var strikeoutRun = new RichTextRun
{
Start = 0,
End = password.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 3),
TextDecorations = TextDecorations.Strikeout
};
// draw password on the image
img.Mutate(x =>
{
x.DrawText(new RichTextOptions(font)
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(35, 17),
TextRuns = [strikeoutRun]
},
password,
Brushes.Solid(Color.White),
outlinePen);
});
using var stream = await img.ToStreamAsync(); using var stream = await img.ToStreamAsync();
var captcha = await Response() var captcha = await Response()
// .Embed(_sender.CreateEmbed()
// .WithOkColor()
// .WithImageUrl("attachment://timely.png"))
.File(stream, "timely.png") .File(stream, "timely.png")
.SendAsync(); .SendAsync();
try try
@ -195,6 +167,8 @@ public partial class Gambling : GamblingModule<GamblingService>
{ {
return; return;
} }
await ClearUserTimelyPassword(ctx.User.Id);
} }
finally finally
{ {
@ -205,6 +179,57 @@ public partial class Gambling : GamblingModule<GamblingService>
await ClaimTimely(); await ClaimTimely();
} }
private static TypedKey<string> TimelyPasswordKey(ulong userId)
=> new($"timely_password:{userId}");
private async Task<string> GetUserTimelyPassword(ulong userId)
{
var pw = await _cache.GetOrAddAsync(TimelyPasswordKey(userId),
() =>
{
var password = _service.GeneratePassword();
return Task.FromResult(password);
});
return pw;
}
private ValueTask<bool> ClearUserTimelyPassword(ulong userId)
=> _cache.RemoveAsync(TimelyPasswordKey(userId));
private Image<Rgba32> GetPasswordImage(string password)
{
var img = new Image<Rgba32>(50, 24);
var font = _fonts.NotoSans.CreateFont(22);
var outlinePen = new SolidPen(Color.Black, 0.5f);
var strikeoutRun = new RichTextRun
{
Start = 0,
End = password.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 4),
TextDecorations = TextDecorations.Strikeout
};
// draw password on the image
img.Mutate(x =>
{
x.DrawText(new RichTextOptions(font)
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(25, 12),
TextRuns = [strikeoutRun]
},
password,
Brushes.Solid(Color.White),
outlinePen);
});
return img;
}
private async Task ClaimTimely() private async Task ClaimTimely()
{ {
var period = Config.Timely.Cooldown; var period = Config.Timely.Cooldown;
@ -265,7 +290,7 @@ public partial class Gambling : GamblingModule<GamblingService>
{ {
msg += "\n\n"; msg += "\n\n";
if (booster) if (booster)
msg += $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*"; msg += $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n";
if (percentBonus > float.Epsilon) if (percentBonus > float.Epsilon)
msg += msg +=
@ -365,6 +390,12 @@ public partial class Gambling : GamblingModule<GamblingService>
public Task CurrencyTransactions([Leftover] IUser usr) public Task CurrencyTransactions([Leftover] IUser usr)
=> InternalCurrencyTransactions(usr.Id, 1); => InternalCurrencyTransactions(usr.Id, 1);
[Cmd]
[OwnerOnly]
[Priority(-1)]
public Task CurrencyTransactions([Leftover] ulong userId)
=> InternalCurrencyTransactions(userId, 1);
[Cmd] [Cmd]
[OwnerOnly] [OwnerOnly]
[Priority(1)] [Priority(1)]
@ -384,11 +415,11 @@ public partial class Gambling : GamblingModule<GamblingService>
trs = await uow.Set<CurrencyTransaction>().GetPageFor(userId, page); trs = await uow.Set<CurrencyTransaction>().GetPageFor(userId, page);
} }
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithTitle(GetText(strs.transactions( .WithTitle(GetText(strs.transactions(
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() ((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}"))) ?? $"{userId}")))
.WithOkColor(); .WithOkColor();
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var tr in trs) foreach (var tr in trs)
@ -435,7 +466,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return; return;
} }
var eb = _sender.CreateEmbed().WithOkColor(); var eb = CreateEmbed().WithOkColor();
eb.WithAuthor(ctx.User); eb.WithAuthor(ctx.User);
eb.WithTitle(GetText(strs.transaction)); eb.WithTitle(GetText(strs.transaction));
@ -692,18 +723,20 @@ public partial class Gambling : GamblingModule<GamblingService>
string str; string str;
if (win > 0) if (win > 0)
{ {
str = GetText(strs.br_win(N(win), result.Threshold + (result.Roll == 100 ? " 👑" : ""))); str = GetText(strs.betroll_win(result.Threshold + (result.Roll == 100 ? " 👑" : "")));
} }
else else
{ {
str = GetText(strs.better_luck); str = GetText(strs.better_luck);
} }
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithDescription(Format.Bold(str)) .WithDescription(Format.Bold(str))
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture)) .AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture), true)
.WithOkColor(); .AddField(GetText(strs.bet), N(amount), true)
.AddField(GetText(strs.won), N((long)result.Won), true)
.WithOkColor();
await Response().Embed(eb).SendAsync(); await Response().Embed(eb).SendAsync();
} }
@ -766,9 +799,9 @@ public partial class Gambling : GamblingModule<GamblingService>
.CurrentPage(page) .CurrentPage(page)
.Page((toSend, curPage) => .Page((toSend, curPage) =>
{ {
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard)); .WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
if (!toSend.Any()) if (!toSend.Any())
{ {
@ -829,7 +862,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return; return;
} }
var embed = _sender.CreateEmbed(); var embed = CreateEmbed();
string msg; string msg;
if (result.Result == RpsResultType.Draw) if (result.Result == RpsResultType.Draw)
@ -838,9 +871,6 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
else if (result.Result == RpsResultType.Win) else if (result.Result == RpsResultType.Win)
{ {
if ((long)result.Won > 0)
embed.AddField(GetText(strs.won), N((long)result.Won));
msg = GetText(strs.rps_win(ctx.User.Mention, msg = GetText(strs.rps_win(ctx.User.Mention,
GetRpsPick(pick), GetRpsPick(pick),
GetRpsPick((InputRpsPick)result.ComputerPick))); GetRpsPick((InputRpsPick)result.ComputerPick)));
@ -856,6 +886,13 @@ public partial class Gambling : GamblingModule<GamblingService>
.WithOkColor() .WithOkColor()
.WithDescription(msg); .WithDescription(msg);
if (amount > 0)
{
embed
.AddField(GetText(strs.bet), N(amount), true)
.AddField(GetText(strs.won), $"{N((long)result.Won)}", true);
}
await Response().Embed(embed).SendAsync(); await Response().Embed(embed).SendAsync();
} }
@ -893,12 +930,12 @@ public partial class Gambling : GamblingModule<GamblingService>
sb.AppendLine(); sb.AppendLine();
} }
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(sb.ToString()) .WithDescription(sb.ToString())
.AddField(GetText(strs.multiplier), $"{result.Multiplier:0.##}x", true) .AddField(GetText(strs.bet), N(amount), true)
.AddField(GetText(strs.won), $"{(long)result.Won}", true) .AddField(GetText(strs.won), $"{N((long)result.Won)}", true)
.WithAuthor(ctx.User); .WithAuthor(ctx.User);
await Response().Embed(eb).SendAsync(); await Response().Embed(eb).SendAsync();

View file

@ -0,0 +1,92 @@
// namespace EllieBot.Modules.Gambling;
// public sealed class Loan
// {
// public int Id { get; set; }
// public ulong LenderId { get; set; }
// public string LenderName { get; set; }
// public ulong BorrowerId { get; set; }
// public string BorrowerName { get; set; }
// public long Amount { get; set; }
// public decimal Interest { get; set; }
// public DateTime DueDate { get; set; }
// public bool Repaid { get; set; }
// }
//
// public sealed class LoanService : INService
// {
// public async Task<IReadOnlyList<Loan>> GetLoans(ulong userId)
// {
// }
//
// public async Task<object> RepayAsync(object loandId)
// {
// }
// }
//
// public partial class Gambling
// {
// public partial class LoanCommands : EllieModule<LoanService>
// {
// [Cmd]
// public async Task Loan(
// IUser lender,
// long amount,
// decimal interest = 0,
// TimeSpan dueIn = default)
// {
// var eb = CreateEmbed()
// .WithOkColor()
// .WithDescription("User 0 Requests a loan from User {1}")
// .AddField("Amount", amount, true)
// .AddField("Interest", (interest * 0.01m).ToString("P2"), true);
// }
//
// public Task Loans()
// => Loans(ctx.User);
//
// public async Task Loans([Leftover] IUser user)
// {
// var loans = await _service.GetLoans(user.Id);
//
// Response()
// .Paginated()
// .PageItems(loans)
// .Page((items, page) =>
// {
// var eb = CreateEmbed()
// .WithOkColor()
// .WithDescription("Current Loans");
//
// foreach (var item in items)
// {
// eb.AddField(new kwum(item.id).ToString(),
// $"""
// To: {item.LenderName}
// Amount: {}
// """,
// true);
// }
//
// return eb;
// });
// }
//
// [Cmd]
// public async Task Repay(kwum loanId)
// {
// var res = await _service.RepayAsync(loandId);
//
// if (res.TryPickT0(out var _, out var err))
// {
// }
// else
// {
// var errStr = err.Match(
// _ => "Not enough funds",
// _ => "Loan not found");
//
// await Response().Error(errStr).SendAsync();
// }
// }
// }
// }

View file

@ -59,7 +59,7 @@ public partial class Gambling
} }
var success = await _service.PlantAsync(ctx.Guild.Id, var success = await _service.PlantAsync(ctx.Guild.Id,
ctx.Channel, (ITextChannel)ctx.Channel,
ctx.User.Id, ctx.User.Id,
ctx.User.ToString(), ctx.User.ToString(),
amount, amount,
@ -103,9 +103,9 @@ public partial class Gambling
.Page((items, _) => .Page((items, _) =>
{ {
if (!items.Any()) if (!items.Any())
return _sender.CreateEmbed().WithErrorColor().WithDescription("-"); return CreateEmbed().WithErrorColor().WithDescription("-");
return items.Aggregate(_sender.CreateEmbed().WithOkColor(), return items.Aggregate(CreateEmbed().WithOkColor(),
(eb, i) => eb.AddField(i.GuildId.ToString(), i.ChannelId)); (eb, i) => eb.AddField(i.GuildId.ToString(), i.ChannelId));
}) })
.SendAsync(); .SendAsync();

View file

@ -241,12 +241,18 @@ public class PlantPickService : IEService, IExecNoCommand
await using (stream) await using (stream)
sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend); sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend);
await AddPlantToDatabase(channel.GuildId, var res = await AddPlantToDatabase(channel.GuildId,
channel.Id, channel.Id,
_client.CurrentUser.Id, _client.CurrentUser.Id,
sent.Id, sent.Id,
dropAmount, dropAmount,
pw); pw,
true);
if (res.toDelete.Length > 0)
{
await channel.DeleteMessagesAsync(res.toDelete);
}
} }
} }
} }
@ -335,7 +341,7 @@ public class PlantPickService : IEService, IExecNoCommand
public async Task<bool> PlantAsync( public async Task<bool> PlantAsync(
ulong gid, ulong gid,
IMessageChannel ch, ITextChannel ch,
ulong uid, ulong uid,
string user, string user,
long amount, long amount,
@ -361,6 +367,7 @@ public class PlantPickService : IEService, IExecNoCommand
// if it doesn't fail, put the plant in the database for other people to pick // if it doesn't fail, put the plant in the database for other people to pick
await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass); await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass);
return true; return true;
} }
@ -368,25 +375,41 @@ public class PlantPickService : IEService, IExecNoCommand
return false; return false;
} }
private async Task AddPlantToDatabase( private async Task<(long totalAmount, ulong[] toDelete)> AddPlantToDatabase(
ulong gid, ulong gid,
ulong cid, ulong cid,
ulong uid, ulong uid,
ulong mid, ulong mid,
long amount, long amount,
string pass) string pass,
bool auto = false)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
uow.Set<PlantedCurrency>()
.Add(new() PlantedCurrency[] deleted = [];
{ if (!string.IsNullOrWhiteSpace(pass) && auto)
Amount = amount, {
GuildId = gid, deleted = await uow.GetTable<PlantedCurrency>()
ChannelId = cid, .Where(x => x.GuildId == gid
Password = pass, && x.ChannelId == cid
UserId = uid, && x.Password != null
MessageId = mid && x.Password.Length == pass.Length)
}); .DeleteWithOutputAsync();
await uow.SaveChangesAsync(); }
var totalDeletedAmount = deleted.Length == 0 ? 0 : deleted.Sum(x => x.Amount);
await uow.GetTable<PlantedCurrency>()
.InsertAsync(() => new()
{
Amount = totalDeletedAmount + amount,
GuildId = gid,
ChannelId = cid,
Password = pass,
UserId = uid,
MessageId = mid,
});
return (totalDeletedAmount + amount, deleted.Select(x => x.MessageId).ToArray());
} }
} }

View file

@ -56,8 +56,8 @@ public partial class Gambling
.Page((items, curPage) => .Page((items, curPage) =>
{ {
if (!items.Any()) if (!items.Any())
return _sender.CreateEmbed().WithErrorColor().WithDescription(GetText(strs.shop_none)); return CreateEmbed().WithErrorColor().WithDescription(GetText(strs.shop_none));
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.shop)); var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.shop));
for (var i = 0; i < items.Count; i++) for (var i = 0; i < items.Count; i++)
{ {
@ -188,7 +188,7 @@ public partial class Gambling
{ {
await Response() await Response()
.User(ctx.User) .User(ctx.User)
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.shop_purchase(ctx.Guild.Name))) .WithTitle(GetText(strs.shop_purchase(ctx.Guild.Name)))
.AddField(GetText(strs.item), item.Text) .AddField(GetText(strs.item), item.Text)
@ -254,7 +254,7 @@ public partial class Gambling
.Replace("%you.name%", buyer.GlobalName ?? buyer.Username) .Replace("%you.name%", buyer.GlobalName ?? buyer.Username)
.Replace("%you.nick%", buyer.DisplayName); .Replace("%you.nick%", buyer.DisplayName);
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithPendingColor() .WithPendingColor()
.WithTitle("Executing shop command") .WithTitle("Executing shop command")
.WithDescription(cmd); .WithDescription(cmd);
@ -541,7 +541,7 @@ public partial class Gambling
public EmbedBuilder EntryToEmbed(ShopEntry entry) public EmbedBuilder EntryToEmbed(ShopEntry entry)
{ {
var embed = _sender.CreateEmbed().WithOkColor(); var embed = CreateEmbed().WithOkColor();
if (entry.Type == ShopEntryType.Role) if (entry.Type == ShopEntryType.Role)
{ {

View file

@ -65,11 +65,11 @@ public partial class Gambling
await using var imgStream = await image.ToStreamAsync(); await using var imgStream = await image.ToStreamAsync();
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithDescription(Format.Bold(text)) .WithDescription(Format.Bold(text))
.WithImageUrl($"attachment://result.png") .WithImageUrl($"attachment://result.png")
.WithOkColor(); .WithOkColor();
var bb = new ButtonBuilder(emote: Emoji.Parse("🔁"), customId: "slot:again", label: "Pull Again"); var bb = new ButtonBuilder(emote: Emoji.Parse("🔁"), customId: "slot:again", label: "Pull Again");
var inter = _inter.Create(ctx.User.Id, var inter = _inter.Create(ctx.User.Id,
@ -168,7 +168,8 @@ public partial class Gambling
await using (var uow = _db.GetDbContext()) await using (var uow = _db.GetDbContext())
{ {
ownedAmount = uow.Set<DiscordUser>() ownedAmount = uow.Set<DiscordUser>()
.FirstOrDefault(x => x.UserId == ctx.User.Id)?.CurrencyAmount .FirstOrDefault(x => x.UserId == ctx.User.Id)
?.CurrencyAmount
?? 0; ?? 0;
} }

View file

@ -8,7 +8,7 @@ namespace EllieBot.Modules.Gambling.Services;
public sealed class UserBetStatsService : IEService public sealed class UserBetStatsService : IEService
{ {
private const long RESET_MIN_PRICE = 1000; private const long RESET_MIN_PRICE = 1000;
private const decimal RESET_TOTAL_MULTIPLIER = 0.02m; private const decimal RESET_TOTAL_MULTIPLIER = 0.002m;
private readonly DbService _db; private readonly DbService _db;
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
@ -52,4 +52,16 @@ public sealed class UserBetStatsService : IEService
await ctx.GetTable<GamblingStats>() await ctx.GetTable<GamblingStats>()
.DeleteAsync(); .DeleteAsync();
} }
public async Task<IReadOnlyList<UserBetStats>> GetWinLbAsync(int page)
{
ArgumentOutOfRangeException.ThrowIfNegative(page);
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<UserBetStats>()
.OrderByDescending(x => x.MaxWin)
.Skip(page * 9)
.Take(9)
.ToArrayAsyncLinqToDB();
}
} }

View file

@ -21,7 +21,7 @@ public partial class Gambling
public async Task WaifuReset() public async Task WaifuReset()
{ {
var price = _service.GetResetPrice(ctx.User); var price = _service.GetResetPrice(ctx.User);
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithTitle(GetText(strs.waifu_reset_confirm)) .WithTitle(GetText(strs.waifu_reset_confirm))
.WithDescription(GetText(strs.waifu_reset_price(Format.Bold(N(price))))); .WithDescription(GetText(strs.waifu_reset_price(Format.Bold(N(price)))));
@ -46,7 +46,7 @@ public partial class Gambling
.PageItems(async (page) => await _service.GetClaimsAsync(ctx.User.Id, page)) .PageItems(async (page) => await _service.GetClaimsAsync(ctx.User.Id, page))
.Page((items, page) => .Page((items, page) =>
{ {
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("Waifus"); .WithTitle("Waifus");
@ -266,7 +266,7 @@ public partial class Gambling
return; return;
} }
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.waifus_top_waifus)).WithOkColor(); var embed = CreateEmbed().WithTitle(GetText(strs.waifus_top_waifus)).WithOkColor();
var i = 0; var i = 0;
foreach (var w in waifus) foreach (var w in waifus)
@ -350,7 +350,7 @@ public partial class Gambling
if (string.IsNullOrWhiteSpace(fansStr)) if (string.IsNullOrWhiteSpace(fansStr))
fansStr = "-"; fansStr = "-";
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.waifu) .WithTitle(GetText(strs.waifu)
+ " " + " "
@ -393,7 +393,7 @@ public partial class Gambling
.CurrentPage(page) .CurrentPage(page)
.Page((items, _) => .Page((items, _) =>
{ {
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.waifu_gift_shop)).WithOkColor(); var embed = CreateEmbed().WithTitle(GetText(strs.waifu_gift_shop)).WithOkColor();
items items
.ToList() .ToList()

View file

@ -67,7 +67,7 @@ public partial class Games
private Task Game_OnStarted(AcrophobiaGame game) private Task Game_OnStarted(AcrophobiaGame game)
{ {
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.acrophobia)) .WithTitle(GetText(strs.acrophobia))
.WithDescription( .WithDescription(
@ -92,7 +92,7 @@ public partial class Games
if (submissions.Length == 1) if (submissions.Length == 1)
{ {
await Response().Embed(_sender.CreateEmbed() await Response().Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(GetText( .WithDescription(GetText(
strs.acro_winner_only( strs.acro_winner_only(
@ -103,7 +103,7 @@ public partial class Games
var i = 0; var i = 0;
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed)) .WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed))
.WithDescription(GetText(strs.acro_nym_was( .WithDescription(GetText(strs.acro_nym_was(
@ -127,7 +127,7 @@ public partial class Games
var table = votes.OrderByDescending(v => v.Value); var table = votes.OrderByDescending(v => v.Value);
var winner = table.First(); var winner = table.First();
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.acrophobia)) .WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName), .WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName),

View file

@ -1,5 +1,6 @@
#nullable disable #nullable disable
using EllieBot.Modules.Games.Services; using EllieBot.Modules.Games.Services;
using System.Text;
namespace EllieBot.Modules.Games; namespace EllieBot.Modules.Games;
@ -38,10 +39,72 @@ public partial class Games : EllieModule<GamesService>
return; return;
var res = _service.GetEightballResponse(ctx.User.Id, question); var res = _service.GetEightballResponse(ctx.User.Id, question);
await Response().Embed(_sender.CreateEmbed() await Response()
.WithOkColor() .Embed(CreateEmbed()
.WithDescription(ctx.User.ToString()) .WithOkColor()
.AddField("❓ " + GetText(strs.question), question) .WithDescription(ctx.User.ToString())
.AddField("🎱 " + GetText(strs._8ball), res)).SendAsync(); .AddField("❓ " + GetText(strs.question), question)
.AddField("🎱 " + GetText(strs._8ball), res))
.SendAsync();
}
private readonly string[] _numberEmojis = ["0⃣", "1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣"];
[Cmd]
public async Task Minesweeper(int numberOfMines = 12)
{
var boardSizeX = 9;
var boardSizeY = 10;
if (numberOfMines < 1)
{
numberOfMines = 1;
}
else if (numberOfMines > boardSizeX * boardSizeY / 2)
{
numberOfMines = boardSizeX * boardSizeY / 2;
}
var mineIndicies = Enumerable.Range(0, boardSizeX * boardSizeY)
.ToArray()
.Shuffle()
.Take(numberOfMines)
.ToHashSet();
string GetNumberOnCell(int x, int y)
{
var count = 0;
for (var i = -1; i < 2; i++)
{
for (var j = -1; j < 2; j++)
{
if (y + j >= boardSizeY || y + j < 0)
continue;
if (x + i >= boardSizeX || x + i < 0)
continue;
var boardIndex = (y + j) * boardSizeX + (x + i);
if (mineIndicies.Contains(boardIndex))
count++;
}
}
return _numberEmojis[count];
}
var sb = new StringBuilder();
sb.AppendLine($"### Minesweeper [{numberOfMines}\\💣]");
for (var i = 0; i < boardSizeY; i++)
{
for (var j = 0; j < boardSizeX; j++)
{
var emoji = mineIndicies.Contains((i * boardSizeX) + j) ? "💣" : GetNumberOnCell(j, i);
sb.Append($"||{emoji}||");
}
sb.AppendLine();
}
await Response().Text(sb.ToString()).SendAsync();
} }
} }

View file

@ -53,7 +53,7 @@ public partial class Games
var hint = GetText(strs.nc_hint(prefix, _service.GetWidth(), _service.GetHeight())); var hint = GetText(strs.nc_hint(prefix, _service.GetWidth(), _service.GetHeight()));
await Response() await Response()
.File(stream, "ncanvas.png") .File(stream, "ncanvas.png")
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
#if GLOBAL_ELLIE #if GLOBAL_ELLIE
.WithDescription("This is not available yet.") .WithDescription("This is not available yet.")
@ -164,7 +164,7 @@ public partial class Games
Culture, Culture,
_gcs.Data.Currency.Sign)))); _gcs.Data.Currency.Sign))));
if (!await PromptUserConfirmAsync(_sender.CreateEmbed() if (!await PromptUserConfirmAsync(CreateEmbed()
.WithPendingColor() .WithPendingColor()
.WithDescription(prompt))) .WithDescription(prompt)))
{ {
@ -193,7 +193,7 @@ public partial class Games
await using var stream = await img.ToStreamAsync(); await using var stream = await img.ToStreamAsync();
await Response() await Response()
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(GetText(strs.nc_pixel_set(Format.Code(position.ToString())))) .WithDescription(GetText(strs.nc_pixel_set(Format.Code(position.ToString()))))
.WithImageUrl($"attachment://zoom_{position}.png")) .WithImageUrl($"attachment://zoom_{position}.png"))
@ -231,7 +231,7 @@ public partial class Games
var pos = new kwum(pixel.Position); var pos = new kwum(pixel.Position);
await Response() await Response()
.File(stream, $"{pixel.Position}.png") .File(stream, $"{pixel.Position}.png")
.Embed(_sender.CreateEmbed() .Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithDescription(string.IsNullOrWhiteSpace(pixel.Text) ? string.Empty : pixel.Text) .WithDescription(string.IsNullOrWhiteSpace(pixel.Text) ? string.Empty : pixel.Text)
.WithTitle(GetText(strs.nc_pixel(pos))) .WithTitle(GetText(strs.nc_pixel(pos)))
@ -263,7 +263,7 @@ public partial class Games
return; return;
} }
if (!await PromptUserConfirmAsync(_sender.CreateEmbed() if (!await PromptUserConfirmAsync(CreateEmbed()
.WithDescription( .WithDescription(
"This will reset the canvas to the specified image. All prices, text and colors will be reset.\n\n" "This will reset the canvas to the specified image. All prices, text and colors will be reset.\n\n"
+ "Are you sure you want to continue?"))) + "Are you sure you want to continue?")))
@ -293,7 +293,7 @@ public partial class Games
{ {
await _service.ResetAsync(); await _service.ResetAsync();
if (!await PromptUserConfirmAsync(_sender.CreateEmbed() if (!await PromptUserConfirmAsync(CreateEmbed()
.WithDescription( .WithDescription(
"This will delete all pixels and reset the canvas.\n\n" "This will delete all pixels and reset the canvas.\n\n"
+ "Are you sure you want to continue?"))) + "Are you sure you want to continue?")))

View file

@ -94,7 +94,7 @@ public partial class Games
if (removed is null) if (removed is null)
return; return;
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithTitle($"Removed typing article #{index + 1}") .WithTitle($"Removed typing article #{index + 1}")
.WithDescription(removed.Text.TrimTo(50)) .WithDescription(removed.Text.TrimTo(50))
.WithOkColor(); .WithOkColor();

View file

@ -160,7 +160,7 @@ public partial class Games
{ {
try try
{ {
questionEmbed = _sender.CreateEmbed() questionEmbed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.trivia_game)) .WithTitle(GetText(strs.trivia_game))
.AddField(GetText(strs.category), question.Category) .AddField(GetText(strs.category), question.Category)
@ -189,7 +189,7 @@ public partial class Games
{ {
try try
{ {
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithErrorColor() .WithErrorColor()
.WithTitle(GetText(strs.trivia_game)) .WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_times_up(Format.Bold(question.Answer)))); .WithDescription(GetText(strs.trivia_times_up(Format.Bold(question.Answer))));
@ -221,7 +221,7 @@ public partial class Games
{ {
try try
{ {
await Response().Embed(_sender.CreateEmbed() await Response().Embed(CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(GetText(strs.trivia_ended)) .WithAuthor(GetText(strs.trivia_ended))
.WithTitle(GetText(strs.leaderboard)) .WithTitle(GetText(strs.leaderboard))
@ -247,7 +247,7 @@ public partial class Games
{ {
try try
{ {
var embed = _sender.CreateEmbed() var embed = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.trivia_game)) .WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_win(user.Name, .WithDescription(GetText(strs.trivia_win(user.Name,

View file

@ -114,7 +114,7 @@ public sealed partial class Help : EllieModule<HelpService>
.AddFooter(false) .AddFooter(false)
.Page((items, _) => .Page((items, _) =>
{ {
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.list_of_modules)); var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.list_of_modules));
if (!items.Any()) if (!items.Any())
{ {
@ -315,7 +315,7 @@ public sealed partial class Help : EllieModule<HelpService>
.WithPlaceholder("Select a submodule to see detailed commands"); .WithPlaceholder("Select a submodule to see detailed commands");
var groups = cmdsWithGroup.ToArray(); var groups = cmdsWithGroup.ToArray();
var embed = _sender.CreateEmbed().WithOkColor(); var embed = CreateEmbed().WithOkColor();
foreach (var g in groups) foreach (var g in groups)
{ {
sb.AddOption(g.Key, g.Key); sb.AddOption(g.Key, g.Key);
@ -383,7 +383,7 @@ public sealed partial class Help : EllieModule<HelpService>
.Interaction(inter) .Interaction(inter)
.Page((items, _) => .Page((items, _) =>
{ {
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithTitle(GetText(strs.cmd_group_commands(group.Name))) .WithTitle(GetText(strs.cmd_group_commands(group.Name)))
.WithOkColor(); .WithOkColor();
@ -520,7 +520,7 @@ public sealed partial class Help : EllieModule<HelpService>
[OnlyPublicBot] [OnlyPublicBot]
public async Task Donate() public async Task Donate()
{ {
var eb = _sender.CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("Thank you for considering to donate to the EllieBot project!"); .WithTitle("Thank you for considering to donate to the EllieBot project!");

Some files were not shown because too many files have changed in this diff Show more