Compare commits

...

71 Commits
4.1.2 ... 4.2.2

Author SHA1 Message Date
Kwoth
dbeb83561a Upped version, updated CHANGELOG.md 2022-06-15 21:39:42 +02:00
Kwoth
6c11d11645 Rss errors will now show error counter until deletion 2022-06-15 13:18:00 +02:00
Kwoth
e9923a7691 Fixed bugs when users update their patreon pledge. Updated some packages 2022-06-15 13:11:27 +02:00
Kwoth
5fbe93d898 Possible fix for pledge updates 2022-06-15 10:24:09 +02:00
Kwoth
65995bdca4 Added missing patron tiers. Servers whose owner is bot owner will get excluded from patron quota. Use discord slowmode or cmdcd + nadeko permissions to limit usage 2022-06-15 10:11:46 +02:00
Kwoth
f7c333b671 Ignoring error if creds_example.yml fails to generate, as this happens in docker containers 2022-06-15 07:48:57 +02:00
Kwoth
f9d18aa086 Added enabled property to .config patron just so it's not empty 2022-06-14 14:43:00 +02:00
Kwoth
571e1c801f Upped version to 4.2.1, updated CHANGELOG.md 2022-06-14 14:36:53 +02:00
Kwoth
f922543d33 Fixed .exexport, .savechat, and .quoteexport, closes #358 2022-06-14 14:31:43 +02:00
Kwoth
6bec67006c Fixed plaintext-only embeds. closes #360 2022-06-14 14:26:59 +02:00
Kwoth
050eaa48eb Fixed greet message footer not showing, closes #359 2022-06-14 13:57:10 +02:00
Kwoth
248ce8b3d2 Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-06-14 13:55:26 +02:00
Kwoth
04a488cdf2 Updated .gitlab-ci.yml, to fix medusa versioning and docker builds. hopefully for the last time 2022-06-14 13:55:19 +02:00
Kwoth
6bc2fc88f9 Update responses.uk-UA.json (POEditor.com) 2022-06-14 11:34:10 +00:00
Kwoth
69b6ed6a49 Update responses.es-ES.json (POEditor.com) 2022-06-14 11:34:09 +00:00
Kwoth
e30b126726 Update responses.ru-RU.json (POEditor.com) 2022-06-14 11:34:08 +00:00
Kwoth
a5e2321c5b Update responses.pt-BR.json (POEditor.com) 2022-06-14 11:34:07 +00:00
Kwoth
322e9a329d Update responses.pl-PL.json (POEditor.com) 2022-06-14 11:34:06 +00:00
Kwoth
7ca6ab8562 Update responses.it-IT.json (POEditor.com) 2022-06-14 11:34:05 +00:00
Kwoth
8a27dcc481 Update responses.id-ID.json (POEditor.com) 2022-06-14 11:34:03 +00:00
Kwoth
bed61c521f Update responses.de-DE.json (POEditor.com) 2022-06-14 11:34:02 +00:00
Kwoth
46ea1698eb Update responses.fr-FR.json (POEditor.com) 2022-06-14 11:34:01 +00:00
Kwoth
c47417024d Update responses.nl-NL.json (POEditor.com) 2022-06-14 11:34:00 +00:00
Kwoth
eedc2d05ff Update responses.zh-TW.json (POEditor.com) 2022-06-14 11:33:59 +00:00
Kwoth
d24dba7ed0 Update responses.zh-CN.json (POEditor.com) 2022-06-14 11:33:58 +00:00
Kwoth
9bdf58ec27 Update responses.ar.json (POEditor.com) 2022-06-14 11:33:56 +00:00
Kwoth
5de9c5d067 4.2.0 release 2022-06-14 13:21:12 +02:00
Kwoth
64b2a46c95 Re-added .google search result scraper and set it as the default again. Also added versioning to searches.yml as it was forgotten previously 2022-06-14 13:18:35 +02:00
Kwoth
f42deda3e2 Updated .gitlab-ci.yml. Fixed tags in Nadeko.Medusa and docker images 2022-06-14 11:21:18 +02:00
Kwoth
a464e7c643 Updated .gitlab-ci.yml 2022-06-14 11:07:28 +02:00
Kwoth
1f36fa75c4 Updated .gitlab-ci.yml 2022-06-14 10:52:08 +02:00
Kwoth
ad6d732687 Fixed .gitlab-ci for medusa build 2022-06-14 09:42:58 +02:00
Kwoth
1f51c54449 Merge branch 'v4-prem' into 'v4'
NadekoBot Patronage system, Search commands improvements + fixes

See merge request Kwoth/nadekobot!252
2022-06-14 07:24:34 +00:00
Kwoth
7b5145f116 NadekoBot Patronage system, Search commands improvements + fixes 2022-06-14 07:24:33 +00:00
Kwoth
18b10b8c6f Merge branch 'hokutochen-v4-patch-19985' into 'v4'
Updated Docs regarding source and expressions along with help text

See merge request Kwoth/nadekobot!247
2022-05-27 23:55:57 +00:00
Hokuto Chen
f05435f864 Updated Docs regarding source and expressions along with help text 2022-05-27 23:55:57 +00:00
Kwoth
7cbedc82bf Merge branch 'fix/docker' into 'v4'
Reduce Docker image size

See merge request Kwoth/nadekobot!249
2022-05-26 15:11:52 +00:00
Manuel
3be208f1b3 Reduce Docker image size 2022-05-26 17:01:06 +02:00
Kwoth
2606bda8df Upped Nadeko.Medusa.csproj to 1.0.3 (thx cata) 2022-05-20 10:35:16 +02:00
Kwoth
ab1272b491 Fixed .crypto sparklines 2022-05-17 17:05:30 +02:00
Kwoth
43047c0ab0 [ci] updated gitlab-ci - reverted dotnet sdk and runtime versions 2022-05-14 05:09:58 +02:00
Kwoth
34471abd64 Fixed a warning during windows publish, thx kotz 2022-05-14 04:34:53 +02:00
Kwoth
fa259384f1 Possible fix for the windows release, updated packages 2022-05-14 04:27:16 +02:00
Kwoth
cb865d5012 Updated packages 2022-05-12 23:56:12 +02:00
Kwoth
1db97decd1 Fixed rule34, small refactor of the downloader classes 2022-05-12 10:23:01 +02:00
Kwoth
b02768a08e User friendlier errors in the console when getting rss feeds fails 2022-05-11 12:43:59 +02:00
Kwoth
e55d60f1aa Small fix for the changelog 2022-05-11 12:39:02 +02:00
Kwoth
b009438e0e Fixed .stock command, closes #356 2022-05-11 12:36:50 +02:00
Kwoth
4b5d27d963 Fixed an issue which caused onmessage handlers to not block further execution properly, causing execution of both custom reactions and commands, ref #357 2022-05-11 11:51:01 +02:00
Kwoth
91ee0d121c DM send by the bot when being the target of a .give command will now once again contain amount. 2022-05-07 09:32:27 +02:00
Kwoth
a8767f1136 .clubdesc will now have a nicer response 2022-05-07 08:28:23 +02:00
Kwoth
44478e0f47 Upped version to 4.1.4 2022-05-06 19:15:29 +02:00
Kwoth
c73c2da6a4 fixed .yun, closes #332 2022-05-06 04:31:44 +02:00
Kwoth
5ed005211e Added bank information to .economy 2022-05-05 23:14:58 +02:00
Kwoth
d80cbb4647 Refactors. Cleanup. Refactored responses source gen a little. Parametrized localized strings are now generic. Refactored .cash bank interaction. Updated changelog. Reverted clubapps/ban/unban to use .ToString method and configured it to not have right to left unicode. Added extension methods to SocketMessageComponent akin to ones on the IMessageChannel (RespondConfirm, etc...) 2022-05-05 22:59:07 +02:00
Kwoth
9a96ef76ba Reverted smarttext to newtonsoft to relax the allowed json rules. Upped version to 4.1.3 2022-05-05 05:00:47 +02:00
Kwoth
5b5bc278ff - Reaction roles rewritten completely. They now support multiple exclusivity groups per message and level requirements. However they can only be added one by one
- Bot now support much higher XP values for global and server levels
2022-05-05 04:47:31 +02:00
Kwoth
5cb95cf94d Fixed an issue with embed array not building if the color is invalid or unspecified 2022-05-01 19:20:48 +02:00
Kwoth
f132aa2624 - 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.
- Added a button on a .$ command which, when clicked, sends you a message with your bank balance that only you can see.
- Updated pagination, it now uses buttons instead of reactions
- using .h <command group> (atm only .bank is a proper group) will list commands with their descriptions in that group
2022-04-29 07:22:49 +02:00
Kwoth
3b6b3bcf07 Added missing postgresql and mysql stondel migrations 2022-04-28 06:46:35 +02:00
Kwoth
78d97db224 .modules commanad now has a medusa module description 2022-04-28 06:20:46 +02:00
Kwoth
35ddd150ba Pagination is now using buttons instead of reactions 2022-04-28 06:10:06 +02:00
Kwoth
39ae070c9d Added .stondel which makes the bot delete stream online messages after the stream goes offline 2022-04-28 01:23:08 +02:00
Kwoth
24a9a02cc3 .give will send dms again 2022-04-27 00:36:27 +02:00
Kwoth
0f68abcac9 Fixed .deletexp command 2022-04-26 14:15:58 +02:00
Kwoth
908c61633d Added bash script prereq installer reference 2022-04-26 02:47:37 +02:00
Kwoth
054fc30672 Added prerequisites to linux release installation guide 2022-04-26 02:45:54 +02:00
Kwoth
11ffdd84a3 Updated changelog 2022-04-26 02:34:52 +02:00
Kwoth
5d2d74b92a Full support for embed arrays in .greet/.bye, .say and other commands which use embeds
- Website to create them is live at eb2.nadeko.bot (it will soon be replacing eb.nadeko.bot)
- Embed arrays don't have a plainText property (it's renamed to 'content')
- Embed arrays use color hex values instead of an integer
- Old embed format will still work
- There shouldn't be any breaking changes
2022-04-26 02:33:19 +02:00
Kwoth
18400dc53a Fixed a nullref message when the bot is loading medusae. Added support for multiple embeds in features which support custom embeds in the form of
{plainText:text-here, embeds: [embedObject, embedObject, embedObject]}
2022-04-17 09:58:30 +02:00
Kwoth
29d94640af Fix some build warnings 2022-04-16 19:00:28 +02:00
255 changed files with 48157 additions and 3342 deletions

View File

@@ -7,6 +7,7 @@ stages:
- release
- publish-windows
- upload-windows-updater-release
- publish-medusa-package
variables:
project: "NadekoBot"
@@ -18,41 +19,41 @@ variables:
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/NadekoBot-build/${CI_COMMIT_TAG}"
INSTALLER_OUTPUT_DIR: "nadeko-installers/${CI_COMMIT_TAG}"
INSTALLER_FILE_NAME: "nadeko-setup-${CI_COMMIT_TAG}.exe"
build:
stage: build
script:
- "dotnet publish -c Release -r linux-x64 -o $LINUX_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
- "dotnet publish -c Release -r win7-x64 -o $WIN_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
- "dotnet publish -c Release -r linux-x64 --self-contained -o $LINUX_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
- "dotnet publish -c Release -r win7-x64 --self-contained -o $WIN_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
artifacts:
paths:
- "$LINUX_X64_OUTPUT_DIR/"
- "$WIN_X64_OUTPUT_DIR/"
upload-builds:
stage: upload-builds
image: alpine:latest
rules:
- if: $CI_COMMIT_TAG
script:
- apk add --no-cache curl tar zip
- "tar cvf $LINUX_X64_RELEASE $LINUX_X64_OUTPUT_DIR/*"
- "zip -r $WIN_X64_RELEASE $WIN_X64_OUTPUT_DIR/*"
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $LINUX_X64_RELEASE $PACKAGE_REGISTRY_URL/$LINUX_X64_RELEASE
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $WIN_X64_RELEASE $PACKAGE_REGISTRY_URL/$WIN_X64_RELEASE
stage: upload-builds
image: alpine:latest
rules:
- if: $CI_COMMIT_TAG
script:
- apk add --no-cache curl tar zip
- "tar cvf $LINUX_X64_RELEASE $LINUX_X64_OUTPUT_DIR/*"
- "zip -r $WIN_X64_RELEASE $WIN_X64_OUTPUT_DIR/*"
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $LINUX_X64_RELEASE $PACKAGE_REGISTRY_URL/$LINUX_X64_RELEASE
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $WIN_X64_RELEASE $PACKAGE_REGISTRY_URL/$WIN_X64_RELEASE
release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- |
release-cli create --name "NadekoBot v$CI_COMMIT_TAG" --description "## [Changelog](https://gitlab.com/Kwoth/nadekobot/-/blob/v4/CHANGELOG.md#$(echo "$CI_COMMIT_TAG" | sed "s/\.//g")-$(date +%d%m%Y))" --tag-name $CI_COMMIT_TAG \
--assets-link "{\"name\":\"${LINUX_X64_RELEASE}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_X64_RELEASE}\"}" \
--assets-link "{\"name\":\"${WIN_X64_RELEASE}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WIN_X64_RELEASE}\"}"
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- |
release-cli create --name "NadekoBot v$CI_COMMIT_TAG" --description "## [Changelog](https://gitlab.com/Kwoth/nadekobot/-/blob/v4/CHANGELOG.md#$(echo "$CI_COMMIT_TAG" | sed "s/\.//g")-$(date +%d%m%Y))" --tag-name $CI_COMMIT_TAG \
--assets-link "{\"name\":\"${LINUX_X64_RELEASE}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_X64_RELEASE}\"}" \
--assets-link "{\"name\":\"${WIN_X64_RELEASE}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WIN_X64_RELEASE}\"}"
test:
stage: test
@@ -63,63 +64,74 @@ test:
- "dotnet test"
publish-windows:
stage: publish-windows
rules:
- if: '$CI_COMMIT_TAG'
image: scottyhardy/docker-wine
before_script:
- choco install dotnet-6.0-runtime -y
- choco install dotnet-6.0-sdk -y
- choco install innosetup -y
artifacts:
paths:
- "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
script:
- dotnet clean
- dotnet restore
- dotnet publish -c Release --runtime win7-x64 /p:Version=$CI_COMMIT_TAG src/NadekoBot
- $env:NADEKOBOT_INSTALL_VERSION = $CI_COMMIT_TAG
- iscc.exe "/O+" ".\exe_builder.iss"
tags:
- windows
stage: publish-windows
rules:
- if: "$CI_COMMIT_TAG"
image: scottyhardy/docker-wine
before_script:
- choco install dotnet-6.0-runtime --version=6.0.4 -y
- choco install dotnet-6.0-sdk --version=6.0.202 -y
- choco install innosetup -y
artifacts:
paths:
- "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
script:
- dotnet clean
- dotnet restore -f --no-cache -v n
- dotnet publish -c Release --runtime win7-x64 /p:Version=$CI_COMMIT_TAG src/NadekoBot
- $env:NADEKOBOT_INSTALL_VERSION = $CI_COMMIT_TAG
- iscc.exe "/O+" ".\exe_builder.iss"
tags:
- windows
upload-windows-updater-release:
stage: upload-windows-updater-release
rules:
- if: '$CI_COMMIT_TAG'
image:
name: amazon/aws-cli
entrypoint: [""]
script:
- sed -i "s/_INSTALLER_FILE_NAME_/$INSTALLER_FILE_NAME/g" releases-v3.json
- sed -i "s/_VERSION_/$CI_COMMIT_TAG/g" releases-v3.json
- aws --version
- aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/$INSTALLER_FILE_NAME" --acl public-read --body "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
- aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/releases-v3.json" --acl public-read --body "releases-v3.json"
stage: upload-windows-updater-release
rules:
- if: "$CI_COMMIT_TAG"
image:
name: amazon/aws-cli
entrypoint: [""]
script:
- sed -i "s/_INSTALLER_FILE_NAME_/$INSTALLER_FILE_NAME/g" releases-v3.json
- sed -i "s/_VERSION_/$CI_COMMIT_TAG/g" releases-v3.json
- aws --version
- aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/$INSTALLER_FILE_NAME" --acl public-read --body "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
- aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/releases-v3.json" --acl public-read --body "releases-v3.json"
publish-medusa-package:
stage: publish-medusa-package
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script:
- LAST_TAG=$(git describe --tags --abbrev=0)
- if [ $CI_COMMIT_TAG ];then MEDUSA_VERSION="$CI_COMMIT_TAG"; else MEDUSA_VERSION="$LAST_TAG-$CI_COMMIT_SHA"; fi
- cd src/Nadeko.Medusa/
- dotnet pack -c Release /p:Version=$MEDUSA_VERSION -o bin/Release/packed
- dotnet nuget push bin/Release/packed/ --source https://www.myget.org/F/nadeko/api/v2/package --api-key "$MYGET_API_KEY"
docker-build:
# Use the official docker image.
image: docker:latest
stage: build
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# Default branch leaves tag empty (= latest tag)
# All other branches are tagged with the escaped branch name (commit ref slug)
script:
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
# Use the official docker image.
image: docker:latest
stage: build
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# Default branch leaves tag empty (= latest tag)
# All other branches are tagged with the escaped branch name (commit ref slug)
script:
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_SHA"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
exists:
- Dockerfile

View File

