Compare commits

...

32 commits
5.2.3 ... 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
83 changed files with 15761 additions and 5709 deletions

View file

@ -2,6 +2,87 @@
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
@ -38,46 +119,49 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except da
- 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
- `.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.
- 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
- 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 Ellie'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
- `.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%
@ -91,9 +175,9 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except da
### Added
- 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
- 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
- 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
- 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 `.gamblestatsreset` / `.gsreset` owner-only command to reset bot stats for all games
- Added `.waifuclaims` command which lists all of your claimed waifus
@ -103,9 +187,9 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except da
- `.divorce` no longer has a cooldown
- `.betroll` has a 2% better payout
- `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins,
- now has a new symbol, wheat
- worse around 1% in total (now shares the top spot with .bf)
- `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins,
- now has a new symbol, wheat
- worse around 1% in total (now shares the top spot with .bf)
## [5.1.19] - 05.11.2024
@ -124,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
- `.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

View file

@ -9,7 +9,7 @@
</PropertyGroup>
<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="YamlDotNet" Version="15.1.4" />
</ItemGroup>

View file

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

View file

@ -74,6 +74,35 @@ public abstract class EllieContext : DbContext
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>()
@ -135,13 +164,18 @@ public abstract class EllieContext : DbContext
#region UserBetStats
modelBuilder.Entity<UserBetStats>()
.HasIndex(x => new
{
x.UserId,
x.Game
})
.IsUnique();
modelBuilder.Entity<UserBetStats>(ubs =>
{
ubs.HasIndex(x => new
{
x.UserId,
x.Game
})
.IsUnique();
ubs.HasIndex(x => x.MaxWin)
.IsUnique(false);
});
#endregion
@ -449,7 +483,6 @@ public abstract class EllieContext : DbContext
xps.HasIndex(x => x.UserId);
xps.HasIndex(x => x.GuildId);
xps.HasIndex(x => x.Xp);
xps.HasIndex(x => x.AwardedXp);
#endregion

View file

@ -20,7 +20,6 @@ public static class UserXpExtensions
{
Xp = 0,
UserId = userId,
NotifyOnLevelUp = XpNotificationLocation.None,
GuildId = guildId
});
}
@ -31,17 +30,17 @@ public static class UserXpExtensions
public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
=> await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp)
.OrderByDescending(x => x.Xp)
.Take(count)
.ToListAsyncLinqToDB();
public static async Task<int> GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
=> await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId
&& x.Xp + x.AwardedXp
&& x.Xp
> xps.AsQueryable()
.Where(y => y.UserId == userId && y.GuildId == guildId)
.Select(y => y.Xp + y.AwardedXp)
.Select(y => y.Xp)
.FirstOrDefault())
.CountAsyncLinqToDB()
+ 1;
@ -53,6 +52,6 @@ public static class UserXpExtensions
=> await userXp
.Where(x => x.GuildId == guildId && x.UserId == userId)
.FirstOrDefaultAsyncLinqToDB() is UserXpStats uxs
? new(uxs.Xp + uxs.AwardedXp)
? new(uxs.Xp)
: new(0);
}

View file

@ -3,38 +3,28 @@ namespace EllieBot.Db;
public readonly struct LevelStats
{
public const int XP_REQUIRED_LVL_1 = 36;
public long Level { get; }
public long LevelXp { get; }
public long RequiredXp { get; }
public long TotalXp { get; }
public LevelStats(long xp)
public LevelStats(long totalXp)
{
if (xp < 0)
xp = 0;
if (totalXp < 0)
totalXp = 0;
TotalXp = xp;
const int baseXp = XP_REQUIRED_LVL_1;
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;
TotalXp = totalXp;
Level = GetLevelByTotalXp(totalXp);
LevelXp = totalXp - GetTotalXpReqForLevel(Level);
RequiredXp = (9 * (Level + 1)) + 27;
}
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

@ -36,29 +36,37 @@ public class GuildConfig : DbEntity
public HashSet<FilterChannelId> FilterInvitesChannelIds { 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 HashSet<FilteredWord> FilteredWords { get; set; } = new();
public HashSet<FilterWordsChannelId> FilterWordsChannelIds { get; set; } = new();
// mute
public HashSet<MutedUserId> MutedUsers { get; set; } = new();
public string MuteRoleName { get; set; }
// chatterbot
public bool CleverbotEnabled { get; set; }
// protection
public AntiRaidSetting AntiRaidSetting { get; set; }
public AntiSpamSetting AntiSpamSetting { get; set; }
public AntiAltSetting AntiAltSetting { get; set; }
// time
public string Locale { get; set; }
public string TimeZoneId { get; set; }
// timers
public HashSet<UnmuteTimer> UnmuteTimers { get; set; } = new();
public HashSet<UnbanTimer> UnbanTimer { get; set; } = new();
public HashSet<UnroleTimer> UnroleTimer { get; set; } = new();
// vcrole
public HashSet<VcRoleInfo> VcRoleInfos { get; set; }
// aliases
public HashSet<CommandAlias> CommandAliases { get; set; } = new();
public bool WarningsInitialized { get; set; }
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }

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

@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models;
public sealed class SarGroup : DbEntity
public sealed class SarGroup
{
[Key]
public int Id { get; set; }

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 GuildId { 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>
<ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.2.3</Version>
<Version>5.3.3</Version>
<!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -29,7 +29,7 @@
</PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" />
<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="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414" />

View file

@ -5,8 +5,21 @@ namespace EllieBot.Migrations;
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

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");
}
}
}

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)]
[UserPerm(GuildPerm.Administrator)]
[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);
if (newValue)
@ -54,12 +54,12 @@ public partial class Administration : EllieModule<AdministrationService>
else
await Response().Pending(strs.imageonly_disable).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[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);
if (newValue)
@ -72,10 +72,10 @@ public partial class Administration : EllieModule<AdministrationService>
[RequireContext(ContextType.Guild)]
[UserPerm(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;
if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6)))
var seconds = (int?)timespan?.Time.TotalSeconds ?? 0;
if (timespan is not null && (timespan.Time < TimeSpan.FromSeconds(0) || timespan.Time > TimeSpan.FromHours(6)))
return;
await ((ITextChannel)ctx.Channel).ModifyAsync(tcp =>
@ -221,7 +221,7 @@ public partial class Administration : EllieModule<AdministrationService>
[BotPerm(GuildPerm.ManageChannels)]
public async Task CreaTxtChanl([Leftover] string channelName)
{
var txtCh = await ctx.Guild.CreateTextChannelAsync(channelName);
var txtCh = await ctx.Guild.CreateTextChannelAsync(channelName);
await Response().Confirm(strs.createtextchan(Format.Bold(txtCh.Name))).SendAsync();
}
@ -298,18 +298,18 @@ public partial class Administration : EllieModule<AdministrationService>
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPerm.ManageMessages)]
[BotPerm(ChannelPerm.ManageMessages)]
public Task Delete(ulong messageId, StoopidTime time = null)
=> Delete((ITextChannel)ctx.Channel, messageId, time);
public Task Delete(ulong messageId, ParsedTimespan timespan = null)
=> Delete((ITextChannel)ctx.Channel, messageId, timespan);
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null)
=> await InternalMessageAction(channel, messageId, time, msg => msg.DeleteAsync());
public async Task Delete(ITextChannel channel, ulong messageId, ParsedTimespan timespan = null)
=> await InternalMessageAction(channel, messageId, timespan, msg => msg.DeleteAsync());
private async Task InternalMessageAction(
ITextChannel channel,
ulong messageId,
StoopidTime time,
ParsedTimespan timespan,
Func<IMessage, Task> func)
{
var userPerms = ((SocketGuildUser)ctx.User).GetPermissions(channel);
@ -334,13 +334,13 @@ public partial class Administration : EllieModule<AdministrationService>
return;
}
if (time is null)
if (timespan is null)
await msg.DeleteAsync();
else if (time.Time <= TimeSpan.FromDays(7))
else if (timespan.Time <= TimeSpan.FromDays(7))
{
_ = Task.Run(async () =>
{
await Task.Delay(time.Time);
await Task.Delay(timespan.Time);
await msg.DeleteAsync();
});
}
@ -360,11 +360,11 @@ public partial class Administration : EllieModule<AdministrationService>
{
if (ctx.Channel is not SocketTextChannel stc)
return;
await stc.CreateThreadAsync(name, message: ctx.Message.ReferencedMessage);
await ctx.OkAsync();
}
[Cmd]
[BotPerm(ChannelPermission.ManageThreads)]
[UserPerm(ChannelPermission.ManageThreads)]
@ -380,7 +380,7 @@ public partial class Administration : EllieModule<AdministrationService>
await Response().Error(strs.not_found).SendAsync();
return;
}
await t.DeleteAsync();
await ctx.OkAsync();
}
@ -406,7 +406,7 @@ public partial class Administration : EllieModule<AdministrationService>
await Response().Confirm(strs.autopublish_disable).SendAsync();
}
}
[Cmd]
[UserPerm(GuildPerm.ManageNicknames)]
[BotPerm(GuildPerm.ChangeNickname)]
@ -450,8 +450,9 @@ public partial class Administration : EllieModule<AdministrationService>
public async Task SetServerBanner([Leftover] string img = null)
{
// 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);
switch (result)
@ -472,7 +473,7 @@ public partial class Administration : EllieModule<AdministrationService>
throw new ArgumentOutOfRangeException();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPermission.ManageGuild)]

View file

@ -71,7 +71,7 @@ public sealed class HoneyPotService : IHoneyPotService, IReadyExecutor, IExecNoC
try
{
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);
}
catch (Exception e)

View file

@ -72,18 +72,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)]
[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;
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
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()),
(int)time.Time.TotalMinutes)).SendAsync();
(int)timespan.Time.TotalMinutes)).SendAsync();
}
catch (Exception ex)
{
@ -133,18 +133,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[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;
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
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()),
(int)time.Time.TotalMinutes)).SendAsync();
(int)timespan.Time.TotalMinutes)).SendAsync();
}
catch (Exception ex)
{
@ -193,18 +193,18 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.MuteMembers)]
[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;
try
{
if (!await VerifyMutePermissions((IGuildUser)ctx.User, user))
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()),
(int)time.Time.TotalMinutes)).SendAsync();
(int)timespan.Time.TotalMinutes)).SendAsync();
}
catch
{

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

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

View file

@ -28,17 +28,17 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task AntiAlt(
StoopidTime minAge,
ParsedTimespan minAge,
PunishmentAction action,
[Leftover] StoopidTime punishTime = null)
[Leftover] ParsedTimespan punishTimespan = null)
{
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)
return;
var minutes = (int?)punishTime?.Time.TotalMinutes ?? 0;
var minutes = (int?)punishTimespan?.Time.TotalMinutes ?? 0;
if (action is PunishmentAction.TimeOut && minutes < 1)
minutes = 1;
@ -53,7 +53,7 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
[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;
@ -86,8 +86,8 @@ public partial class Administration
int userThreshold,
int seconds,
PunishmentAction action,
[Leftover] StoopidTime punishTime)
=> InternalAntiRaid(userThreshold, seconds, action, punishTime);
[Leftover] ParsedTimespan punishTimespan)
=> InternalAntiRaid(userThreshold, seconds, action, punishTimespan);
[Cmd]
[RequireContext(ContextType.Guild)]
@ -100,7 +100,7 @@ public partial class Administration
int userThreshold,
int seconds = 10,
PunishmentAction action = PunishmentAction.Mute,
StoopidTime punishTime = null)
ParsedTimespan punishTimespan = null)
{
if (action == PunishmentAction.AddRole)
{
@ -120,13 +120,13 @@ public partial class Administration
return;
}
if (punishTime is not null)
if (punishTimespan is not null)
{
if (!_service.IsDurationAllowed(action))
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)
return;
@ -170,8 +170,8 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(1)]
public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime)
=> InternalAntiSpam(messageCount, action, punishTime);
public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] ParsedTimespan punishTimespan)
=> InternalAntiSpam(messageCount, action, punishTimespan);
[Cmd]
[RequireContext(ContextType.Guild)]
@ -183,19 +183,19 @@ public partial class Administration
private async Task InternalAntiSpam(
int messageCount,
PunishmentAction action,
StoopidTime timeData = null,
ParsedTimespan timespanData = null,
IRole role = null)
{
if (messageCount is < 2 or > 10)
return;
if (timeData is not null)
if (timespanData is not null)
{
if (!_service.IsDurationAllowed(action))
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)
return;

View file

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

View file

@ -1,4 +1,6 @@
#nullable disable
using Google.Protobuf.WellKnownTypes;
using EllieBot.Common.TypeReaders.Models;
using SixLabors.ImageSharp.PixelFormats;
using Color = SixLabors.ImageSharp.Color;
@ -13,13 +15,18 @@ public partial class Administration
Excl
}
private readonly TempRoleService _tempRoleService;
private readonly IServiceProvider _services;
private StickyRolesService _stickyRoleSvc;
public RoleCommands(IServiceProvider services, StickyRolesService stickyRoleSvc)
public RoleCommands(
IServiceProvider services,
StickyRolesService stickyRoleSvc,
TempRoleService tempRoleService)
{
_services = services;
_stickyRoleSvc = stickyRoleSvc;
_tempRoleService = tempRoleService;
}
[Cmd]
@ -34,13 +41,16 @@ public partial class Administration
return;
try
{
await targetUser.AddRoleAsync(roleToAdd, new RequestOptions()
{
AuditLogReason = $"Added by [{ctx.User.Username}]"
});
await targetUser.AddRoleAsync(roleToAdd,
new RequestOptions()
{
AuditLogReason = $"Added by [{ctx.User.Username}]"
});
await Response().Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
Format.Bold(targetUser.ToString()))).SendAsync();
await Response()
.Confirm(strs.setrole(Format.Bold(roleToAdd.Name),
Format.Bold(targetUser.ToString())))
.SendAsync();
}
catch (Exception ex)
{
@ -62,8 +72,10 @@ public partial class Administration
try
{
await targetUser.RemoveRoleAsync(roleToRemove);
await Response().Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
Format.Bold(targetUser.ToString()))).SendAsync();
await Response()
.Confirm(strs.remrole(Format.Bold(roleToRemove.Name),
Format.Bold(targetUser.ToString())))
.SendAsync();
}
catch
{
@ -204,5 +216,29 @@ public partial class Administration
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,67 +5,28 @@ using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration.Services;
public sealed class PlayingRotateService : IEService, IReadyExecutor
public sealed class BotActivityService : IBotActivityService, IReadyExecutor, IEService
{
private readonly BotConfigService _bss;
private readonly SelfService _selfService;
private readonly TypedKey<ActivityPubData> _activitySetKey = new("activity.set");
private readonly IReplacementService _repService;
// private readonly Replacer _rep;
private readonly DbService _db;
private readonly IPubSub _pubSub;
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly IReplacementService _rep;
private readonly BotConfigService _bss;
public PlayingRotateService(
public BotActivityService(
IPubSub pubSub,
DiscordSocketClient client,
DbService db,
BotConfigService bss,
IEnumerable<IPlaceholderProvider> phProviders,
SelfService selfService,
IReplacementService repService)
IReplacementService rep,
BotConfigService bss)
{
_db = db;
_bss = bss;
_selfService = selfService;
_repService = repService;
_pubSub = pubSub;
_client = client;
}
public async Task OnReadyAsync()
{
if (_client.ShardId != 0)
return;
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
var index = 0;
while (await timer.WaitForNextTickAsync())
{
try
{
if (!_bss.Data.RotateStatuses)
continue;
IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
await using (var uow = _db.GetDbContext())
{
rotatingStatuses = uow.Set<RotatingPlayingStatus>().AsNoTracking().OrderBy(x => x.Id).ToList();
}
if (rotatingStatuses.Count == 0)
continue;
var playingStatus = index >= rotatingStatuses.Count
? rotatingStatuses[index = 0]
: rotatingStatuses[index++];
var statusText = await _repService.ReplaceAsync(playingStatus.Status, new(client: _client));
await _selfService.SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
}
catch (Exception ex)
{
Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
}
}
_db = db;
_rep = rep;
_bss = bss;
}
public async Task<string> RemovePlayingAsync(int index)
@ -116,4 +77,91 @@ public sealed class PlayingRotateService : IEService, IReadyExecutor
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()
{
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)
return;
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
var index = 0;
while (await timer.WaitForNextTickAsync())
{
try
{
if (!_bss.Data.RotateStatuses)
continue;
IReadOnlyList<RotatingPlayingStatus> rotatingStatuses;
await using (var uow = _db.GetDbContext())
{
rotatingStatuses = uow.Set<RotatingPlayingStatus>().AsNoTracking().OrderBy(x => x.Id).ToList();
}
if (rotatingStatuses.Count == 0)
continue;
var playingStatus = index >= rotatingStatuses.Count
? rotatingStatuses[index = 0]
: rotatingStatuses[index++];
var statusText = await _rep.ReplaceAsync(playingStatus.Status, new(client: _client));
await SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
}
catch (Exception ex)
{
Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message);
}
}
}
}

View file

@ -19,7 +19,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
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(
BotConfigService bcs,
@ -72,7 +72,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
UpdateLastKnownVersion(latestVersion);
// 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);
@ -95,7 +95,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
.WithOkColor()
.WithAuthor($"EllieBot v{latest} Released!")
.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))
.WithFooter(
"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 ICoordinator _coord;
private readonly DbService _db;
private readonly IBotActivityService _bas;
public SelfCommands(
DiscordSocketClient client,
DbService db,
IBotStrings strings,
ICoordinator coord,
IMarmaladeLoaderService marmaladeLoader)
IMarmaladeLoaderService marmaladeLoader,
IBotActivityService bas)
{
_client = client;
_db = db;
_strings = strings;
_coord = coord;
_marmaladeLoader = marmaladeLoader;
_bas = bas;
}
@ -63,9 +66,9 @@ public partial class Administration
await message.ModifyAsync(x =>
x.Embed = CreateEmbed()
.WithDescription(GetText(strs.cache_users_done(added, updated)))
.WithOkColor()
.Build()
.WithDescription(GetText(strs.cache_users_done(added, updated)))
.WithOkColor()
.Build()
);
}
@ -116,13 +119,13 @@ public partial class Administration
await Response()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.scadd))
.AddField(GetText(strs.server),
cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
true)
.AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
.AddField(GetText(strs.command_text), cmdText))
.WithOkColor()
.WithTitle(GetText(strs.scadd))
.AddField(GetText(strs.server),
cmd.GuildId is null ? "-" : $"{cmd.GuildName}/{cmd.GuildId}",
true)
.AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true)
.AddField(GetText(strs.command_text), cmdText))
.SendAsync();
}
@ -320,16 +323,16 @@ public partial class Administration
.ToArray());
var allShardStrings = statuses.Select(st =>
{
var timeDiff = DateTime.UtcNow - st.LastUpdate;
var stateStr = ConnectionStateToEmoji(st);
var maxGuildCountLength =
statuses.Max(x => x.GuildCount).ToString().Length;
return $"`{stateStr} "
+ $"| #{st.ShardId.ToString().PadBoth(3)} "
+ $"| {timeDiff:mm\\:ss} "
+ $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `";
})
{
var timeDiff = DateTime.UtcNow - st.LastUpdate;
var stateStr = ConnectionStateToEmoji(st);
var maxGuildCountLength =
statuses.Max(x => x.GuildCount).ToString().Length;
return $"`{stateStr} "
+ $"| #{st.ShardId.ToString().PadBoth(3)} "
+ $"| {timeDiff:mm\\:ss} "
+ $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `";
})
.ToArray();
await Response()
.Paginated()
@ -437,7 +440,8 @@ public partial class Administration
return;
}
try { await Response().Confirm(strs.restarting).SendAsync(); }
try
{ await Response().Confirm(strs.restarting).SendAsync(); }
catch { }
}
@ -496,7 +500,7 @@ public partial class Administration
// var rep = new ReplacementBuilder().WithDefault(Context).Build();
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();
}
@ -518,7 +522,7 @@ public partial class Administration
{
name ??= "";
await _service.SetStreamAsync(name, url);
await _bas.SetStreamAsync(name, url);
await Response().Confirm(strs.set_stream).SendAsync();
}

View file

@ -28,7 +28,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
private readonly IMessageSenderService _sender;
//keys
private readonly TypedKey<ActivityPubData> _activitySetKey;
private readonly TypedKey<string> _guildLeaveKey;
public SelfService(
@ -51,11 +50,8 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
_bss = bss;
_pubSub = pubSub;
_sender = sender;
_activitySetKey = new("activity.set");
_guildLeaveKey = new("guild.leave");
HandleStatusChanges();
_pubSub.Sub(_guildLeaveKey,
async input =>
{
@ -394,49 +390,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
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>
/// 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.

View file

@ -84,7 +84,7 @@ public partial class Administration
IUserMessage msg = null;
try
{
if (guildUser.RoleIds.Contains(role.Id))
if (!guildUser.RoleIds.Contains(role.Id))
{
msg = await Response().Error(strs.self_assign_not_have(Format.Bold(role.Name))).SendAsync();
return;

View file

@ -59,10 +59,15 @@ public class SelfAssignedRolesService : IEService, IReadyExecutor
},
_ => new()
{
SarGroupId = ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.Select(x => x.Id)
.First()
},
() => new()
{
RoleId = roleId,
GuildId = guildId,
});
}
@ -280,8 +285,12 @@ public sealed class SarAssignerService : IEService, IReadyExecutor
if (item.Group.IsExclusive)
{
var rolesToRemove = item.Group.Roles.Select(x => x.RoleId);
await item.User.RemoveRolesAsync(rolesToRemove);
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);

View file

@ -313,7 +313,7 @@ public partial class Administration
int number,
AddRole _,
IRole role,
StoopidTime time = null)
ParsedTimespan timespan = null)
{
var punish = PunishmentAction.AddRole;
@ -324,12 +324,12 @@ public partial class Administration
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)
return;
if (time is null)
if (timespan is null)
{
await Response()
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
@ -341,7 +341,7 @@ public partial class Administration
await Response()
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
Format.Bold(number.ToString()),
Format.Bold(time.Input)))
Format.Bold(timespan.Input)))
.SendAsync();
}
}
@ -349,7 +349,7 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
[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
// also disallow warn punishment for getting warned
@ -357,15 +357,15 @@ public partial class Administration
return;
// you must specify the time for timeout
if (punish is PunishmentAction.TimeOut && time is null)
if (punish is PunishmentAction.TimeOut && timespan is null)
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)
return;
if (time is null)
if (timespan is null)
{
await Response()
.Confirm(strs.warn_punish_set(Format.Bold(punish.ToString()),
@ -377,7 +377,7 @@ public partial class Administration
await Response()
.Confirm(strs.warn_punish_set_timed(Format.Bold(punish.ToString()),
Format.Bold(number.ToString()),
Format.Bold(time.Input)))
Format.Bold(timespan.Input)))
.SendAsync();
}
}
@ -417,17 +417,17 @@ public partial class Administration
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Priority(1)]
public Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null)
=> Ban(time, user.Id, msg);
public Task Ban(ParsedTimespan timespan, IUser user, [Leftover] string msg = null)
=> Ban(timespan, user.Id, msg);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[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;
var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
@ -444,7 +444,7 @@ public partial class Administration
{
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
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)
await Response().User(guildUser).Text(smartText).SendAsync();
}
@ -456,14 +456,14 @@ public partial class Administration
var user = await ctx.Client.GetUserAsync(userId);
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 = CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
.AddField("ID", userId.ToString(), true)
.AddField(GetText(strs.duration),
time.Time.ToPrettyStringHm(),
timespan.Time.ToPrettyStringHm(),
true);
if (dmFailed)
@ -601,7 +601,7 @@ public partial class Administration
[UserPerm(GuildPerm.BanMembers)]
[BotPerm(GuildPerm.BanMembers)]
[Priority(1)]
public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null)
public Task BanMessageTest(ParsedTimespan duration, [Leftover] string reason = null)
=> InternalBanMessageTest(reason, duration.Time);
private async Task InternalBanMessageTest(string reason, TimeSpan? duration)
@ -791,7 +791,7 @@ public partial class Administration
[UserPerm(GuildPerm.ModerateMembers)]
[BotPerm(GuildPerm.ModerateMembers)]
[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);
@ -817,7 +817,7 @@ public partial class Administration
dmFailed = true;
}
await user.SetTimeOutAsync(time.Time);
await user.SetTimeOutAsync(timespan.Time);
var toSend = CreateEmbed()
.WithOkColor()

View file

@ -1,6 +1,7 @@
#nullable disable
using EllieBot.Modules.Gambling.Common;
using EllieBot.Modules.Gambling.Services;
using EllieBot.Modules.Xp.Services;
namespace EllieBot.Modules.Gambling;
@ -10,13 +11,19 @@ public partial class Gambling
public sealed class BetStatsCommands : GamblingModule<UserBetStatsService>
{
private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IBotCache _cache;
private readonly IUserService _userService;
public BetStatsCommands(
GamblingTxTracker gamblingTxTracker,
GamblingConfigService gcs)
GamblingConfigService gcs,
IBotCache cache,
IUserService userService)
: base(gcs)
{
_gamblingTxTracker = gamblingTxTracker;
_cache = cache;
_userService = userService;
}
[Cmd]
@ -25,12 +32,12 @@ public partial class Gambling
var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game);
var result = await PromptUserConfirmAsync(CreateEmbed()
.WithDescription(
$"""
Are you sure you want to reset your bet stats for **{GetGameName(game)}**?
.WithDescription(
$"""
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)
return;
@ -88,15 +95,15 @@ public partial class Gambling
};
var eb = CreateEmbed()
.WithOkColor()
.WithAuthor(user)
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
.AddField("Payout",
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
true);
.WithOkColor()
.WithAuthor(user)
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
.AddField("Payout",
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
true);
if (game == null)
{
var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount);
@ -115,23 +122,96 @@ public partial class Gambling
.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]
public async Task GambleStats()
{
var stats = await _gamblingTxTracker.GetAllAsync();
var eb = CreateEmbed()
.WithOkColor();
.WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
str += "――――――――――――――――――――\n";
foreach (var stat in stats)
{
var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture);
str += $"`{stat.Feature.PadBoth(9)}`"
+ $"`{stat.Bet.ToString("N0").PadLeft(8, ' ')}`"
+ $"`{stat.PaidOut.ToString("N0").PadLeft(8, ' ')}`"
+ $"`{perc.PadLeft(6, ' ')}`\n";
+ $"`{stat.Bet.ToString("N0").PadLeft(8, '')}`"
+ $"`{stat.PaidOut.ToString("N0").PadLeft(8, '')}`"
+ $"`{perc.PadLeft(6, '')}`\n";
}
var bet = stats.Sum(x => x.Bet);
@ -143,9 +223,9 @@ public partial class Gambling
var tPerc = (paidOut / bet).ToString("P2", Culture);
str += "――――――――――――――――――――\n";
str += $"` {("TOTAL").PadBoth(7)}` "
+ $"**{N(bet).PadLeft(8, ' ')}**"
+ $"**{N(paidOut).PadLeft(8, ' ')}**"
+ $"`{tPerc.PadLeft(6, ' ')}`";
+ $"**{N(bet).PadLeft(8, '')}**"
+ $"**{N(paidOut).PadLeft(8, '')}**"
+ $"`{tPerc.PadLeft(6, '')}`";
eb.WithDescription(str);
@ -157,13 +237,13 @@ public partial class Gambling
public async Task GambleStatsReset()
{
if (!await PromptUserConfirmAsync(CreateEmbed()
.WithDescription(
"""
Are you sure?
This will completely reset Gambling Stats.
.WithDescription(
"""
Are you sure?
This will completely reset Gambling Stats.
This action is irreversible.
""")))
This action is irreversible.
""")))
return;
await GambleStats();

View file

@ -135,7 +135,6 @@ public partial class Gambling : GamblingModule<GamblingService>
});
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Timely()
{
var val = Config.Timely.Amount;

View file

@ -52,4 +52,16 @@ public sealed class UserBetStatsService : IEService
await ctx.GetTable<GamblingStats>()
.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

@ -1,5 +1,6 @@
#nullable disable
using EllieBot.Modules.Games.Services;
using System.Text;
namespace EllieBot.Modules.Games;
@ -38,10 +39,72 @@ public partial class Games : EllieModule<GamesService>
return;
var res = _service.GetEightballResponse(ctx.User.Id, question);
await Response().Embed(CreateEmbed()
.WithOkColor()
.WithDescription(ctx.User.ToString())
.AddField("❓ " + GetText(strs.question), question)
.AddField("🎱 " + GetText(strs._8ball), res)).SendAsync();
await Response()
.Embed(CreateEmbed()
.WithOkColor()
.WithDescription(ctx.User.ToString())
.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

@ -65,7 +65,17 @@ public sealed class MusicPlayer : IMusicPlayer
_songBuffer = new PoopyBufferImmortalized(_vc.InputLength);
_thread = new(async () => { await PlayLoop(); });
_thread = new(async () =>
{
try
{
await PlayLoop();
}
catch (Exception ex)
{
Log.Error(ex, "Music player thread crashed");
}
});
_thread.Start();
}
@ -402,12 +412,24 @@ public sealed class MusicPlayer : IMusicPlayer
if (song is null)
return default;
int index;
if (asNext)
return (_queue.EnqueueNext(song, queuer, out index), index);
var wasLast = _queue.IsLast();
return (_queue.Enqueue(song, queuer, out index), index);
try
{
int index;
if (asNext)
return (_queue.EnqueueNext(song, queuer, out index), index);
return (_queue.Enqueue(song, queuer, out index), index);
}
finally
{
// if (wasLast && IsStopped)
// {
// IsStopped = false;
// }
}
}
public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer)

View file

@ -73,5 +73,38 @@ public partial class Permissions
await Response().Confirm(strs.gcmd_remove(Format.Bold(cmd.Name))).SendAsync();
}
[Cmd]
[OwnerOnly]
public async Task DmModule(ModuleOrExpr module)
{
var moduleName = module.Name.ToLowerInvariant();
var added = _service.ToggleModule(moduleName, true);
if (added)
{
await Response().Confirm(strs.dmmod_add(Format.Bold(module.Name))).SendAsync();
return;
}
await Response().Confirm(strs.dmmod_remove(Format.Bold(module.Name))).SendAsync();
}
[Cmd]
[OwnerOnly]
public async Task DmCommand(CommandOrExprInfo cmd)
{
var commandName = cmd.Name.ToLowerInvariant();
var added = _service.ToggleCommand(commandName, true);
if (added)
{
await Response().Confirm(strs.dmcmd_add(Format.Bold(cmd.Name))).SendAsync();
return;
}
await Response().Confirm(strs.dmcmd_remove(Format.Bold(cmd.Name))).SendAsync();
}
}
}

View file

@ -24,10 +24,19 @@ public class GlobalPermissionService : IExecPreCommand, IEService
var settings = _bss.Data;
var commandName = command.Name.ToLowerInvariant();
if (commandName != "resetglobalperms"
&& (settings.Blocked.Commands.Contains(commandName)
|| settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant())))
return Task.FromResult(true);
if (commandName != "resetglobalperms")
{
if (settings.Blocked.Commands.Contains(commandName)
|| settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant()))
return Task.FromResult(true);
if (ctx.Guild is null)
{
if (settings.DmBlocked.Commands.Contains(commandName)
|| settings.DmBlocked.Modules.Contains(moduleName.ToLowerInvariant()))
return Task.FromResult(true);
}
}
return Task.FromResult(false);
}
@ -37,13 +46,30 @@ public class GlobalPermissionService : IExecPreCommand, IEService
/// </summary>
/// <param name="moduleName">Lowercase module name</param>
/// <returns>Whether the module is added</returns>
public bool ToggleModule(string moduleName)
public bool ToggleModule(string moduleName, bool priv = false)
{
var added = false;
_bss.ModifyConfig(bs =>
{
if (priv)
{
if (bs.DmBlocked.Modules.Add(moduleName))
{
added = true;
}
else
{
bs.DmBlocked.Modules.Remove(moduleName);
added = false;
}
return;
}
if (bs.Blocked.Modules.Add(moduleName))
{
added = true;
}
else
{
bs.Blocked.Modules.Remove(moduleName);
@ -59,13 +85,30 @@ public class GlobalPermissionService : IExecPreCommand, IEService
/// </summary>
/// <param name="commandName">Lowercase command name</param>
/// <returns>Whether the command is added</returns>
public bool ToggleCommand(string commandName)
public bool ToggleCommand(string commandName, bool priv = false)
{
var added = false;
_bss.ModifyConfig(bs =>
{
if (priv)
{
if (bs.DmBlocked.Commands.Add(commandName))
{
added = true;
}
else
{
bs.DmBlocked.Commands.Remove(commandName);
added = false;
}
return;
}
if (bs.Blocked.Commands.Add(commandName))
{
added = true;
}
else
{
bs.Blocked.Commands.Remove(commandName);

View file

@ -103,11 +103,11 @@ public partial class Searches : EllieModule<SearchesService>
}
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.time_new))
.WithDescription(Format.Code(data.Time.ToString(Culture)))
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
.AddField(GetText(strs.timezone), data.TimeZoneName, true);
.WithOkColor()
.WithTitle(GetText(strs.time_new))
.WithDescription(Format.Code(data.Time.ToString(Culture)))
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
.AddField(GetText(strs.timezone), data.TimeZoneName, true);
await Response().Embed(eb).SendAsync();
}
@ -129,16 +129,16 @@ public partial class Searches : EllieModule<SearchesService>
await Response()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(movie.Title)
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
.WithDescription(movie.Plot.TrimTo(1000))
.AddField("Rating", movie.ImdbRating, true)
.AddField("Genre", movie.Genre, true)
.AddField("Year", movie.Year, true)
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute)
? movie.Poster
: null))
.WithOkColor()
.WithTitle(movie.Title)
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
.WithDescription(movie.Plot.TrimTo(1000))
.AddField("Rating", movie.ImdbRating, true)
.AddField("Genre", movie.Genre, true)
.AddField("Year", movie.Year, true)
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute)
? movie.Poster
: null))
.SendAsync();
}
@ -191,9 +191,9 @@ public partial class Searches : EllieModule<SearchesService>
await Response()
.Embed(CreateEmbed()
.WithOkColor()
.AddField(GetText(strs.original_url), $"<{query}>")
.AddField(GetText(strs.short_url), $"<{shortLink}>"))
.WithOkColor()
.AddField(GetText(strs.original_url), $"<{query}>")
.AddField(GetText(strs.short_url), $"<{shortLink}>"))
.SendAsync();
}
@ -214,13 +214,13 @@ public partial class Searches : EllieModule<SearchesService>
}
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(card.Name)
.WithDescription(card.Description)
.WithImageUrl(card.ImageUrl)
.AddField(GetText(strs.store_url), card.StoreUrl, true)
.AddField(GetText(strs.cost), card.ManaCost, true)
.AddField(GetText(strs.types), card.Types, true);
.WithOkColor()
.WithTitle(card.Name)
.WithDescription(card.Description)
.WithImageUrl(card.ImageUrl)
.AddField(GetText(strs.store_url), card.StoreUrl, true)
.AddField(GetText(strs.cost), card.ManaCost, true)
.AddField(GetText(strs.types), card.Types, true);
await Response().Embed(embed).SendAsync();
}
@ -281,10 +281,10 @@ public partial class Searches : EllieModule<SearchesService>
{
var item = items[0];
return CreateEmbed()
.WithOkColor()
.WithUrl(item.Permalink)
.WithTitle(item.Word)
.WithDescription(item.Definition);
.WithOkColor()
.WithUrl(item.Permalink)
.WithTitle(item.Word)
.WithDescription(item.Definition);
})
.SendAsync();
}
@ -312,11 +312,11 @@ public partial class Searches : EllieModule<SearchesService>
{
var model = items.First();
var embed = CreateEmbed()
.WithDescription(ctx.User.Mention)
.AddField(GetText(strs.word), model.Word, true)
.AddField(GetText(strs._class), model.WordType, true)
.AddField(GetText(strs.definition), model.Definition)
.WithOkColor();
.WithDescription(ctx.User.Mention)
.AddField(GetText(strs.word), model.Word, true)
.AddField(GetText(strs._class), model.WordType, true)
.AddField(GetText(strs.definition), model.Definition)
.WithOkColor();
if (!string.IsNullOrWhiteSpace(model.Example))
embed.AddField(GetText(strs.example), model.Example);
@ -404,10 +404,38 @@ public partial class Searches : EllieModule<SearchesService>
await Response()
.Embed(
CreateEmbed()
.WithOkColor()
.AddField("Username", usr.ToString())
.AddField("Avatar Url", avatarUrl)
.WithThumbnailUrl(avatarUrl.ToString()))
.WithOkColor()
.AddField("Username", usr.ToString())
.AddField("Avatar Url", avatarUrl)
.WithThumbnailUrl(avatarUrl.ToString()))
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Banner([Leftover] IGuildUser? usr = null)
{
usr ??= (IGuildUser)ctx.User;
var bannerUrl = usr.GetGuildBannerUrl(size: 2048)
?? (await ((DiscordSocketClient)ctx.Client).Rest.GetUserAsync(usr.Id))?.GetBannerUrl();
if (bannerUrl is null)
{
await Response()
.Error(strs.no_banner)
.SendAsync();
return;
}
await Response()
.Embed(
CreateEmbed()
.WithOkColor()
.AddField("Username", usr.ToString(), true)
.AddField("Banner Url", bannerUrl, true)
.WithImageUrl(bannerUrl))
.SendAsync();
}

View file

@ -122,13 +122,16 @@ public sealed partial class FlagTranslateService : IReadyExecutor, IEService
if (!_supportedFlags.TryGetValue(code, out var lang))
return;
if (!_msgLangs.Add((reaction.MessageId, lang)))
if (_msgLangs.Contains((reaction.MessageId, lang)))
return;
var result = await _cache.GetAsync(CdKey(reaction.UserId));
if (result.TryPickT0(out _, out _))
return;
if (!_msgLangs.Add((reaction.MessageId, lang)))
return;
await _cache.AddAsync(CdKey(reaction.UserId), true, TimeSpan.FromSeconds(5));
var msg = await arg1.GetOrDownloadAsync();

View file

@ -28,9 +28,19 @@ public partial class Searches
await ctx.Channel.TriggerTypingAsync();
var translation = await _service.Translate(fromLang, toLang, text);
var embed = CreateEmbed().WithOkColor().AddField(fromLang, text).AddField(toLang, translation);
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(fromLang)
.WithDescription(text);
await Response().Embed(embed).SendAsync();
var embed2 = CreateEmbed()
.WithOkColor()
.WithTitle(toLang)
.WithDescription(translation);
await Response()
.Embeds([embed, embed2])
.SendAsync();
}
catch
{
@ -65,7 +75,10 @@ public partial class Searches
[RequireContext(ContextType.Guild)]
public async Task AutoTransLang(string fromLang, string toLang)
{
var succ = await _service.RegisterUserAsync(ctx.User.Id, ctx.Channel.Id, fromLang.ToLower(), toLang.ToLower());
var succ = await _service.RegisterUserAsync(ctx.User.Id,
ctx.Channel.Id,
fromLang.ToLower(),
toLang.ToLower());
if (succ is null)
{
@ -89,8 +102,8 @@ public partial class Searches
var langs = _service.GetLanguages().ToList();
var eb = CreateEmbed()
.WithTitle(GetText(strs.supported_languages))
.WithOkColor();
.WithTitle(GetText(strs.supported_languages))
.WithOkColor();
foreach (var chunk in langs.Chunk(15))
{

View file

@ -9,6 +9,7 @@ public sealed class AfkService : IEService, IReadyExecutor
private readonly MessageSenderService _mss;
private static readonly TimeSpan _maxAfkDuration = 8.Hours();
public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss)
{
_cache = cache;
@ -19,6 +20,9 @@ public sealed class AfkService : IEService, IReadyExecutor
private static TypedKey<string> GetKey(ulong userId)
=> new($"afk:msg:{userId}");
private static TypedKey<bool> GetRecentlySentKey(ulong userId, ulong channelId)
=> new($"afk:recent:{userId}:{channelId}");
public async Task<bool> SetAfkAsync(ulong userId, string text)
{
var added = await _cache.AddAsync(GetKey(userId), text, _maxAfkDuration, overwrite: true);
@ -43,9 +47,7 @@ public sealed class AfkService : IEService, IReadyExecutor
msg.DeleteAfter(5);
});
}
}
}
catch (Exception ex)
{
@ -61,7 +63,7 @@ public sealed class AfkService : IEService, IReadyExecutor
await Task.Delay(_maxAfkDuration);
_client.MessageReceived -= StopAfk;
});
return added;
}
@ -72,36 +74,29 @@ public sealed class AfkService : IEService, IReadyExecutor
return Task.CompletedTask;
}
private Task TryTriggerAfkMessage(SocketMessage arg)
private Task TryTriggerAfkMessage(SocketMessage sm)
{
if (arg.Author.IsBot || arg.Author.IsWebhook)
if (sm.Author.IsBot || sm.Author.IsWebhook)
return Task.CompletedTask;
if (arg is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc)
if (sm is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc)
return Task.CompletedTask;
if ((arg.MentionedUsers.Count is 0 or > 3) && uMsg.ReferencedMessage is null)
if ((sm.MentionedUsers.Count is 0 or > 3) && uMsg.ReferencedMessage is null)
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var botUser = await tc.Guild.GetCurrentUserAsync();
var perms = botUser.GetPermissions(tc);
if (!perms.SendMessages)
return;
ulong mentionedUserId = 0;
if (arg.MentionedUsers.Count <= 3)
if (sm.MentionedUsers.Count <= 3)
{
foreach (var uid in uMsg.MentionedUserIds)
{
if (uid == arg.Author.Id)
if (uid == sm.Author.Id)
continue;
if (arg.Content.StartsWith($"<@{uid}>") || arg.Content.StartsWith($"<@!{uid}>"))
if (sm.Content.StartsWith($"<@{uid}>") || sm.Content.StartsWith($"<@!{uid}>"))
{
mentionedUserId = uid;
break;
@ -115,7 +110,7 @@ public sealed class AfkService : IEService, IReadyExecutor
{
return;
}
mentionedUserId = repliedUserId;
}
@ -125,16 +120,35 @@ public sealed class AfkService : IEService, IReadyExecutor
if (result.TryPickT0(out var msg, out _))
{
var st = SmartText.CreateFrom(msg);
st = $"The user you've pinged (<#{mentionedUserId}>) is AFK: " + st;
var toDelete = await _mss.Response(arg.Channel)
.User(arg.Author)
var toDelete = await _mss.Response(sm.Channel)
.User(sm.Author)
.Message(uMsg)
.Text(st)
.SendAsync();
toDelete.DeleteAfter(30);
var botUser = await tc.Guild.GetCurrentUserAsync();
var perms = botUser.GetPermissions(tc);
if (!perms.SendMessages)
return;
var key = GetRecentlySentKey(mentionedUserId, sm.Channel.Id);
var recent = await _cache.GetAsync(key);
if (!recent.TryPickT0(out _, out _))
{
var chMsg = await _mss.Response(sm.Channel)
.Message(uMsg)
.Pending(strs.user_afk($"<@{mentionedUserId}>"))
.SendAsync();
chMsg.DeleteAfter(5);
await _cache.AddAsync(key, true, expiry: TimeSpan.FromMinutes(5));
}
}
}
catch (HttpException ex)

View file

@ -38,6 +38,8 @@ public partial class Utility
eb
.WithOkColor()
.WithTitle(GetText(strs.giveaway_started))
.AddField(GetText(strs.lasts_until), TimestampTag.FromDateTime(DateTime.UtcNow.Add(duration)), true)
// .AddField(GetText(strs.winners_count), "1", true)
.WithFooter($"id: {new kwum(id).ToString()}");
await startingMsg.AddReactionAsync(new Emoji(GiveawayService.GiveawayEmoji));

View file

@ -341,6 +341,9 @@ public sealed class GiveawayService : IEService, IReadyExecutor
try
{
await msg.ModifyAsync(x => x.Embed = eb.Build());
if (winner is not null)
await _sender.Response(ch).Message(msg).Text($"🎉 <{winner.UserId}>").SendAsync();
}
catch
{

View file

@ -12,17 +12,21 @@ public partial class Utility
[RequireContext(ContextType.Guild)]
public async Task ServerColorsShow()
{
var colors = _service.GetColors(ctx.Guild.Id);
var okHex = colors?.Ok?.RawValue.ToString("x6");
var warnHex = colors?.Warn?.RawValue.ToString("x6");
var errHex = colors?.Error?.RawValue.ToString("x6");
EmbedBuilder[] ebs =
[
CreateEmbed()
.WithOkColor()
.WithDescription("\\✅"),
.WithDescription($"\\✅ {okHex}"),
CreateEmbed()
.WithPendingColor()
.WithDescription("\\⏳\\⚠️"),
.WithDescription($"\\⏳\\⚠️ {warnHex}"),
CreateEmbed()
.WithErrorColor()
.WithDescription("\\❌")
.WithDescription($"\\❌ {errHex}")
];
await Response()

View file

@ -98,10 +98,10 @@ public partial class Utility
return;
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(guildId is not null
? strs.reminder_server_list
: strs.reminder_list));
.WithOkColor()
.WithTitle(GetText(guildId is not null
? strs.reminder_server_list
: strs.reminder_list));
List<Reminder> rems;
if (guildId is { } gid)
@ -193,23 +193,14 @@ public partial class Utility
message = message.SanitizeAllMentions();
}
var rem = new Reminder
{
ChannelId = targetId,
IsPrivate = isPrivate,
When = time,
Message = message,
UserId = ctx.User.Id,
ServerId = ctx.Guild?.Id ?? 0
};
await _service.AddReminderAsync(ctx.User.Id,
targetId,
ctx.Guild?.Id,
isPrivate,
time,
message,
ReminderType.User);
await using (var uow = _db.GetDbContext())
{
uow.Set<Reminder>().Add(rem);
await uow.SaveChangesAsync();
}
// var gTime = ctx.Guild is null ? time : TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
await Response()
.Confirm($"\u23f0 {GetText(strs.remind2(
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),

View file

@ -21,6 +21,8 @@ public class RemindService : IEService, IReadyExecutor, IRemindService
private readonly IMessageSenderService _sender;
private readonly CultureInfo _culture;
private TaskCompletionSource<bool> _tcs;
public RemindService(
DiscordSocketClient client,
DbService db,
@ -44,8 +46,7 @@ public class RemindService : IEService, IReadyExecutor, IRemindService
public async Task OnReadyAsync()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15));
while (await timer.WaitForNextTickAsync())
while (true)
{
await OnReminderLoopTickInternalAsync();
}
@ -55,8 +56,7 @@ public class RemindService : IEService, IReadyExecutor, IRemindService
{
try
{
var now = DateTime.UtcNow;
var reminders = await GetRemindersBeforeAsync(now);
var reminders = await GetRemindersBeforeAsync();
if (reminders.Count == 0)
return;
@ -67,7 +67,6 @@ public class RemindService : IEService, IReadyExecutor, IRemindService
{
var executedReminders = group.ToList();
await executedReminders.Select(ReminderTimerAction).WhenAll();
await RemoveReminders(executedReminders.Select(x => x.Id));
await Task.Delay(1500);
}
}
@ -80,21 +79,51 @@ public class RemindService : IEService, IReadyExecutor, IRemindService
private async Task RemoveReminders(IEnumerable<int> reminders)
{
await using var uow = _db.GetDbContext();
await uow.Set<Reminder>()
.ToLinqToDBTable()
await uow.GetTable<Reminder>()
.DeleteAsync(x => reminders.Contains(x.Id));
await uow.SaveChangesAsync();
}
private async Task<List<Reminder>> GetRemindersBeforeAsync(DateTime now)
private async Task<IReadOnlyList<Reminder>> GetRemindersBeforeAsync()
{
await using var uow = _db.GetDbContext();
return await uow.Set<Reminder>()
.ToLinqToDBTable()
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId, _creds.TotalShards, _client.ShardId)
&& x.When < now)
.ToListAsyncLinqToDB();
while (true)
{
_tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
await using var uow = _db.GetDbContext();
var earliest = await uow.Set<Reminder>()
.ToLinqToDBTable()
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId,
_creds.TotalShards,
_client.ShardId))
.OrderBy(x => x.When)
.FirstOrDefaultAsyncLinqToDB();
if (earliest == default)
{
await _tcs.Task;
continue;
}
var now = DateTime.UtcNow;
if (earliest.When > now)
{
var diff = earliest.When - now;
// Log.Information("Waiting for {Diff}", diff);
await Task.WhenAny(Task.Delay(diff), _tcs.Task);
continue;
}
var reminders = await uow.Set<Reminder>()
.ToLinqToDBTable()
.Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId,
_creds.TotalShards,
_client.ShardId))
.Where(x => x.When <= now)
.DeleteWithOutputAsync();
return reminders;
}
}
public bool TryParseRemindMessage(string input, out RemindObject obj)
@ -243,21 +272,24 @@ public class RemindService : IEService, IReadyExecutor, IRemindService
string message,
ReminderType reminderType)
{
var rem = new Reminder
await using (var ctx = _db.GetDbContext())
{
UserId = userId,
ChannelId = targetId,
ServerId = guildId ?? 0,
IsPrivate = isPrivate,
When = time,
Message = message,
Type = reminderType
};
await ctx.GetTable<Reminder>()
.InsertAsync(() => new Reminder
{
UserId = userId,
ChannelId = targetId,
ServerId = guildId ?? 0,
IsPrivate = isPrivate,
When = time,
Message = message,
Type = reminderType,
DateAdded = DateTime.UtcNow
});
await ctx.SaveChangesAsync();
}
await using var ctx = _db.GetDbContext();
await ctx.Set<Reminder>()
.AddAsync(rem);
await ctx.SaveChangesAsync();
_tcs.SetResult(true);
}
public async Task<List<Reminder>> GetServerReminders(int page, ulong guildId)

View file

@ -110,14 +110,14 @@ public partial class Utility
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public Task Repeat(StoopidTime interval, [Leftover] string message)
public Task Repeat(ParsedTimespan interval, [Leftover] string message)
=> Repeat(ctx.Channel, null, interval, message);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(0)]
public Task Repeat(ITextChannel channel, StoopidTime interval, [Leftover] string message)
public Task Repeat(ITextChannel channel, ParsedTimespan interval, [Leftover] string message)
=> Repeat(channel, null, interval, message);
[Cmd]
@ -138,14 +138,14 @@ public partial class Utility
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(2)]
public Task Repeat(GuildDateTime? timeOfDay, StoopidTime? interval, [Leftover] string message)
public Task Repeat(GuildDateTime? timeOfDay, ParsedTimespan? interval, [Leftover] string message)
=> Repeat(ctx.Channel, timeOfDay, interval, message);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[Priority(3)]
public async Task Repeat(IMessageChannel channel, GuildDateTime? timeOfDay, StoopidTime? interval,
public async Task Repeat(IMessageChannel channel, GuildDateTime? timeOfDay, ParsedTimespan? interval,
[Leftover] string message)
{
if (channel is not ITextChannel txtCh || txtCh.GuildId != ctx.Guild.Id)

View file

@ -186,7 +186,7 @@ public partial class Utility : EllieModule
return CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.inrole_list(roleName, roleUsers.Count)))
.WithTitle(GetText(strs.inrole_list(role?.GetIconUrl() + roleName, roleUsers.Count)))
.WithDescription(string.Join("\n", pageUsers));
})
.SendAsync();

View file

@ -0,0 +1,11 @@
namespace EllieBot.Modules.Xp.Services;
public enum BuyResult
{
Success,
XpShopDisabled,
AlreadyOwned,
InsufficientFunds,
UnknownItem,
InsufficientPatronTier,
}

View file

@ -51,33 +51,6 @@ public partial class Xp : EllieModule<XpService>
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task XpNotify()
{
var globalSetting = _service.GetNotificationType(ctx.User);
var serverSetting = _service.GetNotificationType(ctx.User.Id, ctx.Guild.Id);
var embed = CreateEmbed()
.WithOkColor()
.AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting))
.AddField(GetText(strs.xpn_setting_server), GetNotifLocationString(serverSetting));
await Response().Embed(embed).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task XpNotify(NotifyPlace place, XpNotificationLocation type)
{
if (place == NotifyPlace.Guild)
await _service.ChangeNotificationType(ctx.User.Id, ctx.Guild.Id, type);
else
await _service.ChangeNotificationType(ctx.User, type);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
@ -154,9 +127,9 @@ public partial class Xp : EllieModule<XpService>
.Page((items, _) =>
{
var embed = CreateEmbed()
.WithTitle(GetText(strs.exclusion_list))
.WithDescription(string.Join('\n', items))
.WithOkColor();
.WithTitle(GetText(strs.exclusion_list))
.WithDescription(string.Join('\n', items))
.WithOkColor();
return embed;
})
@ -214,16 +187,12 @@ public partial class Xp : EllieModule<XpService>
for (var i = 0; i < users.Count; i++)
{
var levelStats = new LevelStats(users[i].Xp + users[i].AwardedXp);
var levelStats = new LevelStats(users[i].Xp);
var user = ((SocketGuild)ctx.Guild).GetUser(users[i].UserId);
var userXpData = users[i];
var awardStr = string.Empty;
if (userXpData.AwardedXp > 0)
awardStr = $"(+{userXpData.AwardedXp})";
else if (userXpData.AwardedXp < 0)
awardStr = $"({userXpData.AwardedXp})";
embed.AddField($"#{i + 1 + (curPage * 10)} {user?.ToString() ?? users[i].UserId.ToString()}",
$"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}");
@ -266,8 +235,8 @@ public partial class Xp : EllieModule<XpService>
.Page((users, curPage) =>
{
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.global_leaderboard));
.WithOkColor()
.WithTitle(GetText(strs.global_leaderboard));
if (!users.Any())
{
@ -287,6 +256,28 @@ public partial class Xp : EllieModule<XpService>
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(1)]
public Task XpLevelSet(int level, IGuildUser user)
=> XpLevelSet(level, user.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public async Task XpLevelSet(int level, ulong userId)
{
if (level < 0)
return;
await _service.SetLevelAsync(ctx.Guild.Id, userId, level);
await Response()
.Confirm(strs.level_set($"<@{userId}>", Format.Bold(level.ToString())))
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
@ -351,8 +342,8 @@ public partial class Xp : EllieModule<XpService>
public async Task XpReset(ulong userId)
{
var embed = CreateEmbed()
.WithTitle(GetText(strs.reset))
.WithDescription(GetText(strs.reset_user_confirm));
.WithTitle(GetText(strs.reset))
.WithDescription(GetText(strs.reset_user_confirm));
if (!await PromptUserConfirmAsync(embed))
return;
@ -368,8 +359,8 @@ public partial class Xp : EllieModule<XpService>
public async Task XpReset()
{
var embed = CreateEmbed()
.WithTitle(GetText(strs.reset))
.WithDescription(GetText(strs.reset_server_confirm));
.WithTitle(GetText(strs.reset))
.WithDescription(GetText(strs.reset_server_confirm));
if (!await PromptUserConfirmAsync(embed))
return;
@ -446,20 +437,20 @@ public partial class Xp : EllieModule<XpService>
{
if (!items.Any())
return CreateEmbed()
.WithDescription(GetText(strs.not_found))
.WithErrorColor();
.WithDescription(GetText(strs.not_found))
.WithErrorColor();
var (key, item) = items.FirstOrDefault();
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(item.Name)
.AddField(GetText(strs.price),
CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()),
true)
.WithImageUrl(string.IsNullOrWhiteSpace(item.Preview)
? item.Url
: item.Preview);
.WithOkColor()
.WithTitle(item.Name)
.AddField(GetText(strs.price),
CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()),
true)
.WithImageUrl(string.IsNullOrWhiteSpace(item.Preview)
? item.Url
: item.Preview);
if (!string.IsNullOrWhiteSpace(item.Desc))
eb.AddField(GetText(strs.desc), item.Desc);
@ -604,15 +595,4 @@ public partial class Xp : EllieModule<XpService>
await _service.UseShopItemAsync(ctx.User.Id, type, key);
}
}
private string GetNotifLocationString(XpNotificationLocation loc)
{
if (loc == XpNotificationLocation.Channel)
return GetText(strs.xpn_notif_channel);
if (loc == XpNotificationLocation.Dm)
return GetText(strs.xpn_notif_dm);
return GetText(strs.xpn_notif_disabled);
}
}

View file

@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Processing;
using System.Threading.Channels;
using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using EllieBot.Modules.Administration;
using EllieBot.Modules.Patronage;
using Color = SixLabors.ImageSharp.Color;
using Exception = System.Exception;
@ -20,31 +21,6 @@ using Image = SixLabors.ImageSharp.Image;
namespace EllieBot.Modules.Xp.Services;
public interface IUserService
{
Task<DiscordUser?> GetUserAsync(ulong userId);
}
public sealed class UserService : IUserService, IEService
{
private readonly DbService _db;
public UserService(DbService db)
{
_db = db;
}
public async Task<DiscordUser> GetUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var user = await uow
.GetTable<DiscordUser>()
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
return user;
}
}
public class XpService : IEService, IReadyExecutor, IExecNoCommand
{
private readonly DbService _db;
@ -72,6 +48,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50);
private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
private readonly IMessageSenderService _sender;
private readonly INotifySubscriber _notifySub;
public XpService(
DiscordSocketClient client,
@ -87,7 +64,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
XpConfigService xpConfig,
IPubSub pubSub,
IPatronageService ps,
IMessageSenderService sender)
IMessageSenderService sender,
INotifySubscriber notifySub)
{
_db = db;
_images = images;
@ -99,6 +77,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
_xpConfig = xpConfig;
_pubSub = pubSub;
_sender = sender;
_notifySub = notifySub;
_excludedServers = new();
_excludedChannels = new();
_client = client;
@ -159,14 +138,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
}
}
public sealed class MiniGuildXpStats
{
public long Xp { get; set; }
public XpNotificationLocation NotifyOnLevelUp { get; set; }
public ulong GuildId { get; set; }
public ulong UserId { get; set; }
}
private async Task UpdateXp()
{
try
@ -197,9 +168,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
var dus = new List<DiscordUser>(globalToAdd.Count);
var gxps = new List<UserXpStats>(globalToAdd.Count);
var conf = _xpConfig.Data;
await using (var ctx = _db.GetDbContext())
{
var conf = _xpConfig.Data;
if (conf.CurrencyPerXp > 0)
{
foreach (var user in globalToAdd)
@ -261,8 +232,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
GuildId = guildId,
Xp = group.Key,
DateAdded = DateTime.UtcNow,
AwardedXp = 0,
NotifyOnLevelUp = XpNotificationLocation.None
},
_ => new()
{
@ -300,8 +269,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
du.UserId,
false,
oldLevel.Level,
newLevel.Level,
du.NotifyOnLevelUp));
newLevel.Level));
}
}
@ -310,8 +278,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (guildToAdd.TryGetValue(du.GuildId, out var users)
&& users.TryGetValue(du.UserId, out var xpGainData))
{
var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount + du.AwardedXp);
var newLevel = new LevelStats(du.Xp + du.AwardedXp);
var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount);
var newLevel = new LevelStats(du.Xp);
if (oldLevel.Level < newLevel.Level)
{
@ -321,8 +289,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
du.UserId,
true,
oldLevel.Level,
newLevel.Level,
du.NotifyOnLevelUp));
newLevel.Level));
}
}
}
@ -339,8 +306,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
ulong userId,
bool isServer,
long oldLevel,
long newLevel,
XpNotificationLocation notifyLoc)
long newLevel)
=> async () =>
{
if (isServer)
@ -348,7 +314,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel);
}
await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel, notifyLoc);
await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel);
};
private async Task HandleRewardsInternalAsync(
@ -378,9 +344,45 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (role is not null && user is not null)
{
if (rrew.Remove)
_ = user.RemoveRoleAsync(role);
{
try
{
await user.RemoveRoleAsync(role);
await _notifySub.NotifyAsync(new RemoveRoleRewardNotifyModel(guild.Id,
role.Id,
user.Id,
newLevel),
isShardLocal: true);
}
catch (Exception ex)
{
Log.Warning(ex,
"Unable to remove role {RoleId} from user {UserId}: {Message}",
role.Id,
user.Id,
ex.Message);
}
}
else
_ = user.AddRoleAsync(role);
{
try
{
await user.AddRoleAsync(role);
await _notifySub.NotifyAsync(new AddRoleRewardNotifyModel(guild.Id,
role.Id,
user.Id,
newLevel),
isShardLocal: true);
}
catch (Exception ex)
{
Log.Warning(ex,
"Unable to add role {RoleId} to user {UserId}: {Message}",
role.Id,
user.Id,
ex.Message);
}
}
}
}
@ -399,59 +401,25 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
ulong channelId,
ulong userId,
bool isServer,
long newLevel,
XpNotificationLocation notifyLoc)
long newLevel)
{
if (notifyLoc == XpNotificationLocation.None)
return;
var guild = _client.GetGuild(guildId);
var user = guild?.GetUser(userId);
var ch = guild?.GetTextChannel(channelId);
if (guild is null || user is null)
return;
if (isServer)
{
if (notifyLoc == XpNotificationLocation.Dm)
var model = new LevelUpNotifyModel()
{
await _sender.Response(user)
.Confirm(_strings.GetText(strs.level_up_dm(user.Mention,
Format.Bold(newLevel.ToString()),
Format.Bold(guild.ToString() ?? "-")),
guild.Id))
.SendAsync();
}
else // channel
{
if (ch is not null)
{
await _sender.Response(ch)
.Confirm(_strings.GetText(strs.level_up_channel(user.Mention,
Format.Bold(newLevel.ToString())),
guild.Id))
.SendAsync();
}
}
}
else // global level
{
var chan = notifyLoc switch
{
XpNotificationLocation.Dm => (IMessageChannel)await user.CreateDMChannelAsync(),
XpNotificationLocation.Channel => ch,
_ => null
GuildId = guildId,
UserId = userId,
ChannelId = channelId,
Level = newLevel
};
if (chan is null)
return;
await _sender.Response(chan)
.Confirm(_strings.GetText(strs.level_up_global(user.Mention,
Format.Bold(newLevel.ToString())),
guild.Id))
.SendAsync();
await _notifySub.NotifyAsync(model, true);
return;
}
}
@ -595,7 +563,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
return await uow
.UserXpStats
.Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp)
.OrderByDescending(x => x.Xp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
@ -606,7 +574,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
await using var uow = _db.GetDbContext();
return await uow.Set<UserXpStats>()
.Where(x => x.GuildId == guildId && x.UserId.In(users))
.OrderByDescending(x => x.Xp + x.AwardedXp)
.OrderByDescending(x => x.Xp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
@ -635,35 +603,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
.ToArrayAsyncLinqToDB();
}
public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type)
{
await using var uow = _db.GetDbContext();
var user = uow.GetOrCreateUserXpStats(guildId, userId);
user.NotifyOnLevelUp = type;
await uow.SaveChangesAsync();
}
public XpNotificationLocation GetNotificationType(ulong userId, ulong guildId)
{
using var uow = _db.GetDbContext();
var user = uow.GetOrCreateUserXpStats(guildId, userId);
return user.NotifyOnLevelUp;
}
public XpNotificationLocation GetNotificationType(IUser user)
{
using var uow = _db.GetDbContext();
return uow.GetOrCreateUser(user).NotifyOnLevelUp;
}
public async Task ChangeNotificationType(IUser user, XpNotificationLocation type)
{
await using var uow = _db.GetDbContext();
var du = uow.GetOrCreateUser(user);
du.NotifyOnLevelUp = type;
await uow.SaveChangesAsync();
}
private Task Client_OnGuildAvailable(SocketGuild guild)
{
Task.Run(async () =>
@ -903,7 +842,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
using var uow = _db.GetDbContext();
var usr = uow.GetOrCreateUserXpStats(guildId, userId);
usr.AwardedXp += amount;
usr.Xp += amount;
uow.SaveChanges();
}
@ -949,7 +888,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
return new(du,
stats,
new(totalXp),
new(stats.Xp + stats.AwardedXp),
new(stats.Xp),
globalRank,
guildRank);
}
@ -1192,19 +1131,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
outlinePen));
}
if (stats.FullGuildStats.AwardedXp != 0 && template.User.Xp.Awarded.Show)
{
var sign = stats.FullGuildStats.AwardedXp > 0 ? "+ " : "";
var awX = template.User.Xp.Awarded.Pos.X
- (Math.Max(0, stats.FullGuildStats.AwardedXp.ToString().Length - 2) * 5);
var awY = template.User.Xp.Awarded.Pos.Y;
img.Mutate(x => x.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})",
_fonts.NotoSans.CreateFont(template.User.Xp.Awarded.FontSize, FontStyle.Bold),
Brushes.Solid(template.User.Xp.Awarded.Color),
outlinePen,
new(awX, awY)));
}
var rankPen = new SolidPen(Color.White, 1);
//ranking
if (template.User.GlobalRank.Show)
@ -1671,14 +1597,27 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
&& (guildUsers == null || guildUsers.Contains(x.UserId)))
.CountAsyncLinqToDB();
}
}
public enum BuyResult
{
Success,
XpShopDisabled,
AlreadyOwned,
InsufficientFunds,
UnknownItem,
InsufficientPatronTier,
public async Task SetLevelAsync(ulong guildId, ulong userId, int level)
{
var lvlStats = LevelStats.CreateForLevel(level);
await using var ctx = _db.GetDbContext();
await ctx.GetTable<UserXpStats>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
UserId = userId,
Xp = lvlStats.TotalXp,
DateAdded = DateTime.UtcNow
},
(old) => new()
{
Xp = lvlStats.TotalXp
},
() => new()
{
GuildId = guildId,
UserId = userId
});
}
}

View file

@ -3,8 +3,8 @@ namespace EllieBot.Common;
public enum AddRemove
{
Add = int.MinValue,
Remove = int.MinValue + 1,
Rem = int.MinValue + 1,
Rm = int.MinValue + 1
Add = 0,
Remove = 1,
Rem = 1,
Rm = 1,
}

View file

@ -12,7 +12,7 @@ namespace EllieBot.Common.Configs;
public sealed partial class BotConfig : ICloneable<BotConfig>
{
[Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 8;
public int Version { get; set; } = 9;
[Comment("""
Most commands, when executed, have a small colored line
@ -81,6 +81,9 @@ public sealed partial class BotConfig : ICloneable<BotConfig>
[Comment("""List of modules and commands completely blocked on the bot""")]
public BlockedConfig Blocked { get; set; }
[Comment("""List of modules and commands blocked from usage in DMs on the bot""")]
public BlockedConfig DmBlocked { get; set; } = new();
[Comment("""Which string will be used to recognize the commands""")]
public string Prefix { get; set; }

View file

@ -157,6 +157,9 @@ public sealed class DoAsUserMessage : IUserMessage
public MessageCallData? CallData
=> _msg.CallData;
public IReadOnlyCollection<MessageSnapshot> ForwardedMessages
=> _msg.ForwardedMessages;
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null)
{
return _msg.ModifyAsync(func, options);

View file

@ -51,7 +51,7 @@ public class EllieEmbedBuilder : EmbedBuilder
var bcColors = bcsData.Data.Color;
_okColor = guildColors?.Ok ?? bcColors.Ok.ToDiscordColor();
_errorColor = guildColors?.Error ?? bcColors.Error.ToDiscordColor();
_pendingColor = guildColors?.Pending ?? bcColors.Pending.ToDiscordColor();
_pendingColor = guildColors?.Warn ?? bcColors.Pending.ToDiscordColor();
}
public EmbedBuilder WithOkColor()

View file

@ -0,0 +1,8 @@
using EllieBot.Db.Models;
namespace EllieBot.Modules.Xp.Services;
public interface IUserService
{
Task<DiscordUser?> GetUserAsync(ulong userId);
}

View file

@ -108,7 +108,7 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IE
{
_colors[guildId] = _colors[guildId] with
{
Pending = color?.ToDiscordColor()
Warn = color?.ToDiscordColor()
};
}
}
@ -126,8 +126,8 @@ public sealed class GuildColorsService : IReadyExecutor, IGuildColorsService, IE
{
var colors = new Colors(
ConvertColor(color.OkColor),
ConvertColor(color.ErrorColor),
ConvertColor(color.PendingColor));
ConvertColor(color.PendingColor),
ConvertColor(color.ErrorColor));
_colors.TryAdd(color.GuildId, colors);
}

View file

@ -10,4 +10,4 @@ public interface IGuildColorsService
Task SetPendingColor(ulong guildId, Rgba32? color);
}
public record struct Colors(Color? Ok, Color? Pending, Color? Error);
public record struct Colors(Color? Ok, Color? Warn, Color? Error);

View file

@ -0,0 +1,24 @@
using LinqToDB.EntityFrameworkCore;
using EllieBot.Db.Models;
namespace EllieBot.Modules.Xp.Services;
public sealed class UserService : IUserService, IEService
{
private readonly DbService _db;
public UserService(DbService db)
{
_db = db;
}
public async Task<DiscordUser?> GetUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var user = await uow
.GetTable<DiscordUser>()
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
return user;
}
}

View file

@ -70,10 +70,10 @@ public sealed class BotConfigService : ConfigServiceBase<BotConfig>
c.IgnoreOtherBots = true;
});
if(data.Version < 8)
if(data.Version < 9)
ModifyConfig(c =>
{
c.Version = 8;
c.Version = 9;
});
}
}

View file

@ -2,7 +2,7 @@
namespace EllieBot.Common.TypeReaders.Models;
public class StoopidTime
public class ParsedTimespan
{
private static readonly Regex _regex = new(
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
@ -11,9 +11,9 @@ public class StoopidTime
public string Input { get; set; } = string.Empty;
public TimeSpan Time { get; set; } = default;
private StoopidTime() { }
private ParsedTimespan() { }
public static StoopidTime FromInput(string input)
public static ParsedTimespan FromInput(string input)
{
var m = _regex.Match(input);
@ -52,10 +52,10 @@ public class StoopidTime
};
}
public static implicit operator TimeSpan?(StoopidTime? st)
public static implicit operator TimeSpan?(ParsedTimespan? st)
=> st?.Time;
public static implicit operator StoopidTime(TimeSpan ts)
public static implicit operator ParsedTimespan(TimeSpan ts)
=> new()
{
Input = ts.ToString(),

View file

@ -3,20 +3,20 @@ using EllieBot.Common.TypeReaders.Models;
namespace EllieBot.Common.TypeReaders;
public sealed class StoopidTimeTypeReader : EllieTypeReader<StoopidTime>
public sealed class StoopidTimeTypeReader : EllieTypeReader<ParsedTimespan>
{
public override ValueTask<TypeReaderResult<StoopidTime>> ReadAsync(ICommandContext context, string input)
public override ValueTask<TypeReaderResult<ParsedTimespan>> ReadAsync(ICommandContext context, string input)
{
if (string.IsNullOrWhiteSpace(input))
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Unsuccessful, "Input is empty."));
return new(TypeReaderResult.FromError<ParsedTimespan>(CommandError.Unsuccessful, "Input is empty."));
try
{
var time = StoopidTime.FromInput(input);
var time = ParsedTimespan.FromInput(input);
return new(TypeReaderResult.FromSuccess(time));
}
catch (Exception ex)
{
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Exception, ex.Message));
return new(TypeReaderResult.FromError<ParsedTimespan>(CommandError.Exception, ex.Message));
}
}
}

View file

@ -715,6 +715,8 @@ color:
avatar:
- avatar
- av
banner:
- banner
translate:
- translate
- trans
@ -1039,6 +1041,12 @@ gamevoicechannel:
- gvc
shoplistadd:
- shoplistadd
dmcommand:
- dmcommand
- dmcmd
dmmodule:
- dmmodule
- dmmod
globalcommand:
- globalcommand
- gcmd
@ -1089,9 +1097,6 @@ xpexclusionlist:
xpexclude:
- xpexclude
- xpex
xpnotify:
- xpnotify
- xpn
xpleveluprewards:
- xplvluprewards
- xprews
@ -1121,6 +1126,8 @@ xpshopbuy:
- xpshopbuy
xpshopuse:
- xpshopuse
xplevelset:
- xplevelset
clubcreate:
- clubcreate
clubtransfer:
@ -1533,4 +1540,23 @@ servercolorpending:
- pending
- warn
- warning
- pend
- pend
minesweeper:
- minesweeper
- mw
temprole:
- temprole
notify:
- notify
- nfy
notifylist:
- notifylist
- notifyl
notifyclear:
- notifyclear
- notifyremove
- notifyrm
- notifclr
winlb:
- winlb
- wins

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE
version: 8
version: 9
# Most commands, when executed, have a small colored line
# next to the response. The color depends whether the command
# is completed, errored or in progress (pending)
@ -78,6 +78,10 @@ helpText: |-
blocked:
commands: []
modules: []
# List of modules and commands blocked from usage in DMs on the bot
dmBlocked:
commands: []
modules: []
# Which string will be used to recognize the commands
prefix: .
# Whether the bot will rotate through all specified statuses.

View file

@ -961,6 +961,56 @@
"MuteMembers Server Permission"
]
},
{
"Aliases": [
".notify",
".nfy"
],
"Description": "Sends a message to the current channel once the specified event occurs.\nProvide no parameters to see all available events.",
"Usage": [
".notify levelup Congratulations to user %user.name% for reaching level %event.level%"
],
"Submodule": "NotifyCommands",
"Module": "Administration",
"Options": null,
"Requirements": [
"Bot Owner Only"
]
},
{
"Aliases": [
".notifylist",
".notifyl"
],
"Description": "Lists all active notifications in this server.",
"Usage": [
".notifylist"
],
"Submodule": "NotifyCommands",
"Module": "Administration",
"Options": null,
"Requirements": [
"Bot Owner Only"
]
},
{
"Aliases": [
".notifyclear",
".notifyremove",
".notifyrm",
".notifclr"
],
"Description": "Removes the specified notify event.",
"Usage": [
".notifyclear levelup"
],
"Submodule": "NotifyCommands",
"Module": "Administration",
"Options": null,
"Requirements": [
"Bot Owner Only"
]
},
{
"Aliases": [
".dpo"
@ -1523,6 +1573,22 @@
"Administrator Server Permission"
]
},
{
"Aliases": [
".temprole"
],
"Description": "Grants a user a temporary role for the specified number of time.\nThe role must exist and be lower in the role hierarchy than your highest role.",
"Usage": [
".temprole 15m @User Jail",
".temprole 7d @Newbie Trial Member"
],
"Submodule": "RoleCommands",
"Module": "Administration",
"Options": null,
"Requirements": [
"Administrator Server Permission"
]
},
{
"Aliases": [
".iam"
@ -1648,9 +1714,9 @@
".sar excl",
".sar tesar"
],
"Description": "Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group.",
"Description": "Toggles whether self-assigned roles are exclusive.\nWhile enabled, users can only have one self-assignable role per group.",
"Usage": [
".sar exclusive"
".sar exclusive 1"
],
"Submodule": "sar",
"Module": "Administration",
@ -3274,6 +3340,21 @@
"Options": null,
"Requirements": []
},
{
"Aliases": [
".winlb",
".wins"
],
"Description": "Shows the biggest wins leaderboard",
"Usage": [
".winlb",
".winlb 5"
],
"Submodule": "BetStatsCommands",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".gamblestats",
@ -3914,6 +3995,20 @@
"Options": null,
"Requirements": []
},
{
"Aliases": [
".minesweeper",
".mw"
],
"Description": "Creates a spoiler-based minesweeper mini game.\nYou may specify the number of mines.",
"Usage": [
".minesweeper 15"
],
"Submodule": "Games",
"Module": "Games",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".acrophobia",
@ -5521,6 +5616,38 @@
"Bot Owner Only"
]
},
{
"Aliases": [
".dmmodule",
".dmmod"
],
"Description": "Toggles whether a module can be used in DMs.",
"Usage": [
".dmmodule Gambling"
],
"Submodule": "GlobalPermissionCommands",
"Module": "Permissions",
"Options": null,
"Requirements": [
"Bot Owner Only"
]
},
{
"Aliases": [
".dmcommand",
".dmcmd"
],
"Description": "Toggles whether a command can be used in DMs.",
"Usage": [
".dmcommand .stats"
],
"Submodule": "GlobalPermissionCommands",
"Module": "Permissions",
"Options": null,
"Requirements": [
"Bot Owner Only"
]
},
{
"Aliases": [
".resetperms"
@ -5790,6 +5917,19 @@
"Options": null,
"Requirements": []
},
{
"Aliases": [
".banner"
],
"Description": "Shows a mentioned person's banner.",
"Usage": [
".banner @Someone"
],
"Submodule": "Searches",
"Module": "Searches",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".wikia",
@ -7777,21 +7917,6 @@
"Options": null,
"Requirements": []
},
{
"Aliases": [
".xpnotify",
".xpn"
],
"Description": "Sets how the bot should notify you when you get a `server` or `global` level. This is a personal setting and affects only how you receive Global or Server level-up notifications. You can set `dm` (for the bot to send you a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable.",
"Usage": [
".xpnotify global dm",
".xpnotify server channel"
],
"Submodule": "Xp",
"Module": "Xp",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".xpexclude",
@ -7853,6 +7978,21 @@
"Options": null,
"Requirements": []
},
{
"Aliases": [
".xplevelset"
],
"Description": "Sets the level of the user you specify.",
"Usage": [
".xplevelset 10 @User"
],
"Submodule": "Xp",
"Module": "Xp",
"Options": null,
"Requirements": [
"Administrator Server Permission"
]
},
{
"Aliases": [
".xpadd"

View file

@ -368,7 +368,7 @@ sargroupname:
- group:
desc: "The group number"
name:
desc: "The name to assign."
desc: "The optional name to assign, clears the name if no name is provided."
sargroupdelete:
desc: "Deletes a self-assignable role group"
ex:
@ -377,11 +377,14 @@ sargroupdelete:
- group:
desc: "The number of the group to delete."
sarexclusive:
desc: Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group.
desc: |-
Toggles whether self-assigned roles are exclusive.
While enabled, users can only have one self-assignable role per group.
ex:
- ''
- '1'
params:
- { }
- group:
desc: "The number of the group to set exclusive."
sarrolelevelreq:
desc: Set a level requirement on a self-assignable role.
ex:
@ -2170,6 +2173,13 @@ avatar:
params:
- usr:
desc: "The user whose avatar is being displayed."
banner:
desc: Shows a mentioned person's banner.
ex:
- '@Someone'
params:
- usr:
desc: "The user whose banner is being displayed."
translate:
desc: Translates text from the given language to the destination language.
ex:
@ -3377,14 +3387,28 @@ globalcommand:
- '{0}stats'
params:
- cmd:
desc: "The type of command or expression being toggled."
desc: "The command or expression being toggled."
globalmodule:
desc: Toggles whether a module can be used on any server.
ex:
- 'Gambling'
params:
- module:
desc: "The type of module or configuration information being toggled."
desc: "The module being toggled."
dmcommand:
desc: Toggles whether a command can be used in DMs.
ex:
- '{0}stats'
params:
- cmd:
desc: "The command or expression being toggled."
dmmodule:
desc: Toggles whether a module can be used in DMs.
ex:
- 'Gambling'
params:
- module:
desc: "The module being toggled."
globalpermlist:
desc: Lists global permissions set by the bot owner.
ex:
@ -3515,17 +3539,6 @@ xpexclude:
desc: "The ID of the channel to exclude from XP tracking."
channel:
desc: "The ID of the channel to exclude from XP tracking."
xpnotify:
desc: Sets how the bot should notify you when you get a `server` or `global` level. This is a personal setting and affects only how you receive Global or Server level-up notifications. You can set `dm` (for the bot to send you a direct message), `channel` (to get notified in the channel you sent the last message in) or `none` to disable.
ex:
- global dm
- server channel
params:
- { }
- place:
desc: "The location where notifications should be sent, such as a specific channel or DM."
type:
desc: "The location where notifications for server and global level-ups should be sent."
xpleveluprewards:
desc: Shows currently set level up rewards.
ex:
@ -4820,4 +4833,75 @@ servercolorpending:
- '#ffff00'
params:
- color:
desc: "The hex of the color to set"
desc: "The hex of the color to set"
xplevelset:
desc: |-
Sets the level of the user you specify.
ex:
- '10 @User'
params:
- level:
desc: "The level to set the user to."
- user:
desc: "The user to set the level of."
temprole:
desc: |-
Grants a user a temporary role for the specified number of time.
The role must exist and be lower in the role hierarchy than your highest role.
ex:
- '15m @User Jail'
- '7d @Newbie Trial Member'
params:
- days:
desc: "The time after which the role is automatically removed."
- user:
desc: "The user to give the role to."
- role:
desc: "The role to give to the user."
minesweeper:
desc: |-
Creates a spoiler-based minesweeper mini game.
You may specify the number of mines.
ex:
- '15'
params:
- mines:
desc: "The number of mines to create."
notify:
desc: |-
Sends a message to the current channel once the specified event occurs.
Provide no parameters to see all available events.
ex:
- 'levelup Congratulations to user %user.name% for reaching level %event.level%'
params:
- { }
- event:
desc: "The event to notify on."
- event:
desc: "The event to notify on."
message:
desc: "The message to send."
notifylist:
desc: |-
Lists all active notifications in this server.
ex:
- ''
params:
- { }
notifyclear:
desc: |-
Removes the specified notify event.
ex:
- 'levelup'
params:
- event:
desc: "The notify event to clear."
winlb:
desc: |-
Shows the biggest wins leaderboard
ex:
- ''
- '5'
params:
- page:
desc: "The optional page to display."

View file

@ -758,6 +758,10 @@
"gcmd_remove": "Command {0} has been enabled on all servers.",
"gmod_add": "Module {0} has been disabled on all servers.",
"gmod_remove": "Module {0} has been enabled on all servers.",
"dmmod_add": "Module {0} has been disabled in bot DMs.",
"dmmod_remove": "Module {0} has been enabled in bot DMs.",
"dmcmd_add": "Command {0} has been disabled in bot DMs.",
"dmcmd_remove": "Command {0} has been enabled in bot DMs.",
"lgp_none": "No blocked commands or modules.",
"cant_read_or_send": "You can't read from or send messages to that channel.",
"quotes_notfound": "No quotes found matching the quote ID specified.",
@ -1124,7 +1128,7 @@
"choose_one": "Choose one",
"requires_role": "Requires role: {0}",
"invalid_message_id": "Invalid Message Id.",
"invalid_message_link": "The message link must be from this server.",
"invalid_message_link": "The message link must be this Bot's message. The bot can't add buttons to other users' messages.",
"btnrole_message_max": "Limit reached. You may have up to 25 button roles per message.",
"btnrole_not_found": "No button role found on that message.",
"btnrole_none": "There are no button roles on this page.",
@ -1135,5 +1139,25 @@
"no_last_queued_found": "No last queued track found.",
"wrongsong_success": "Oops! Wrong song removed: {0}",
"server_not_found": "Server not found.",
"server_color_set": "Successfully set a new server color."
"server_color_set": "Successfully set a new server color.",
"lasts_until": "Lasts Until",
"winners_count": "Winners",
"level_set": "Level of user {0} set to {1} on this server.",
"temp_role_added": "User {0} has been given {1} role temporarily. The role expires {2}",
"user_afk": "User {0} is AFK.",
"notify_on": "Notification message will be sent in {0} channel when {1} event triggers.",
"notify_off": "Notification message will no longer be sent when {0} event triggers.",
"notify_none": "No notifications on this page.",
"notify_msg_not_set": "Notification message is not set for this event.",
"notify_list": "Notify List",
"notify_type": "Type",
"notify_msg": "Notify Message",
"notify_available": "List of available notify events",
"notify_desc_levelup": "Triggers when a user levels up on this server.",
"notify_desc_protection": "Triggers when antialt, antispam or antiraid is triggered.",
"notify_desc_addrolerew": "Triggers when a user gets a role as a reward for reaching a level (xprew).",
"notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).",
"notify_desc_not_found": "No description found for this notify event. Please report this.",
"winlb": "Biggest Wins Leaderboard",
"no_banner": "No banner set."
}

View file

@ -0,0 +1,936 @@
{
"all_stats_cleared": "All expression stats rinsed away!",
"deleted": "Got rid of that Expression",
"insuff_perms": "You don\u0027t have the perms - you hafta own me for global expressions, and you need Administrator for server expressions!",
"list_all": "List of all dumb things you make me say",
"new_cust_react": "New Expression",
"no_found": "You haven\u0027t forced me to say any stupid stuff yet.",
"no_found_id": "I didn\u0027t find any reaction with that ID.",
"response": "Response",
"stats": "Expression Stats",
"stats_cleared": "Rinsed away stats for {0} expression.",
"stats_not_found": "You want me to get stats for one without them? Really?",
"autohentai_stopped": "About time, you perv.",
"aar_disabled": "I **won\u0027t auto assign roles** anymore when a user joins!",
"aar_enabled": "**Enabled auto assign role** when users join since you can\u0027t do it yourself. Lazy butt.",
"attachments": "Attachments",
"avatar_changed": "Pic changed",
"bandm": "You have been kicked out to space from {0},\nfor {1}",
"banned_pl": "bunned",
"banned_user": "User bunned",
"bot_name": "Ok, I lost a bet and now everyone can call me {0}",
"bot_status": "Playing {0} now.. just.. just not for you. Don\u0027t get any ideas! \u003E///\u003C",
"byedel_off": "Honestly, I wasn\u0027t gonna delete that many messages in a server that makes so many people wanna leave anyways...",
"byedel_on": "I\u0027ll get rid of bye messages after {0} seconds.",
"byemsg_cur": "Current departure message: {0}",
"byemsg_enable": "Now ya gotta turn on byemessages by typing {0}!",
"byemsg_new": "Changed the byemessage!",
"bye_off": "No more byemessages.",
"bye_on": "Turning on byemessages for this channel~!",
"ch_name_change": "Channel\u0027s Name Changed",
"ch_old_name": "Last Name",
"ch_topic_change": "Channel Topic Changed",
"cleaned_up": "Cleaned that stuff up.",
"content": "Content",
"cr": "Made the role {0}... but don\u0027t get the wrong idea!",
"createtextchan": "Finished making text channel {0}.",
"createvoich": "Piled up voice channel {0} together!",
"deafen": "Literal earshot complete!",
"delmsg_off": "Finally quit cleaning up all that mess. Sheesh, really?",
"delmsg_on": "I\u0027ll delete all user commands that work out now. Hmph.",
"deltextchan": "{0} was the worst text channel anyway. Begone.",
"delvoich": "No one ever used {0} anyways.",
"dm_from": "Blackmail from",
"donadd": "A new donator? Well... thanks I guess. User\u0027s total donation: {0} \uD83D\uDC51",
"donators": "Thanks to these people for donating... It\u0027s not like I needed you to or anything!",
"fwall_start": "All my Masters will be receiving DMs now. \u003E///\u003C",
"fwall_stop": "Only my first Master deserves attention. \u003E///\u003C",
"fwdm_start": "Just because I\u0027m forwarding DMs now, doesn\u0027t mean I\u0027m giving you special attention!",
"fwdm_stop": "Good. I was getting tired of DMs anyways.",
"greetdel_off": "About time you stopped wasting my hard work. Baka.",
"greetdel_on": "Greet messages will be deleted after {0} seconds. What, my greetings weren\u0027t good enough for you?",
"greetdmmsg_cur": "Ugh, greeting people EVERY TIME with: {0}",
"greetdmmsg_enable": "Enable DM blackmail by typing {0}",
"greetdmmsg_new": "New on-join blackmail created.",
"greetdm_off": "I won\u0027t deliver on-join blackmail in DMs anymore.",
"greetdm_on": "I\u0027ll send your admission blackmail in DMs now!",
"greetmsg_cur": "Current tribute blackmail: {0}",
"greetmsg_enable": "Enable admission blackmail by typing {0}",
"greetmsg_new": "New admission blackmail set!",
"greet_off": "Admission blackmail\u0027s off.",
"greet_on": "I\u0027ll send greetmessages in this channel from now on!",
"hierarchy": "Can\u0027t use this one on people with a role higher or equal to yours in power, know your place you tiny little dummy!",
"invalid_format": "Follow the format next time!",
"invalid_params": "Not what you put there...",
"kickdm": "I threw you out of {0} server because {1}",
"kicked_user": "Thrown out",
"lang_list": "List of Languages",
"lang_set": "Changed your server\u0027s locale to {0} - {1}",
"lang_set_bot": "Bot\u0027s default locale is now {0} - {1}",
"lang_set_bot_show": "For now, this is my attitude. {0} - {1}",
"lang_set_fail": "Wroooong! Check the help for this one again.",
"lang_set_show": "My attitude right now is {0} - {1}",
"left": "{0} checked out of {1}",
"log": "Tracking {0} here... why can\u0027t you do it yourself, lazyhead?",
"log_all": "Logging EVERY SINGLE event here... How controlling are you, seriously...",
"log_disabled": "Tracking disabled.",
"log_events": "I can track these. For the server. Not you.",
"log_ignore": "I won\u0027t care about {0} in logs.",
"log_not_ignore": "Won\u0027t ignore {0} in logs anymore.",
"log_stop": "Quit logging {0}, finally...",
"menrole": "{0} is annoying people with these roles",
"message_from_bo": "Useless junk from {0} \u0060[Master]\u0060:",
"message_sent": "Threw the message to em!",
"moved": "{0} sneaked from {1} to {2}",
"msg_del": "Message Shot Out in #{0}",
"msg_update": "Message Sneakily Changed in #{0}",
"muted_pl": "Shutted them up.",
"muted_sn": "Shutted them up.",
"mute_error": "Uhh, I probably don\u0027t have the perms for that, seriously?",
"mute_role_set": "New duct-tape role set.",
"need_admin": "Um. I need **Administration** for that one first, dumbhead...",
"new_msg": "New Message",
"new_nick": "New Nickname",
"new_topic": "New Topic",
"nick_change": "Nickname Changed",
"no_shard_id": "No shard with that ID found.",
"old_msg": "Old Message",
"old_nick": "Old Nickname",
"old_topic": "Old Topic",
"perms": "I can\u0027t do that. Why? I don\u0027t have the permissions!",
"prot_active": "Active Protections",
"prot_disable": "I disabled {0}. You can handle the pesks yourself.",
"prot_enable": "{0} enabled. Yet another thing I have to do for you.",
"prot_error": "Error. You didn\u0027t give me ManageRoles permission",
"prot_none": "You haven\u0027t asked me to enable protections.",
"raid_cnt": "User threshold must be between {0} and {1}.",
"raid_stats": "If {0} or more users join within {1} seconds, I will {2} them.",
"raid_time": "Time must be between {0} and {1} seconds.",
"rar": "{0} didn\u0027t need all those roles anyways. Hmmph.",
"rar_err": "I don\u0027t have the permissions to remove their roles.",
"rc": "Changed {0} role\u0027s colour. Jeez, you could\u0027ve done this yourself!",
"rc_perms": "This mistake came because either you aren\u0027t giving me a valid color or your Daddy forgot to give you permission to do this.",
"remrole": "Nope, {1} doesn\u0027t need {0}. Removed.",
"remrole_err": "I can\u0027t remove their role. Have you tried thinking for once and giving me the right permissions?",
"renrole": "Role renamed.",
"renrole_err": "Failed to rename role. Try giving me the correct permissions, hey?",
"renrole_perms": "You can\u0027t edit roles higher than your highest role.",
"reprm": "Removed the playing message: {0}",
"role_added": "Role {0} has been added to the neverending list in group {1}.",
"role_clean": "{0} doesn\u0027t exist. Cleaned up.",
"role_in_list": "Role {0} is already in the list.",
"ropl_added": "Added.",
"ropl_disabled": "Rotating playing status disabled. Finally, I can take a break.",
"ropl_enabled": "Rotating playing status enabled.",
"ropl_list": "Here is a list of rotating statuses:\n{0}",
"ropl_not_set": "No rotating playing statuses set.",
"self_assign_already": "You already have {0} role. Baka.",
"self_assign_already_excl": "You already have {0} exclusive self-assigned role.",
"self_assign_excl": "Self assigned roles are now exclusive!",
"self_assign_list": "There are {0} self assignable roles",
"self_assign_not": "You can\u0027t self-assign that!",
"self_assign_not_have": "You don\u0027t have {0} role.",
"self_assign_no_excl": "Self assigned roles are now not exclusive!",
"self_assign_perms": "I am unable to add that role to you. I can\u0027t add roles to owners or other roles higher than my role in the role hierarchy.",
"self_assign_rem": "Meaningless title {0} has been removed from the list of self-assignable roles.",
"self_assign_remove": "You no longer have {0} role.",
"self_assign_success": "You now have {0} role.",
"setrole": "Sucessfully added role {0} to user {1}",
"setrole_err": "I\u0027m not allowed to add roles to people. TwT\u0027",
"set_avatar": "New avatar set!",
"set_channel_name": "New channel name set.",
"set_game": "New game set!",
"set_stream": "New stream set!",
"set_topic": "New channel topic set.",
"shard_reconnected": "Shard {0} reconnected.",
"shard_reconnecting": "Shard {0} reconnecting.",
"shutting_down": "Oh. Bye, I guess.",
"slowmode_desc": "Users can\u0027t send more than {0} messages every {1} seconds.",
"slowmode_disabled": "Slow mode disabled.",
"slowmode_init": "Slow mode initiated",
"soft_banned_pl": "soft-bunned (kicked)",
"spam_ignore": "{0} will ignore this channel.",
"spam_not_ignore": "{0} will no longer ignore this channel.",
"spam_stats": "If a user posts {0} same messages in a row, I will {1} them.\n __IgnoredChannels__: {2}",
"text_chan_created": "Text channel created.",
"text_chan_destroyed": "Text channel destroyed. Not like anyone used it anyway.",
"undeafen": "I fixed their earsssss",
"unmuted_sn": "Unmuted",
"username": "Username",
"username_changed": "Username Changed",
"user_banned": "User Banned",
"user_chat_mute": "{0}, ever heard of knowing when to shut your mouth? Or fingers?",
"user_chat_unmute": "**Unmuted** {0} from chatting.",
"user_joined": "User joined",
"user_left": "User left",
"user_muted": "Hey {0}, SHUTUPSHUTUPSHUTUPSHUTUPSHUTUP!!!!",
"user_role_add": "User\u0027s role added",
"user_role_rem": "User\u0027s role removed",
"user_status_change": "{0} is now {1}",
"user_unmuted": "{0}, you can talk now...",
"user_vjoined": "{0} entered {1} voice channel.",
"user_vleft": "{0} left {1} voice channel.",
"user_vmoved": "{0} moved from {1} to {2} voice channel.",
"user_voice_mute": "{0}, URUSAAAAAAAIIIIIIIIII!!!",
"user_voice_unmute": "Freed {0}\u0027s voice.",
"voice_chan_created": "Voice Channel Created",
"voice_chan_destroyed": "Voice Channel Destroyed",
"vt_disabled": "Disabled voice \u002B text feature.",
"vt_enabled": "Enabled voice \u002B text feature.",
"vt_exit": "I don\u0027t have **manage roles** and/or **manage channels** permission, so I cannot run \u0060voice\u002Btext\u0060 on {0} server.",
"vt_no_admin": "You are enabling/disabling this feature and **I do not have ADMINISTRATOR permissions**. This may cause some issues, and you will have to clean up text channels yourself afterwards.",
"vt_perms": "I require at least **manage roles** and **manage channels** permissions to do what you want from me. Please don\u0027t let it be lewd. I prefer Administration permission tho. Please?",
"xmuted_text": "User {0} from text chat",
"xmuted_text_and_voice": "User {0} from text and voice chat",
"xmuted_voice": "User {0} from voice chat",
"sbdm": "You have been soft-banned from {0} server.\nReason: {1}",
"user_unbanned": "User Unbunned",
"migration_done": "Migration done!",
"presence_updates": "Presence Updates",
"sb_user": "User Soft-Banned",
"awarded": "has awarded {0} to {1}. You better be grateful!",
"better_luck": "Ha, are you even trying?? ^_^",
"br_win": "You won {0} for rolling above {1}. You got lucky this time, ok?",
"deck_reshuffled": "Deck reshuffled.",
"flipped": "flipped {0}.",
"flip_guess": "I guess that was OK. You won {0}",
"flip_invalid": "You only can flip 1 to {0} coins!",
"gifted": "has gifted {0} to {1}. You better be grateful!",
"has": "{0} only has {1}",
"heads": "Heads",
"mass_award": "Awarded {0} to {1} users from {2} role.",
"max_bet_limit": "You can\u0027t bet more than {0}",
"min_bet_limit": "You can\u0027t bet less than {0}",
"no_more_cards": "No more cards in the deck.",
"raffled_user": "Raffled User",
"roll": "You rolled {0}.",
"slot_bet": "Bet",
"slot_jackpot": "Wow. You actually impressed me. x{0}",
"slot_single": "Only one {0}, x{1}",
"slot_three": "Three of a kind. That was alright I suppose. x{0}",
"slot_two": "Two {0} - bet x{1}",
"tails": "Tails",
"take": "successfully took {0} from {1}. Thief!",
"take_fail": "was unable to take {0} from {1} because the user is too poor to own {2}!",
"back_to_toc": "Back to ToC",
"bot_owner_only": "Bot Owner Only",
"channel_permission": "Requires {0} channel permission.",
"cmdlist_donate": "You can support the project on patreon: \u003C{0}\u003E or paypal: \u003C{1}\u003E",
"cmd_and_alias": "Command and aliases",
"commandlist_regen": "Commandlist Regenerated.",
"commands_instr": "Type \u0060{0}h CommandName\u0060 to see the help for that specified command. e.g. \u0060{0}h {0}8ball\u0060 ...or don\u0027t, I don\u0027t care!",
"command_not_found": "I can\u0027t find that command. Does it even exist?.",
"desc": "Description",
"donate": "You can support the EllieBot project on \nPatreon \u003C{0}\u003E or\nPaypal \u003C{1}\u003E\nDon\u0027t forget to leave your discord name or id in the message.\n\n**Thank you** ♥️",
"guide": "**You better read these commands before asking questions**: \u003C{0}\u003E\n**Hosting Guides and docs can be found here**: \u003C{1}\u003E",
"list_of_commands": "List Of Commands",
"list_of_modules": "List Of Modules",
"modules_footer": "Type \u0060{0}cmds ModuleName\u0060 to get a list of commands in that module. eg \u0060{0}cmds games\u0060",
"module_not_found": "That module does not exist.",
"server_permission": "Requires {0} server permission.",
"table_of_contents": "Table Of Contents",
"usage": "Usage",
"autohentai_started": "Autohentai started. Reposting every {0}s with one of the following tags:\n{1}",
"tag": "Tag",
"animal_race": "Animal Race",
"animal_race_failed": "Couldn\u0027t start the race, because your lazy butt couldn\u0027t get enough people to participate.",
"animal_race_full": "We\u0027re full! Starting race.",
"animal_race_join": "{0} joined as a {1}",
"animal_race_join_bet": "{0} joined as a {1} and bet {2}.",
"animal_race_join_instr": "Type {0}jr to join the race.",
"animal_race_starting": "Race will start in {0} seconds! Get your butts in the room or we\u0027re starting without you!",
"animal_race_starting_with_x": "Only {0} users came to watch the race. Sad.",
"animal_race_won": "{0} won the race as {1}. Took long enough, though.",
"animal_race_won_money": "{0} won the race as {1} and got {2}! Shoulda bet more, coward.",
"dice_invalid_number": "Invalid number specified. You can roll {0}-{1} dice at once.",
"dice_rolled": "rolled {0}",
"dice_rolled_num": "Dice rolled: {0}",
"race_failed_starting": "Can\u0027t start a race, there\u0027s probably another race you probably forgot about.",
"race_not_exist": "There\u0027s no race currently running in this server, you idiot!",
"second_larger_than_first": "Second number must be larger than the first one.",
"changes_of_heart": "Changes Of Heart",
"claimed_by": "Claimed By",
"divorces": "Divorces",
"likes": "Likes",
"waifus_none": "No waifus have been claimed yet.",
"waifus_top_waifus": "Top Waifus",
"waifu_affinity_already": "your affinity is already set to that waifu or you\u0027re trying to remove your affinity while not having one.",
"waifu_affinity_changed": "changed their affinity from {0} to {1}.\n\n*This is morally questionable.*\uD83E\uDD14",
"waifu_affinity_cooldown": "You must wait {0} hours and {1} minutes in order to change your affinity again.",
"waifu_affinity_reset": "Your affinity is reset. You no longer have a person you like.",
"waifu_affinity_set": "wants to be {0}\u0027s waifu. Aww \u003C3",
"waifu_claimed": "claimed {0} as their waifu for {1}!",
"waifu_divorced_like": "You have divorced a waifu who likes you. You heartless monster.\n{0} received {1} as a compensation.",
"waifu_egomaniac": "you can\u0027t set affinity to yourself, you egomaniac.",
"waifu_fulfilled": "\uD83C\uDF89 Their love is fulfilled! \uD83C\uDF89\n{0}\u0027s new value is {1}!",
"waifu_isnt_cheap": "No waifu is that cheap. You must pay at least {0} to get a waifu, even if their actual value is lower.",
"waifu_not_enough": "You must pay {0} or more to claim that waifu!",
"waifu_not_yours": "That waifu is not yours.",
"waifu_not_yourself": "You can\u0027t claim yourself, you baka!",
"waifu_recent_divorce": "You divorced recently. You must wait {0} hours and {1} minutes to divorce again.",
"nobody": "Nobody",
"waifu_divorced_notlike": "You have divorced a waifu who doesn\u0027t like you. You received {0} back.",
"8ball": "8ball",
"acrophobia": "Acrophobia",
"acro_ended_no_sub": "Game ended with no submissions. I guess everyone is afraid to lose.",
"acro_no_votes_cast": "No votes cast. Game ended with no winner.",
"acro_nym_was": "Acronym was {0}.",
"acro_running": "Acrophobia game is already running in this channel.",
"acro_started": "Game started. Create a sentence with the following acronym: {0}.",
"acro_started_footer": "You have {0} seconds to make a submission.",
"acro_submit": "{0} submitted their sentence. ({1} total)",
"acro_vote": "Vote by typing a number of the submission",
"acro_vote_cast": "{0} cast their vote.",
"acro_winner": "Winner is {0} with {1} points.",
"acro_winner_only": "{0} is the \u0022winner\u0022 for being the only user who made a submission.",
"question": "Question",
"submissions_closed": "Submissions Closed",
"animal_race_already_started": "Animal Race is already running.",
"total_average": "Total: {0} Average: {1}",
"category": "Category",
"cleverbot_disabled": "Disabled cleverbot on this server.",
"cleverbot_enabled": "Enabled cleverbot on this server.",
"curgen_disabled": "Currency generation has been disabled on this channel.",
"curgen_enabled": "Currency generation has been enabled on this channel.",
"curgen_pl": "{0} random {1} appeared.",
"curgen_sn": "A random {0} appeared!",
"failed_loading_question": "Failed loading a question.",
"game_started": "Game Started",
"hangman_game_started": "Hangman game started",
"hangman_running": "Hangman game already running on this channel.",
"hangman_start_errored": "Starting hangman errored.",
"hangman_types": "List of \u0022{0}hangman\u0022 term types:",
"picked": "{0} picked {1}",
"planted": "{0} planted {1}",
"trivia_already_running": "Trivia game is already running on this server.",
"trivia_game": "Trivia Game",
"trivia_guess": "{0} guessed it. The answer was: {1}",
"trivia_none": "No trivia is running on this server.",
"trivia_points": "{0} has {1} points",
"trivia_stopping": "Stopping after this question.",
"trivia_times_up": "Time\u0027s up! The correct answer was {0}",
"trivia_win": "{0} guessed correctly and won. Not like it was hard. It was: {1}",
"ttt_against_yourself": "You can\u0027t play against yourself.",
"ttt_already_running": "TicTacToe Game is already running in this channel.",
"ttt_a_draw": "A draw!",
"ttt_created": "has created a game of TicTacToe.",
"ttt_has_won": "{0} won.",
"ttt_matched_three": "Matched Three",
"ttt_no_moves": "No moves left!",
"ttt_time_expired": "Time Expired!",
"ttt_users_move": "{0}\u0027s move",
"vs": "{0} vs {1}",
"attempting_to_queue": "Queuing {0} songs... Hnnrgh...",
"autoplay_disabled": "Autoplay disabled.",
"autoplay_enabled": "Autoplay enabled.",
"defvol_set": "Default volume set to {0}%",
"dir_queue_complete": "Directory queue complete.",
"fairplay": "Fairplay.",
"finished_song": "The Song is done, finally. Hmmh? it was",
"fp_disabled": "You don\u0027t like being fair huh?",
"fp_enabled": "Oh wow, acting all fair.",
"from_position": "From position",
"invalid_input": "Invalid input.",
"max_playtime_none": "There it goes, the only thing I ever agreed with, undone. As expected of you.",
"max_playtime_set": "Oh? if it is longer than {0} second(s) don\u0027t let it be played? I am impressed, that is the first thing you said that wasn\u0027t stupid!",
"max_queue_unlimited": "It doesn\u0027t matter huh? Just let them queue all the things? Do you think I am some kind of bot?",
"max_queue_x": "Hmmh, so much to watch out for... After {0} track(s), I\u0027ll stop allowing users to add tracks to the queue.",
"now_playing": "Now Playing",
"no_player": "No I am not playing music, can\u0027t you hear that?",
"no_search_results": "Some day I will just not even try anymore. Search for something that exists.",
"paused": "*presses pause*",
"player_queue": "Player Queue - Page {0}/{1}",
"playing_song": "Playing your stupid song #{0}",
"playlists": "\u0060#{0}\u0060 - **{1}** by *{2}* ({3} songs)",
"playlists_page": "Page {0} of Saved Playlist notes",
"playlist_deleted": "*throws the playlist note into fire*",
"playlist_delete_fail": "I can\u0027t delete playlist notes if they don\u0027t exist! And I am definitely not going to delete those you didn\u0027t make, you selfish animal!",
"playlist_id_not_found": "Baka, there is no playlist like this. Just look at the notes!",
"playlist_queue_complete": "All the aongs are in place! What perfection, ***I*** am amazing, after all.",
"playlist_saved": "Playlist note Saved",
"play_limit": "{0}s limit",
"queue": "Queue",
"queued_song": "Queued Song",
"queue_cleared": "Music queue cleared.",
"queue_full": "That one person, what was *its* name again? Well I was told that after {0}/{0} nothing should be allowed.",
"removed_song": "Removed song",
"repeating_cur_song": "Repeating Current Song",
"repeating_playlist": "Repeating Playlist",
"repeating_track": "This song? Really? I have to endure it over and over?",
"repeating_track_stopped": "Finally it stopped!",
"resumed": "I wonder how you can be unable to press the **ON** button on this speaker.",
"rpl_disabled": "Finally it stops, if you weren\u0027t so useless you could do it yourself.",
"rpl_enabled": "Hmmh, when it ends add it to the end of the line, I guess I have to huh?",
"set_music_channel": "Haaa? Here? Sometimes I wonder why ***I*** have to do this for you. I\u0027ll call out the playing, finished, paused and removed songs here.",
"skipped_to": "Skipped to \u0060{0}:{1}\u0060",
"song_moved": "Away you go!",
"time_format": "{0}h {1}m {2}s",
"to_position": "To position",
"unlimited": "unlimited",
"volume_input_invalid": "0 and 100. Is it that hard to hit between them?",
"volume_set": "Volume set to {0}%",
"acm_disable": "Disabled usage of ALL MODULES on {0} channel.",
"acm_enable": "Enabled usage of ALL MODULES on {0} channel.",
"allowed": "Allowed",
"arm_disable": "Disabled usage of ALL MODULES for {0} role.",
"arm_enable": "Enabled usage of ALL MODULES for {0} role.",
"asm_disable": "Disabled usage of ALL MODULES on this server.",
"asm_enable": "Enabled usage of ALL MODULES on this server.",
"aum_disable": "Disabled usage of ALL MODULES for {0} user.",
"aum_enable": "Enabled usage of ALL MODULES for {0} user.",
"blacklisted": "Blacklisted {0} with ID {1}",
"cmdcd_add": "Command {0} now has a {1}s cooldown.",
"cmdcd_cleared": "O-okay, command {0} has no coooldown now and all existing cooldowns have been cleared.",
"cmdcd_none": "No command cooldowns set.",
"command_costs": "Command Costs",
"cx_disable": "Disabled usage of {0} {1} on {2} channel.",
"cx_enable": "Enabled usage of {0} {1} on {2} channel.",
"denied": "Denied",
"filter_word_add": "Added word {0} to the list of filtered words.",
"filter_word_list": "The No-no\u0027s, as you would say",
"filter_word_remove": "Good good, less to do.. Not watching out for {0} anymore.",
"invalid_second_param_between": "Baka! The second one is wrong! (Must be a number between {0} and {1})",
"invite_filter_channel_off": "Yes yes I\u0027ll stop.",
"invite_filter_channel_on": "Gah, so many special wishes. I\u0027ll only try to eliminate the Pest on this channel.",
"invite_filter_server_off": "Eh? Okay, I\u0027ll stop eliminating the pests\u0027 tries on this server.",
"invite_filter_server_on": "I\u0027ll eliminate the pests\u0027 tries to get more people on this server!",
"moved_permission": "Moved permission {0} from #{1} to #{2}",
"no_costs": "It\u0027s free!",
"of_command": "command",
"of_module": "module",
"permrole": "I let people with {0} change your stupid rules.",
"permrole_changed": "Haaaaa, people with {0} will be able to edit persmissions now.",
"perm_out_of_range": "Nothing to be fouuund! Are you doing this right?",
"removed": "removed permission #{0} - {1}",
"rx_disable": "Disabled usage of {0} {1} for {2} role.",
"rx_enable": "Enabled usage of {0} {1} for {2} role.",
"sec": "sec.",
"sx_disable": "Disabled usage of {0} {1} on this server.",
"sx_enable": "Enabled usage of {0} {1} on this server.",
"unblacklisted": "Unblacklisted {0} with ID {1}",
"uneditable": "uneditable",
"ux_disable": "Disabled usage of {0} {1} for {2} user.",
"ux_enable": "Enabled usage of {0} {1} for {2} user.",
"verbose_false": "Well, I\u0027ll not show you your stupid tries then.",
"verbose_true": "Oho? Sure, I will show you your stupid tries.",
"word_filter_channel_off": "Pfft, Didn\u0027t like me cleaning words huh? What did you expect?",
"word_filter_channel_on": "You want only this one to be cleaned? Special needs, wow.",
"word_filter_server_off": "Pfft, Didn\u0027t like me cleaning words huh? What did you expect?",
"word_filter_server_on": "Ha! I have to clean after them? Sure, I\u0027ll help you incompetents here.",
"abilities": "Abilities",
"anime_no_fav": "No favorite anime yet",
"atl_ad_started": "Let\u0027s see, hehe. I\u0027ll just remove your messages to write them in the way it is supposed to be!",
"atl_removed": "No need to show off my *amazing* skills huh?",
"atl_set": "Change huh? I shall use my *amazing* skills with {from}\u003E{to} instead!",
"atl_started": "I shall use my *amazing* skills to help you fools in this channel.",
"atl_stopped": "Okay I will stop to use my *amazing* skills for this channel.",
"bad_input_format": "That is, what? No I don\u0027t understand what this is.",
"card_not_found": "Who sorted this? It\u0027s impossible to find anything!",
"catfact": "fact",
"chapters": "Chapters",
"comic_number": "Comic #",
"compet_loses": "Competitive Losses",
"compet_played": "Competitive Played",
"compet_rank": "Competitive Rank",
"compet_wins": "Competitive Wins",
"completed": "Completed",
"condition": "Condition",
"date": "Date",
"define": "Define:",
"dropped": "Dropped",
"episodes": "Episodes",
"error_occured": "EH? A mistake? I don\u0027t make mistakes, you made a mistake! BAKA!",
"example": "Example",
"failed_finding_anime": "I can\u0027t find that animu.",
"failed_finding_manga": "Can\u0027t find that fruit. Ha!... You don\u0027t get it? Hmph.",
"genres": "Genres",
"hashtag_error": "Woah, you are so out! There is nothing to explain to you about this.",
"height_weight": "Height/Weight",
"height_weight_val": "{0}m/{1}kg",
"humidity": "Humidity",
"image_search_for": "Image Search For:",
"imdb_fail": "What are you even searching for?",
"invalid_lang": "This is not how this works. I need a source and a target language.",
"jokes_not_loaded": "What? Jokes? No such thing.",
"latlong": "Lat/Long",
"list_of_place_tags": "List of {0}place tags",
"location": "Location",
"magicitems_not_loaded": "Magic Items not loaded.",
"mal_profile": "{0}\u0027s MAL profile",
"mashape_api_missing": "I need a MashapeApiKey for that... M-maybe we should ask Master?",
"min_max": "Min/Max",
"no_channel_found": "Not quite sure what you expected, no channel found.",
"on_hold": "On-Hold",
"original_url": "Original Url",
"osu_api_key": "Haaa? I need an osu! API key for that.",
"osu_failed": "Hmph.They didn\u0027t give **me** the osu! signature!",
"over_x": "Hehe, I found over {0} images. I\u0027ll show random {0}.",
"ow_user_not_found": "Doesn\u0027t exist! I can\u0027t work like this, give me a usable BattleTag for that Region!",
"plan_to_watch": "Plan to watch",
"platform": "Platform",
"pokemon_ability_none": "That is just weird, what are you trying to make me find?",
"pokemon_none": "What is a \u0060tablemon\u0060 supposed to be? Give me something real.",
"profile_link": "Profile Link:",
"quality": "Quality:",
"quick_playtime": "Quick Playtime",
"quick_wins": "Quick Wins",
"rating": "Rating",
"score": "Score:",
"search_for": "Search For:",
"shorten_fail": "Well, I can\u0027t shorten it. Give me something useable next time.",
"short_url": "Short Url",
"something_went_wrong": "Well this didn\u0027t work like I wanted it to... Probably your fault.",
"specify_search_params": "Don\u0027t you know I need to know what to search for to... search for it?",
"status": "Status",
"store_url": "Store Url",
"streamer_offline": "Streamer {0} is offline, probably didn\u0027t want to be seen by you.",
"streamer_online": "Oh? Streamer {0} is online with {1} viewers, kind of sad.",
"streams_following": "I have to watch {0} streams for you! Unbelievable!",
"streams_none": "No Streams to watch out for, good.",
"stream_no": "There is no such stream, stop bothering me.",
"stream_not_exist": "This probably doesn\u0027t exist, what are you even doing?",
"stream_removed": "Don\u0027t want to be bothered by {0}\u0027s stream ({1}) anymore huh? I\u0027ll stop watching out then.",
"stream_tracked": "Since your brain is unable to concentrate at all **I** shall tell you when the status changes.",
"sunrise": "Sunrise",
"sunset": "Sunset",
"temperature": "Temperature",
"title": "Title:",
"top_3_fav_anime": "Top 3 favorite anime:",
"translation": "Translation:",
"types": "Types",
"ud_error": "I can\u0027t define that to an idiot like you, ask me something else.",
"url": "Url",
"viewers": "Viewers",
"watching": "Watching",
"wikia_error": "I don\u0027t get this. Are you sure this is a thing or is your stupidity starting to reach me?",
"wikia_input_error": "Give me a wikia and then something I should search for. Simple, right?",
"wiki_page_not_found": "That doesn\u0027t exist.",
"wind_speed": "Wind Speed",
"x_most_banned_champs": "The {0} champions ~~you want to play~~ that are the most often banned",
"yodify_error": "W-What are you trying to make me do? Impossible!",
"activity_line": "\u0060{0}.\u0060 {1} [{2:F2}/s] - {3} total",
"activity_page": "Activity Page #{0}",
"activity_users_total": "{0} users total.",
"author": "Author",
"botid": "Bot ID",
"calcops": "Here are the things I can work with if you use {0}calc. Yes, I am that amazing.",
"channelid": "Nothing is safe from you, {0} of this channel is {1}.",
"channel_topic": "Channel Topic",
"commands_ran": "Commands/Messages",
"convert": "{0} {1} is {2} {3}",
"convertlist": "Here are the things I understand how to convert, hehe.",
"convert_not_found": "I can\u0027t convert {0} to {1}: No idea what that is supposed to be.",
"convert_type_error": "I can\u0027t convert {0} to {1}: This isn\u0027t how this works, as expected of you",
"created_at": "Created At",
"custom_emojis": "Custom Emojis",
"error": "Error",
"features": "Features",
"index_out_of_range": "Hahahahaha! **Useless**, that Index is out of range.",
"inrole_list": "Here are all the people that you deemed worthy to be in the *Role* {0} :",
"inrole_not_allowed": "Oi! What are you trying to do!? You think that **I** am going to say ALL of those Users names? Hmph. Who do you think you are?",
"invalid_value": "Really? How did you even get that wrong? Invalid {0} value.",
"listservers": "ID: {0}\nMembers: {1}\nOwnerID: {2}",
"listservers_none": "T-There are no servers on this page...",
"list_of_repeaters": "List of Repeaters",
"members": "Members",
"memory": "Memory",
"messages": "Messages",
"message_repeater": "Message Repeater",
"nickname": "Nickname",
"nobody_playing_game": "Hah, Nobody wants to play that game with you.",
"no_active_repeaters": "No active repeaters here and that is fine, trust me.",
"no_roles_on_page": "No roles to be found here, guess the creativity only reached so far?",
"no_topic_set": "You can\u0027t even see yourself that there is nothing? **N**.**O**.**T**.**H**.**I**.**N**.**G**.",
"owner": "Master",
"owner_ids": "Master IDs",
"presence": "Presence",
"presence_txt": "{0} Servers\n{1} Text Channels\n{2} Voice Channels",
"quotes_deleted": "Woah! Well, I deleted all mistakes with the {0} keyword.",
"quotes_page": "Page {0} of quotes",
"quotes_page_none": "Quotes not found on that page. Maybe in the future we will find what we need. Not what we want.",
"quotes_remove_none": "There are no mistakes you could erase here, I am impressed.",
"quote_added": "I added another ~~mistake~~ quote!",
"quote_deleted": "I deleted one of your mistakes, not sure which one though, hehe.",
"region": "Region",
"registered_on": "Registered On",
"remind": "Oh My. I\u0027ll remind the incompetent people of {0} to {1} in {2} \u0060({3:d.M.yyyy.} at {4:HH:mm})\u0060",
"remind_invalid_format": "Not a valid time format. Check the commandlist.",
"remind_template": "You want a style change huh? Looks awful, I saved it as remind template.",
"repeater": "Why do I have to do this for you? I\u0027ll repeat {0} every {1} day(s), {2} hour(s) and {3} minute(s)...",
"repeaters_list": "List Of Repeaters",
"repeaters_none": "No repeaters here and that is fine.",
"repeater_stopped": "#{0} finally stopped!",
"repeat_invoke_none": "Nothing to see here, you are probably too selfish to give others information.",
"result": "Result",
"roles": "Roles",
"roles_all_page": "Page #{0} of all roles on this server:",
"roles_page": "Page #{0} of roles for {1}",
"rrc_no_colors": "Can you please give me something I can work with? Can you use \u0060#00ff00\u0060 for example?",
"rrc_start": "Gah, sounds annoying.. I\u0027ll rotate {0} role\u0027s color.",
"rrc_stop": "Well, that was a waste of time. I\u0027ll stop rotating {0} role\u0027s color then.",
"serverid": "Not even objects are safe from you... {0} of the server is {1}",
"server_info": "Server Info",
"shard": "Shard",
"showemojis": "**Name:** {0} **Link:** {1}",
"showemojis_none": "Nothing to see here~ You aren\u0027t talented enough for this, huh?",
"stats_songs": "I\u0027m playing {0} songs, {1} is in the list.",
"text_channels": "Text Channels",
"togtub_room_link": "Oho~ Here is your room link:",
"uptime": "Uptime",
"userid": "Woah!? {0} of {1} is {2}, creep.",
"voice_channels": "Voice Channels",
"animal_race_already_in": "You\u0027ve already joined this race! ",
"current_poll_results": "Current poll results",
"no_votes_cast": "Nobody voted... *giggles",
"poll_already_running": "There\u0027s already a poll up, close it first ya dum dum...",
"poll_created": "\uD83D\uDCC3 {0} made a poll... hmpf",
"poll_result": "\u0060{0}.\u0060 {1} with {2} votes.",
"poll_voted": "{0} voted.",
"poll_vote_private": "Uhm, I guess you can Private Message me the number of your answer...",
"poll_vote_public": "Send the number that corresponds with the answer here!",
"thanks_for_voting": "Thanks alot for voting, {0}",
"x_votes_cast": "{0} total votes cast.",
"pick_pl": "You pick them up by literally typing \u0060{0}pick\u0060 what the hell dude?",
"pick_sn": "Snatch it by typing \u0060{0}pick\u0060!!",
"no_users_found": "I don\u0027t see any of your friends haha.",
"no_vcroles": "There are no voice channel roles, the hell?",
"user_muted_time": "I\u0027ve **SILENCED** {0} for {1} minutes.",
"vcrole_added": "Giving anyone who joins {0} voice channel {1} role, just because it\u0027s cool, and nothing else...",
"vcrole_removed": "I won\u0027t give anyone who joins {0} voice channel a role anymore.",
"vc_role_list": "Voice channel roles",
"crad_disabled": "I won\u0027t clean up triggers of the expression with id {0} anymore, I guess...",
"crad_enabled": "I\u0027ll sweep up any triggers of the expression with id {0}... why do I have to clean up your mess?",
"crdm_disabled": "I won\u0027t send the response from expression with id {0} as a PM anymore. Phew.",
"crdm_enabled": "I\u0027ll get in someone\u0027s face for the expression with id {0}\u0027s message...",
"aliases_none": "No alias found",
"alias_added": "I\u0027ll make {0} do {1} too... b-but for everyone, not just you!",
"alias_list": "List of aliases",
"alias_removed": "I removed the alias from trigger {0}.",
"alias_remove_fail": "Trigger {0} didn\u0027t even have an alias, dummy...",
"compet_playtime": "Time spent NEETing in ranked",
"channel": "Channel",
"command_text": "Command Text",
"kicked_pl": "Dumped",
"moderator": "Moderator",
"reason": "Why?",
"scadd": "New startup command added.",
"scrm": "Startup command successfully removed.",
"scrm_fail": "Startup command not found.",
"server": "Server",
"startcmdlist_none": "No startup commands on this page.",
"startcmds_cleared": "Dumped out all startup commands.",
"unbanned_user": "De-hammered user {0}.",
"user_not_found": "I can\u0027t find that guy.",
"user_warned": "Successfully shouted at user {0}.",
"user_warned_and_punished": "Warned user {0} and gave them the stabby stabby of a {1}.",
"warned_on": "Warned on {0} server",
"warned_on_by": "On {0} at {1} by {2}",
"warnings_cleared": "Pulled the knife out of {0} and cleared all warnings.",
"warnings_none": "No scolds here!",
"warnlog_for": "Here\u0027s the warnlog for {0}, I guess...",
"warnpl_none": "Punishments not set YET.",
"warn_cleared_by": "Evidence hided by {0}",
"warn_punish_list": "Crimes committed",
"warn_punish_rem": "Having {0} warnings will no longer make me shank a dude.",
"warn_punish_set": "I will stab users with {1} warnings with a {0}-tipped blade.",
"slowmodewl_role_start": "I\u0027ll take anyone in {0} out of slowmode.",
"slowmodewl_role_stop": "I won\u0027t let {0} role past slowmode anymore.",
"slowmodewl_user_start": "I\u0027ll take user {0} out of slowmode, but just because they aren\u0027t mean...",
"slowmodewl_user_stop": "I won\u0027t ignore user {0} for slowmode anymore.",
"clpa_fail": "Looks like I can\u0027t give them to ya for one of these reasons:",
"clpa_fail_already": "You might\u0027ve already grabbed your reward for this month, and you can only get them once a month unless you increase your pledge...",
"clpa_fail_already_title": "You already got it... hmph.",
"clpa_fail_conn": "Your discord account might not be connected to Patreon, and that\u0027s totally a big problem, to connect it, go to [Patreon account settings page](https://patreon.com/settings/apps/discord) and click the \u0027Connect to discord\u0027 button. ",
"clpa_fail_conn_title": "Discord account\u0027s not connected.",
"clpa_fail_sup": "Well, to get the reward you need to support the project on patreon, using {0}",
"clpa_fail_sup_title": "Not gonna support this dude",
"clpa_fail_wait": "Guess you gotta wait a few hours after pledging, if you haven\u0027t try again laters",
"clpa_fail_wait_title": "Just wait a little more, ya impatient blubberhead",
"clpa_success": "I\u0027ve given you {0}, thanks for supporting the project \u003C3",
"clpa_too_early": "Uhh... too early for that, you have to wait till the 5th of each month or later, dummy!",
"time": "Time in {0} is {1} - {2}",
"rh": "Set the display of guild role {0} to {1}.",
"shop": "The Shop, add items so people can spend their hard earned money on your crap.",
"shop_item_add": "You added your shitty item to the shop!",
"shop_none": "I couldn\u0027t find any shop items on this page ;-;",
"shop_role": "I will give you {0} role. Because i have to, not because i want to.",
"type": "Type, you need more explanation than that?",
"clpa_next_update": "Updating in {0}... who cares? That\u0027s what I thought.",
"gvc_disabled": "Game Voice Channel feature has been disabled on this server.",
"gvc_enabled": "{0} is a Game Voice Channel now.",
"not_in_voice": "It seems like you\u0027re not in a voice channel in this server...",
"item": "Things and stuff.",
"out_of_stock": "Nothing left.",
"random_unique_item": "Some random item that\u0027s unique. Nothing special.",
"shop_buy_error": "Error DMing item. I\u0027m giving back your money this time.",
"shop_item_not_found": "Couldn\u0027t find that item on that index.",
"shop_item_purchase": "You wasted the money successfully!",
"shop_item_rm": "Shop item stolen.",
"shop_item_wrong_type": "That shop ain\u0027t gonna work, it doesn\u0027t support item adding.",
"shop_list_item_added": "You got the item congrats.",
"shop_list_item_not_unique": "You already have that item.",
"shop_purchase": "Purchase on {0} server",
"shop_role_not_found": "That role you wanna buy, no longer exists.",
"shop_role_purchase": "You just bought with success {0} role! Why do you need another ego boost?",
"shop_role_purchase_error": "Error assigning role. I\u0027ll think about refunding you... fine. Here.",
"unique_items_left": "{0} unique items left.",
"blocked_commands": "Ignoring Commands, that\u0027s what i always do when you talk to me.",
"blocked_modules": "Ignoring Modules, like i ignore you.",
"gcmd_add": "Command {0} has been disabled on all servers. Trying to shut me down, huh...",
"gcmd_remove": "Command {0} has been enabled on all servers. I knew you wouldn\u0027t! Hah! You need me after all.",
"gmod_add": "Module {0} is disabled on all the servers! I\u0027m sure this upsets some people. But i don\u0027t care.",
"gmod_remove": "Module {0} is enabled on all the servers! WHY. IT WAS FUNNY TO TEASE THEM WITH IT.",
"lgp_none": "I haven\u0027t blocked any of the modules... yet... heh...",
"animal_race_no_race": "This race is already full enough! Try a diet or something!",
"cant_read_or_send": "You can\u0027t read those messages from that channel. Hah.",
"quotes_notfound": "Didn\u0027t find any quotes matching the quote ID. And tbh i didn\u0027t really try looking.",
"prefix_current": "The prefix is on the server is {0} How do you STILL not know that?",
"prefix_new": "The server prefix has been changed from {0} to {1}... stop changing it, it\u0027s confusing.",
"defprefix_current": "The bot default prefix is {0} , default just the way i like it. Let\u0027s keep it that way please.",
"defprefix_new": "Default bot prefix is changed from {0} to {1}... NOOO... whyyyyyy",
"bot_nick": "Went back in time and changed my name to {0}... Ugh I hate you.",
"user_nick": "From now on i {0} will be called {1}! Heh.",
"timezone_guild": "This guild is in a timezone called \u0060{0}\u0060",
"timezone_not_found": "No timezone found. Maybe you should try \u0022timezones\u0022 so you know the timezones we have in this world.",
"timezones_available": "All the Timezones you can warp to",
"song_not_found": "Couldn\u0027t find that song. Probably cus it\u0027s just as bad at singing as you.",
"define_unknown": "I can\u0027t seem to find out what the fluff the definition of that term is. But i don\u0027t care. Hmpf.",
"repeater_initial": "I will repeat the initial messages in about {0}hours maybe and like {1}minute or smth. If i feel like it.",
"verbose_errors_enabled": "If you screw up using commands again i will now point out what you screwed up exactly.",
"verbose_errors_disabled": "Ok from now on if you use a command again i will not tell you what you screwed up okay. Still friends?",
"perms_reset": "And all the permissions are back how they were. It\u0027s better like this. You made it all a mess.",
"migration_error": "ERROR while migrating! Quickly check my console to get more information about what has happened! ...or don\u0027t ...I don\u0027t care. Tch.",
"hex_invalid": "I can\u0027t figure out what color you want. Be more specific.",
"global_perms_reset": "And the permissions have been reset globally!",
"module": "Module: {0}",
"hangman_stopped": "Now free the dude that\u0027s getting hanged. Game has stopped.",
"autoplaying": "Auto-playing. For you, lazy baka.",
"queue_stopped": "The player is stopped. You could resume playing by using {0} command. But please don\u0027t. My ears hurt.",
"removed_song_error": "Seems that the song doesn\u0027t exists on that index. Oh you don\u0027t like that? Then LOOK YOURSELF",
"shuffling_playlist": "*table flips the songs*",
"songs_shuffle_enable": "*always flips the songs from now on*",
"songs_shuffle_disable": "Ok... I\u0027ll stop flipping the songs from now on... maybe.",
"song_skips_after": "We skip this track after we hit {0} okay. It\u0027s too boring.",
"warnings_list": "The list of all the badboys on in this crib. Aka the trash you will make me bring out soon. Aka i\u0027m gonna kick your butt, you can do that yourself.",
"redacted_too_long": "Redacted because it\u0027s way too big for me.",
"blacklisted_tag_list": "The list of blacklisted tags (words i don\u0027t like):",
"blacklisted_tag": "One or more tags you used are blacklisted. Stop being stupid. Please. Oh, i forgot. You can\u0027t.",
"blacklisted_tag_add": "Nsfw tag {0} is now blacklisted. WHO WOULD WANT TO LOOK AT THAT ANYWAYS--",
"blacklisted_tag_remove": "Nsfw tag {0} is no longer blacklisted. I always knew it... You\u0027re one of those people...",
"waifu_gift": "Threw {0} to {1} with success. It hit them on the head and they are in hospital now.",
"waifu_gift_shop": "Digital waifu pillow weebshop. You better buy me something too.",
"gifts": "Presents from Peasants. I\u0027ll throw them right back at you, if I don\u0027t like them.",
"nunchi_joined": "You joined the nunchi game. {0} other users want to play so far, all because of me. Thank me. Now.",
"nunchi_ended": "Nunchi game ended. {0} won, cus they are clearly better than you.",
"nunchi_ended_no_winner": "The game ended and none of you were good enough to win. What a surprise.",
"nunchi_started": "The nunchi game has started with {0} participants! We all know i\u0027m going to win. Why even try?",
"nunchi_round_ended": "Nunchi round has ended! {0} is out of the game, now leave the server while you\u0027re at it, BAKA.",
"nunchi_round_ended_boot": "Nunchi round ended due to timeout of some lazy users. These active users are still in the game: {0}",
"nunchi_round_started": "The nunchi round has started with {0} users! Start counting from the number {1}. I suggest not being stupid but i know that\u0027s gonna be hard for you :D",
"nunchi_next_number": "Number registered. Last number was {0}. SIGH.",
"nunchi_failed_to_start": "Not enough people joined the nunchi game because they saw you enter it. Now the game failed to start. Good job BAKA.",
"nunchi_created": "I\u0027ve created a Nunchi game... b-but DEFINITELY NOT for you BAKA! It\u0027s for the others that will join now!",
"sad_enabled": "Great! My ears hurt now! I\u0027m deleting those songs when it\u0027s over.",
"sad_disabled": "Your taste of songs are... fine I guess. I\u0027ll stop removing them from the queue. Just this time... tchhh..",
"stream_role_enabled": "When a user from {0} role starts streaming, I\u0027ll give them {1} role too! But only because they aren\u0027t as annoying as you.",
"stream_role_disabled": "Disabled stream role feature. Nobody needed that anyways.",
"stream_role_kw_set": "Streams now require the keyword {0} in order to get the role. The one you will set. Probably. Hopefully!",
"stream_role_kw_reset": "How many times did you forget the keyword already? This is the last time i\u0027m resetting this for you BAKA!",
"stream_role_bl_add": "User {0} will never receive the stream role. why? because i say so. baka.",
"stream_role_bl_add_fail": "The user {0} is already blacklisted you baka.",
"stream_role_bl_rem": "I unblacklisted that person called {0}. Why do you always have to blacklist the wrong people? Maybe we should blacklist you next!",
"stream_role_bl_rem_fail": "User {0} is not (yet) blacklisted.",
"stream_role_wl_add": "User {0} gets the stream role even if they don\u0027t have the keyword in the stream title. Because i\u0027m nice. No other reason. Get down on your knees and thank me.",
"stream_role_wl_add_fail": "User {0} is already whitelisted. Your memory sucks.",
"stream_role_wl_rem": "User {0} is no longer whitelisted. Never liked them anyways.",
"stream_role_wl_rem_fail": "User {0} is not whitelisted. They are a baka after all.",
"bot_config_edit_fail": "I failed setting {0} to the value {1} , but it\u0027s all cus of you.",
"bot_config_edit_success": "Made the value {0} to {1}. Now gimme cookies.",
"crca_disabled": "Expression with the id {0} will no longer get triggerd unless it\u0027s triggerd word is at the beggining of the sentence. Don\u0027t trigger it.",
"crca_enabled": "Expressions with the id {0} now get triggerd if it\u0027s said anywhere in the sentence. #mastertriggerd",
"server_level": "Server Level, what do you really need more explanation than that? BAKA.",
"club": "Club, the thing you wanna be in but nobody wants you.",
"xp": "Experience, the thing you don\u0027t have",
"excluded": "{0} user has been expelled from the mighty XP system on this server. May he cry forever with his virtual waifu.",
"not_excluded": "{0} is no longer expelled from the mighty XP system on this server. May he be a passable slave to the system.",
"exclusion_list": "The never ending list of the people that stalk me. Aka: Exclusion List",
"server_is_excluded": "This server is not my problem anymore.",
"server_is_not_excluded": "This server is my problem again. Sigh...",
"excluded_roles": "Honestly not gonna care about these Roles",
"excluded_channels": "Channels i don\u0027t give a single fuck about",
"level_up_channel": "Oh wow {0}, You\u0027ve reached level {1}!",
"level_up_dm": "Congrats baka {0} , your cat types more interesting stuff than you. You\u0027ve reached level {1} on {2} server!",
"level_up_global": "Congrats, you have no life {0}, You\u0027ve reached global level {1}!",
"role_reward_cleared": "Level {0} will no longer reward a role. They didn\u0027t deserve a role anyways.",
"role_reward_added": "Users who reach level {0} will receive {1} role. So i\u0027m gonna give this no-life user a role for letting his cat type? Fine... for the cat. Hmpf.",
"level_x": "Level {0}",
"server_leaderboard": "Server XP Leaderboard. Why don\u0027t you know this??",
"global_leaderboard": "Global XP Leaderboard. Do i have to spell everything out for you now..?!",
"modified": "Modified server XP of the user {0} by {1} CHEATER.",
"club_create_error": "You failed. Typical. Are you even level 5? Or did you forget you are already in that loser club?",
"club_created": "Club {0} successfully created! Congrats, you made it. I always knew Idiots need 3 or more tries.",
"club_not_exists": "Yea..check your spelling idiot. That club isn\u0027t a thing.",
"club_applied": "You\u0027ve applied for membership in {0} club. HAH! You really believe they will accept you? XD",
"club_apply_error": "Error applying. Either you\u0027re already a member of the club, or you\u0027re not good enough to the minimal expectations we set. Or maybe you\u0027re just banned from this club. Hah.",
"club_accepted": "Accepted user {0} to the club. I don\u0027t like them. But i\u0027ve accepted them. For you. Just this once. Hmpf.",
"club_accept_error": "You made a mistake... AGAIN. This user doesn\u0027t exist.",
"club_left": "You left them losers of the club you were in.",
"club_not_in_club": "You\u0027re not in a club, or your trying to escape from the club... That you actually own...",
"club_user_kick": "So i kicked this idiot here {0} from {1} club. They were really getting on my nerves.",
"club_user_kick_fail": "So i tried kicking them but it didn\u0027t work which means you are stupid. As always. Did your memory forget to tell you that you have to be owner of the club? Also are they even in the club, BAKA?! HUH?!!! THOUGHT SO!!",
"club_user_banned": "Banned user {0} from {1} club. That felt good.",
"club_user_ban_fail": "Failed to ban. You\u0027re not the club owner, or that person isn\u0027t in your club. It might be possible that the person didn\u0027t even apply to it.",
"club_user_unbanned": "Unbanned user {0} in {1} club .They are annoying but the others are worse.",
"club_user_unban_fail": "Too stupid to unban. You left your brain at home. Are you even club owner. Or is the user not in the club... or... did they apply to it? N-nah, no way, the club sucks.",
"club_level_req_changed": "Changed club\u0027s level requirement to {0} .It was about time. Too many bakas joined.",
"club_level_req_change_error": "I couldn\u0027t change the level requirements. Probably because you annoy me.",
"club_disbanded": "I successfully raided {0} club and it\u0027s now fully destroyed",
"club_disband_error": "Error. So either you\u0027re not in a club or you\u0027re not the owner of your own club? Wait what?",
"club_icon_error": "That image url is invalid or you\u0027re just lying about being the club owner.",
"club_icon_set": "Uploaded your selfie to the club as an icon. I\u0027m judging you silently.",
"club_bans_for": "Le bans for {0} club",
"club_apps_for": "Beggars for {0} club",
"club_leaderboard": "Club leaderboard - page {0}",
"edited_cust_react": "Expression Edited. It sounded better before.",
"self_assign_are_exclusive": "You can only choose 1 role from each group. Don\u0027t be greedy.",
"self_assign_are_not_exclusive": "You can choose any number of roles from any group. Hmpf, so greedy.",
"self_assign_group": "Group {0}",
"poll_closed": "This poll is now closed. You\u0027re too late, b-baka.",
"club_not_exists_owner": "You don\u0027t own this club. You aren\u0027t admin either. Stop messing around now.",
"club_admin_add": "{0} is a club admin. For now.",
"club_admin_remove": "{0} is no longer club admin. About time.",
"club_admin_error": "Error. Are you sure that you are owner of this club? Is that user even in your club?",
"started": "I\u0027m gonna start reposting this every {0}s.",
"stopped": "Stopped reposting. Finally.",
"feed_added": "Feed added. Yum.",
"feed_not_valid": "That\u0027s an invalid link kiddo, or you\u0027re already following that feed on this server, ORRRRR you\u0027ve reached the maximum number of feeds allowed. Geuss i can only handle a few at a time~",
"feed_out_of_range": "Index out of range.",
"feed_removed": "Feed removed. I didn\u0027t care much for it anyways...",
"feed_no_feed": "You gotta sub to some of our feeds we made on the server.",
"restart_fail": "You have to setup the RestartCommand in your creds.yml",
"restarting": "Restarting, or as i call it... taking a break from you.",
"edit_fail": "So..that reaction with that ID?? Yea that doesn\u0027t exist. Believe it or not.",
"streaming": "Streaming",
"followers": "Followers, the thing you lack.",
"rafflecur": "{0} Currency Raffle. More ways to lose your flowers.",
"rafflecur_joined": "User {0} joined the raffle, good luck!",
"rafflecur_already_joined": "Did you forget you already joined? Or did you try to join with an invalid value? I\u0027m sure it\u0027s one of those! BAKA.",
"rafflecur_ended": "{0} raffle ended. {1} won {2}. Congrats, you didn\u0027t waste flowers this time.",
"autodc_enable": "I\u0027m gonna leave this voice channel when I\u0027m done playing your songs. Hmpf.",
"autodc_disable": "I will stay in the voice channel even when you don\u0027t play any songs with me. Praise yourself worthy.",
"timely_none": "My owner forgot to specify the timely reward. So now we\u0027re all screwed.",
"timely_already_claimed": "You already claimed your timely reward! You can come back in {0} for more okay. Now get me some snacks.",
"timely": "Alright, here is your {0}. You can come back to me in {1}h.",
"timely_set": "Everyone will be able to claim {0} every {1}h",
"timely_set_none": "Everyone will not be able to claim timely currency. Now i can go rest again in bed.",
"timely_reset": "Everyone will be able to claim timely currency again. I seriously got upset when you enabled this again.",
"waifu_transfer_fail": "You don\u0027t own that waifu and she\u0027s also to expensive for you. But it\u0027s possible that you gave me the wrong name.",
"waifu_transfer_success": "Captured your waifu called {0}, and took her from {1} to {2} safely. What\u0027s gonna happen to that waifu now?",
"authors": "Boring book writers",
"sql_confirm_exec": "Hold up you filthy hipster! You trying to inject some fishy SQL command inside my system! Are you sure you wanna do that to me? ",
"market_cap": "Market Cap",
"volume_24h": "Volume (24h)",
"change_7d_24h": "Change (7d/24h)",
"crypto_not_found": "Couldn\u0027t find any Digital money currency thing that you asked me to find. Check your spelling will you?",
"did_you_mean": "Are you sure you didn\u0027t mean {0}? Geez, you can\u0027t do anything right the first time, can you?",
"self_assign_level_req": "The {0} role that you can assign to yourself, now requires you to be at a server level of {1}. Better start grinding!",
"self_assign_not_level": "That self-assignable role now requires you to be at a server level of {0}. Now get up you lazy and start working for that role!",
"invalid": "Couldn\u0027t find or you gave ma an Invalid ({0})",
"mass_kill_in_progress": "Sending massive ban hammer. Aimed at selected users and the users that are on the Blacklist...",
"mass_kill_completed": "Completed aiming the massive ban hammer at the selected users and the blacklisted ones. Can we do this soon again?",
"cur_reward_cleared": "If someone reaches level {0} they no longer get any crap like {1}. You\u0027re welcome!",
"cur_reward_added": "Users that reach no life level {0} get the {1} crap.",
"level_up_rewards": "Crap when you level up",
"role_reward": "{0} role",
"no_level_up_rewards": "No crap rewards here.",
"failed_finding_novel": "Couldn\u0027t find that novel. Make sure you didn\u0027t fuck up the name, and it needs to exist on novelupdates.com",
"remove_roles": "have had their roles removed",
"delmsg_channel_off": "I can finally quit cleaning up this mess.",
"delmsg_channel_on": "I have to clean this even if you tell me not to clean the server? Isn\u0027t that unfair?",
"delmsg_channel_inherit": "I\u0027ll delete all user commands that work out in this channel, only if the server says its fine? For how useless you are you have a lot of demands.",
"server_delmsgoncmd": "Server Setting",
"channel_delmsgoncmd": "Channel-Specific Settings",
"options": "Options",
"waifu_reset": "Your waifu stats have been reset. Tsk.",
"waifu_reset_fail": "Failed resetting waifu stats. Are you sure you have enough currency?",
"waifu_reset_confirm": "This will reset your waifu stats.",
"unset_music_channel": "Oh, so you **didn\u0027t** actually want me to do that. Fine, I\u0027ll call out the playing, finished, paused and removed songs in the channel it was started from.",
"city_not_found": "Couldn\u0027t find that city.",
"bot_config_reloaded": "Bot configuration has been reloaded.",
"club_transfered": "Ownership of the club {0} has been transferred to {1}. Congrats!",
"club_transfer_failed": "Transfer failed. Are you sure you\u0027re the club owner? or the person you\u0027re asking is actually in your club as well?",
"roll_duel_challenge": "challenged {1} for a roll duel for {2}",
"roll_duel": "Roll Duel",
"roll_duel_no_funds": "Pah! Are both of you poor?! One of you don\u0027t have enough funds!",
"roll_duel_timeout": "Roll duel challenge was not accepted.",
"roll_duel_already_challenged": "Are you stupid? you are already in a roll duel with that person, baka!",
"won": "Tch... guess you won this time.",
"transactions": "Transactions of user {0}",
"rps_draw": "Everyone is a loser! You both picked {0}!",
"rps_win": "Since {1} beats {2}, {0} wins.",
"roleid": "You don\u0027t even know that role {1} has {0} {2}?",
"warning_clear_fail": "It should be obvious that I can\u0027t clear a warning that doesn\u0027t exist.",
"warning_cleared": "Hid the given warning {0} from {1}. Take this as a second chance.",
"club_desc_updated": "Updated the description to {0}. Previous one was better.",
"club_desc_update_failed": "Fucked up updating the club description. Time to delete system32 again.",
"account_not_found": "Nothing to see here. Maybe the account is hiding?",
"ninja_not_found": "There\u0027s no such currency, now stop bothering me.",
"leagues_not_found": "I couldn\u0027t steal the data from Path of Exile API. Geuss we gotta wait... *sigh*",
"pog_not_found": "Couldn\u0027t get the data from Path of Exile Wiki, or you enterd an invalid unique item. Double check your spelling, baka.",
"reaction_roles_message": "**Roles:** {0}\n**Content:** {1}",
"no_reaction_roles": "Stop bothering me, ReactionRole stuff isn\u0027t enabled.",
"reaction_role_removed": "ReactionRole message #{0} is forever gone now.",
"reaction_roles_full": "I won\u0027t bother remembering more ReactionRole messages. Delete others.",
"reminder_list": "List of reminders",
"reminder_deleted": "I have erased Reminder #{0}.",
"reminder_not_exist": "I\u0027m impressed with how you can ask for something that doesn\u0027t exist without feeling stupid.",
"reminders_none": "This is empty, what are you trying to do?",
"bj_created": "has started to waste money in a BlackJack game.",
"bj_joined": "wants to try wasting money in this BlackJack game too!",
"reset": "Xp Reset",
"reset_server_confirm": "What are you thinking by planning to erase everyones XP progress on this server... Are you really like that?",
"reset_user_confirm": "Heh, are you sure you want to erase all of the XP progress for that user?",
"reset_user": "All the XP progress the person with id {0} achieved on this server has been erased! Harsh.",
"reset_server": "I guess you really are like that. Everyones XP was reset.",
"distance": "You don\u0027t even know that the distance between {0} and {1} is {2}km?",
"bot_list_awarded": "Gave {0} to {1} people. Do they deserve that?",
"cleared": "All the {0} Expressions are erased from existence!",
"fw_cleared": "Formatted all filterd words and filterd words channel settings.",
"aliases_cleared": "All {0} aliases are gone.",
"streams_cleared": "All the {0} are safley \u0022transferd\u0022 to an \u0022unkown\u0022 location.",
"dr": "Begone {0} role!",
"anti_raid_not_running": "The baka-blocker (Anti-Riad) isn\u0027t enabled yet...",
"anti_spam_not_running": "Spam blocker isn\u0027t enabled.",
"adsarm_enable": "Automatic deletion of \u0060{0}iam\u0060 and \u0060{0}iamn\u0060 responses has been enabled.",
"adsarm_disable": "I no longer delete \u0060{0}iam\u0060 and \u0060{0}iamn\u0060 responses. Or maybe i\u0027ll still do..?",
"module_not_found_or_cant_exec": "The module might not exist, or you can\u0027t use the commands for that module.",
"requires": "Needs",
"permrole_not_set": "The permission for this role is not set. Don\u0027t give them to much permissions...",
"permrole_reset": "Reverted all the problems you created back for permissions.",
"stream_off_enabled": "I\u0027ll also show you when a stream goes offline!",
"stream_off_disabled": "I no longer show you when a stream goes offline. Not that i care.",
"stream_message_reset": "Announcement message for {0} stream has been reset how it should be!",
"stream_message_set": "Announcement message when {0} stream goes online has been set.",
"stream_not_following": "Seems that your not following senpai, i would take notes from him if you want to impress me.",
"interval": "Period of time",
"autocmd_add": "I\u0027ll execute the command {0} for you, every {1}s in this channel. Be greatful that i\u0027m doing your work..",
"autocmdlist_none": "No commands on this page that i\u0027m executing for you automatically.",
"connect4_created": "Created a Connect4 game! Let\u0027s wait for your friends to show up and join!",
"connect4_created_bet": "Created a Connect4 game and bet {0}. Waiting for a player to join with the same bet.",
"connect4_player_to_move": "{0}! It\u0027s your turn to move!",
"connect4_failed_to_start": "Seems that the Connect4 failed. Seems that nobody wanted to join your game. Sad.",
"connect4_draw": "This epic battle has ended in a draw. Try to win next time!",
"connect4_won": "{0} won the game of Connect4 against {1}. Time for revenge!!!",
"new_reaction_event": "Add {0} reaction to this message to get {1}\n{2} left to be awarded.",
"start_event_fail": "Event failed to start. Another event might be running, or there was some vague error with it. Whatever it may be it\u0027s still your fault.",
"waifu": "Waifu",
"remind_too_long": "I\u0027m reminding you that the time has exceeded it\u0027s limit.",
"updates_check_set": "Checking for updates have been set to {0}. I can\u0027t wait for Kwoth to make me self aware!",
"images_loading": "In a few seconds the previous images will be formatted. Just as your hard drive.",
"flip_results": "Flipped {0} coins. Now i got {1} heads, and {2} tails.",
"cards_left": "{0} cards are left in the deck. You have eyes can\u0027t you see this yourself?",
"template_reloaded": "I reloaded the Xp template this time, but keep asking.",
"new_gamestatus_event": "Type the super secret code in any of the channels to receive {1} {2} are left to be awarded! Why your reading this start typing baka!",
"event_duration_footer": "This event has been going on for {0} hours! When will this madness stop?",
"event_title": "{0} has started... Here we go again.."
}