Compare commits

...

88 Commits

Author SHA1 Message Date
Kwoth
8598419c5f fix: .dmcmd will now correctly block commands in dms, not globally
change: timely will no longer require guild context, as dmcmd .timely will do the same thing
2024-12-13 08:22:13 +00:00
Kwoth
758093eb32 fix: bannersize fixed, honeypot will now put 'Honeypot' as a ban reason 2024-12-12 08:56:45 +00:00
Kwoth
f44dd03f1a fix: .sclr will now correctly show the color without alpha prefixed 2024-12-11 23:06:01 +00:00
Kwoth
8ac5ec9f57 fix: .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-11 23:00:59 +00:00
Kwoth
5209ba802a change: winlb now has 9 items per page to look not broken 2024-12-11 17:21:25 +00:00
Kwoth
adfce6670c change: winlb now has a title 2024-12-11 17:13:59 +00:00
Kwoth
f8fbc71985 change: added role icon to .inrole, .winlb will now show userids when user can't be found 2024-12-10 14:16:24 +00:00
Kwoth
d4e2516a17 change: winlb embed fields are now inline to use less space 2024-12-10 11:24:02 +00:00
Kwoth
f62a67e2e6 docs: Updated commandlist 2024-12-10 07:20:47 +00:00
Kwoth
7895b5e702 fix: .translate will now use 2 embeds 2024-12-10 07:17:49 +00:00
Kwoth
28c8ccfb5f fix: .sar ex had an outdated description 2024-12-10 07:12:10 +00:00
Kwoth
79026e0c27 fix: .winlb looks better when there are no items 2024-12-09 17:31:23 +00:00
Kwoth
8b228b840a docs: slight docs improvement, added windows from source guide in the menu, separated it from the windows installer guide 2024-12-09 14:54:52 +00:00
Kwoth
f61c1a159d docs: fixed docs 2024-12-09 14:43:55 +00:00
Kwoth
2cdcdb2b23 dev: Fix tests 2024-12-07 16:54:18 +00:00
Kwoth
8d0a3ecb20 docs: Updated changelog.md 2024-12-07 16:51:02 +00:00
Kwoth
97f1405a94 add: added addrolereward and removerolereward events for .notify
add: added .notify with no params showing events with descriptions
add: .winlb
docs: updated docs
dev: updated discord.net, redid migrations
2024-12-07 16:46:06 +00:00
Kwoth
0622236523 add: Added .notify and migrations, added levelup and protection events for notify, removed xpnotify completely 2024-12-05 14:35:42 +00:00
Kwoth
0f240925e8 add: notify, minesweeper, migrations
dev: renames, refactors
change: remind optimized wait
2024-12-03 14:13:31 +00:00
Kwoth
d583e2b99a change: QUeueing a song after the queue is finished will restart the playback 2024-12-02 03:21:43 +00:00
Kwoth
2d72f6f498 add: Added .dmmod and .dmcmd to disable modules and commands in bot DMs 2024-12-02 02:20:33 +00:00
Kwoth
5143e42dff add: Added .xplevelset
remove: removed awardedxp from database.
change: .sclr show will now show hex
change: .awardxp will now add directly to user's real xp
2024-12-01 03:57:31 +00:00
Kwoth
790c36df8d fix: .setstream and .setactivity will now pause .ropl 2024-11-30 10:02:08 +00:00
Kwoth
15709bc8fb change: Finished giveaway will now reply to the giveaway message and ping a winner 2024-11-29 08:52:47 +00:00
Kwoth
f280f72227 fix: fixed giveaway load broken in the last patch 2024-11-29 08:50:55 +00:00
Kwoth
4069368beb change: added ending date for givaway as a timestamp tag
fixed: fixed an issue with flag translates
2024-11-28 08:55:53 +00:00
Kwoth
e036a2d3c9 fix: fixed .sclr again, again, and fixed .iamn 2024-11-27 03:26:57 +00:00
Kwoth
62a16f3faf fix: fixed .iam
fix: fixed .sclr not being respected on many different commands
change: .rps now also has the amount bet
2024-11-27 02:41:05 +00:00
Kwoth
231451f978 fix: fixed .sclr again 2024-11-27 01:28:17 +00:00
Kwoth
2bed4e7eac fix: Fixed many issues with 5.2.0 2024-11-27 01:23:38 +00:00
Kwoth
22eea3fa7b docs: updated changelog 2024-11-26 23:49:32 +00:00
Kwoth
8b1767078e fix: fixed sar migration 2024-11-26 23:31:27 +00:00
Kwoth
6654ccdad5 docs: updated commandlist 2024-11-26 20:56:49 +00:00
Kwoth
99827b0fa1 change: MessageXpCooldown is not in seconds 2024-11-26 19:58:20 +00:00
Kwoth
0451551ddf change: .gc will now remove previous plants with the same pw length from the channel and add them to itself. This is to avoid missed gcs. The amount text will be wrong however, as it will only show how much flowers spawned now. The user will get full amount of all gcs previously 2024-11-26 17:02:09 +00:00
Kwoth
75d0eb631d change: gambling commands now show amount bet. Slightly changed the layout. Updated some gambling strings
add: added .btr excl
2024-11-26 04:50:56 +00:00
Kwoth
96ce7e23bb Merge branch '520' into v5 2024-11-21 22:28:08 +00:00
Kwoth
aac475be62 fix: fixed time conversion 2024-11-21 19:02:22 +00:00
Kwoth
55fae53f6d fix: fixed time conversion 2024-11-21 19:00:36 +00:00
Kwoth
879f196f8b dev: new method in rotating service 2024-11-21 18:59:31 +00:00
Kwoth
796086538a .btr and .sclr added, cleanup 2024-11-20 17:16:41 +00:00
Kwoth
3178074828 fix: fixed .sinfo for guilds on other shards 2024-11-19 18:03:12 +00:00
Kwoth
0a9d53d380 change: sar rework, improved 2024-11-17 23:36:11 +00:00
Kwoth
d7fbf44d53 fix: re-added a missing extension method 2024-11-17 19:32:41 +00:00
Kwoth
cdd07d0559 fix: fixed accidental rename in previous patch 2024-11-17 19:17:04 +00:00
Kwoth
80a41c1f38 fix: fixed nullref in blacklist 2024-11-17 19:14:40 +00:00
Kwoth
1631394638 change: .bsreset price reduced 10x 2024-11-12 10:48:03 +00:00
Kwoth
7eb4895570 fix: fixed update usernames query 2024-11-12 01:09:06 +00:00
Kwoth
9b55028ba6 fix: fixed command description for betroll 2024-11-10 23:03:04 +00:00
Kwoth
53363d2840 fix: fixed newline missing in .timely 2024-11-10 22:31:48 +00:00
Kwoth
ca5a870bf8 docs: updated changelog 2024-11-10 16:08:26 +00:00
Kwoth
7f5e065c4d docs: updated commandlist, version upped to 5.1.20 2024-11-10 15:56:43 +00:00
Kwoth
86b214163a fix: typo 2024-11-10 12:28:06 +00:00
Kwoth
9dbb08d85f add: added .snipe command
add: added .gsreset and .bsreset commands
change: improved .timely rewards for patrons
dev: Improved how blacklist works under the hood
2024-11-08 18:24:50 +00:00
Kwoth
89ca56c77c fix: rich logging fix for userids, updated commandlist
add: added bot.date and changed bot.time placeholder. They use timestamp tags now.
fix: fixed double log on server leave
2024-11-07 05:38:22 +00:00
Kwoth
9e96679099 change: patreon reward bonuses increased slightly 2024-11-06 07:10:12 +00:00
Kwoth
1280d2b397 change: .divorce no longer has a cooldown
add: Added .waifuclaims / .claims command which lists your waifus (name, price and ids)
change: Timely now shows patreon multiplier bonus if there is any, (alongside boost)
2024-11-06 05:20:04 +00:00
Kwoth
c731127607 fix: missing example in commands 2024-11-05 09:40:01 +00:00
Kwoth
c15930306a fix: fix pipeline, missing aliases 2024-11-05 09:34:01 +00:00
Kwoth
701501d678 add: added .rakeback to get a part of the house edge back. Rakeback is accumulated by betting (not winning or losing in particular). All games have manually specified rakeback values
add: slot now has 1 more icon (wheat!), and multipliers have been modified to even out the gains
change: betroll is improved (around 2% better payout), as 66 is now a winning number, not a losing one
2024-11-05 08:24:21 +00:00
Kwoth
fa12fcea58 change: .race will now have 82-94% payout rate based on the number of players playign (1-12, x0.01 per player). Any player over 12 won't increase payout 2024-11-05 04:27:27 +00:00
Kwoth
274219c40b docs: Upped version to 5.1.19, updated changelog
fix: Fixed timely on different shards
2024-11-05 02:57:01 +00:00
Kwoth
96c9b47da2 add: timely now has an option in gambling whether to use no protection, captcha, or button.
fix: grpc api fix for dashy
2024-11-04 14:35:59 +00:00
Kwoth
b5d1469df1 Merge branch 'v5' of https://gitlab.com/kwoth/nadekobot into v5 2024-11-04 12:28:52 +00:00
Kwoth
d7747bd25a fix: timely fixes 2024-11-04 12:28:42 +00:00
Kwoth
7d162d1f04 fix: timely fixes 2024-11-04 12:28:01 +00:00
Kwoth
704d061d46 fix: fixed pipeline, added missing strings 2024-11-04 10:58:44 +00:00
Kwoth
c39c9061fd add: added timely boost bonus to gambling.yml
change: .betstats renamed to .gamblestats/.gs
add: added .betstats, .betstats <game> and .betstats <user> <game?> command which shows you your stats for gambling commands
2024-11-04 10:42:05 +00:00
Kwoth
619ddba4f8 fix: fixed pagination numbers in xplb and xpglb 2024-11-04 02:03:53 +00:00
Kwoth
3acef04b32 change: strikeout slightly thinner to make password easier to read on plants 2024-11-03 13:45:12 +00:00
Kwoth
83a1d959b1 fix: Added nordic and ugro finnic languages to flag translate 2024-11-03 12:05:51 +00:00
Kwoth
a1632722bc fix: fix timely 2024-11-03 09:32:43 +00:00
Kwoth
ee0a28afab fix: revert patron migration temporarily as ef core is bugging out hard 2024-11-03 08:39:37 +00:00
Kwoth
2b301c0aab fix: possible fix for patron table 2024-11-03 08:31:23 +00:00
Kwoth
b6b6b4e19e fix: Fixed UserId patron table error
fix: Added au and kz countries as en and kz languages respectively
fix: Strikeout is thinner now on plants
2024-11-03 08:24:00 +00:00
Kwoth
32fc8b6e03 docs: Upped version to 5.1.18, updated changelog 2024-11-03 03:44:33 +00:00
Kwoth
297e2fde0e change: timely 'password' is now a button 2024-11-03 03:41:34 +00:00
Kwoth
729f26caab button for timely 2024-11-03 02:48:39 +00:00
Kwoth
4b12e4e923 dev: Removed discrim from the database
add: .translateflags command
add: captcha to timely, configurable in .conf gambling
change: change bonuses for patreon rewards
fix: nunchi message color fix
2024-11-02 16:23:58 +00:00
Kwoth
12f4ce7f2a change: animal race will update more frequently, but animals will move slightly slower. Overall everything will be slightly faster 2024-11-01 04:53:20 +00:00
Kwoth
00944e08c3 fix: .ncs will now show an error if setting a pixel fails 2024-11-01 04:52:29 +00:00
Kwoth
569abd7194 api: work on server xp api 2024-10-31 11:48:31 +00:00
Kwoth
474a1db41d add: timely now has a 3 letter password by default. Configurable via .conf gamb 2024-10-31 11:48:09 +00:00
Kwoth
0f6255947e fix: fixed ubl pagination 2024-10-30 13:16:04 +00:00
Kwoth
f68f219a25 fix: ytdataapiv3 searches will no longer duplicate youtube urls 2024-10-30 07:02:13 +00:00
Kwoth
8f16b11d02 api: finance api implementation 2024-10-29 08:15:53 +00:00
Kwoth
df5eced904 change: Error sending greet dm will now be a warning
change: initial canvas price down to 3 from 10, 10 is way too expensive
2024-10-29 01:53:42 +00:00
Kwoth
1dcd158f43 fix: Bot will now not accept .aar Role if that Role is higher than or equal to bot's role. Previously bot would just fail silently, now there is a proper error message. 2024-10-28 21:42:05 +00:00
250 changed files with 51999 additions and 2720 deletions

View File