@@ -1,8 +1,222 @@
# Changelog
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## Unreleased
## [4.2.2] - 15.06.2022
### Fixed
- Added missing Patron Tiers and fixed Patron pledge update bugs
- Prevented creds_example.yml error in docker containers from crashing it
### Changed
- Rss feeds will now show error counter before deletion
## [4.2.1] - 14.06.2022
### Added
- Localized strings updated
### Fixed
- Fixed `.exexport`, `.savechat`, and `.quoteexport`
- Fixed plaintext-only embeds
- Fixed greet message footer not showing origin server
## [4.2.0] - 14.06.2022
### Added
- Added `data/searches.yml` file which configures some of the new search functionality
The file comments explaining what each property does.
Explained briefly here:
```yml
# what will be used for .google command. Either google (official api) or searx
webSearchEngine: Google
# what will be used for .img command. Either google (official api) or searx
imgSearchEngine: Google
# how will yt results be retrieved: ytdataapi or ytdl or ytdlp
ytProvider: YtDataApiv3
# in case web or img search is set to searx, the following instances will be used:
searxInstances: []
# in case ytProvider is set to invidious, the following instances will be used
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.
`searchId` is used for web search
`searchimageId` is used for image search
```yml
google:
searchId: ""
searchImageId: ""
```
- 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
- 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
- Tier X - 10$ - 19.99$ a month
- Tier XX - 20$ - 49.99$ a month
- Tier L - 50$ - 99.99$ a month
- 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
- Quota system applies to entire server owner by a patron
- Patron spends own quota by using the commands on any server
- Any user on *any* server owned by a patron spends that patron's quota
- When users subscribe to patreon they will receive a welcome message
- 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.
### Changed
- CurrencyReward now support adding additional flowers to patrons.
- `.donate` command completely reworked.
- Works only on public bot (OnlyPublicBotAttribute)
- Guides user on how to donate to support the project
- 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
- 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`
- `.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
- Xp number outline is now slightly thicker
- Xp number will now have Center vertical and horizontal alignment
- LastLevelUp no longer supported
- Some commands will now use timestamp tags for better user experience
- `.prune` was slightly slowed down to avoid ratelimits
- `.wof` moved from it's own group to the default Gambling group
- `.feed` urls which error for more than 100 times will be automatically removed.
- `.ve` is now enabled by default
- [dev] nadeko interaction slightly improved to make it less nonsense (they still don't make sense)
- [dev] RewardedUsers table slightly changed to make it more general
- [dev] renamed `// todo`s which aren't planned soon to `// FUTURE`
- [dev] currency rewards have been reimplemented and moved to a separate service
### Fixed
- `.rh` no longer needs quotes for multi word roles
- `.deletexp` will now properly delete server xp too
- Fixed `.crypto` sparklines
- [dev] added support for configs to properly parse enums without case sensitivity (ConfigParsers.InsensitiveEnum)
- [dev] Fixed a bug in .gencmdlist
- [dev] small fixes to creds provider
### Removed
- `.ddg` removed.
- [dev] removed some dead code and comments
## [4.1.6] - 14.05.2022
### Fixed
- Fixed windows release and updated packages
## [4.1.5] - 11.05.2022
### Changed
- `.clubdesc <msg>` will now have a nicer response
### Fixed
- `.give` DM will once again show an amount
- Fixed an issue with filters not working and with custom reactions no longer being able to override commands.
- Fixed `.stock` command
## [4.1.4] - 06.05.2022
### Fixed
- Fixed `.yun`
## [4.1.3] - 06.05.2022
### Added
- Added support for embed arrays in commands such as .say, .greet, .bye, etc...
- Website to create them is live at eb.nadeko.bot (old one is moved to oldeb.nadeko.bot)
- Embed arrays don't have a plainText property (it's renamed to 'content')
- 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 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.
- Added a button on a .$ command which, when clicked, sends you a message with your bank balance that only you can see.
- Added `.h <command group>`
- Using this command will list all commands in the specified group
- Atm only .bank is a proper group (`.h bank`)
- Added "Bank Accounts" entry to `.economy`
### Changed
- Reaction roles rewritten completely
- Supports multiple exclusivity groups per message
- Supports level requirements
- However they can only be added one by one
- Use the following commands for more information
- `.h .reroa`
- `.h .reroli`
- `.h .rerot`
- `.h .rerorm`
- `.h .rerodela`
- Pagination is now using buttons instead of reactions
- 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
- `.give` command should send DMs again
- `.modules` command now has a medusa module description
## [4.1.2] - 16.04.2022

View File

@@ -25,12 +25,14 @@ WORKDIR /app
RUN set -xe; \
useradd -m nadeko; \
apt-get update; \
apt-get install -y libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 python3-pip sudo; \
apt-get install -y --no-install-recommends libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 python3-pip sudo; \
update-alternatives --install /usr/bin/python python /usr/bin/python3.9 1; \
echo 'Defaults>nadeko env_keep+="ASPNETCORE_* DOTNET_* NadekoBot_* shard_id total_shards TZ"' > /etc/sudoers.d/nadeko; \
pip3 install --upgrade youtube-dl; \
apt-get remove -y python3-pip; \
chmod +x /usr/local/bin/youtube-dl
pip3 install --no-cache-dir --upgrade youtube-dl; \
apt-get purge -y python3-pip; \
chmod +x /usr/local/bin/youtube-dl; \
apt-get autoremove -y; \
apt-get autoclean -y
COPY --from=build /app ./
COPY docker-entrypoint.sh /usr/local/sbin

View File

@@ -1,23 +1,23 @@
## Custom Reactions / Expressions
## Expressions
### Important
- For modifying **global** custom reactions, the ones which will work across all the servers your bot is connected to, you **must** be a Bot Owner.
- For modifying **global** expressions, the ones which will work across all the servers your bot is connected to, you **must** be a Bot Owner.
You must also use the commands for adding, deleting and listing these reactions in a direct message with the bot.
- For modifying **local** custom reactions, the ones which will only work on the server that they are added on, it is required to have the **Administrator** permission.
You must also use the commands for adding, deleting and listing these reactions in the server you want the custom reactions to work on.
- For modifying **local** expressions, the ones which will only work on the server that they are added on, it is required to have the **Administrator** permission.
You must also use the commands for adding, deleting and listing these reactions in the server you want the expressions to work on.
### Commands and Their Use
| Command Name | Description | Example |
| :----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| `.acr` | Add a custom reaction with a trigger and a response. Running this command in a server requries the Administrator permission. Running this command in DM is Bot Owner only, and adds a new global custom reaction. | `.acr "hello" Hi there, %user%!` |
| `.lcr` | Lists a page of global or server custom reactions (15 reactions per page). Running this command in a DM will list the global custom reactions, while running it in a server will list that server's custom reactions. | `.lcr 1` |
| `.dcr` | Deletes a custom reaction based on the provided index. Running this command in a server requires the Administrator permission. Running this command in DM is Bot Owner only, and will delete a global custom reaction. | `.dcr 5` |
| `.exadd` | Add an expression with a trigger and a response. Running this command in a server requries the Administrator permission. Running this command in DM is Bot Owner only, and adds a new global expression. | `.exadd "hello" Hi there, %user%!` |
| `exl` | Lists a page of global or server expression(15 reactions / expressions per page). Running this command in a DM will list the global expression, while running it in a server will list that server's expression. | `.exl 1` |
| `.exd` | Deletes an expression based on the provided index. Running this command in a server requires the Administrator permission. Running this command in DM is Bot Owner only, and will delete a global expression. | `.exd 5` |
#### Now that we know the commands let's take a look at an example of adding a command with `.acr`,
`.acr "Nice Weather" It sure is, %user%!`
`.exadd "Nice Weather" It sure is, %user%!`
This command can be split into two different arguments:
@@ -28,16 +28,16 @@ An important thing to note about the triger is that, to be more than one word, w
There's no special requirement for the formatting of the response, so we could just write it in exactly the same way we want it to respond, albeit with a placeholder - which will be explained in this next section.
Now, if that command was ran in a server, anyone on that server can make the bot mention them, saying `It sure is, @Username` anytime they say "Nice Weather". If the command is ran in a direct message with the bot, then the custom reaction can be used on every server the bot is connected to.
Now, if that command was ran in a server, anyone on that server can make the bot mention them, saying `It sure is, @Username` anytime they say "Nice Weather". If the command is ran in a direct message with the bot, then the expression can be used on every server the bot is connected to.
### Block global Custom Reactions
### Block global Expressions
If you want to disable a global custom reaction which you do not like, and you do not want to remove it, or you are not the bot owner, you can do so by adding a new Custom Reaction with the same trigger on your server, and set the response to `-`.
If you want to disable a global expression which you do not like, and you do not want to remove it, or you are not the bot owner, you can do so by adding a new expression with the same trigger on your server, and set the response to `-`.
For example:
`.acr /o/ -`
Now if you try to trigger `/o/`, it won't print anything even if there is a global custom reaction with the same name.
Now if you try to trigger `/o/`, it won't print anything even if there is a global expression with the same name.
### Placeholders!

View File

@@ -10,7 +10,7 @@ Donating to us also gives you the following benefits:
- A hoisted **Donators role** in our [Discord server][discord-server]
- Access to exclusive **#noticed** text and voice channels
- **1000 flowers** on the public bot per dollar donated (after fees)
- **Custom Reactions** on the public bot for [Patreon pledges][patreon] of $5 or higher
- **Expressions** on the public bot for [Patreon pledges][patreon] of $5 or higher
## Patreon

View File

@@ -92,6 +92,19 @@ Open Terminal (if you're on an installation with a window manager) and navigate
##### Release Update Instructions
###### Prerequisites
1. Nadeko requires redis to function
- ubuntu installation command: `sudo apt-get install redis-server`
2. Playing music requires `ffmpeg`, `libopus`, `libsodium` and `youtube-dl` (which in turn requires python3)
- ubuntu installation command: `sudo apt-get install ffmpeg libopus0 opus-tools libopus-dev libsodium-dev -y`
3. Make sure your python is version 3+ with `python --version`
- if it's not, you can install python 3 and make it the default with: `sudo apt-get install python3.8 python-is-python3`
*You can use nadeko bash script [prerequisites installer](https://gitlab.com/Kwoth/nadeko-bash-installer/-/blob/v4/n-prereq.sh) as a reference*
###### Installation
1. Stop the bot
2. Download the latest release from <https://gitlab.com/Kwoth/nadekobot/-/releases>
- Look for the file called "x.x.x-linux-x64-build.tar" (where `X.X.X` is a version, for example 3.0.4) and download it

View File

@@ -17,8 +17,6 @@ To self-host your own Nadeko, use the guides below:
- [:material-linux: Linux guide][linux-guide]
- [:material-apple: Mac OS guide][macos-guide]
Alternatively, you may also setup the bot [from source][from-source-guide] if you want to modify the code.
In case you need any help, join our [Discord server][discord-server] where we may provide support.
---

View File

@@ -73,14 +73,14 @@ Say you want to only enable NSFW commands for a specific role, just do the follo
2. `.rm NSFW enable Lewd`
- Enables usage of the NSFW module for the Lewd role
#### How do I disable custom reactions from triggering?
#### How do I disable Expressions from triggering?
If you don't want server or global custom reactions, just block the module that controls their usage:
If you don't want server or global Expressions, just block the module that controls their usage:
1. `.sm ActualCustomReactions disable`
1. `.sm Expressions disable`
- Disables the ActualCustomReactions module from being used
**Note**: The `ActualCustomReactions` module controls the usage of custom reactions. The `CustomReactions` module controls commands related to custom reactions (such as `.acr`, `.lcr`, `.crca`, etc).
**Note**: The `Expressions` module controls the usage of Expressions. The `Expressions` module controls commands related to Expressions (such as `.acr`, `.lcr`, `.crca`, etc).
#### I've broken permissions and am stuck, can I reset permissions?

View File

@@ -1,6 +1,6 @@
# Placeholders
Placeholders are used in Quotes, Custom Reactions, Greet/Bye messages, playing statuses, and a few other places.
Placeholders are used in Quotes, Expressions, Greet/Bye messages, playing statuses, and a few other places.
They can be used to make the message more user friendly, generate random numbers or pictures, etc.
@@ -88,7 +88,7 @@ Some features have their own specific placeholders which are noted in that featu
### Miscellaneous placeholders
- `%rngX-Y%` - Returns a random number between X and Y
- `%target%` - Returns anything the user has written after the trigger (only works on custom reactions)
- `%img:stuff%` - Returns an `imgur.com` search for "stuff" (only works on custom reactions)
- `%target%` - Returns anything the user has written after the trigger (only works on Expressions)
- `%img:stuff%` - Returns an `imgur.com` search for "stuff" (only works on Expressions)
![img](https://puu.sh/B7mgI.png)

View File

@@ -5,7 +5,7 @@ namespace NadekoBot;
public interface IEmbedBuilder
{
IEmbedBuilder WithDescription(string? desc);
IEmbedBuilder WithTitle(string title);
IEmbedBuilder WithTitle(string? title);
IEmbedBuilder AddField(string title, object value, bool isInline = false);
IEmbedBuilder WithFooter(string text, string? iconUrl = null);
IEmbedBuilder WithAuthor(string name, string? iconUrl = null, string? url = null);

View File

@@ -9,12 +9,11 @@
<RootNamespace>Nadeko.Snake</RootNamespace>
<Authors>The NadekoBot Team</Authors>
<Version>1.0.2</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net.Core" Version="3.5.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Discord.Net.Core" Version="3.6.1" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>

View File

@@ -9,10 +9,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.44.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Grpc.AspNetCore" Version="2.45.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>

View File

@@ -97,13 +97,12 @@ public class CmdAttribute : System.Attribute
var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs";
try
{
Debug.WriteLine($"Writing {name}");
var source = GetSourceText(model);
ctx.AddSource(name, SourceText.From(source, Encoding.UTF8));
}
catch (Exception ex)
{
Debug.WriteLine($"Error writing source file {name}\n" + ex);
Console.WriteLine($"Error writing source file {name}\n" + ex);
}
}
}

View File

@@ -62,6 +62,7 @@ namespace NadekoBot.Generators
sw.WriteLine("{");
sw.Indent++;
var typedParamStrings = new List<string>(10);
foreach (var field in fields)
{
var matches = Regex.Matches(field.Value, @"{(?<num>\d)[}:]");
@@ -71,20 +72,30 @@ namespace NadekoBot.Generators
max = Math.Max(max, int.Parse(match.Groups["num"].Value) + 1);
}
List<string> typedParamStrings = new List<string>();
var paramStrings = string.Empty;
typedParamStrings.Clear();
var typeParams = new string[max];
var passedParamString = string.Empty;
for (var i = 0; i < max; i++)
{
typedParamStrings.Add($"object p{i}");
paramStrings += $", p{i}";
typedParamStrings.Add($"in T{i} p{i}");
passedParamString += $", p{i}";
typeParams[i] = $"T{i}";
}
var sig = string.Empty;
if(max > 0)
var typeParamStr = string.Empty;
if (max > 0)
{
sig = $"({string.Join(", ", typedParamStrings)})";
sw.WriteLine($"public static LocStr {field.Name}{sig} => new LocStr(\"{field.Name}\"{paramStrings});");
typeParamStr = $"<{string.Join(", ", typeParams)}>";
}
sw.WriteLine("public static LocStr {0}{1}{2} => new LocStr(\"{3}\"{4});",
field.Name,
typeParamStr,
sig,
field.Name,
passedParamString);
}
sw.Indent--;

View File

@@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
</ItemGroup>
</Project>

View File

@@ -4,9 +4,11 @@ using NadekoBot.Common.Configs;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
using NadekoBot.Modules.Utility;
using NadekoBot.Services.Database.Models;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Net;
using System.Reflection;
using RunMode = Discord.Commands.RunMode;
@@ -67,12 +69,13 @@ public sealed class Bot
? GatewayIntents.All
: GatewayIntents.AllUnprivileged,
LogGatewayIntentWarnings = false,
FormatUsersInBidirectionalUnicode = false,
});
_commandService = new(new()
{
CaseSensitiveCommands = false,
DefaultRunMode = RunMode.Sync
DefaultRunMode = RunMode.Sync,
});
// _interactionService = new(Client.Rest);
@@ -124,6 +127,12 @@ public sealed class Bot
{
AllowAutoRedirect = false
});
svcs.AddHttpClient("google:search")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});
if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
@@ -163,6 +172,7 @@ public sealed class Bot
//initialize Services
Services = svcs.BuildServiceProvider();
Services.GetRequiredService<IBehaviorHandler>().Initialize();
Services.GetRequiredService<CurrencyRewardService>();
if (Client.ShardId == 0)
ApplyConfigMigrations();

View File

@@ -0,0 +1,11 @@
#nullable disable
namespace NadekoBot.Common;
/// <summary>
/// Classed marked with this attribute will not be added to the service provider
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class DontAddToIocContainerAttribute : Attribute
{
}

View File

@@ -20,11 +20,19 @@ public sealed class NoPublicBotAttribute : PreconditionAttribute
}
}
/// <summary>
/// Classed marked with this attribute will not be added to the service provider
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class DontAddToIocContainerAttribute : Attribute
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
#if GLOBAL_NADEKO || DEBUG
return Task.FromResult(PreconditionResult.FromSuccess());
#else
return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
#endif
}
}

View File

@@ -18,7 +18,7 @@ public sealed class Creds : IBotCredentials
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
public bool UsePrivilegedIntents { get; set; }
[Comment(@"The number of shards that the bot will running on.
[Comment(@"The number of shards that the bot will be running on.
Leave at 1 if you don't know what you're doing.")]
public int TotalShards { get; set; }
@@ -27,6 +27,16 @@ Leave at 1 if you don't know what you're doing.")]
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
Used only for Youtube Data Api (at the moment).")]
public string GoogleApiKey { get; set; }
[Comment(
@"Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
Enable SafeSearch
Remove all Sites to Search
Enable Search the entire web
Copy the 'Search Engine ID' to the SearchId field
Do all steps again but enable image search for the ImageSearchId")]
public GoogleApiConfig Google { get; set; }
[Comment(@"Settings for voting system for discordbots. Meant for use on global Nadeko.")]
public VotesSettings Votes { get; set; }
@@ -119,6 +129,7 @@ Windows default
CoordinatorUrl = "http://localhost:3442";
RestartCommand = new();
Google = new();
}
@@ -200,4 +211,10 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
DiscordsKey = discordsKey;
}
}
}
public class GoogleApiConfig
{
public string SearchId { get; init; }
public string ImageSearchId { get; init; }
}

View File

@@ -25,6 +25,7 @@ public interface IBotCredentials
string CoordinatorUrl { get; set; }
string TwitchClientId { get; set; }
string TwitchClientSecret { get; set; }
GoogleApiConfig Google { get; set; }
}
public class RestartConfig

View File

@@ -0,0 +1,29 @@
namespace NadekoBot;
public sealed class NadekoButtonActionInteraction : NadekoButtonOwnInteraction
{
private readonly NadekoInteractionData _data;
private readonly Func<SocketMessageComponent, Task> _action;
public NadekoButtonActionInteraction(
DiscordSocketClient client,
ulong authorId,
NadekoInteractionData data,
Func<SocketMessageComponent, Task> action
)
: base(client, authorId)
{
_data = data;
_action = action;
}
protected override string Name
=> _data.CustomId;
protected override IEmote Emote
=> _data.Emote;
protected override string? Text
=> _data.Text;
public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
=> _action(smc);
}

View File

@@ -0,0 +1,83 @@
namespace NadekoBot;
public abstract class NadekoButtonInteraction
{
// improvements:
// - state in OnAction
// - configurable delay
// -
protected abstract string Name { get; }
protected abstract IEmote Emote { get; }
protected virtual string? Text { get; } = null;
public DiscordSocketClient Client { get; }
protected readonly TaskCompletionSource<bool> _interactionCompletedSource;
protected IUserMessage message = null!;
protected NadekoButtonInteraction(DiscordSocketClient client)
{
Client = client;
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
}
public async Task RunAsync(IUserMessage msg)
{
message = msg;
Client.InteractionCreated += OnInteraction;
await Task.WhenAny(Task.Delay(10_000), _interactionCompletedSource.Task);
Client.InteractionCreated -= OnInteraction;
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
}
protected abstract ValueTask<bool> Validate(SocketMessageComponent smc);
private async Task OnInteraction(SocketInteraction arg)
{
if (arg is not SocketMessageComponent smc)
return;
if (smc.Message.Id != message.Id)
return;
if (smc.Data.CustomId != Name)
return;
if (!await Validate(smc))
{
await smc.DeferAsync();
return;
}
_ = Task.Run(async () =>
{
await ExecuteOnActionAsync(smc);
// this should only be a thing on single-response buttons
_interactionCompletedSource.TrySetResult(true);
if (!smc.HasResponded)
{
await smc.DeferAsync();
}
});
}
public virtual MessageComponent CreateComponent()
{
var comp = new ComponentBuilder()
.WithButton(GetButtonBuilder());
return comp.Build();
}
public ButtonBuilder GetButtonBuilder()
=> new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name, label: Text);
public abstract Task ExecuteOnActionAsync(SocketMessageComponent smc);
}
// this is all so wrong ...

