Compare commits

..

67 Commits
4.0.4 ... v3

Author SHA1 Message Date
Kwoth
2797e56666 Merge branch 'hokutochen-v3-patch-48466' into 'v3'
updated migration guide with better link

See merge request Kwoth/nadekobot!267
2022-08-31 14:37:43 +00:00
Hokuto Chen
11f27ab67a updated migration guide with better link 2022-08-29 01:29:30 +00:00
Kwoth
c5f8bf1891 Merge branch 'hokutochen-v3-patch-79546' into 'v3'
update migration guide using Hoku's script since old link is dead

See merge request Kwoth/nadekobot!263
2022-08-10 22:27:31 +00:00
Hokuto Chen
66b57482bd update migration guide using Hoku's script since old link is dead 2022-08-10 05:03:12 +00:00
Kwoth
dd9d914943 Merge branch 'hokutochen-v3-patch-15731' into 'v3'
Update bot version stat 3.0.12 to 3.0.13

See merge request Kwoth/nadekobot!233
2022-03-02 21:06:53 +00:00
Hokuto Chen
8b0b1af1f3 Update bot version stat 3.0.12 to 3.0.13 2022-03-02 20:30:54 +00:00
Kwoth
bdff19c718 Update responses.uk-UA.json (POEditor.com) 2022-02-17 16:33:33 +00:00
Kwoth
eec4a91743 Update responses.es-ES.json (POEditor.com) 2022-02-17 16:33:32 +00:00
Kwoth
42caefbd1e Update responses.ru-RU.json (POEditor.com) 2022-02-17 16:33:31 +00:00
Kwoth
3edf435fa9 Update responses.pt-BR.json (POEditor.com) 2022-02-17 16:33:29 +00:00
Kwoth
32da43ad46 Update responses.pl-PL.json (POEditor.com) 2022-02-17 16:33:28 +00:00
Kwoth
3f2d1e128e Update responses.it-IT.json (POEditor.com) 2022-02-17 16:33:26 +00:00
Kwoth
352ced1553 Update responses.id-ID.json (POEditor.com) 2022-02-17 16:33:25 +00:00
Kwoth
17a3fb2c52 Update responses.de-DE.json (POEditor.com) 2022-02-17 16:33:24 +00:00
Kwoth
2235729158 Update responses.fr-FR.json (POEditor.com) 2022-02-17 16:33:23 +00:00
Kwoth
3e4d5d07e5 Update responses.nl-NL.json (POEditor.com) 2022-02-17 16:33:22 +00:00
Kwoth
1857d65b77 Merge branch 'warnpatch' into 'v3'
Weighted warnings now properly apply punishments

See merge request Kwoth/nadekobot!198
2022-02-10 12:52:45 +00:00
Alan Beatty
38da171675 Weighted warnings now properly apply punishments 2022-02-10 12:52:45 +00:00
Kwoth
df076be15c Merge branch 'hokutochen-v3-patch-07454' into 'v3'
modified step 6 of linux release instructions.

See merge request Kwoth/nadekobot!231
2022-02-09 08:43:09 +00:00
Hokuto Chen
190f030b93 modified step 6 of linux release instructions. 2022-02-09 07:31:01 +00:00
Kwoth
411b873c84 Increased .crypto refresh from 1h to 2h to prevent monthly ratelimits 2022-01-22 18:22:35 +01:00
Kwoth
50346fc5b4 Fixed patreon reward bug 2022-01-20 13:26:47 +01:00
Kwoth
e32a65bca4 Merge branch 'hokutochen-v3-patch-72839' into 'v3'
same as !227

See merge request Kwoth/nadekobot!228
2022-01-16 19:25:36 +00:00
Kwoth
ffff539545 Merge branch 'hokutochen-v3-patch-44571' into 'v3'
added quotes to windows default restart command example.

See merge request Kwoth/nadekobot!229
2022-01-16 19:25:08 +00:00
Hokuto Chen
4b3b6e37a9 added quotes to windows default restart command example. 2022-01-16 11:24:31 +00:00
Hokuto Chen
620bcae4bb same as !227 2022-01-16 11:23:40 +00:00
Kwoth
ab0fd44b46 Updated changelog 2022-01-14 20:50:19 +01:00
Kwoth
b61f499f91 Merge branch 'hokutochen-v3-patch-35474' into 'v3'
Updated Linux guide to list the following supported versions.

See merge request Kwoth/nadekobot!221
2022-01-11 10:31:43 +00:00
Hokuto Chen
53d365db3a Updated Linux guide to list the following supported versions. 2022-01-11 10:31:43 +00:00
Kwoth
140c4f7fd6 Merge branch 'hokutochen-v3-patch-28697' into 'v3'
added warning to not follow release if following source

See merge request Kwoth/nadekobot!218
2022-01-07 18:12:58 +00:00
Kwoth
5627a3b172 Merge branch 'hokutochen-v3-patch-45634' into 'v3'
added warning to not follow manual release if following source

See merge request Kwoth/nadekobot!219
2022-01-07 18:12:43 +00:00
Kwoth
4795fa98a0 Merge branch 'hokutochen-v3-patch-32876' into 'v3'
added warning to not follow source guide if using windows updater

See merge request Kwoth/nadekobot!217
2022-01-07 18:11:49 +00:00
Hokuto Chen
93453ba522 added warning to not follow manual release if following source 2022-01-07 08:05:15 +00:00
Hokuto Chen
c6a9108474 added warning to not follow release if following source 2022-01-07 06:50:00 +00:00
Kwoth
c3ba805acf Possible fix for patreon auto-creds update 2022-01-07 06:28:58 +01:00
Kwoth
c0ce22a6b7 .greetdm staggering to avoid ratelimits during raids 2022-01-06 22:53:59 +01:00
Kwoth
22183501fe Fixed .gelbooru 2022-01-06 21:07:32 +01:00
Hokuto Chen
2fbdab3235 added warning to not follow source guide if using windows updater 2022-01-06 20:00:48 +00:00
Kwoth
804d3f79fd Updated changelog 2022-01-06 05:26:45 +01:00
Kwoth
fb119cca4c Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2022-01-06 05:16:20 +01:00
Kwoth
31af5ea8c2 Upped version and changelog 2022-01-06 05:15:11 +01:00
Kwoth
e1776d6093 GreetDmMessage will now show a footer with the source server -.- 2022-01-06 05:09:01 +01:00
Kwoth
33dd4bbf0e Merge branch 'make-image-use-safesearch-for-google-images' into 'v3'
Add safesearch to .img when using google

See merge request Kwoth/nadekobot!216
2022-01-05 02:28:58 +00:00
ZeroNyan
af343ac1f0 Add safesearch to .img when using google 2022-01-05 02:28:58 +00:00
Kwoth
065807c180 Merge branch 'hokutochen-v3-patch-71695' into 'v3'
updated creds.yml example owner ID section

See merge request Kwoth/nadekobot!215
2022-01-05 02:28:18 +00:00
Kwoth
9cd24feccc Merge branch 'hokutochen-v3-patch-15423' into 'v3'
Transferred over VPS guide from 1.9

See merge request Kwoth/nadekobot!214
2022-01-05 02:27:47 +00:00
Hokuto Chen
a2d1506915 Transferred over VPS guide from 1.9 2022-01-05 02:27:46 +00:00
Kwoth
54a32a5770 Merge branch 'hokutochen-v3-patch-85263' into 'v3'
Omitted comma explanation for multi owner ID section to avoid confusion

See merge request Kwoth/nadekobot!213
2022-01-05 02:26:53 +00:00
Hokuto Chen
5b9abeb0b2 Omitted comma explanation for multi owner ID section to avoid confusion 2022-01-05 02:26:53 +00:00
Hokuto Chen
accfb2d1ac updated creds.yml example owner ID section 2022-01-04 17:58:47 +00:00
Kwoth
71d383c4db Merge branch 'hokutochen-v3-patch-31256' into 'v3'
fixed GoogleApiKey, formatting error, thanks to alaruba for catching the mistake

See merge request Kwoth/nadekobot!212
2022-01-04 06:03:05 +00:00
Hokuto Chen
197ee9f5ff fixed GoogleApiKey, formatting error, thanks to alaruba for catching the mistake 2022-01-04 00:19:28 +00:00
Kwoth
d51d159962 Merge branch 'hokutochen-v3-patch-51803' into 'v3'
added: Enable "custom search api" for GoogleAPIKey section.

See merge request Kwoth/nadekobot!211
2022-01-03 12:09:06 +00:00
Hokuto Chen
89b0eabd41 added: Enable "custom search api" for GoogleAPIKey section. 2022-01-03 12:09:06 +00:00
Kwoth
8d932d546a Merge branch 'hangman-patch' into 'v3'
small bugfix for hangman

See merge request Kwoth/nadekobot!210
2022-01-03 12:08:41 +00:00
Alan Beatty
9ea3460e3d small bugfix for hangman 2022-01-03 12:08:41 +00:00
Kwoth
7bd4db60a8 Wrong condition in downloadtracker 2022-01-01 16:31:23 +01:00
Kwoth
42e1f35df2 Removed useless #if 2022-01-01 16:28:16 +01:00
Kwoth
179784da3e Possible fix for slowdown with inrole and xplb clean commands 2022-01-01 16:27:30 +01:00
Kwoth
9ed0c870d1 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-12-28 10:59:11 +01:00
Kwoth
77e288ee54 Possible fix for .smch 2021-12-28 10:59:02 +01:00
Kwoth
58adaa9110 Merge branch 'hokutochen-v3-patch-89665' into 'v3'
fixed "from source guide" links and "manual prereq" link

See merge request Kwoth/nadekobot!209
2021-12-27 19:10:05 +00:00
Hokuto Chen
d3a73945e7 fixed "from source guide" links and "manual prereq" link 2021-12-27 19:10:05 +00:00
Kwoth
caca407abd Merge branch 'hokutochen-v3-patch-31383' into 'v3'
fixed error in Source guide (accidentally used quotes)

See merge request Kwoth/nadekobot!208
2021-12-24 22:02:51 +00:00
Hokuto Chen
4fd7b2d8cd fixed error in Source guide (accidentally used quotes) 2021-12-24 21:46:24 +00:00
Kwoth
eaea6e3c54 Merge branch 'hokutochen-v3-patch-44970' into 'v3'
Update step 4 of "linux from source" to be more specific.

See merge request Kwoth/nadekobot!206
2021-12-21 20:18:15 +00:00
Hokuto Chen
0bb68c7723 Update step 4 of "linux from source" to be more specific. 2021-12-21 08:48:22 +00:00
959 changed files with 59954 additions and 70468 deletions

4
.gitignore vendored
View File

@@ -8,7 +8,7 @@ src/NadekoBot/creds.yml
src/NadekoBot/Command Errors*.txt
src/NadekoBot/creds.yml
# credentials file before and after v3
# credentials file before and after migrations
src/NadekoBot/credentials.json
src/NadekoBot/old_credentials.json
src/NadekoBot/credentials.json.bak
@@ -256,7 +256,7 @@ PublishScripts/
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v4's project.json files produces more ignoreable files
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets

View File

@@ -1,4 +1,4 @@
image: mcr.microsoft.com/dotnet/sdk:6.0
image: mcr.microsoft.com/dotnet/sdk:5.0
stages:
- build
@@ -50,7 +50,7 @@ release:
- 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 \
release-cli create --name "NadekoBot v$CI_COMMIT_TAG" --description "## [Changelog](https://gitlab.com/Kwoth/nadekobot/-/blob/v3/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}\"}"
@@ -68,8 +68,8 @@ publish-windows:
- 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 dotnet-5.0-runtime -y
- choco install dotnet-5.0-sdk -y
- choco install innosetup -y
artifacts:
paths:
@@ -97,29 +97,29 @@ upload-windows-updater-release:
- 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"
# 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
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

View File

