Compare commits

...

21 Commits
4.1.5 ... 4.2.0

Author SHA1 Message Date
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
198 changed files with 15429 additions and 1674 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
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
exists:
- Dockerfile

View File

@@ -5,6 +5,147 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
## Unreleased
-
## [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
- [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
### Obsolete
### Fixed
- Fixed `.crypto` sparklines
## [4.1.6] - 14.05.2022
### Fixed
- Fixed windows release and updated packages
## [4.1.5] - 11.05.2022
### Changed
@@ -17,7 +158,7 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
- 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
## [4.1.4] - 06.05.2022
### Fixed

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

@@ -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

@@ -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;
@@ -125,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>();
@@ -164,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

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

View File

@@ -1,23 +1,24 @@
namespace NadekoBot;
public abstract class NadekoInteraction
public abstract class NadekoButtonInteraction
{
// improvements:
// - state in OnAction
// - configurable delay
// -
public abstract string Name { get; }
public abstract IEmote Emote { get; }
protected abstract string Name { get; }
protected abstract IEmote Emote { get; }
protected virtual string? Text { get; } = null;
protected readonly DiscordSocketClient _client;
public DiscordSocketClient Client { get; }
protected readonly TaskCompletionSource<bool> _interactionCompletedSource;
protected IUserMessage message = null!;
protected NadekoInteraction(DiscordSocketClient client)
protected NadekoButtonInteraction(DiscordSocketClient client)
{
_client = client;
Client = client;
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
}
@@ -25,9 +26,9 @@ public abstract class NadekoInteraction
{
message = msg;
_client.InteractionCreated += OnInteraction;
Client.InteractionCreated += OnInteraction;
await Task.WhenAny(Task.Delay(10_000), _interactionCompletedSource.Task);
_client.InteractionCreated -= OnInteraction;
Client.InteractionCreated -= OnInteraction;
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
}
@@ -65,13 +66,18 @@ public abstract class NadekoInteraction
}
public MessageComponent CreateComponent()
public virtual MessageComponent CreateComponent()
{
var comp = new ComponentBuilder()
.WithButton(new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name));
.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

@@ -20,6 +20,7 @@ public class NadekoInteractionBuilder
// {
// this.isOwn = isOwn;
// return this;
// }
public NadekoInteractionBuilder WithAction(in Func<SocketMessageComponent, Task> fn)
@@ -28,7 +29,7 @@ public class NadekoInteractionBuilder
return this;
}
public NadekoActionInteraction Build(DiscordSocketClient client, ulong userId)
public NadekoButtonActionInteraction Build(DiscordSocketClient client, ulong userId)
{
if (iData is null)
throw new InvalidOperationException("You have to specify the data before building the interaction");

View File

@@ -5,4 +5,4 @@
/// </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);
public record NadekoInteractionData(IEmote Emote, string CustomId, string? Text = null);

View File

@@ -3,11 +3,11 @@
/// <summary>
/// Interaction which only the author can use
/// </summary>
public abstract class NadekoOwnInteraction : NadekoInteraction
public abstract class NadekoButtonOwnInteraction : NadekoButtonInteraction
{
protected readonly ulong _authorId;
protected NadekoOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
protected NadekoButtonOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
=> _authorId = authorId;
protected override ValueTask<bool> Validate(SocketMessageComponent smc)

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

@@ -36,7 +36,7 @@ public abstract class NadekoModule : ModuleBase
string error,
string url = null,
string footer = null,
NadekoInteraction inter = null)
NadekoButtonInteraction inter = null)
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
public Task<IUserMessage> SendConfirmAsync(
@@ -47,32 +47,32 @@ public abstract class NadekoModule : ModuleBase
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
//
public Task<IUserMessage> SendErrorAsync(string text, NadekoInteraction inter = null)
public Task<IUserMessage> SendErrorAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Error, inter);
public Task<IUserMessage> SendConfirmAsync(string text, NadekoInteraction inter = null)
public Task<IUserMessage> SendConfirmAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Ok, inter);
public Task<IUserMessage> SendPendingAsync(string text, NadekoInteraction inter = null)
public Task<IUserMessage> SendPendingAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Pending, inter);
// localized normal
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendErrorAsync(GetText(str), inter);
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendPendingAsync(GetText(str), inter);
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendConfirmAsync(GetText(str), inter);
// localized replies
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
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

@@ -1,5 +1,4 @@
#nullable disable
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace NadekoBot;
@@ -45,7 +44,6 @@ public abstract record SmartText
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
[CanBeNull]
public static SmartText CreateFrom(string input)
{
if (string.IsNullOrWhiteSpace(input))

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

View File

@@ -84,7 +84,7 @@ 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; }

View File

@@ -0,0 +1,38 @@
#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; }
}

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

@@ -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; }
@@ -51,19 +50,23 @@ public abstract class NadekoContext : DbContext
public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
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
@@ -77,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);
@@ -193,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>();
@@ -369,12 +369,13 @@ public abstract class NadekoContext : DbContext
.IsUnique(false);
rr2.HasIndex(x => new
{
x.MessageId,
x.Emote
}).IsUnique();
{
x.MessageId,
x.Emote
})
.IsUnique();
});
#endregion
#region LogSettings
@@ -419,7 +420,37 @@ public abstract class NadekoContext : DbContext
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

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,7 +16,7 @@ namespace NadekoBot.Migrations.Mysql
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.4")
.HasAnnotation("ProductVersion", "6.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
@@ -186,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")
@@ -265,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")
@@ -1138,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")
@@ -1962,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")
@@ -1974,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")
@@ -1985,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);
});
@@ -2404,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")
@@ -2430,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")

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,7 +17,7 @@ namespace NadekoBot.Migrations.PostgreSql
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.4")
.HasAnnotation("ProductVersion", "6.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -194,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")
@@ -275,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")
@@ -1194,7 +1262,9 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnName("timezoneid");
b.Property<bool>("VerboseErrors")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("verboseerrors");
b.Property<bool>("VerbosePermissions")
@@ -2058,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")
@@ -2070,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)")
@@ -2081,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);
});
@@ -2526,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")
@@ -2552,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")

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,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,7 @@ 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 =>
{
@@ -149,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");
@@ -209,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")
@@ -890,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");
@@ -1531,7 +1586,7 @@ namespace NadekoBot.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AmountRewardedThisMonth")
b.Property<long>("AmountRewardedThisMonth")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
@@ -1540,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")
@@ -1548,7 +1603,7 @@ namespace NadekoBot.Migrations
b.HasKey("Id");
b.HasIndex("PatreonUserId")
b.HasIndex("PlatformUserId")
.IsUnique();
b.ToTable("RewardedUsers");
@@ -1877,7 +1932,7 @@ namespace NadekoBot.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AwardedXp")
b.Property<long>("AwardedXp")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
@@ -1897,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");

View File

@@ -23,6 +23,8 @@ public class DangerousCommandsService : INService
// IsClubAdmin = false,
TotalXp = 0
});
await ctx.UserXpStats.DeleteAsync();
await ctx.ClubApplicants.DeleteAsync();
await ctx.ClubBans.DeleteAsync();
await ctx.Clubs.DeleteAsync();

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