View File

@@ -0,0 +1,43 @@
// namespace NadekoBot;
//
// public class NadekoButtonInteractionArray : NadekoButtonInteraction
// {
// private readonly ButtonBuilder[] _bbs;
// private readonly NadekoButtonInteraction[] _inters;
//
// public NadekoButtonInteractionArray(params NadekoButtonInteraction[] inters)
// : base(inters[0].Client)
// {
// _inters = inters;
// _bbs = inters.Map(x => x.GetButtonBuilder());
// }
//
// protected override string Name
// => throw new NotSupportedException();
// protected override IEmote Emote
// => throw new NotSupportedException();
//
// protected override ValueTask<bool> Validate(SocketMessageComponent smc)
// => new(true);
//
// public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
// {
// for (var i = 0; i < _bbs.Length; i++)
// {
// if (_bbs[i].CustomId == smc.Data.CustomId)
// return _inters[i].ExecuteOnActionAsync(smc);
// }
//
// return Task.CompletedTask;
// }
//
// public override MessageComponent CreateComponent()
// {
// var comp = new ComponentBuilder();
//
// foreach (var bb in _bbs)
// comp.WithButton(bb);
//
// return comp.Build();
// }
// }

View File

@@ -0,0 +1,42 @@
namespace NadekoBot;
/// <summary>
/// Builder class for NadekoInteractions
/// </summary>
public class NadekoInteractionBuilder
{
private NadekoInteractionData? iData;
private Func<SocketMessageComponent, Task>? action;
// private bool isOwn;
public NadekoInteractionBuilder WithData<T>(in T data)
where T : NadekoInteractionData
{
iData = data;
return this;
}
// public NadekoOwnInteractionBuiler WithIsOwn(bool isOwn = true)
// {
// this.isOwn = isOwn;
// return this;
// }
public NadekoInteractionBuilder WithAction(in Func<SocketMessageComponent, Task> fn)
{
this.action = fn;
return this;
}
public NadekoButtonActionInteraction Build(DiscordSocketClient client, ulong userId)
{
if (iData is null)
throw new InvalidOperationException("You have to specify the data before building the interaction");
if (action is null)
throw new InvalidOperationException("You have to specify the action before building the interaction");
return new(client, userId, iData, action);
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot;
/// <summary>
/// Represents essential interacation data
/// </summary>
/// <param name="Emote">Emote which will show on a button</param>
/// <param name="CustomId">Custom interaction id</param>
public record NadekoInteractionData(IEmote Emote, string CustomId, string? Text = null);

View File

@@ -0,0 +1,15 @@
namespace NadekoBot;
/// <summary>
/// Interaction which only the author can use
/// </summary>
public abstract class NadekoButtonOwnInteraction : NadekoButtonInteraction
{
protected readonly ulong _authorId;
protected NadekoButtonOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
=> _authorId = authorId;
protected override ValueTask<bool> Validate(SocketMessageComponent smc)
=> new(smc.User.Id == _authorId);
}

View File

@@ -0,0 +1,16 @@
#nullable disable
using LinqToDB;
using System.Linq.Expressions;
namespace NadekoBot.Common;
public static class Linq2DbExpressions
{
[ExpressionMethod(nameof(GuildOnShardExpression))]
public static bool GuildOnShard(ulong guildId, int totalShards, int shardId)
=> throw new NotSupportedException();
private static Expression<Func<ulong, int, int, bool>> GuildOnShardExpression()
=> (guildId, totalShards, shardId)
=> guildId / 4194304 % (ulong)totalShards == (ulong)shardId;
}

View File

@@ -1,4 +1,5 @@
using Cloneable;
#nullable enable
using Cloneable;
using NadekoBot.Common.Yml;
namespace Nadeko.Medusa;
@@ -10,7 +11,7 @@ public sealed partial class MedusaConfig : ICloneable<MedusaConfig>
public int Version { get; set; } = 1;
[Comment("List of medusae automatically loaded at startup")]
public List<string> Loaded { get; set; }
public List<string>? Loaded { get; set; }
public MedusaConfig()
{

View File

@@ -18,7 +18,7 @@ public sealed class MedusaConfigService : ConfigServiceBase<MedusaConfig>, IMedu
}
public IReadOnlyCollection<string> GetLoadedMedusae()
=> Data.Loaded.ToList();
=> Data.Loaded?.ToList() ?? new List<string>();
public void AddLoadedMedusa(string name)
{
@@ -26,6 +26,9 @@ public sealed class MedusaConfigService : ConfigServiceBase<MedusaConfig>, IMedu
ModifyConfig(conf =>
{
if (conf.Loaded is null)
conf.Loaded = new();
if(!conf.Loaded.Contains(name))
conf.Loaded.Add(name);
});
@@ -37,6 +40,9 @@ public sealed class MedusaConfigService : ConfigServiceBase<MedusaConfig>, IMedu
ModifyConfig(conf =>
{
if (conf.Loaded is null)
conf.Loaded = new();
conf.Loaded.Remove(name);
});
}

View File

@@ -191,14 +191,12 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
await _lock.WaitAsync();
try
{
var success = LoadAssemblyInternal(safeName,
out var ctx,
out var snekData,
out var services,
out var strings,
out var typeReaders);
if (success)
if (LoadAssemblyInternal(safeName,
out var ctx,
out var snekData,
out var services,
out var strings,
out var typeReaders))
{
var moduleInfos = new List<ModuleInfo>();
@@ -769,7 +767,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor,
var paramName = pi.Name ?? "unnamed";
var isContext = paramCounter == 0 && pi.ParameterType.IsAssignableTo(typeof(AnyContext));
var leftoverAttribute = pi.GetCustomAttribute<Nadeko.Snake.leftoverAttribute>(true);
var leftoverAttribute = pi.GetCustomAttribute<leftoverAttribute>(true);
var hasDefaultValue = pi.HasDefaultValue;
var isLeftover = leftoverAttribute != null;
var isParams = pi.GetCustomAttribute<ParamArrayAttribute>() is not null;

View File

@@ -0,0 +1,26 @@
namespace NadekoBot.Common;
public abstract class NInteraction
{
private readonly DiscordSocketClient _client;
private readonly ulong _userId;
private readonly Func<SocketMessageComponent, Task> _action;
protected abstract NadekoInteractionData Data { get; }
public NInteraction(
DiscordSocketClient client,
ulong userId,
Func<SocketMessageComponent, Task> action)
{
_client = client;
_userId = userId;
_action = action;
}
public NadekoButtonInteraction GetInteraction()
=> new NadekoInteractionBuilder()
.WithData(Data)
.WithAction(_action)
.Build(_client, _userId);
}

View File

@@ -1,5 +1,6 @@
#nullable disable
using System.Globalization;
using MessageType = NadekoBot.Extensions.MessageType;
// ReSharper disable InconsistentNaming
@@ -29,20 +30,15 @@ public abstract class NadekoModule : ModuleBase
protected string GetText(in LocStr data)
=> Strings.GetText(data, Culture);
public Task<IUserMessage> SendErrorAsync(string error)
=> ctx.Channel.SendErrorAsync(_eb, error);
public Task<IUserMessage> SendErrorAsync(
string title,
string error,
string url = null,
string footer = null)
string footer = null,
NadekoButtonInteraction inter = null)
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
public Task<IUserMessage> SendConfirmAsync(string text)
=> ctx.Channel.SendConfirmAsync(_eb, text);
public Task<IUserMessage> SendConfirmAsync(
string title,
string text,
@@ -50,25 +46,33 @@ public abstract class NadekoModule : ModuleBase
string footer = null)
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
public Task<IUserMessage> SendPendingAsync(string text)
=> ctx.Channel.SendPendingAsync(_eb, text);
//
public Task<IUserMessage> SendErrorAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Error, inter);
public Task<IUserMessage> SendConfirmAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Ok, inter);
public Task<IUserMessage> SendPendingAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Pending, inter);
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str)
=> SendErrorAsync(GetText(str));
// localized normal
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendErrorAsync(GetText(str), inter);
public Task<IUserMessage> PendingLocalizedAsync(LocStr str)
=> SendPendingAsync(GetText(str));
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendPendingAsync(GetText(str), inter);
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str)
=> SendConfirmAsync(GetText(str));
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendConfirmAsync(GetText(str), inter);
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str)
// localized replies
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str)
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str)
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)

View File

@@ -34,61 +34,59 @@ public class Replacer
public SmartText Replace(SmartText data)
=> data switch
{
SmartEmbedText embedData => Replace(embedData),
SmartEmbedText embedData => Replace(embedData) with
{
PlainText = Replace(embedData.PlainText),
Color = embedData.Color
},
SmartPlainText plain => Replace(plain),
SmartEmbedTextArray arr => Replace(arr),
_ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type")
};
public SmartPlainText Replace(SmartPlainText plainText)
=> Replace(plainText.Text);
public SmartEmbedText Replace(SmartEmbedText embedData)
{
var newEmbedData = new SmartEmbedText
private SmartEmbedTextArray Replace(SmartEmbedTextArray embedArr)
=> new()
{
Embeds = embedArr.Embeds.Map(e => Replace(e) with
{
Color = e.Color
}),
Content = Replace(embedArr.Content)
};
private SmartPlainText Replace(SmartPlainText plain)
=> Replace(plain.Text);
private T Replace<T>(T embedData) where T: SmartEmbedTextBase, new()
{
var newEmbedData = new T
{
PlainText = Replace(embedData.PlainText),
Description = Replace(embedData.Description),
Title = Replace(embedData.Title),
Thumbnail = Replace(embedData.Thumbnail),
Image = Replace(embedData.Image),
Url = Replace(embedData.Url)
};
if (embedData.Author is not null)
{
newEmbedData.Author = new()
{
Name = Replace(embedData.Author.Name),
IconUrl = Replace(embedData.Author.IconUrl)
};
}
if (embedData.Fields is not null)
{
var fields = new List<SmartTextEmbedField>();
foreach (var f in embedData.Fields)
{
var newF = new SmartTextEmbedField
Url = Replace(embedData.Url),
Author = embedData.Author is null
? null
: new()
{
Name = Replace(f.Name),
Value = Replace(f.Value),
Inline = f.Inline
};
fields.Add(newF);
}
newEmbedData.Fields = fields.ToArray();
}
if (embedData.Footer is not null)
{
newEmbedData.Footer = new()
Name = Replace(embedData.Author.Name),
IconUrl = Replace(embedData.Author.IconUrl)
},
Fields = embedData.Fields?.Map(f => new SmartTextEmbedField
{
Text = Replace(embedData.Footer.Text),
IconUrl = Replace(embedData.Footer.IconUrl)
};
}
newEmbedData.Color = embedData.Color;
Name = Replace(f.Name),
Value = Replace(f.Value),
Inline = f.Inline
}),
Footer = embedData.Footer is null
? null
: new()
{
Text = Replace(embedData.Footer.Text),
IconUrl = Replace(embedData.Footer.IconUrl)
}
};
return newEmbedData;
}

View File

@@ -1,20 +1,67 @@
#nullable disable
#nullable disable warnings
using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot;
public sealed record SmartEmbedText : SmartText
public sealed record SmartEmbedArrayElementText : SmartEmbedTextBase
{
public string PlainText { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Url { get; set; }
public string Thumbnail { get; set; }
public string Image { get; set; }
public string Color { get; init; } = string.Empty;
public SmartTextEmbedAuthor Author { get; set; }
public SmartTextEmbedFooter Footer { get; set; }
public SmartTextEmbedField[] Fields { get; set; }
public SmartEmbedArrayElementText() : base()
{
}
public SmartEmbedArrayElementText(IEmbed eb) : base(eb)
{
}
public uint Color { get; set; } = 7458112;
protected override EmbedBuilder GetEmbedInternal()
{
var embed = base.GetEmbedInternal();
if (Rgba32.TryParseHex(Color, out var color))
return embed.WithColor(color.ToDiscordColor());
return embed;
}
}
public sealed record SmartEmbedText : SmartEmbedTextBase
{
public string PlainText { get; init; }
public uint Color { get; init; } = 7458112;
public SmartEmbedText()
{
}
private SmartEmbedText(IEmbed eb, string? plainText = null)
: base(eb)
=> (PlainText, Color) = (plainText, eb.Color?.RawValue ?? 0);
public static SmartEmbedText FromEmbed(IEmbed eb, string? plainText = null)
=> new(eb, plainText);
protected override EmbedBuilder GetEmbedInternal()
{
var embed = base.GetEmbedInternal();
return embed.WithColor(Color);
}
}
public abstract record SmartEmbedTextBase : SmartText
{
public string Title { get; init; }
public string Description { get; init; }
public string Url { get; init; }
public string Thumbnail { get; init; }
public string Image { get; init; }
public SmartTextEmbedAuthor Author { get; init; }
public SmartTextEmbedFooter Footer { get; init; }
public SmartTextEmbedField[] Fields { get; init; }
public bool IsValid
=> !string.IsNullOrWhiteSpace(Title)
@@ -26,36 +73,37 @@ public sealed record SmartEmbedText : SmartText
&& (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl)))
|| Fields is { Length: > 0 };
public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null)
protected SmartEmbedTextBase()
{
var set = new SmartEmbedText
{
PlainText = plainText,
Title = eb.Title,
Description = eb.Description,
Url = eb.Url,
Thumbnail = eb.Thumbnail?.Url,
Image = eb.Image?.Url,
Author = eb.Author is { } ea
? new()
{
Name = ea.Name,
Url = ea.Url,
IconUrl = ea.IconUrl
}
: null,
Footer = eb.Footer is { } ef
? new()
{
Text = ef.Text,
IconUrl = ef.IconUrl
}
: null
};
}
protected SmartEmbedTextBase(IEmbed eb)
{
Title = eb.Title;
Description = eb.Description;
Url = eb.Url;
Thumbnail = eb.Thumbnail?.Url;
Image = eb.Image?.Url;
Author = eb.Author is { } ea
? new()
{
Name = ea.Name,
Url = ea.Url,
IconUrl = ea.IconUrl
}
: null;
Footer = eb.Footer is { } ef
? new()
{
Text = ef.Text,
IconUrl = ef.IconUrl
}
: null;
if (eb.Fields.Length > 0)
{
set.Fields = eb.Fields.Select(field
Fields = eb.Fields.Select(field
=> new SmartTextEmbedField
{
Inline = field.Inline,
@@ -64,14 +112,14 @@ public sealed record SmartEmbedText : SmartText
})
.ToArray();
}
set.Color = eb.Color?.RawValue ?? 0;
return set;
}
public EmbedBuilder GetEmbed()
=> GetEmbedInternal();
protected virtual EmbedBuilder GetEmbedInternal()
{
var embed = new EmbedBuilder().WithColor(Color);
var embed = new EmbedBuilder();
if (!string.IsNullOrWhiteSpace(Title))
embed.WithTitle(Title);

View File

@@ -0,0 +1,31 @@
#nullable disable
namespace NadekoBot;
public sealed record SmartEmbedTextArray : SmartText
{
public string Content { get; set; }
public SmartEmbedArrayElementText[] Embeds { get; set; }
public bool IsValid
=> Embeds?.All(x => x.IsValid) ?? false;
public EmbedBuilder[] GetEmbedBuilders()
{
if (Embeds is null)
return Array.Empty<EmbedBuilder>();
return Embeds
.Where(x => x.IsValid)
.Select(em => em.GetEmbed())
.ToArray();
}
public void NormalizeFields()
{
if (Embeds is null)
return;
foreach(var eb in Embeds)
eb.NormalizeFields();
}
}

View File

@@ -1,5 +1,5 @@
#nullable disable
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace NadekoBot;
@@ -11,6 +11,9 @@ public abstract record SmartText
public bool IsPlainText
=> this is SmartPlainText;
public bool IsEmbedArray
=> this is SmartEmbedTextArray;
public static SmartText operator +(SmartText text, string input)
=> text switch
{
@@ -19,6 +22,10 @@ public abstract record SmartText
PlainText = set.PlainText + input
},
SmartPlainText spt => new SmartPlainText(spt.Text + input),
SmartEmbedTextArray arr => arr with
{
Content = arr.Content + input
},
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
@@ -30,27 +37,45 @@ public abstract record SmartText
PlainText = input + set.PlainText
},
SmartPlainText spt => new SmartPlainText(input + spt.Text),
SmartEmbedTextArray arr => arr with
{
Content = input + arr.Content
},
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
public static SmartText CreateFrom(string input)
{
if (string.IsNullOrWhiteSpace(input) || !input.TrimStart().StartsWith("{"))
if (string.IsNullOrWhiteSpace(input))
return new SmartPlainText(input);
try
{
var smartEmbedText = JsonConvert.DeserializeObject<SmartEmbedText>(input);
var doc = JObject.Parse(input);
var root = doc.Root;
if (root.Type == JTokenType.Object)
{
if (((JObject)root).TryGetValue("embeds", out _))
{
var arr = root.ToObject<SmartEmbedTextArray>();
if (smartEmbedText is null)
throw new FormatException();
if (arr is null)
return new SmartPlainText(input);
smartEmbedText.NormalizeFields();
arr!.NormalizeFields();
return arr;
}
if (!smartEmbedText.IsValid)
return new SmartPlainText(input);
var obj = root.ToObject<SmartEmbedText>();
return smartEmbedText;
if (obj is null || !(obj.IsValid || !string.IsNullOrWhiteSpace(obj.PlainText)))
return new SmartPlainText(input);
obj.NormalizeFields();
return obj;
}
return new SmartPlainText(input);
}
catch
{

View File

@@ -1,95 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public sealed class ReactionEventWrapper : IDisposable
{
public event Action<SocketReaction> OnReactionAdded = delegate { };
public event Action<SocketReaction> OnReactionRemoved = delegate { };
public event Action OnReactionsCleared = delegate { };
public IUserMessage Message { get; }
private readonly DiscordSocketClient _client;
private bool disposing;
public ReactionEventWrapper(DiscordSocketClient client, IUserMessage msg)
{
Message = msg ?? throw new ArgumentNullException(nameof(msg));
_client = client;
_client.ReactionAdded += Discord_ReactionAdded;
_client.ReactionRemoved += Discord_ReactionRemoved;
_client.ReactionsCleared += Discord_ReactionsCleared;
}
public void Dispose()
{
if (disposing)
return;
disposing = true;
UnsubAll();
}
private Task Discord_ReactionsCleared(Cacheable<IUserMessage, ulong> msg, Cacheable<IMessageChannel, ulong> channel)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionsCleared?.Invoke();
}
catch { }
});
return Task.CompletedTask;
}
private Task Discord_ReactionRemoved(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> cacheable,
SocketReaction reaction)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionRemoved?.Invoke(reaction);
}
catch { }
});
return Task.CompletedTask;
}
private Task Discord_ReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> cacheable,
SocketReaction reaction)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionAdded?.Invoke(reaction);
}
catch
{
}
});
return Task.CompletedTask;
}
public void UnsubAll()
{
_client.ReactionAdded -= Discord_ReactionAdded;
_client.ReactionRemoved -= Discord_ReactionRemoved;
_client.ReactionsCleared -= Discord_ReactionsCleared;
OnReactionAdded = null;
OnReactionRemoved = null;
OnReactionsCleared = null;
}
}