@@ -2,6 +2,221 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [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] - 27.11.2024
## Fixed
- More fixes for .sclr
- `.iamn` fixed
## [5.2.3] - 27.11.2024
## Fixed
- `.iam` Fixed
- `.sclr` will now properly change color on many commands it didn't work previously
### Changed
- `.rps` now also has bet amount in the result, like other gambling commands
## [5.2.2] - 27.11.2024
### Changed
- Button roles are now non-exclusive by default
### Fixed
- Fixed sar migration, again (this time correctly)
- Fixed `.sclr` not updating unless bot is restarted, the changes should be immediate now for warn and error
- Fixed group buttons exclusivity message always saying groups are exclusive
## [5.2.1] - 26.11.2024
### Fixed
- Fixed old self assigned missing
## [5.2.0] - 26.11.2024
### Added
- Added `.todo undone` command to unmark a todo as done
- Added Button Roles!
- `.btr a` to add a button role to the specified message
- `.btr list` to list all button roles on the server
- `.btr rm` to remove a button role from the specified message
- `.btr rma` to remove all button roles on the specified message
- `.btr excl` to toggle exclusive button roles (only 1 role per message or any number)
- Use `.h btr` for more info
- Added `.wrongsong` which will delete the last queued song.
- Useful in case you made a mistake, or the bot queued a wrong song
- It will reset after a shuffle or fairplay toggle, or similar events.
- Added Server color Commands!
- Every Server can now set their own colors for ok/error/pending embed (the default green/red/yellow color on the
left side of the message the bot sends)
- Use `.h .sclr` to see the list of commands
- `.sclr show` will show the current server colors
- `.sclr ok <color hex>` to set ok color
- `.sclr warn <color hex>` to set warn color
- `.sclr error <color hex>` to set error color
### Changed
- Self Assigned Roles reworked! Use `.h .sar` for the list of commands
- `.sar autodel`
- Toggles the automatic deletion of the user's message and Nadeko's confirmations for .iam and .iamn commands.
- `.sar ad`
- Adds a role to the list of self-assignable roles. You can also specify a group.
- If 'Exclusive self-assignable roles' feature is enabled (.sar exclusive), users will be able to pick one role
per group.
- `.sar groupname`
- Sets a self assignable role group name. Provide no name to remove.
- `.sar remove`
- Removes a specified role from the list of self-assignable roles.
- `.sar list`
- Lists self-assignable roles. Shows 20 roles per page.
- `.sar exclusive`
- Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role
per group.
- `.sar rolelvlreq`
- Set a level requirement on a self-assignable role.
- `.sar grouprolereq`
- Set a role that users have to have in order to assign a self-assignable role from the specified group.
- `.sar groupdelete`
- Deletes a self-assignable role group
- `.iam` and `.iamn` are unchanged
- Removed patron limits from Reaction Roles. Anyone can have as many reros as they like.
- `.timely` captcha made stronger and cached per user.
- `.bsreset` price reduced by 90%
### Fixed
- Fixed `.sinfo` for servers on other shard
## [5.1.20] - 10.11.2024
### 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 `.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
- Added and changed `%bot.time%` and `%bot.date%` placeholders. They use timestamp tags now
### Changed
- `.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)
## [5.1.19] - 04.11.2024
### Added
- Added `.betstats`
- See your own stats with .betstats
- Target someone else: .betstats @seraphe
- You can also specify a game .betstats lula
- Or both! .betstats seraphe br
- `.timely` can now have a server boost bonus
- Configure server ids and reward amount in data/gambling.yml
- anyone who boosts one of the sepcified servers gets the amount as base timely bonus
### Changed
- `.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
### Fixed
- `.xplb` and `.xpglb` now have proper ranks after page 1
- Fixed boost bonus on shards different than the specified servers' shard
## [5.1.18] - 02.11.2024
### Added
- Added `.translateflags` / `.trfl` command.
- Enable on a per-channel basis.
- Reacting on any message in that channel with a flag emoji will post the translation of that message in the
language of that country
- 5 second cooldown per user
- The message can only be translated once per language (counter resets every 24h)
- `.timely` now has a button. Togglable via `.conf gambling` it's called pass because previously it was a captcha, but
captchas are too annoying
## Changed
- [public bot] Patreon reward bonus for flowers reduced. Timely bonuses stay the same
- discriminators removed from the databases. All users who had ???? as discriminator have been renamed to ??username.
- all new unknown users will have ??Unknown as their name
- Flower currency generation will now have a strikeout to try combat the pickbots. This is the weakest but easiest
protection to implement. There may be more options in the future
## Fixed
- nunchi join game message is now ok color instead of error color
## [5.1.17] - 29.10.2024
### Fixed
- fix: Bot will now not accept .aar Role if that Role is higher than or equal to bot's role. Previously bot would just
fail silently, now there is a proper error message.
## [5.1.16] - 28.10.2024
## Added
@@ -70,7 +285,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Added
- Added `%user.displayname%` placeholder. It will show users nickname, if there is one, otherwise it will show the username.
- Added `%user.displayname%` placeholder. It will show users nickname, if there is one, otherwise it will show the
username.
- Nickname won't be shown in bye messages.
- Added initial version of grpc api. Beta
@@ -103,17 +319,20 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Fixed `.greettest`, and other `.*test` commands if you didn't have them enabled.
- Fixed `.greetdmtest` sending messages twice.
- Fixed a serious bug which caused greet messages to be jumbled up, and wrong ones to be sent for the wrong events.
- There is no database issue, all greet messages are safe, the cache was caching any setting every 3 seconds with no regard for the type of the event
- There is no database issue, all greet messages are safe, the cache was caching any setting every 3 seconds with no
regard for the type of the event
- This also caused `.greetdm` messages to not be sent if `.greet` is enabled
- This bug was introduced in 5.1.8. PLEASE UPDATE if you are on 5.1.8
- Selfhosters only: Fixed medusa dependency loading
- Note: Make sure to not publish any other DLLs besides the ones you are sure you will need, as there can be version conflicts which didn't happen before.
- Note: Make sure to not publish any other DLLs besides the ones you are sure you will need, as there can be version
conflicts which didn't happen before.
## [5.1.8] - 19.09.2024
### Added
- Added `.leaveunkeptservers` which will make the bot leave all servers on all shards whose owners didn't run `.keep` command.
- Added `.leaveunkeptservers` which will make the bot leave all servers on all shards whose owners didn't run `.keep`
command.
- This is a dangerous and irreversible command, don't use it. Meant for use on the public bot.
- `.adpl` now supports custom statuses (you no longer need to specify Playing, Watching, etc...)
@@ -125,7 +344,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- `.quotesearch` / `.qse` is now paginated for easier searching
- `.whosplaying` is now paginated
- `.img` is now paginated
- `.setgame` renamed to`.setactivity` and now supports custom text activity. You don't have to specify playing, listening etc before the activity
- `.setgame` renamed to`.setactivity` and now supports custom text activity. You don't have to specify playing,
listening etc before the activity
- Clarified and added some embed / placeholder links to command help where needed
- dev: A lot of code cleanup and internal improvements
@@ -166,6 +386,7 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Possible fix for `.remind` timestamp
### Removed
- Removed old bloat / semi broken / dumb commands
- `.memelist` / `.memegen` (too inconvenient to use)
- `.activity` (useless owner-only command)
@@ -192,7 +413,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Updated some bet descriptions to include 'all' 'half' usage instructions
- Updated some command strings
- dev: Vastly simplified medusa creation using dotnet templates, docs updated
- Slight refactor of .wiki, time, .catfact, .wikia, .define, .bible and .quran commands, no significant change in functionality
- Slight refactor of .wiki, time, .catfact, .wikia, .define, .bible and .quran commands, no significant change in
functionality
### Fixed
@@ -200,6 +422,7 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- You can once again disable cleverbot responses using fake 'cleverbot:response' module name in permission commands
### Removed
- Removed .rip command
## [5.1.4] - 13.07.2024
@@ -208,8 +431,10 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Added `.coins` command which lists top 10 cryptos ordered by marketcap
- Added Clubs rank in the leaderboard to `.clubinfo`
- Bot owners can now check other people's bank balance (Not server owners, only bot owner, the person who is hosting the bot)
- You can now send multiple waifu gifts at once to waifus. For example `.waifugift 3xRose @user` will give that user 3 roses
- Bot owners can now check other people's bank balance (Not server owners, only bot owner, the person who is hosting the
bot)
- You can now send multiple waifu gifts at once to waifus. For example `.waifugift 3xRose @user` will give that user 3
roses
- The format is `<NUMBER>x<ITEM>`, no spaces
- Added `.boosttest` command
- Added support for any openai compatible api for the chatterbot feature change:
@@ -235,7 +460,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Changed
- Replying to the bot's message in the channel where chatterbot is enabled will also trigger the ai response, as if you pinged the bot. This only works for chatterbot, but not for nadeko ai command prompts
- Replying to the bot's message in the channel where chatterbot is enabled will also trigger the ai response, as if you
pinged the bot. This only works for chatterbot, but not for nadeko ai command prompts
### Fixed
@@ -268,28 +494,33 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Added
- Added `.prompt` command, Nadeko Ai Assistant
- You can send natural language questions, queries or execute commands. For example "@Nadeko how's the weather in paris" and it will return `.we Paris` and run it for you.
- In case the bot can't execute a command using your query, It will fall back to your chatter bot, in case you have it enabled in data/games.yml. (Cleverbot or chatgpt)
- You can send natural language questions, queries or execute commands. For example "@Nadeko how's the weather in
paris" and it will return `.we Paris` and run it for you.
- In case the bot can't execute a command using your query, It will fall back to your chatter bot, in case you have
it enabled in data/games.yml. (Cleverbot or chatgpt)
- (It's far from perfect so please don't ask the bot to do dangerous things like banning or pruning)
- Requires Patreon subscription, after which you'll be able to run it on global @Nadeko bot.
- Selfhosters: If you're selfhosting, you also will need to acquire the api key from <https://dashy.nadeko.bot/me> after pledging on patreon and put it in nadekoAiToken in creds.yml
- Selfhosters: If you're selfhosting, you also will need to acquire the api key
from <https://dashy.nadeko.bot/me> after pledging on patreon and put it in nadekoAiToken in creds.yml
- Added support for `gpt-4o` in `data/games.yml`
### Changed
- Remind will now show a timestamp tag for durations
- Only `Gpt35Turbo` and `Gpt4o` are valid inputs in games.yml now
- `data/patron.yml` changed. It now has limits. The entire feature limit system has been reworked. Your previous settings will be reset
- `data/patron.yml` changed. It now has limits. The entire feature limit system has been reworked. Your previous
settings will be reset
- A lot of updates to bot strings (thanks Ene)
- Improved cleanup command to delete a lot more data once cleanup is ran, not only guild configs (please don't use this command unless you have your database bakced up and you know 100% what you're doing)
- Improved cleanup command to delete a lot more data once cleanup is ran, not only guild configs (please don't use this
command unless you have your database bakced up and you know 100% what you're doing)
### Fixed
- Fixed xp bg buy button not working, and possibly some other buttons too
- Fixed shopbuy %user% placeholders and updated help text
- All .feed overloads should now work"
- `.xpexclude` should will now work with forums too. If you exclude a forum you won't be able to gain xp in any of the threads.
- `.xpexclude` should will now work with forums too. If you exclude a forum you won't be able to gain xp in any of the
threads.
- Fixed remind not showing correct time (thx cata)
### Removed
@@ -303,8 +534,10 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Added `.setserverbanner` and `.setservericon` commands (thx cata)
- Added overloads section to `.h command` which will show you all versions of command usage with param names
- You can now check commands for submodules, for example `.cmds SelfAssignedRoles` will show brief help for each of the commands in that submodule
- Added dropdown menus for .mdls and .cmds (both module and group versions) which will give you the option to see more detailed help for each specific module, group or command respectively
- You can now check commands for submodules, for example `.cmds SelfAssignedRoles` will show brief help for each of the
commands in that submodule
- Added dropdown menus for .mdls and .cmds (both module and group versions) which will give you the option to see more
detailed help for each specific module, group or command respectively
- Self-Hosters only:
- Added a dangerous cleanup command that you don't have to know about
@@ -323,7 +556,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Fixed
- `.streammessage` will once again be able to mention anyone (as long as the user setting the message has the permission to mention everyone)
- `.streammessage` will once again be able to mention anyone (as long as the user setting the message has the permission
to mention everyone)
- `.streammsgall` fixed
- `.xplb` and `.xpglb` pagination fixed
- Fixed page number when the total number of elements is unknown
@@ -352,15 +586,19 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Added
- Added `.shopadd command` You can now sell commands in the shop. The command will execute as if you were the one running it when someone buys it
- Added `.shopadd command` You can now sell commands in the shop. The command will execute as if you were the one
running it when someone buys it
- type `.h .shopadd` for more info
- Added `.stickyroles` Users leaving the server will have their roles saved to the database and reapplied if they rejoin within 30 days.
- Added `.stickyroles` Users leaving the server will have their roles saved to the database and reapplied if they rejoin
within 30 days.
- Giveaway commands
- `.ga start <duration> <text>` starts the giveway with the specified duration and message (prize). You may have up to 5 giveaways on the server at once
- `.ga start <duration> <text>` starts the giveway with the specified duration and message (prize). You may have up
to 5 giveaways on the server at once
- `.ga end <id>` prematurely ends the giveaway and selects a winner
- `.ga cancel <id>` cancels the giveaway and doesn't select a winner
- `.ga list` lists active giveaways on the current server
- `.ga reroll <id>` rerolls the winner on the completed giveaway. This only works for 24 hours after the giveaway has ended, or until the bot restarts.
- `.ga reroll <id>` rerolls the winner on the completed giveaway. This only works for 24 hours after the giveaway
has ended, or until the bot restarts.
- Users can join the giveaway by adding a :tada: reaction
- Added Todo Commands
- `.todo add <name>` - adds a new todo
@@ -370,7 +608,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- `.todo edit <id> <new message>` - edits a todo item message
- `.todo show <id>` - Shows the text of the specified todo item
- In addition to that, there are also Todo archive commands
- `.todo archive add <name>` - adds all current todos (completed and not completed) to the archived list, your current todo list will become cleared
- `.todo archive add <name>` - adds all current todos (completed and not completed) to the archived list, your
current todo list will become cleared
- `.todo archive list` - lists all your archived todo lists
- `.todo archive show <id>` - shows the todo items from one of your archived lists
- `.todo archive delete <id>` - deletes and archived todo list
@@ -389,8 +628,10 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Changed
- Users who have manage messages perm in the channel will now be excluded from link and invite filtering (`.sfi` and `.sfl`)
- `.send` command should work consistently and correctly now. You can have targets from other shards too. The usage has been changed. refer to `.h .send` for more info
- Users who have manage messages perm in the channel will now be excluded from link and invite filtering (`.sfi`
and `.sfl`)
- `.send` command should work consistently and correctly now. You can have targets from other shards too. The usage has
been changed. refer to `.h .send` for more info
- `.serverinfo` no longer takes a server name. It only takes an id or no arguments
- You can now target a different channel with .repeat
- `.cmds <module name>`, `.cmds <group name` and `.mdls` looks better
@@ -402,7 +643,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Fixed
- `.feed` should now correctly accept (and show) the message which can be passed as the third parameter
- `.say` will now correctly report errors if the user or the bot don't have sufficent perms to send a message in the targeted channel
- `.say` will now correctly report errors if the user or the bot don't have sufficent perms to send a message in the
targeted channel
- Fixed `.invitelist` not paginating correctly
- `.serverinfo` will now correctly work for other shards
- `.send` will now correctly work for other shards
@@ -420,15 +662,17 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
## [4.3.22] - 23.04.2024
### Added
- Added `.setbanner` command (thx cata)
### Fixed
- Fixed pagination error due to a missing emoji
- Fixed pagination error due to a missing emoji
## [4.3.21] - 19.04.2024
### Fixed
- Possible fix for a duplicate in `.h bank`
- Fixed `.stock` command
- Fixed `.clubapply` and `.clubaccept`
@@ -437,14 +681,17 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
## [4.3.20] - 20.01.2024
### Fixed
- Fixed `.config searches followedStreams.maxCount` not working
## [4.3.19] - 20.01.2024
### Added
- Added `followedStreams.maxCount` to `searches.yml` which lets bot owners change the default of 10 per server
### Changed
- Improvements to GPT ChatterBot (thx alexandra)
- Add a personality prompt to tweak the way chatgpt bot behaves
- Added Chat history support to chatgpt ChatterBot
@@ -467,7 +714,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Fixed `icon_url` when using `.showembed`
- Fixed `.quoteshow` not showing sometimes (thx Cata)
- Notifications will no longer be sent if dms are off when using `.give`
- Users should no longer be able to apply to clubs while in a club already (especially not to the same club they're already in)
- Users should no longer be able to apply to clubs while in a club already (especially not to the same club they're
already in)
### Removed
@@ -566,10 +814,12 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Changed
- .meload and .meunload are now case sensitive. Previously loaded medusae may need to be reloaded or data/medusae/medusa.yml may need to be edited manually
- .meload and .meunload are now case sensitive. Previously loaded medusae may need to be reloaded or
data/medusae/medusa.yml may need to be edited manually
- Several club related command have their error messages improved
- Updated help text for .antispam and .antiraid
- You can now specify time and date (time is optional) in `.remind` command instead of relative time, in the format `HH:mm dd.MM.YYYY`
- You can now specify time and date (time is optional) in `.remind` command instead of relative time, in the
format `HH:mm dd.MM.YYYY`
- OwnerId will be automatically added to `creds.yml` at bot startup if it's missing
### Fixed
@@ -629,13 +879,15 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Added
- Added `.exprdelserv` (.exds) to completement .exas. Deletes an expression on the current server and is susceptible to .dpo, unlike .exd
- Added `.exprdelserv` (.exds) to completement .exas. Deletes an expression on the current server and is susceptible to
.dpo, unlike .exd
- Added `.shopreq` which lets you set role requirement for specific shop items
- Added `.shopbuy` alias to `.buy`
### Fixed
- Fixed `.convertlist` showing currencies twice (this may not apply to existing users and it may require you to manually remove all currencies from units.json)
- Fixed `.convertlist` showing currencies twice (this may not apply to existing users and it may require you to manually
remove all currencies from units.json)
### Removed
@@ -645,9 +897,12 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Added
- Added `.expraddserver` (.exas) which will server as a server-only alternative to '.exa' in case users want to override default Admin permissions with .dpo
- Added .banprune command which sets how many days worth of messages will be pruned when bot (soft)bans a person either through a command or another punishment feature.
- Added .qdelauth - Delete all quotes by the specified author on this server. If you target yourself - no permission required
- Added `.expraddserver` (.exas) which will server as a server-only alternative to '.exa' in case users want to override
default Admin permissions with .dpo
- Added .banprune command which sets how many days worth of messages will be pruned when bot (soft)bans a person either
through a command or another punishment feature.
- Added .qdelauth - Delete all quotes by the specified author on this server. If you target yourself - no permission
required
- Added `.timeout` command
- Added an option to award currency based on received xp
@@ -705,7 +960,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Possibly fixed .trivia not stopping bug
- Fixed very low payout rate on `.betroll`
- Fixed an issue with youtube song resolver which caused invalid data to be cached
- Added client id to the cache key as a potential fix for VoiceXp 'bug'. The solution may be to use different redis instances for each bot, or to switch from botCache: from 'redis' to 'memory' in creds.yml
- Added client id to the cache key as a potential fix for VoiceXp 'bug'. The solution may be to use different redis
instances for each bot, or to switch from botCache: from 'redis' to 'memory' in creds.yml
- Bot owner should now be able to buy items from the xpshop when patron requirement is set
- Fixed youtube-dl caching invalid data. Please use yt-dlp instead
@@ -737,10 +993,12 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- multipliers < 1 are considered losses, > 1 considered wins
- Added `.betdraw` command which lets you guess red/black and/or high/low for a random card
- They payouts are very good, but seven always loses
- Added `.lula` command. Plays the same as `.wof` but looks much nicer, and is easily customizable from gambling.yml without any changes to the sourcecode needed.
- Added `.lula` command. Plays the same as `.wof` but looks much nicer, and is easily customizable from gambling.yml
without any changes to the sourcecode needed.
- Added `.repeatskip` command which makes the next repeat trigger not post anything
- Added `.linkonly` which will make the bot only allow link posts in the channel. Exclusive with `.imageonly`
- Added release notifications. Bot owners will now receive new release notifications in dms if they have `checkForUpdates` set to `true` in data/bot.yml
- Added release notifications. Bot owners will now receive new release notifications in dms if they
have `checkForUpdates` set to `true` in data/bot.yml
- You can also configure it via `.conf bot checkfor
- updates <true/false>`
- Added `.xpshop` which lets bot owners add xp backgrounds and xp frames for sale by configuring `data/xp.yml`
@@ -808,7 +1066,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Fixed
- Fixed elipsis character issue with aliases/quotes. You should now be able to set an elipsis to be an alias of `.quoteprint`
- Fixed elipsis character issue with aliases/quotes. You should now be able to set an elipsis to be an alias
of `.quoteprint`
## [4.2.13] - 30.06.2022
@@ -874,7 +1133,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
### Fixed
- Fixed `.crypto`, you will still need coinmarketcapApiKey in `creds.yml` in order to make it run consistently as the key is shared
- Fixed `.crypto`, you will still need coinmarketcapApiKey in `creds.yml` in order to make it run consistently as the
key is shared
## [4.2.3] - 17.06.2022
@@ -927,7 +1187,8 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
invidiousInstances: []
```
- Added new properties to `creds.yml`. google -> searchId and google -> searchImageId.
- These properties are used as `cx` (google api query parameter) in case you've setup your `data/searches.yml` to use the official google api.
- These properties are used as `cx` (google api query parameter) in case you've setup your `data/searches.yml` to use
the official google api.
`searchId` is used for web search
`searchimageId` is used for image search
```yml
@@ -938,12 +1199,15 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Check `creds_example.yml` for comments explaining how to obtain them.
#### Patronage system added
- Added `data/patron.yml` for configuration
- Implemented only for patreon so far
- Patreon subscription code completely rewritten
- Users who pledge on patreon get benefits based on the amount they pledged
- Public nadeko only. But selfhosters can adapt it to their own patreon pages by configuring their patreon credentials in `creds.yml` and enabling the system in `data/patron.yml` file.
- Most of the patronage system strings are hardcoded atm, so if you wish to use this system on selfhosts, you will have to modify the source
- Public nadeko only. But selfhosters can adapt it to their own patreon pages by configuring their patreon credentials
in `creds.yml` and enabling the system in `data/patron.yml` file.
- Most of the patronage system strings are hardcoded atm, so if you wish to use this system on selfhosts, you will
have to modify the source
- Pledge amounts are split into tiers. This is not configurable atm.
- Tier I - 1$ - 4.99$ a month
- Tier V - 5$ - 9.99$ a month
@@ -953,12 +1217,14 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except da
- Tier C - 100$+ a month
- Rewards and command quotas for each of the tiers are configurable
- Limitations to certain features are also configurable. ex:
```yml
quotas:
features:
"rero:max_count":
x: 50
```
- ^ this setting would set the maximum number of reaction roles to be 50 for a user who is in Patron Tier X
- Read the comments in the .yml file for (much) more info
- Quota system allows the owner to set up hourly, daily and monthly quota usage for each tier
@@ -969,10 +1235,12 @@ quotas:
- If you're enabling patron system for a selfhost, you will want to edit it
Added `.patron` and `.patronmessage` commands
- `.patron` checks your patronage status, and quotas. Requires patron system to be enabled.
- `.patronmessage` (owner only) sends message to all patrons with the specified tier or higher. Supports embeds
- Added a fake `.cmdcd` command `cleverbot:response` which can be used to limit how often users can talk to the cleverbot.
- Added a fake `.cmdcd` command `cleverbot:response` which can be used to limit how often users can talk to the
cleverbot.
### Changed
@@ -983,26 +1251,34 @@ Added `.patron` and `.patronmessage` commands
- Added interaction explaining selfhosting
- `.google` reimplemented. It now has 2 modes configurable in `data/searches.yml` under the `webSearchengine` property
- If set to `google`, official custom search api will be used. You will need to set googleapikey and google.searchId in `creds.yml`
- if set to `searx` one of the instances specified in the `searxInstances:` property will be randomly chosen for each request
- If set to `google`, official custom search api will be used. You will need to set googleapikey and google.searchId
in `creds.yml`
- if set to `searx` one of the instances specified in the `searxInstances:` property will be randomly chosen for
each request
- instances must have `format=json` allowed (public ones usually don't allow it)
- instances are specified as a fully qualified url, example: `https://my.cool.searx.instance.io`
- `.image` reimplemented. Same as `.google` - it uses either `google` official api (in which case it uses `google.searchImageId` from `creds.yml`) or `searx`
- `.image` reimplemented. Same as `.google` - it uses either `google` official api (in which case it
uses `google.searchImageId` from `creds.yml`) or `searx`
- `.youtube` reimplemented. It will use a `ytProvider:` property from `data/searches.yml` to determine how to retrieve results
- `ytdataapi` will use the official google api (requires `GoogleApiKey` specified in `creds.yml`) and YoutubeDataApi enabled in the dev console
- `ytdl` will use `youtube-dl` program from the host machine. It must be downloaded and it's location must be added to path env variable.
- `.youtube` reimplemented. It will use a `ytProvider:` property from `data/searches.yml` to determine how to retrieve
results
- `ytdataapi` will use the official google api (requires `GoogleApiKey` specified in `creds.yml`) and YoutubeDataApi
enabled in the dev console
- `ytdl` will use `youtube-dl` program from the host machine. It must be downloaded and it's location must be added
to path env variable.
- `ytdlp` will use `yt-dlp` program from the host machine. Same as `youtube-dl` - must be in path env variable.
- `invidious` will use one of invidious instances specified in the `invidiousInstances` property. Very good.
- `.google`, `.youtube` and `.image` moved to the new Search group
Note: Results of each `.youtube` query will be cached for 1 hour to improve perfomance
- Removed 30 second `.ping` ratelimit on public nadeko
- xp image generation changes
- In case you have default settings, your xp image will look slightly different
- If you've modified xp_template.json, your xp image might look broken. Your old template will be saved in xp_template.json.old
- If you've modified xp_template.json, your xp image might look broken. Your old template will be saved in
xp_template.json.old
- Xp number outline is now slightly thicker
- Xp number will now have Center vertical and horizontal alignment
- LastLevelUp no longer supported
@@ -1066,7 +1342,8 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Embed arrays use color hex values instead of an integer
- Old embed format will still work
- There shouldn't be any breaking changes
- Added `.stondel` command which, when toggled, will make the bot delete online stream messages on the server when the stream goes offline
- Added `.stondel` command which, when toggled, will make the bot delete online stream messages on the server when the
stream goes offline
- Added a simple bank system.
- Users can deposit, withdraw and check the balance of their currency in the bank.
- Users can't check other user's bank balances.
@@ -1092,7 +1369,6 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Bot will now support much higher XP values for global and server levels
- [dev] Small change and generation perf improvement for the localized response strings
### Fixed
- Fixed `.deletexp` command
@@ -1111,7 +1387,8 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- NadekoBot now supports mysql, postgresql and sqlite
- To change the db nadeko will use, simply change the `db type` in `creds.yml`
- There is no migration code right now, which means that if you want to switch to another system you'll either have to manually export/import your database or start fresh
- There is no migration code right now, which means that if you want to switch to another system you'll either have
to manually export/import your database or start fresh
- Medusa system
- A massive new feature which allows developers to create custom modules/plugins/cogs
- They can be load/unloaded/updated at runtime without restarting the bot
@@ -1121,13 +1398,15 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Minor club rework
- Clubs names are now case sensitive (owo and OwO can be 2 different clubs)
- Removed discriminators
- Current discriminators which are greater than 1 are appended to clubnames to avoid duplicates, you can rename your club with `.clubrename` to remove it
- Current discriminators which are greater than 1 are appended to clubnames to avoid duplicates, you can rename
your club with `.clubrename` to remove it
- Most of the clubs with #1 discriminator no longer have it (For example MyClub#1 will now just be MyClub)
- [dev] A lot of refactoring and slight functionality changes within Nadeko's behavior system and command handler which were required in order to support the medusa system
- [dev] A lot of refactoring and slight functionality changes within Nadeko's behavior system and command handler which
were required in order to support the medusa system
### Removed
- Removed `.clublevelreq` command as it doesn't serve much purpose
- Removed `.clublevelreq` command as it doesn't serve much purpose
## [4.0.6] - 21.03.2022
@@ -1173,6 +1452,7 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
## [4.0.0] - 02.03.2022
### Added
- Added `.deleteemptyservers` command
- Added `.curtr <id>` which lets you see full information about one of your own transactions with the specified id
- Added trovo.live support for stream notifications (`.stadd`)
@@ -1180,18 +1460,24 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Added 3 new settings to `data/gambling.yml` to control it:
- waifu.decay.percent - How much % to subtract from unclaimed waifu
- waifu.decay.hourInterval - How often to decay the price
- waifu.decay.minPrice - Unclaimed waifus with price lower than the one specified here will not be affected by the decay
- Added `currency.transactionsLifetime` to `data/gambling.yml` Any transaction older than the number of days specified will be automatically deleted
- waifu.decay.minPrice - Unclaimed waifus with price lower than the one specified here will not be affected by
the decay
- Added `currency.transactionsLifetime` to `data/gambling.yml` Any transaction older than the number of days specified
will be automatically deleted
- Added `.stock` command to check stock prices and charts
- Re-added `.qap / .queueautoplay`
### Changed
- CustomReactions module (and customreactions db table) has been renamed to Expressions.
- This was done to remove confusion about how it relates to discord Reactions (it doesn't, it was created and named before discord reactions existed)
- This was done to remove confusion about how it relates to discord Reactions (it doesn't, it was created and named
before discord reactions existed)
- Expression command now start with ex/expr and end with the name of the action or setting.
- For example `.exd` (`.dcr`) is expression delete, `.exa` (`.acr`)
- Permissions (`.lp`) be automatically updated with "ACTUALEXPRESSIONS", "EXPRESSIONS" instead of "ACTUALCUSTOMREACTIONS" and "CUSTOMREACTIONS"
- Permissions for `.ecr` (now `.exe`), `.scr` (now `.exs`), `.dcr` (now `.exd`), `.acr` (now `.exa`), `.lcr` (now `.exl`) will be automatically updated
- Permissions (`.lp`) be automatically updated with "ACTUALEXPRESSIONS", "EXPRESSIONS" instead of "
ACTUALCUSTOMREACTIONS" and "CUSTOMREACTIONS"
- Permissions for `.ecr` (now `.exe`), `.scr` (now `.exs`), `.dcr` (now `.exd`), `.acr` (now `.exa`), `.lcr` (
now `.exl`) will be automatically updated
- If you have custom permissions for other CustomReaction commands
- Some of the old aliases like `.acr` `.dcr` `.lcr` and a few others have been kept
- Currency output format improvement (will use guild locale now for some commands)
@@ -1210,6 +1496,7 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- [dev] Moved FilterWordsChannelId to a separate table
### Fixed
- Fixed twitch stream notifications (rewrote it to use the new api)
- Fixed an extra whitespace in usage part of command help if the command has no arguments
- Possible small fix for `.prune` ratelimiting
@@ -1221,8 +1508,10 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Fixed embed color when disabling `.antialt`
### Removed
- Removed `.bce` - use `.config` or `.config bot` specifically for bot config
- Removed obsolete placeholders: %users% %servers% %userfull% %username% %userdiscrim% %useravatar% %id% %uid% %chname% %cid% %sid% %members% %server_time% %shardid% %time% %mention%
- Removed obsolete placeholders: %users% %servers% %userfull% %username% %userdiscrim% %useravatar% %id% %uid% %chname%
%cid% %sid% %members% %server_time% %shardid% %time% %mention%
- Removed some obsolete commands and strings
- Removed code which migrated 2.x to v3 credentials, settings, etc...
@@ -1236,6 +1525,7 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
## [3.0.12] - 06.01.2022
### Fixed
- `.smch` Fixed
- `.trans` command will now work properly with capitilized language names
- Ban message color with plain text fixed
@@ -1247,12 +1537,15 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
## [3.0.11] - 17.12.2021
### Added
- `.remindl` and `.remindrm` commands now supports optional 'server' parameter for Administrators which allows them to delete any reminder created on the server
- `.remindl` and `.remindrm` commands now supports optional 'server' parameter for Administrators which allows them to
delete any reminder created on the server
- Added slots.currencyFontColor to gambling.yml
- Added `.qexport` and `.qimport` commands which allow you to export and import quotes just like `.crsexport`
- Added `.showembed <msgid>` and `.showembed #channel <msgid>` which will show you embed json from the specified message
### Changed
- `.at` and `.atl` commands reworked
- Persist restarts
- Will now only translate non-commands
@@ -1262,55 +1555,67 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Looks much nicer
- Bot will now reply to user messages with a translation if `del` is disabled
- Bot will make an embed with original and translated text with user avatar and name if `del` is enabled
- If the bot is unable to delete messages while having `del` enabled, it will reset back to the no-del behavior for the current session
- If the bot is unable to delete messages while having `del` enabled, it will reset back to the no-del behavior for
the current session
### Fixed
- `.crypto` now supports top 5000 coins
## [3.0.10] - 01.12.2021
### Changed
- `.warn` now supports weighted warnings
- `.warnlog` will now show current amount and total amount of warnings
### Fixed
- `.xprewsreset` now has correct permissions
### Removed
- Removed slot.numbers from `images.yml` as they're no longer used
## [3.0.9] - 21.11.2021
### Changed
- `.ea` will now use an image attachments if you omit imageUrl
### Added
- Added `.emojiadd` with 3 overloads
- `.ea :customEmoji:` which copies another server's emoji
- `.ea newName :customEmoji:` which copies emoji under a different name
- `.ea emojiName <imagelink.png>` which creates a new emoji from the specified image
- Patreon Access and Refresh Tokens should now be automatically updated once a month as long as the user has provided the necessary credentials in creds.yml file:
- Patreon Access and Refresh Tokens should now be automatically updated once a month as long as the user has provided
the necessary credentials in creds.yml file:
- `Patreon.ClientId`
- `Patreon.RefreshToken` (will also get updated once a month but needs an initial value)
- `Patreon.ClientSecret`
- `Patreon.CampaignId`
### Fixed
- Fixed an error that would show up in the console when a club image couldn't be drawn in certain circumstances
## [3.0.8] - 03.11.2021
### Added
- Created VotesApi project nad re-worked vote rewards handling
- Updated votes entries in creds.yml with explanations on how to set up vote links
### Fixed
- Fixed adding currency to users who don't exist in the database
- Memory used by the bot is now correct (thanks to kotz)
- Ban/kick will no longer fail due to too long reasons
- Fixed some fields not preserving inline after string replacements
### Changed
- `images.json` moved to `images.yml`
- Links will use the new cdn url
- Heads and Tails images will be updated if you haven't changed them already
@@ -1320,15 +1625,18 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
## [3.0.7] - 05.10.2021
### Added
- `.streamsclear` re-added. It will remove all followed streams on the server.
- `.gifts` now have 3 new ✂️ Haircut 🧻 ToiletPaper and 🥀 WiltedRose which **reduce** waifu's value
- They are called negative gifts
- They show up at the end of the `.gifts` page and are marked with a broken heart
- They have a separate multiplier (`waifu.multi.negative_gift_effect` default 0.5, changeable via `.config gambling` or `data/gambling.yml`)
- They have a separate multiplier (`waifu.multi.negative_gift_effect` default 0.5, changeable via `.config gambling`
or `data/gambling.yml`)
- When gifted, the waifu's price will be reduced by the `price * multiplier`
- Negative gifts don't show up in `.waifuinfo` nor is the record of them kept in the database
### Fixed
- Fixed `%users%` and `%shard.usercount%` placeholders not showing correct values
## [3.0.6] - 27.09.2021
@@ -1397,7 +1705,8 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- `.rero` now optionally takes a message id to which to attach the reaction roles
- Fully translated to German 🎉
- Added `.boost`, `.boostmsg` and `.boostdel` commands which allow you to have customizable messages when someone boosts your server, with auto-deletion support
- Added `.boost`, `.boostmsg` and `.boostdel` commands which allow you to have customizable messages when someone boosts
your server, with auto-deletion support
### Changed
@@ -1430,10 +1739,12 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Explanations on how to get the keys are added as the comments
- Code cleanup
- Command attributes cleaned up
- Removed dummy Remarks and Usages attributes as hey were unused for a few patches but stayed in the code to avoid big git diffsmigration code has ran and it can be safely removed
- Removed dummy Remarks and Usages attributes as hey were unused for a few patches but stayed in the code to
avoid big git diffsmigration code has ran and it can be safely removed
- There are 2 projects: NadekoBot and NadekoBot.Coordinator
- You can directly run NadekoBot as the regular bot with one shard
- Run NadekoBot.Coordinator if you want more control over your shards and a grpc api for coordinator with which you can start, restart, kill and see status of shards
- Run NadekoBot.Coordinator if you want more control over your shards and a grpc api for coordinator with which
you can start, restart, kill and see status of shards
- Small performance improvements
- Db Migrations squashed
- A lot of cleanup all around
@@ -1447,7 +1758,8 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
### Removed
- Removed All database migrations and data (json file) migrations
- As updating to the latest 2.x version before switching over to v3 is mandated (or fresh v3 install), that means all
- As updating to the latest 2.x version before switching over to v3 is mandated (or fresh v3 install), that means
all
## [2.46.2] - 14.07.2021
@@ -1477,9 +1789,11 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Use `.aar` to list roles which will be added
- Roles which are deleted are automatically cleaned up from `.aar`
- `.inrole` now also shows user ids
- Blacklist commands (owner only) `.ubl` `.sbl` and `.cbl` will now list blacklisted items when no argument (or a page number) is provided
- Blacklist commands (owner only) `.ubl` `.sbl` and `.cbl` will now list blacklisted items when no argument (or a page
number) is provided
- `.cmdcd` now works with customreactions too
- `.xprr` usage changed. It now takes add/rm parameter to add/remove a role ex. You can only take or remove a single role, adding and removing a role at the same level doesn't work (yet?)
- `.xprr` usage changed. It now takes add/rm parameter to add/remove a role ex. You can only take or remove a single
role, adding and removing a role at the same level doesn't work (yet?)
- example: `.xprr 5 add Member` or `.xprr 1 rm Newbie`
## [2.45.2] - 14.06.2021
@@ -1500,16 +1814,19 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
### Added
- Added many new aliases to custom reaction commands in the format ex + "action" to prepare for the future rename from CustomReactions to Expressions
- Added many new aliases to custom reaction commands in the format ex + "action" to prepare for the future rename from
CustomReactions to Expressions
- You can now `.divorce` via username#discrim even if the user no longer exists
### Changed
- DmHelpText should now have %prefix% and %bot.prefix% placeholders available
- Added squares which show enabled features for each cr in `.lcr`
- Changed CustomReactions' IDs to show, and accept base 32 unambigous characters instead of the normal database IDs (this will result in much shorter cr IDs in case you have a lot of them)
- Changed CustomReactions' IDs to show, and accept base 32 unambigous characters instead of the normal database IDs (
this will result in much shorter cr IDs in case you have a lot of them)
- Improved `.lcr` helptext to explain what's shown in the output
- `.rolecolor <color> <role>` changed to take color, then the role, to make it easier to set color for roles with multiple words without mentioning the role
- `.rolecolor <color> <role>` changed to take color, then the role, to make it easier to set color for roles with
multiple words without mentioning the role
- `.acmdcds` alias chanaged to `.cmdcds`
- `.8ball` will now cache results for a day
- `.chatmute` and `.voicemute` now support timed mutes
@@ -1526,10 +1843,13 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Allows for quick export/import of server or global custom reactions
- Requires admin permissions for server crs, and owner for global crs
- Explanation of the fields is in the comment at the top of the `.crsexport` .yml file
- Added `.mquality` / `.musicquality` - Set encoding quality. Has 4 presets - Low, Medium, High, Highest. Default is Highest
- Added `.mquality` / `.musicquality` - Set encoding quality. Has 4 presets - Low, Medium, High, Highest. Default is
Highest
- Added `.xprewsreset` which resets all currently set xp level up rewards
- Added `.purgeuser @User` which will remove the specified from the database completely. Removed settings include: Xp, clubs, waifu, currency, etc...
- Added `.config xp txt.per_image` and xpFromImage to xp.yml - Change this config to allow xp gain from posting images. Images must be 128x128 or greater in size
- Added `.purgeuser @User` which will remove the specified from the database completely. Removed settings include: Xp,
clubs, waifu, currency, etc...
- Added `.config xp txt.per_image` and xpFromImage to xp.yml - Change this config to allow xp gain from posting images.
Images must be 128x128 or greater in size
- Added `.take <amount> <role>` to complement `.award <amount> role`
- Added **Fans** list to `.waifuinfo` which shows how many people have their affinity set to you
- Added `.antialt` which will punish any user whose account is younger than specified threshold
@@ -1543,7 +1863,8 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Re-created GuildRepeaters table and renamed to Repeaters
- confirmation prompts will now use pending color from bot config, instead of okcolor
- `.mute` can now have up to 49 days mute to match .warnp
- `.warnlog` now has proper pagination (with reactions) and checking your own warnings past page 1 works correctly now with `.warnlog 2`
- `.warnlog` now has proper pagination (with reactions) and checking your own warnings past page 1 works correctly now
with `.warnlog 2`
### Fixed
@@ -1556,7 +1877,7 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Re-added `%music.playing%` and `%music.queued%` (#290)
- Added `%music.servers%` which shows how many servers have a song queued up to play
^ Only available to `.ropl` / `.adpl` feature atm
^ Only available to `.ropl` / `.adpl` feature atm
- `.autodc` re-added
- `.qrp`, `.vol`, `.smch` `.autodc` will now persist
@@ -1607,7 +1928,8 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
- Higher quality audio (no stuttering too!)
- Local tracks will now have durations if you have ffprobe installed (comes with ffmpeg)
- Bot supports joining a different vc without skipping the song if you use `.j`
- ⚠️ **DO NOT DRAG THE BOT** to another vc, as it's not properly supported atm, and you will have to do `.play` after dragging it)
- ⚠️ **DO NOT DRAG THE BOT** to another vc, as it's not properly supported atm, and you will have to do `.play`
after dragging it)
- `.j` makes the bot join your voice channel
- `.p` is now alias of play, pause is `.pause`
- `.qs` should work without google api key now for most users as it is using a custom loader
@@ -1630,4 +1952,5 @@ Note: Results of each `.youtube` query will be cached for 1 hour to improve perf
### Removed
- Removed admin requirement on `.scrm` as it didn't make sense
- Some Music commands are removed because of the complexity they bring in with little value (if you *really* want them back, you can open an issue and specify your *good* reason)
- Some Music commands are removed because of the complexity they bring in with little value (if you *really* want them
back, you can open an issue and specify your *good* reason)

View File

@@ -1,5 +1,5 @@
# Config
`.config` is the new `.bce`, it gives you a fast and easy way to edit most bot settings/values. Use `.h .config` for explanation.
`.config` gives you a fast and easy way to edit most bot settings/values. Use `.h .config` for explanation.
Use `.config` to see the list of editable config files
Use `.config <config-name>` to see the list of settable properties on that config

View File

@@ -1,27 +1,30 @@
## Creds Guide
This document aims to guide you through the process of creating a Discord account for your bot
(the Discord Bot application), and inviting that account into your Discord server.
This guide will show you how to create your own discord bot, invite it to your server, and copy it's credentials to your `creds.yml` in order to run your bot.
- Start by opening your creds.yml
- If you're on a windows installer version, click on the creds button next to your bot's RUN button.
- If you're on linux from source or windows from source version, open `nadekobot/output/creds.yml`. Please use visual studio code, notepad++ or another code editor. Usage of notepad is discouraged.
![Create a bot application and copy token to creds.yml file](https://cdn.nadeko.bot/tutorial/bot-creds-guide.gif)
- Go to [the Discord developer application page][DiscordApp].
- Log in with your Discord account.
- Click **New Application**.
- Fill out the `Name` field however you like.
- Go to the **Bot** tab on the left sidebar.
- Click on the `Add a Bot` button and confirm that you do want to add a bot to this app.
- **Optional:** Add bot's avatar and description.
- Copy your Token to `creds.yml` as shown above.
- Scroll down to the **`Privileged Gateway Intents`** section
- **Enable the following:**
1. Go to [the Discord developer application page][DiscordApp].
2. Log in with your Discord account.
3. Click **New Application**.
3. Fill out the `Name` field however you like, accept the terms, and confirm.
1. Go to the **Bot** tab on the left sidebar.
1. Click on the `Add a Bot` button and confirm that you do want to add a bot to this app.
1. **Optional:** Add bot's avatar and description.
1. Copy your Token to `creds.yml` as shown above.
1. Scroll down to the **`Privileged Gateway Intents`** section
- Enable the following:
- **PRESENCE INTENT**
- **SERVER MEMBERS INTENT**
- **MESSAGE CONTENT INTENT**
These are required for a number of features to function properly, and all should be on.
##### Getting Owner ID*(s)*:
##### Getting Owner ID
- Go to your Discord server and attempt to mention yourself, but put a backslash at the start
*(to make it slightly easier, add the backslash after the mention has been typed)*.

View File

@@ -7,10 +7,9 @@ Donations go a long way in helping us keep the project alive, and we appreciate
Donating to us also gives you the following benefits:
- A hoisted **Donators role** in our [Discord server][discord-server]
- A hoisted **Patron** role in [Nadeko Discord server][discord-server]
- Access to exclusive **#noticed** text and voice channels
- **1000 flowers** on the public bot per dollar donated (after fees)
- **Expressions** on the public bot for [Patreon pledges][patreon] of $5 or higher
- **3000 flowers** on the public bot per dollar donated (after fees)
## Patreon
@@ -26,7 +25,7 @@ You can set up a monthly pledge on [Patreon][patreon] and support the project's
You can also donate to us through [PayPal][paypal] for one-time donations using the button below, or by donating to `nadekodiscordbot@gmail.com`.
!!! Note
Mention your Discord tag (Username#1234) in the payment note to receive flower rewards.
Mention your Discord username or user id in the payment note to receive flower rewards.
[![img][paypal-button]][paypal]

View File

@@ -1,7 +1,7 @@
## Setting Up NadekoBot on Windows With the Updater
| Table of Contents|
| :---------------------------------------------------------------------------------------------------------------------------|
| :-|
| [Prerequisites](#prerequisites) |
| [Setup](#setup) |
| [Starting the Bot](#starting-the-bot) |
@@ -13,7 +13,6 @@
#### Prerequisites
- Windows 10 or later (64-bit)
- [Create a Discord Bot application and invite the bot to your server](../creds-guide.md)
**Optional**
@@ -29,8 +28,7 @@
![Create a new bot](https://i.imgur.com/JxtRk9e.png "Create a new bot")
- Click on **`DOWNLOAD`** at the lower right
![Bot Setup](https://i.imgur.com/HqAl36p.png "Bot Setup")
- **Note: Redis is optional. install Redis manually here: [Redis] Download and run the **`.msi`** file.**
- If you will use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DLP`**.
- If you want to use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DLP`**.
- If any dependencies fail to install, you can temporarily disable your Windows Defender/AV until you install them. If you don't want to, then read [the last section of this guide](#Manual-Prerequisite-Installation).
- When installation is finished, click on **`CREDS`** to the left of **`RUN`** at the lower right.
- Follow the guide on how to [Set up the creds.yml](../../creds-guide) file.
@@ -39,8 +37,6 @@
- Either click on **`RUN`** button in the updater or run the bot via its desktop shortcut.
### If you get a "No owner channels created..." message. Please follow the creds guide again [**HERE**](../../creds-guide).
#### Updating Nadeko
- Make sure Nadeko is closed and not running
@@ -52,71 +48,6 @@
- Launch the bot
- You've updated and are running again, easy as that!
#### Manual Prerequisite Installation
You can still install them manually:
- [Redis] (OPTIONAL) - Download and run the **`.msi`** file
- [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, then just move the `ffmpeg.exe` file to NadekoBot/system
- [youtube-dlp] - Click to download the `yt-dlp.exe` file then put `yt-dlp.exe` in a path that's in your PATH environment variable. If you don't know what that is, then just move the `yt-dlp.exe` file to NadekoBot/system
## **⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
### Windows From Source
##### Prerequisites
**Install these before proceeding or your bot will not work!**
- [.net 8](https://dotnet.microsoft.com/en-us/download) - needed to compile and run the bot
- [git](https://git-scm.com/downloads) - needed to clone the repository (you can also download the zip manually and extract it, but this guide assumes you're using git)
- [Redis] (OPTIONAL)- to cache things needed by some features and persist through restarts
##### Installation Instructions
Open PowerShell (press windows button on your keyboard and type powershell, it should show up; alternatively, right click the start menu and select Windows PowerShell), and navigate to the location where you want to install the bot (for example `cd ~/Desktop/`)
1. `git clone https://gitlab.com/kwoth/nadekobot -b v5 --depth 1`
2. `cd nadekobot`
3. `dotnet publish -c Release -o output/ src/NadekoBot/`
4. `cd output`
5. `cp creds_example.yml creds.yml`
6. Open `creds.yml` with your favorite text editor (Please don't use Notepad or WordPad. You can use Notepad++, VSCode, Atom, Sublime, or something similar)
7. [Enter your bot's token](#creds-guide)
8. Run the bot `dotnet NadekoBot.dll`
9. 🎉
##### Update Instructions
Open PowerShell as described above and run the following commands:
1. Stop the bot
- ⚠️ Make sure you don't have your database, credentials or any other nadekobot folder open in some application, this might prevent some of the steps from executing succesfully
2. Navigate to your bot's folder, example:
- `cd ~/Desktop/nadekobot`
3. Pull the new version, and make sure you're on the v5 branch
- *⚠️ the first 3 lines can be omitted if you're already on v5. If you're updating from v4, you must run them*
- `git remote set-branches origin '*'`
- `git fetch -v --depth=1`
- `git checkout v5`
- `git pull`
- ⚠️ If this fails, you may want to stash or remove your code changes if you don't know how to resolve merge conflicts
4. **Backup** old output in case your data is overwritten
- `cp -r -fo output/ output-old`
5. Build the bot again
- `dotnet publish -c Release -o output/ src/NadekoBot/`
6. Remove old strings and aliases to avoid overwriting the updated versions of those files
- ⚠ If you've modified said files, back them up instead
- `rm output-old/data/aliases.yml`
- `rm -r output-old/data/strings`
7. Copy old data
- `cp -Recurse .\output-old\data\ .\output\ -Force`
8. Copy creds.yml
- `cp output-old/creds.yml output/`
9. Run the bot
- `cd output`
- `dotnet NadekoBot.dll`
🎉 Enjoy
#### Music prerequisites
In order to use music commands, you need ffmpeg and yt-dlp installed.

View File

@@ -0,0 +1,77 @@
## Setting Up NadekoBot on Windows from source
1. Prerequisites
- Windows 10 or later (64-bit)
- [.net 8 sdk](https://dotnet.microsoft.com/download/dotnet/8.0)
- If you want nadeko to play music: [Visual C++ 2010 (x86)] and [Visual C++ 2017 (x64)] (both are required, you may install them later)
- [git](https://git-scm.com/downloads) - needed to clone the repository (you can also download the zip manually and extract it, but this guide assumes you're using git)
- **Optional** Any code editor, for example [Visual Studio Code](https://code.visualstudio.com/Download)
- You'll need to at least modify creds.yml, notepad is inadequate
##### Installation Instructions
Open PowerShell (press windows button on your keyboard and type powershell, it should show up; alternatively, right click the start menu and select Windows PowerShell), and
0. Navigate to the location where you want to install the bot
- for example, type `cd ~/Desktop/` and press enter
1. `git clone https://gitlab.com/kwoth/nadekobot -b v5 --depth 1`
2. `cd nadekobot`
3. `dotnet publish -c Release -o output/ src/NadekoBot/`
4. `cd output`
5. `cp creds_example.yml creds.yml`
6. "You're done installing, you may now proceed to set up your bot's credentials by following the [#creds-guide]
- Once done, come back here and run the last command
8. Run the bot `dotnet NadekoBot.dll`
9. 🎉 Enjoy
##### Update Instructions
Open PowerShell as described above and run the following commands:
1. Stop the bot
- ⚠️ Make sure you don't have your database, credentials or any other nadekobot folder open in some application, this might prevent some of the steps from executing succesfully
2. Navigate to your bot's folder, example:
- `cd ~/Desktop/nadekobot`
3. Pull the new version, and make sure you're on the v5 branch
- *⚠️ If you're on v4, you must run these commands, if not, you may skip them.*
- `git remote set-branches origin '*'`
- `git fetch -v --depth=1`
- `git checkout v5`
- `git pull`
- ⚠️ If this fails, you may want to stash or remove your code changes if you don't know how to resolve merge conflicts
4. **Backup** old output in case your data is overwritten
- `cp -r -fo output/ output-old`
5. Build the bot again
- `dotnet publish -c Release -o output/ src/NadekoBot/`
6. Remove old strings and aliases to avoid overwriting the updated versions of those files
- ⚠ If you've modified said files, back them up instead
- `rm output-old/data/aliases.yml`
- `rm -r output-old/data/strings`
7. Copy old data, and new strings
- `cp -Recurse -Force .\output-old\data\ .\output\`
- `cp -Recurse -Force src/NadekoBot/data/strings/ output/data/`
8. Copy creds.yml
- `cp output-old/creds.yml output/`
9. Run the bot
- `cd output`
- `dotnet NadekoBot.dll`
🎉 Enjoy
#### Music prerequisites
In order to use music commands, you need ffmpeg and yt-dlp installed.
- [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `ffmpeg.exe` file to `NadekoBot/output`.
- [youtube-dlp] - Click to download the `yt-dlp.exe` file, then move `yt-dlp.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `yt-dlp.exe` file to `NadekoBot/system`.
[Updater]: https://dl.nadeko.bot/v3/
[Notepad++]: https://notepad-plus-plus.org/
[.net]: https://dotnet.microsoft.com/download/dotnet/8.0
[Redis]: https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.msi
[Visual C++ 2010 (x86)]: https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe
[Visual C++ 2017 (x64)]: https://aka.ms/vs/15/release/vc_redist.x64.exe
[ffmpeg-32bit]: https://cdn.nadeko.bot/dl/ffmpeg-32.zip
[ffmpeg-64bit]: https://cdn.nadeko.bot/dl/ffmpeg-64.zip
[youtube-dlp]: https://github.com/yt-dlp/yt-dlp/releases

View File

@@ -16,6 +16,7 @@ To self-host your own Nadeko, use the guides below:
- [:material-microsoft-windows: Windows guide][windows-guide]
- [:material-linux: Linux guide][linux-guide]
- [:material-apple: Mac OS guide][macos-guide]
- [:material-microsoft-windows: Windows (from source) guide][windows-source-guide]
In case you need any help, join our [Discord server][discord-server] where we may provide support.
@@ -32,6 +33,7 @@ If you're unsure whether something is an issue, ask in our support server first.
[invite]: https://invite.nadeko.bot/
[commands]: https://nadeko.bot/commands/
[windows-guide]: ./guides/windows-guide.md
[windows-source-guide]: ./guides/windows-source-guide.md
[linux-guide]: ./guides/linux-guide.md
[macos-guide]: ./guides/osx-guide.md
[from-source-guide]: ./guides/from-source.md

View File

@@ -75,6 +75,7 @@ nav:
- Windows Guide: guides/windows-guide.md
- Linux Guide: guides/linux-guide.md
- OSX Guide: guides/osx-guide.md
- Windows Guide (from source): guides/windows-source-guide.md
- Docker Guide (unsupported): guides/docker-guide.md
- Commands:
- Readme: commands-readme.md

View File

@@ -1,3 +0,0 @@
dotnet ef migrations remove -c SqliteContext -f -p src/NadekoBot/NadekoBot.csproj
dotnet ef migrations remove -c PostgreSqlContext -f -p src/NadekoBot/NadekoBot.csproj

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

@@ -0,0 +1,60 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
import "google/protobuf/timestamp.proto";
package fin;
service GrpcFin {
rpc GetTransactions(GetTransactionsRequest) returns (GetTransactionsReply);
rpc GetHoldings(GetHoldingsRequest) returns (GetHoldingsReply);
rpc Withdraw(WithdrawRequest) returns (WithdrawReply);
rpc Deposit(DepositRequest) returns (DepositReply);
}
message GetTransactionsRequest {
int32 page = 1;
uint64 userId = 2;
}
message GetTransactionsReply {
repeated TransactionReply transactions = 1;
int32 total = 2;
}
message TransactionReply {
int64 amount = 1;
string note = 2;
string type = 3;
string extra = 4;
google.protobuf.Timestamp timestamp = 5;
string id = 6;
}
message GetHoldingsRequest {
uint64 userId = 1;
}
message GetHoldingsReply {
int64 cash = 1;
int64 bank = 2;
}
message WithdrawRequest {
uint64 userId = 1;
int64 amount = 2;
}
message WithdrawReply {
bool success = 1;
}
message DepositRequest {
uint64 userId = 1;
int64 amount = 2;
}
message DepositReply {
bool success = 1;
}

View File

@@ -0,0 +1,120 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package xp;
service GrpcXp {
rpc GetXpLb(GetXpLbRequest) returns (GetXpLbReply);
rpc ResetUserXp(ResetUserXpRequest) returns (ResetUserXpReply);
rpc GetXpSettings(GetXpSettingsRequest) returns (GetXpSettingsReply);
rpc AddExclusion(AddExclusionRequest) returns (AddExclusionReply);
rpc DeleteExclusion(DeleteExclusionRequest) returns (DeleteExclusionReply);
rpc AddReward(AddRewardRequest) returns (AddRewardReply);
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
rpc SetServerExclusion(SetServerExclusionRequest) returns (SetServerExclusionReply);
}
message SetServerExclusionRequest {
uint64 guildId = 1;
bool serverExcluded = 2;
}
message SetServerExclusionReply {
bool success = 1;
}
message GetXpLbRequest {
uint64 guildId = 1;
int32 page = 2;
}
message GetXpLbReply {
repeated XpLbUserReply users = 1;
int32 total = 2;
}
message XpLbUserReply {
uint64 userId = 1;
string username = 2;
int64 xp = 3;
int64 level = 4;
int64 levelPercent = 5;
string avatar = 6;
}
message ResetUserXpRequest {
uint64 guildId = 1;
uint64 userId = 2;
}
message ResetUserXpReply {
bool success = 1;
}
message GetXpSettingsReply {
repeated ExclItemReply exclusions = 1;
repeated RewItemReply rewards = 2;
bool serverExcluded = 3;
}
message GetXpSettingsRequest {
uint64 guildId = 1;
}
message ExclItemReply {
string type = 1;
uint64 id = 2;
string name = 3;
}
message RewItemReply {
int32 level = 1;
string type = 2;
string value = 3;
}
message AddExclusionRequest {
uint64 guildId = 1;
string type = 2;
uint64 id = 3;
}
message AddExclusionReply {
bool success = 1;
}
message DeleteExclusionRequest {
uint64 guildId = 1;
string type = 2;
uint64 id = 3;
}
message DeleteExclusionReply {
bool success = 1;
}
message AddRewardRequest {
uint64 guildId = 1;
int32 level = 2;
string type = 3;
string value = 4;
}
message AddRewardReply {
bool success = 1;
}
message DeleteRewardRequest {
uint64 guildId = 1;
int32 level = 2;
string type = 3;
}
message DeleteRewardReply {
bool success = 1;
}

View File

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

View File

@@ -25,7 +25,6 @@ public static class DiscordUserExtensions
{
UserId = userId,
Username = username,
Discriminator = discrim,
AvatarId = avatarId,
TotalXp = 0,
CurrencyAmount = 0
@@ -33,7 +32,6 @@ public static class DiscordUserExtensions
old => new()
{
Username = username,
Discriminator = discrim,
AvatarId = avatarId
},
() => new()
@@ -49,8 +47,7 @@ public static class DiscordUserExtensions
() => new()
{
UserId = userId,
Username = "Unknown",
Discriminator = "????",
Username = "??Unknown",
AvatarId = string.Empty,
TotalXp = 0,
CurrencyAmount = 0

View File

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

View File

@@ -1,5 +1,4 @@
#nullable disable
using LinqToDB;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Db.Models;
@@ -8,6 +7,9 @@ namespace NadekoBot.Db;
public static class UserXpExtensions
{
public static async Task<UserXpStats?> GetGuildUserXp(this ITable<UserXpStats> table, ulong guildId, ulong userId)
=> await table.FirstOrDefaultAsyncLinqToDB(x => x.GuildId == guildId && x.UserId == userId);
public static UserXpStats GetOrCreateUserXpStats(this DbContext ctx, ulong guildId, ulong userId)
{
var usr = ctx.Set<UserXpStats>().FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId);
@@ -18,7 +20,6 @@ public static class UserXpExtensions
{
Xp = 0,
UserId = userId,
NotifyOnLevelUp = XpNotificationLocation.None,
GuildId = guildId
});
}
@@ -29,24 +30,21 @@ 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;
public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
=> xps.Delete(x => x.UserId == userId && x.GuildId == guildId);
public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId)
=> xps.Delete(x => x.GuildId == guildId);
@@ -54,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

@@ -4,38 +4,28 @@ namespace NadekoBot.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++;
TotalXp = totalXp;
Level = GetLevelByTotalXp(totalXp);
LevelXp = totalXp - GetTotalXpReqForLevel(Level);
RequiredXp = (9 * (Level + 1)) + 27;
}
Level = lvl - 1;
LevelXp = xp - totalXp;
RequiredXp = required;
}
public static LevelStats CreateForLevel(long level)
=> new(GetTotalXpReqForLevel(level));
public static long GetTotalXpReqForLevel(long level)
=> ((9 * level * level) + (63 * level)) / 2;
public static long GetLevelByTotalXp(long totalXp)
=> (long)((-7.0 / 2) + (1 / 6.0 * Math.Sqrt((8 * totalXp) + 441)));
}

View File

@@ -7,7 +7,7 @@ public class DiscordUser : DbEntity
{
public ulong UserId { get; set; }
public string Username { get; set; }
public string Discriminator { get; set; }
// public string Discriminator { get; set; }
public string AvatarId { get; set; }
public int? ClubId { get; set; }
@@ -27,9 +27,6 @@ public class DiscordUser : DbEntity
public override string ToString()
{
if (string.IsNullOrWhiteSpace(Discriminator) || Discriminator == "0000")
return Username;
return Username + "#" + Discriminator;
}
}

View File

@@ -0,0 +1,8 @@
#nullable disable
namespace NadekoBot.Db.Models;
public class FlagTranslateChannel : DbEntity
{
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
}

View File

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

View File

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

View File

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

View File

@@ -13,28 +13,11 @@ public class GuildConfig : DbEntity
public string AutoAssignRoleIds { get; set; }
// //greet stuff
// public int AutoDeleteGreetMessagesTimer { get; set; } = 30;
// public int AutoDeleteByeMessagesTimer { get; set; } = 30;
//
// public ulong GreetMessageChannelId { get; set; }
// public ulong ByeMessageChannelId { get; set; }
//
// public bool SendDmGreetMessage { get; set; }
// public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
//
// public bool SendChannelGreetMessage { get; set; }
// public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
//
// public bool SendChannelByeMessage { get; set; }
// public string ChannelByeMessageText { get; set; } = "%user% has left!";
// public bool SendBoostMessage { get; set; }
// pulic int BoostMessageDeleteAfter { get; set; }
//self assignable roles
//todo FUTURE: DELETE, UNUSED
public bool ExclusiveSelfAssignedRoles { get; set; }
public bool AutoDeleteSelfAssignedRoleMessages { get; set; }
//stream notifications
public HashSet<FollowedStream> FollowedStreams { get; set; } = new();
@@ -53,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; }
@@ -91,15 +82,10 @@ public class GuildConfig : DbEntity
public List<FeedSub> FeedSubs { get; set; } = new();
public bool NotifyStreamOffline { get; set; }
public bool DeleteStreamOnlineMessage { get; set; }
public List<GroupName> SelfAssignableRoleGroupNames { get; set; }
public int WarnExpireHours { get; set; }
public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear;
public bool DisableGlobalExpressions { get; set; } = false;
#region Boost Message
public bool StickyRoles { get; set; }
#endregion
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
namespace NadekoBot.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

@@ -1,8 +1,12 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
namespace NadekoBot.Db.Models;
public class PatronUser
{
// [Key]
// public int Id { get; set; }
public string UniquePlatformUserId { get; set; }
public ulong UserId { get; set; }
public int AmountCents { 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

@@ -1,4 +1,4 @@
namespace NadekoBot.Db.Models;
namespace NadekoBot.Db.Models;
public enum XpNotificationLocation
{

View File

@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Administration.Services;
// ReSharper disable UnusedAutoPropertyAccessor.Global
@@ -14,7 +15,6 @@ public abstract class NadekoContext : DbContext
public DbSet<Quote> Quotes { get; set; }
public DbSet<Reminder> Reminders { get; set; }
public DbSet<SelfAssignedRole> SelfAssignableRoles { get; set; }
public DbSet<MusicPlaylist> MusicPlaylists { get; set; }
public DbSet<NadekoExpression> Expressions { get; set; }
public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; }
@@ -62,6 +62,7 @@ public abstract class NadekoContext : DbContext
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
// public DbSet<GuildColors> GuildColors { get; set; }
@@ -73,6 +74,123 @@ public abstract class NadekoContext : 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>()
.HasIndex(x => x.GuildId)
.IsUnique(true);
#endregion
#region Button Roles
modelBuilder.Entity<ButtonRole>(br =>
{
br.HasIndex(x => x.GuildId)
.IsUnique(false);
br.HasAlternateKey(x => new
{
x.RoleId,
x.MessageId,
});
});
#endregion
#region New Sar
modelBuilder.Entity<SarGroup>(sg =>
{
sg.HasAlternateKey(x => new
{
x.GuildId,
x.GroupNumber
});
sg.HasMany(x => x.Roles)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Sar>()
.HasAlternateKey(x => new
{
x.GuildId,
x.RoleId
});
modelBuilder.Entity<SarAutoDelete>()
.HasIndex(x => x.GuildId)
.IsUnique();
#endregion
#region Rakeback
modelBuilder.Entity<Rakeback>()
.HasKey(x => x.UserId);
#endregion
#region UserBetStats
modelBuilder.Entity<UserBetStats>(ubs =>
{
ubs.HasIndex(x => new
{
x.UserId,
x.Game
})
.IsUnique();
ubs.HasIndex(x => x.MaxWin)
.IsUnique(false);
});
#endregion
#region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>()
.HasIndex(x => new
{
x.GuildId,
x.ChannelId
})
.IsUnique();
#endregion
#region NCanvas
modelBuilder.Entity<NCPixel>()
@@ -261,11 +379,6 @@ public abstract class NadekoContext : DbContext
.HasForeignKey(x => x.GuildConfigId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GuildConfig>()
.HasMany(x => x.SelfAssignableRoleGroupNames)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<FeedSub>()
.HasAlternateKey(x => new
{
@@ -294,21 +407,6 @@ public abstract class NadekoContext : DbContext
#endregion
#region Self Assignable Roles
var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>();
selfassignableRolesEntity.HasIndex(s => new
{
s.GuildId,
s.RoleId
})
.IsUnique();
selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0);
#endregion
#region MusicPlaylists
var musicPlaylistEntity = modelBuilder.Entity<MusicPlaylist>();
@@ -385,7 +483,6 @@ public abstract class NadekoContext : DbContext
xps.HasIndex(x => x.UserId);
xps.HasIndex(x => x.GuildId);
xps.HasIndex(x => x.Xp);
xps.HasIndex(x => x.AwardedXp);
#endregion
@@ -491,23 +588,6 @@ public abstract class NadekoContext : DbContext
#endregion
#region GroupName
modelBuilder.Entity<GroupName>()
.HasIndex(x => new
{
x.GuildConfigId,
x.Number
})
.IsUnique();
modelBuilder.Entity<GroupName>()
.HasOne(x => x.GuildConfig)
.WithMany(x => x.SelfAssignableRoleGroupNames)
.IsRequired();
#endregion
#region BanTemplate
modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique();

View File

@@ -5,6 +5,51 @@ namespace NadekoBot.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)
{
migrationBuilder.Sql("""
INSERT INTO GroupName (Number, GuildConfigId)
SELECT DISTINCT "Group", GC.Id
FROM SelfAssignableRoles as SAR
INNER JOIN GuildConfigs as GC
ON SAR.GuildId = GC.GuildId
WHERE SAR.GuildId not in (SELECT GuildConfigs.GuildId from GroupName LEFT JOIN GuildConfigs ON GroupName.GuildConfigId = GuildConfigs.Id);
INSERT INTO SarGroup (Id, GroupNumber, Name, IsExclusive, GuildId)
SELECT GN.Id, GN.Number, GN.Name, GC.ExclusiveSelfAssignedRoles, GC.GuildId
FROM GroupName as GN
INNER JOIN GuildConfigs as GC ON GN.GuildConfigId = GC.Id;
INSERT INTO Sar (GuildId, RoleId, SarGroupId, LevelReq)
SELECT SAR.GuildId, SAR.RoleId, (SELECT Id FROM SarGroup WHERE SG.Number = SarGroup.GroupNumber AND SG.GuildId = SarGroup.GuildId), MIN(SAR.LevelRequirement)
FROM SelfAssignableRoles as SAR
INNER JOIN (SELECT GuildId, gn.Number FROM GroupName as gn
INNER JOIN GuildConfigs as gc ON gn.GuildConfigId =gc.Id
) as SG
ON SG.GuildId = SAR.GuildId
WHERE SG.Number IN (SELECT GroupNumber FROM SarGroup WHERE Sar.GuildId = SarGroup.GuildId)
GROUP BY SAR.GuildId, SAR.RoleId;
INSERT INTO SarAutoDelete (GuildId, IsEnabled)
SELECT GuildId, AutoDeleteSelfAssignedRoleMessages FROM GuildConfigs WHERE AutoDeleteSelfAssignedRoleMessages = TRUE;
""");
}
public static void UpdateUsernames(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' || Username WHERE Discriminator = '????';");
}
public static void MigrateRero(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsSqlite())

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class nodiscrimandflagtranslate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "discriminator",
table: "discorduser");
migrationBuilder.CreateTable(
name: "flagtranslatechannel",
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),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_flagtranslatechannel", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_flagtranslatechannel_guildid_channelid",
table: "flagtranslatechannel",
columns: new[] { "guildid", "channelid" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "flagtranslatechannel");
migrationBuilder.AddColumn<string>(
name: "discriminator",
table: "discorduser",
type: "text",
nullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class betstats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "userbetstats",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
game = table.Column<int>(type: "integer", nullable: false),
wincount = table.Column<long>(type: "bigint", nullable: false),
losecount = table.Column<long>(type: "bigint", nullable: false),
totalbet = table.Column<decimal>(type: "numeric", nullable: false),
paidout = table.Column<decimal>(type: "numeric", nullable: false),
maxwin = table.Column<long>(type: "bigint", nullable: false),
maxbet = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_userbetstats", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_userbetstats_userid_game",
table: "userbetstats",
columns: new[] { "userid", "game" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "userbetstats");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class rakeback : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rakeback",
columns: table => new
{
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
amount = table.Column<decimal>(type: "numeric", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_rakeback", x => x.userid);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rakeback");
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -451,6 +451,69 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("blacklist", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ButtonRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ButtonId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("buttonid");
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<string>("Emote")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("emote");
b.Property<bool>("Exclusive")
.HasColumnType("boolean")
.HasColumnName("exclusive");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("label");
b.Property<decimal>("MessageId")
.HasColumnType("numeric(20,0)")
.HasColumnName("messageid");
b.Property<int>("Position")
.HasColumnType("integer")
.HasColumnName("position");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_buttonrole");
b.HasAlternateKey("RoleId", "MessageId")
.HasName("ak_buttonrole_roleid_messageid");
b.HasIndex("GuildId")
.HasDatabaseName("ix_buttonrole_guildid");
b.ToTable("buttonrole", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@@ -751,10 +814,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<string>("Discriminator")
.HasColumnType("text")
.HasColumnName("discriminator");
b.Property<bool>("IsClubAdmin")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
@@ -998,6 +1057,37 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("filteredword", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.FlagTranslateChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.HasKey("Id")
.HasName("pk_flagtranslatechannel");
b.HasIndex("GuildId", "ChannelId")
.IsUnique()
.HasDatabaseName("ix_flagtranslatechannel_guildid_channelid");
b.ToTable("flagtranslatechannel", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b =>
{
b.Property<int>("Id")
@@ -1175,7 +1265,7 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("giveawayuser", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.GroupName", b =>
modelBuilder.Entity("NadekoBot.Db.Models.GuildColors", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1184,30 +1274,33 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<string>("ErrorColor")
.HasMaxLength(9)
.HasColumnType("character varying(9)")
.HasColumnName("errorcolor");
b.Property<int>("GuildConfigId")
.HasColumnType("integer")
.HasColumnName("guildconfigid");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Name")
.HasColumnType("text")
.HasColumnName("name");
b.Property<string>("OkColor")
.HasMaxLength(9)
.HasColumnType("character varying(9)")
.HasColumnName("okcolor");
b.Property<int>("Number")
.HasColumnType("integer")
.HasColumnName("number");
b.Property<string>("PendingColor")
.HasMaxLength(9)
.HasColumnType("character varying(9)")
.HasColumnName("pendingcolor");
b.HasKey("Id")
.HasName("pk_groupname");
.HasName("pk_guildcolors");
b.HasIndex("GuildConfigId", "Number")
b.HasIndex("GuildId")
.IsUnique()
.HasDatabaseName("ix_groupname_guildconfigid_number");
.HasDatabaseName("ix_guildcolors_guildid");
b.ToTable("groupname", (string)null);
b.ToTable("guildcolors", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.GuildConfig", b =>
@@ -1724,6 +1817,42 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("expressions", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.Notify", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(10000)
.HasColumnType("character varying(10000)")
.HasColumnName("message");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.HasKey("Id")
.HasName("pk_notify");
b.HasAlternateKey("GuildId", "Type")
.HasName("ak_notify_guildid_type");
b.ToTable("notify", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{
b.Property<decimal>("UserId")
@@ -2173,7 +2302,7 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("rotatingstatus", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.SelfAssignedRole", b =>
modelBuilder.Entity("NadekoBot.Db.Models.Sar", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -2182,36 +2311,98 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<int>("Group")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("group");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<int>("LevelRequirement")
b.Property<int>("LevelReq")
.HasColumnType("integer")
.HasColumnName("levelrequirement");
.HasColumnName("levelreq");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.Property<int>("SarGroupId")
.HasColumnType("integer")
.HasColumnName("sargroupid");
b.HasKey("Id")
.HasName("pk_selfassignableroles");
.HasName("pk_sar");
b.HasIndex("GuildId", "RoleId")
b.HasAlternateKey("GuildId", "RoleId")
.HasName("ak_sar_guildid_roleid");
b.HasIndex("SarGroupId")
.HasDatabaseName("ix_sar_sargroupid");
b.ToTable("sar", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.SarAutoDelete", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasColumnName("isenabled");
b.HasKey("Id")
.HasName("pk_sarautodelete");
b.HasIndex("GuildId")
.IsUnique()
.HasDatabaseName("ix_selfassignableroles_guildid_roleid");
.HasDatabaseName("ix_sarautodelete_guildid");
b.ToTable("selfassignableroles", (string)null);
b.ToTable("sarautodelete", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.SarGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("GroupNumber")
.HasColumnType("integer")
.HasColumnName("groupnumber");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<bool>("IsExclusive")
.HasColumnType("boolean")
.HasColumnName("isexclusive");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name");
b.Property<decimal?>("RoleReq")
.HasColumnType("numeric(20,0)")
.HasColumnName("rolereq");
b.HasKey("Id")
.HasName("pk_sargroup");
b.HasAlternateKey("GuildId", "GroupNumber")
.HasName("ak_sargroup_guildid_groupnumber");
b.ToTable("sargroup", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ShopEntry", b =>
@@ -2547,6 +2738,47 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("streamrolewhitelisteduser", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.TempRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp without time zone")
.HasColumnName("expiresat");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<bool>("Remove")
.HasColumnType("boolean")
.HasColumnName("remove");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_temprole");
b.HasAlternateKey("GuildId", "UserId", "RoleId")
.HasName("ak_temprole_guildid_userid_roleid");
b.HasIndex("ExpiresAt")
.HasDatabaseName("ix_temprole_expiresat");
b.ToTable("temprole", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.TodoModel", b =>
{
b.Property<int>("Id")
@@ -2703,10 +2935,6 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("AwardedXp")
.HasColumnType("bigint")
.HasColumnName("awardedxp");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
@@ -2715,10 +2943,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<int>("NotifyOnLevelUp")
.HasColumnType("integer")
.HasColumnName("notifyonlevelup");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
@@ -2730,9 +2954,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.HasKey("Id")
.HasName("pk_userxpstats");
b.HasIndex("AwardedXp")
.HasDatabaseName("ix_userxpstats_awardedxp");
b.HasIndex("GuildId")
.HasDatabaseName("ix_userxpstats_guildid");
@@ -3200,6 +3421,77 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("greetsettings", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Rakeback", b =>
{
b.Property<decimal>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<decimal>("Amount")
.HasColumnType("numeric")
.HasColumnName("amount");
b.HasKey("UserId")
.HasName("pk_rakeback");
b.ToTable("rakeback", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.UserBetStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Game")
.HasColumnType("integer")
.HasColumnName("game");
b.Property<long>("LoseCount")
.HasColumnType("bigint")
.HasColumnName("losecount");
b.Property<long>("MaxBet")
.HasColumnType("bigint")
.HasColumnName("maxbet");
b.Property<long>("MaxWin")
.HasColumnType("bigint")
.HasColumnName("maxwin");
b.Property<decimal>("PaidOut")
.HasColumnType("numeric")
.HasColumnName("paidout");
b.Property<decimal>("TotalBet")
.HasColumnType("numeric")
.HasColumnName("totalbet");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<long>("WinCount")
.HasColumnType("bigint")
.HasColumnName("wincount");
b.HasKey("Id")
.HasName("pk_userbetstats");
b.HasIndex("MaxWin")
.HasDatabaseName("ix_userbetstats_maxwin");
b.HasIndex("UserId", "Game")
.IsUnique()
.HasDatabaseName("ix_userbetstats_userid_game");
b.ToTable("userbetstats", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.AntiAltSetting", b =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)
@@ -3432,18 +3724,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasConstraintName("fk_giveawayuser_giveawaymodel_giveawayid");
});
modelBuilder.Entity("NadekoBot.Db.Models.GroupName", b =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", "GuildConfig")
.WithMany("SelfAssignableRoleGroupNames")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_groupname_guildconfigs_guildconfigid");
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Db.Models.IgnoredLogItem", b =>
{
b.HasOne("NadekoBot.Db.Models.LogSetting", "LogSetting")
@@ -3483,6 +3763,16 @@ namespace NadekoBot.Migrations.PostgreSql
.HasConstraintName("fk_playlistsong_musicplaylists_musicplaylistid");
});
modelBuilder.Entity("NadekoBot.Db.Models.Sar", b =>
{
b.HasOne("NadekoBot.Db.Models.SarGroup", null)
.WithMany("Roles")
.HasForeignKey("SarGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_sar_sargroup_sargroupid");
});
modelBuilder.Entity("NadekoBot.Db.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)
@@ -3759,8 +4049,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("Permissions");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
b.Navigation("SlowmodeIgnoredRoles");
@@ -3790,6 +4078,11 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("Songs");
});
modelBuilder.Entity("NadekoBot.Db.Models.SarGroup", b =>
{
b.Navigation("Roles");
});
modelBuilder.Entity("NadekoBot.Db.Models.ShopEntry", b =>
{
b.Navigation("Items");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
/// <inheritdoc />
public partial class nodiscrimandflagtranslate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Discriminator",
table: "DiscordUser");
migrationBuilder.CreateTable(
name: "FlagTranslateChannel",
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),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_FlagTranslateChannel", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_FlagTranslateChannel_GuildId_ChannelId",
table: "FlagTranslateChannel",
columns: new[] { "GuildId", "ChannelId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "FlagTranslateChannel");
migrationBuilder.AddColumn<string>(
name: "Discriminator",
table: "DiscordUser",
type: "TEXT",
nullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
/// <inheritdoc />
public partial class betstats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UserBetStats",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Game = table.Column<int>(type: "INTEGER", nullable: false),
WinCount = table.Column<long>(type: "INTEGER", nullable: false),
LoseCount = table.Column<long>(type: "INTEGER", nullable: false),
TotalBet = table.Column<decimal>(type: "TEXT", nullable: false),
PaidOut = table.Column<decimal>(type: "TEXT", nullable: false),
MaxWin = table.Column<long>(type: "INTEGER", nullable: false),
MaxBet = table.Column<long>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserBetStats", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_UserBetStats_UserId_Game",
table: "UserBetStats",
columns: new[] { "UserId", "Game" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserBetStats");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
/// <inheritdoc />
public partial class rakeback : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Rakeback",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Amount = table.Column<decimal>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Rakeback", x => x.UserId);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Rakeback");
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.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

@@ -335,6 +335,54 @@ namespace NadekoBot.Migrations
b.ToTable("Blacklist");
});
modelBuilder.Entity("NadekoBot.Db.Models.ButtonRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ButtonId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<string>("Emote")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("Exclusive")
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<int>("Position")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("RoleId", "MessageId");
b.HasIndex("GuildId");
b.ToTable("ButtonRole");
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@@ -560,9 +608,6 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.HasColumnType("TEXT");
b.Property<bool>("IsClubAdmin")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -743,6 +788,29 @@ namespace NadekoBot.Migrations
b.ToTable("FilteredWord");
});
modelBuilder.Entity("NadekoBot.Db.Models.FlagTranslateChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId", "ChannelId")
.IsUnique();
b.ToTable("FlagTranslateChannel");
});
modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b =>
{
b.Property<int>("Id")
@@ -874,30 +942,33 @@ namespace NadekoBot.Migrations
b.ToTable("GiveawayUser");
});
modelBuilder.Entity("NadekoBot.Db.Models.GroupName", b =>
modelBuilder.Entity("NadekoBot.Db.Models.GuildColors", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
b.Property<string>("ErrorColor")
.HasMaxLength(9)
.HasColumnType("TEXT");
b.Property<int>("GuildConfigId")
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
b.Property<string>("OkColor")
.HasMaxLength(9)
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<string>("PendingColor")
.HasMaxLength(9)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GuildConfigId", "Number")
b.HasIndex("GuildId")
.IsUnique();
b.ToTable("GroupName");
b.ToTable("GuildColors");
});
modelBuilder.Entity("NadekoBot.Db.Models.GuildConfig", b =>
@@ -1285,6 +1356,33 @@ namespace NadekoBot.Migrations
b.ToTable("Expressions");
});
modelBuilder.Entity("NadekoBot.Db.Models.Notify", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(10000)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("GuildId", "Type");
b.ToTable("Notify");
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{
b.Property<ulong>("UserId")
@@ -1620,35 +1718,80 @@ namespace NadekoBot.Migrations
b.ToTable("RotatingStatus");
});
modelBuilder.Entity("NadekoBot.Db.Models.SelfAssignedRole", b =>
modelBuilder.Entity("NadekoBot.Db.Models.Sar", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<int>("Group")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0);
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<int>("LevelRequirement")
b.Property<int>("LevelReq")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.Property<int>("SarGroupId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId", "RoleId")
b.HasAlternateKey("GuildId", "RoleId");
b.HasIndex("SarGroupId");
b.ToTable("Sar");
});
modelBuilder.Entity("NadekoBot.Db.Models.SarAutoDelete", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId")
.IsUnique();
b.ToTable("SelfAssignableRoles");
b.ToTable("SarAutoDelete");
});
modelBuilder.Entity("NadekoBot.Db.Models.SarGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GroupNumber")
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<bool>("IsExclusive")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<ulong?>("RoleReq")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("GuildId", "GroupNumber");
b.ToTable("SarGroup");
});
modelBuilder.Entity("NadekoBot.Db.Models.ShopEntry", b =>
@@ -1897,6 +2040,36 @@ namespace NadekoBot.Migrations
b.ToTable("StreamRoleWhitelistedUser");
});
modelBuilder.Entity("NadekoBot.Db.Models.TempRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<bool>("Remove")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("GuildId", "UserId", "RoleId");
b.HasIndex("ExpiresAt");
b.ToTable("TempRole");
});
modelBuilder.Entity("NadekoBot.Db.Models.TodoModel", b =>
{
b.Property<int>("Id")
@@ -2011,18 +2184,12 @@ namespace NadekoBot.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<long>("AwardedXp")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<int>("NotifyOnLevelUp")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
@@ -2031,8 +2198,6 @@ namespace NadekoBot.Migrations
b.HasKey("Id");
b.HasIndex("AwardedXp");
b.HasIndex("GuildId");
b.HasIndex("UserId");
@@ -2379,6 +2544,60 @@ namespace NadekoBot.Migrations
b.ToTable("GreetSettings");
});
modelBuilder.Entity("NadekoBot.Services.Rakeback", b =>
{
b.Property<ulong>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("Amount")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.ToTable("Rakeback");
});
modelBuilder.Entity("NadekoBot.Services.UserBetStats", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Game")
.HasColumnType("INTEGER");
b.Property<long>("LoseCount")
.HasColumnType("INTEGER");
b.Property<long>("MaxBet")
.HasColumnType("INTEGER");
b.Property<long>("MaxWin")
.HasColumnType("INTEGER");
b.Property<decimal>("PaidOut")
.HasColumnType("TEXT");
b.Property<decimal>("TotalBet")
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<long>("WinCount")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("MaxWin");
b.HasIndex("UserId", "Game")
.IsUnique();
b.ToTable("UserBetStats");
});
modelBuilder.Entity("NadekoBot.Db.Models.AntiAltSetting", b =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)
@@ -2588,17 +2807,6 @@ namespace NadekoBot.Migrations
.IsRequired();
});
modelBuilder.Entity("NadekoBot.Db.Models.GroupName", b =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", "GuildConfig")
.WithMany("SelfAssignableRoleGroupNames")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Db.Models.IgnoredLogItem", b =>
{
b.HasOne("NadekoBot.Db.Models.LogSetting", "LogSetting")
@@ -2634,6 +2842,15 @@ namespace NadekoBot.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("NadekoBot.Db.Models.Sar", b =>
{
b.HasOne("NadekoBot.Db.Models.SarGroup", null)
.WithMany("Roles")
.HasForeignKey("SarGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("NadekoBot.Db.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)
@@ -2888,8 +3105,6 @@ namespace NadekoBot.Migrations
b.Navigation("Permissions");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
b.Navigation("SlowmodeIgnoredRoles");
@@ -2919,6 +3134,11 @@ namespace NadekoBot.Migrations
b.Navigation("Songs");
});
modelBuilder.Entity("NadekoBot.Db.Models.SarGroup", b =>
{
b.Navigation("Roles");
});
modelBuilder.Entity("NadekoBot.Db.Models.ShopEntry", b =>
{
b.Navigation("Items");

View File

@@ -46,7 +46,7 @@ public partial class Administration : NadekoModule<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)
@@ -59,7 +59,7 @@ public partial class Administration : NadekoModule<AdministrationService>
[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 : NadekoModule<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 =>
@@ -96,7 +96,7 @@ public partial class Administration : NadekoModule<AdministrationService>
var guild = (SocketGuild)ctx.Guild;
var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id);
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.server_delmsgoncmd))
.WithDescription(enabled ? "✅" : "❌");
@@ -298,18 +298,18 @@ public partial class Administration : NadekoModule<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 : NadekoModule<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();
});
}

View File

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

View File

@@ -25,6 +25,13 @@ public partial class Administration
return;
}
// the user can't aar the role which is greater or equal to the bot's highest role
if (role.Position >= ((SocketGuild)ctx.Guild).CurrentUser.GetRoles().Max(x => x.Position))
{
await Response().Error(strs.hierarchy).SendAsync();
return;
}
var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id);
if (roles.Count == 0)
await Response().Confirm(strs.aar_disabled).SendAsync();

View File

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

View File

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

View File

@@ -339,7 +339,7 @@ public class GreetService : INService, IReadyExecutor
}
catch (Exception ex)
{
Log.Error(ex, "Error sending greet dm");
Log.Warning(ex, "Unable to send Greet DM. Probably the user has closed DMs");
return false;
}

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

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

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

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

View File

@@ -0,0 +1,23 @@
using NadekoBot.Db.Models;
using System.Collections;
namespace NadekoBot.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 NadekoBot.Modules.Administration;
public interface INotifySubscriber
{
Task NotifyAsync<T>(T data, bool isShardLocal = false)
where T : struct, INotifyModel;
}

View File

@@ -0,0 +1,36 @@
using NadekoBot.Db.Models;
using NadekoBot.Modules.Administration;
namespace NadekoBot.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 NadekoBot.Db.Models;
namespace NadekoBot.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 NadekoBot.Db.Models;
namespace NadekoBot.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 NadekoBot.Db.Models;
using NadekoBot.Modules.Administration;
namespace NadekoBot.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 NadekoBot.Db.Models;
using System.Text;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
public class NotifyCommands : NadekoModule<NotifyService>
{
[Cmd]
[OwnerOnly]
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]
[OwnerOnly]
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]
[OwnerOnly]
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]
[OwnerOnly]
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 NadekoBot.Modules.Administration;
public static class NotifyKeys
{
public static TypedKey<LevelUpNotifyModel> LevelUp { get; } = new("notify:levelup");
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.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,226 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using NadekoBot.Generators;
namespace NadekoBot.Modules.Administration;
public sealed class NotifyService : IReadyExecutor, INotifySubscriber, INService
{
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)
.SendAsync();
}
public async Task EnableAsync(
ulong guildId,
ulong channelId,
NotifyType nType,
string message)
{
await using var uow = _db.GetDbContext();
await uow.GetTable<Notify>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = channelId,
Type = nType,
Message = message,
},
(_) => new()
{
Message = message,
ChannelId = channelId
},
() => new()
{
GuildId = guildId,
Type = nType
});
var eventDict = _events.GetOrAdd(nType, _ => new());
eventDict[guildId] = new()
{
GuildId = guildId,
ChannelId = channelId,
Type = nType,
Message = message
};
}
public async Task DisableAsync(ulong guildId, NotifyType nType)
{
await using var uow = _db.GetDbContext();
var deleted = await uow.GetTable<Notify>()
.Where(x => x.GuildId == guildId && x.Type == nType)
.DeleteAsync();
if (deleted == 0)
return;
if (!_events.TryGetValue(nType, out var guildsDict))
return;
guildsDict.TryRemove(guildId, out _);
}
public async Task<IReadOnlyCollection<Notify>> GetForGuildAsync(ulong guildId, int page = 0)
{
ArgumentOutOfRangeException.ThrowIfNegative(page);
await using var ctx = _db.GetDbContext();
var list = await ctx.GetTable<Notify>()
.Where(x => x.GuildId == guildId)
.OrderBy(x => x.Type)
.Skip(page * 10)
.Take(10)
.ToListAsyncLinqToDB();
return list;
}
public async Task<Notify?> GetNotifyAsync(ulong guildId, NotifyType nType)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<Notify>()
.Where(x => x.GuildId == guildId && x.Type == nType)
.FirstOrDefaultAsyncLinqToDB();
}
}

View File

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

View File

@@ -6,7 +6,7 @@ namespace NadekoBot.Modules.Administration;
public partial class Administration
{
[Group]
public partial class PlayingRotateCommands : NadekoModule<PlayingRotateService>
public partial class PlayingRotateCommands : NadekoModule<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;
@@ -241,7 +241,7 @@ public partial class Administration
return;
}
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.prot_active));
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.prot_active));
if (spam is not null)
embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true);

View File

@@ -22,6 +22,7 @@ public class ProtectionService : INService
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 : INService
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 : INService
alts.RoleId,
user);
await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id,
ProtectionType.Alting,
user.Id));
return;
}
}
@@ -194,6 +200,8 @@ public class ProtectionService : INService
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 : INService
settings.MuteTime,
settings.RoleId,
(IGuildUser)msg.Author);
await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId,
ProtectionType.Spamming,
msg.Author.Id));
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
#nullable disable
using Google.Protobuf.WellKnownTypes;
using NadekoBot.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()
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
{
@@ -174,12 +186,11 @@ public partial class Administration
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task RoleColor(Color color, [Leftover] IRole role)
public async Task RoleColor(Rgba32 color, [Leftover] IRole role)
{
try
{
var rgba32 = color.ToPixel<Rgba32>();
await role.ModifyAsync(r => r.Color = new Discord.Color(rgba32.R, rgba32.G, rgba32.B));
await role.ModifyAsync(r => r.Color = new Discord.Color(color.R, color.G, color.B));
await Response().Confirm(strs.rc(Format.Bold(role.Name))).SendAsync();
}
catch (Exception)
@@ -205,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 NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Administration;
public class TempRoleService : IReadyExecutor, INService
{
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

@@ -1,37 +1,135 @@
#nullable disable
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Administration.Services;
public sealed class PlayingRotateService : INService, IReadyExecutor
public sealed class BotActivityService : IBotActivityService, IReadyExecutor, INService
{
private readonly BotConfigService _bss;
private readonly SelfService _selfService;
private readonly IReplacementService _repService;
// private readonly Replacer _rep;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly TypedKey<ActivityPubData> _activitySetKey = new("activity.set");
public PlayingRotateService(
private readonly IPubSub _pubSub;
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly IReplacementService _rep;
private readonly BotConfigService _bss;
public BotActivityService(
IPubSub pubSub,
DiscordSocketClient client,
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;
_db = db;
_rep = rep;
_bss = bss;
}
public async Task<string> RemovePlayingAsync(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
await using var uow = _db.GetDbContext();
var toRemove = await uow.Set<RotatingPlayingStatus>()
.AsQueryable()
.AsNoTracking()
.Skip(index)
.FirstOrDefaultAsync();
if (toRemove is null)
return null;
uow.Remove(toRemove);
await uow.SaveChangesAsync();
return toRemove.Status;
}
public async Task AddPlaying(ActivityType activityType, string status)
{
await using var uow = _db.GetDbContext();
var toAdd = new RotatingPlayingStatus
{
Status = status,
Type = (NadekoBot.Db.DbActivityType)activityType
};
uow.Add(toAdd);
await uow.SaveChangesAsync();
}
public void DisableRotatePlaying()
{
_bss.ModifyConfig(bs => { bs.RotateStatuses = false; });
}
public bool ToggleRotatePlaying()
{
var enabled = false;
_bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; });
return enabled;
}
public IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses()
{
using var uow = _db.GetDbContext();
return uow.Set<RotatingPlayingStatus>().AsNoTracking().ToList();
}
public Task SetActivityAsync(string game, ActivityType? type)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = game,
Link = null,
Type = type
});
public Task SetStreamAsync(string name, string link)
=> _pubSub.Pub(_activitySetKey,
new()
{
Name = name,
Link = link,
Type = ActivityType.Streaming
});
private sealed class ActivityPubData
{
public string Name { get; init; }
public string Link { get; init; }
public ActivityType? Type { get; init; }
}
public async Task OnReadyAsync()
{
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;
@@ -57,8 +155,8 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
? rotatingStatuses[index = 0]
: rotatingStatuses[index++];
var statusText = await _repService.ReplaceAsync(playingStatus.Status, new (client: _client));
await _selfService.SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
var statusText = await _rep.ReplaceAsync(playingStatus.Status, new(client: _client));
await SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
}
catch (Exception ex)
{
@@ -66,44 +164,4 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
}
}
}
public async Task<string> RemovePlayingAsync(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
await using var uow = _db.GetDbContext();
var toRemove = await uow.Set<RotatingPlayingStatus>().AsQueryable().AsNoTracking().Skip(index).FirstOrDefaultAsync();
if (toRemove is null)
return null;
uow.Remove(toRemove);
await uow.SaveChangesAsync();
return toRemove.Status;
}
public async Task AddPlaying(ActivityType activityType, string status)
{
await using var uow = _db.GetDbContext();
var toAdd = new RotatingPlayingStatus
{
Status = status,
Type = (NadekoBot.Db.DbActivityType)activityType
};
uow.Add(toAdd);
await uow.SaveChangesAsync();
}
public bool ToggleRotatePlaying()
{
var enabled = false;
_bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; });
return enabled;
}
public IReadOnlyList<RotatingPlayingStatus> GetRotatingStatuses()
{
using var uow = _db.GetDbContext();
return uow.Set<RotatingPlayingStatus>().AsNoTracking().ToList();
}
}