@@ -1,6 +1,8 @@
#nullable disable
using NadekoBot.Modules.Utility.Patronage;
using NadekoBot.Services.Database.Models;
using System.Collections;
using OneOf;
using OneOf.Types;
namespace NadekoBot.Modules.Administration.Services;
@@ -9,18 +11,16 @@ public interface IReactionRoleService
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guildId"></param>
/// <param name="msg"></param>
/// <param name="channel"></param>
/// <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></returns>
Task<bool> AddReactionRole(
ulong guildId,
/// <returns>The result of the operation</returns>
Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
ITextChannel channel,
string emote,
IRole role,
int group = 0,

View File

@@ -15,7 +15,6 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRoleAdd(
@@ -46,27 +45,26 @@ public partial class Administration
var emote = emoteStr.ToIEmote();
await msg.AddReactionAsync(emote);
var succ = await _rero.AddReactionRole(ctx.Guild.Id,
var res = await _rero.AddReactionRole(ctx.Guild,
msg,
(ITextChannel)ctx.Channel,
emoteStr,
role,
group,
levelReq);
if (succ)
{
await ctx.OkAsync();
}
else
{
await ctx.ErrorAsync();
}
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)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesList()
@@ -109,7 +107,6 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesRemove(ulong messageId)
@@ -123,7 +120,6 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesDeleteAll()
@@ -134,7 +130,6 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Ratelimit(60)]

View File

@@ -2,8 +2,11 @@
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;
@@ -16,20 +19,33 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new();
private readonly SemaphoreSlim _assignementLock = new(1, 1);
private readonly IPatronageService _ps;
public ReactionRolesService(DiscordSocketClient client, DbService db, IBotCredentials creds)
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))
.Where(
x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
foreach (var group in reros.GroupBy(x => x.MessageId))
@@ -126,12 +142,12 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
{
await using var ctx = _db.GetDbContext();
var levelData = await ctx.GetTable<UserXpStats>()
.GetLevelDataFor(user.GuildId, user.Id);
.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)
@@ -141,7 +157,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
.Select(x => x.RoleId)
.Distinct();
try { await user.RemoveRolesAsync(exclusive); }
catch { }
@@ -181,18 +197,16 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guildId"></param>
/// <param name="msg"></param>
/// <param name="channel"></param>
/// <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></returns>
public async Task<bool> AddReactionRole(
ulong guildId,
/// <returns>The result of the operation</returns>
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
ITextChannel channel,
string emote,
IRole role,
int group = 0,
@@ -205,44 +219,46 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
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 == guildId)
.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;
if (activeReactionRoles >= 50)
return false;
await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guild.Id,
ChannelId = msg.Channel.Id,
var changed = await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = channel.Id,
MessageId = msg.Id,
Emote = emote,
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,
});
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
(old) => new()
{
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
() => new()
{
MessageId = msg.Id,
Emote = emote,
});
if (changed == 0)
return false;
await tran.CommitAsync();
var obj = new ReactionRoleV2()
{
GuildId = guildId,
GuildId = guild.Id,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
@@ -265,7 +281,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
});
}
return true;
return new Success();
}
/// <summary>
@@ -326,7 +342,10 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
return output.Length;
}
public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId)
public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(
ulong guildId,
ulong fromMessageId,
ulong toMessageId)
{
await using var ctx = _db.GetDbContext();
var updated = ctx.GetTable<ReactionRoleV2>()

View File

@@ -149,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,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

@@ -65,7 +65,7 @@ public partial class Gambling
}
catch
{
await ReplyErrorLocalizedAsync(strs.unable_to_dm_user);
await ReplyErrorLocalizedAsync(strs.cant_dm);
}
}
}

View File

@@ -1,5 +1,6 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Gambling.Bank;
@@ -74,4 +75,19 @@ public sealed class BankService : IBankService, INService
?.Balance
?? 0;
}
public async Task<long> BurnAllAsync(ulong userId)
{
await using var ctx = _db.GetDbContext();
var output = await ctx.GetTable<BankUser>()
.Where(x => x.UserId == userId)
.UpdateWithOutputAsync(old => new()
{
Balance = 0
});
if (output.Length == 0)
return 0;
return output[0].Deleted.Balance;
}
}

View File

@@ -5,4 +5,5 @@ public interface IBankService
Task<bool> DepositAsync(ulong userId, long amount);
Task<bool> WithdrawAsync(ulong userId, long amount);
Task<long> GetBalanceAsync(ulong userId);
Task<long> BurnAllAsync(ulong userId);
}

View File

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

View File

@@ -3,11 +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;
@@ -42,6 +44,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService;
private readonly IBankService _bank;
private readonly IPatronageService _ps;
private IUserMessage rdMsg;
@@ -52,7 +55,8 @@ public partial class Gambling : GamblingModule<GamblingService>
DiscordSocketClient client,
DownloadTracker tracker,
GamblingConfigService configService,
IBankService bank)
IBankService bank,
IPatronageService ps)
: base(configService)
{
_db = db;
@@ -60,6 +64,7 @@ public partial class Gambling : GamblingModule<GamblingService>
_cache = cache;
_client = client;
_bank = bank;
_ps = ps;
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
@@ -102,6 +107,12 @@ public partial class Gambling : GamblingModule<GamblingService>
await ctx.Channel.EmbedAsync(embed);
}
private static readonly FeatureLimitKey _timelyKey = new FeatureLimitKey()
{
Key = "timely:extra_percent",
PrettyName = "Timely"
};
[Cmd]
public async partial Task Timely()
{
@@ -119,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));
@@ -331,8 +346,8 @@ public partial class Gambling : GamblingModule<GamblingService>
.Pipe(text => smc.RespondConfirmAsync(_eb, text, ephemeral: true));
}
private NadekoInteraction CreateCashInteraction()
=> CashInteraction.CreateInstance(_client, ctx.User.Id, BankAction);
private NadekoButtonInteraction CreateCashInteraction()
=> new CashInteraction(_client, ctx.User.Id, BankAction).GetInteraction();
[Cmd]
[Priority(1)]
@@ -780,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,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 (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;
}
}

View File