@@ -1,102 +1,15 @@
# Changelog
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
#todo .trans fix
## Unreleased
- More cool stuff coming soon
## [4.0.4] - 04.03.2022
### Fixed
- Fixed the `id` which shows up when you add a new Expression
- Fixed some strings which were still referring to "CustomReaction(s)" instead of "Expression(s)"
## [4.0.3] - 04.03.2022
### Fixed
- Console should no longer spam numbers when `.antispam` is enabled
## [4.0.2] - 03.03.2022
### Fixed
- Fixed `.rero` not working due to a bug introduced in 4.0
## [4.0.1] - 03.03.2022
### Added
- Added `usePrivilegedIntents` to creds.yml if you don't have or don't want (?) to use them
- Added a human-readable, detailed error message if logging in fails due to missing privileged intents
## [4.0.0] - 02.03.2022
### Added
- Added `.deleteemptyservers` command
- Added `.curtr <id>` which lets you see full information about one of your own transactions with the specified id
- Added trovo.live support for stream notifications (`.stadd`)
- Added unclaimed waifu decay functionality
- Added 3 new settings to `data/gambling.yml` to control it:
- waifu.decay.percent - How much % to subtract from unclaimed waifu
- waifu.decay.hourInterval - How often to decay the price
- waifu.decay.minPrice - Unclaimed waifus with price lower than the one specified here will not be affected by the decay
- Added `currency.transactionsLifetime` to `data/gambling.yml` Any transaction older than the number of days specified will be automatically deleted
- Added `.stock` command to check stock prices and charts
- Re-added `.qap / .queueautoplay`
### Changed
- CustomReactions module (and customreactions db table) has been renamed to Expressions.
- This was done to remove confusion about how it relates to discord Reactions (it doesn't, it was created and named before discord reactions existed)
- Expression command now start with ex/expr and end with the name of the action or setting.
- For example `.exd` (`.dcr`) is expression delete, `.exa` (`.acr`)
- Permissions (`.lp`) be automatically updated with "ACTUALEXPRESSIONS", "EXPRESSIONS" instead of "ACTUALCUSTOMREACTIONS" and "CUSTOMREACTIONS"
- Permissions for `.ecr` (now `.exe`), `.scr` (now `.exs`), `.dcr` (now `.exd`), `.acr` (now `.exa`), `.lcr` (now `.exl`) will be automatically updated
- If you have custom permissions for other CustomReaction commands
- Some of the old aliases like `.acr` `.dcr` `.lcr` and a few others have been kept
- Currency output format improvement (will use guild locale now for some commands)
- `.crypto` will now also show CoinMarketCap rank
- Waifus can now be claimed for much higher prices (int -> long)
- Several strings and commands related to music have been changed
- Changed `.ms / .movesong` to `.tm / .trackmove` but kept old aliases
- Changed ~~song~~ -> `track` throughout music module strings
- Improved .curtrs (It will now have a lot more useful data in the database, show Tx ids, and be partially localized)
- [dev] Reason renamed to Note
- [dev] Added Type, Extra, OtherId fields to the database
- [dev] CommandStrings will now use methodname as the key, and **not** the command name (first entry in aliases.yml)
- In other words aliases.yml and commands.en-US.yml will use the same keys (once again)
- [dev] Reorganized module and submodule folders
- [dev] Permissionv2 db table renamed to Permissions
- [dev] Moved FilterWordsChannelId to a separate table
### Fixed
- Fixed twitch stream notifications (rewrote it to use the new api)
- Fixed an extra whitespace in usage part of command help if the command has no arguments
- Possible small fix for `.prune` ratelimiting
- `.gvc` should now properly trigger when a user is already in a gvc and changes his activity
- `.gvc` should now properly detect multiple activities
- Fixed reference to non-existent command in bot.yml
- Comment indentation in .yml files should now make more sense
- Fixed `.warn` punishments not being applied properly when using weighted warnings
- Fixed embed color when disabling `.antialt`
### Removed
- Removed `.bce` - use `.config` or `.config bot` specifically for bot config
- Removed obsolete placeholders: %users% %servers% %userfull% %username% %userdiscrim% %useravatar% %id% %uid% %chname% %cid% %sid% %members% %server_time% %shardid% %time% %mention%
- Removed some obsolete commands and strings
- Removed code which migrated 2.x to v3 credentials, settings, etc...
## [3.0.13] - 14.01.2022
## [3.0.13] - 14.04.2021
### Fixed
- Fixed `.greetdm` causing ratelimits during raids
- Fixed `.gelbooru`
## [3.0.12] - 06.01.2022
## [3.0.12] - 06.01.2021
### Fixed
- `.smch` Fixed

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
WORKDIR /source
COPY src/NadekoBot/*.csproj src/NadekoBot/
@@ -18,7 +18,7 @@ RUN set -xe; \
chmod +x /app/NadekoBot
# final stage/image
FROM mcr.microsoft.com/dotnet/runtime:6.0
FROM mcr.microsoft.com/dotnet/runtime:5.0-buster-slim
WORKDIR /app
RUN set -xe; \

8
NuGet.Config Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="Discord.Net" value="https://www.myget.org/F/discord-net/api/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="Kwoth-myget" value="https://www.myget.org/F/kwoth/api/v3/index.json" />
</packageSources>
</configuration>

View File

@@ -1,5 +1,5 @@
[![Discord](https://discordapp.com/api/guilds/117523346618318850/widget.png)](https://discord.gg/nadekobot)
[![Documentation Status](https://readthedocs.org/projects/nadekobot/badge/?version=latest)](http://nadekobot.readthedocs.io/en/v4/?badge=v4)
[![Documentation Status](https://readthedocs.org/projects/nadekobot/badge/?version=latest)](http://nadekobot.readthedocs.io/en/v3/?badge=v3)
[![Discord Bots](https://discordbots.org/api/widget/status/116275390695079945.svg)](https://top.gg/bot/116275390695079945)
@@ -10,5 +10,6 @@
[![nadeko2](https://cdn.nadeko.bot/tutorial/docs-bot.png)](https://nadeko.bot/commands)
### Useful links
- [Self hosting Guides and Docs](https://nadekobot.readthedocs.io/en/v4)
- ❗ [2.x to v3 migration guide](https://nadekobot.readthedocs.io/en/v3/guides/migration-guide/)
- [Self hosting Guides and Docs](https://nadekobot.readthedocs.io/en/v3)
- [Discord support server](https://discord.nadeko.bot)

View File

@@ -1,6 +1,6 @@
# How to contribute
1. Make Merge Requests to the [**v4 branch**](https://gitlab.com/Kwoth/nadekobot/tree/v4)
1. Make Merge Requests to the [**v3 branch**](https://gitlab.com/Kwoth/nadekobot/tree/v3)
2. Keep a single Merge Request to a single feature
3. Fill out the MR template

View File

@@ -13,11 +13,11 @@ This document aims to guide you through the process of creating a Discord accoun
- Click on the `Add a Bot` button and confirm that you do want to add a bot to this app.
- **Optional:** Add bot's avatar and description.
- Copy your Token to `creds.yml` as shown above.
- Scroll down to the **`Privileged Gateway Intents`** section
- **Enable the following:**
- **PRESENCE INTENT**
- **SERVER MEMBERS INTENT**
- **MESSAGE CONTENT INTENT**
- Scroll down to the `Privileged Gateway Intents` section
- Enabled the following:
- PRESENCE INTENT
- SERVER MEMBERS INTENT
- MESSAGE CONTENT INTENT
These are required for a number of features to function properly, and all should be on.

View File

@@ -2,6 +2,8 @@
# DO NOT USE YET - WORK IN PROGRESS
Upgrade from 2.x to v3 does not work because the file is mount readonly
### Docker Compose
```yml
version: "3.7"

View File

@@ -1,15 +1,8 @@
# Setting up NadekoBot on Linux
## Migration from 2.x
| Table of Contents |
| :-------------------------------------------------- |
| [Linux From Source] |
| [Source Update Instructions] |
| [Linux Release] |
| [Release Update Instructions] |
| [Tmux (Preferred Method)] |
| [Systemd] |
| [Systemd + Script] |
| [Setting up Nadeko on a VPS (Digital Ocean)] |
##### ⚠ If you're already hosting NadekoBot, _You **MUST** update to latest version of 2.x and **run your bot at least once**_ before switching over to v3.
#### [Linux migration instructions](../migration-guide/#linux)
#### Operating System Compatibility
@@ -26,24 +19,14 @@ It is recommended that you use **Ubuntu 20.04**, as there have been nearly no pr
## Linux From Source
##### Migration from v3 -> v4
Follow the following few steps only if you're migrating from v3. If not, skip to installation instructions.
Use the new installer script: `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/v4/linuxAIO.sh && bash linuxAIO.sh`
> - Install prerequisites (type `1` and press `enter`)
> - Download (type `2` and press `enter`)
> - Run (type `3` and press `enter`)
> - Done
Open Terminal (if you're on an installation with a window manager) and navigate to the location where you want to install the bot (for example `cd ~`)
##### Installation Instructions
Open Terminal (if you're on an installation with a window manager) and navigate to the location where you want to install the bot (for example `cd ~`)
1. Download and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/v4/linuxAIO.sh && bash linuxAIO.sh`
1. Download and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/master/linuxAIO.sh && bash linuxAIO.sh`
2. Install prerequisites (type `1` and press enter)
3. Download the bot (type `2` and press enter)
4. Exit the installer (type `6` and press enter)
4. Exit the installer (type `5` and press enter)
5. Copy the creds.yml template `cp nadekobot/output/creds_example.yml nadekobot/output/creds.yml`
6. Open `nadekobot/output/creds.yml` with your favorite text editor. We will use nano here
- `nano nadekobot/output/creds.yml`
@@ -52,20 +35,19 @@ Open Terminal (if you're on an installation with a window manager) and navigate
- `CTRL` + `X`
- `Y`
- `Enter`
8. Run the installer script again `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/v4/linuxAIO.sh && bash linuxAIO.sh`
9. Run the bot (type `3` and press enter)
8. Run the bot (type `3` and press enter)
##### Source Update Instructions
##### Update Instructions
1. ⚠ Stop the bot
2. Update and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/v4/linuxAIO.sh && bash linuxAIO.sh`
1. ⚠ Stop the bot
2. Update and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/master/linuxAIO.sh && bash linuxAIO.sh`
3. Update the bot (type `2` and press enter)
4. Run the bot (type `3` and press enter)
5. 🎉
## Linux Release
**⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠
##### Installation Instructions
@@ -81,7 +63,7 @@ Open Terminal (if you're on an installation with a window manager) and navigate
5. Copy the creds.yml template
- `cp creds_example.yml creds.yml`
6. Open `creds.yml` with your favorite text editor. We will use nano here
- `nano nadekobot/output/creds.yml`
- `nano creds.yml`
8. [Click here to follow creds guide](../../creds-guide)
- After you're done, you can close nano (and save the file) by inputting, in order
- `CTRL` + `X`
@@ -90,7 +72,7 @@ Open Terminal (if you're on an installation with a window manager) and navigate
9. Run the bot
- `./NadekoBot`
##### Release Update Instructions
##### Update Instructions
1. Stop the bot
2. Download the latest release from <https://gitlab.com/Kwoth/nadekobot/-/releases>
@@ -137,7 +119,7 @@ cd nadekobot && chmod +x NadekoBot
While there are two run modes built into the installer, these options only run Nadeko within the current session. Below are 3 methods of running Nadeko as a background process.
### Tmux Method (Preferred)
### Tmux (Preferred Method)
Using `tmux` is the simplest method, and is therefore recommended for most users.
@@ -308,12 +290,3 @@ If you are running your droplet for the first time, it will most likely ask you
**Save the new password somewhere safe.**
After that, your droplet should be ready for use. [Follow the guide from the beginning](#linux-from-source) to set Nadeko up on your newly created VPS.
[Linux From Source]: #linux-from-source
[Source Update Instructions]: #source-update-instructions
[Linux Release]: #linux-release
[Release Update Instructions]: #release-update-instructions
[Tmux (Preferred Method)]: #tmux-preferred-method
[Systemd]: #systemd
[Systemd + Script]: #systemd-script
[Setting up Nadeko on a VPS (Digital Ocean)]: #setting-up-nadeko-on-a-linux-vps-digital-ocean-droplet

View File

@@ -0,0 +1,66 @@
# Migration instructions (2.x to v3)
## Windows
1. Run your NadekoBot Updater first, and **make sure your bot is updated to at least 2.46.5**
- **Run your 2.46.5 Bot** and make sure it works, and then **stop it**
- Close your old NadekoBot Updater
2. Get the new NadekoBot v3 Updater [here](https://dl.nadeko.bot/v3)
3. Click on the + icon to add a new bot
4. Next to the path, click on the folder icon and select the folder where your 2.46.5 bot is
- In case you're not sure where it's located, you can open your old updater and see it
5. If you've selected the correct path, you should have an **Update** button available, click it
6. You're done; you can now run your bot, and you can uninstall your old updater if you no longer have 2.x bots
7. 🎉
## Linux
1. In order to migrate a bot hosted on **Linux**, first **BACKUP** then update your current version to the latest **2.x** version using the **2.x** installer, run the bot, and make sure it works. Then:
- Run the **old** installer with `cd ~ && wget -N https://gitlab.com/hokutochen/becausekwoth/-/raw/main/linuxAIO.sh && bash linuxAIO.sh`
- Run option **2**
- You **MUST** Run the bot now to ensure database is ready for migration
- Type `.stats` and ensure the version is `2.46.5` or later
- Stop the bot
2. Make sure your bot's folder is called `NadekoBot`
- Run `cd ~ && ls`
- Confirm there is a folder called NadekoBot (not nadekobot, in all lowercase)
3. Migrate your bot's data using the new installer:
- Run the **new** installer `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/master/linuxAIO.sh && bash linuxAIO.sh`
- The installer should notify you that your data is ready for migration in a message above the menu.
- Install prerequisites (type `1` and press enter), and make sure it is successful
- Download NadekoBot v3 (type `2` and press enter)
- Run the bot (type `3` and press enter)
4. Make sure your permissions, custom reactions, credentials, and other data is preserved
- `.stats` to ensure owner id (credentials) is correct
- `.lcr` to see custom reactions
- `.lp` to list permissions
5. 🎉 Enjoy. If you want to learn how to update the bot, click [here](../linux-guide/#update-instructions)
## Manual
⚠ NOT RECOMMENDED
⚠ NadekoBot v3 requires [.net 5](https://dotnet.microsoft.com/download/dotnet/5.0)
1. In order to migrate a bot hosted **on Linux or from source on Windows**
- First update your current version to the latest 2.x version using the 2.x installer
- Then you **must** run the bot to prepare the database for the migration, and make sure the bot works prior to upgrade.
Then:
2. Rename your old nadeko bot folder to `nadekobot_2x`
- `mv NadekoBot nadekobot_2x`
3. Build the new version and move old data to the output folder
1. Clone the v3 branch to a separate folder
- `git clone https://gitlab.com/kwoth/nadekobot -b v3 --depth 1`
2. Build the bot
- `dotnet publish -c Release -o output/ src/NadekoBot/`
3. Copy old data
- ⚠ Be sure you copy the correct command for your system!
- **Windows:** `cp -r -fo nadekobot_2x/src/NadekoBot/data nadekobot/src/NadekoBot/data`
- **Linux:** `cp -rf nadekobot_2x/src/NadekoBot/data nadekobot/src/NadekoBot/data`
4. Copy the database
- `cp nadekobot_2x/src/NadekoBot/bin/Release/netcoreapp2.1/data/NadekoBot.db nadekobot/output/data`
5. Copy your credentials
- `cp nadekobot_2x/src/NadekoBot/credentials.json nadekobot/output/`
4. Run the bot
- `cd nadekobot/output`
- `dotnet NadekoBot.dll`
5. That's it. Just make sure that when you're updating the bot, you're properly backing up your old data.

View File

@@ -12,7 +12,7 @@ Open Terminal (if you don't know how to, click on the magnifying glass on the to
- `brew install wget`
###### Dotnet
- Download [.net6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)
- Download [.net5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)
- Open the `.pkg` file you've downloaded and install it.
- Run this command in Terminal. There might be output. If there is, disregard it. (copy-paste the entire block)
```bash
@@ -31,7 +31,7 @@ sudo ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/
##### Installation Instructions
1. Download and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/v4/linuxAIO.sh && bash linuxAIO.sh`
1. Download and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/master/linuxAIO.sh && bash linuxAIO.sh`
2. Install prerequisites (type `1` and press enter)
3. Download the bot (type `2` and press enter)
4. Exit the installer in order to set up your `creds.yml`
@@ -49,7 +49,7 @@ sudo ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/
##### Update Instructions
1. ⚠ Stop the bot
2. Update and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/v4/linuxAIO.sh && bash linuxAIO.sh`
2. Update and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/master/linuxAIO.sh && bash linuxAIO.sh`
3. Update the bot (type `2` and press enter)
4. Run the bot (type `3` and press enter)
5. 🎉

View File

@@ -1,3 +1,9 @@
## Migration from 2.x
⚠ If you're already hosting NadekoBot, You **MUST** update to latest version of 2.x and **run your bot at least once** before switching over to v3.
#### [Windows migration instructions](../migration-guide#windows)
## Setting Up NadekoBot on Windows With the Updater
| Table of Contents|
@@ -42,8 +48,6 @@
- Either click on **`RUN`** button in the updater or run the bot via its desktop shortcut.
### If you get a "No owner channels created..." message. Please follow the creds guide again [**HERE**](../../creds-guide).
#### Updating Nadeko
- Make sure Nadeko is closed and not running
@@ -70,7 +74,7 @@ You can still install them manually:
##### Prerequisites
**Install these before proceeding or your bot will not work!**
- [.net 6](https://dotnet.microsoft.com/download/dotnet/6.0) - needed to compile and run the bot
- [.net 5](https://dotnet.microsoft.com/download/dotnet/5.0) - needed to compile and run the bot
- [git](https://git-scm.com/downloads) - needed to clone the repository (you can also download the zip manually and extract it, but this guide assumes you're using git)
- [redis](https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.msi) - to cache things needed by some features and persist through restarts
@@ -78,7 +82,7 @@ You can still install them manually:
Open PowerShell (press windows button on your keyboard and type powershell, it should show up; alternatively, right click the start menu and select Windows PowerShell), and navigate to the location where you want to install the bot (for example `cd ~/Desktop/`)
1. `git clone https://gitlab.com/kwoth/nadekobot -b v4 --depth 1`
1. `git clone https://gitlab.com/kwoth/nadekobot -b v3 --depth 1`
2. `cd nadekobot`
3. `dotnet publish -c Release -o output/ src/NadekoBot/`
4. `cd output`
@@ -122,7 +126,7 @@ In order to use music commands, you need ffmpeg and youtube-dl installed.
- [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `ffmpeg.exe` file to `NadekoBot/output`.
- [youtube-dl] - Click to download the file, then move `youtube-dl.exe` to a path that's in your PATH environment variable. If you don't know what that is, just move the `youtube-dl.exe` file to `NadekoBot/system`.
[Updater]: https://dl.nadeko.bot/
[Updater]: https://dl.nadeko.bot/v3
[Notepad++]: https://notepad-plus-plus.org/
[.net]: https://dotnet.microsoft.com/download/dotnet/5.0
[Redis]: https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.msi

View File

@@ -1,7 +1,7 @@
#define sysfolder "system"
#define version GetEnv("NADEKOBOT_INSTALL_VERSION")
#define target "win7-x64"
#define platform "net6.0"
#define platform "net5.0"
[Setup]
AppName = {param:botname|NadekoBot}

View File

@@ -72,6 +72,7 @@ markdown_extensions:
nav:
- Home: index.md
- Guides:
- ❗ Migration Guide: guides/migration-guide.md
- Windows Guide: guides/windows-guide.md
- Linux Guide: guides/linux-guide.md
- OSX Guide: guides/osx-guide.md

View File

@@ -12,7 +12,9 @@ namespace NadekoBot.Coordinator
public IConfiguration Configuration { get; }
public CoordStartup(IConfiguration config)
=> Configuration = config;
{
Configuration = config;
}
public void ConfigureServices(IServiceCollection services)
{

View File

@@ -21,7 +21,7 @@ namespace NadekoBot.Services
.Enrich.WithProperty("LogSource", source)
.CreateLogger();
Console.OutputEncoding = Encoding.UTF8;
System.Console.OutputEncoding = Encoding.UTF8;
}
private static ConsoleTheme GetTheme()

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@@ -9,7 +9,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.42.0" />
<PackageReference Include="Grpc.AspNetCore" Version="2.41.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;
using YamlDotNet.Serialization;
@@ -29,7 +30,7 @@ namespace NadekoBot.Coordinator
private readonly Random _rng;
private bool _gracefulImminent;
public CoordinatorRunner()
public CoordinatorRunner(IConfiguration configuration)
{
_serializer = new();
_deserializer = new();
@@ -90,7 +91,7 @@ namespace NadekoBot.Coordinator
var shardIds = Enumerable.Range(0, 1) // shard 0 is always first
.Append((int)((117523346618318850 >> 22) % _config.TotalShards)) // then nadeko server shard
.Concat(Enumerable.Range(1, _config.TotalShards - 1)
.OrderBy(_ => _rng.Next())) // then all other shards in a random order
.OrderBy(x => _rng.Next())) // then all other shards in a random order
.Distinct()
.ToList();
@@ -190,7 +191,8 @@ namespace NadekoBot.Coordinator
}
private Process StartShardProcess(int shardId)
=> Process.Start(new ProcessStartInfo()
{
return Process.Start(new ProcessStartInfo()
{
FileName = _config.ShardStartCommand,
Arguments = string.Format(_config.ShardStartArgs,
@@ -203,6 +205,7 @@ namespace NadekoBot.Coordinator
// CreateNoWindow = true,
// UseShellExecute = false,
});
}
public bool Heartbeat(int shardId, int guildCount, ConnState state)
{
@@ -236,6 +239,7 @@ namespace NadekoBot.Coordinator
{
lock (locker)
{
ref var toSave = ref _config;
SaveConfig(new Config(
totalShards,
_config.RecheckIntervalMs,
@@ -280,7 +284,7 @@ namespace NadekoBot.Coordinator
for (var shardId = 0; shardId < _shardStatuses.Length; shardId++)
{
var status = _shardStatuses[shardId];
if (status.Process is { } p)
if (status.Process is Process p)
{
p.Kill();
p.Dispose();
@@ -310,7 +314,7 @@ namespace NadekoBot.Coordinator
})
.ToList()
};
var jsonState = JsonSerializer.Serialize(coordState, new JsonSerializerOptions()
var jsonState = JsonSerializer.Serialize(coordState, new ()
{
WriteIndented = true,
});
@@ -342,7 +346,7 @@ namespace NadekoBot.Coordinator
if (savedState.StatusObjects.Count != _config.TotalShards)
{
Log.Error("Unable to restore old state because shard count doesn't match");
Log.Error("Unable to restore old state because shard count doesn't match.");
File.Move(GRACEFUL_STATE_PATH, GRACEFUL_STATE_BACKUP_PATH, overwrite: true);
return false;
}
@@ -353,7 +357,7 @@ namespace NadekoBot.Coordinator
{
var statusObj = savedState.StatusObjects[shardId];
Process p = null;
if (statusObj.Pid is { } pid)
if (statusObj.Pid is int pid)
{
try
{
@@ -361,7 +365,7 @@ namespace NadekoBot.Coordinator
}
catch (Exception ex)
{
Log.Warning(ex, "Process for shard {ShardId} is not runnning", shardId);
Log.Warning(ex, $"Process for shard {shardId} is not runnning.");
}
}
@@ -437,7 +441,9 @@ namespace NadekoBot.Coordinator
}
public string GetConfigText()
=> File.ReadAllText(CONFIG_PATH);
{
return File.ReadAllText(CONFIG_PATH);
}
public void SetConfigText(string text)
{

View File

@@ -5,12 +5,14 @@ using Grpc.Core;
namespace NadekoBot.Coordinator
{
public sealed class CoordinatorService : Coordinator.CoordinatorBase
public sealed class CoordinatorService : NadekoBot.Coordinator.Coordinator.CoordinatorBase
{
private readonly CoordinatorRunner _runner;
public CoordinatorService(CoordinatorRunner runner)
=> _runner = runner;
{
_runner = runner;
}
public override Task<HeartbeatReply> Heartbeat(HeartbeatRequest request, ServerCallContext context)
{
@@ -111,10 +113,11 @@ namespace NadekoBot.Coordinator
return new DieReply();
}
public override Task<SetConfigTextReply> SetConfigText(SetConfigTextRequest request, ServerCallContext context)
public override async Task<SetConfigTextReply> SetConfigText(SetConfigTextRequest request, ServerCallContext context)
{
var error = string.Empty;
var success = true;
await Task.Yield();
string error = string.Empty;
bool success = true;
try
{
_runner.SetConfigText(request.ConfigYml);
@@ -125,11 +128,11 @@ namespace NadekoBot.Coordinator
success = false;
}
return Task.FromResult<SetConfigTextReply>(new(new()
return new(new()
{
Success = success,
Error = error
}));
});
}
public override Task<GetConfigTextReply> GetConfigText(GetConfigTextRequest request, ServerCallContext context)

View File

@@ -1,4 +1,6 @@
namespace NadekoBot.Coordinator
using System;
namespace NadekoBot.Coordinator
{
public class JsonStatusObject
{

View File

@@ -1,254 +0,0 @@
// Code temporarily yeeted from
// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs
// because of NRT issue
#nullable enable
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Cloneable
{
[Generator]
public class CloneableGenerator : ISourceGenerator
{
private const string PREVENT_DEEP_COPY_KEY_STRING = "PreventDeepCopy";
private const string EXPLICIT_DECLARATION_KEY_STRING = "ExplicitDeclaration";
private const string CLONEABLE_NAMESPACE = "Cloneable";
private const string CLONEABLE_ATTRIBUTE_STRING = "CloneableAttribute";
private const string CLONE_ATTRIBUTE_STRING = "CloneAttribute";
private const string IGNORE_CLONE_ATTRIBUTE_STRING = "IgnoreCloneAttribute";
private const string CLONEABLE_ATTRIBUTE_TEXT = @"// <AutoGenerated/>
using System;
namespace " + CLONEABLE_NAMESPACE + @"
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true, AllowMultiple = false)]
public sealed class " + CLONEABLE_ATTRIBUTE_STRING + @" : Attribute
{
public " + CLONEABLE_ATTRIBUTE_STRING + @"()
{
}
public bool " + EXPLICIT_DECLARATION_KEY_STRING + @" { get; set; }
}
}
";
private const string CLONE_PROPERTY_ATTRIBUTE_TEXT = @"// <AutoGenerated/>
using System;
namespace " + CLONEABLE_NAMESPACE + @"
{
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class " + CLONE_ATTRIBUTE_STRING + @" : Attribute
{
public " + CLONE_ATTRIBUTE_STRING + @"()
{
}
public bool " + PREVENT_DEEP_COPY_KEY_STRING + @" { get; set; }
}
}
";
private const string IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT = @"// <AutoGenerated/>
using System;
namespace " + CLONEABLE_NAMESPACE + @"
{
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class " + IGNORE_CLONE_ATTRIBUTE_STRING + @" : Attribute
{
public " + IGNORE_CLONE_ATTRIBUTE_STRING + @"()
{
}
}
}
";
private INamedTypeSymbol? _cloneableAttribute;
private INamedTypeSymbol? _ignoreCloneAttribute;
private INamedTypeSymbol? _cloneAttribute;
public void Initialize(GeneratorInitializationContext context)
=> context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
public void Execute(GeneratorExecutionContext context)
{
InjectCloneableAttributes(context);
GenerateCloneMethods(context);
}
private void GenerateCloneMethods(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
return;
Compilation compilation = GetCompilation(context);
InitAttributes(compilation);
var classSymbols = GetClassSymbols(compilation, receiver);
foreach (var classSymbol in classSymbols)
{
if (!classSymbol.TryGetAttribute(_cloneableAttribute!, out var attributes))
continue;
var attribute = attributes.Single();
var isExplicit = (bool?)attribute.NamedArguments.FirstOrDefault(e => e.Key.Equals(EXPLICIT_DECLARATION_KEY_STRING)).Value.Value ?? false;
context.AddSource($"{classSymbol.Name}_cloneable.g.cs", SourceText.From(CreateCloneableCode(classSymbol, isExplicit), Encoding.UTF8));
}
}
private void InitAttributes(Compilation compilation)
{
_cloneableAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{CLONEABLE_ATTRIBUTE_STRING}")!;
_cloneAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{CLONE_ATTRIBUTE_STRING}")!;
_ignoreCloneAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{IGNORE_CLONE_ATTRIBUTE_STRING}")!;
}
private static Compilation GetCompilation(GeneratorExecutionContext context)
{
var options = context.Compilation.SyntaxTrees.First().Options as CSharpParseOptions;
var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(CLONEABLE_ATTRIBUTE_TEXT, Encoding.UTF8), options)).
AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8), options)).
AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8), options));
return compilation;
}
private string CreateCloneableCode(INamedTypeSymbol classSymbol, bool isExplicit)
{
string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var fieldAssignmentsCode = GenerateFieldAssignmentsCode(classSymbol, isExplicit).ToList();
var fieldAssignmentsCodeSafe = fieldAssignmentsCode.Select(x =>
{
if (x.isCloneable)
return x.line + "Safe(referenceChain)";
return x.line;
});
var fieldAssignmentsCodeFast = fieldAssignmentsCode.Select(x =>
{
if (x.isCloneable)
return x.line + "()";
return x.line;
});
return $@"using System.Collections.Generic;
namespace {namespaceName}
{{
{GetAccessModifier(classSymbol)} partial class {classSymbol.Name}
{{
/// <summary>
/// Creates a copy of {classSymbol.Name} with NO circular reference checking. This method should be used if performance matters.
///
/// <exception cref=""StackOverflowException"">Will occur on any object that has circular references in the hierarchy.</exception>
/// </summary>
public {classSymbol.Name} Clone()
{{
return new {classSymbol.Name}
{{
{string.Join(",\n", fieldAssignmentsCodeFast)}
}};
}}
/// <summary>
/// Creates a copy of {classSymbol.Name} with circular reference checking. If a circular reference was detected, only a reference of the leaf object is passed instead of cloning it.
/// </summary>
/// <param name=""referenceChain"">Should only be provided if specific objects should not be cloned but passed by reference instead.</param>
public {classSymbol.Name} CloneSafe(Stack<object> referenceChain = null)
{{
if(referenceChain?.Contains(this) == true)
return this;
referenceChain ??= new Stack<object>();
referenceChain.Push(this);
var result = new {classSymbol.Name}
{{
{string.Join($",\n", fieldAssignmentsCodeSafe)}
}};
referenceChain.Pop();
return result;
}}
}}
}}";
}
private IEnumerable<(string line, bool isCloneable)> GenerateFieldAssignmentsCode(INamedTypeSymbol classSymbol, bool isExplicit )
{
var fieldNames = GetCloneableProperties(classSymbol, isExplicit);
var fieldAssignments = fieldNames.Select(field => IsFieldCloneable(field, classSymbol))
.OrderBy(x => x.isCloneable)
.Select(x => (GenerateAssignmentCode(x.item.Name, x.isCloneable), x.isCloneable));
return fieldAssignments;
}
private string GenerateAssignmentCode(string name, bool isCloneable)
{
if (isCloneable)
{
return $@" {name} = this.{name}?.Clone";
}
return $@" {name} = this.{name}";
}
private (IPropertySymbol item, bool isCloneable) IsFieldCloneable(IPropertySymbol x, INamedTypeSymbol classSymbol)
{
if (SymbolEqualityComparer.Default.Equals(x.Type, classSymbol))
{
return (x, false);
}
if (!x.Type.TryGetAttribute(_cloneableAttribute!, out var attributes))
{
return (x, false);
}
var preventDeepCopy = (bool?)attributes.Single().NamedArguments.FirstOrDefault(e => e.Key.Equals(PREVENT_DEEP_COPY_KEY_STRING)).Value.Value ?? false;
return (item: x, !preventDeepCopy);
}
private string GetAccessModifier(INamedTypeSymbol classSymbol)
=> classSymbol.DeclaredAccessibility.ToString().ToLowerInvariant();
private IEnumerable<IPropertySymbol> GetCloneableProperties(ITypeSymbol classSymbol, bool isExplicit)
{
var targetSymbolMembers = classSymbol.GetMembers().OfType<IPropertySymbol>()
.Where(x => x.SetMethod is not null &&
x.CanBeReferencedByName);
if (isExplicit)
{
return targetSymbolMembers.Where(x => x.HasAttribute(_cloneAttribute!));
}
else
{
return targetSymbolMembers.Where(x => !x.HasAttribute(_ignoreCloneAttribute!));
}
}
private static IEnumerable<INamedTypeSymbol> GetClassSymbols(Compilation compilation, SyntaxReceiver receiver)
=> receiver.CandidateClasses.Select(clazz => GetClassSymbol(compilation, clazz));
private static INamedTypeSymbol GetClassSymbol(Compilation compilation, ClassDeclarationSyntax clazz)
{
var model = compilation.GetSemanticModel(clazz.SyntaxTree);
var classSymbol = model.GetDeclaredSymbol(clazz)!;
return classSymbol;
}
private static void InjectCloneableAttributes(GeneratorExecutionContext context)
{
context.AddSource(CLONEABLE_ATTRIBUTE_STRING, SourceText.From(CLONEABLE_ATTRIBUTE_TEXT, Encoding.UTF8));
context.AddSource(CLONE_ATTRIBUTE_STRING, SourceText.From(CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8));
context.AddSource(IGNORE_CLONE_ATTRIBUTE_STRING, SourceText.From(IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8));
}
}
}

View File

@@ -1,24 +0,0 @@
// Code temporarily yeeted from
// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs
// because of NRT issue
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace Cloneable
{
internal static class SymbolExtensions
{
public static bool TryGetAttribute(this ISymbol symbol, INamedTypeSymbol attributeType,
out IEnumerable<AttributeData> attributes)
{
attributes = symbol.GetAttributes()
.Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType));
return attributes.Any();
}
public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType)
=> symbol.GetAttributes()
.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType));
}
}

View File

@@ -1,27 +0,0 @@
// Code temporarily yeeted from
// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs
// because of NRT issue
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Cloneable
{
internal class SyntaxReceiver : ISyntaxReceiver
{
public IList<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();
/// <summary>
/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
/// </summary>
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// any field with at least one attribute is a candidate for being cloneable
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&
classDeclarationSyntax.AttributeLists.Count > 0)
{
CandidateClasses.Add(classDeclarationSyntax);
}
}
}
}

View File

@@ -1,323 +0,0 @@
#nullable enable
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace NadekoBot.Generators.Command;
[Generator]
public class CommandAttributesGenerator : IIncrementalGenerator
{
public const string ATTRIBUTE = @"// <AutoGenerated />
namespace NadekoBot.Common;
[System.AttributeUsage(System.AttributeTargets.Method)]
public class CmdAttribute : System.Attribute
{
}";
public class MethodModel
{
public string? Namespace { get; }
public IReadOnlyCollection<string> Classes { get; }
public string ReturnType { get; }
public string MethodName { get; }
public IEnumerable<string> Params { get; }
public MethodModel(string? ns, IReadOnlyCollection<string> classes, string returnType, string methodName, IEnumerable<string> @params)
{
Namespace = ns;
Classes = classes;
ReturnType = returnType;
MethodName = methodName;
Params = @params;
}
}
public class FileModel
{
public string? Namespace { get; }
public IReadOnlyCollection<string> ClassHierarchy { get; }
public IReadOnlyCollection<MethodModel> Methods { get; }
public FileModel(string? ns, IReadOnlyCollection<string> classHierarchy, IReadOnlyCollection<MethodModel> methods)
{
Namespace = ns;
ClassHierarchy = classHierarchy;
Methods = methods;
}
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// #if DEBUG
// SpinWait.SpinUntil(() => Debugger.IsAttached);
// #endif
context.RegisterPostInitializationOutput(static ctx => ctx.AddSource(
"CmdAttribute.g.cs",
SourceText.From(ATTRIBUTE, Encoding.UTF8)));
var methods = context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
static (ctx, cancel) => Transform(ctx, cancel))
.Where(static m => m is not null)
.Where(static m => m?.ChildTokens().Any(static x => x.IsKind(SyntaxKind.PublicKeyword)) ?? false);
var compilationMethods = context.CompilationProvider.Combine(methods.Collect());
context.RegisterSourceOutput(compilationMethods,
static (ctx, tuple) => RegisterAction(in ctx, tuple.Left, in tuple.Right));
}
private static void RegisterAction(in SourceProductionContext ctx,
Compilation comp,
in ImmutableArray<MethodDeclarationSyntax?> methods)
{
if (methods is { IsDefaultOrEmpty: true })
return;
var models = GetModels(comp, methods, ctx.CancellationToken);
foreach (var model in models)
{
var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs";
try
{
Debug.WriteLine($"Writing {name}");
var source = GetSourceText(model);
ctx.AddSource(name, SourceText.From(source, Encoding.UTF8));
}
catch (Exception ex)
{
Debug.WriteLine($"Error writing source file {name}\n" + ex);
}
}
}
private static string GetSourceText(FileModel model)
{
using var sw = new StringWriter();
using var tw = new IndentedTextWriter(sw);
tw.WriteLine("// <AutoGenerated />");
tw.WriteLine("#pragma warning disable CS1066");
if (model.Namespace is not null)
{
tw.WriteLine($"namespace {model.Namespace};");
tw.WriteLine();
}
foreach (var className in model.ClassHierarchy)
{
tw.WriteLine($"public partial class {className}");
tw.WriteLine("{");
tw.Indent ++;
}
foreach (var method in model.Methods)
{
tw.WriteLine("[NadekoCommand]");
tw.WriteLine("[NadekoDescription]");
tw.WriteLine("[Aliases]");
tw.WriteLine($"public partial {method.ReturnType} {method.MethodName}({string.Join(", ", method.Params)});");
}
foreach (var _ in model.ClassHierarchy)
{
tw.Indent --;
tw.WriteLine("}");
}
tw.Flush();
return sw.ToString();
}
private static IReadOnlyCollection<FileModel> GetModels(Compilation compilation,
in ImmutableArray<MethodDeclarationSyntax?> inputMethods,
CancellationToken cancel)
{
var models = new List<FileModel>();
var methods = inputMethods
.Where(static x => x is not null)
.Distinct();
var methodModels = methods
.Select(x => MethodDeclarationToMethodModel(compilation, x!));
var groups = methodModels
.GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}");
foreach (var group in groups)
{
if (cancel.IsCancellationRequested)
return new Collection<FileModel>();
if (group is null)
continue;
var elems = group.ToList();
if (elems.Count is 0)
continue;
var model = new FileModel(
methods: elems,
ns: elems[0].Namespace,
classHierarchy: elems[0].Classes
);
models.Add(model);
}
return models;
}
private static MethodModel MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl)
{
// SpinWait.SpinUntil(static () => Debugger.IsAttached);
var semanticModel = comp.GetSemanticModel(decl.SyntaxTree);
var methodModel = new MethodModel(
@params: decl.ParameterList.Parameters
.Where(p => p.Type is not null)
.Select(p =>
{
var prefix = p.Modifiers.Any(static x => x.IsKind(SyntaxKind.ParamsKeyword))
? "params "
: string.Empty;
var type = semanticModel
.GetTypeInfo(p.Type!)
.Type
?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var name = p.Identifier.Text;
var suffix = string.Empty;
if (p.Default is not null)
{
if (p.Default.Value is LiteralExpressionSyntax)
{
suffix = " = " + p.Default.Value;
}
else if (p.Default.Value is MemberAccessExpressionSyntax maes)
{
var maesSemModel = comp.GetSemanticModel(maes.SyntaxTree);
var sym = maesSemModel.GetSymbolInfo(maes.Name);
if (sym.Symbol is null)
{
suffix = " = " + p.Default.Value;
}
else
{
suffix = " = " + sym.Symbol.ToDisplayString();
}
}
}
return $"{prefix}{type} {name}{suffix}";
})
.ToList(),
methodName: decl.Identifier.Text,
returnType: decl.ReturnType.ToString(),
ns: GetNamespace(decl),
classes: GetClasses(decl)
);
return methodModel;
}
//https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/main/src/NetEscapades.EnumGenerators/EnumGenerator.cs
static string? GetNamespace(MethodDeclarationSyntax declarationSyntax)
{
// determine the namespace the class is declared in, if any
string? nameSpace = null;
var parentOfInterest = declarationSyntax.Parent;
while (parentOfInterest is not null)
{
parentOfInterest = parentOfInterest.Parent;
if (parentOfInterest is BaseNamespaceDeclarationSyntax ns)
{
nameSpace = ns.Name.ToString();
while (true)
{
if (ns.Parent is not NamespaceDeclarationSyntax parent)
{
break;
}
ns = parent;
nameSpace = $"{ns.Name}.{nameSpace}";
}
return nameSpace;
}
}
return nameSpace;
}
static IReadOnlyCollection<string> GetClasses(MethodDeclarationSyntax declarationSyntax)
{
// determine the namespace the class is declared in, if any
var classes = new LinkedList<string>();
var parentOfInterest = declarationSyntax.Parent;
while (parentOfInterest is not null)
{
if (parentOfInterest is ClassDeclarationSyntax cds)
{
classes.AddFirst(cds.Identifier.ToString());
}
parentOfInterest = parentOfInterest.Parent;
}
Debug.WriteLine($"Method {declarationSyntax.Identifier.Text} has {classes.Count} classes");
return classes;
}
private static MethodDeclarationSyntax? Transform(GeneratorSyntaxContext ctx, CancellationToken cancel)
{
var methodDecl = ctx.Node as MethodDeclarationSyntax;
if (methodDecl is null)
return default;
foreach (var attListSyntax in methodDecl.AttributeLists)
{
foreach (var attSyntax in attListSyntax.Attributes)
{
if (cancel.IsCancellationRequested)
return default;
var symbol = ctx.SemanticModel.GetSymbolInfo(attSyntax).Symbol;
if (symbol is not IMethodSymbol attSymbol)
continue;
if (attSymbol.ContainingType.ToDisplayString() == "NadekoBot.Common.CmdAttribute")
return methodDecl;
}
}
return default;
}
}

View File

@@ -1,32 +1,26 @@
#nullable enable
using System;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Newtonsoft.Json;
namespace NadekoBot.Generators
{
internal readonly struct TranslationPair
internal class TranslationPair
{
public string Name { get; }
public string Value { get; }
public TranslationPair(string name, string value)
{
Name = name;
Value = value;
}
public string Name { get; set; }
public string Value { get; set; }
}
[Generator]
public class LocalizedStringsGenerator : ISourceGenerator
{
private const string LOC_STR_SOURCE = @"namespace NadekoBot
private const string LocStrSource = @"namespace NadekoBot
{
public readonly struct LocStr
{
@@ -55,8 +49,9 @@ namespace NadekoBot.Generators
using (var stringWriter = new StringWriter())
using (var sw = new IndentedTextWriter(stringWriter))
{
sw.WriteLine("namespace NadekoBot;");
sw.WriteLine();
sw.WriteLine("namespace NadekoBot");
sw.WriteLine("{");
sw.Indent++;
sw.WriteLine("public static class strs");
sw.WriteLine("{");
@@ -89,42 +84,32 @@ namespace NadekoBot.Generators
sw.Indent--;
sw.WriteLine("}");
sw.Indent--;
sw.WriteLine("}");
sw.Flush();
context.AddSource("strs.g.cs", stringWriter.ToString());
context.AddSource("strs.cs", stringWriter.ToString());
}
context.AddSource("LocStr.g.cs", LOC_STR_SOURCE);
context.AddSource("LocStr.cs", LocStrSource);
}
private List<TranslationPair> GetFields(string? dataText)
private List<TranslationPair> GetFields(string dataText)
{
if (string.IsNullOrWhiteSpace(dataText))
return new();
throw new ArgumentNullException(nameof(dataText));
Dictionary<string, string> data;
try
{
var output = JsonConvert.DeserializeObject<Dictionary<string, string>>(dataText!);
if (output is null)
return new();
data = output;
}
catch
{
Debug.WriteLine("Failed parsing responses file.");
return new();
}
var data = JsonConvert.DeserializeObject<Dictionary<string, string>>(dataText);
var list = new List<TranslationPair>();
foreach (var entry in data)
{
list.Add(new(
entry.Key,
entry.Value
));
list.Add(new TranslationPair()
{
Name = entry.Key,
Value = entry.Value
});
}
return list;

View File

@@ -1,15 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IncludeBuildOutput>false</IncludeBuildOutput>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup>

View File

@@ -14,7 +14,6 @@ namespace NadekoBot.Tests
private const string responsesPath = "../../../../NadekoBot/data/strings/responses";
private const string commandsPath = "../../../../NadekoBot/data/strings/commands";
private const string aliasesPath = "../../../../NadekoBot/data/aliases.yml";
[Test]
public void AllCommandNamesHaveStrings()
{
@@ -26,13 +25,15 @@ namespace NadekoBot.Tests
var culture = new CultureInfo("en-US");
var isSuccess = true;
foreach (var (methodName, _) in CommandNameLoadHelper.LoadAliases(aliasesPath))
foreach (var entry in CommandNameLoadHelper.LoadCommandNames(aliasesPath))
{
var cmdStrings = strings.GetCommandStrings(culture.Name, methodName);
var commandName = entry.Value[0];
var cmdStrings = strings.GetCommandStrings(culture.Name, commandName);
if (cmdStrings is null)
{
isSuccess = false;
TestContext.Out.WriteLine($"{methodName} doesn't exist in commands.en-US.yml");
TestContext.Out.WriteLine($"{commandName} doesn't exist in commands.en-US.yml");
}
}
@@ -40,7 +41,7 @@ namespace NadekoBot.Tests
}
private static string[] GetCommandMethodNames()
=> typeof(Bot).Assembly
=> typeof(NadekoBot.Bot).Assembly
.GetExportedTypes()
.Where(type => type.IsClass && !type.IsAbstract)
.Where(type => typeof(NadekoModule).IsAssignableFrom(type) // if its a top level module
@@ -54,7 +55,7 @@ namespace NadekoBot.Tests
[Test]
public void AllCommandMethodsHaveNames()
{
var allAliases = CommandNameLoadHelper.LoadAliases(
var allAliases = CommandNameLoadHelper.LoadCommandNames(
aliasesPath);
var methodNames = GetCommandMethodNames();
@@ -62,7 +63,7 @@ namespace NadekoBot.Tests
var isSuccess = true;
foreach (var methodName in methodNames)
{
if (!allAliases.TryGetValue(methodName, out _))
if (!allAliases.TryGetValue(methodName, out var _))
{
TestContext.Error.WriteLine($"{methodName} is missing an alias.");
isSuccess = false;
@@ -75,7 +76,7 @@ namespace NadekoBot.Tests
[Test]
public void NoObsoleteAliases()
{
var allAliases = CommandNameLoadHelper.LoadAliases(aliasesPath);
var allAliases = CommandNameLoadHelper.LoadCommandNames(aliasesPath);
var methodNames = GetCommandMethodNames()
.ToHashSet();
@@ -93,10 +94,7 @@ namespace NadekoBot.Tests
}
}
if(isSuccess)
Assert.Pass();
else
Assert.Warn("There are some unused entries in data/aliases.yml");
Assert.IsTrue(isSuccess);
}
// [Test]

View File

@@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Extensions;
using NadekoBot.Services;
using NUnit.Framework;
@@ -12,7 +13,9 @@ namespace NadekoBot.Tests
[SetUp]
public void Setup()
=> _grouper = new GreetGrouper<int>();
{
_grouper = new GreetGrouper<int>();
}
[Test]
public void CreateTest()
@@ -59,8 +62,8 @@ namespace NadekoBot.Tests
_grouper.CreateOrAdd(0, 5);
// add 15 items
await Enumerable.Range(10, 15)
.Select(x => Task.Run(() => _grouper.CreateOrAdd(0, x))).WhenAll();
await Task.WhenAll(Enumerable.Range(10, 15)
.Select(x => Task.Run(() => _grouper.CreateOrAdd(0, x))));
// get 5 at most
_grouper.ClearGroup(0, 5, out var items);

View File

@@ -62,10 +62,7 @@ namespace NadekoBot.Tests
collection.Clear();
Assert.IsTrue(collection.Count == 0, "Collection has not been cleared.");
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
_ = collection[0];
}, "Collection has not been cleared.");
Assert.Throws<ArgumentOutOfRangeException>(() => collection.Contains(collection[0]), "Collection has not been cleared.");
}
[Test]
@@ -118,7 +115,7 @@ namespace NadekoBot.Tests
[Test]
public void ContainsTest()
{
var subCol = new[]
var subCol = new ShopEntry[]
{
new ShopEntry() { Id = 111 },
new ShopEntry() { Id = 222 },

View File

@@ -1,4 +1,5 @@
using NadekoBot.Common;
using System.Linq;
using NadekoBot.Common;
using NUnit.Framework;
namespace NadekoBot.Tests

View File

@@ -1,16 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<EnablePreviewFeatures>True</EnablePreviewFeatures>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,7 +9,9 @@ namespace NadekoBot.Tests
{
[SetUp]
public void Setup()
=> Console.OutputEncoding = Encoding.UTF8;
{
Console.OutputEncoding = Encoding.UTF8;
}
[Test]
public void Utf8CodepointsToEmoji()

View File

@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NadekoBot.VotesApi.Controllers;
namespace NadekoBot.VotesApi
{
@@ -23,7 +24,9 @@ namespace NadekoBot.VotesApi
ISystemClock clock,
IConfiguration conf)
: base(options, logger, encoder, clock)
=> _conf = conf;
{
_conf = conf;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{

View File

@@ -23,7 +23,7 @@
public bool Weekend { get; set; }
/// <summary>
/// Query string params found on the /bot/:ID/vote page. Example: ?a=1&amp;b=2.
/// Query string params found on the /bot/:ID/vote page. Example: ?a=1&b=2.
/// </summary>
public string Query { get; set; }
}

View File

@@ -11,10 +11,10 @@ namespace NadekoBot.VotesApi.Controllers
[Route("[controller]")]
public class DiscordsController : ControllerBase
{
private readonly ILogger<DiscordsController> _logger;
private readonly ILogger<TopGgController> _logger;
private readonly IVotesCache _cache;
public DiscordsController(ILogger<DiscordsController> logger, IVotesCache cache)
public DiscordsController(ILogger<TopGgController> logger, IVotesCache cache)
{
_logger = logger;
_cache = cache;
@@ -26,7 +26,7 @@ namespace NadekoBot.VotesApi.Controllers
{
var votes = await _cache.GetNewDiscordsVotesAsync();
if(votes.Count > 0)
_logger.LogInformation("Sending {NewDiscordsVotes} new discords votes", votes.Count);
_logger.LogInformation("Sending {NewDiscordsVotes} new discords votes.", votes.Count);
return votes;
}
}

View File

@@ -26,7 +26,7 @@ namespace NadekoBot.VotesApi.Controllers
{
var votes = await _cache.GetNewTopGgVotesAsync();
if(votes.Count > 0)
_logger.LogInformation("Sending {NewTopggVotes} new topgg votes", votes.Count);
_logger.LogInformation("Sending {NewTopggVotes} new topgg votes.", votes.Count);
return votes;
}

View File

@@ -1,6 +1,8 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NadekoBot.VotesApi.Services;
@@ -11,11 +13,13 @@ namespace NadekoBot.VotesApi.Controllers
{
private readonly ILogger<WebhookController> _logger;
private readonly IVotesCache _votesCache;
private readonly IConfiguration _conf;
public WebhookController(ILogger<WebhookController> logger, IVotesCache votesCache)
public WebhookController(ILogger<WebhookController> logger, IVotesCache votesCache, IConfiguration conf)
{
_logger = logger;
_votesCache = votesCache;
_conf = conf;
}
[HttpPost("/discordswebhook")]

View File

@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using NadekoBot.VotesApi;
using Microsoft.Extensions.Logging;
namespace NadekoBot.VotesApi
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
static IHostBuilder CreateHostBuilder(string[] args) =>
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -9,33 +10,37 @@ namespace NadekoBot.VotesApi.Services
{
public class FileVotesCache : IVotesCache
{
// private const string STATS_FILE = "store/stats.json";
private const string TOPGG_FILE = "store/topgg.json";
private const string DISCORDS_FILE = "store/discords.json";
private const string statsFile = "store/stats.json";
private const string topggFile = "store/topgg.json";
private const string discordsFile = "store/discords.json";
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
public FileVotesCache()
{
if (!Directory.Exists("store"))
Directory.CreateDirectory("store");
if(!File.Exists(TOPGG_FILE))
File.WriteAllText(TOPGG_FILE, "[]");
if(!File.Exists(topggFile))
File.WriteAllText(topggFile, "[]");
if(!File.Exists(DISCORDS_FILE))
File.WriteAllText(DISCORDS_FILE, "[]");
if(!File.Exists(discordsFile))
File.WriteAllText(discordsFile, "[]");
}
public ITask AddNewTopggVote(string userId)
=> AddNewVote(TOPGG_FILE, userId);
{
return AddNewVote(topggFile, userId);
}
public ITask AddNewDiscordsVote(string userId)
=> AddNewVote(DISCORDS_FILE, userId);
{
return AddNewVote(discordsFile, userId);
}
private async ITask AddNewVote(string file, string userId)
{
await _locker.WaitAsync();
await locker.WaitAsync();
try
{
var votes = await GetVotesAsync(file);
@@ -44,7 +49,7 @@ namespace NadekoBot.VotesApi.Services
}
finally
{
_locker.Release();
locker.Release();
}
}
@@ -61,14 +66,14 @@ namespace NadekoBot.VotesApi.Services
}
private ITask<List<Vote>> EvictTopggVotes()
=> EvictVotes(TOPGG_FILE);
=> EvictVotes(topggFile);
private ITask<List<Vote>> EvictDiscordsVotes()
=> EvictVotes(DISCORDS_FILE);
=> EvictVotes(discordsFile);
private async ITask<List<Vote>> EvictVotes(string file)
{
await _locker.WaitAsync();
await locker.WaitAsync();
try
{
@@ -86,7 +91,7 @@ namespace NadekoBot.VotesApi.Services
}
finally
{
_locker.Release();
locker.Release();
}
}

View File

@@ -11,18 +11,19 @@ namespace NadekoBot.VotesApi
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
=> Configuration = configuration;
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IVotesCache, FileVotesCache>();
services.AddSwaggerGen(static c =>
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "NadekoBot.VotesApi", Version = "v1" });
});
@@ -35,13 +36,13 @@ namespace NadekoBot.VotesApi
});
services
.AddAuthorization(static opts =>
.AddAuthorization(opts =>
{
opts.DefaultPolicy = new AuthorizationPolicyBuilder(AuthHandler.SchemeName)
.RequireAssertion(static _ => false)
.RequireAssertion(x => false)
.Build();
opts.AddPolicy(Policies.DiscordsAuth, static policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
opts.AddPolicy(Policies.TopggAuth, static policy => policy.RequireClaim(AuthHandler.TopggClaim));
opts.AddPolicy(Policies.DiscordsAuth, policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
opts.AddPolicy(Policies.TopggAuth, policy => policy.RequireClaim(AuthHandler.TopggClaim));
});
}
@@ -52,7 +53,7 @@ namespace NadekoBot.VotesApi
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(static c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "NadekoBot.VotesApi v1"));
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "NadekoBot.VotesApi v1"));
}
app.UseHttpsRedirection();
@@ -62,7 +63,7 @@ namespace NadekoBot.VotesApi
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(static endpoints => { endpoints.MapControllers(); });
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
}

View File

@@ -1,3 +1,5 @@
using System;
namespace NadekoBot.VotesApi
{
public class Vote

View File

@@ -1,359 +0,0 @@
root = true
# Remove the line below if you want to inherit .editorconfig settings from higher directories
[obj/**]
generated_code = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
# Modifier preferences
dotnet_style_require_accessibility_modifiers = always:error
# Expression-level preferences
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:warning
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
dotnet_style_prefer_conditional_expression_over_return = false:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true:suggestion
# Parameter preferences
dotnet_code_quality_unused_parameters = all:warning
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = true
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = true:suggestion
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:error
csharp_style_pattern_matching_over_is_with_cast_check = true:error
csharp_style_prefer_not_pattern = true:error
csharp_style_prefer_pattern_matching = true:suggestion
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true:error
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
# Code-block preferences
csharp_prefer_braces = when_multiline:warning
csharp_prefer_simple_using_statement = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true:error
csharp_style_inlined_variable_declaration = true:warning
csharp_style_pattern_local_over_anonymous_function = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_range_operator = true
csharp_style_throw_expression = true:error
csharp_style_unused_value_assignment_preference = discard_variable:warning
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:error
# Enforce file-scoped namespaces
csharp_style_namespace_declarations = file_scoped:error
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
csharp_style_allow_embedded_statements_on_same_line_experimental = false
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
#### Naming styles ####
# Naming rules
dotnet_naming_rule.private_readonly_field.symbols = private_readonly_field
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
dotnet_naming_rule.private_readonly_field.severity = warning
dotnet_naming_rule.private_field.symbols = private_field
dotnet_naming_rule.private_field.style = camel_case
dotnet_naming_rule.private_field.severity = warning
dotnet_naming_rule.const_fields.symbols = const_fields
dotnet_naming_rule.const_fields.style = all_upper
dotnet_naming_rule.const_fields.severity = warning
# dotnet_naming_rule.class_should_be_pascal_case.severity = error
# dotnet_naming_rule.class_should_be_pascal_case.symbols = class
# dotnet_naming_rule.class_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.struct_should_be_pascal_case.severity = error
dotnet_naming_rule.struct_should_be_pascal_case.symbols = struct
dotnet_naming_rule.struct_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
# dotnet_naming_rule.types_should_be_pascal_case.severity = error
# dotnet_naming_rule.types_should_be_pascal_case.symbols = types
# dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
# dotnet_naming_rule.enum_should_be_pascal_case.severity = error
# dotnet_naming_rule.enum_should_be_pascal_case.symbols = enum
# dotnet_naming_rule.enum_should_be_pascal_case.style = pascal_case
# dotnet_naming_rule.property_should_be_pascal_case.severity = error
# dotnet_naming_rule.property_should_be_pascal_case.symbols = property
# dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.method_should_be_pascal_case.severity = error
dotnet_naming_rule.method_should_be_pascal_case.symbols = method
dotnet_naming_rule.method_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.async_method_should_be_ends_with_async.severity = error
dotnet_naming_rule.async_method_should_be_ends_with_async.symbols = async_method
dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async
# dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
# dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
# dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.local_variable_should_be_camel_case.severity = error
dotnet_naming_rule.local_variable_should_be_camel_case.symbols = local_variable
dotnet_naming_rule.local_variable_should_be_camel_case.style = camel_case
# Symbol specifications
dotnet_naming_symbols.const_fields.required_modifiers = const
dotnet_naming_symbols.const_fields.applicable_kinds = field
dotnet_naming_symbols.class.applicable_kinds = class
dotnet_naming_symbols.class.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.class.required_modifiers =
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.struct.applicable_kinds = struct
dotnet_naming_symbols.struct.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.struct.required_modifiers =
dotnet_naming_symbols.enum.applicable_kinds = enum
dotnet_naming_symbols.enum.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enum.required_modifiers =
dotnet_naming_symbols.method.applicable_kinds = method
dotnet_naming_symbols.method.applicable_accessibilities = public
dotnet_naming_symbols.method.required_modifiers =
dotnet_naming_symbols.property.applicable_kinds = property
dotnet_naming_symbols.property.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.property.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.private_readonly_field.applicable_kinds = field
dotnet_naming_symbols.private_readonly_field.applicable_accessibilities = private, protected
dotnet_naming_symbols.private_readonly_field.required_modifiers = readonly
dotnet_naming_symbols.private_field.applicable_kinds = field
dotnet_naming_symbols.private_field.applicable_accessibilities = private, protected
dotnet_naming_symbols.private_field.required_modifiers =
dotnet_naming_symbols.async_method.applicable_kinds = method, local_function
dotnet_naming_symbols.async_method.applicable_accessibilities = *
dotnet_naming_symbols.async_method.required_modifiers = async
dotnet_naming_symbols.local_variable.applicable_kinds = parameter, local
dotnet_naming_symbols.local_variable.applicable_accessibilities = local
dotnet_naming_symbols.local_variable.required_modifiers =
# Naming styles
dotnet_naming_style.all_upper.capitalization = all_upper
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.begins_with_underscore.required_prefix = _
dotnet_naming_style.begins_with_underscore.required_suffix =
dotnet_naming_style.begins_with_underscore.word_separator =
dotnet_naming_style.begins_with_underscore.capitalization = camel_case
dotnet_naming_style.ends_with_async.required_prefix =
# dotnet_naming_style.ends_with_async.required_suffix = Async
dotnet_naming_style.ends_with_async.word_separator =
dotnet_naming_style.ends_with_async.capitalization = pascal_case
dotnet_naming_style.camel_case.required_prefix =
dotnet_naming_style.camel_case.required_suffix =
dotnet_naming_style.camel_case.word_separator =
dotnet_naming_style.camel_case.capitalization = camel_case
# CA1822: Mark members as static
dotnet_diagnostic.ca1822.severity = suggestion
# IDE0004: Cast is redundant
dotnet_diagnostic.ide0004.severity = warning
# IDE0058: Expression value is never used
dotnet_diagnostic.ide0058.severity = none
# # IDE0011: Add braces to 'if'/'else' statement
# dotnet_diagnostic.ide0011.severity = none
resharper_wrap_after_invocation_lpar = false
resharper_wrap_before_invocation_rpar = false
# ReSharper properties
resharper_align_multiline_calls_chain = true
resharper_csharp_wrap_after_declaration_lpar = true
resharper_csharp_wrap_after_invocation_lpar = false
resharper_csharp_wrap_before_binary_opsign = true
resharper_csharp_wrap_before_invocation_rpar = false
resharper_csharp_wrap_parameters_style = chop_if_long
resharper_force_chop_compound_if_expression = false
resharper_keep_existing_linebreaks = true
resharper_keep_user_linebreaks = true
resharper_max_formal_parameters_on_line = 3
resharper_place_simple_embedded_statement_on_same_line = false
resharper_wrap_chained_binary_expressions = chop_if_long
resharper_wrap_chained_binary_patterns = chop_if_long
resharper_wrap_chained_method_calls = chop_if_long
resharper_wrap_object_and_collection_initializer_style = chop_always
resharper_csharp_wrap_before_first_type_parameter_constraint = true
resharper_csharp_place_type_constraints_on_same_line = false
resharper_csharp_wrap_before_extends_colon = true
resharper_csharp_place_constructor_initializer_on_same_line = false
resharper_force_attribute_style = separate
resharper_csharp_braces_for_ifelse = required_for_multiline_statement
resharper_csharp_braces_for_foreach = required_for_multiline
resharper_csharp_braces_for_while = required_for_multiline
resharper_csharp_braces_for_for = required_for_multiline
resharper_arrange_redundant_parentheses_highlighting = hint
# IDE0011: Add braces
dotnet_diagnostic.IDE0011.severity = warning

View File

@@ -1,19 +1,36 @@
#nullable disable
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Common.Configs;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
using NadekoBot.Common;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using RunMode = Discord.Commands.RunMode;
namespace NadekoBot;
using System.Threading.Tasks;
using Discord.Net;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Configs;
using NadekoBot.Db;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Searches;
using Serilog;
namespace NadekoBot
{
public sealed class Bot
{
private readonly IBotCredentials _creds;
private readonly CommandService _commandService;
private readonly DbService _db;
private readonly IBotCredsProvider _credsProvider;
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
public DiscordSocketClient Client { get; }
@@ -23,89 +40,68 @@ public sealed class Bot
public string Mention { get; private set; }
public bool IsReady { get; private set; }
public int ShardId { get; set; }
private readonly IBotCredentials _creds;
private readonly CommandService _commandService;
private readonly DbService _db;
private readonly IBotCredsProvider _credsProvider;
// private readonly InteractionService _interactionService;
public Bot(int shardId, int? totalShards)
{
if (shardId < 0)
throw new ArgumentOutOfRangeException(nameof(shardId));
ShardId = shardId;
_credsProvider = new BotCredsProvider(totalShards);
_creds = _credsProvider.GetCreds();
_db = new(_creds);
_db = new DbService(_creds);
if (shardId == 0)
_db.Setup();
var messageCacheSize =
#if GLOBAL_NADEKO
0;
#else
50;
#endif
if(!_creds.UsePrivilegedIntents)
Log.Warning("You are not using privileged intents. Some features will not work properly");
Client = new(new()
{
MessageCacheSize = messageCacheSize,
_db.Setup();
}
Client = new DiscordSocketClient(new DiscordSocketConfig
{
MessageCacheSize = 50,
LogLevel = LogSeverity.Warning,
ConnectionTimeout = int.MaxValue,
TotalShards = _creds.TotalShards,
ShardId = shardId,
AlwaysDownloadUsers = false,
AlwaysResolveStickers = false,
AlwaysDownloadDefaultStickers = false,
GatewayIntents = _creds.UsePrivilegedIntents
? GatewayIntents.All
: GatewayIntents.AllUnprivileged,
LogGatewayIntentWarnings = false,
ExclusiveBulkDelete = true,
});
_commandService = new(new()
_commandService = new CommandService(new CommandServiceConfig()
{
CaseSensitiveCommands = false,
DefaultRunMode = RunMode.Sync
DefaultRunMode = RunMode.Sync,
});
// _interactionService = new(Client.Rest);
#if GLOBAL_NADEKO || DEBUG
Client.Log += Client_Log;
#endif
}
public List<ulong> GetCurrentGuildIds()
=> Client.Guilds.Select(x => x.Id).ToList();
{
return Client.Guilds.Select(x => x.Id).ToList();
}
private void AddServices()
{
var startingGuildIdList = GetCurrentGuildIds();
var sw = Stopwatch.StartNew();
var bot = Client.CurrentUser;
var _bot = Client.CurrentUser;
using (var uow = _db.GetDbContext())
{
uow.EnsureUserCreated(bot.Id, bot.Username, bot.Discriminator, bot.AvatarId);
uow.EnsureUserCreated(_bot.Id, _bot.Username, _bot.Discriminator, _bot.AvatarId);
AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray();
}
var svcs = new ServiceCollection().AddTransient(_ => _credsProvider.GetCreds()) // bot creds
.AddSingleton(_credsProvider)
var svcs = new ServiceCollection()
.AddTransient<IBotCredentials>(_ => _credsProvider.GetCreds()) // bot creds
.AddSingleton<IBotCredsProvider>(_credsProvider)
.AddSingleton(_db) // database
.AddRedis(_creds.RedisOptions) // redis
.AddSingleton(Client) // discord socket client
.AddSingleton(_commandService)
// .AddSingleton(_interactionService)
.AddSingleton(this)
.AddSingleton<ISeria, JsonSeria>()
.AddSingleton<IPubSub, RedisPubSub>()
@@ -115,23 +111,25 @@ public sealed class Bot
.AddConfigMigrators()
.AddMemoryCache()
// music
.AddMusic();
.AddMusic()
// admin
#if GLOBAL_NADEKO
svcs.AddSingleton<ILogCommandService, DummyLogCommandService>();
.AddSingleton<ILogCommandService, DummyLogCommandService>()
#else
svcs.AddSingleton<ILogCommandService, LogCommandService>();
.AddSingleton<ILogCommandService, LogCommandService>()
#endif
;
svcs.AddHttpClient();
svcs.AddHttpClient("memelist")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
svcs.AddHttpClient("memelist").ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
AllowAutoRedirect = false
});
if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
{
svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
}
else
{
svcs.AddSingleton<RemoteGrpcCoordinator>()
@@ -146,8 +144,10 @@ public sealed class Bot
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RedisImagesCache>())
.AddSingleton<IDataCache, RedisCache>();
svcs.Scan(scan => scan.FromAssemblyOf<IReadyExecutor>()
.AddClasses(classes => classes.AssignableToAny(
svcs.Scan(scan => scan
.FromAssemblyOf<IReadyExecutor>()
.AddClasses(classes => classes
.AssignableToAny(
// services
typeof(INService),
@@ -161,7 +161,8 @@ public sealed class Bot
#endif
)
.AsSelfWithInterfaces()
.WithSingletonLifetime());
.WithSingletonLifetime()
);
//initialize Services
Services = svcs.BuildServiceProvider();
@@ -169,12 +170,14 @@ public sealed class Bot
exec.Initialize();
if (Client.ShardId == 0)
{
ApplyConfigMigrations();
}
_ = LoadTypeReaders(typeof(Bot).Assembly);
sw.Stop();
Log.Information( "All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
Log.Information($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s");
}
private void ApplyConfigMigrations()
@@ -182,8 +185,10 @@ public sealed class Bot
// execute all migrators
var migrators = Services.GetServices<IConfigMigrator>();
foreach (var migrator in migrators)
{
migrator.EnsureMigrated();
}
}
private IEnumerable<object> LoadTypeReaders(Assembly assembly)
{
@@ -197,9 +202,9 @@ public sealed class Bot
Log.Warning(ex.LoaderExceptions[0], "Error getting types");
return Enumerable.Empty<object>();
}
var filteredTypes = allTypes.Where(x => x.IsSubclassOf(typeof(TypeReader))
&& x.BaseType?.GetGenericArguments().Length > 0
var filteredTypes = allTypes
.Where(x => x.IsSubclassOf(typeof(TypeReader))
&& x.BaseType.GetGenericArguments().Length > 0
&& !x.IsAbstract);
var toReturn = new List<object>();
@@ -207,8 +212,6 @@ public sealed class Bot
{
var x = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft);
var baseType = ft.BaseType;
if (baseType is null)
continue;
var typeArgs = baseType.GetGenericArguments();
_commandService.AddTypeReader(typeArgs[0], x);
toReturn.Add(x);
@@ -219,30 +222,34 @@ public sealed class Bot
private async Task LoginAsync(string token)
{
var clientReady = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var clientReady = new TaskCompletionSource<bool>();
async Task SetClientReady()
Task SetClientReady()
{
var _ = Task.Run(async () =>
{
clientReady.TrySetResult(true);
try
{
foreach (var chan in await Client.GetDMChannelsAsync())
await chan.CloseAsync();
foreach (var chan in (await Client.GetDMChannelsAsync().ConfigureAwait(false)))
{
await chan.CloseAsync().ConfigureAwait(false);
}
}
catch
{
// ignored
}
});
return Task.CompletedTask;
}
//connect
Log.Information("Shard {ShardId} logging in ...", Client.ShardId);
try
{
Client.Ready += SetClientReady;
await Client.LoginAsync(TokenType.Bot, token);
await Client.StartAsync();
await Client.LoginAsync(TokenType.Bot, token).ConfigureAwait(false);
await Client.StartAsync().ConfigureAwait(false);
}
catch (HttpException ex)
{
@@ -255,33 +262,33 @@ public sealed class Bot
Helpers.ReadErrorAndExit(4);
}
Client.Ready += SetClientReady;
await clientReady.Task.ConfigureAwait(false);
Client.Ready -= SetClientReady;
Client.JoinedGuild += Client_JoinedGuild;
Client.LeftGuild += Client_LeftGuild;
Log.Information("Shard {ShardId} logged in", Client.ShardId);
Log.Information("Shard {0} logged in.", Client.ShardId);
}
private Task Client_LeftGuild(SocketGuild arg)
{
Log.Information("Left server: {GuildName} [{GuildId}]", arg?.Name, arg?.Id);
Log.Information("Left server: {0} [{1}]", arg?.Name, arg?.Id);
return Task.CompletedTask;
}
private Task Client_JoinedGuild(SocketGuild arg)
{
Log.Information("Joined server: {GuildName} [{GuildId}]", arg.Name, arg.Id);
_ = Task.Run(async () =>
Log.Information($"Joined server: {0} [{1}]", arg.Name, arg.Id);
var _ = Task.Run(async () =>
{
GuildConfig gc;
await using (var uow = _db.GetDbContext())
using (var uow = _db.GetDbContext())
{
gc = uow.GuildConfigsForId(arg.Id, null);
gc = uow.GuildConfigsForId(arg.Id);
}
await JoinedGuild.Invoke(gc);
await JoinedGuild.Invoke(gc).ConfigureAwait(false);
});
return Task.CompletedTask;
}
@@ -290,7 +297,7 @@ public sealed class Bot
{
var sw = Stopwatch.StartNew();
await LoginAsync(_creds.Token);
await LoginAsync(_creds.Token).ConfigureAwait(false);
Mention = Client.CurrentUser.Mention;
Log.Information("Shard {ShardId} loading services...", Client.ShardId);
@@ -309,10 +316,10 @@ public sealed class Bot
var commandHandler = Services.GetRequiredService<CommandHandler>();
// start handling messages received in commandhandler
await commandHandler.StartHandling();
await commandHandler.StartHandling().ConfigureAwait(false);
await _commandService.AddModulesAsync(typeof(Bot).Assembly, Services);
// await _interactionService.AddModulesAsync(typeof(Bot).Assembly, Services);
IsReady = true;
_ = Task.Run(ExecuteReadySubscriptions);
Log.Information("Shard {ShardId} ready", Client.ShardId);
@@ -336,48 +343,23 @@ public sealed class Bot
}
});
return tasks.WhenAll();
return Task.WhenAll(tasks);
}
private Task Client_Log(LogMessage arg)
{
if (arg.Message?.Contains("unknown dispatch", StringComparison.InvariantCultureIgnoreCase) ?? false)
return Task.CompletedTask;
if (arg.Exception is { InnerException: WebSocketClosedException { CloseCode: 4014 } })
{
Log.Error(@"
Login failed.
*** Please enable privileged intents ***
Certain Nadeko features require Discord's privileged gateway intents.
These include greeting and goodbye messages, as well as creating the Owner message channels for DM forwarding.
How to enable privileged intents:
1. Head over to the Discord Developer Portal https://discord.com/developers/applications/
2. Select your Application.
3. Click on `Bot` in the left side navigation panel, and scroll down to the intents section.
4. Enable all intents.
5. Restart your bot.
Read this only if your bot is in 100 or more servers:
You'll need to apply to use the intents with Discord, but for small selfhosts, all that is required is enabling the intents in the developer portal.
Yes, this is a new thing from Discord, as of October 2020. No, there's nothing we can do about it. Yes, we're aware it worked before.
While waiting for your bot to be accepted, you can change the 'usePrivilegedIntents' inside your creds.yml to 'false', although this will break many of the nadeko's features");
}
else if (arg.Exception is not null)
Log.Warning(arg.Exception, "{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message);
if (arg.Exception != null)
Log.Warning(arg.Exception, arg.Source + " | " + arg.Message);
else
Log.Warning("{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message);
Log.Warning(arg.Source + " | " + arg.Message);
return Task.CompletedTask;
}
public async Task RunAndBlockAsync()
{
await RunAsync();
await Task.Delay(-1);
await RunAsync().ConfigureAwait(false);
await Task.Delay(-1).ConfigureAwait(false);
}
}
}

View File

@@ -1,10 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public enum AddRemove
{
Add = int.MinValue,
Remove = int.MinValue + 1,
Rem = int.MinValue + 1,
Rm = int.MinValue + 1
}

View File

@@ -1,20 +1,20 @@
#nullable disable
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory)
: base(() => Task.Run(valueFactory))
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Run(valueFactory))
{ }
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Run(taskFactory))
{ }
public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
}
public AsyncLazy(Func<Task<T>> taskFactory)
: base(() => Task.Run(taskFactory))
{
}
public TaskAwaiter<T> GetAwaiter()
=> Value.GetAwaiter();
}

View File

@@ -1,7 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
namespace NadekoBot.Common.Attributes;
using Discord.Commands;
using NadekoBot.Services;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class AliasesAttribute : AliasAttribute
{
@@ -10,3 +15,4 @@ public sealed class AliasesAttribute : AliasAttribute
{
}
}
}

View File

@@ -0,0 +1,15 @@
using Discord.Commands;
namespace Discord
{
public class BotPermAttribute : RequireBotPermissionAttribute
{
public BotPermAttribute(GuildPerm permission) : base((GuildPermission)permission)
{
}
public BotPermAttribute(ChannelPerm permission) : base((ChannelPermission)permission)
{
}
}
}

View File

@@ -1,31 +1,36 @@
using YamlDotNet.Serialization;
namespace NadekoBot.Common.Attributes;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace NadekoBot.Common.Attributes
{
public static class CommandNameLoadHelper
{
private static readonly IDeserializer _deserializer = new Deserializer();
private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
= new(() => LoadAliases());
private static YamlDotNet.Serialization.IDeserializer _deserializer
= new YamlDotNet.Serialization.Deserializer();
public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
public static Lazy<Dictionary<string, string[]>> LazyCommandAliases
= new Lazy<Dictionary<string, string[]>>(() => LoadCommandNames());
public static Dictionary<string, string[]> LoadCommandNames(string aliasesFilePath = "data/aliases.yml")
{
var text = File.ReadAllText(aliasesFilePath);
return _deserializer.Deserialize<Dictionary<string, string[]>>(text);
}
public static string[] GetAliasesFor(string methodName)
=> _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
=> LazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
? aliases.Skip(1).ToArray()
: Array.Empty<string>();
public static string GetCommandNameFor(string methodName)
{
methodName = methodName.ToLowerInvariant();
var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
var toReturn = LazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
? aliases[0]
: methodName;
return toReturn;
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Runtime.CompilerServices;
using Discord.Commands;
using NadekoBot.Services;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class DescriptionAttribute : SummaryAttribute
{
// Localization.LoadCommand(memberName.ToLowerInvariant()).Desc
public DescriptionAttribute(string text = "") : base(text)
{
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Discord.Commands
{
public class LeftoverAttribute : RemainderAttribute
{
public LeftoverAttribute()
{
}
}
}

View File

@@ -1,13 +1,19 @@
using System;
using System.Runtime.CompilerServices;
using Discord.Commands;
using NadekoBot.Services;
namespace NadekoBot.Common.Attributes;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class NadekoCommandAttribute : CommandAttribute
{
public string MethodName { get; }
public NadekoCommandAttribute([CallerMemberName] string memberName="")
: base(CommandNameLoadHelper.GetCommandNameFor(memberName))
=> MethodName = memberName.ToLowerInvariant();
{
this.MethodName = memberName.ToLowerInvariant();
}
public string MethodName { get; }
}
}

View File

@@ -1,30 +1,14 @@
using System.Runtime.CompilerServices;
namespace NadekoBot.Common.Attributes;
using System;
using Discord.Commands;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Class)]
internal sealed class NadekoModuleAttribute : GroupAttribute
sealed class NadekoModuleAttribute : GroupAttribute
{
public NadekoModuleAttribute(string moduleName)
: base(moduleName)
public NadekoModuleAttribute(string moduleName) : base(moduleName)
{
}
}
}
[AttributeUsage(AttributeTargets.Method)]
internal sealed class NadekoDescriptionAttribute : SummaryAttribute
{
public NadekoDescriptionAttribute([CallerMemberName] string name = "")
: base(name.ToLowerInvariant())
{
}
}
[AttributeUsage(AttributeTargets.Method)]
internal sealed class NadekoUsageAttribute : RemarksAttribute
{
public NadekoUsageAttribute([CallerMemberName] string name = "")
: base(name.ToLowerInvariant())
{
}
}

View File

@@ -1,10 +1,15 @@
namespace NadekoBot.Common.Attributes;
using System;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class NadekoOptionsAttribute : Attribute
{
public Type OptionType { get; set; }
public NadekoOptionsAttribute(Type t)
=> OptionType = t;
{
this.OptionType = t;
}
}
}

View File

@@ -1,19 +1,20 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Extensions;
using NadekoBot.Services;
namespace NadekoBot.Common.Attributes;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class OwnerOnlyAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services)
{
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
return Task.FromResult(creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id
? PreconditionResult.FromSuccess()
: PreconditionResult.FromError("Not owner"));
return Task.FromResult((creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Not owner")));
}
}
}

View File

@@ -1,7 +1,11 @@
using Discord.Commands;
using NadekoBot.Services;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Common.Attributes;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class RatelimitAttribute : PreconditionAttribute
{
@@ -15,10 +19,7 @@ public sealed class RatelimitAttribute : PreconditionAttribute
Seconds = seconds;
}
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
if (Seconds == 0)
return Task.FromResult(PreconditionResult.FromSuccess());
@@ -34,3 +35,4 @@ public sealed class RatelimitAttribute : PreconditionAttribute
return Task.FromResult(PreconditionResult.FromError(msgContent));
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Runtime.CompilerServices;
using Discord.Commands;
using NadekoBot.Services;
using Newtonsoft.Json;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class UsageAttribute : RemarksAttribute
{
// public static string GetUsage(string memberName)
// {
// var usage = Localization.LoadCommand(memberName.ToLowerInvariant()).Usage;
// return JsonConvert.SerializeObject(usage);
// }
public UsageAttribute(string text = "") : base(text)
{
}
}
}

View File

@@ -1,30 +1,33 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Modules.Administration.Services;
namespace Discord;
namespace Discord
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class UserPermAttribute : PreconditionAttribute
{
public RequireUserPermissionAttribute UserPermissionAttribute { get; }
[AttributeUsage(AttributeTargets.Method)]
public class UserPermAttribute : RequireUserPermissionAttribute
{
public UserPermAttribute(GuildPerm permission)
: base(permission)
{
UserPermissionAttribute = new RequireUserPermissionAttribute((GuildPermission)permission);
}
public UserPermAttribute(ChannelPerm permission)
: base(permission)
{
UserPermissionAttribute = new RequireUserPermissionAttribute((ChannelPermission)permission);
}
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
var permService = services.GetRequiredService<DiscordPermOverrideService>();
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out _))
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out var _))
return Task.FromResult(PreconditionResult.FromSuccess());
return base.CheckPermissionsAsync(context, command, services);
return UserPermissionAttribute.CheckPermissionsAsync(context, command, services);
}
}
}

View File

@@ -1,17 +1,20 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Common;
using Newtonsoft.Json;
namespace NadekoBot.Common
{
public class CmdStrings
{
public string[] Usages { get; }
public string Description { get; }
[JsonConstructor]
public CmdStrings([JsonProperty("args")] string[] usages, [JsonProperty("desc")] string description)
public CmdStrings(
[JsonProperty("args")]string[] usages,
[JsonProperty("desc")]string description
)
{
Usages = usages;
Description = description;
}
}
}

View File

@@ -1,12 +1,15 @@
#nullable disable
#pragma warning disable
// License MIT
// License MIT
// Source: https://github.com/i3arnon/ConcurrentHashSet
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace System.Collections.Generic;
namespace NadekoBot.Common.Collections
{
/// <summary>
/// Represents a thread-safe hash-based unique collection.
/// </summary>
@@ -18,19 +21,55 @@ namespace System.Collections.Generic;
[DebuggerDisplay("Count = {Count}")]
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T>
{
private const int DEFAULT_CAPACITY = 31;
private const int MAX_LOCK_NUMBER = 1024;
private const int DefaultCapacity = 31;
private const int MaxLockNumber = 1024;
private static int DefaultConcurrencyLevel
=> PlatformHelper.ProcessorCount;
private readonly IEqualityComparer<T> _comparer;
private readonly bool _growLockArray;
private int _budget;
private volatile Tables _tables;
private static int DefaultConcurrencyLevel => PlatformHelper.ProcessorCount;
/// <summary>
/// Gets the number of items contained in the <see
/// cref="ConcurrentHashSet{T}"/>.
/// </summary>
/// <value>The number of items contained in the <see
/// cref="ConcurrentHashSet{T}"/>.</value>
/// <remarks>Count has snapshot semantics and represents the number of items in the <see
/// cref="ConcurrentHashSet{T}"/>
/// at the moment when Count was accessed.</remarks>
public int Count
{
get
{
var count = 0;
var acquiredLocks = 0;
try
{
AcquireAllLocks(ref acquiredLocks);
for (var i = 0; i < _tables.CountPerLock.Length; i++)
{
count += _tables.CountPerLock[i];
}
}
finally
{
ReleaseLocks(0, acquiredLocks);
}
return count;
}
}
/// <summary>
/// Gets a value that indicates whether the <see cref="ConcurrentHashSet{T}"/> is empty.
/// </summary>
/// <value>
/// true if the <see cref="ConcurrentHashSet{T}" /> is empty; otherwise,
/// false.
/// </value>
/// <value>true if the <see cref="ConcurrentHashSet{T}"/> is empty; otherwise,
/// false.</value>
public bool IsEmpty
{
get
@@ -40,12 +79,14 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
{
AcquireAllLocks(ref acquiredLocks);
for (var i = 0; i < tables.CountPerLock.Length; i++)
for (var i = 0; i < _tables.CountPerLock.Length; i++)
{
if (_tables.CountPerLock[i] != 0)
{
if (tables.CountPerLock[i] != 0)
return false;
}
}
}
finally
{
ReleaseLocks(0, acquiredLocks);
@@ -55,92 +96,32 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
}
}
bool ICollection<T>.IsReadOnly
=> false;
/// <summary>
/// Gets the number of items contained in the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// .
/// </summary>
/// <value>
/// The number of items contained in the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// .
/// </value>
/// <remarks>
/// Count has snapshot semantics and represents the number of items in the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// at the moment when Count was accessed.
/// </remarks>
public int Count
{
get
{
var count = 0;
var acquiredLocks = 0;
try
{
AcquireAllLocks(ref acquiredLocks);
for (var i = 0; i < tables.CountPerLock.Length; i++)
count += tables.CountPerLock[i];
}
finally
{
ReleaseLocks(0, acquiredLocks);
}
return count;
}
}
private readonly IEqualityComparer<T> _comparer;
private readonly bool _growLockArray;
private int budget;
private volatile Tables tables;
/// <summary>
/// Initializes a new instance of the
/// <see
/// Initializes a new instance of the <see
/// cref="ConcurrentHashSet{T}"/>
/// class that is empty, has the default concurrency level, has the default initial capacity, and
/// uses the default comparer for the item type.
/// </summary>
public ConcurrentHashSet()
: this(DefaultConcurrencyLevel, DEFAULT_CAPACITY, true, EqualityComparer<T>.Default)
: this(DefaultConcurrencyLevel, DefaultCapacity, true, EqualityComparer<T>.Default)
{
}
/// <summary>
/// Initializes a new instance of the
/// <see
/// Initializes a new instance of the <see
/// cref="ConcurrentHashSet{T}"/>
/// class that is empty, has the specified concurrency level and capacity, and uses the default
/// comparer for the item type.
/// </summary>
/// <param name="concurrencyLevel">
/// The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}" /> concurrently.
/// </param>
/// <param name="capacity">
/// The initial number of elements that the
/// <see
/// <param name="concurrencyLevel">The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}"/> concurrently.</param>
/// <param name="capacity">The initial number of elements that the <see
/// cref="ConcurrentHashSet{T}"/>
/// can contain.
/// </param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="concurrencyLevel" /> is
/// less than 1.
/// </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="capacity" /> is less than
/// 0.
/// </exception>
/// can contain.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="concurrencyLevel"/> is
/// less than 1.</exception>
/// <exception cref="T:System.ArgumentOutOfRangeException"> <paramref name="capacity"/> is less than
/// 0.</exception>
public ConcurrentHashSet(int concurrencyLevel, int capacity)
: this(concurrencyLevel, capacity, false, EqualityComparer<T>.Default)
{
@@ -148,20 +129,14 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{T}"/>
/// class that contains elements copied from the specified
/// <see
/// cref="T:System.Collections.IEnumerable{T}" />
/// , has the default concurrency
/// class that contains elements copied from the specified <see
/// cref="T:System.Collections.IEnumerable{T}"/>, has the default concurrency
/// level, has the default initial capacity, and uses the default comparer for the item type.
/// </summary>
/// <param name="collection">
/// The
/// <see
/// cref="T:System.Collections.IEnumerable{T}" />
/// whose elements are copied to
/// <param name="collection">The <see
/// cref="T:System.Collections.IEnumerable{T}"/> whose elements are copied to
/// the new
/// <see cref="ConcurrentHashSet{T}" />.
/// </param>
/// <see cref="ConcurrentHashSet{T}"/>.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="collection"/> is a null reference.</exception>
public ConcurrentHashSet(IEnumerable<T> collection)
: this(collection, EqualityComparer<T>.Default)
@@ -173,47 +148,35 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
/// class that is empty, has the specified concurrency level and capacity, and uses the specified
/// <see cref="T:System.Collections.Generic.IEqualityComparer{T}"/>.
/// </summary>
/// <param name="comparer">
/// The <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />
/// implementation to use when comparing items.
/// </param>
/// <param name="comparer">The <see cref="T:System.Collections.Generic.IEqualityComparer{T}"/>
/// implementation to use when comparing items.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="comparer"/> is a null reference.</exception>
public ConcurrentHashSet(IEqualityComparer<T> comparer)
: this(DefaultConcurrencyLevel, DEFAULT_CAPACITY, true, comparer)
: this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{T}"/>
/// class that contains elements copied from the specified
/// <see
/// cref="T:System.Collections.IEnumerable" />
/// , has the default concurrency level, has the default
/// class that contains elements copied from the specified <see
/// cref="T:System.Collections.IEnumerable"/>, has the default concurrency level, has the default
/// initial capacity, and uses the specified
/// <see cref="T:System.Collections.Generic.IEqualityComparer{T}"/>.
/// </summary>
/// <param name="collection">
/// The
/// <see
/// cref="T:System.Collections.IEnumerable{T}" />
/// whose elements are copied to
/// <param name="collection">The <see
/// cref="T:System.Collections.IEnumerable{T}"/> whose elements are copied to
/// the new
/// <see cref="ConcurrentHashSet{T}" />.
/// </param>
/// <param name="comparer">
/// The <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />
/// implementation to use when comparing items.
/// </param>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="collection" /> is a null reference
/// <see cref="ConcurrentHashSet{T}"/>.</param>
/// <param name="comparer">The <see cref="T:System.Collections.Generic.IEqualityComparer{T}"/>
/// implementation to use when comparing items.</param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="collection"/> is a null reference
/// (Nothing in Visual Basic). -or-
/// <paramref name="comparer"/> is a null reference (Nothing in Visual Basic).
/// </exception>
public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
: this(comparer)
{
if (collection is null)
throw new ArgumentNullException(nameof(collection));
if (collection is null) throw new ArgumentNullException(nameof(collection));
InitializeFromCollection(collection);
}
@@ -225,18 +188,12 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
/// has the specified concurrency level, has the specified initial capacity, and uses the specified
/// <see cref="T:System.Collections.Generic.IEqualityComparer{T}"/>.
/// </summary>
/// <param name="concurrencyLevel">
/// The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}" /> concurrently.
/// </param>
/// <param name="collection">
/// The <see cref="T:System.Collections.IEnumerable{T}" /> whose elements are copied to the new
/// <see cref="ConcurrentHashSet{T}" />.
/// </param>
/// <param name="comparer">
/// The <see cref="T:System.Collections.Generic.IEqualityComparer{T}" /> implementation to use
/// when comparing items.
/// </param>
/// <param name="concurrencyLevel">The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}"/> concurrently.</param>
/// <param name="collection">The <see cref="T:System.Collections.IEnumerable{T}"/> whose elements are copied to the new
/// <see cref="ConcurrentHashSet{T}"/>.</param>
/// <param name="comparer">The <see cref="T:System.Collections.Generic.IEqualityComparer{T}"/> implementation to use
/// when comparing items.</param>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="collection"/> is a null reference.
/// -or-
@@ -246,12 +203,10 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
/// <paramref name="concurrencyLevel"/> is less than 1.
/// </exception>
public ConcurrentHashSet(int concurrencyLevel, IEnumerable<T> collection, IEqualityComparer<T> comparer)
: this(concurrencyLevel, DEFAULT_CAPACITY, false, comparer)
: this(concurrencyLevel, DefaultCapacity, false, comparer)
{
if (collection is null)
throw new ArgumentNullException(nameof(collection));
if (comparer is null)
throw new ArgumentNullException(nameof(comparer));
if (collection is null) throw new ArgumentNullException(nameof(collection));
if (comparer is null) throw new ArgumentNullException(nameof(comparer));
InitializeFromCollection(collection);
}
@@ -261,20 +216,13 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
/// class that is empty, has the specified concurrency level, has the specified initial capacity, and
/// uses the specified <see cref="T:System.Collections.Generic.IEqualityComparer{T}"/>.
/// </summary>
/// <param name="concurrencyLevel">
/// The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}" /> concurrently.
/// </param>
/// <param name="capacity">
/// The initial number of elements that the
/// <see
/// <param name="concurrencyLevel">The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}"/> concurrently.</param>
/// <param name="capacity">The initial number of elements that the <see
/// cref="ConcurrentHashSet{T}"/>
/// can contain.
/// </param>
/// <param name="comparer">
/// The <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />
/// implementation to use when comparing items.
/// </param>
/// can contain.</param>
/// <param name="comparer">The <see cref="T:System.Collections.Generic.IEqualityComparer{T}"/>
/// implementation to use when comparing items.</param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="concurrencyLevel"/> is less than 1. -or-
/// <paramref name="capacity"/> is less than 0.
@@ -285,35 +233,44 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
{
}
private ConcurrentHashSet(
int concurrencyLevel,
int capacity,
bool growLockArray,
IEqualityComparer<T> comparer)
private ConcurrentHashSet(int concurrencyLevel, int capacity, bool growLockArray, IEqualityComparer<T> comparer)
{
if (concurrencyLevel < 1)
throw new ArgumentOutOfRangeException(nameof(concurrencyLevel));
if (capacity < 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException(nameof(concurrencyLevel));
if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity));
// The capacity should be at least as large as the concurrency level. Otherwise, we would have locks that don't guard
// any buckets.
if (capacity < concurrencyLevel)
{
capacity = concurrencyLevel;
}
var locks = new object[concurrencyLevel];
for (var i = 0; i < locks.Length; i++)
locks[i] = new();
{
locks[i] = new object();
}
var countPerLock = new int[locks.Length];
var buckets = new Node[capacity];
tables = new(buckets, locks, countPerLock);
_tables = new Tables(buckets, locks, countPerLock);
_growLockArray = growLockArray;
budget = buckets.Length / locks.Length;
_budget = buckets.Length / locks.Length;
_comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
}
/// <summary>
/// Adds the specified item to the <see cref="ConcurrentHashSet{T}"/>.
/// </summary>
/// <param name="item">The item to add.</param>
/// <returns>true if the items was added to the <see cref="ConcurrentHashSet{T}"/>
/// successfully; false if it already exists.</returns>
/// <exception cref="T:System.OverflowException">The <see cref="ConcurrentHashSet{T}"/>
/// contains too many items.</exception>
public bool Add(T item) =>
AddInternal(item, _comparer.GetHashCode(item), true);
/// <summary>
/// Removes all items from the <see cref="ConcurrentHashSet{T}"/>.
/// </summary>
@@ -324,9 +281,9 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
{
AcquireAllLocks(ref locksAcquired);
var newTables = new Tables(new Node[DEFAULT_CAPACITY], tables.Locks, new int[tables.CountPerLock.Length]);
tables = newTables;
budget = Math.Max(1, newTables.Buckets.Length / newTables.Locks.Length);
var newTables = new Tables(new Node[DefaultCapacity], _tables.Locks, new int[_tables.CountPerLock.Length]);
_tables = newTables;
_budget = Math.Max(1, newTables.Buckets.Length / newTables.Locks.Length);
}
finally
{
@@ -342,113 +299,29 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
/// <returns>true if the <see cref="ConcurrentHashSet{T}"/> contains the item; otherwise, false.</returns>
public bool Contains(T item)
{
var hashcode = _comparer.GetHashCode(item!);
var hashcode = _comparer.GetHashCode(item);
// We must capture the _buckets field in a local variable. It is set to a new table on each table resize.
var localTables = tables;
var tables = _tables;
var bucketNo = GetBucket(hashcode, localTables.Buckets.Length);
var bucketNo = GetBucket(hashcode, tables.Buckets.Length);
// We can get away w/out a lock here.
// The Volatile.Read ensures that the load of the fields of 'n' doesn't move before the load from buckets[i].
var current = Volatile.Read(ref localTables.Buckets[bucketNo]);
var current = Volatile.Read(ref tables.Buckets[bucketNo]);
while (current is not null)
while (current != null)
{
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
{
return true;
}
current = current.Next;
}
return false;
}
void ICollection<T>.Add(T item)
=> Add(item);
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
if (array is null)
throw new ArgumentNullException(nameof(array));
if (arrayIndex < 0)
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
var locksAcquired = 0;
try
{
AcquireAllLocks(ref locksAcquired);
var count = 0;
for (var i = 0; i < tables.Locks.Length && count >= 0; i++)
count += tables.CountPerLock[i];
if (array.Length - count < arrayIndex || count < 0) //"count" itself or "count + arrayIndex" can overflow
{
throw new ArgumentException(
"The index is equal to or greater than the length of the array, or the number of elements in the set is greater than the available space from index to the end of the destination array.");
}
CopyToItems(array, arrayIndex);
}
finally
{
ReleaseLocks(0, locksAcquired);
}
}
bool ICollection<T>.Remove(T item)
=> TryRemove(item);
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <summary>
/// Returns an enumerator that iterates through the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// .
/// </summary>
/// <returns>An enumerator for the <see cref="ConcurrentHashSet{T}" />.</returns>
/// <remarks>
/// The enumerator returned from the collection is safe to use concurrently with
/// reads and writes to the collection, however it does not represent a moment-in-time snapshot
/// of the collection. The contents exposed through the enumerator may contain modifications
/// made to the collection after <see cref="GetEnumerator" /> was called.
/// </remarks>
public IEnumerator<T> GetEnumerator()
{
var buckets = tables.Buckets;
for (var i = 0; i < buckets.Length; i++)
{
// The Volatile.Read ensures that the load of the fields of 'current' doesn't move before the load from buckets[i].
var current = Volatile.Read(ref buckets[i]);
while (current is not null)
{
yield return current.Item;
current = current.Next;
}
}
}
/// <summary>
/// Adds the specified item to the <see cref="ConcurrentHashSet{T}" />.
/// </summary>
/// <param name="item">The item to add.</param>
/// <returns>
/// true if the items was added to the <see cref="ConcurrentHashSet{T}" />
/// successfully; false if it already exists.
/// </returns>
/// <exception cref="T:System.OverflowException">
/// The <see cref="ConcurrentHashSet{T}" />
/// contains too many items.
/// </exception>
public bool Add(T item)
=> AddInternal(item, _comparer.GetHashCode(item), true);
/// <summary>
/// Attempts to remove the item from the <see cref="ConcurrentHashSet{T}"/>.
/// </summary>
@@ -459,38 +332,38 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
var hashcode = _comparer.GetHashCode(item);
while (true)
{
var localTables = tables;
var tables = _tables;
GetBucketAndLockNo(hashcode,
out var bucketNo,
out var lockNo,
localTables.Buckets.Length,
localTables.Locks.Length);
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length);
lock (localTables.Locks[lockNo])
lock (tables.Locks[lockNo])
{
// If the table just got resized, we may not be holding the right lock, and must retry.
// This should be a rare occurrence.
if (localTables != tables)
if (tables != _tables)
{
continue;
}
Node previous = null;
for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
for (var current = tables.Buckets[bucketNo]; current != null; current = current.Next)
{
Debug.Assert((previous is null && current == localTables.Buckets[bucketNo])
|| previous!.Next == current);
Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current);
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
{
if (previous is null)
Volatile.Write(ref localTables.Buckets[bucketNo], current.Next);
{
Volatile.Write(ref tables.Buckets[bucketNo], current.Next);
}
else
{
previous.Next = current.Next;
localTables.CountPerLock[lockNo]--;
return true;
}
tables.CountPerLock[lockNo]--;
return true;
}
previous = current;
}
}
@@ -499,55 +372,121 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>Returns an enumerator that iterates through the <see
/// cref="ConcurrentHashSet{T}"/>.</summary>
/// <returns>An enumerator for the <see cref="ConcurrentHashSet{T}"/>.</returns>
/// <remarks>
/// The enumerator returned from the collection is safe to use concurrently with
/// reads and writes to the collection, however it does not represent a moment-in-time snapshot
/// of the collection. The contents exposed through the enumerator may contain modifications
/// made to the collection after <see cref="GetEnumerator"/> was called.
/// </remarks>
public IEnumerator<T> GetEnumerator()
{
var buckets = _tables.Buckets;
for (var i = 0; i < buckets.Length; i++)
{
// The Volatile.Read ensures that the load of the fields of 'current' doesn't move before the load from buckets[i].
var current = Volatile.Read(ref buckets[i]);
while (current != null)
{
yield return current.Item;
current = current.Next;
}
}
}
void ICollection<T>.Add(T item) => Add(item);
bool ICollection<T>.IsReadOnly => false;
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
if (array is null) throw new ArgumentNullException(nameof(array));
if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex));
var locksAcquired = 0;
try
{
AcquireAllLocks(ref locksAcquired);
var count = 0;
for (var i = 0; i < _tables.Locks.Length && count >= 0; i++)
{
count += _tables.CountPerLock[i];
}
if (array.Length - count < arrayIndex || count < 0) //"count" itself or "count + arrayIndex" can overflow
{
throw new ArgumentException("The index is equal to or greater than the length of the array, or the number of elements in the set is greater than the available space from index to the end of the destination array.");
}
CopyToItems(array, arrayIndex);
}
finally
{
ReleaseLocks(0, locksAcquired);
}
}
bool ICollection<T>.Remove(T item) => TryRemove(item);
private void InitializeFromCollection(IEnumerable<T> collection)
{
foreach (var item in collection)
{
AddInternal(item, _comparer.GetHashCode(item), false);
}
if (budget == 0)
budget = tables.Buckets.Length / tables.Locks.Length;
if (_budget == 0)
{
_budget = _tables.Buckets.Length / _tables.Locks.Length;
}
}
private bool AddInternal(T item, int hashcode, bool acquireLock)
{
while (true)
{
var localTables = tables;
GetBucketAndLockNo(hashcode,
out var bucketNo,
out var lockNo,
localTables.Buckets.Length,
localTables.Locks.Length);
var tables = _tables;
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length);
var resizeDesired = false;
var lockTaken = false;
try
{
if (acquireLock)
Monitor.Enter(localTables.Locks[lockNo], ref lockTaken);
Monitor.Enter(tables.Locks[lockNo], ref lockTaken);
// If the table just got resized, we may not be holding the right lock, and must retry.
// This should be a rare occurrence.
if (localTables != tables)
if (tables != _tables)
{
continue;
}
// Try to find this item in the bucket
Node previous = null;
for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
for (var current = tables.Buckets[bucketNo]; current != null; current = current.Next)
{
Debug.Assert((previous is null && current == localTables.Buckets[bucketNo])
|| previous!.Next == current);
Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current);
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
{
return false;
}
previous = current;
}
// The item was not found in the bucket. Insert the new item.
Volatile.Write(ref localTables.Buckets[bucketNo], new(item, hashcode, localTables.Buckets[bucketNo]));
Volatile.Write(ref tables.Buckets[bucketNo], new Node(item, hashcode, tables.Buckets[bucketNo]));
checked
{
localTables.CountPerLock[lockNo]++;
tables.CountPerLock[lockNo]++;
}
//
@@ -555,13 +494,15 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
// It is also possible that GrowTable will increase the budget but won't resize the bucket table.
// That happens if the bucket table is found to be poorly utilized due to a bad hash function.
//
if (localTables.CountPerLock[lockNo] > budget)
if (tables.CountPerLock[lockNo] > _budget)
{
resizeDesired = true;
}
}
finally
{
if (lockTaken)
Monitor.Exit(localTables.Locks[lockNo]);
Monitor.Exit(tables.Locks[lockNo]);
}
//
@@ -573,7 +514,9 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
// and then verify that the table we passed to it as the argument is still the current table.
//
if (resizeDesired)
GrowTable(localTables);
{
GrowTable(tables);
}
return true;
}
@@ -586,12 +529,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
return bucketNo;
}
private static void GetBucketAndLockNo(
int hashcode,
out int bucketNo,
out int lockNo,
int bucketCount,
int lockCount)
private static void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount)
{
bucketNo = (hashcode & 0x7fffffff) % bucketCount;
lockNo = bucketNo % lockCount;
@@ -600,7 +538,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
Debug.Assert(lockNo >= 0 && lockNo < lockCount);
}
private void GrowTable(Tables localTables)
private void GrowTable(Tables tables)
{
const int maxArrayLength = 0X7FEFFFFF;
var locksAcquired = 0;
@@ -610,26 +548,31 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
AcquireLocks(0, 1, ref locksAcquired);
// Make sure nobody resized the table while we were waiting for lock 0:
if (localTables != tables)
if (tables != _tables)
{
// We assume that since the table reference is different, it was already resized (or the budget
// was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons,
// we will have to revisit this logic.
return;
}
// Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow.
long approxCount = 0;
for (var i = 0; i < localTables.CountPerLock.Length; i++)
approxCount += localTables.CountPerLock[i];
for (var i = 0; i < tables.CountPerLock.Length; i++)
{
approxCount += tables.CountPerLock[i];
}
//
// If the bucket array is too empty, double the budget instead of resizing the table
//
if (approxCount < localTables.Buckets.Length / 4)
if (approxCount < tables.Buckets.Length / 4)
{
budget = 2 * budget;
if (budget < 0)
budget = int.MaxValue;
_budget = 2 * _budget;
if (_budget < 0)
{
_budget = int.MaxValue;
}
return;
}
@@ -642,19 +585,23 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
checked
{
// Double the size of the buckets table and add one, so that we have an odd integer.
newLength = (localTables.Buckets.Length * 2) + 1;
newLength = tables.Buckets.Length * 2 + 1;
// Now, we only need to check odd integers, and find the first that is not divisible
// by 3, 5 or 7.
while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0)
{
newLength += 2;
}
Debug.Assert(newLength % 2 != 0);
if (newLength > maxArrayLength)
{
maximizeTableSize = true;
}
}
}
catch (OverflowException)
{
maximizeTableSize = true;
@@ -669,40 +616,38 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
//
// (There is one special case that would allow GrowTable() to be called in the future:
// calling Clear() on the ConcurrentHashSet will shrink the table and lower the budget.)
budget = int.MaxValue;
_budget = int.MaxValue;
}
// Now acquire all other locks for the table
AcquireLocks(1, localTables.Locks.Length, ref locksAcquired);
AcquireLocks(1, tables.Locks.Length, ref locksAcquired);
var newLocks = localTables.Locks;
var newLocks = tables.Locks;
// Add more locks
if (_growLockArray && localTables.Locks.Length < MAX_LOCK_NUMBER)
if (_growLockArray && tables.Locks.Length < MaxLockNumber)
{
newLocks = new object[localTables.Locks.Length * 2];
Array.Copy(localTables.Locks, 0, newLocks, 0, localTables.Locks.Length);
for (var i = localTables.Locks.Length; i < newLocks.Length; i++)
newLocks[i] = new();
newLocks = new object[tables.Locks.Length * 2];
Array.Copy(tables.Locks, 0, newLocks, 0, tables.Locks.Length);
for (var i = tables.Locks.Length; i < newLocks.Length; i++)
{
newLocks[i] = new object();
}
}
var newBuckets = new Node[newLength];
var newCountPerLock = new int[newLocks.Length];
// Copy all data into a new table, creating new nodes for all elements
for (var i = 0; i < localTables.Buckets.Length; i++)
for (var i = 0; i < tables.Buckets.Length; i++)
{
var current = localTables.Buckets[i];
while (current is not null)
var current = tables.Buckets[i];
while (current != null)
{
var next = current.Next;
GetBucketAndLockNo(current.Hashcode,
out var newBucketNo,
out var newLockNo,
newBuckets.Length,
newLocks.Length);
GetBucketAndLockNo(current.Hashcode, out var newBucketNo, out var newLockNo, newBuckets.Length, newLocks.Length);
newBuckets[newBucketNo] = new(current.Item, current.Hashcode, newBuckets[newBucketNo]);
newBuckets[newBucketNo] = new Node(current.Item, current.Hashcode, newBuckets[newBucketNo]);
checked
{
@@ -714,10 +659,10 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
}
// Adjust the budget
budget = Math.Max(1, newBuckets.Length / newLocks.Length);
_budget = Math.Max(1, newBuckets.Length / newLocks.Length);
// Replace tables with the new versions
tables = new(newBuckets, newLocks, newCountPerLock);
_tables = new Tables(newBuckets, newLocks, newCountPerLock);
}
finally
{
@@ -732,10 +677,9 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
var removed = 0;
foreach (var elem in elems)
{
if (TryRemove(elem))
if (this.TryRemove(elem))
removed++;
}
return removed;
}
@@ -746,14 +690,14 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
// Now that we have lock 0, the _locks array will not change (i.e., grow),
// and so we can safely read _locks.Length.
AcquireLocks(1, tables.Locks.Length, ref locksAcquired);
Debug.Assert(locksAcquired == tables.Locks.Length);
AcquireLocks(1, _tables.Locks.Length, ref locksAcquired);
Debug.Assert(locksAcquired == _tables.Locks.Length);
}
private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired)
{
Debug.Assert(fromInclusive <= toExclusive);
var locks = tables.Locks;
var locks = _tables.Locks;
for (var i = fromInclusive; i < toExclusive; i++)
{
@@ -765,29 +709,35 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
finally
{
if (lockTaken)
{
locksAcquired++;
}
}
}
}
private void ReleaseLocks(int fromInclusive, int toExclusive)
{
Debug.Assert(fromInclusive <= toExclusive);
for (var i = fromInclusive; i < toExclusive; i++)
Monitor.Exit(tables.Locks[i]);
{
Monitor.Exit(_tables.Locks[i]);
}
}
private void CopyToItems(T[] array, int index)
{
var buckets = tables.Buckets;
var buckets = _tables.Buckets;
for (var i = 0; i < buckets.Length; i++)
for (var current = buckets[i]; current is not null; current = current.Next)
{
for (var current = buckets[i]; current != null; current = current.Next)
{
array[index] = current.Item;
index++; //this should never flow, CopyToItems is only called when there's no overflow risk
}
}
}
private sealed class Tables
{
@@ -806,8 +756,8 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
private sealed class Node
{
public readonly int Hashcode;
public readonly T Item;
public readonly int Hashcode;
public volatile Node Next;
@@ -819,3 +769,4 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
}
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace NadekoBot.Common.Collections
{
public static class DisposableReadOnlyListExtensions
{
public static IDisposableReadOnlyList<T> AsDisposable<T>(this IReadOnlyList<T> arr) where T : IDisposable
=> new DisposableReadOnlyList<T>(arr);
public static IDisposableReadOnlyList<KeyValuePair<TKey, TValue>> AsDisposable<TKey, TValue>(this IReadOnlyList<KeyValuePair<TKey, TValue>> arr) where TValue : IDisposable
=> new DisposableReadOnlyList<TKey, TValue>(arr);
}
public interface IDisposableReadOnlyList<T> : IReadOnlyList<T>, IDisposable
{
}
public sealed class DisposableReadOnlyList<T> : IDisposableReadOnlyList<T>
where T : IDisposable
{
private readonly IReadOnlyList<T> _arr;
public int Count => _arr.Count;
public T this[int index] => _arr[index];
public DisposableReadOnlyList(IReadOnlyList<T> arr)
{
this._arr = arr;
}
public IEnumerator<T> GetEnumerator()
=> _arr.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> _arr.GetEnumerator();
public void Dispose()
{
foreach (var item in _arr)
{
item.Dispose();
}
}
}
public sealed class DisposableReadOnlyList<T, U> : IDisposableReadOnlyList<KeyValuePair<T, U>>
where U : IDisposable
{
private readonly IReadOnlyList<KeyValuePair<T, U>> _arr;
public int Count => _arr.Count;
KeyValuePair<T, U> IReadOnlyList<KeyValuePair<T, U>>.this[int index] => _arr[index];
public DisposableReadOnlyList(IReadOnlyList<KeyValuePair<T, U>> arr)
{
this._arr = arr;
}
public IEnumerator<KeyValuePair<T, U>> GetEnumerator() =>
_arr.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
_arr.GetEnumerator();
public void Dispose()
{
foreach (var item in _arr)
{
item.Value.Dispose();
}
}
}
}

View File

@@ -1,37 +1,23 @@
#nullable disable
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NadekoBot.Services.Database.Models;
using System.Collections;
namespace NadekoBot.Common.Collections;
public class IndexedCollection<T> : IList<T>
where T : class, IIndexed
namespace NadekoBot.Common.Collections
{
public class IndexedCollection<T> : IList<T> where T : class, IIndexed
{
public List<T> Source { get; }
private readonly object _locker = new object();
public int Count
=> Source.Count;
public bool IsReadOnly
=> false;
public virtual T this[int index]
{
get => Source[index];
set
{
lock (_locker)
{
value.Index = index;
Source[index] = value;
}
}
}
private readonly object _locker = new();
public int Count => Source.Count;
public bool IsReadOnly => false;
public int IndexOf(T item) => item.Index;
public IndexedCollection()
=> Source = new();
{
Source = new List<T>();
}
public IndexedCollection(IEnumerable<T> source)
{
@@ -42,19 +28,31 @@ public class IndexedCollection<T> : IList<T>
}
}
public int IndexOf(T item)
=> item?.Index ?? -1;
public void UpdateIndexes()
{
lock (_locker)
{
for (var i = 0; i < Source.Count; i++)
{
if (Source[i].Index != i)
Source[i].Index = i;
}
}
}
public IEnumerator<T> GetEnumerator()
=> Source.GetEnumerator();
public static implicit operator List<T>(IndexedCollection<T> x) =>
x.Source;
IEnumerator IEnumerable.GetEnumerator()
=> Source.GetEnumerator();
public List<T> ToList() => Source.ToList();
public IEnumerator<T> GetEnumerator() =>
Source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
Source.GetEnumerator();
public void Add(T item)
{
ArgumentNullException.ThrowIfNull(item);
lock (_locker)
{
item.Index = Source.Count;
@@ -88,21 +86,19 @@ public class IndexedCollection<T> : IList<T>
public virtual bool Remove(T item)
{
bool removed;
lock (_locker)
{
if (Source.Remove(item))
if (removed = Source.Remove(item))
{
for (var i = 0; i < Source.Count; i++)
for (int i = 0; i < Source.Count; i++)
{
if (Source[i].Index != i)
Source[i].Index = i;
}
return true;
}
}
return false;
return removed;
}
public virtual void Insert(int index, T item)
@@ -110,36 +106,36 @@ public class IndexedCollection<T> : IList<T>
lock (_locker)
{
Source.Insert(index, item);
for (var i = index; i < Source.Count; i++)
for (int i = index; i < Source.Count; i++)
{
Source[i].Index = i;
}
}
}
public virtual void RemoveAt(int index)
{
lock (_locker)
{
Source.RemoveAt(index);
for (var i = index; i < Source.Count; i++)
for (int i = index; i < Source.Count; i++)
{
Source[i].Index = i;
}
}
}
public void UpdateIndexes()
public virtual T this[int index]
{
get { return Source[index]; }
set
{
lock (_locker)
{
for (var i = 0; i < Source.Count; i++)
{
if (Source[i].Index != i)
Source[i].Index = i;
value.Index = index;
Source[index] = value;
}
}
}
}
public static implicit operator List<T>(IndexedCollection<T> x)
=> x.Source;
public List<T> ToList()
=> Source.ToList();
}

View File

@@ -1,9 +1,9 @@
#nullable disable
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
public class CommandData
{
public string Cmd { get; set; }
public string Desc { get; set; }
public string[] Usage { get; set; }
}
}

View File

@@ -1,13 +1,13 @@
#nullable disable
using System.Collections.Generic;
using System.Globalization;
using Cloneable;
using NadekoBot.Common.Yml;
using SixLabors.ImageSharp.PixelFormats;
using System.Globalization;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
namespace NadekoBot.Common.Configs;
namespace NadekoBot.Common.Configs
{
[Cloneable]
public sealed partial class BotConfig : ICloneable<BotConfig>
{
@@ -39,8 +39,7 @@ Allowed values: Simple, Normal, None")]
[Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")]
public bool ForwardMessages { get; set; }
[Comment(
@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
[Comment(@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
or all owners? (this might cause the bot to lag if there's a lot of owners specified)")]
public bool ForwardToAllOwners { get; set; }
@@ -58,7 +57,6 @@ Leave empty to reply with DmHelpText to every DM.")]
[Comment(@"This is the response for the .h command")]
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
public string HelpText { get; set; }
[Comment(@"List of modules and commands completely blocked on the bot")]
public BlockedConfig Blocked { get; set; }
@@ -77,15 +75,28 @@ note: This setting is primarily used if you're afraid of raids, or you're runnin
public bool GroupGreets { get; set; }
[Comment(@"Whether the bot will rotate through all specified statuses.
This setting can be changed via .ropl command.
This setting can be changed via .rots command.
See RotatingStatuses submodule in Administration.")]
public bool RotateStatuses { get; set; }
// [Comment(@"Whether the prefix will be a suffix, or prefix.
// For example, if your prefix is ! you will run a command called 'cash' by typing either
// '!cash @Someone' if your prefixIsSuffix: false or
// 'cash @Someone!' if your prefixIsSuffix: true")]
// public bool PrefixIsSuffix { get; set; }
// public string Prefixed(string text) => PrefixIsSuffix
// ? text + Prefix
// : Prefix + text;
public string Prefixed(string text)
=> Prefix + text;
public BotConfig()
{
var color = new ColorConfig();
Color = color;
DefaultLocale = new("en-US");
DefaultLocale = new CultureInfo("en-US");
ConsoleOutputType = ConsoleOutputType.Normal;
ForwardMessages = false;
ForwardToAllOwners = false;
@@ -120,7 +131,7 @@ See RotatingStatuses submodule in Administration.")]
Prefix = ".";
RotateStatuses = false;
GroupGreets = false;
DmHelpTextKeywords = new()
DmHelpTextKeywords = new List<string>()
{
"help",
"commands",
@@ -129,19 +140,6 @@ See RotatingStatuses submodule in Administration.")]
"can you do"
};
}
// [Comment(@"Whether the prefix will be a suffix, or prefix.
// For example, if your prefix is ! you will run a command called 'cash' by typing either
// '!cash @Someone' if your prefixIsSuffix: false or
// 'cash @Someone!' if your prefixIsSuffix: true")]
// public bool PrefixIsSuffix { get; set; }
// public string Prefixed(string text) => PrefixIsSuffix
// ? text + Prefix
// : Prefix + text;
public string Prefixed(string text)
=> Prefix + text;
}
[Cloneable]
@@ -152,8 +150,8 @@ public sealed partial class BlockedConfig
public BlockedConfig()
{
Modules = new();
Commands = new();
Modules = new HashSet<string>();
Commands = new HashSet<string>();
}
}
@@ -181,5 +179,6 @@ public enum ConsoleOutputType
{
Normal = 0,
Simple = 1,
None = 2
None = 2,
}
}

View File

@@ -1,5 +1,5 @@
namespace NadekoBot.Common.Configs;
namespace NadekoBot.Common.Configs
{
/// <summary>
/// Base interface for available config serializers
/// </summary>
@@ -8,11 +8,11 @@ public interface IConfigSeria
/// <summary>
/// Serialize the object to string
/// </summary>
public string Serialize<T>(T obj)
where T : notnull;
public string Serialize<T>(T obj);
/// <summary>
/// Deserialize string data into an object of the specified type
/// </summary>
public T Deserialize<T>(string data);
}
}

View File

@@ -1,10 +1,36 @@
#nullable disable
using System.Collections.Generic;
using NadekoBot.Common.Yml;
using YamlDotNet.Serialization;
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
public sealed class Creds : IBotCredentials
{
public Creds()
{
Version = 1;
Token = string.Empty;
OwnerIds = new List<ulong>();
TotalShards = 1;
GoogleApiKey = string.Empty;
Votes = new(string.Empty, string.Empty, string.Empty, string.Empty);
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
BotListToken = string.Empty;
CleverbotApiKey = string.Empty;
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
Db = new()
{
Type = "sqlite",
ConnectionString = "Data Source=data/NadekoBot.db"
};
CoordinatorUrl = "http://localhost:3442";
RestartCommand = new()
{
};
}
[Comment(@"DO NOT CHANGE")]
public int Version { get; set; }
@@ -15,15 +41,11 @@ public sealed class Creds : IBotCredentials
**DO NOT ADD PEOPLE YOU DON'T TRUST**")]
public ICollection<ulong> OwnerIds { get; set; }
[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.
Leave at 1 if you don't know what you're doing.")]
public int TotalShards { get; set; }
[Comment(
@"Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
[Comment(@"Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
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; }
@@ -51,8 +73,7 @@ go to https://www.patreon.com/portal -> my clients -> create client")]
Change only if you've changed the coordinator address or port.")]
public string CoordinatorUrl { get; set; }
[Comment(
@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
[Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
public string RapidApiKey { get; set; }
[Comment(@"https://locationiq.com api key (register and you will receive the token in the email).
@@ -67,23 +88,9 @@ Used only for .time command")]
Used for cryptocurrency related commands.")]
public string CoinmarketcapApiKey { get; set; }
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
// Used for stocks related commands.")]
// public string PolygonIoApiKey { get; set; }
[Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")]
public string OsuApiKey { get; set; }
[Comment(@"Optional Trovo client id.
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.")]
public string TrovoClientId { get; set; }
[Comment(@"Obtain by creating an application at https://dev.twitch.tv/console/apps")]
public string TwitchClientId { get; set; }
[Comment(@"Obtain by creating an application at https://dev.twitch.tv/console/apps")]
public string TwitchClientSecret { get; set; }
[Comment(@"Command and args which will be used to restart the bot.
Only used if bot is executed directly (NOT through the coordinator)
placeholders:
@@ -97,40 +104,16 @@ Windows default
args: {0}")]
public RestartConfig RestartCommand { get; set; }
public Creds()
{
Version = 4;
Token = string.Empty;
UsePrivilegedIntents = true;
OwnerIds = new List<ulong>();
TotalShards = 1;
GoogleApiKey = string.Empty;
Votes = new(string.Empty, string.Empty, string.Empty, string.Empty);
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
BotListToken = string.Empty;
CleverbotApiKey = string.Empty;
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
Db = new()
{
Type = "sqlite",
ConnectionString = "Data Source=data/NadekoBot.db"
};
CoordinatorUrl = "http://localhost:3442";
RestartCommand = new();
}
public class DbOptions
{
[Comment(@"Database type. Only sqlite supported atm")]
public string Type { get; set; }
[Comment(@"Connection string. Will default to ""Data Source=data/NadekoBot.db""")]
public string ConnectionString { get; set; }
}
// todo fixup patreon
public sealed record PatreonSettings
{
public string ClientId { get; set; }
@@ -138,15 +121,10 @@ Windows default
public string RefreshToken { get; set; }
public string ClientSecret { get; set; }
[Comment(
@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")]
[Comment(@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")]
public string CampaignId { get; set; }
public PatreonSettings(
string accessToken,
string refreshToken,
string clientSecret,
string campaignId)
public PatreonSettings(string accessToken, string refreshToken, string clientSecret, string campaignId)
{
AccessToken = accessToken;
RefreshToken = refreshToken;
@@ -156,6 +134,7 @@ Windows default
public PatreonSettings()
{
}
}
@@ -181,13 +160,10 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
public VotesSettings()
{
}
public VotesSettings(
string topggServiceUrl,
string topggKey,
string discordsServiceUrl,
string discordsKey)
public VotesSettings(string topggServiceUrl, string topggKey, string discordsServiceUrl, string discordsKey)
{
TopggServiceUrl = topggServiceUrl;
TopggKey = topggKey;
@@ -195,4 +171,49 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
DiscordsKey = discordsKey;
}
}
public class Old
{
public string Token { get; set; } = string.Empty;
public ulong[] OwnerIds { get; set; } = new ulong[1];
public string LoLApiKey { get; set; } = string.Empty;
public string GoogleApiKey { get; set; } = string.Empty;
public string MashapeKey { get; set; } = string.Empty;
public string OsuApiKey { get; set; } = string.Empty;
public string SoundCloudClientId { get; set; } = string.Empty;
public string CleverbotApiKey { get; set; } = string.Empty;
public string CarbonKey { get; set; } = string.Empty;
public int TotalShards { get; set; } = 1;
public string PatreonAccessToken { get; set; } = string.Empty;
public string PatreonCampaignId { get; set; } = "334038";
public RestartConfig RestartCommand { get; set; } = null;
public string ShardRunCommand { get; set; } = string.Empty;
public string ShardRunArguments { get; set; } = string.Empty;
public int? ShardRunPort { get; set; } = null;
public string MiningProxyUrl { get; set; } = string.Empty;
public string MiningProxyCreds { get; set; } = string.Empty;
public string BotListToken { get; set; } = string.Empty;
public string TwitchClientId { get; set; } = string.Empty;
public string VotesToken { get; set; } = string.Empty;
public string VotesUrl { get; set; } = string.Empty;
public string RedisOptions { get; set; } = string.Empty;
public string LocationIqApiKey { get; set; } = string.Empty;
public string TimezoneDbApiKey { get; set; } = string.Empty;
public string CoinmarketcapApiKey { get; set; } = string.Empty;
public class RestartConfig
{
public RestartConfig(string cmd, string args)
{
this.Cmd = cmd;
this.Args = args;
}
public string Cmd { get; set; }
public string Args { get; set; }
}
}
}
}

View File

@@ -1,10 +1,16 @@
#nullable disable
namespace NadekoBot.Common;
using NadekoBot.Services;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Discord;
namespace NadekoBot.Common
{
public class DownloadTracker : INService
{
private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new();
private readonly SemaphoreSlim _downloadUsersSemaphore = new(1, 1);
private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new ConcurrentDictionary<ulong, DateTime>();
private SemaphoreSlim downloadUsersSemaphore = new SemaphoreSlim(1, 1);
/// <summary>
/// Ensures all users on the specified guild were downloaded within the last hour.
@@ -16,15 +22,16 @@ public class DownloadTracker : INService
#if GLOBAL_NADEKO
return;
#endif
await _downloadUsersSemaphore.WaitAsync();
await downloadUsersSemaphore.WaitAsync();
try
{
var now = DateTime.UtcNow;
// download once per hour at most
var added = LastDownloads.AddOrUpdate(guild.Id,
var added = LastDownloads.AddOrUpdate(
guild.Id,
now,
(_, old) => now - old > TimeSpan.FromHours(1) ? now : old);
(key, old) => (now - old) > TimeSpan.FromHours(1) ? now : old);
// means that this entry was just added - download the users
if (added == now)
@@ -32,7 +39,8 @@ public class DownloadTracker : INService
}
finally
{
_downloadUsersSemaphore.Release();
downloadUsersSemaphore.Release();
}
}
}
}

View File

@@ -0,0 +1,11 @@
using Discord;
using NadekoBot.Common;
namespace NadekoBot.Extensions
{
public static class BotCredentialsExtensions
{
public static bool IsOwner(this IBotCredentials creds, IUser user)
=> creds.OwnerIds.Contains(user.Id);
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Common;
using NadekoBot.Modules.Music;
using NadekoBot.Services;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Music.Resolvers;
using NadekoBot.Modules.Music.Services;
using StackExchange.Redis;
namespace NadekoBot.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBotStringsServices(this IServiceCollection services, int totalShards)
=> totalShards <= 1
? services
.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>()
: services.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, RedisBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>();
public static IServiceCollection AddConfigServices(this IServiceCollection services)
{
var baseType = typeof(ConfigServiceBase<>);
foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed))
{
if (type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == baseType)
{
services.AddSingleton(type);
services.AddSingleton(x => (IConfigService)x.GetRequiredService(type));
}
}
return services;
}
public static IServiceCollection AddConfigMigrators(this IServiceCollection services)
=> services.AddSealedSubclassesOf(typeof(IConfigMigrator));
public static IServiceCollection AddMusic(this IServiceCollection services)
=> services
.AddSingleton<IMusicService, MusicService>()
.AddSingleton<ITrackResolveProvider, TrackResolveProvider>()
.AddSingleton<IYoutubeResolver, YtdlYoutubeResolver>()
.AddSingleton<ISoundcloudResolver, SoundcloudResolver>()
.AddSingleton<ILocalTrackResolver, LocalTrackResolver>()
.AddSingleton<IRadioResolver, RadioResolver>()
.AddSingleton<ITrackCacher, RedisTrackCacher>()
.AddSingleton<YtLoader>()
.AddSingleton<IPlaceholderProvider>(svc => svc.GetRequiredService<IMusicService>());
// consider using scrutor, because slightly different versions
// of this might be needed in several different places
public static IServiceCollection AddSealedSubclassesOf(this IServiceCollection services, Type baseType)
{
var subTypes = Assembly.GetCallingAssembly()
.ExportedTypes
.Where(type => type.IsSealed && baseType.IsAssignableFrom(type));
foreach (var subType in subTypes)
{
services.AddSingleton(baseType, subType);
}
return services;
}
public static IServiceCollection AddRedis(this IServiceCollection services, string redisOptions)
{
var conf = ConfigurationOptions.Parse(redisOptions);
services.AddSingleton(ConnectionMultiplexer.Connect(conf));
return services;
}
}
}

View File

@@ -0,0 +1,255 @@
using System;
namespace Discord
{
// just a copy paste from discord.net in order to rename it, for compatibility iwth v3 which is gonna use custom lib
// Summary:
// Defines the available permissions for a channel.
[Flags]
public enum GuildPerm : ulong
{
//
// Summary:
// Allows creation of instant invites.
CreateInstantInvite = 1,
//
// Summary:
// Allows kicking members.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
KickMembers = 2,
//
// Summary:
// Allows banning members.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
BanMembers = 4,
//
// Summary:
// Allows all permissions and bypasses channel permission overwrites.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
Administrator = 8,
//
// Summary:
// Allows management and editing of channels.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
ManageChannels = 16,
//
// Summary:
// Allows management and editing of the guild.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
ManageGuild = 32,
//
// Summary:
// Allows for the addition of reactions to messages.
AddReactions = 64,
//
// Summary:
// Allows for viewing of audit logs.
ViewAuditLog = 128,
PrioritySpeaker = 256,
ReadMessages = 1024,
ViewChannel = 1024,
SendMessages = 2048,
//
// Summary:
// Allows for sending of text-to-speech messages.
SendTTSMessages = 4096,
//
// Summary:
// Allows for deletion of other users messages.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
ManageMessages = 8192,
//
// Summary:
// Allows links sent by users with this permission will be auto-embedded.
EmbedLinks = 16384,
//
// Summary:
// Allows for uploading images and files.
AttachFiles = 32768,
//
// Summary:
// Allows for reading of message history.
ReadMessageHistory = 65536,
//
// Summary:
// Allows for using the @everyone tag to notify all users in a channel, and the
// @here tag to notify all online users in a channel.
MentionEveryone = 131072,
//
// Summary:
// Allows the usage of custom emojis from other servers.
UseExternalEmojis = 262144,
//
// Summary:
// Allows for joining of a voice channel.
Connect = 1048576,
//
// Summary:
// Allows for speaking in a voice channel.
Speak = 2097152,
//
// Summary:
// Allows for muting members in a voice channel.
MuteMembers = 4194304,
//
// Summary:
// Allows for deafening of members in a voice channel.
DeafenMembers = 8388608,
//
// Summary:
// Allows for moving of members between voice channels.
MoveMembers = 16777216,
//
// Summary:
// Allows for using voice-activity-detection in a voice channel.
UseVAD = 33554432,
//
// Summary:
// Allows for modification of own nickname.
ChangeNickname = 67108864,
//
// Summary:
// Allows for modification of other users nicknames.
ManageNicknames = 134217728,
//
// Summary:
// Allows management and editing of roles.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
ManageRoles = 268435456,
//
// Summary:
// Allows management and editing of webhooks.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
ManageWebhooks = 536870912,
//
// Summary:
// Allows management and editing of emojis.
//
// Remarks:
// This permission requires the owner account to use two-factor authentication when
// used on a guild that has server-wide 2FA enabled.
ManageEmojis = 1073741824
}
//
// Summary:
// Defines the available permissions for a channel.
[Flags]
public enum ChannelPerm : ulong
{
//
// Summary:
// Allows creation of instant invites.
CreateInstantInvite = 1,
//
// Summary:
// Allows management and editing of channels.
ManageChannel = 16,
//
// Summary:
// Allows for the addition of reactions to messages.
AddReactions = 64,
PrioritySpeaker = 256,
//
// Summary:
// Allows for reading of messages. This flag is obsolete, use Discord.ChannelPermission.ViewChannel
// instead.
ReadMessages = 1024,
//
// Summary:
// Allows guild members to view a channel, which includes reading messages in text
// channels.
ViewChannel = 1024,
//
// Summary:
// Allows for sending messages in a channel.
SendMessages = 2048,
//
// Summary:
// Allows for sending of text-to-speech messages.
SendTTSMessages = 4096,
//
// Summary:
// Allows for deletion of other users messages.
ManageMessages = 8192,
//
// Summary:
// Allows links sent by users with this permission will be auto-embedded.
EmbedLinks = 16384,
//
// Summary:
// Allows for uploading images and files.
AttachFiles = 32768,
//
// Summary:
// Allows for reading of message history.
ReadMessageHistory = 65536,
//
// Summary:
// Allows for using the @everyone tag to notify all users in a channel, and the
// @here tag to notify all online users in a channel.
MentionEveryone = 131072,
//
// Summary:
// Allows the usage of custom emojis from other servers.
UseExternalEmojis = 262144,
//
// Summary:
// Allows for joining of a voice channel.
Connect = 1048576,
//
// Summary:
// Allows for speaking in a voice channel.
Speak = 2097152,
//
// Summary:
// Allows for muting members in a voice channel.
MuteMembers = 4194304,
//
// Summary:
// Allows for deafening of members in a voice channel.
DeafenMembers = 8388608,
//
// Summary:
// Allows for moving of members between voice channels.
MoveMembers = 16777216,
//
// Summary:
// Allows for using voice-activity-detection in a voice channel.
UseVAD = 33554432,
//
// Summary:
// Allows management and editing of roles.
ManageRoles = 268435456,
//
// Summary:
// Allows management and editing of webhooks.
ManageWebhooks = 536870912
}
}

View File

@@ -1,6 +1,7 @@
#nullable disable
namespace NadekoBot.Common;
using System;
namespace NadekoBot.Common
{
public static class Helpers
{
public static void ReadErrorAndExit(int exitCode)
@@ -11,3 +12,4 @@ public static class Helpers
Environment.Exit(exitCode);
}
}
}

View File

@@ -1,12 +1,16 @@
#nullable disable
namespace NadekoBot;
using System.Collections.Generic;
using Discord;
using System.Collections.Immutable;
using System.Linq;
using NadekoBot.Common;
namespace NadekoBot
{
public interface IBotCredentials
{
string Token { get; }
string GoogleApiKey { get; }
ICollection<ulong> OwnerIds { get; }
bool UsePrivilegedIntents { get; }
string RapidApiKey { get; }
Creds.DbOptions Db { get; }
@@ -21,10 +25,7 @@ public interface IBotCredentials
string LocationIqApiKey { get; }
string TimezoneDbApiKey { get; }
string CoinmarketcapApiKey { get; }
string TrovoClientId { get; }
string CoordinatorUrl { get; set; }
string TwitchClientId { get; set; }
string TwitchClientSecret { get; set; }
}
public class RestartConfig
@@ -32,3 +33,4 @@ public class RestartConfig
public string Cmd { get; set; }
public string Args { get; set; }
}
}

View File

@@ -1,8 +1,7 @@
#nullable disable
namespace NadekoBot.Common;
public interface ICloneable<T>
where T : new()
namespace NadekoBot.Common
{
public interface ICloneable<T> where T : new()
{
public T Clone();
}
}

View File

@@ -1,6 +1,7 @@
#nullable disable
namespace NadekoBot;
using Discord;
namespace NadekoBot
{
public interface IEmbedBuilder
{
IEmbedBuilder WithDescription(string desc);
@@ -19,5 +20,6 @@ public enum EmbedColor
{
Ok,
Pending,
Error
Error,
}
}

View File

@@ -1,31 +0,0 @@
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Common;
public interface ILogCommandService
{
void AddDeleteIgnore(ulong xId);
Task LogServer(ulong guildId, ulong channelId, bool actionValue);
bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType);
LogSetting? GetGuildLogSettings(ulong guildId);
bool Log(ulong guildId, ulong? channelId, LogType type);
}
public enum LogType
{
Other,
MessageUpdated,
MessageDeleted,
UserJoined,
UserLeft,
UserBanned,
UserUnbanned,
UserUpdated,
ChannelCreated,
ChannelDestroyed,
ChannelUpdated,
UserPresence,
VoicePresence,
VoicePresenceTts,
UserMuted
}

View File

@@ -1,7 +1,7 @@
#nullable disable
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
public interface INadekoCommandOptions
{
void NormalizeOptions();
}
}

View File

@@ -1,7 +1,10 @@
#nullable disable
namespace NadekoBot.Common;
using System;
using System.Collections.Generic;
namespace NadekoBot.Common
{
public interface IPlaceholderProvider
{
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders();
}
}

View File

@@ -1,8 +1,8 @@
#nullable disable
using System;
using NadekoBot.Common.Yml;
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
public class ImageUrls
{
[Comment("DO NOT CHANGE")]
@@ -47,3 +47,4 @@ public class ImageUrls
public Uri Bg { get; set; }
}
}
}

View File

@@ -1,14 +0,0 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common.JsonConverters;
public class CultureInfoConverter : JsonConverter<CultureInfo>
{
public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> new(reader.GetString() ?? "en-US");
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.Name);
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot.Common.JsonConverters
{
public class Rgba32Converter : JsonConverter<Rgba32>
{
public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return Rgba32.ParseHex(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToHex());
}
}
public class CultureInfoConverter : JsonConverter<CultureInfo>
{
public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return new CultureInfo(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.Name);
}
}
}

View File

@@ -1,14 +0,0 @@
using SixLabors.ImageSharp.PixelFormats;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common.JsonConverters;
public class Rgba32Converter : JsonConverter<Rgba32>
{
public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> Rgba32.ParseHex(reader.GetString());
public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToHex());
}

View File

@@ -1,17 +1,14 @@
#nullable disable
using System;
using System.Runtime.CompilerServices;
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
// needs proper invalid input check (character array input out of range)
// needs negative number support
// ReSharper disable once InconsistentNaming
#pragma warning disable IDE1006
public readonly struct kwum : IEquatable<kwum>
#pragma warning restore IDE1006
{
private const string VALID_CHARACTERS = "23456789abcdefghijkmnpqrstuvwxyz";
private readonly int _value;
private const string ValidCharacters = "23456789abcdefghijkmnpqrstuvwxyz";
public kwum(int num)
=> _value = num;
@@ -24,8 +21,12 @@ public readonly struct kwum : IEquatable<kwum>
_value = InternalCharToValue(c);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int InternalCharToValue(in char c)
=> ValidCharacters.IndexOf(c);
public kwum(in ReadOnlySpan<char> input)
{
{;
_value = 0;
for (var index = 0; index < input.Length; index++)
{
@@ -33,29 +34,23 @@ public readonly struct kwum : IEquatable<kwum>
if (!IsValidChar(c))
throw new ArgumentException("All characters need to be a valid kwum characters.", nameof(input));
_value += VALID_CHARACTERS.IndexOf(c) * (int)Math.Pow(VALID_CHARACTERS.Length, input.Length - index - 1);
_value += ValidCharacters.IndexOf(c) * (int)Math.Pow(ValidCharacters.Length, input.Length - index - 1);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int InternalCharToValue(in char c)
=> VALID_CHARACTERS.IndexOf(c);
public static bool TryParse(in ReadOnlySpan<char> input, out kwum value)
{
value = default;
foreach(var c in input)
{
if (!IsValidChar(c))
return false;
}
value = new(input);
value = new kwum(input);
return true;
}
public static kwum operator +(kwum left, kwum right)
=> new(left._value + right._value);
=> new kwum(left._value + right._value);
public static bool operator ==(kwum left, kwum right)
=> left._value == right._value;
@@ -68,26 +63,25 @@ public readonly struct kwum : IEquatable<kwum>
public static implicit operator int(kwum kwum)
=> kwum._value;
public static implicit operator kwum(int num)
=> new(num);
=> new kwum(num);
public static bool IsValidChar(char c)
=> VALID_CHARACTERS.Contains(c);
=> ValidCharacters.Contains(c);
public override string ToString()
{
var count = VALID_CHARACTERS.Length;
var count = ValidCharacters.Length;
var localValue = _value;
var arrSize = (int)Math.Log(localValue, count) + 1;
Span<char> chars = new char[arrSize];
while (localValue > 0)
{
localValue = Math.DivRem(localValue, count, out var rem);
chars[--arrSize] = VALID_CHARACTERS[rem];
chars[--arrSize] = ValidCharacters[(int)rem];
}
return new(chars);
return new string(chars);
}
public override bool Equals(object obj)
@@ -97,5 +91,8 @@ public readonly struct kwum : IEquatable<kwum>
=> other == this;
public override int GetHashCode()
=> _value.GetHashCode();
{
return _value.GetHashCode();
}
}
}

View File

@@ -1,14 +1,14 @@
#nullable disable
using CommandLine;
namespace NadekoBot.Common;
using CommandLine;
namespace NadekoBot.Common
{
public class LbOpts : INadekoCommandOptions
{
[Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")]
public bool Clean { get; set; }
public void NormalizeOptions()
{
}
}
}

View File

@@ -1,14 +1,18 @@
#nullable disable
using System;
using System.Net;
using System.Runtime.CompilerServices;
using Discord.Net;
using Serilog;
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
public class LoginErrorHandler
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Handle(Exception ex)
=> Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord");
{
Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Handle(HttpException ex)
@@ -16,20 +20,20 @@ public class LoginErrorHandler
switch (ex.HttpCode)
{
case HttpStatusCode.Unauthorized:
Log.Error("Your bot token is wrong.\n"
+ "You can find the bot token under the Bot tab in the developer page.\n"
+ "Fix your token in the credentials file and restart the bot");
Log.Error("Your bot token is wrong.\n" +
"You can find the bot token under the Bot tab in the developer page.\n" +
"Fix your token in the credentials file and restart the bot");
break;
case HttpStatusCode.BadRequest:
Log.Error("Something has been incorrectly formatted in your credentials file.\n"
+ "Use the JSON Guide as reference to fix it and restart the bot");
Log.Error("Something has been incorrectly formatted in your credentials file.\n" +
"Use the JSON Guide as reference to fix it and restart the bot.");
Log.Error("If you are on Linux, make sure Redis is installed and running");
break;
case HttpStatusCode.RequestTimeout:
Log.Error("The request timed out. Make sure you have no external program blocking the bot "
+ "from connecting to the internet");
Log.Error("The request timed out. Make sure you have no external program blocking the bot " +
"from connecting to the internet");
break;
case HttpStatusCode.ServiceUnavailable:
@@ -38,8 +42,8 @@ public class LoginErrorHandler
break;
case HttpStatusCode.TooManyRequests:
Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n"
+ "Global ratelimits usually last for an hour");
Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n" +
"Global ratelimits usually last for an hour");
break;
default:
@@ -47,6 +51,7 @@ public class LoginErrorHandler
break;
}
Log.Fatal(ex, "Fatal error occurred while loading credentials");
Log.Fatal(ex.ToString());
}
}
}

View File

@@ -1,5 +1,8 @@
namespace NadekoBot.Common.ModuleBehaviors;
using System.Threading.Tasks;
using Discord;
namespace NadekoBot.Common.ModuleBehaviors
{
/// <summary>
/// Implemented by modules which block execution before anything is executed
/// </summary>
@@ -8,3 +11,4 @@ public interface IEarlyBehavior
int Priority { get; }
Task<bool> RunBehavior(IGuild guild, IUserMessage msg);
}
}

View File

@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Discord;
namespace NadekoBot.Common.ModuleBehaviors
{
public interface IInputTransformer
{
Task<string> TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input);
}
}

View File

@@ -1,10 +0,0 @@
namespace NadekoBot.Common.ModuleBehaviors;
public interface IInputTransformer
{
Task<string> TransformInput(
IGuild guild,
IMessageChannel channel,
IUser user,
string input);
}

View File

@@ -1,8 +1,13 @@
namespace NadekoBot.Common.ModuleBehaviors;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.WebSocket;
namespace NadekoBot.Common.ModuleBehaviors
{
public interface ILateBlocker
{
public int Priority { get; }
Task<bool> TryBlockLate(ICommandContext context, string moduleName, CommandInfo command);
}
}

View File

@@ -1,5 +1,8 @@
namespace NadekoBot.Common.ModuleBehaviors;
using System.Threading.Tasks;
using Discord;
namespace NadekoBot.Common.ModuleBehaviors
{
/// <summary>
/// Last thing to be executed, won't stop further executions
/// </summary>
@@ -7,3 +10,4 @@ public interface ILateExecutor
{
Task LateExecute(IGuild guild, IUserMessage msg);
}
}

View File

@@ -0,0 +1,34 @@
using Discord;
using Discord.WebSocket;
using System.Threading.Tasks;
namespace NadekoBot.Common.ModuleBehaviors
{
public struct ModuleBehaviorResult
{
public bool Blocked { get; set; }
public string NewInput { get; set; }
public static ModuleBehaviorResult None() => new ModuleBehaviorResult
{
Blocked = false,
NewInput = null,
};
public static ModuleBehaviorResult FromBlocked(bool blocked) => new ModuleBehaviorResult
{
Blocked = blocked,
NewInput = null,
};
}
public interface IModuleBehavior
{
/// <summary>
/// Negative priority means it will try to apply as early as possible
/// Positive priority menas it will try to apply as late as possible
/// </summary>
int Priority { get; }
Task<ModuleBehaviorResult> ApplyBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg);
}
}

View File

@@ -1,5 +1,7 @@
namespace NadekoBot.Common.ModuleBehaviors;
using System.Threading.Tasks;
namespace NadekoBot.Common.ModuleBehaviors
{
/// <summary>
/// All services which need to execute something after
/// the bot is ready should implement this interface
@@ -11,3 +13,4 @@ public interface IReadyExecutor
/// </summary>
public Task OnReadyAsync();
}
}

View File

@@ -1,53 +1,47 @@
#nullable disable
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Services;
using NadekoBot.Extensions;
using System.Globalization;
using System.Threading.Tasks;
// ReSharper disable InconsistentNaming
namespace NadekoBot.Modules;
[UsedImplicitly(ImplicitUseTargetFlags.Default
| ImplicitUseTargetFlags.WithInheritors
| ImplicitUseTargetFlags.WithMembers)]
namespace NadekoBot.Modules
{
public abstract class NadekoModule : ModuleBase
{
protected CultureInfo Culture { get; set; }
// Injected by Discord.net
protected CultureInfo _cultureInfo { get; set; }
public IBotStrings Strings { get; set; }
public CommandHandler _cmdHandler { get; set; }
public ILocalization _localization { get; set; }
public CommandHandler CmdHandler { get; set; }
public ILocalization Localization { get; set; }
public IEmbedBuilderService _eb { get; set; }
protected string prefix
=> _cmdHandler.GetPrefix(ctx.Guild);
public string Prefix => CmdHandler.GetPrefix(ctx.Guild);
protected ICommandContext ctx
=> Context;
protected ICommandContext ctx => Context;
protected override void BeforeExecute(CommandInfo command)
=> Culture = _localization.GetCultureInfo(ctx.Guild?.Id);
protected NadekoModule()
{
}
protected string GetText(in LocStr data)
=> Strings.GetText(data, Culture);
protected override void BeforeExecute(CommandInfo cmd)
{
_cultureInfo = Localization.GetCultureInfo(ctx.Guild?.Id);
}
protected string GetText(in LocStr data) =>
Strings.GetText(data, _cultureInfo);
public Task<IUserMessage> SendErrorAsync(string error)
=> ctx.Channel.SendErrorAsync(_eb, error);
public Task<IUserMessage> SendErrorAsync(
string title,
string error,
string url = null,
string footer = null)
public Task<IUserMessage> SendErrorAsync(string title, string error, string url = null, string footer = null)
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
public Task<IUserMessage> SendConfirmAsync(string text)
=> ctx.Channel.SendConfirmAsync(_eb, text);
public Task<IUserMessage> SendConfirmAsync(
string title,
string text,
string url = null,
string footer = null)
public Task<IUserMessage> SendConfirmAsync(string title, string text, string url = null, string footer = null)
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
public Task<IUserMessage> SendPendingAsync(string text)
@@ -73,22 +67,26 @@ public abstract class NadekoModule : ModuleBase
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
{
embed.WithPendingColor().WithFooter("yes/no");
embed
.WithPendingColor()
.WithFooter("yes/no");
var msg = await ctx.Channel.EmbedAsync(embed);
var msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
try
{
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false);
input = input?.ToUpperInvariant();
if (input != "YES" && input != "Y")
{
return false;
}
return true;
}
finally
{
_ = Task.Run(() => msg.DeleteAsync());
var _ = Task.Run(() => msg.DeleteAsync());
}
}
@@ -101,10 +99,12 @@ public abstract class NadekoModule : ModuleBase
{
dsc.MessageReceived += MessageReceived;
if (await Task.WhenAny(userInputTask.Task, Task.Delay(10000)) != userInputTask.Task)
if ((await Task.WhenAny(userInputTask.Task, Task.Delay(10000)).ConfigureAwait(false)) != userInputTask.Task)
{
return null;
}
return await userInputTask.Task;
return await userInputTask.Task.ConfigureAwait(false);
}
finally
{
@@ -113,17 +113,20 @@ public abstract class NadekoModule : ModuleBase
Task MessageReceived(SocketMessage arg)
{
_ = Task.Run(() =>
var _ = Task.Run(() =>
{
if (!(arg is SocketUserMessage userMsg) ||
!(userMsg.Channel is ITextChannel chan) ||
userMsg.Author.Id != userId ||
userMsg.Channel.Id != channelId)
{
if (arg is not SocketUserMessage userMsg
|| userMsg.Channel is not ITextChannel
|| userMsg.Author.Id != userId
|| userMsg.Channel.Id != channelId)
return Task.CompletedTask;
}
if (userInputTask.TrySetResult(arg.Content))
{
userMsg.DeleteAfter(1);
}
return Task.CompletedTask;
});
return Task.CompletedTask;
@@ -134,4 +137,21 @@ public abstract class NadekoModule : ModuleBase
public abstract class NadekoModule<TService> : NadekoModule
{
public TService _service { get; set; }
protected NadekoModule() : base()
{
}
}
public abstract class NadekoSubmodule : NadekoModule
{
protected NadekoSubmodule() : base() { }
}
public abstract class NadekoSubmodule<TService> : NadekoModule<TService>
{
protected NadekoSubmodule() : base()
{
}
}
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Modules
{
public static class NadekoModuleExtensions
{
}
}

View File

@@ -1,14 +1,16 @@
#nullable disable
using System;
using System.Security.Cryptography;
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
public class NadekoRandom : Random
{
private readonly RandomNumberGenerator _rng;
readonly RandomNumberGenerator _rng;
public NadekoRandom()
=> _rng = RandomNumberGenerator.Create();
public NadekoRandom() : base()
{
_rng = RandomNumberGenerator.Create();
}
public override int Next()
{
@@ -35,7 +37,7 @@ public class NadekoRandom : Random
var bytes = new byte[sizeof(int)];
_rng.GetBytes(bytes);
var sign = Math.Sign(BitConverter.ToInt32(bytes, 0));
return (sign * BitConverter.ToInt32(bytes, 0) % (maxValue - minValue)) + minValue;
return (sign * BitConverter.ToInt32(bytes, 0)) % (maxValue - minValue) + minValue;
}
public long NextLong(long minValue, long maxValue)
@@ -47,17 +49,19 @@ public class NadekoRandom : Random
var bytes = new byte[sizeof(long)];
_rng.GetBytes(bytes);
var sign = Math.Sign(BitConverter.ToInt64(bytes, 0));
return (sign * BitConverter.ToInt64(bytes, 0) % (maxValue - minValue)) + minValue;
return (sign * BitConverter.ToInt64(bytes, 0)) % (maxValue - minValue) + minValue;
}
public override void NextBytes(byte[] buffer)
=> _rng.GetBytes(buffer);
{
_rng.GetBytes(buffer);
}
protected override double Sample()
{
var bytes = new byte[sizeof(double)];
_rng.GetBytes(bytes);
return Math.Abs((BitConverter.ToDouble(bytes, 0) / double.MaxValue) + 1);
return Math.Abs(BitConverter.ToDouble(bytes, 0) / double.MaxValue + 1);
}
public override double NextDouble()
@@ -67,3 +71,4 @@ public class NadekoRandom : Random
return BitConverter.ToDouble(bytes, 0);
}
}
}

View File

@@ -1,16 +1,13 @@
#nullable disable
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Common;
using System;
using System.Threading.Tasks;
using Discord.Commands;
namespace NadekoBot.Common
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
public sealed class NoPublicBotAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
#if GLOBAL_NADEKO
return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://nadekobot.readthedocs.io/en/latest/)."));
@@ -19,3 +16,4 @@ public sealed class NoPublicBotAttribute : PreconditionAttribute
#endif
}
}
}

View File

@@ -1,46 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public class OldCreds
{
public string Token { get; set; } = string.Empty;
public ulong[] OwnerIds { get; set; } = new ulong[1];
public string LoLApiKey { get; set; } = string.Empty;
public string GoogleApiKey { get; set; } = string.Empty;
public string MashapeKey { get; set; } = string.Empty;
public string OsuApiKey { get; set; } = string.Empty;
public string SoundCloudClientId { get; set; } = string.Empty;
public string CleverbotApiKey { get; set; } = string.Empty;
public string CarbonKey { get; set; } = string.Empty;
public int TotalShards { get; set; } = 1;
public string PatreonAccessToken { get; set; } = string.Empty;
public string PatreonCampaignId { get; set; } = "334038";
public RestartConfig RestartCommand { get; set; }
public string ShardRunCommand { get; set; } = string.Empty;
public string ShardRunArguments { get; set; } = string.Empty;
public int? ShardRunPort { get; set; }
public string MiningProxyUrl { get; set; } = string.Empty;
public string MiningProxyCreds { get; set; } = string.Empty;
public string BotListToken { get; set; } = string.Empty;
public string TwitchClientId { get; set; } = string.Empty;
public string VotesToken { get; set; } = string.Empty;
public string VotesUrl { get; set; } = string.Empty;
public string RedisOptions { get; set; } = string.Empty;
public string LocationIqApiKey { get; set; } = string.Empty;
public string TimezoneDbApiKey { get; set; } = string.Empty;
public string CoinmarketcapApiKey { get; set; } = string.Empty;
public class RestartConfig
{
public string Cmd { get; set; }
public string Args { get; set; }
public RestartConfig(string cmd, string args)
{
Cmd = cmd;
Args = args;
}
}
}

View File

@@ -1,6 +1,7 @@
#nullable disable
namespace NadekoBot.Common;
using System;
namespace NadekoBot.Common
{
public class OldImageUrls
{
public int Version { get; set; } = 2;
@@ -45,3 +46,4 @@ public class OldImageUrls
public Uri Bg { get; set; }
}
}
}

View File

@@ -1,24 +1,24 @@
#nullable disable
using CommandLine;
namespace NadekoBot.Common;
using CommandLine;
namespace NadekoBot.Common
{
public static class OptionsParser
{
public static T ParseFrom<T>(string[] args)
where T : INadekoCommandOptions, new()
public static T ParseFrom<T>(string[] args) where T : INadekoCommandOptions, new()
=> ParseFrom(new T(), args).Item1;
public static (T, bool) ParseFrom<T>(T options, string[] args)
where T : INadekoCommandOptions
public static (T, bool) ParseFrom<T>(T options, string[] args) where T : INadekoCommandOptions
{
using var p = new Parser(x =>
using (var p = new Parser(x =>
{
x.HelpWriter = null;
});
}))
{
var res = p.ParseArguments<T>(args);
var output = res.MapResult(x => x, _ => options);
output.NormalizeOptions();
return (output, res.Tag == ParserResultType.Parsed);
options = res.MapResult(x => x, x => options);
options.NormalizeOptions();
return (options, res.Tag == ParserResultType.Parsed);
}
}
}
}

View File

@@ -1,9 +1,9 @@
#nullable disable
namespace NadekoBot.Common;
namespace NadekoBot.Common
{
public class OsuMapData
{
public string Title { get; set; }
public string Artist { get; set; }
public string Version { get; set; }
}
}

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