View File

@@ -50,9 +50,7 @@ public static class GuildConfigExtensions
.Include(gc => gc.StreamRole)
.Include(gc => gc.XpSettings)
.ThenInclude(x => x.ExclusionList)
.Include(gc => gc.DelMsgOnCmdChannels)
.Include(gc => gc.ReactionRoleMessages)
.ThenInclude(x => x.ReactionRoles);
.Include(gc => gc.DelMsgOnCmdChannels);
public static IEnumerable<GuildConfig> GetAllGuildConfigs(
this DbSet<GuildConfig> configs,

View File

@@ -0,0 +1,9 @@
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Db.Models;
public class BankUser : DbEntity
{
public ulong UserId { get; set; }
public long Balance { get; set; }
}

View File

@@ -3,6 +3,8 @@ using NadekoBot.Services.Database.Models;
namespace NadekoBot.Db.Models;
// FUTURE remove LastLevelUp from here and UserXpStats
public class DiscordUser : DbEntity
{
public ulong UserId { get; set; }
@@ -14,7 +16,7 @@ public class DiscordUser : DbEntity
public ClubInfo Club { get; set; }
public bool IsClubAdmin { get; set; }
public int TotalXp { get; set; }
public long TotalXp { get; set; }
public DateTime LastLevelUp { get; set; } = DateTime.UtcNow;
public DateTime LastXpGain { get; set; } = DateTime.MinValue;
public XpNotificationLocation NotifyOnLevelUp { get; set; }

View File

@@ -84,14 +84,14 @@ public class GuildConfig : DbEntity
public List<ShopEntry> ShopEntries { get; set; }
public ulong? GameVoiceChannel { get; set; }
public bool VerboseErrors { get; set; }
public bool VerboseErrors { get; set; } = true;
public StreamRoleSettings StreamRole { get; set; }
public XpSettings XpSettings { get; set; }
public List<FeedSub> FeedSubs { get; set; } = new();
public IndexedCollection<ReactionRoleMessage> ReactionRoleMessages { 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;

View File

@@ -0,0 +1,48 @@
#nullable disable
namespace NadekoBot.Db.Models;
/// <summary>
/// Contains data about usage of Patron-Only commands per user
/// in order to provide support for quota limitations
/// (allow user x who is pledging amount y to use the specified command only
/// x amount of times in the specified time period)
/// </summary>
public class PatronQuota
{
public ulong UserId { get; set; }
public FeatureType FeatureType { get; set; }
public string Feature { get; set; }
public uint HourlyCount { get; set; }
public uint DailyCount { get; set; }
public uint MonthlyCount { get; set; }
}
public enum FeatureType
{
Command,
Group,
Module,
Limit
}
public class PatronUser
{
public string UniquePlatformUserId { get; set; }
public ulong UserId { get; set; }
public int AmountCents { get; set; }
public DateTime LastCharge { get; set; }
// Date Only component
public DateTime ValidThru { get; set; }
public PatronUser Clone()
=> new PatronUser()
{
UniquePlatformUserId = this.UniquePlatformUserId,
UserId = this.UserId,
AmountCents = this.AmountCents,
LastCharge = this.LastCharge,
ValidThru = this.ValidThru
};
}

View File

@@ -1,22 +1,18 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
namespace NadekoBot.Services.Database.Models;
public class ReactionRoleMessage : DbEntity, IIndexed
public class ReactionRoleV2 : DbEntity
{
public int Index { get; set; }
public int GuildConfigId { get; set; }
public GuildConfig GuildConfig { get; set; }
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
public ulong MessageId { get; set; }
public List<ReactionRole> ReactionRoles { get; set; }
public bool Exclusive { get; set; }
}
public class ReactionRole : DbEntity
{
public string EmoteName { get; set; }
[MaxLength(100)]
public string Emote { get; set; }
public ulong RoleId { get; set; }
public int Group { get; set; }
public int LevelReq { get; set; }
}

View File

@@ -4,7 +4,7 @@ namespace NadekoBot.Services.Database.Models;
public class RewardedUser : DbEntity
{
public ulong UserId { get; set; }
public string PatreonUserId { get; set; }
public int AmountRewardedThisMonth { get; set; }
public string PlatformUserId { get; set; }
public long AmountRewardedThisMonth { get; set; }
public DateTime LastReward { get; set; }
}

View File

@@ -5,8 +5,8 @@ public class UserXpStats : DbEntity
{
public ulong UserId { get; set; }
public ulong GuildId { get; set; }
public int Xp { get; set; }
public int AwardedXp { get; set; }
public long Xp { get; set; }
public long AwardedXp { get; set; }
public XpNotificationLocation NotifyOnLevelUp { get; set; }
public DateTime LastLevelUp { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,6 +1,5 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using NadekoBot.Db.Models;
using NadekoBot.Services.Database.Models;
@@ -26,7 +25,7 @@ public abstract class NadekoContext : DbContext
public DbSet<ClubInfo> Clubs { get; set; }
public DbSet<ClubBans> ClubBans { get; set; }
public DbSet<ClubApplicants> ClubApplicants { get; set; }
//logging
public DbSet<LogSetting> LogSettings { get; set; }
@@ -52,14 +51,22 @@ public abstract class NadekoContext : DbContext
public DbSet<Permissionv2> Permissions { get; set; }
public DbSet<BankUser> BankUsers { get; set; }
public DbSet<ReactionRoleV2> ReactionRoles { get; set; }
public DbSet<PatronUser> Patrons { get; set; }
public DbSet<PatronQuota> PatronQuotas { get; set; }
#region Mandatory Provider-Specific Values
protected abstract string CurrencyTransactionOtherIdDefaultValue { get; }
protected abstract string DiscordUserLastXpGainDefaultValue { get; }
protected abstract string LastLevelUpDefaultValue { get; }
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region QUOTES
@@ -73,7 +80,11 @@ public abstract class NadekoContext : DbContext
#region GuildConfig
var configEntity = modelBuilder.Entity<GuildConfig>();
configEntity.HasIndex(c => c.GuildId).IsUnique();
configEntity.HasIndex(c => c.GuildId)
.IsUnique();
configEntity.Property(x => x.VerboseErrors)
.HasDefaultValue(true);
modelBuilder.Entity<AntiSpamSetting>().HasOne(x => x.GuildConfig).WithOne(x => x.AntiSpamSetting);
@@ -189,13 +200,6 @@ public abstract class NadekoContext : DbContext
#endregion
#region PatreonRewards
var pr = modelBuilder.Entity<RewardedUser>();
pr.HasIndex(x => x.PatreonUserId).IsUnique();
#endregion
#region XpStats
var xps = modelBuilder.Entity<UserXpStats>();
@@ -359,10 +363,18 @@ public abstract class NadekoContext : DbContext
#region Reaction roles
modelBuilder.Entity<ReactionRoleMessage>(rrm => rrm
.HasMany(x => x.ReactionRoles)
.WithOne()
.OnDelete(DeleteBehavior.Cascade));
modelBuilder.Entity<ReactionRoleV2>(rr2 =>
{
rr2.HasIndex(x => x.GuildId)
.IsUnique(false);
rr2.HasIndex(x => new
{
x.MessageId,
x.Emote
})
.IsUnique();
});
#endregion
@@ -402,6 +414,43 @@ public abstract class NadekoContext : DbContext
x.ChannelId,
x.UserId
}));
#region BANK
modelBuilder.Entity<BankUser>(bu => bu.HasIndex(x => x.UserId).IsUnique());
#endregion
#region Patron
// currency rewards
var pr = modelBuilder.Entity<RewardedUser>();
pr.HasIndex(x => x.PlatformUserId).IsUnique();
// patrons
// patrons are not identified by their user id, but by their platform user id
// as multiple accounts (even maybe on different platforms) could have
// the same account connected to them
modelBuilder.Entity<PatronUser>(pu =>
{
pu.HasIndex(x => x.UniquePlatformUserId).IsUnique();
pu.HasKey(x => x.UserId);
});
// quotes are per user id
modelBuilder.Entity<PatronQuota>(pq =>
{
pq.HasIndex(x => x.UserId).IsUnique(false);
pq.HasKey(x => new
{
x.UserId,
x.FeatureType,
x.Feature
});
});
#endregion
}
#if DEBUG

View File

@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations;
public static class MigrationQueries
{
public static void MigrateRero(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"insert or ignore into reactionroles(guildid, channelid, messageid, emote, roleid, 'group', levelreq, dateadded)
select guildid, channelid, messageid, emotename, roleid, exclusive, 0, reactionrolemessage.dateadded
from reactionrole
left join reactionrolemessage on reactionrolemessage.id = reactionrole.reactionrolemessageid
left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class stondel : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "deletestreamonlinemessage",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "deletestreamonlinemessage",
table: "guildconfigs");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "bankusers",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
balance = table.Column<long>(type: "bigint", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_bankusers", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_bankusers_userid",
table: "bankusers",
column: "userid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "bankusers");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class newrero : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "reactionroles",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
guildid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
messageid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
emote = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
roleid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
group = table.Column<int>(type: "int", nullable: false),
levelreq = table.Column<int>(type: "int", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionroles", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_reactionroles_guildid",
table: "reactionroles",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_reactionroles_messageid_emote",
table: "reactionroles",
columns: new[] { "messageid", "emote" },
unique: true);
MigrationQueries.MigrateRero(migrationBuilder);
migrationBuilder.DropTable(
name: "reactionrole");
migrationBuilder.DropTable(
name: "reactionrolemessage");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "reactionroles");
migrationBuilder.CreateTable(
name: "reactionrolemessage",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
guildconfigid = table.Column<int>(type: "int", nullable: false),
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true),
exclusive = table.Column<bool>(type: "tinyint(1)", nullable: false),
index = table.Column<int>(type: "int", nullable: false),
messageid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionrolemessage", x => x.id);
table.ForeignKey(
name: "fk_reactionrolemessage_guildconfigs_guildconfigid",
column: x => x.guildconfigid,
principalTable: "guildconfigs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "reactionrole",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true),
emotename = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
reactionrolemessageid = table.Column<int>(type: "int", nullable: true),
roleid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionrole", x => x.id);
table.ForeignKey(
name: "fk_reactionrole_reactionrolemessage_reactionrolemessageid",
column: x => x.reactionrolemessageid,
principalTable: "reactionrolemessage",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_reactionrole_reactionrolemessageid",
table: "reactionrole",
column: "reactionrolemessageid");
migrationBuilder.CreateIndex(
name: "ix_reactionrolemessage_guildconfigid",
table: "reactionrolemessage",
column: "guildconfigid");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class patronagesystem : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "patreonuserid",
table: "rewardedusers",
newName: "platformuserid");
migrationBuilder.RenameIndex(
name: "ix_rewardedusers_patreonuserid",
table: "rewardedusers",
newName: "ix_rewardedusers_platformuserid");
migrationBuilder.AlterColumn<long>(
name: "xp",
table: "userxpstats",
type: "bigint",
nullable: false,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.AlterColumn<long>(
name: "awardedxp",
table: "userxpstats",
type: "bigint",
nullable: false,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.AlterColumn<long>(
name: "amountrewardedthismonth",
table: "rewardedusers",
type: "bigint",
nullable: false,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.AlterColumn<bool>(
name: "verboseerrors",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "tinyint(1)");
migrationBuilder.AlterColumn<long>(
name: "totalxp",
table: "discorduser",
type: "bigint",
nullable: false,
defaultValue: 0L,
oldClrType: typeof(int),
oldType: "int",
oldDefaultValue: 0);
migrationBuilder.CreateTable(
name: "patronquotas",
columns: table => new
{
userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
featuretype = table.Column<int>(type: "int", nullable: false),
feature = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
hourlycount = table.Column<uint>(type: "int unsigned", nullable: false),
dailycount = table.Column<uint>(type: "int unsigned", nullable: false),
monthlycount = table.Column<uint>(type: "int unsigned", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_patronquotas", x => new { x.userid, x.featuretype, x.feature });
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "patrons",
columns: table => new
{
userid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
uniqueplatformuserid = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
amountcents = table.Column<int>(type: "int", nullable: false),
lastcharge = table.Column<DateTime>(type: "datetime(6)", nullable: false),
validthru = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_patrons", x => x.userid);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_patronquotas_userid",
table: "patronquotas",
column: "userid");
migrationBuilder.CreateIndex(
name: "ix_patrons_uniqueplatformuserid",
table: "patrons",
column: "uniqueplatformuserid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "patronquotas");
migrationBuilder.DropTable(
name: "patrons");
migrationBuilder.RenameColumn(
name: "platformuserid",
table: "rewardedusers",
newName: "patreonuserid");
migrationBuilder.RenameIndex(
name: "ix_rewardedusers_platformuserid",
table: "rewardedusers",
newName: "ix_rewardedusers_patreonuserid");
migrationBuilder.AlterColumn<int>(
name: "xp",
table: "userxpstats",
type: "int",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<int>(
name: "awardedxp",
table: "userxpstats",
type: "int",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<int>(
name: "amountrewardedthismonth",
table: "rewardedusers",
type: "int",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<bool>(
name: "verboseerrors",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
oldClrType: typeof(bool),
oldType: "tinyint(1)",
oldDefaultValue: true);
migrationBuilder.AlterColumn<int>(
name: "totalxp",
table: "discorduser",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(long),
oldType: "bigint",
oldDefaultValue: 0L);
}
}
}

View File

@@ -16,9 +16,38 @@ namespace NadekoBot.Migrations.Mysql
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.3")
.HasAnnotation("ProductVersion", "6.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<long>("Balance")
.HasColumnType("bigint")
.HasColumnName("balance");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<ulong>("UserId")
.HasColumnType("bigint unsigned")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_bankusers");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("ix_bankusers_userid");
b.ToTable("bankusers", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@@ -157,10 +186,10 @@ namespace NadekoBot.Migrations.Mysql
.HasDefaultValue(0)
.HasColumnName("notifyonlevelup");
b.Property<int>("TotalXp")
b.Property<long>("TotalXp")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(0)
.HasColumnType("bigint")
.HasDefaultValue(0L)
.HasColumnName("totalxp");
b.Property<ulong>("UserId")
@@ -236,6 +265,74 @@ namespace NadekoBot.Migrations.Mysql
b.ToTable("followedstream", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("bigint unsigned")
.HasColumnName("userid");
b.Property<int>("FeatureType")
.HasColumnType("int")
.HasColumnName("featuretype");
b.Property<string>("Feature")
.HasColumnType("varchar(255)")
.HasColumnName("feature");
b.Property<uint>("DailyCount")
.HasColumnType("int unsigned")
.HasColumnName("dailycount");
b.Property<uint>("HourlyCount")
.HasColumnType("int unsigned")
.HasColumnName("hourlycount");
b.Property<uint>("MonthlyCount")
.HasColumnType("int unsigned")
.HasColumnName("monthlycount");
b.HasKey("UserId", "FeatureType", "Feature")
.HasName("pk_patronquotas");
b.HasIndex("UserId")
.HasDatabaseName("ix_patronquotas_userid");
b.ToTable("patronquotas", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{
b.Property<ulong>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("bigint unsigned")
.HasColumnName("userid");
b.Property<int>("AmountCents")
.HasColumnType("int")
.HasColumnName("amountcents");
b.Property<DateTime>("LastCharge")
.HasColumnType("datetime(6)")
.HasColumnName("lastcharge");
b.Property<string>("UniquePlatformUserId")
.HasColumnType("varchar(255)")
.HasColumnName("uniqueplatformuserid");
b.Property<DateTime>("ValidThru")
.HasColumnType("datetime(6)")
.HasColumnName("validthru");
b.HasKey("UserId")
.HasName("pk_patrons");
b.HasIndex("UniquePlatformUserId")
.IsUnique()
.HasDatabaseName("ix_patrons_uniqueplatformuserid");
b.ToTable("patrons", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
{
b.Property<int>("Id")
@@ -1032,6 +1129,10 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("tinyint(1)")
.HasColumnName("deletemessageoncommand");
b.Property<bool>("DeleteStreamOnlineMessage")
.HasColumnType("tinyint(1)")
.HasColumnName("deletestreamonlinemessage");
b.Property<string>("DmGreetMessageText")
.HasColumnType("longtext")
.HasColumnName("dmgreetmessagetext");
@@ -1105,7 +1206,9 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnName("timezoneid");
b.Property<bool>("VerboseErrors")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true)
.HasColumnName("verboseerrors");
b.Property<bool>("VerbosePermissions")
@@ -1780,39 +1883,7 @@ namespace NadekoBot.Migrations.Mysql
b.ToTable("quotes", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<string>("EmoteName")
.HasColumnType("longtext")
.HasColumnName("emotename");
b.Property<int?>("ReactionRoleMessageId")
.HasColumnType("int")
.HasColumnName("reactionrolemessageid");
b.Property<ulong>("RoleId")
.HasColumnType("bigint unsigned")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_reactionrole");
b.HasIndex("ReactionRoleMessageId")
.HasDatabaseName("ix_reactionrole_reactionrolemessageid");
b.ToTable("reactionrole", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1827,29 +1898,42 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<bool>("Exclusive")
.HasColumnType("tinyint(1)")
.HasColumnName("exclusive");
b.Property<string>("Emote")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("emote");
b.Property<int>("GuildConfigId")
b.Property<int>("Group")
.HasColumnType("int")
.HasColumnName("guildconfigid");
.HasColumnName("group");
b.Property<int>("Index")
b.Property<ulong>("GuildId")
.HasColumnType("bigint unsigned")
.HasColumnName("guildid");
b.Property<int>("LevelReq")
.HasColumnType("int")
.HasColumnName("index");
.HasColumnName("levelreq");
b.Property<ulong>("MessageId")
.HasColumnType("bigint unsigned")
.HasColumnName("messageid");
b.Property<ulong>("RoleId")
.HasColumnType("bigint unsigned")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_reactionrolemessage");
.HasName("pk_reactionroles");
b.HasIndex("GuildConfigId")
.HasDatabaseName("ix_reactionrolemessage_guildconfigid");
b.HasIndex("GuildId")
.HasDatabaseName("ix_reactionroles_guildid");
b.ToTable("reactionrolemessage", (string)null);
b.HasIndex("MessageId", "Emote")
.IsUnique()
.HasDatabaseName("ix_reactionroles_messageid_emote");
b.ToTable("reactionroles", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b =>
@@ -1948,8 +2032,8 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("int")
.HasColumnName("id");
b.Property<int>("AmountRewardedThisMonth")
.HasColumnType("int")
b.Property<long>("AmountRewardedThisMonth")
.HasColumnType("bigint")
.HasColumnName("amountrewardedthismonth");
b.Property<DateTime?>("DateAdded")
@@ -1960,9 +2044,9 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("datetime(6)")
.HasColumnName("lastreward");
b.Property<string>("PatreonUserId")
b.Property<string>("PlatformUserId")
.HasColumnType("varchar(255)")
.HasColumnName("patreonuserid");
.HasColumnName("platformuserid");
b.Property<ulong>("UserId")
.HasColumnType("bigint unsigned")
@@ -1971,9 +2055,9 @@ namespace NadekoBot.Migrations.Mysql
b.HasKey("Id")
.HasName("pk_rewardedusers");
b.HasIndex("PatreonUserId")
b.HasIndex("PlatformUserId")
.IsUnique()
.HasDatabaseName("ix_rewardedusers_patreonuserid");
.HasDatabaseName("ix_rewardedusers_platformuserid");
b.ToTable("rewardedusers", (string)null);
});
@@ -2390,8 +2474,8 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("int")
.HasColumnName("id");
b.Property<int>("AwardedXp")
.HasColumnType("int")
b.Property<long>("AwardedXp")
.HasColumnType("bigint")
.HasColumnName("awardedxp");
b.Property<DateTime?>("DateAdded")
@@ -2416,8 +2500,8 @@ namespace NadekoBot.Migrations.Mysql
.HasColumnType("bigint unsigned")
.HasColumnName("userid");
b.Property<int>("Xp")
.HasColumnType("int")
b.Property<long>("Xp")
.HasColumnType("bigint")
.HasColumnName("xp");
b.HasKey("Id")
@@ -3075,27 +3159,6 @@ namespace NadekoBot.Migrations.Mysql
.HasConstraintName("fk_pollvote_poll_pollid");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.ReactionRoleMessage", null)
.WithMany("ReactionRoles")
.HasForeignKey("ReactionRoleMessageId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_reactionrole_reactionrolemessage_reactionrolemessageid");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig")
.WithMany("ReactionRoleMessages")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_reactionrolemessage_guildconfigs_guildconfigid");
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
@@ -3345,8 +3408,6 @@ namespace NadekoBot.Migrations.Mysql
b.Navigation("Permissions");
b.Navigation("ReactionRoleMessages");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
@@ -3387,11 +3448,6 @@ namespace NadekoBot.Migrations.Mysql
b.Navigation("Votes");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.Navigation("ReactionRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.Navigation("Items");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class stondel : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "deletestreamonlinemessage",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "deletestreamonlinemessage",
table: "guildconfigs");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "bankusers",
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),
balance = table.Column<long>(type: "bigint", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_bankusers", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_bankusers_userid",
table: "bankusers",
column: "userid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "bankusers");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class newrero : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "reactionroles",
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),
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
emote = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
group = table.Column<int>(type: "integer", nullable: false),
levelreq = table.Column<int>(type: "integer", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionroles", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_reactionroles_guildid",
table: "reactionroles",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_reactionroles_messageid_emote",
table: "reactionroles",
columns: new[] { "messageid", "emote" },
unique: true);
MigrationQueries.MigrateRero(migrationBuilder);
migrationBuilder.DropTable(
name: "reactionrole");
migrationBuilder.DropTable(
name: "reactionrolemessage");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "reactionroles");
migrationBuilder.CreateTable(
name: "reactionrolemessage",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildconfigid = table.Column<int>(type: "integer", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
exclusive = table.Column<bool>(type: "boolean", nullable: false),
index = table.Column<int>(type: "integer", nullable: false),
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionrolemessage", x => x.id);
table.ForeignKey(
name: "fk_reactionrolemessage_guildconfigs_guildconfigid",
column: x => x.guildconfigid,
principalTable: "guildconfigs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "reactionrole",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
emotename = table.Column<string>(type: "text", nullable: true),
reactionrolemessageid = table.Column<int>(type: "integer", nullable: true),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_reactionrole", x => x.id);
table.ForeignKey(
name: "fk_reactionrole_reactionrolemessage_reactionrolemessageid",
column: x => x.reactionrolemessageid,
principalTable: "reactionrolemessage",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_reactionrole_reactionrolemessageid",
table: "reactionrole",
column: "reactionrolemessageid");
migrationBuilder.CreateIndex(
name: "ix_reactionrolemessage_guildconfigid",
table: "reactionrolemessage",
column: "guildconfigid");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class patronagesystem : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "patreonuserid",
table: "rewardedusers",
newName: "platformuserid");
migrationBuilder.RenameIndex(
name: "ix_rewardedusers_patreonuserid",
table: "rewardedusers",
newName: "ix_rewardedusers_platformuserid");
migrationBuilder.AlterColumn<long>(
name: "xp",
table: "userxpstats",
type: "bigint",
nullable: false,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<long>(
name: "awardedxp",
table: "userxpstats",
type: "bigint",
nullable: false,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<long>(
name: "amountrewardedthismonth",
table: "rewardedusers",
type: "bigint",
nullable: false,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<bool>(
name: "verboseerrors",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "boolean");
migrationBuilder.AlterColumn<long>(
name: "totalxp",
table: "discorduser",
type: "bigint",
nullable: false,
defaultValue: 0L,
oldClrType: typeof(int),
oldType: "integer",
oldDefaultValue: 0);
migrationBuilder.CreateTable(
name: "patronquotas",
columns: table => new
{
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
featuretype = table.Column<int>(type: "integer", nullable: false),
feature = table.Column<string>(type: "text", nullable: false),
hourlycount = table.Column<long>(type: "bigint", nullable: false),
dailycount = table.Column<long>(type: "bigint", nullable: false),
monthlycount = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_patronquotas", x => new { x.userid, x.featuretype, x.feature });
});
migrationBuilder.CreateTable(
name: "patrons",
columns: table => new
{
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
uniqueplatformuserid = table.Column<string>(type: "text", nullable: true),
amountcents = table.Column<int>(type: "integer", nullable: false),
lastcharge = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
validthru = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_patrons", x => x.userid);
});
migrationBuilder.CreateIndex(
name: "ix_patronquotas_userid",
table: "patronquotas",
column: "userid");
migrationBuilder.CreateIndex(
name: "ix_patrons_uniqueplatformuserid",
table: "patrons",
column: "uniqueplatformuserid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "patronquotas");
migrationBuilder.DropTable(
name: "patrons");
migrationBuilder.RenameColumn(
name: "platformuserid",
table: "rewardedusers",
newName: "patreonuserid");
migrationBuilder.RenameIndex(
name: "ix_rewardedusers_platformuserid",
table: "rewardedusers",
newName: "ix_rewardedusers_patreonuserid");
migrationBuilder.AlterColumn<int>(
name: "xp",
table: "userxpstats",
type: "integer",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<int>(
name: "awardedxp",
table: "userxpstats",
type: "integer",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<int>(
name: "amountrewardedthismonth",
table: "rewardedusers",
type: "integer",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<bool>(
name: "verboseerrors",
table: "guildconfigs",
type: "boolean",
nullable: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true);
migrationBuilder.AlterColumn<int>(
name: "totalxp",
table: "discorduser",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(long),
oldType: "bigint",
oldDefaultValue: 0L);
}
}
}

View File

@@ -17,11 +17,42 @@ namespace NadekoBot.Migrations.PostgreSql
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.3")
.HasAnnotation("ProductVersion", "6.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("Balance")
.HasColumnType("bigint")
.HasColumnName("balance");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp with time zone")
.HasColumnName("dateadded");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_bankusers");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("ix_bankusers_userid");
b.ToTable("bankusers", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@@ -163,10 +194,10 @@ namespace NadekoBot.Migrations.PostgreSql
.HasDefaultValue(0)
.HasColumnName("notifyonlevelup");
b.Property<int>("TotalXp")
b.Property<long>("TotalXp")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnType("bigint")
.HasDefaultValue(0L)
.HasColumnName("totalxp");
b.Property<decimal>("UserId")
@@ -244,6 +275,74 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("followedstream", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
{
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<int>("FeatureType")
.HasColumnType("integer")
.HasColumnName("featuretype");
b.Property<string>("Feature")
.HasColumnType("text")
.HasColumnName("feature");
b.Property<long>("DailyCount")
.HasColumnType("bigint")
.HasColumnName("dailycount");
b.Property<long>("HourlyCount")
.HasColumnType("bigint")
.HasColumnName("hourlycount");
b.Property<long>("MonthlyCount")
.HasColumnType("bigint")
.HasColumnName("monthlycount");
b.HasKey("UserId", "FeatureType", "Feature")
.HasName("pk_patronquotas");
b.HasIndex("UserId")
.HasDatabaseName("ix_patronquotas_userid");
b.ToTable("patronquotas", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{
b.Property<decimal>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<int>("AmountCents")
.HasColumnType("integer")
.HasColumnName("amountcents");
b.Property<DateTime>("LastCharge")
.HasColumnType("timestamp with time zone")
.HasColumnName("lastcharge");
b.Property<string>("UniquePlatformUserId")
.HasColumnType("text")
.HasColumnName("uniqueplatformuserid");
b.Property<DateTime>("ValidThru")
.HasColumnType("timestamp with time zone")
.HasColumnName("validthru");
b.HasKey("UserId")
.HasName("pk_patrons");
b.HasIndex("UniquePlatformUserId")
.IsUnique()
.HasDatabaseName("ix_patrons_uniqueplatformuserid");
b.ToTable("patrons", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
{
b.Property<int>("Id")
@@ -1086,6 +1185,10 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("boolean")
.HasColumnName("deletemessageoncommand");
b.Property<bool>("DeleteStreamOnlineMessage")
.HasColumnType("boolean")
.HasColumnName("deletestreamonlinemessage");
b.Property<string>("DmGreetMessageText")
.HasColumnType("text")
.HasColumnName("dmgreetmessagetext");
@@ -1159,7 +1262,9 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnName("timezoneid");
b.Property<bool>("VerboseErrors")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("verboseerrors");
b.Property<bool>("VerbosePermissions")
@@ -1866,41 +1971,7 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("quotes", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp with time zone")
.HasColumnName("dateadded");
b.Property<string>("EmoteName")
.HasColumnType("text")
.HasColumnName("emotename");
b.Property<int?>("ReactionRoleMessageId")
.HasColumnType("integer")
.HasColumnName("reactionrolemessageid");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_reactionrole");
b.HasIndex("ReactionRoleMessageId")
.HasDatabaseName("ix_reactionrole_reactionrolemessageid");
b.ToTable("reactionrole", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1917,29 +1988,42 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp with time zone")
.HasColumnName("dateadded");
b.Property<bool>("Exclusive")
.HasColumnType("boolean")
.HasColumnName("exclusive");
b.Property<string>("Emote")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("emote");
b.Property<int>("GuildConfigId")
b.Property<int>("Group")
.HasColumnType("integer")
.HasColumnName("guildconfigid");
.HasColumnName("group");
b.Property<int>("Index")
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<int>("LevelReq")
.HasColumnType("integer")
.HasColumnName("index");
.HasColumnName("levelreq");
b.Property<decimal>("MessageId")
.HasColumnType("numeric(20,0)")
.HasColumnName("messageid");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_reactionrolemessage");
.HasName("pk_reactionroles");
b.HasIndex("GuildConfigId")
.HasDatabaseName("ix_reactionrolemessage_guildconfigid");
b.HasIndex("GuildId")
.HasDatabaseName("ix_reactionroles_guildid");
b.ToTable("reactionrolemessage", (string)null);
b.HasIndex("MessageId", "Emote")
.IsUnique()
.HasDatabaseName("ix_reactionroles_messageid_emote");
b.ToTable("reactionroles", (string)null);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b =>
@@ -2044,8 +2128,8 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AmountRewardedThisMonth")
.HasColumnType("integer")
b.Property<long>("AmountRewardedThisMonth")
.HasColumnType("bigint")
.HasColumnName("amountrewardedthismonth");
b.Property<DateTime?>("DateAdded")
@@ -2056,9 +2140,9 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp with time zone")
.HasColumnName("lastreward");
b.Property<string>("PatreonUserId")
b.Property<string>("PlatformUserId")
.HasColumnType("text")
.HasColumnName("patreonuserid");
.HasColumnName("platformuserid");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
@@ -2067,9 +2151,9 @@ namespace NadekoBot.Migrations.PostgreSql
b.HasKey("Id")
.HasName("pk_rewardedusers");
b.HasIndex("PatreonUserId")
b.HasIndex("PlatformUserId")
.IsUnique()
.HasDatabaseName("ix_rewardedusers_patreonuserid");
.HasDatabaseName("ix_rewardedusers_platformuserid");
b.ToTable("rewardedusers", (string)null);
});
@@ -2512,8 +2596,8 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AwardedXp")
.HasColumnType("integer")
b.Property<long>("AwardedXp")
.HasColumnType("bigint")
.HasColumnName("awardedxp");
b.Property<DateTime?>("DateAdded")
@@ -2538,8 +2622,8 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<int>("Xp")
.HasColumnType("integer")
b.Property<long>("Xp")
.HasColumnType("bigint")
.HasColumnName("xp");
b.HasKey("Id")
@@ -3215,27 +3299,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasConstraintName("fk_pollvote_poll_pollid");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.ReactionRoleMessage", null)
.WithMany("ReactionRoles")
.HasForeignKey("ReactionRoleMessageId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_reactionrole_reactionrolemessage_reactionrolemessageid");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig")
.WithMany("ReactionRoleMessages")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_reactionrolemessage_guildconfigs_guildconfigid");
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
@@ -3485,8 +3548,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("Permissions");
b.Navigation("ReactionRoleMessages");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
@@ -3527,11 +3588,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("Votes");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.Navigation("ReactionRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.Navigation("Items");

View File

@@ -87,7 +87,7 @@ namespace NadekoBot.Migrations
name: "VoicePresenceChannelId",
table: "LogSettings");
// todo cleanup guildconfigs which have logsettings id set to null
// FUTURE cleanup guildconfigs which have logsettings id set to null
migrationBuilder.Sql("UPDATE GuildConfigs SET LogSettingId = null WHERE LogSettingId NOT IN (SELECT Id from LogSettings)");
migrationBuilder.DropTable(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class stondel : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "DeleteStreamOnlineMessage",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DeleteStreamOnlineMessage",
table: "GuildConfigs");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BankUsers",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Balance = table.Column<long>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BankUsers", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_BankUsers_UserId",
table: "BankUsers",
column: "UserId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BankUsers");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class newrero : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ReactionRoles",
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),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
Emote = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
Group = table.Column<int>(type: "INTEGER", nullable: false),
LevelReq = table.Column<int>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ReactionRoles", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ReactionRoles_GuildId",
table: "ReactionRoles",
column: "GuildId");
migrationBuilder.CreateIndex(
name: "IX_ReactionRoles_MessageId_Emote",
table: "ReactionRoles",
columns: new[] { "MessageId", "Emote" },
unique: true);
MigrationQueries.MigrateRero(migrationBuilder);
migrationBuilder.DropTable(
name: "ReactionRole");
migrationBuilder.DropTable(
name: "ReactionRoleMessage");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ReactionRoles");
migrationBuilder.CreateTable(
name: "ReactionRoleMessage",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
Exclusive = table.Column<bool>(type: "INTEGER", nullable: false),
Index = table.Column<int>(type: "INTEGER", nullable: false),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ReactionRoleMessage", x => x.Id);
table.ForeignKey(
name: "FK_ReactionRoleMessage_GuildConfigs_GuildConfigId",
column: x => x.GuildConfigId,
principalTable: "GuildConfigs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ReactionRole",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
EmoteName = table.Column<string>(type: "TEXT", nullable: true),
ReactionRoleMessageId = table.Column<int>(type: "INTEGER", nullable: true),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ReactionRole", x => x.Id);
table.ForeignKey(
name: "FK_ReactionRole_ReactionRoleMessage_ReactionRoleMessageId",
column: x => x.ReactionRoleMessageId,
principalTable: "ReactionRoleMessage",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ReactionRole_ReactionRoleMessageId",
table: "ReactionRole",
column: "ReactionRoleMessageId");
migrationBuilder.CreateIndex(
name: "IX_ReactionRoleMessage_GuildConfigId",
table: "ReactionRoleMessage",
column: "GuildConfigId");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class patronagesystem : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "PatreonUserId",
table: "RewardedUsers",
newName: "PlatformUserId");
migrationBuilder.RenameIndex(
name: "IX_RewardedUsers_PatreonUserId",
table: "RewardedUsers",
newName: "IX_RewardedUsers_PlatformUserId");
migrationBuilder.AlterColumn<bool>(
name: "VerboseErrors",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "INTEGER");
migrationBuilder.AlterColumn<long>(
name: "TotalXp",
table: "DiscordUser",
type: "INTEGER",
nullable: false,
defaultValue: 0L,
oldClrType: typeof(int),
oldType: "INTEGER",
oldDefaultValue: 0);
migrationBuilder.CreateTable(
name: "PatronQuotas",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
FeatureType = table.Column<int>(type: "INTEGER", nullable: false),
Feature = table.Column<string>(type: "TEXT", nullable: false),
HourlyCount = table.Column<uint>(type: "INTEGER", nullable: false),
DailyCount = table.Column<uint>(type: "INTEGER", nullable: false),
MonthlyCount = table.Column<uint>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PatronQuotas", x => new { x.UserId, x.FeatureType, x.Feature });
});
migrationBuilder.CreateTable(
name: "Patrons",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UniquePlatformUserId = table.Column<string>(type: "TEXT", nullable: true),
AmountCents = table.Column<int>(type: "INTEGER", nullable: false),
LastCharge = table.Column<DateTime>(type: "TEXT", nullable: false),
ValidThru = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Patrons", x => x.UserId);
});
migrationBuilder.CreateIndex(
name: "IX_PatronQuotas_UserId",
table: "PatronQuotas",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Patrons_UniquePlatformUserId",
table: "Patrons",
column: "UniquePlatformUserId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PatronQuotas");
migrationBuilder.DropTable(
name: "Patrons");
migrationBuilder.RenameColumn(
name: "PlatformUserId",
table: "RewardedUsers",
newName: "PatreonUserId");
migrationBuilder.RenameIndex(
name: "IX_RewardedUsers_PlatformUserId",
table: "RewardedUsers",
newName: "IX_RewardedUsers_PatreonUserId");
migrationBuilder.AlterColumn<bool>(
name: "VerboseErrors",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
oldClrType: typeof(bool),
oldType: "INTEGER",
oldDefaultValue: true);
migrationBuilder.AlterColumn<int>(
name: "TotalXp",
table: "DiscordUser",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(long),
oldType: "INTEGER",
oldDefaultValue: 0L);
}
}
}

View File

@@ -15,7 +15,30 @@ namespace NadekoBot.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
modelBuilder.HasAnnotation("ProductVersion", "6.0.5");
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<long>("Balance")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("BankUsers");
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
@@ -126,10 +149,10 @@ namespace NadekoBot.Migrations
.HasColumnType("INTEGER")
.HasDefaultValue(0);
b.Property<int>("TotalXp")
b.Property<long>("TotalXp")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0);
.HasDefaultValue(0L);
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
@@ -186,6 +209,59 @@ namespace NadekoBot.Migrations
b.ToTable("FollowedStream");
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("FeatureType")
.HasColumnType("INTEGER");
b.Property<string>("Feature")
.HasColumnType("TEXT");
b.Property<uint>("DailyCount")
.HasColumnType("INTEGER");
b.Property<uint>("HourlyCount")
.HasColumnType("INTEGER");
b.Property<uint>("MonthlyCount")
.HasColumnType("INTEGER");
b.HasKey("UserId", "FeatureType", "Feature");
b.HasIndex("UserId");
b.ToTable("PatronQuotas");
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{
b.Property<ulong>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AmountCents")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastCharge")
.HasColumnType("TEXT");
b.Property<string>("UniquePlatformUserId")
.HasColumnType("TEXT");
b.Property<DateTime>("ValidThru")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.HasIndex("UniquePlatformUserId")
.IsUnique();
b.ToTable("Patrons");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
{
b.Property<int>("Id")
@@ -809,6 +885,9 @@ namespace NadekoBot.Migrations
b.Property<bool>("DeleteMessageOnCommand")
.HasColumnType("INTEGER");
b.Property<bool>("DeleteStreamOnlineMessage")
.HasColumnType("INTEGER");
b.Property<string>("DmGreetMessageText")
.HasColumnType("TEXT");
@@ -864,7 +943,9 @@ namespace NadekoBot.Migrations
.HasColumnType("TEXT");
b.Property<bool>("VerboseErrors")
.HasColumnType("INTEGER");
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<bool>("VerbosePermissions")
.HasColumnType("INTEGER");
@@ -1389,32 +1470,7 @@ namespace NadekoBot.Migrations
b.ToTable("Quotes");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<string>("EmoteName")
.HasColumnType("TEXT");
b.Property<int?>("ReactionRoleMessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ReactionRoleMessageId");
b.ToTable("ReactionRole");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1426,23 +1482,33 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<bool>("Exclusive")
b.Property<string>("Emote")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("Group")
.HasColumnType("INTEGER");
b.Property<int>("GuildConfigId")
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<int>("Index")
b.Property<int>("LevelReq")
.HasColumnType("INTEGER");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildConfigId");
b.HasIndex("GuildId");
b.ToTable("ReactionRoleMessage");
b.HasIndex("MessageId", "Emote")
.IsUnique();
b.ToTable("ReactionRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b =>
@@ -1520,7 +1586,7 @@ namespace NadekoBot.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AmountRewardedThisMonth")
b.Property<long>("AmountRewardedThisMonth")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
@@ -1529,7 +1595,7 @@ namespace NadekoBot.Migrations
b.Property<DateTime>("LastReward")
.HasColumnType("TEXT");
b.Property<string>("PatreonUserId")
b.Property<string>("PlatformUserId")
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
@@ -1537,7 +1603,7 @@ namespace NadekoBot.Migrations
b.HasKey("Id");
b.HasIndex("PatreonUserId")
b.HasIndex("PlatformUserId")
.IsUnique();
b.ToTable("RewardedUsers");
@@ -1866,7 +1932,7 @@ namespace NadekoBot.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AwardedXp")
b.Property<long>("AwardedXp")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
@@ -1886,7 +1952,7 @@ namespace NadekoBot.Migrations
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Xp")
b.Property<long>("Xp")
.HasColumnType("INTEGER");
b.HasKey("Id");
@@ -2430,25 +2496,6 @@ namespace NadekoBot.Migrations
.HasForeignKey("PollId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRole", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.ReactionRoleMessage", null)
.WithMany("ReactionRoles")
.HasForeignKey("ReactionRoleMessageId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig")
.WithMany("ReactionRoleMessages")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
@@ -2676,8 +2723,6 @@ namespace NadekoBot.Migrations
b.Navigation("Permissions");
b.Navigation("ReactionRoleMessages");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
@@ -2718,11 +2763,6 @@ namespace NadekoBot.Migrations
b.Navigation("Votes");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleMessage", b =>
{
b.Navigation("ReactionRoles");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b =>
{
b.Navigation("Items");

View File

@@ -19,10 +19,12 @@ public class DangerousCommandsService : INService
await using var ctx = _db.GetDbContext();
await ctx.DiscordUser.UpdateAsync(_ => new DiscordUser()
{
Club = null,
ClubId = null,
// IsClubAdmin = false,
TotalXp = 0
});
await ctx.UserXpStats.DeleteAsync();
await ctx.ClubApplicants.DeleteAsync();
await ctx.ClubBans.DeleteAsync();
await ctx.Clubs.DeleteAsync();

View File

@@ -256,15 +256,55 @@ public class GreetService : INService, IReadyExecutor
{
text = new SmartEmbedText()
{
PlainText = pt.Text
Description = pt.Text
};
}
((SmartEmbedText)text).Footer = new()
if (text is SmartEmbedText set)
{
Text = $"This message was sent from {user.Guild} server.",
IconUrl = user.Guild.IconUrl
};
text = set with
{
Footer = CreateFooterSource(user)
};
}
else if (text is SmartEmbedTextArray seta)
{
// if the greet dm message is a text array
var ebElem = seta.Embeds.LastOrDefault();
if (ebElem is null)
{
// if there are no embeds, add an embed with the footer
text = seta with
{
Embeds = new[]
{
new SmartEmbedArrayElementText()
{
Footer = CreateFooterSource(user)
}
}
};
}
else
{
// if the maximum amount of embeds is reached, edit the last embed
if (seta.Embeds.Length >= 10)
{
seta.Embeds[^1] = seta.Embeds[^1] with
{
Footer = CreateFooterSource(user)
};
}
else
{
// if there is less than 10 embeds, add an embed with footer only
seta.Embeds = seta.Embeds.Append(new SmartEmbedArrayElementText()
{
Footer = CreateFooterSource(user)
}).ToArray();
}
}
}
await user.SendAsync(text);
}
@@ -276,6 +316,13 @@ public class GreetService : INService, IReadyExecutor
return true;
}
private static SmartTextEmbedFooter CreateFooterSource(IGuildUser user)
=> new()
{
Text = $"This message was sent from {user.Guild} server.",
IconUrl = user.Guild.IconUrl
};
private Task OnUserJoined(IGuildUser user)
{
_ = Task.Run(async () =>

View File

@@ -23,12 +23,13 @@ public class PruneService : INService
try
{
var now = DateTime.UtcNow;
IMessage[] msgs;
IMessage lastMessage = null;
msgs = (await channel.GetMessagesAsync(50).FlattenAsync()).Where(predicate).Take(amount).ToArray();
while (amount > 0 && msgs.Any())
{
lastMessage = msgs[msgs.Length - 1];
lastMessage = msgs[^1];
var bulkDeletable = new List<IMessage>();
var singleDeletable = new List<IMessage>();
@@ -36,17 +37,23 @@ public class PruneService : INService
{
_logService.AddDeleteIgnore(x.Id);
if (DateTime.UtcNow - x.CreatedAt < _twoWeeks)
if (now - x.CreatedAt < _twoWeeks)
bulkDeletable.Add(x);
else
singleDeletable.Add(x);
}
if (bulkDeletable.Count > 0)
await Task.WhenAll(Task.Delay(1000), channel.DeleteMessagesAsync(bulkDeletable));
{
await channel.DeleteMessagesAsync(bulkDeletable);
await Task.Delay(2000);
}
foreach (var group in singleDeletable.Chunk(5))
await Task.WhenAll(Task.Delay(5000), group.Select(x => x.DeleteAsync()).WhenAll());
{
await group.Select(x => x.DeleteAsync()).WhenAll();
await Task.Delay(5000);
}
//this isn't good, because this still work as if i want to remove only specific user's messages from the last
//100 messages, Maybe this needs to be reduced by msgs.Length instead of 100

View File

@@ -0,0 +1,52 @@
#nullable disable
using NadekoBot.Modules.Utility.Patronage;
using NadekoBot.Services.Database.Models;
using OneOf;
using OneOf.Types;
namespace NadekoBot.Modules.Administration.Services;
public interface IReactionRoleService
{
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guild">Guild where to add a reaction role</param>
/// <param name="msg">Message to which to add a reaction role</param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns>The result of the operation</returns>
Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
string emote,
IRole role,
int group = 0,
int levelReq = 0);
/// <summary>
/// Get all reaction roles on the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId);
/// <summary>
/// Remove reaction roles on the specified message
/// </summary>
/// <param name="guildId"></param>
/// <param name="messageId"></param>
/// <returns></returns>
Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId);
/// <summary>
/// Remove all reaction roles in the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
Task<int> RemoveAllReactionRoles(ulong guildId);
Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId);
}

View File

@@ -0,0 +1,161 @@
using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
public partial class ReactionRoleCommands : NadekoModule
{
private readonly IReactionRoleService _rero;
public ReactionRoleCommands(IReactionRoleService rero)
{
_rero = rero;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRoleAdd(
ulong messageId,
string emoteStr,
IRole role,
int group = 0,
int levelReq = 0)
{
if (group < 0)
return;
if (levelReq < 0)
return;
var msg = await ctx.Channel.GetMessageAsync(messageId);
if (msg is null)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
if (ctx.User.Id != ctx.Guild.OwnerId && ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position) <= role.Position)
{
await ReplyErrorLocalizedAsync(strs.hierarchy);
return;
}
var emote = emoteStr.ToIEmote();
await msg.AddReactionAsync(emote);
var res = await _rero.AddReactionRole(ctx.Guild,
msg,
emoteStr,
role,
group,
levelReq);
await res.Match(
_ => ctx.OkAsync(),
fl =>
{
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
return !fl.IsPatronLimit
? ReplyErrorLocalizedAsync(strs.limit_reached(fl.Quota))
: ReplyPendingLocalizedAsync(strs.feature_limit_reached_owner(fl.Quota, fl.Name));
});
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesList()
{
var reros = await _rero.GetReactionRolesAsync(ctx.Guild.Id);
var embed = _eb.Create(ctx)
.WithOkColor();
var content = string.Empty;
foreach (var g in reros.GroupBy(x => x.MessageId).OrderBy(x => x.Key))
{
var messageId = g.Key;
content +=
$"[{messageId}](https://discord.com/channels/{ctx.Guild.Id}/{g.First().ChannelId}/{g.Key})\n";
var groupGroups = g.GroupBy(x => x.Group);
foreach (var ggs in groupGroups)
{
content += $"`< {(g.Key == 0 ? ("Not Exclusive (Group 0)") : ($"Group {ggs.Key}"))} >`\n";
foreach (var rero in ggs)
{
content += $"\t{rero.Emote} -> {(ctx.Guild.GetRole(rero.RoleId)?.Mention ?? "<missing role>")}";
if (rero.LevelReq > 0)
content += $" (lvl {rero.LevelReq}+)";
content += '\n';
}
}
}
embed.WithDescription(string.IsNullOrWhiteSpace(content)
? "There are no reaction roles on this server"
: content);
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesRemove(ulong messageId)
{
var succ = await _rero.RemoveReactionRoles(ctx.Guild.Id, messageId);
if (succ)
await ctx.OkAsync();
else
await ctx.ErrorAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesDeleteAll()
{
await _rero.RemoveAllReactionRoles(ctx.Guild.Id);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Ratelimit(60)]
public async partial Task ReactionRolesTransfer(ulong fromMessageId, ulong toMessageId)
{
var msg = await ctx.Channel.GetMessageAsync(toMessageId);
if (msg is null)
{
await ctx.ErrorAsync();
return;
}
var reactions = await _rero.TransferReactionRolesAsync(ctx.Guild.Id, fromMessageId, toMessageId);
if (reactions.Count == 0)
{
await ctx.ErrorAsync();
}
else
{
foreach (var r in reactions)
{
await msg.AddReactionAsync(r);
}
}
}
}
}

View File

@@ -0,0 +1,375 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Utility.Patronage;
using NadekoBot.Modules.Xp.Extensions;
using NadekoBot.Services.Database.Models;
using OneOf.Types;
using OneOf;
namespace NadekoBot.Modules.Administration.Services;
public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionRoleService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new();
private readonly SemaphoreSlim _assignementLock = new(1, 1);
private readonly IPatronageService _ps;
private static readonly FeatureLimitKey _reroFLKey = new()
{
Key = "rero:max_count",
PrettyName = "Reaction Role"
};
public ReactionRolesService(
DiscordSocketClient client,
DbService db,
IBotCredentials creds,
IPatronageService ps)
{
_db = db;
_ps = ps;
_client = client;
_creds = creds;
_cache = new();
}
public async Task OnReadyAsync()
{
await using var uow = _db.GetDbContext();
var reros = await uow.GetTable<ReactionRoleV2>()
.Where(
x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
foreach (var group in reros.GroupBy(x => x.MessageId))
{
_cache[group.Key] = group.ToList();
}
_client.ReactionAdded += ClientOnReactionAdded;
_client.ReactionRemoved += ClientOnReactionRemoved;
}
private async Task<(IGuildUser, IRole)> GetUserAndRoleAsync(
SocketReaction r,
ReactionRoleV2 rero)
{
var guild = _client.GetGuild(rero.GuildId);
var role = guild?.GetRole(rero.RoleId);
if (role is null)
return default;
var user = guild.GetUser(r.UserId) as IGuildUser
?? await _client.Rest.GetGuildUserAsync(guild.Id, r.UserId);
if (user is null)
return default;
return (user, role);
}
private Task ClientOnReactionRemoved(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> ch,
SocketReaction r)
{
if (!_cache.TryGetValue(msg.Id, out var reros))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString());
if (rero is null)
return;
var (user, role) = await GetUserAndRoleAsync(r, rero);
if (user.IsBot)
return;
await _assignementLock.WaitAsync();
try
{
if (user.RoleIds.Contains(role.Id))
{
await user.RemoveRoleAsync(role.Id);
}
}
finally
{
_assignementLock.Release();
}
});
return Task.CompletedTask;
}
private Task ClientOnReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> ch,
SocketReaction r)
{
if (!_cache.TryGetValue(msg.Id, out var reros))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString());
if (rero is null)
return;
var (user, role) = await GetUserAndRoleAsync(r, rero);
if (user.IsBot)
return;
await _assignementLock.WaitAsync();
try
{
if (!user.RoleIds.Contains(role.Id))
{
// first check if there is a level requirement
// and if there is, make sure user satisfies it
if (rero.LevelReq > 0)
{
await using var ctx = _db.GetDbContext();
var levelData = await ctx.GetTable<UserXpStats>()
.GetLevelDataFor(user.GuildId, user.Id);
if (levelData.Level < rero.LevelReq)
return;
}
// remove all other roles from the same group from the user
// execept in group 0, which is a special, non-exclusive group
if (rero.Group != 0)
{
var exclusive = reros
.Where(x => x.Group == rero.Group && x.RoleId != role.Id)
.Select(x => x.RoleId)
.Distinct();
try { await user.RemoveRolesAsync(exclusive); }
catch { }
// remove user's previous reaction
try
{
var m = await msg.GetOrDownloadAsync();
if (m is not null)
{
var reactToRemove = m.Reactions
.FirstOrDefault(x => x.Key.ToString() != r.Emote.ToString())
.Key;
if (reactToRemove is not null)
{
await m.RemoveReactionAsync(reactToRemove, user);
}
}
}
catch
{
}
}
await user.AddRoleAsync(role.Id);
}
}
finally
{
_assignementLock.Release();
}
});
return Task.CompletedTask;
}
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guild">Guild where to add a reaction role</param>
/// <param name="msg">Message to which to add a reaction role</param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns>The result of the operation</returns>
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
string emote,
IRole role,
int group = 0,
int levelReq = 0)
{
if (group < 0)
throw new ArgumentOutOfRangeException(nameof(group));
if (levelReq < 0)
throw new ArgumentOutOfRangeException(nameof(group));
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 result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
return result;
await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guild.Id,
ChannelId = msg.Channel.Id,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
(old) => new()
{
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
() => new()
{
MessageId = msg.Id,
Emote = emote,
});
await tran.CommitAsync();
var obj = new ReactionRoleV2()
{
GuildId = guild.Id,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
Group = group,
LevelReq = levelReq
};
lock (_cacheLock)
{
_cache.AddOrUpdate(msg.Id,
_ => new()
{
obj
},
(_, list) =>
{
list.RemoveAll(x => x.Emote == emote);
list.Add(obj);
return list;
});
}
return new Success();
}
/// <summary>
/// Get all reaction roles on the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
public async Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.ToListAsync();
}
/// <summary>
/// Remove reaction roles on the specified message
/// </summary>
/// <param name="guildId"></param>
/// <param name="messageId"></param>
/// <returns></returns>
public async Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId)
{
// guildid is used for quick index lookup
await using var ctx = _db.GetDbContext();
var changed = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.DeleteAsync();
_cache.TryRemove(messageId, out _);
if (changed == 0)
return false;
return true;
}
/// <summary>
/// Remove all reaction roles in the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
public async Task<int> RemoveAllReactionRoles(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var output = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.DeleteWithOutputAsync(x => x.MessageId);
lock (_cacheLock)
{
foreach (var o in output)
{
_cache.TryRemove(o, out _);
}
}
return output.Length;
}
public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(
ulong guildId,
ulong fromMessageId,
ulong toMessageId)
{
await using var ctx = _db.GetDbContext();
var updated = ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == fromMessageId)
.UpdateWithOutput(old => new()
{
MessageId = toMessageId
},
(old, neu) => neu);
lock (_cacheLock)
{
if (_cache.TryRemove(fromMessageId, out var data))
{
if (_cache.TryGetValue(toMessageId, out var newData))
{
newData.AddRange(data);
}
else
{
_cache[toMessageId] = data;
}
}
}
return updated.Select(x => x.Emote.ToIEmote()).ToList();
}
}

View File

@@ -7,172 +7,18 @@ using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
public partial class RoleCommands : NadekoModule<RoleCommandsService>
public partial class RoleCommands : NadekoModule
{
public enum Exclude { Excl }
private readonly IServiceProvider _services;
public RoleCommands(IServiceProvider services)
=> _services = services;
public async Task InternalReactionRoles(bool exclusive, ulong? messageId, params string[] input)
{
var target = messageId is { } msgId
? await ctx.Channel.GetMessageAsync(msgId)
: (await ctx.Channel.GetMessagesAsync(2).FlattenAsync()).Skip(1).FirstOrDefault();
if (input.Length % 2 != 0 || target is null)
return;
var all = await input.Chunk(2)
.Select(async x =>
{
var inputRoleStr = x.First();
var roleReader = new RoleTypeReader<SocketRole>();
var roleResult = await roleReader.ReadAsync(ctx, inputRoleStr, _services);
if (!roleResult.IsSuccess)
{
Log.Warning("Role {Role} not found", inputRoleStr);
return null;
}
var role = (IRole)roleResult.BestMatch;
if (role.Position
> ((IGuildUser)ctx.User).GetRoles()
.Select(r => r.Position)
.Max()
&& ctx.User.Id != ctx.Guild.OwnerId)
return null;
var emote = x.Last().ToIEmote();
return new
{
role,
emote
};
})
.Where(x => x is not null)
.WhenAll();
if (!all.Any())
return;
foreach (var x in all)
{
try
{
await target.AddReactionAsync(x.emote,
new()
{
RetryMode = RetryMode.Retry502 | RetryMode.RetryRatelimit
});
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.BadRequest)
{
await ReplyErrorLocalizedAsync(strs.reaction_cant_access(Format.Code(x.emote.ToString())));
return;
}
await Task.Delay(500);
}
if (_service.Add(ctx.Guild.Id,
new()
{
Exclusive = exclusive,
MessageId = target.Id,
ChannelId = target.Channel.Id,
ReactionRoles = all.Select(x =>
{
return new ReactionRole
{
EmoteName = x.emote.ToString(),
RoleId = x.role.Id
};
})
.ToList()
}))
await ctx.OkAsync();
else
await ReplyErrorLocalizedAsync(strs.reaction_roles_full);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public partial Task ReactionRoles(ulong messageId, params string[] input)
=> InternalReactionRoles(false, messageId, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public partial Task ReactionRoles(ulong messageId, Exclude _, params string[] input)
=> InternalReactionRoles(true, messageId, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public partial Task ReactionRoles(params string[] input)
=> InternalReactionRoles(false, null, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public partial Task ReactionRoles(Exclude _, params string[] input)
=> InternalReactionRoles(true, null, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesList()
{
var embed = _eb.Create().WithOkColor();
if (!_service.Get(ctx.Guild.Id, out var rrs) || !rrs.Any())
embed.WithDescription(GetText(strs.no_reaction_roles));
else
{
var g = (SocketGuild)ctx.Guild;
foreach (var rr in rrs)
{
var ch = g.GetTextChannel(rr.ChannelId);
IUserMessage msg = null;
if (ch is not null)
msg = await ch.GetMessageAsync(rr.MessageId) as IUserMessage;
var content = msg?.Content.TrimTo(30) ?? "DELETED!";
embed.AddField($"**{rr.Index + 1}.** {ch?.Name ?? "DELETED!"}",
GetText(strs.reaction_roles_message(rr.ReactionRoles?.Count ?? 0, content)));
}
}
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesRemove(int index)
{
if (index < 1 || !_service.Get(ctx.Guild.Id, out var rrs) || !rrs.Any() || rrs.Count < index)
return;
index--;
_service.Remove(ctx.Guild.Id, index);
await ReplyConfirmLocalizedAsync(strs.reaction_role_removed(index + 1));
_services = services;
}
[Cmd]
@@ -303,7 +149,7 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task RoleHoist(IRole role)
public async partial Task RoleHoist([Leftover] IRole role)
{
var newHoisted = !role.IsHoisted;
await role.ModifyAsync(r => r.Hoist = newHoisted);

View File

@@ -1,253 +0,0 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.Collections;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration.Services;
public class RoleCommandsService : INService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
/// <summary>
/// Contains the (Message ID, User ID) of reaction roles that are currently being processed.
/// </summary>
private readonly ConcurrentHashSet<(ulong, ulong)> _reacting = new();
public RoleCommandsService(DiscordSocketClient client, DbService db, Bot bot)
{
_db = db;
_client = client;
#if !GLOBAL_NADEKO
_models = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.ReactionRoleMessages).ToConcurrent();
_client.ReactionAdded += _client_ReactionAdded;
_client.ReactionRemoved += _client_ReactionRemoved;
#endif
}
private Task _client_ReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> chan,
SocketReaction reaction)
{
_ = Task.Run(async () =>
{
if (!reaction.User.IsSpecified
|| reaction.User.Value.IsBot
|| reaction.User.Value is not SocketGuildUser gusr
|| chan.Value is not SocketGuildChannel gch
|| !_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf is null)
return;
// compare emote names for backwards compatibility :facepalm:
var reactionRole = conf.ReactionRoles.FirstOrDefault(x
=> x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole is not null)
{
if (!conf.Exclusive)
{
await AddReactionRoleAsync(gusr, reactionRole);
return;
}
// If same (message, user) are being processed in an exclusive rero, quit
if (!_reacting.Add((msg.Id, reaction.UserId)))
return;
try
{
var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg,
gusr,
reaction,
conf,
reactionRole,
CancellationToken.None);
var addRoleTask = AddReactionRoleAsync(gusr, reactionRole);
await Task.WhenAll(removeExclusiveTask, addRoleTask);
}
finally
{
// Free (message/user) for another exclusive rero
_reacting.TryRemove((msg.Id, reaction.UserId));
}
}
else
{
var dl = await msg.GetOrDownloadAsync();
await dl.RemoveReactionAsync(reaction.Emote,
dl.Author,
new()
{
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
});
Log.Warning("User {Author} is adding unrelated reactions to the reaction roles message", dl.Author);
}
});
return Task.CompletedTask;
}
private Task _client_ReactionRemoved(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> chan,
SocketReaction reaction)
{
_ = Task.Run(async () =>
{
try
{
if (!reaction.User.IsSpecified
|| reaction.User.Value.IsBot
|| reaction.User.Value is not SocketGuildUser gusr)
return;
if (chan.Value is not SocketGuildChannel gch)
return;
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf is null)
return;
var reactionRole = conf.ReactionRoles.FirstOrDefault(x
=> x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole is not null)
{
var role = gusr.Guild.GetRole(reactionRole.RoleId);
if (role is null)
return;
await gusr.RemoveRoleAsync(role);
}
}
catch { }
});
return Task.CompletedTask;
}
public bool Get(ulong id, out IndexedCollection<ReactionRoleMessage> rrs)
=> _models.TryGetValue(id, out rrs);
public bool Add(ulong id, ReactionRoleMessage rrm)
{
using var uow = _db.GetDbContext();
var table = uow.GetTable<ReactionRoleMessage>();
table.Delete(x => x.MessageId == rrm.MessageId);
var gc = uow.GuildConfigsForId(id,
set => set.Include(x => x.ReactionRoleMessages).ThenInclude(x => x.ReactionRoles));
if (gc.ReactionRoleMessages.Count >= 10)
return false;
gc.ReactionRoleMessages.Add(rrm);
uow.SaveChanges();
_models.AddOrUpdate(id, gc.ReactionRoleMessages, delegate { return gc.ReactionRoleMessages; });
return true;
}
public void Remove(ulong id, int index)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(id,
set => set.Include(x => x.ReactionRoleMessages).ThenInclude(x => x.ReactionRoles));
uow.Set<ReactionRole>().RemoveRange(gc.ReactionRoleMessages[index].ReactionRoles);
gc.ReactionRoleMessages.RemoveAt(index);
_models.AddOrUpdate(id, gc.ReactionRoleMessages, delegate { return gc.ReactionRoleMessages; });
uow.SaveChanges();
}
/// <summary>
/// Adds a reaction role to the specified user.
/// </summary>
/// <param name="user">A Discord guild user.</param>
/// <param name="dbRero">The database settings of this reaction role.</param>
private Task AddReactionRoleAsync(SocketGuildUser user, ReactionRole dbRero)
{
var toAdd = user.Guild.GetRole(dbRero.RoleId);
return toAdd is not null && !user.Roles.Contains(toAdd) ? user.AddRoleAsync(toAdd) : Task.CompletedTask;
}
/// <summary>
/// Removes the exclusive reaction roles and reactions from the specified user.
/// </summary>
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
/// <param name="user">A Discord guild user.</param>
/// <param name="reaction">The Discord reaction of the user.</param>
/// <param name="dbReroMsg">The database entry of the reaction role message.</param>
/// <param name="dbRero">The database settings of this reaction role.</param>
/// <param name="cToken">A cancellation token to cancel the operation.</param>
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
private Task RemoveExclusiveReactionRoleAsync(
Cacheable<IUserMessage, ulong> reactionMessage,
SocketGuildUser user,
SocketReaction reaction,
ReactionRoleMessage dbReroMsg,
ReactionRole dbRero,
CancellationToken cToken = default)
{
cToken.ThrowIfCancellationRequested();
var roleIds = dbReroMsg.ReactionRoles.Select(x => x.RoleId)
.Where(x => x != dbRero.RoleId)
.Select(x => user.Guild.GetRole(x))
.Where(x => x is not null);
var removeReactionsTask = RemoveOldReactionsAsync(reactionMessage, user, reaction, cToken);
var removeRolesTask = user.RemoveRolesAsync(roleIds);
return Task.WhenAll(removeReactionsTask, removeRolesTask);
}
/// <summary>
/// Removes old reactions from an exclusive reaction role.
/// </summary>
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
/// <param name="user">A Discord guild user.</param>
/// <param name="reaction">The Discord reaction of the user.</param>
/// <param name="cToken">A cancellation token to cancel the operation.</param>
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
private async Task RemoveOldReactionsAsync(
Cacheable<IUserMessage, ulong> reactionMessage,
SocketGuildUser user,
SocketReaction reaction,
CancellationToken cToken = default)
{
cToken.ThrowIfCancellationRequested();
//if the role is exclusive,
// remove all other reactions user added to the message
var dl = await reactionMessage.GetOrDownloadAsync();
foreach (var r in dl.Reactions)
{
if (r.Key.Name == reaction.Emote.Name)
continue;
try { await dl.RemoveReactionAsync(r.Key, user); }
catch { }
await Task.Delay(100, cToken);
}
}
}

View File

@@ -530,7 +530,7 @@ public class UserPunishService : INService, IReadyExecutor
return default;
// if template is an embed, send that embed with replacements
// otherwise, treat template as a regular string with replacements
else if (!SmartText.CreateFrom(template).IsEmbed)
else if (SmartText.CreateFrom(template) is not { IsEmbed: true } or { IsEmbedArray: true })
{
template = JsonConvert.SerializeObject(new
{

View File

@@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace NadekoBot.Modules.NadekoExpressions;
@@ -266,8 +266,8 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
public async partial Task ExprClear()
{
if (await PromptUserConfirmAsync(_eb.Create()
.WithTitle("Custom reaction clear")
.WithDescription("This will delete all custom reactions on this server.")))
.WithTitle("Expression clear")
.WithDescription("This will delete all expressions on this server.")))
{
var count = _service.DeleteAllExpressions(ctx.Guild.Id);
await ReplyConfirmLocalizedAsync(strs.exprs_cleared(count));
@@ -334,4 +334,4 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
await ctx.OkAsync();
}
}
}

View File

@@ -0,0 +1,72 @@
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
[Name("Bank")]
[Group("bank")]
public partial class BankCommands : GamblingModule<IBankService>
{
private readonly IBankService _bank;
public BankCommands(GamblingConfigService gcs, IBankService bank) : base(gcs)
{
_bank = bank;
}
[Cmd]
public async partial Task BankDeposit(ShmartNumber amount)
{
if (amount <= 0)
return;
if (await _bank.DepositAsync(ctx.User.Id, amount))
{
await ReplyConfirmLocalizedAsync(strs.bank_deposited(N(amount)));
}
else
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
}
}
[Cmd]
public async partial Task BankWithdraw(ShmartNumber amount)
{
if (amount <= 0)
return;
if (await _bank.WithdrawAsync(ctx.User.Id, amount))
{
await ReplyConfirmLocalizedAsync(strs.bank_withdrew(N(amount)));
}
else
{
await ReplyErrorLocalizedAsync(strs.bank_withdraw_insuff(CurrencySign));
}
}
[Cmd]
public async partial Task BankBalance()
{
var bal = await _bank.GetBalanceAsync(ctx.User.Id);
var eb = _eb.Create(ctx)
.WithOkColor()
.WithDescription(GetText(strs.bank_balance(N(bal))));
try
{
await ctx.User.EmbedAsync(eb);
await ctx.OkAsync();
}
catch
{
await ReplyErrorLocalizedAsync(strs.cant_dm);
}
}
}
}

View File

@@ -0,0 +1,78 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Gambling.Bank;
public sealed class BankService : IBankService, INService
{
private readonly ICurrencyService _cur;
private readonly DbService _db;
public BankService(ICurrencyService cur, DbService db)
{
_cur = cur;
_db = db;
}
public async Task<bool> DepositAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
if (!await _cur.RemoveAsync(userId, amount, new("bank", "deposit")))
return false;
await using var ctx = _db.GetDbContext();
await ctx.BankUsers
.ToLinqToDBTable()
.InsertOrUpdateAsync(() => new()
{
UserId = userId,
Balance = amount
},
(old) => new()
{
Balance = old.Balance + amount
},
() => new()
{
UserId = userId
});
return true;
}
public async Task<bool> WithdrawAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
await using var ctx = _db.GetDbContext();
var rows = await ctx.BankUsers
.ToLinqToDBTable()
.Where(x => x.UserId == userId && x.Balance >= amount)
.UpdateAsync((old) => new()
{
Balance = old.Balance - amount
});
if (rows > 0)
{
await _cur.AddAsync(userId, amount, new("bank", "withdraw"));
return true;
}
return false;
}
public async Task<long> GetBalanceAsync(ulong userId)
{
await using var ctx = _db.GetDbContext();
return (await ctx.BankUsers
.ToLinqToDBTable()
.FirstOrDefaultAsync(x => x.UserId == userId))
?.Balance
?? 0;
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Gambling.Bank;
public interface IBankService
{
Task<bool> DepositAsync(ulong userId, long amount);
Task<bool> WithdrawAsync(ulong userId, long amount);
Task<long> GetBalanceAsync(ulong userId);
}

View File

@@ -0,0 +1,13 @@
#nullable disable
namespace NadekoBot.Modules.Gambling;
public class CashInteraction : NInteraction
{
protected override NadekoInteractionData Data
=> new NadekoInteractionData(new Emoji("🏦"), "cash:bank_show_balance");
public CashInteraction(DiscordSocketClient client, ulong userId, Func<SocketMessageComponent, Task> action)
: base(client, userId, action)
{
}
}

View File

@@ -3,10 +3,13 @@ using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Db;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Utility.Patronage;
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Services.Currency;
using NadekoBot.Services.Database.Models;
using System.Collections.Immutable;
using System.Globalization;
using System.Text;
@@ -40,6 +43,8 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService;
private readonly IBankService _bank;
private readonly IPatronageService _ps;
private IUserMessage rdMsg;
@@ -49,13 +54,18 @@ public partial class Gambling : GamblingModule<GamblingService>
IDataCache cache,
DiscordSocketClient client,
DownloadTracker tracker,
GamblingConfigService configService)
GamblingConfigService configService,
IBankService bank,
IPatronageService ps)
: base(configService)
{
_db = db;
_cs = currency;
_cache = cache;
_client = client;
_bank = bank;
_ps = ps;
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
_enUsCulture.NumberGroupSeparator = "";
@@ -72,7 +82,7 @@ public partial class Gambling : GamblingModule<GamblingService>
[Cmd]
public async partial Task Economy()
{
var ec = _service.GetEconomy();
var ec = await _service.GetEconomyAsync();
decimal onePercent = 0;
// This stops the top 1% from owning more than 100% of the money
@@ -84,19 +94,25 @@ public partial class Gambling : GamblingModule<GamblingService>
// [21:03] Bob Page: Kinda remids me of US economy
var embed = _eb.Create()
.WithTitle(GetText(strs.economy_state))
.AddField(GetText(strs.currency_owned),
N(ec.Cash - ec.Bot))
.AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot))
.AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
.AddField(GetText(strs.currency_planted), N(ec.Planted))
.AddField(GetText(strs.owned_waifus_total), N(ec.Waifus))
.AddField(GetText(strs.bot_currency), N(ec.Bot))
.AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus))
.AddField(GetText(strs.bank_accounts), N(ec.Bank))
.AddField(GetText(strs.total), N(ec.Cash + ec.Planted + ec.Waifus + ec.Bank))
.WithOkColor();
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
await ctx.Channel.EmbedAsync(embed);
}
private static readonly FeatureLimitKey _timelyKey = new FeatureLimitKey()
{
Key = "timely:extra_percent",
PrettyName = "Timely"
};
[Cmd]
public async partial Task Timely()
{
@@ -114,6 +130,10 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0);
val = (int)(val * (1 + (result.Quota * 0.01f)));
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
await ReplyConfirmLocalizedAsync(strs.timely(N(val), period));
@@ -232,7 +252,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var kwumId = new kwum(tr.Id).ToString();
var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
if (transactionString is not null)
@@ -260,8 +279,7 @@ public partial class Gambling : GamblingModule<GamblingService>
int intId = id;
await using var uow = _db.GetDbContext();
var tr = await uow.CurrencyTransactions
.ToLinqToDBTable()
var tr = await uow.CurrencyTransactions.ToLinqToDBTable()
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
.FirstOrDefaultAsync();
@@ -271,8 +289,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var eb = _eb.Create(ctx)
.WithOkColor();
var eb = _eb.Create(ctx).WithOkColor();
eb.WithAuthor(ctx.User);
eb.WithTitle(GetText(strs.transaction));
@@ -291,7 +308,6 @@ public partial class Gambling : GamblingModule<GamblingService>
eb.AddField("Note", tr.Note);
}
eb.WithFooter(GetFormattedCurtrDate(tr));
await ctx.Channel.EmbedAsync(eb);
@@ -311,7 +327,7 @@ public partial class Gambling : GamblingModule<GamblingService>
(_, _, ulong userId) => $"{type.Titleize()} - {subType.Titleize()} | [{userId}]",
_ => $"{type.Titleize()} - {subType.Titleize()}"
};
[Cmd]
[Priority(0)]
public async partial Task Cash(ulong userId)
@@ -320,13 +336,36 @@ public partial class Gambling : GamblingModule<GamblingService>
await ReplyConfirmLocalizedAsync(strs.has(Format.Code(userId.ToString()), cur));
}
private async Task BankAction(SocketMessageComponent smc)
{
var balance = await _bank.GetBalanceAsync(ctx.User.Id);
await N(balance)
.Pipe(strs.bank_balance)
.Pipe(GetText)
.Pipe(text => smc.RespondConfirmAsync(_eb, text, ephemeral: true));
}
private NadekoButtonInteraction CreateCashInteraction()
=> new CashInteraction(_client, ctx.User.Id, BankAction).GetInteraction();
[Cmd]
[Priority(1)]
public async partial Task Cash([Leftover] IUser user = null)
{
user ??= ctx.User;
var cur = await GetBalanceStringAsync(user.Id);
await ConfirmLocalizedAsync(strs.has(Format.Bold(user.ToString()), cur));
var inter = user == ctx.User
? CreateCashInteraction()
: null;
await ConfirmLocalizedAsync(
user.ToString()
.Pipe(Format.Bold)
.With(cur)
.Pipe(strs.has),
inter);
}
[Cmd]
@@ -339,7 +378,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
if (!await _cs.TransferAsync(ctx.User.Id, receiver.Id, amount, ctx.User.ToString(), msg))
if (!await _cs.TransferAsync(_eb, ctx.User, receiver, amount, msg, N(amount)))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
@@ -386,10 +425,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
await _cs.AddAsync(usr.Id,
amount,
new("award", ctx.User.ToString()!, msg, ctx.User.Id)
);
await _cs.AddAsync(usr.Id, amount, new("award", ctx.User.ToString()!, msg, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.awarded(N(amount), $"<@{usrId}>"));
}
@@ -403,10 +439,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.AddBulkAsync(users.Select(x => x.Id).ToList(),
amount,
new("award",
ctx.User.ToString()!,
role.Name,
ctx.User.Id));
new("award", ctx.User.ToString()!, role.Name, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.mass_award(N(amount),
Format.Bold(users.Count.ToString()),
@@ -423,10 +456,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.RemoveBulkAsync(users.Select(x => x.Id).ToList(),
amount,
new("take",
ctx.User.ToString()!,
null,
ctx.User.Id));
new("take", ctx.User.ToString()!, null, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.mass_take(N(amount),
Format.Bold(users.Count.ToString()),
@@ -444,10 +474,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var extra = new TxData("take",
ctx.User.ToString()!,
null,
ctx.User.Id);
var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id);
if (await _cs.RemoveAsync(user.Id, amount, extra))
{
@@ -459,7 +486,6 @@ public partial class Gambling : GamblingModule<GamblingService>
}
}
[Cmd]
[OwnerOnly]
public async partial Task Take(long amount, [Leftover] ulong usrId)
@@ -469,10 +495,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var extra = new TxData("take",
ctx.User.ToString()!,
null,
ctx.User.Id);
var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id);
if (await _cs.RemoveAsync(usrId, amount, extra))
{
@@ -560,10 +583,7 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else
{
await rdMsg.ModifyAsync(x =>
{
x.Embed = embed.Build();
});
await rdMsg.ModifyAsync(x => { x.Embed = embed.Build(); });
}
}
@@ -613,7 +633,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var result = br.Roll();
var str = Format.Bold(ctx.User.ToString()) + Format.Code(GetText(strs.roll(result.Roll)));
if (result.Multiplier > 0)
{
@@ -742,9 +761,7 @@ public partial class Gambling : GamblingModule<GamblingService>
if (amount > 0)
{
if (!await _cs.RemoveAsync(ctx.User.Id,
amount,
new("rps", "bet", "")))
if (!await _cs.RemoveAsync(ctx.User.Id, amount, new("rps", "bet", "")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
@@ -778,4 +795,31 @@ public partial class Gambling : GamblingModule<GamblingService>
await ctx.Channel.EmbedAsync(embed);
}
private static readonly ImmutableArray<string> _emojis =
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
[Cmd]
public async partial Task WheelOfFortune(ShmartNumber amount)
{
if (!await CheckBetMandatory(amount))
return;
if (!await _cs.RemoveAsync(ctx.User.Id, amount, new("wheel", "bet")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
var result = await _service.WheelOfFortuneSpinAsync(ctx.User.Id, amount);
var wofMultipliers = Config.WheelOfFortune.Multipliers;
await SendConfirmAsync(Format.Bold($@"{ctx.User} won: {N(result.Amount)}
『{wofMultipliers[1]}』 『{wofMultipliers[0]}』 『{wofMultipliers[7]}』
『{wofMultipliers[2]}』 {_emojis[result.Index]} 『{wofMultipliers[6]}』
『{wofMultipliers[3]}』 『{wofMultipliers[4]}』 『{wofMultipliers[5]}』"));
}
}

View File

@@ -1,8 +1,11 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Db.Models;
using NadekoBot.Migrations;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Common.Connect4;
using NadekoBot.Modules.Gambling.Common.Slot;
@@ -158,7 +161,7 @@ public class GamblingService : INService, IReadyExecutor
return toReturn;
}
public EconomyResult GetEconomy()
public async Task<EconomyResult> GetEconomyAsync()
{
if (_cache.TryGetEconomy(out var data))
{
@@ -173,6 +176,7 @@ public class GamblingService : INService, IReadyExecutor
decimal onePercent;
decimal planted;
decimal waifus;
decimal bank;
long bot;
using (var uow = _db.GetDbContext())
@@ -182,6 +186,8 @@ public class GamblingService : INService, IReadyExecutor
planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount);
waifus = uow.WaifuInfo.GetTotalValue();
bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id);
bank = await uow.GetTable<BankUser>()
.SumAsyncLinqToDB(x => x.Balance);
}
var result = new EconomyResult
@@ -190,7 +196,8 @@ public class GamblingService : INService, IReadyExecutor
Planted = planted,
Bot = bot,
Waifus = waifus,
OnePercent = onePercent
OnePercent = onePercent,
Bank = bank
};
_cache.SetEconomy(JsonConvert.SerializeObject(result));
@@ -207,6 +214,7 @@ public class GamblingService : INService, IReadyExecutor
public decimal Planted { get; set; }
public decimal Waifus { get; set; }
public decimal OnePercent { get; set; }
public decimal Bank { get; set; }
public long Bot { get; set; }
}
}

View File

@@ -39,13 +39,19 @@ public abstract class GamblingModule<TService> : NadekoModule<TService>
return true;
}
public static string N(long cur, IFormatProvider format)
=> cur.ToString("C0", format);
public static string N(decimal cur, IFormatProvider format)
=> cur.ToString("C0", format);
protected string N(long cur)
=> cur.ToString("C0", GetFlowersCiInternal());
=> N(cur, GetFlowersCiInternal());
protected string N(decimal cur)
=> cur.ToString("C0", GetFlowersCiInternal());
=> N(cur, GetFlowersCiInternal());
private IFormatProvider GetFlowersCiInternal()
protected IFormatProvider GetFlowersCiInternal()
{
var flowersCi = (CultureInfo)Culture.Clone();
flowersCi.NumberFormat.CurrencySymbol = CurrencySign;

View File

@@ -1,49 +0,0 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using System.Collections.Immutable;
namespace NadekoBot.Modules.Gambling;
public partial class Gambling
{
public partial class WheelOfFortuneCommands : GamblingSubmodule<GamblingService>
{
private static readonly ImmutableArray<string> _emojis =
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
private readonly ICurrencyService _cs;
private readonly DbService _db;
public WheelOfFortuneCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConfService)
: base(gamblingConfService)
{
_cs = cs;
_db = db;
}
[Cmd]
public async partial Task WheelOfFortune(ShmartNumber amount)
{
if (!await CheckBetMandatory(amount))
return;
if (!await _cs.RemoveAsync(ctx.User.Id, amount, new("wheel", "bet")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
var result = await _service.WheelOfFortuneSpinAsync(ctx.User.Id, amount);
var wofMultipliers = Config.WheelOfFortune.Multipliers;
await SendConfirmAsync(Format.Bold($@"{ctx.User} won: {N(result.Amount)}
『{wofMultipliers[1]}』 『{wofMultipliers[0]}』 『{wofMultipliers[7]}』
『{wofMultipliers[2]}』 {_emojis[result.Index]} 『{wofMultipliers[6]}』
『{wofMultipliers[3]}』 『{wofMultipliers[4]}』 『{wofMultipliers[5]}』"));
}
}
}

View File

@@ -1,8 +1,11 @@
#nullable disable
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Games.Common.ChatterBot;
using NadekoBot.Modules.Permissions;
using NadekoBot.Modules.Permissions.Common;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Modules.Utility.Patronage;
namespace NadekoBot.Modules.Games.Services;
@@ -13,6 +16,8 @@ public class ChatterBotService : IExecOnMessage
public int Priority
=> 1;
private readonly FeatureLimitKey _flKey;
private readonly DiscordSocketClient _client;
private readonly PermissionService _perms;
private readonly CommandHandler _cmd;
@@ -20,6 +25,8 @@ public class ChatterBotService : IExecOnMessage
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
private readonly IHttpClientFactory _httpFactory;
private readonly IPatronageService _ps;
private readonly CmdCdService _ccs;
public ChatterBotService(
DiscordSocketClient client,
@@ -29,7 +36,9 @@ public class ChatterBotService : IExecOnMessage
IBotStrings strings,
IHttpClientFactory factory,
IBotCredentials creds,
IEmbedBuilderService eb)
IEmbedBuilderService eb,
IPatronageService ps,
CmdCdService cmdCdService)
{
_client = client;
_perms = perms;
@@ -38,8 +47,17 @@ public class ChatterBotService : IExecOnMessage
_creds = creds;
_eb = eb;
_httpFactory = factory;
_ps = ps;
_ccs = cmdCdService;
ChatterBotGuilds = new(bot.AllGuildConfigs.Where(gc => gc.CleverbotEnabled)
_flKey = new FeatureLimitKey()
{
Key = CleverBotResponseStr.CLEVERBOT_RESPONSE,
PrettyName = "Cleverbot Replies"
};
ChatterBotGuilds = new(bot.AllGuildConfigs
.Where(gc => gc.CleverbotEnabled)
.ToDictionary(gc => gc.GuildId,
_ => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
}
@@ -48,7 +66,9 @@ public class ChatterBotService : IExecOnMessage
{
if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
return new CleverbotIoSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory);
Log.Information("Cleverbot will not work as the api key is missing.");
return null;
}
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
@@ -78,27 +98,11 @@ public class ChatterBotService : IExecOnMessage
return message;
}
public async Task<bool> TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message)
{
await channel.TriggerTypingAsync();
var response = await cleverbot.Think(message);
try
{
await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true));
}
catch
{
await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)); // try twice :\
}
return true;
}
public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
{
if (guild is not SocketGuild sg)
return false;
try
{
var message = PrepareMessage(usrMsg, out var cbs);
@@ -106,7 +110,10 @@ public class ChatterBotService : IExecOnMessage
return false;
var pc = _perms.GetCacheFor(guild.Id);
if (!pc.Permissions.CheckPermissions(usrMsg, "cleverbot", "Games".ToLowerInvariant(), out var index))
if (!pc.Permissions.CheckPermissions(usrMsg,
"cleverbot",
"games",
out var index))
{
if (pc.Verbose)
{
@@ -122,24 +129,78 @@ public class ChatterBotService : IExecOnMessage
return true;
}
var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message);
if (cleverbotExecuted)
if (await _ccs.TryBlock(sg, usrMsg.Author, CleverBotResponseStr.CLEVERBOT_RESPONSE))
{
Log.Information(@"CleverBot Executed
return true;
}
var channel = (ITextChannel)usrMsg.Channel;
var conf = _ps.GetConfig();
if (!_creds.IsOwner(sg.OwnerId) && conf.IsEnabled)
{
var quota = await _ps.TryGetFeatureLimitAsync(_flKey, sg.OwnerId, 0);
uint? daily = quota.Quota is int dVal and < 0
? (uint)-dVal
: null;
uint? monthly = quota.Quota is int mVal and >= 0
? (uint)mVal
: null;
var maybeLimit = await _ps.TryIncrementQuotaCounterAsync(sg.OwnerId,
sg.OwnerId == usrMsg.Author.Id,
FeatureType.Limit,
_flKey.Key,
null,
daily,
monthly);
if (maybeLimit.TryPickT1(out var ql, out var counters))
{
if (ql.Quota == 0)
{
await channel.SendErrorAsync(_eb,
null!,
text:
"In order to use the cleverbot feature, the owner of this server should be [Patron Tier X](https://patreon.com/join/nadekobot) on patreon.",
footer:
"You may disable the cleverbot feature, and this message via '.cleverbot' command");
return true;
}
await channel.SendErrorAsync(_eb,
null!,
$"You've reached your quota limit of **{ql.Quota}** responses {ql.QuotaPeriod.ToFullName()} for the cleverbot feature.",
footer: "You may wait for the quota reset or .");
return true;
}
}
_ = channel.TriggerTypingAsync();
var response = await cbs.Think(message);
await channel.SendConfirmAsync(_eb,
title: null,
response.SanitizeMentions(true)
// , footer: counter > 0 ? counter.ToString() : null
);
Log.Information(@"CleverBot Executed
Server: {GuildName} [{GuildId}]
Channel: {ChannelName} [{ChannelId}]
UserId: {Author} [{AuthorId}]
Message: {Content}",
guild.Name,
guild.Id,
usrMsg.Channel?.Name,
usrMsg.Channel?.Id,
usrMsg.Author,
usrMsg.Author.Id,
usrMsg.Content);
guild.Name,
guild.Id,
usrMsg.Channel?.Name,
usrMsg.Channel?.Id,
usrMsg.Author,
usrMsg.Author.Id,
usrMsg.Content);
return true;
}
return true;
}
catch (Exception ex)
{

View File

@@ -14,7 +14,6 @@ public partial class Games
public ChatterBotCommands(DbService db)
=> _db = db;
[NoPublicBot]
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]

View File

@@ -1,34 +0,0 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Modules.Games.Common.ChatterBot;
public class ChatterBotSession : IChatterBotSession
{
private static NadekoRandom Rng { get; } = new();
private string ApiEndpoint
=> "http://api.program-o.com/v2/chatbot/"
+ $"?bot_id={_botId}&"
+ "say={0}&"
+ $"convo_id=nadekobot_{_chatterBotId}&"
+ "format=json";
private readonly string _chatterBotId;
private readonly IHttpClientFactory _httpFactory;
private readonly int _botId = 6;
public ChatterBotSession(IHttpClientFactory httpFactory)
{
_chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64();
_httpFactory = httpFactory;
}
public async Task<string> Think(string message)
{
using var http = _httpFactory.CreateClient();
var res = await http.GetStringAsync(string.Format(ApiEndpoint, message));
var cbr = JsonConvert.DeserializeObject<ChatterBotResponse>(res);
return cbr.BotSay.Replace("<br/>", "\n", StringComparison.InvariantCulture);
}
}

View File

@@ -5,16 +5,4 @@ public class CleverbotResponse
{
public string Cs { get; set; }
public string Output { get; set; }
}
public class CleverbotIoCreateResponse
{
public string Status { get; set; }
public string Nick { get; set; }
}
public class CleverbotIoAskResponse
{
public string Status { get; set; }
public string Response { get; set; }
}

View File

@@ -35,57 +35,4 @@ public class OfficialCleverbotSession : IChatterBotSession
return null;
}
}
}
public class CleverbotIoSession : IChatterBotSession
{
private readonly string _key;
private readonly string _user;
private readonly IHttpClientFactory _httpFactory;
private readonly AsyncLazy<string> _nick;
private readonly string _createEndpoint = "https://cleverbot.io/1.0/create";
private readonly string _askEndpoint = "https://cleverbot.io/1.0/ask";
public CleverbotIoSession(string user, string key, IHttpClientFactory factory)
{
_key = key;
_user = user;
_httpFactory = factory;
_nick = new(GetNick);
}
private async Task<string> GetNick()
{
using var http = _httpFactory.CreateClient();
using var msg = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("user", _user), new KeyValuePair<string, string>("key", _key)
});
using var data = await http.PostAsync(_createEndpoint, msg);
var str = await data.Content.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<CleverbotIoCreateResponse>(str);
if (obj.Status != "success")
throw new OperationCanceledException(obj.Status);
return obj.Nick;
}
public async Task<string> Think(string input)
{
using var http = _httpFactory.CreateClient();
using var msg = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("user", _user), new KeyValuePair<string, string>("key", _key),
new KeyValuePair<string, string>("nick", await _nick), new KeyValuePair<string, string>("text", input)
});
using var data = await http.PostAsync(_askEndpoint, msg);
var str = await data.Content.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<CleverbotIoAskResponse>(str);
if (obj.Status != "success")
throw new OperationCanceledException(obj.Status);
return obj.Response;
}
}

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