@@ -212,7 +212,7 @@ public partial class Help : NadekoModule<HelpService>
cmds = cmds.Where(x => succ.Contains(x)).ToList();
}
var cmdsWithGroup = cmds.GroupBy(c => c.Module.Name.Replace("Commands", "", StringComparison.InvariantCulture))
var cmdsWithGroup = cmds.GroupBy(c => c.Module.GetGroupName())
.OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count())
.ToList();
@@ -294,7 +294,7 @@ public partial class Help : NadekoModule<HelpService>
if (fail.StartsWith(prefix))
fail = fail.Substring(prefix.Length);
var group = _cmds.Modules
.SelectMany(x => x.Submodules)
.Where(x => !string.IsNullOrWhiteSpace(x.Group))
@@ -393,11 +393,6 @@ public partial class Help : NadekoModule<HelpService>
};
using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config);
using var oldVersionObject = await dlClient.GetObjectAsync(new()
{
BucketName = "nadeko-pictures",
Key = "cmds/versions.json"
});
using (var client = new AmazonS3Client(accessKey, secretAcccessKey, config))
{
@@ -407,14 +402,29 @@ public partial class Help : NadekoModule<HelpService>
ContentType = "application/json",
ContentBody = uploadData,
// either use a path provided in the argument or the default one for public nadeko, other/cmds.json
Key = $"cmds/{StatsService.BOT_VERSION}.json",
Key = $"cmds/v4/{StatsService.BOT_VERSION}.json",
CannedACL = S3CannedACL.PublicRead
});
}
await using var ms = new MemoryStream();
await oldVersionObject.ResponseStream.CopyToAsync(ms);
var versionListString = Encoding.UTF8.GetString(ms.ToArray());
var versionListString = "[]";
try
{
using var oldVersionObject = await dlClient.GetObjectAsync(new()
{
BucketName = "nadeko-pictures",
Key = "cmds/v4/versions.json"
});
await using var ms = new MemoryStream();
await oldVersionObject.ResponseStream.CopyToAsync(ms);
versionListString = Encoding.UTF8.GetString(ms.ToArray());
}
catch (Exception)
{
Log.Information("No old version list found. Creating a new one.");
}
var versionList = JsonSerializer.Deserialize<List<string>>(versionListString);
if (versionList is not null && !versionList.Contains(StatsService.BOT_VERSION))
@@ -435,7 +445,7 @@ public partial class Help : NadekoModule<HelpService>
ContentType = "application/json",
ContentBody = versionListString,
// either use a path provided in the argument or the default one for public nadeko, other/cmds.json
Key = "cmds/versions.json",
Key = "cmds/v4/versions.json",
CannedACL = S3CannedACL.PublicRead
});
}
@@ -455,9 +465,71 @@ public partial class Help : NadekoModule<HelpService>
[Cmd]
public async partial Task Guide()
=> await ConfirmLocalizedAsync(strs.guide("https://nadeko.bot/commands",
"http://nadekobot.readthedocs.io/en/latest/"));
"https://nadekobot.readthedocs.io/en/latest/"));
private Task SelfhostAction(SocketMessageComponent smc)
=> smc.RespondConfirmAsync(_eb,
@"- In case you don't want or cannot Donate to NadekoBot project, but you
- NadekoBot is a completely free and fully [open source](https://gitlab.com/kwoth/nadekobot) project which means you can run your own ""selfhosted"" instance on your computer or server for free.
*Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*
- You can find the selfhosting guides by using the `.guide` command and clicking on the second link that pops up.
- If you decide to selfhost the bot, still consider [supporting the project](https://patreon.com/join/nadekobot) to keep the development going :)",
true);
[Cmd]
[OnlyPublicBot]
public async partial Task Donate()
=> await ReplyConfirmLocalizedAsync(strs.donate(PATREON_URL, PAYPAL_URL));
{
var selfhostInter = new DonateSelfhostingInteraction(_client, ctx.User.Id, SelfhostAction);
var eb = _eb.Create(ctx)
.WithOkColor()
.WithTitle("Thank you for considering to donate to the NadekoBot project!");
eb
.WithDescription("NadekoBot relies on donations to keep the servers, services and APIs running.\n"
+ "Donating will give you access to some exclusive features. You can read about them on the [patreon page](https://patreon.com/join/nadekobot)")
.AddField("Donation Instructions",
$@"
🗒️ Before pledging it is recommended to open your DMs as Nadeko will send you a welcome message with instructions after you pledge has been processed and confirmed.
**Step 1:** ❤️ Pledge on Patreon ❤️
`1.` Go to <https://patreon.com/join/nadekobot> and choose a tier.
`2.` Make sure your payment is processed and accepted.
**Step 2** 🤝 Connect your Discord account 🤝
`1.` Go to your profile settings on Patreon and connect your Discord account to it.
*please make sure you're logged into the correct Discord account*
If you do not know how to do it, you may follow instructions in this link:
<https://support.patreon.com/hc/en-us/articles/212052266-How-do-I-connect-Discord-to-Patreon-Patron->
**Step 3** ⏰ Wait a short while (usually 1-3 minutes) ⏰
Nadeko will DM you the welcome instructions, and you may start using the patron-only commands and features!
🎉 **Enjoy!** 🎉
")
.AddField("Troubleshooting",
@"
*In case you didn't receive the rewards within 5 minutes:*
`1.` Make sure your DMs are open to everyone. Maybe your pledge was processed successfully but the bot was unable to DM you. Use the `.patron` command to check your status.
`2.` Make sure you've connected the CORRECT Discord account. Quite often users log in to different Discord accounts in their browser. You may also try disconnecting and reconnecting your account.
`3.` Make sure your payment has been processed and not declined by Patreon.
`4.` If any of the previous steps don't help, you can join the nadeko support server <https://discord.nadeko.bot> and ask for help in the #help channel");
try
{
await (await ctx.User.CreateDMChannelAsync()).EmbedAsync(eb, inter: selfhostInter.GetInteraction());
_ = ctx.OkAsync();
}
catch
{
await ReplyErrorLocalizedAsync(strs.cant_dm);
}
}
}

View File

@@ -131,6 +131,25 @@ public class HelpService : IExecNoCommand, INService
if (cmd.Preconditions.Any(x => x is OwnerOnlyAttribute))
toReturn.Add("Bot Owner Only");
if(cmd.Preconditions.Any(x => x is NoPublicBotAttribute)
|| cmd.Module
.Preconditions
.Any(x => x is NoPublicBotAttribute)
|| cmd.Module.GetTopLevelModule()
.Preconditions
.Any(x => x is NoPublicBotAttribute))
toReturn.Add("No Public Bot");
if (cmd.Preconditions
.Any(x => x is OnlyPublicBotAttribute)
|| cmd.Module
.Preconditions
.Any(x => x is OnlyPublicBotAttribute)
|| cmd.Module.GetTopLevelModule()
.Preconditions
.Any(x => x is OnlyPublicBotAttribute))
toReturn.Add("Only Public Bot");
var userPerm = (UserPermAttribute)cmd.Preconditions.FirstOrDefault(ca => ca is UserPermAttribute);

View File

@@ -0,0 +1,12 @@
namespace NadekoBot.Modules.Help;
public class DonateSelfhostingInteraction : NInteraction
{
protected override NadekoInteractionData Data
=> new NadekoInteractionData(new Emoji("🖥️"), "donate:selfhosting", "Selfhosting");
public DonateSelfhostingInteraction(DiscordSocketClient client, ulong userId, Func<SocketMessageComponent, Task> action)
: base(client, userId, action)
{
}
}

View File

@@ -0,0 +1,12 @@
namespace NadekoBot.Modules.Help;
public class DonateTroubleshootInteraction : NInteraction
{
protected override NadekoInteractionData Data
=> new NadekoInteractionData(new Emoji("❓"), "donate:troubleshoot", "Troubleshoot");
public DonateTroubleshootInteraction(DiscordSocketClient client, ulong userId, Func<SocketMessageComponent, Task> action)
: base(client, userId, action)
{
}
}

View File

@@ -119,7 +119,7 @@ public sealed partial class YtLoader
var responseSpan = response.AsSpan()[140_000..];
var startIndex = responseSpan.IndexOf(_ytResultInitialData);
if (startIndex == -1)
return null; // todo future try selecting html
return null; // FUTURE try selecting html
startIndex += _ytResultInitialData.Length;
var endIndex =

View File

@@ -219,7 +219,7 @@ public sealed class MusicPlayer : IMusicPlayer
errorCount = 0;
}
// todo future windows multimedia api
// FUTURE windows multimedia api
// wait for slightly less than the latency
Thread.Sleep(delay);

View File

@@ -42,8 +42,6 @@ public class RadioResolver : IRadioResolver
}
if (query.Contains(".pls"))
//File1=http://armitunes.com:8000/
//Regex.Match(query)
{
try
{
@@ -59,11 +57,6 @@ public class RadioResolver : IRadioResolver
}
if (query.Contains(".m3u"))
/*
# This is a comment
C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
*/
{
try
{
@@ -79,7 +72,6 @@ public class RadioResolver : IRadioResolver
}
if (query.Contains(".asx"))
//<ref href="http://armitunes.com:8000"/>
{
try
{
@@ -95,12 +87,6 @@ public class RadioResolver : IRadioResolver
}
if (query.Contains(".xspf"))
/*
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<track><location>file:///mp3s/song_1.mp3</location></track>
*/
{
try
{

View File

@@ -264,18 +264,18 @@ public class SearchImageCacher : INService
return new();
}
private IImageDownloader GetImageDownloader(Booru booru, HttpClient http)
private IImageDownloader GetImageDownloader(Booru booru)
=> booru switch
{
Booru.Danbooru => new DanbooruImageDownloader(http),
Booru.Yandere => new YandereImageDownloader(http),
Booru.Konachan => new KonachanImageDownloader(http),
Booru.Safebooru => new SafebooruImageDownloader(http),
Booru.E621 => new E621ImageDownloader(http),
Booru.Derpibooru => new DerpibooruImageDownloader(http),
Booru.Gelbooru => new GelbooruImageDownloader(http),
Booru.Rule34 => new Rule34ImageDownloader(http),
Booru.Sankaku => new SankakuImageDownloader(http),
Booru.Danbooru => new DanbooruImageDownloader(_httpFactory),
Booru.Yandere => new YandereImageDownloader(_httpFactory),
Booru.Konachan => new KonachanImageDownloader(_httpFactory),
Booru.Safebooru => new SafebooruImageDownloader(_httpFactory),
Booru.E621 => new E621ImageDownloader(_httpFactory),
Booru.Derpibooru => new DerpibooruImageDownloader(_httpFactory),
Booru.Gelbooru => new GelbooruImageDownloader(_httpFactory),
Booru.Rule34 => new Rule34ImageDownloader(_httpFactory),
Booru.Sankaku => new SankakuImageDownloader(_httpFactory),
_ => throw new NotImplementedException($"{booru} downloader not implemented.")
};
@@ -290,8 +290,7 @@ public class SearchImageCacher : INService
{
Log.Information("Downloading from {Type} (page {Page})...", type, page);
using var http = _httpFactory.CreateClient();
var downloader = GetImageDownloader(type, http);
var downloader = GetImageDownloader(type);
var images = await downloader.DownloadImageDataAsync(tags, page, isExplicit, cancel);
if (images.Count == 0)

View File

@@ -9,7 +9,7 @@ public sealed class DanbooruImageDownloader : DapiImageDownloader
private static readonly ConcurrentDictionary<string, bool> _existentTags = new();
private static readonly ConcurrentDictionary<string, bool> _nonexistentTags = new();
public DanbooruImageDownloader(HttpClient http)
public DanbooruImageDownloader(IHttpClientFactory http)
: base(Booru.Danbooru, http, "http://danbooru.donmai.us")
{
}
@@ -22,7 +22,8 @@ public sealed class DanbooruImageDownloader : DapiImageDownloader
if (_nonexistentTags.ContainsKey(tag))
return false;
var tags = await _http.GetFromJsonAsync<DapiTag[]>(
using var http = _http.CreateClient();
var tags = await http.GetFromJsonAsync<DapiTag[]>(
_baseUrl + "/tags.json" + $"?search[name_or_alias_matches]={tag}",
_serializerOptions,
cancel);

View File

@@ -7,7 +7,7 @@ public abstract class DapiImageDownloader : ImageDownloader<DapiImageObject>
{
protected readonly string _baseUrl;
public DapiImageDownloader(Booru booru, HttpClient http, string baseUrl)
public DapiImageDownloader(Booru booru, IHttpClientFactory http, string baseUrl)
: base(booru, http)
=> _baseUrl = baseUrl;
@@ -43,7 +43,8 @@ public abstract class DapiImageDownloader : ImageDownloader<DapiImageObject>
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
var uri = $"{_baseUrl}/posts.json?limit=200&tags={tagString}&page={page}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
using var http = _http.CreateClient();
var imageObjects = await http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
if (imageObjects is null)
return new();
return imageObjects.Where(x => x.FileUrl is not null).ToList();

View File

@@ -5,7 +5,7 @@ namespace NadekoBot.Modules.Nsfw.Common;
public class DerpibooruImageDownloader : ImageDownloader<DerpiImageObject>
{
public DerpibooruImageDownloader(HttpClient http)
public DerpibooruImageDownloader(IHttpClientFactory http)
: base(Booru.Derpibooru, http)
{
}
@@ -21,7 +21,8 @@ public class DerpibooruImageDownloader : ImageDownloader<DerpiImageObject>
$"https://www.derpibooru.org/api/v1/json/search/images?q={tagString.Replace('+', ',')}&per_page=49&page={page}";
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
req.Headers.AddFakeHeaders();
using var res = await _http.SendAsync(req, cancel);
using var http = _http.CreateClient();
using var res = await http.SendAsync(req, cancel);
res.EnsureSuccessStatusCode();
var container = await res.Content.ReadFromJsonAsync<DerpiContainer>(_serializerOptions, cancel);

View File

@@ -5,7 +5,7 @@ namespace NadekoBot.Modules.Nsfw.Common;
public class E621ImageDownloader : ImageDownloader<E621Object>
{
public E621ImageDownloader(HttpClient http)
public E621ImageDownloader(IHttpClientFactory http)
: base(Booru.E621, http)
{
}
@@ -20,7 +20,8 @@ public class E621ImageDownloader : ImageDownloader<E621Object>
var uri = $"https://e621.net/posts.json?limit=32&tags={tagString}&page={page}";
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
req.Headers.AddFakeHeaders();
using var res = await _http.SendAsync(req, cancel);
using var http = _http.CreateClient();
using var res = await http.SendAsync(req, cancel);
res.EnsureSuccessStatusCode();
var data = await res.Content.ReadFromJsonAsync<E621Response>(_serializerOptions, cancel);

View File

@@ -6,7 +6,7 @@ namespace NadekoBot.Modules.Nsfw.Common;
public class GelbooruImageDownloader : ImageDownloader<DapiImageObject>
{
public GelbooruImageDownloader(HttpClient http)
public GelbooruImageDownloader(IHttpClientFactory http)
: base(Booru.Gelbooru, http)
{
}
@@ -26,7 +26,8 @@ public class GelbooruImageDownloader : ImageDownloader<DapiImageObject>
+ $"&tags={tagString}"
+ $"&pid={page}";
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
using var res = await _http.SendAsync(req, cancel);
using var http = _http.CreateClient();
using var res = await http.SendAsync(req, cancel);
res.EnsureSuccessStatusCode();
var resString = await res.Content.ReadAsStringAsync(cancel);
if (string.IsNullOrWhiteSpace(resString))

View File

@@ -8,7 +8,7 @@ public abstract class ImageDownloader<T> : IImageDownloader
where T : IImageData
{
public Booru Booru { get; }
protected readonly HttpClient _http;
protected readonly IHttpClientFactory _http;
protected readonly JsonSerializerOptions _serializerOptions = new()
{
@@ -16,7 +16,7 @@ public abstract class ImageDownloader<T> : IImageDownloader
NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString
};
public ImageDownloader(Booru booru, HttpClient http)
public ImageDownloader(Booru booru, IHttpClientFactory http)
{
_http = http;
Booru = booru;

View File

@@ -7,7 +7,7 @@ public sealed class KonachanImageDownloader : ImageDownloader<DapiImageObject>
{
private readonly string _baseUrl;
public KonachanImageDownloader(HttpClient http)
public KonachanImageDownloader(IHttpClientFactory http)
: base(Booru.Konachan, http)
=> _baseUrl = "https://konachan.com";
@@ -19,7 +19,8 @@ public sealed class KonachanImageDownloader : ImageDownloader<DapiImageObject>
{
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
var uri = $"{_baseUrl}/post.json?s=post&q=index&limit=200&tags={tagString}&page={page}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
using var http = _http.CreateClient();
var imageObjects = await http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
if (imageObjects is null)
return new();
return imageObjects.Where(x => x.FileUrl is not null).ToList();

View File

@@ -5,7 +5,7 @@ namespace NadekoBot.Modules.Nsfw.Common;
public class Rule34ImageDownloader : ImageDownloader<Rule34Object>
{
public Rule34ImageDownloader(HttpClient http)
public Rule34ImageDownloader(IHttpClientFactory http)
: base(Booru.Rule34, http)
{
}
@@ -17,9 +17,21 @@ public class Rule34ImageDownloader : ImageDownloader<Rule34Object>
CancellationToken cancel = default)
{
var tagString = ImageDownloaderHelper.GetTagString(tags);
var uri = "https://rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&limit=100"
+ $"&tags={tagString}&pid={page}";
var images = await _http.GetFromJsonAsync<List<Rule34Object>>(uri, _serializerOptions, cancel);
var uri = $"https://api.rule34.xxx//index.php?page=dapi&s=post"
+ $"&q=index"
+ $"&json=1"
+ $"&limit=100"
+ $"&tags={tagString}"
+ $"&pid={page}";
using var http = _http.CreateClient();
http.DefaultRequestHeaders
.TryAddWithoutValidation("cookie", "cf_clearance=Gg3bVffg9fOL_.9fIdKmu5PJS86eTI.yTrhbR8z2tPc-1652310659-0-250");
http.DefaultRequestHeaders
.TryAddWithoutValidation("user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36");
var images = await http.GetFromJsonAsync<List<Rule34Object>>(uri, _serializerOptions, cancel);
if (images is null)
return new();

View File

@@ -5,7 +5,7 @@ namespace NadekoBot.Modules.Nsfw.Common;
public class SafebooruImageDownloader : ImageDownloader<SafebooruElement>
{
public SafebooruImageDownloader(HttpClient http)
public SafebooruImageDownloader(IHttpClientFactory http)
: base(Booru.Safebooru, http)
{
}
@@ -19,7 +19,9 @@ public class SafebooruImageDownloader : ImageDownloader<SafebooruElement>
var tagString = ImageDownloaderHelper.GetTagString(tags);
var uri =
$"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=200&tags={tagString}&json=1&pid={page}";
var images = await _http.GetFromJsonAsync<List<SafebooruElement>>(uri, _serializerOptions, cancel);
using var http = _http.CreateClient();
var images = await http.GetFromJsonAsync<List<SafebooruElement>>(uri, _serializerOptions, cancel);
if (images is null)
return new();

View File

@@ -7,11 +7,10 @@ public sealed class SankakuImageDownloader : ImageDownloader<SankakuImageObject>
{
private readonly string _baseUrl;
public SankakuImageDownloader(HttpClient http)
public SankakuImageDownloader(IHttpClientFactory http)
: base(Booru.Sankaku, http)
{
_baseUrl = "https://capi-v2.sankakucomplex.com";
_http.AddFakeHeaders();
}
public override async Task<List<SankakuImageObject>> DownloadImagesAsync(
@@ -24,9 +23,12 @@ public sealed class SankakuImageDownloader : ImageDownloader<SankakuImageObject>
var tagString = ImageDownloaderHelper.GetTagString(tags);
var uri = $"{_baseUrl}/posts?tags={tagString}&limit=50";
var data = await _http.GetStringAsync(uri);
using var http = _http.CreateClient();
http.AddFakeHeaders();
var data = await http.GetStringAsync(uri, cancel);
return JsonSerializer.Deserialize<SankakuImageObject[]>(data, _serializerOptions)
.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
?.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
.ToList();
}
}

View File

@@ -7,7 +7,7 @@ public sealed class YandereImageDownloader : ImageDownloader<DapiImageObject>
{
private readonly string _baseUrl;
public YandereImageDownloader(HttpClient http)
public YandereImageDownloader(IHttpClientFactory http)
: base(Booru.Yandere, http)
=> _baseUrl = "https://yande.re";
@@ -20,7 +20,9 @@ public sealed class YandereImageDownloader : ImageDownloader<DapiImageObject>
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
var uri = $"{_baseUrl}/post.json?limit=200&tags={tagString}&page={page}";
var imageObjects = await _http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
using var http = _http.CreateClient();
var imageObjects = await http.GetFromJsonAsync<DapiImageObject[]>(uri, _serializerOptions, cancel);
if (imageObjects is null)
return new();
return imageObjects.Where(x => x.FileUrl is not null).ToList();

View File

@@ -1,4 +1,6 @@
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Nsfw.Common;
public class Rule34Object : IImageData
@@ -7,7 +9,9 @@ public class Rule34Object : IImageData
public int Directory { get; init; }
public string Tags { get; init; }
public int Score { get; init; }
[JsonPropertyName("file_url")]
public string FileUrl { get; init; }
public ImageData ToCachedImageData(Booru type)
=> new($"https://img.rule34.xxx//images/{Directory}/{Image}", Booru.Rule34, Tags.Split(' '), Score.ToString());
=> new(FileUrl, Booru.Rule34, Tags.Split(' '), Score.ToString());
}

View File

@@ -0,0 +1,10 @@
#nullable disable
using System.Runtime.InteropServices;
namespace NadekoBot.Modules.Permissions;
[StructLayout(LayoutKind.Sequential, Size = 1)]
public readonly struct CleverBotResponseStr
{
public const string CLEVERBOT_RESPONSE = "cleverbot:response";
}

View File

@@ -0,0 +1,15 @@
#nullable disable
using NadekoBot.Common.TypeReaders;
using static NadekoBot.Common.TypeReaders.TypeReaderResult;
namespace NadekoBot.Modules.Permissions;
public class CleverbotResponseCmdCdTypeReader : NadekoTypeReader<CleverBotResponseStr>
{
public override ValueTask<TypeReaderResult<CleverBotResponseStr>> ReadAsync(
ICommandContext ctx,
string input)
=> input.ToLowerInvariant() == CleverBotResponseStr.CLEVERBOT_RESPONSE
? new(FromSuccess(new CleverBotResponseStr()))
: new(FromError<CleverBotResponseStr>(CommandError.ParseFailed, "Not a valid cleverbot"));
}

View File

@@ -27,9 +27,7 @@ public partial class Permissions
_db = db;
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async partial Task CmdCooldown(CommandOrCrInfo command, int secs)
private async Task CmdCooldownInternal(string cmdName, int secs)
{
var channel = (ITextChannel)ctx.Channel;
if (secs is < 0 or > 3600)
@@ -38,7 +36,7 @@ public partial class Permissions
return;
}
var name = command.Name.ToLowerInvariant();
var name = cmdName.ToLowerInvariant();
await using (var uow = _db.GetDbContext())
{
var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns));
@@ -71,6 +69,18 @@ public partial class Permissions
else
await ReplyConfirmLocalizedAsync(strs.cmdcd_add(Format.Bold(name), Format.Bold(secs.ToString())));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(0)]
public partial Task CmdCooldown(CleverBotResponseStr command, int secs)
=> CmdCooldownInternal(CleverBotResponseStr.CLEVERBOT_RESPONSE, secs);
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public partial Task CmdCooldown(CommandOrCrInfo command, int secs)
=> CmdCooldownInternal(command.Name, secs);
[Cmd]
[RequireContext(ContextType.Guild)]

View File

@@ -157,18 +157,21 @@ public partial class Searches
var marketCap = usd.MarketCap.ToString("C0", localCulture);
var dominance = (usd.MarketCapDominance / 100).ToString("P2", localCulture);
await using var sparkline = await _service.GetSparklineAsync(crypto.Id, usd.PercentChange7d >= 0);
var fileName = $"{crypto.Slug}_7d.png";
var toSend = _eb.Create()
.WithOkColor()
.WithAuthor($"#{crypto.CmcRank}")
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
.WithThumbnailUrl( $"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
.WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
.AddField(GetText(strs.market_cap), marketCap, true)
.AddField(GetText(strs.price), price, true)
.AddField(GetText(strs.volume_24h), volume, true)
.AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true)
.AddField(GetText(strs.market_cap_dominance), dominance, true)
.WithImageUrl($"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{crypto.Id}.png");
.WithImageUrl($"attachment://{fileName}");
if (crypto.CirculatingSupply is double cs)
{
@@ -185,9 +188,9 @@ public partial class Searches
toSend.AddField(GetText(strs.circulating_supply), csStr, true);
}
}
await ctx.Channel.EmbedAsync(toSend);
await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
}
}
}

View File

@@ -1,6 +1,13 @@
#nullable disable
#nullable enable
using NadekoBot.Modules.Searches.Common;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.Globalization;
using System.Net.Http.Json;
using System.Xml;
using Color = SixLabors.ImageSharp.Color;
using JsonSerializer = System.Text.Json.JsonSerializer;
using StringExtensions = NadekoBot.Extensions.StringExtensions;
@@ -21,7 +28,75 @@ public class CryptoService : INService
_creds = creds;
}
public async Task<(CmcResponseData Data, CmcResponseData Nearest)> GetCryptoData(string name)
private PointF[] GetSparklinePointsFromSvgText(string svgText)
{
var xml = new XmlDocument();
xml.LoadXml(svgText);
var gElement = xml["svg"]?["g"];
if (gElement is null)
return Array.Empty<PointF>();
Span<PointF> points = new PointF[gElement.ChildNodes.Count];
var cnt = 0;
bool GetValuesFromAttributes(XmlAttributeCollection attrs,
out float x1,
out float y1,
out float x2,
out float y2)
{
(x1, y1, x2, y2) = (0, 0, 0, 0);
return attrs["x1"]?.Value is string x1Str
&& float.TryParse(x1Str, NumberStyles.Any, CultureInfo.InvariantCulture, out x1)
&& attrs["y1"]?.Value is string y1Str
&& float.TryParse(y1Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y1)
&& attrs["x2"]?.Value is string x2Str
&& float.TryParse(x2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out x2)
&& attrs["y2"]?.Value is string y2Str
&& float.TryParse(y2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y2);
}
foreach (XmlElement x in gElement.ChildNodes)
{
if (x.Name != "line")
continue;
if (GetValuesFromAttributes(x.Attributes, out var x1, out var y1, out var x2, out var y2))
{
points[cnt++] = new(x1, y1);
// this point will be set twice to the same value
// on all points except the last one
if(cnt + 1 < points.Length)
points[cnt + 1] = new(x2, y2);
}
}
if (cnt == 0)
return Array.Empty<PointF>();
return points.Slice(0, cnt).ToArray();
}
private SixLabors.ImageSharp.Image<Rgba32> GenerateSparklineChart(PointF[] points, bool up)
{
const int width = 164;
const int height = 48;
var img = new Image<Rgba32>(width, height, Color.Transparent);
var color = up
? Color.Green
: Color.FromRgb(220, 0, 0);
img.Mutate(x =>
{
x.DrawLines(color, 2, points);
});
return img;
}
public async Task<(CmcResponseData? Data, CmcResponseData? Nearest)> GetCryptoData(string name)
{
if (string.IsNullOrWhiteSpace(name))
return (null, null);
@@ -29,10 +104,10 @@ public class CryptoService : INService
name = name.ToUpperInvariant();
var cryptos = await GetCryptoDataInternal();
if (cryptos is null)
if (cryptos is null or { Count: 0 })
return (null, null);
var crypto = cryptos?.FirstOrDefault(x
var crypto = cryptos.FirstOrDefault(x
=> x.Slug.ToUpperInvariant() == name
|| x.Name.ToUpperInvariant() == name
|| x.Symbol.ToUpperInvariant() == name);
@@ -50,7 +125,7 @@ public class CryptoService : INService
return (null, nearest.Elem);
}
public async Task<List<CmcResponseData>> GetCryptoDataInternal()
public async Task<List<CmcResponseData>?> GetCryptoDataInternal()
{
await _getCryptoLock.WaitAsync();
try
@@ -79,6 +154,9 @@ public class CryptoService : INService
"",
TimeSpan.FromHours(2));
if (fullStrData is null)
return default;
return JsonSerializer.Deserialize<CryptoResponse>(fullStrData)?.Data ?? new();
}
catch (Exception ex)
@@ -91,4 +169,50 @@ public class CryptoService : INService
_getCryptoLock.Release();
}
}
public async Task<Stream?> GetSparklineAsync(int id, bool up)
{
var key = $"crypto:sparkline:{id}";
// attempt to get from cache
var db = _cache.Redis.GetDatabase();
byte[] bytes = await db.StringGetAsync(key);
// if it succeeds, return it
if (bytes is { Length: > 0 })
{
return bytes.ToStream();
}
// if it fails, generate a new one
var points = await DownloadSparklinePointsAsync(id);
if (points is null)
return default;
var sparkline = GenerateSparklineChart(points, up);
// add to cache for 1h and return it
var stream = sparkline.ToStream();
await db.StringSetAsync(key, stream.ToArray(), expiry: TimeSpan.FromHours(1));
return stream;
}
private async Task<PointF[]?> DownloadSparklinePointsAsync(int id)
{
try
{
using var http = _httpFactory.CreateClient();
var str = await http.GetStringAsync(
$"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{id}.svg");
var points = GetSparklinePointsFromSvgText(str);
return points;
}
catch(Exception ex)
{
Log.Warning(ex,
"Exception occurred while downloading sparkline points: {ErrorMessage}",
ex.Message);
return default;
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Searches;

View File

@@ -5,5 +5,5 @@ namespace NadekoBot.Modules.Searches;
public class YahooQueryModel
{
[JsonPropertyName("quoteResponse")]
public QuoteResponse QuoteResponse { get; set; }
public QuoteResponse QuoteResponse { get; set; } = null!;
}

View File

@@ -1,6 +1,8 @@
#nullable disable
using CodeHollow.FeedReader;
using CodeHollow.FeedReader.Feeds;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
@@ -10,11 +12,12 @@ namespace NadekoBot.Modules.Searches.Services;
public class FeedsService : INService
{
private readonly DbService _db;
private readonly ConcurrentDictionary<string, HashSet<FeedSub>> _subs;
private readonly ConcurrentDictionary<string, List<FeedSub>> _subs;
private readonly DiscordSocketClient _client;
private readonly IEmbedBuilderService _eb;
private readonly ConcurrentDictionary<string, DateTime> _lastPosts = new();
private readonly Dictionary<string, uint> _errorCounters = new();
public FeedsService(
Bot bot,
@@ -33,7 +36,7 @@ public class FeedsService : INService
.ToList()
.SelectMany(x => x.FeedSubs)
.GroupBy(x => x.Url.ToLower())
.ToDictionary(x => x.Key, x => x.ToHashSet())
.ToDictionary(x => x.Key, x => x.ToList())
.ToConcurrent();
}
@@ -43,6 +46,35 @@ public class FeedsService : INService
_ = Task.Run(TrackFeeds);
}
private void ClearErrors(string url)
=> _errorCounters.Remove(url);
private async Task AddError(string url, List<int> ids)
{
try
{
var newValue = _errorCounters[url] = _errorCounters.GetValueOrDefault(url) + 1;
if (newValue >= 100)
{
// remove from db
await using var ctx = _db.GetDbContext();
await ctx.GetTable<FeedSub>()
.DeleteAsync(x => ids.Contains(x.Id));
// remove from the local cache
_subs.TryRemove(url, out _);
// reset the error counter
ClearErrors(url);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error adding rss errors...");
}
}
public async Task<EmbedBuilder> TrackFeeds()
{
while (true)
@@ -134,22 +166,31 @@ public class FeedsService : INService
embed.WithDescription(desc.TrimTo(2048));
//send the created embed to all subscribed channels
var feedSendTasks = kvp.Value.Where(x => x.GuildConfig is not null)
var feedSendTasks = kvp.Value
.Where(x => x.GuildConfig is not null)
.Select(x => _client.GetGuild(x.GuildConfig.GuildId)
?.GetTextChannel(x.ChannelId))
.Where(x => x is not null)
.Select(x => x.EmbedAsync(embed));
allSendTasks.Add(feedSendTasks.WhenAll());
// as data retrieval was sucessful, reset error counter
ClearErrors(rssUrl);
}
}
catch (Exception ex)
{
Log.Warning("An error occured while getting rss stream: {Message}", ex.Message);
Log.Warning("An error occured while getting rss stream {RssFeed}"
+ "\n {Message}",
rssUrl,
$"[{ex.GetType().Name}]: {ex.Message}");
await AddError(rssUrl, kvp.Value.Select(x => x.Id).ToList());
}
}
await Task.WhenAll(Task.WhenAll(allSendTasks), Task.Delay(10000));
await Task.WhenAll(Task.WhenAll(allSendTasks), Task.Delay(30000));
}
}
@@ -185,7 +226,7 @@ public class FeedsService : INService
foreach (var feed in gc.FeedSubs)
{
_subs.AddOrUpdate(feed.Url.ToLower(),
new HashSet<FeedSub>
new List<FeedSub>
{
feed
},
@@ -213,7 +254,7 @@ public class FeedsService : INService
return false;
var toRemove = items[index];
_subs.AddOrUpdate(toRemove.Url.ToLower(),
new HashSet<FeedSub>(),
new List<FeedSub>(),
(_, old) =>
{
old.Remove(toRemove);

View File

@@ -0,0 +1,65 @@
using NadekoBot.Modules.Searches.GoogleScrape;
using NadekoBot.Modules.Searches.Youtube;
namespace NadekoBot.Modules.Searches;
public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INService
{
private readonly SearchesConfigService _scs;
private readonly SearxSearchService _sss;
private readonly GoogleSearchService _gss;
private readonly YtdlpYoutubeSearchService _ytdlp;
private readonly YtdlYoutubeSearchService _ytdl;
private readonly YoutubeDataApiSearchService _ytdata;
private readonly InvidiousYtSearchService _iYtSs;
private readonly GoogleScrapeService _gscs;
public DefaultSearchServiceFactory(
SearchesConfigService scs,
GoogleSearchService gss,
GoogleScrapeService gscs,
SearxSearchService sss,
YtdlpYoutubeSearchService ytdlp,
YtdlYoutubeSearchService ytdl,
YoutubeDataApiSearchService ytdata,
InvidiousYtSearchService iYtSs)
{
_scs = scs;
_sss = sss;
_gss = gss;
_gscs = gscs;
_iYtSs = iYtSs;
_ytdlp = ytdlp;
_ytdl = ytdl;
_ytdata = ytdata;
}
public ISearchService GetSearchService(string? hint = null)
=> _scs.Data.WebSearchEngine switch
{
WebSearchEngine.Google => _gss,
WebSearchEngine.Google_Scrape => _gscs,
WebSearchEngine.Searx => _sss,
_ => _gss
};
public ISearchService GetImageSearchService(string? hint = null)
=> _scs.Data.ImgSearchEngine switch
{
ImgSearchEngine.Google => _gss,
ImgSearchEngine.Searx => _sss,
_ => _gss
};
public IYoutubeSearchService GetYoutubeSearchService(string? hint = null)
=> _scs.Data.YtProvider switch
{
YoutubeSearcher.YtDataApiv3 => _ytdata,
YoutubeSearcher.Ytdlp => _ytdlp,
YoutubeSearcher.Ytdl => _ytdl,
YoutubeSearcher.Invidious => _iYtSs,
_ => _ytdl
};
}

View File

@@ -0,0 +1,65 @@
// using AngleSharp.Html.Dom;
// using MorseCode.ITask;
// using NadekoBot.Modules.Searches.Common;
// using System.Net;
//
// namespace NadekoBot.Modules.Searches.DuckDuckGo;
//
// public sealed class DuckDuckGoSeachService : SearchServiceBase
// {
// private static readonly HtmlParser _googleParser = new(new()
// {
// IsScripting = false,
// IsEmbedded = false,
// IsSupportingProcessingInstructions = false,
// IsKeepingSourceReferences = false,
// IsNotSupportingFrames = true
// });
//
// public override async ITask<SearchResultData> SearchAsync(string query)
// {
// query = WebUtility.UrlEncode(query)?.Replace(' ', '+');
//
// var fullQueryLink = "https://html.duckduckgo.com/html";
//
// using var http = _httpFactory.CreateClient();
// http.DefaultRequestHeaders.Clear();
// http.DefaultRequestHeaders.Add("User-Agent",
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
//
// using var formData = new MultipartFormDataContent();
// formData.Add(new StringContent(query), "q");
// using var response = await http.PostAsync(fullQueryLink, formData);
// var content = await response.Content.ReadAsStringAsync();
//
// using var document = await _googleParser.ParseDocumentAsync(content);
// var searchResults = document.QuerySelector(".results");
// var elems = searchResults.QuerySelectorAll(".result");
//
// if (!elems.Any())
// return default;
//
// var results = elems.Select(elem =>
// {
// if (elem.QuerySelector(".result__a") is not IHtmlAnchorElement anchor)
// return null;
//
// var href = anchor.Href;
// var name = anchor.TextContent;
//
// if (string.IsNullOrWhiteSpace(href) || string.IsNullOrWhiteSpace(name))
// return null;
//
// var txt = elem.QuerySelector(".result__snippet")?.TextContent;
//
// if (string.IsNullOrWhiteSpace(txt))
// return null;
//
// return new GoogleSearchResult(name, href, txt);
// })
// .Where(x => x is not null)
// .ToList();
//
// return new(results.AsReadOnly(), fullQueryLink, "0");
// }
// }

View File

@@ -0,0 +1,22 @@
using NadekoBot.Modules.Searches;
using System.Text.Json.Serialization;
namespace NadekoBot.Services;
public sealed class GoogleCustomSearchResult : ISearchResult
{
ISearchResultInformation ISearchResult.Info
=> Info;
public string? Answer
=> null;
IReadOnlyCollection<ISearchResultEntry> ISearchResult.Entries
=> Entries ?? Array.Empty<OfficialGoogleSearchResultEntry>();
[JsonPropertyName("searchInformation")]
public GoogleSearchResultInformation Info { get; init; } = null!;
[JsonPropertyName("items")]
public IReadOnlyCollection<OfficialGoogleSearchResultEntry>? Entries { get; init; }
}

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace NadekoBot.Services;
public sealed class GoogleImageData
{
[JsonPropertyName("contextLink")]
public string ContextLink { get; init; } = null!;
[JsonPropertyName("thumbnailLink")]
public string ThumbnailLink { get; init; } = null!;
}

View File

@@ -0,0 +1,19 @@
using NadekoBot.Modules.Searches;
using System.Text.Json.Serialization;
namespace NadekoBot.Services;
public sealed class GoogleImageResult : IImageSearchResult
{
ISearchResultInformation IImageSearchResult.Info
=> Info;
IReadOnlyCollection<IImageSearchResultEntry> IImageSearchResult.Entries
=> Entries ?? Array.Empty<GoogleImageResultEntry>();
[JsonPropertyName("searchInformation")]
public GoogleSearchResultInformation Info { get; init; } = null!;
[JsonPropertyName("items")]
public IReadOnlyCollection<GoogleImageResultEntry>? Entries { get; init; }
}

View File

@@ -0,0 +1,13 @@
using NadekoBot.Modules.Searches;
using System.Text.Json.Serialization;
namespace NadekoBot.Services;
public sealed class GoogleImageResultEntry : IImageSearchResultEntry
{
[JsonPropertyName("link")]
public string Link { get; init; } = null!;
[JsonPropertyName("image")]
public GoogleImageData Image { get; init; } = null!;
}

View File

@@ -0,0 +1,13 @@
using NadekoBot.Modules.Searches;
using System.Text.Json.Serialization;
namespace NadekoBot.Services;
public sealed class GoogleSearchResultInformation : ISearchResultInformation
{
[JsonPropertyName("formattedTotalResults")]
public string TotalResults { get; init; } = null!;
[JsonPropertyName("formattedSearchTime")]
public string SearchTime { get; init; } = null!;
}

View File

@@ -0,0 +1,66 @@
using MorseCode.ITask;
namespace NadekoBot.Modules.Searches;
public sealed class GoogleSearchService : SearchServiceBase, INService
{
private readonly IBotCredsProvider _creds;
private readonly IHttpClientFactory _httpFactory;
public GoogleSearchService(IBotCredsProvider creds, IHttpClientFactory httpFactory)
{
_creds = creds;
_httpFactory = httpFactory;
}
public override async ITask<GoogleImageResult?> SearchImagesAsync(string query)
{
ArgumentNullException.ThrowIfNull(query);
var creds = _creds.GetCreds();
var key = creds.Google.ImageSearchId;
var cx = string.IsNullOrWhiteSpace(key)
? "c3f56de3be2034c07"
: key;
using var http = _httpFactory.CreateClient("google:search");
http.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
await using var stream = await http.GetStreamAsync(
$"https://customsearch.googleapis.com/customsearch/v1"
+ $"?cx={cx}"
+ $"&q={Uri.EscapeDataString(query)}"
+ $"&fields=items(image(contextLink%2CthumbnailLink)%2Clink)%2CsearchInformation"
+ $"&key={creds.GoogleApiKey}"
+ $"&searchType=image"
+ $"&safe=active");
var result = await System.Text.Json.JsonSerializer.DeserializeAsync<GoogleImageResult>(stream);
return result;
}
public override async ITask<GoogleCustomSearchResult?> SearchAsync(string? query)
{
ArgumentNullException.ThrowIfNull(query);
var creds = _creds.GetCreds();
var key = creds.Google.SearchId;
var cx = string.IsNullOrWhiteSpace(key)
? "c7f1dac95987d4571"
: key;
using var http = _httpFactory.CreateClient("google:search");
http.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
await using var stream = await http.GetStreamAsync(
$"https://customsearch.googleapis.com/customsearch/v1"
+ $"?cx={cx}"
+ $"&q={Uri.EscapeDataString(query)}"
+ $"&fields=items(title%2Clink%2CdisplayLink%2Csnippet)%2CsearchInformation"
+ $"&key={creds.GoogleApiKey}"
+ $"&safe=active");
var result = await System.Text.Json.JsonSerializer.DeserializeAsync<GoogleCustomSearchResult>(stream);
return result;
}
}

View File

@@ -0,0 +1,19 @@
using NadekoBot.Modules.Searches;
using System.Text.Json.Serialization;
namespace NadekoBot.Services;
public sealed class OfficialGoogleSearchResultEntry : ISearchResultEntry
{
[JsonPropertyName("title")]
public string Title { get; init; } = null!;
[JsonPropertyName("link")]
public string Url { get; init; } = null!;
[JsonPropertyName("displayLink")]
public string DisplayUrl { get; init; } = null!;
[JsonPropertyName("snippet")]
public string Description { get; init; } = null!;
}

View File

@@ -0,0 +1,121 @@
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using MorseCode.ITask;
namespace NadekoBot.Modules.Searches.GoogleScrape;
public sealed class GoogleScrapeService : SearchServiceBase, INService
{
private static readonly HtmlParser _googleParser = new(new()
{
IsScripting = false,
IsEmbedded = false,
IsSupportingProcessingInstructions = false,
IsKeepingSourceReferences = false,
IsNotSupportingFrames = true
});
private readonly IHttpClientFactory _httpFactory;
public GoogleScrapeService(IHttpClientFactory httpClientFactory)
=> _httpFactory = httpClientFactory;
public override async ITask<ISearchResult?> SearchAsync(string? query)
{
ArgumentNullException.ThrowIfNull(query);
query = Uri.EscapeDataString(query)?.Replace(' ', '+');
var fullQueryLink = $"https://www.google.ca/search?q={query}&safe=on&lr=lang_eng&hl=en&ie=utf-8&oe=utf-8";
using var msg = new HttpRequestMessage(HttpMethod.Get, fullQueryLink);
msg.Headers.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
msg.Headers.Add("Cookie", "CONSENT=YES+shp.gws-20210601-0-RC2.en+FX+423;");
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
using var response = await http.SendAsync(msg);
await using var content = await response.Content.ReadAsStreamAsync();
using var document = await _googleParser.ParseDocumentAsync(content);
var elems = document.QuerySelectorAll("div.g, div.mnr-c > div > div");
var resultsElem = document.QuerySelector("#result-stats");
var resultsArr = resultsElem?.TextContent.Split("results");
var totalResults = resultsArr?.Length is null or 0
? null
: resultsArr[0];
var time = resultsArr is null or {Length: < 2}
? null
: resultsArr[1]
.Replace("(", string.Empty)
.Replace("seconds)", string.Empty);
//var time = resultsElem.Children.FirstOrDefault()?.TextContent
//^ this doesn't work for some reason, <nobr> is completely missing in parsed collection
if (!elems.Any())
return default;
var results = elems.Select(elem =>
{
var aTag = elem.QuerySelector("a");
if (aTag is null)
return null;
var url = ((IHtmlAnchorElement)aTag).Href;
var title = aTag.QuerySelector("h3")?.TextContent;
var txt = aTag.ParentElement
?.NextElementSibling
?.QuerySelector("span")
?.TextContent
.StripHtml()
?? elem
?.QuerySelectorAll("span")
.Skip(3)
.FirstOrDefault()
?.TextContent
.StripHtml();
// .Select(x => x.TextContent.StripHtml())
// .Join("\n");
if (string.IsNullOrWhiteSpace(url)
|| string.IsNullOrWhiteSpace(title)
|| string.IsNullOrWhiteSpace(txt))
return null;
return new PlainSearchResultEntry
{
Title = title,
Url = url,
DisplayUrl = url,
Description = txt
};
})
.Where(x => x is not null)
.ToList();
// return new GoogleSearchResult(results.AsReadOnly(), fullQueryLink, totalResults);
return new PlainGoogleScrapeSearchResult()
{
Answer = null,
Entries = results!,
Info = new PlainSearchResultInfo()
{
SearchTime = time ?? "?",
TotalResults = totalResults ?? "?"
}
};
}
// someone can mr this
public override ITask<IImageSearchResult?> SearchImagesAsync(string query)
=> throw new NotSupportedException();
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Searches.GoogleScrape;
public class PlainGoogleScrapeSearchResult : ISearchResult
{
public string? Answer { get; init; } = null!;
public IReadOnlyCollection<ISearchResultEntry> Entries { get; init; } = null!;
public ISearchResultInformation Info { get; init; } = null!;
}

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