View File

@@ -0,0 +1,14 @@
#nullable disable
using NadekoBot.Db.Models;
namespace NadekoBot.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 IMedusaLoaderService _medusaLoader;
private readonly ICoordinator _coord;
private readonly DbService _db;
private readonly IBotActivityService _bas;
public SelfCommands(
DiscordSocketClient client,
DbService db,
IBotStrings strings,
ICoordinator coord,
IMedusaLoaderService medusaLoader)
IMedusaLoaderService medusaLoader,
IBotActivityService bas)
{
_client = client;
_db = db;
_strings = strings;
_coord = coord;
_medusaLoader = medusaLoader;
_bas = bas;
}
@@ -62,7 +65,7 @@ public partial class Administration
var (added, updated) = await _service.RefreshUsersAsync(users);
await message.ModifyAsync(x =>
x.Embed = _sender.CreateEmbed()
x.Embed = CreateEmbed()
.WithDescription(GetText(strs.cache_users_done(added, updated)))
.WithOkColor()
.Build()
@@ -115,7 +118,7 @@ public partial class Administration
_service.AddNewAutoCommand(cmd);
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.scadd))
.AddField(GetText(strs.server),
@@ -343,7 +346,7 @@ public partial class Administration
if (string.IsNullOrWhiteSpace(str))
str = GetText(strs.no_shards_on_page);
return _sender.CreateEmbed().WithOkColor().WithDescription($"{status}\n\n{str}");
return CreateEmbed().WithOkColor().WithDescription($"{status}\n\n{str}");
})
.SendAsync();
}
@@ -496,7 +499,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 +521,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, INService
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, INService
_bss = bss;
_pubSub = pubSub;
_sender = sender;
_activitySetKey = new("activity.set");
_guildLeaveKey = new("guild.leave");
HandleStatusChanges();
_pubSub.Sub(_guildLeaveKey,
async input =>
{
@@ -71,7 +67,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
if (server.OwnerId != _client.CurrentUser.Id)
{
await server.LeaveAsync();
Log.Information("Left server {Name} [{Id}]", server.Name, server.Id);
}
else
{
@@ -395,49 +390,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
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.
@@ -453,7 +405,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
{
x.UserId,
x.Username,
x.Discriminator
})
.Where(x => users.Select(y => y.Id).Contains(x.UserId))
.ToArrayAsyncEF();
@@ -465,12 +416,11 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
UserId = x.Id,
AvatarId = x.AvatarId,
Username = x.Username,
Discriminator = x.Discriminator
});
var added = (await ctx.BulkCopyAsync(usersToAdd)).RowsCopied;
var toUpdateUserIds = presentDbUsers
.Where(x => x.Username == "Unknown" && x.Discriminator == "????")
.Where(x => x.Username.StartsWith("??"))
.Select(x => x.UserId)
.ToArray();
@@ -481,7 +431,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
.UpdateAsync(x => new DiscordUser()
{
Username = user.Username,
Discriminator = user.Discriminator,
// .award tends to set AvatarId and DateAdded to NULL, so account for that.
AvatarId = user.AvatarId,

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,27 +23,6 @@ public partial class Administration
_mute = mute;
}
private async Task<bool> CheckRoleHierarchy(IGuildUser target)
{
var curUser = ((SocketGuild)ctx.Guild).CurrentUser;
var ownerId = ctx.Guild.OwnerId;
var modMaxRole = ((IGuildUser)ctx.User).GetRoles().Max(r => r.Position);
var targetMaxRole = target.GetRoles().Max(r => r.Position);
var botMaxRole = curUser.GetRoles().Max(r => r.Position);
// bot can't punish a user who is higher in the hierarchy. Discord will return 403
// moderator can be owner, in which case role hierarchy doesn't matter
// otherwise, moderator has to have a higher role
if (botMaxRole <= targetMaxRole
|| (ctx.User.Id != ownerId && targetMaxRole >= modMaxRole)
|| target.Id == ownerId)
{
await Response().Error(strs.hierarchy).SendAsync();
return false;
}
return true;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
@@ -65,7 +44,7 @@ public partial class Administration
try
{
await _sender.Response(user)
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithErrorColor()
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
.AddField(GetText(strs.moderator), ctx.User.ToString())
@@ -85,7 +64,7 @@ public partial class Administration
catch (Exception ex)
{
Log.Warning(ex, "Exception occured while warning a user");
var errorEmbed = _sender.CreateEmbed()
var errorEmbed = CreateEmbed()
.WithErrorColor()
.WithDescription(GetText(strs.cant_apply_punishment));
@@ -96,7 +75,7 @@ public partial class Administration
return;
}
var embed = _sender.CreateEmbed().WithOkColor();
var embed = CreateEmbed().WithOkColor();
if (punishment is null)
embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString()))));
else
@@ -205,7 +184,7 @@ public partial class Administration
.Page((warnings, page) =>
{
var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString();
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
if (!warnings.Any())
embed.WithDescription(GetText(strs.warnings_none));
@@ -266,7 +245,7 @@ public partial class Administration
+ $" | {total} ({all} - {forgiven})";
});
return _sender.CreateEmbed()
return CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.warnings_list))
.WithDescription(string.Join("\n", ws));
@@ -334,7 +313,7 @@ public partial class Administration
int number,
AddRole _,
IRole role,
StoopidTime time = null)
ParsedTimespan timespan = null)
{
var punish = PunishmentAction.AddRole;
@@ -345,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()),
@@ -362,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();
}
}
@@ -370,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
@@ -378,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()),
@@ -398,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();
}
}
@@ -438,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);
@@ -465,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();
}
@@ -477,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);
var toSend = _sender.CreateEmbed()
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)
@@ -507,7 +486,7 @@ public partial class Administration
await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField("ID", userId.ToString(), true))
@@ -544,7 +523,7 @@ public partial class Administration
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user.ToString(), true)
@@ -622,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)
@@ -739,7 +718,7 @@ public partial class Administration
try { await ctx.Guild.RemoveBanAsync(user); }
catch { await ctx.Guild.RemoveBanAsync(user); }
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle("☣ " + GetText(strs.sb_user))
.AddField(GetText(strs.username), user.ToString(), true)
@@ -794,7 +773,7 @@ public partial class Administration
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.kicked_user))
.AddField(GetText(strs.username), user.ToString(), true)
@@ -811,7 +790,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);
@@ -827,7 +806,7 @@ public partial class Administration
{
var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg));
await _sender.Response(user)
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithPendingColor()
.WithDescription(dmMessage))
.SendAsync();
@@ -837,9 +816,9 @@ public partial class Administration
dmFailed = true;
}
await user.SetTimeOutAsync(time.Time);
await user.SetTimeOutAsync(timespan.Time);
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle("⏳ " + GetText(strs.timedout_user))
.AddField(GetText(strs.username), user.ToString(), true)
@@ -900,7 +879,7 @@ public partial class Administration
if (string.IsNullOrWhiteSpace(missStr))
missStr = "-";
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
.AddField(GetText(strs.invalid(missing.Count)), missStr)
.WithPendingColor();
@@ -920,7 +899,7 @@ public partial class Administration
}
}
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
await banningMessage.ModifyAsync(x => x.Embed = CreateEmbed()
.WithDescription(
GetText(strs.mass_ban_completed(
banning.Count())))
@@ -948,7 +927,7 @@ public partial class Administration
//send a message but don't wait for it
var banningMessageTask = Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithDescription(
GetText(strs.mass_kill_in_progress(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)
@@ -969,7 +948,7 @@ public partial class Administration
//wait for the message and edit it
var banningMessage = await banningMessageTask;
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
await banningMessage.ModifyAsync(x => x.Embed = CreateEmbed()
.WithDescription(
GetText(strs.mass_kill_completed(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)

View File

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

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