1
Fork 0
mirror of https://gitlab.com/Kwoth/nadekobot.git synced 2024-10-02 12:09:07 +00:00

Killed history

This commit is contained in:
Kwoth 2021-09-06 21:29:22 +02:00
commit 7aca29ae8a
950 changed files with 366651 additions and 0 deletions

365
.gitignore vendored Normal file
View file

@ -0,0 +1,365 @@
#Manually added files
patreon_rewards.json
command_errors*.txt
_output/
src/NadekoBot/Command Errors*.txt
src/NadekoBot/credentials.json
# these 2 are used for migrations
NadekoBot.Core/credentials.json
NadekoBot.Core/credentials_example.json
src/NadekoBot/data/NadekoBot.db
src/NadekoBot/data/musicdata
# Created by https://www.gitignore.io/api/visualstudio,visualstudiocode,windows,linux,macos
### VisualStudioCode ###
.vscode/
.vscode/*
# !.vscode/settings.json
# !.vscode/tasks.json
# !.vscode/launch.json
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/
### VisualStudio Patch ###
build/
site/

29
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,29 @@
# taken from https://gitlab.com/forrestab/dotnet-gitlab-ci/blob/master/.gitlab-ci.yml
image: mcr.microsoft.com/dotnet/core/sdk:3.1
stages:
- build
# - test
variables:
project: "NadekoBot"
before_script:
- "dotnet restore"
build:
stage: build
variables:
build_path: "src/$project"
script:
- "cd $build_path"
- "dotnet build -c Release"
# test:
# image: mcr.microsoft.com/dotnet/core/sdk:2.1
# stage: test
# variables:
# tests_path: "Nadeko.Tests"
# script:
# - "cd $tests_path"
# - "dotnet test"

View file

@ -0,0 +1,26 @@
### Description
Write here a summary of the issue you're having.
### Version
- Write here whether you're using public Nadeko or hosting one yourself.
- If you are hosting, write down:
- The bot version (run the command .stats on Discord).
- Your operating system and its version.
- If you are on Windows, tell us whether you're using the updater version or the source version.
- If you are on Linux or OSX, tell us if you're hosting with tmux or pm2 or any other solution for managing processes.
### Reproduction Steps
- Describe, in detail, the steps necessary to consistently reproduce the issue.
- Preferably write the entire procedure in step-by-step instructions.
### Expected Behavior
Write here the behavior you were expecting to get from the bot.
### Actual Behavior
Write here the behavior you actually got from the bot.
### Screenshots
Include here any relevant screenshot that illustrates the issue you're having or that might help pinpoint the cause of the bug.
### Notes
Write here anything else you want to say that wasn't covered on the previous topics.

View file

@ -0,0 +1,2 @@
GitLab is for bug reports only.
Please, head over to https://nadeko.bot/suggest to make feature requests.

View file

@ -0,0 +1,2 @@
GitLab is for bug reports only.
Please, head over to our support server at https://invite.nadeko.bot/ and ask your question in the #help channel.

View file

@ -0,0 +1,19 @@
### Description
Write here a summary of the change(s) you're proposing and why this merge request would be a necessary or a nice addition to the project.
### Changes Proposed
Describe, item by item, all changes you'd like to propose. Write them in a list, one proposition per line. For example:
- Adds `DoStuff()` method to service X.
- Changes `SomeMethod()` on service Y, so it can handle situation Z better.
- Added a try/catch *somewhere*, so an exception is not thrown on the console when *something* happens.
- Replaced `AMethod()` by `AnotherMethod()` in *some command* for performance reasons.
### Details
Elaborate on the major and minor changes you've made to the source code. Try to explain why you've done something in a certain way.
### Screenshots
If applicable, send us screenshots of the result of your changes.
### Notes
Write here additional considerations that weren't covered on the previous topics.

171
CHANGELOG.md Normal file
View file

@ -0,0 +1,171 @@
# Changelog
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [2.46.0] - 17.06.2021
### Added
- Added some nsfw commands
### Changed
- `.aar` reworked. Now supports multiple roles, up to 3.
- Toggle roles that are added to newly joined users with `.aar RoleName`
- Use `.aar` to list roles which will be added
- Roles which are deleted are automatically cleaned up from `.aar`
- `.inrole` now also shows user ids
- Blacklist commands (owner only) `.ubl` `.sbl` and `.cbl` will now list blacklisted items when no argument (or a page number) is provided
- `.cmdcd` now works with customreactions too
- `.xprr` usage changed. It now takes add/rm parameter to add/remove a role ex. You can only take or remove a single role, adding and removing a role at the same level doesn't work (yet?)
- example: `.xprr 5 add Member` or `.xprr 1 rm Newbie`
## [2.45.2] - 14.06.2021
### Added
- Added `.duckduckgo / .ddg` search
### Changed
- `.invlist` shows expire time and is slightly prettier
### Fixed
- `.antialt` will be properly cleaned up when the bot leaves the server
## [2.45.1] - 12.06.2021
### Added
- Added many new aliases to custom reaction commands in the format ex + "action" to prepare for the future rename from CustomReactions to Expressions
- You can now `.divorce` via username#discrim even if the user no longer exists
### Changed
- DmHelpText should now have %prefix% and %bot.prefix% placeholders available
- Added squares which show enabled features for each cr in `.lcr`
- Changed CustomReactions' IDs to show, and accept base 32 unambigous characters instead of the normal database IDs (this will result in much shorter cr IDs in case you have a lot of them)
- Improved `.lcr` helptext to explain what's shown in the output
- `.rolecolor <color> <role>` changed to take color, then the role, to make it easier to set color for roles with multiple words without mentioning the role
- `.acmdcds` alias chanaged to `.cmdcds`
- `.8ball` will now cache results for a day
- `.chatmute` and `.voicemute` now support timed mutes
### Fixed
- Fixed `.config <conf> <prop>` exceeding embed field character limit
## [2.45.0] - 10.06.2021
### Added
- Added `.crsexport` and `.crsimport`
- Allows for quick export/import of server or global custom reactions
- Requires admin permissions for server crs, and owner for global crs
- Explanation of the fields is in the comment at the top of the `.crsexport` .yml file
- Added `.mquality` / `.musicquality` - Set encoding quality. Has 4 presets - Low, Medium, High, Highest. Default is Highest
- Added `.xprewsreset` which resets all currently set xp level up rewards
- Added `.purgeuser @User` which will remove the specified from the database completely. Removed settings include: Xp, clubs, waifu, currency, etc...
- Added `.config xp txt.per_image` and xpFromImage to xp.yml - Change this config to allow xp gain from posting images. Images must be 128x128 or greater in size
- Added `.take <amount> <role>` to complement `.award <amount> role`
- Added **Fans** list to `.waifuinfo` which shows how many people have their affinity set to you
- Added `.antialt` which will punish any user whose account is younger than specified threshold
### Changed
- `.warne` with no args will now show current state
- .inrole` will now lists users with no roles if no role is provided
- Music suttering fixed on some systems
- `.say` moved to utility module
- Re-created GuildRepeaters table and renamed to Repeaters
- confirmation prompts will now use pending color from bot config, instead of okcolor
- `.mute` can now have up to 49 days mute to match .warnp
- `.warnlog` now has proper pagination (with reactions) and checking your own warnings past page 1 works correctly now with `.warnlog 2`
### Fixed
- obsolete_use string fixed
- Fixed `.crreact`
## [2.44.4] - 06.06.2021
### Added
- Re-added `%music.playing%` and `%music.queued%` (#290)
- Added `%music.servers%` which shows how many servers have a song queued up to play
^ Only available to `.ropl` / `.adpl` feature atm
- `.autodc` re-added
- `.qrp`, `.vol`, `.smch` `.autodc` will now persist
### Changed
- Using `.commands` / `.cmds` without a module will now list modules
- `.qrp` / `.queuerepeat` will now accept one of 3 values
- `none` - don't repeat queue
- `track` - repeat single track
- `queue` (or ommit) - repeat entire queue
- your old `.defvol` and `.smch` settings will be reset
### Fixed
- Fixed `.google` / `.g` command
- Removing last song in the queue will no longer reset queue index
- Having `.rpl` disabled will now correctly stop after the last song, closes #292
### Removed
- `.sad` removed. It's more or less useless. Use `.qrp` and `.autodc` now for similar effect
### Obsolete
- `.rcs` is obsolete, use `.qrp s` or `.qrp song`
- `.defvol` is obsolete, use `.vol`
## [2.44.3] - 04.06.2021
### Changed
- Minor perf improvement for filter checks
### Fixed
- `.qs` result urls are now valid
- Custom reactions with "`-`" as a response should once again disable that custom reaction completely
- Fixed `.acrm` out of range string
- Fixed `.sclist` and `.aclist` not showing correct indexes past page 1
## [2.44.2] - 02.06.2021
### Added
- Music related commands reimplemented with custom code, **considered alpha state**
- Song and playlist caching (faster song queue after first time)
- Much faster starting and skipping once the songs are in the queue
- Higher quality audio (no stuttering too!)
- Local tracks will now have durations if you have ffprobe installed (comes with ffmpeg)
- Bot supports joining a different vc without skipping the song if you use `.j`
- ⚠️ **DO NOT DRAG THE BOT** to another vc, as it's not properly supported atm, and you will have to do `.play` after dragging it)
- `.j` makes the bot join your voice channel
- `.p` is now alias of play, pause is `.pause`
- `.qs` should work without google api key now for most users as it is using a custom loader
- Added `.clubs` alias for `.clublb`
### Changed
- `.ms` no longer takes `>` between arguments (`.ms 1 5` now, was `.ms 1>5` before)
- FlowerShop renamed to Shop
### Fixed
- Fixed decay bug giving everyone 1 flower every 24h
- Fixed feeds which have rss media items without a type
- Fixed `.acrm` index not working
- Fixed and error reply when a waifu item doesn't exist
- Disabled colored console on windows as they were causing issues for some users
- Fixed/Updated some strings and several minor bugfixes
### Removed
- Removed admin requirement on `.scrm` as it didn't make sense
- Some Music commands are removed because of the complexity they bring in with little value (if you *really* want them back, you can open an issue and specify your *good* reason)

View file

@ -0,0 +1,129 @@
using NUnit.Framework;
using System.Globalization;
using System.Linq;
using System.Reflection;
using AngleSharp.Common;
using Discord.Commands;
using NadekoBot.Common.Attributes;
using NadekoBot.Core.Services;
using NadekoBot.Modules;
using YamlDotNet.Serialization;
namespace Nadeko.Tests
{
public class CommandStringsTests
{
private const string responsesPath = "../../../../src/NadekoBot/data/strings/responses";
private const string commandsPath = "../../../../src/NadekoBot/data/strings/commands";
private const string aliasesPath = "../../../../src/NadekoBot/data/aliases.yml";
[Test]
public void AllCommandNamesHaveStrings()
{
var stringsSource = new LocalFileStringsSource(
responsesPath,
commandsPath);
var strings = new LocalBotStringsProvider(stringsSource);
var culture = new CultureInfo("en-US");
var isSuccess = true;
foreach (var entry in CommandNameLoadHelper.LoadCommandNames(aliasesPath))
{
var commandName = entry.Value[0];
var cmdStrings = strings.GetCommandStrings(culture.Name, commandName);
if (cmdStrings is null)
{
isSuccess = false;
TestContext.Out.WriteLine($"{commandName} doesn't exist in commands.en-US.yml");
}
}
Assert.IsTrue(isSuccess);
}
private static string[] GetCommandMethodNames()
=> typeof(NadekoBot.NadekoBot).Assembly
.GetExportedTypes()
.Where(type => type.IsClass && !type.IsAbstract)
.Where(type => typeof(NadekoModule).IsAssignableFrom(type) // if its a top level module
|| !(type.GetCustomAttribute<GroupAttribute>(true) is null)) // or a submodule
.SelectMany(x => x.GetMethods()
.Where(mi => mi.CustomAttributes
.Any(ca => ca.AttributeType == typeof(NadekoCommandAttribute))))
.Select(x => x.Name.ToLowerInvariant())
.ToArray();
[Test]
public void AllCommandMethodsHaveNames()
{
var allAliases = CommandNameLoadHelper.LoadCommandNames(
aliasesPath);
var methodNames = GetCommandMethodNames();
var isSuccess = true;
foreach (var methodName in methodNames)
{
if (!allAliases.TryGetValue(methodName, out var _))
{
TestContext.Error.WriteLine($"{methodName} is missing an alias.");
isSuccess = false;
}
}
Assert.IsTrue(isSuccess);
}
[Test]
public void NoObsoleteAliases()
{
var allAliases = CommandNameLoadHelper.LoadCommandNames(aliasesPath);
var methodNames = GetCommandMethodNames()
.ToHashSet();
var isSuccess = true;
foreach (var item in allAliases)
{
var methodName = item.Key;
if (!methodNames.Contains(methodName))
{
TestContext.WriteLine($"'{methodName}' from aliases.yml doesn't have a matching command method.");
isSuccess = false;
}
}
Assert.IsTrue(isSuccess);
}
// [Test]
// public void NoObsoleteCommandStrings()
// {
// var stringsSource = new LocalFileStringsSource(responsesPath, commandsPath);
//
// var culture = new CultureInfo("en-US");
//
// var isSuccess = true;
// var allCommandNames = CommandNameLoadHelper.LoadCommandNames(aliasesPath);
// var enUsCommandNames = allCommandNames
// .Select(x => x.Value[0]) // first alias is command name
// .ToHashSet();
// foreach (var entry in stringsSource.GetCommandStrings()[culture.Name])
// {
// // key is command name which should be specified in aliases[0] of any method name
// var cmdName = entry.Key;
//
// if (!enUsCommandNames.Contains(cmdName))
// {
// TestContext.Out.WriteLine($"'{cmdName}' It's either obsolete or missing an alias entry.");
// isSuccess = false;
// }
// }
//
// Assert.IsTrue(isSuccess);
// }
}
}

View file

@ -0,0 +1,79 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using NUnit.Framework;
namespace Nadeko.Tests
{
public class GroupGreetTests
{
private GreetGrouper<int> _grouper;
[SetUp]
public void Setup()
{
_grouper = new GreetGrouper<int>();
}
[Test]
public void CreateTest()
{
var created = _grouper.CreateOrAdd(0, 5);
Assert.True(created);
}
[Test]
public void CreateClearTest()
{
_grouper.CreateOrAdd(0, 5);
_grouper.ClearGroup(0, 5, out var items);
Assert.AreEqual(0, items.Count());
}
[Test]
public void NotCreatedTest()
{
_grouper.CreateOrAdd(0, 5);
var created = _grouper.CreateOrAdd(0, 4);
Assert.False(created);
}
[Test]
public void ClearAddedTest()
{
_grouper.CreateOrAdd(0, 5);
_grouper.CreateOrAdd(0, 4);
_grouper.ClearGroup(0, 5, out var items);
var list = items.ToList();
Assert.AreEqual(1, list.Count, $"Count was {list.Count}");
Assert.AreEqual(4, list[0]);
}
[Test]
public async Task ClearManyTest()
{
_grouper.CreateOrAdd(0, 5);
// add 15 items
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);
var list = items.ToList();
Assert.AreEqual(5, list.Count, $"Count was {list.Count}");
// try to get 15, but there should be 10 left
_grouper.ClearGroup(0, 15, out items);
list = items.ToList();
Assert.AreEqual(10, list.Count, $"Count was {list.Count}");
}
}
}

View file

@ -0,0 +1,185 @@
using NadekoBot.Common.Collections;
using NadekoBot.Core.Services.Database.Models;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Nadeko.Tests
{
public class IndexedCollectionTests
{
[Test]
public void AddTest()
{
var collection = GetCollectionSample(Enumerable.Empty<ShopEntry>());
// Add the items
for (var counter = 0; counter < 10; counter++)
collection.Add(new ShopEntry());
// Evaluate the items are ordered
CheckIndices(collection);
}
[Test]
public void RemoveTest()
{
var collection = GetCollectionSample<ShopEntry>();
collection.Remove(collection[1]);
collection.Remove(collection[1]);
// Evaluate the indices are ordered
CheckIndices(collection);
Assert.AreEqual(8, collection.Count);
}
[Test]
public void RemoveAtTest()
{
var collection = GetCollectionSample<ShopEntry>();
// Remove items 5 and 7
collection.RemoveAt(5);
collection.RemoveAt(6);
// Evaluate if the items got removed
foreach (var item in collection)
Assert.IsFalse(item.Id == 5 || item.Id == 7, $"Item at index {item.Index} was not removed");
CheckIndices(collection);
// RemoveAt out of range
Assert.Throws<ArgumentOutOfRangeException>(() => collection.RemoveAt(999), $"No exception thrown when removing from index 999 in a collection of size {collection.Count}.");
Assert.Throws<ArgumentOutOfRangeException>(() => collection.RemoveAt(-3), $"No exception thrown when removing from negative index -3.");
}
[Test]
public void ClearTest()
{
var collection = GetCollectionSample<ShopEntry>();
collection.Clear();
Assert.IsTrue(collection.Count == 0, "Collection has not been cleared.");
Assert.Throws<ArgumentOutOfRangeException>(() => collection.Contains(collection[0]), "Collection has not been cleared.");
}
[Test]
public void CopyToTest()
{
var collection = GetCollectionSample<ShopEntry>();
var fullCopy = new ShopEntry[10];
collection.CopyTo(fullCopy, 0);
// Evaluate copy
for (var index = 0; index < fullCopy.Length; index++)
Assert.AreEqual(index, fullCopy[index].Index);
Assert.Throws<ArgumentException>(() => collection.CopyTo(new ShopEntry[10], 4));
Assert.Throws<ArgumentException>(() => collection.CopyTo(new ShopEntry[6], 0));
}
[Test]
public void IndexOfTest()
{
var collection = GetCollectionSample<ShopEntry>();
Assert.AreEqual(4, collection.IndexOf(collection[4]));
Assert.AreEqual(0, collection.IndexOf(collection[0]));
Assert.AreEqual(7, collection.IndexOf(collection[7]));
Assert.AreEqual(9, collection.IndexOf(collection[9]));
}
[Test]
public void InsertTest()
{
var collection = GetCollectionSample<ShopEntry>();
// Insert items at indices 5 and 7
collection.Insert(5, new ShopEntry() { Id = 555 });
collection.Insert(7, new ShopEntry() { Id = 777 });
Assert.AreEqual(12, collection.Count);
Assert.AreEqual(555, collection[5].Id);
Assert.AreEqual(777, collection[7].Id);
CheckIndices(collection);
// Insert out of range
Assert.Throws<ArgumentOutOfRangeException>(() => collection.Insert(999, new ShopEntry() { Id = 999 }), $"No exception thrown when inserting at index 999 in a collection of size {collection.Count}.");
Assert.Throws<ArgumentOutOfRangeException>(() => collection.Insert(-3, new ShopEntry() { Id = -3 }), $"No exception thrown when inserting at negative index -3.");
}
[Test]
public void ContainsTest()
{
var subCol = new ShopEntry[]
{
new ShopEntry() { Id = 111 },
new ShopEntry() { Id = 222 },
new ShopEntry() { Id = 333 }
};
var collection = GetCollectionSample(
Enumerable.Range(0, 10)
.Select(x => new ShopEntry() { Id = x })
.Concat(subCol)
);
collection.Remove(subCol[1]);
CheckIndices(collection);
Assert.IsTrue(collection.Contains(subCol[0]));
Assert.IsFalse(collection.Contains(subCol[1]));
Assert.IsTrue(collection.Contains(subCol[2]));
}
[Test]
public void EnumeratorTest()
{
var collection = GetCollectionSample<ShopEntry>();
var enumerator = collection.GetEnumerator();
foreach (var item in collection)
{
enumerator.MoveNext();
Assert.AreEqual(item, enumerator.Current);
}
}
[Test]
public void IndexTest()
{
var collection = GetCollectionSample<ShopEntry>();
collection[4] = new ShopEntry() { Id = 444 };
collection[7] = new ShopEntry() { Id = 777 };
CheckIndices(collection);
Assert.AreEqual(444, collection[4].Id);
Assert.AreEqual(777, collection[7].Id);
}
/// <summary>
/// Checks whether all indices of the items are properly ordered.
/// </summary>
/// <typeparam name="T">An indexed, reference type.</typeparam>
/// <param name="collection">The indexed collection to be checked.</param>
private void CheckIndices<T>(IndexedCollection<T> collection) where T : class, IIndexed
{
for (var index = 0; index < collection.Count; index++)
Assert.AreEqual(index, collection[index].Index);
}
/// <summary>
/// Gets an <see cref="IndexedCollection{T}"/> from the specified <paramref name="sample"/> or a collection with 10 shop entries if none is provided.
/// </summary>
/// <typeparam name="T">An indexed, database entity type.</typeparam>
/// <param name="sample">A sample collection to be added as an indexed collection.</param>
/// <returns>An indexed collection of <typeparamref name="T"/>.</returns>
private IndexedCollection<T> GetCollectionSample<T>(IEnumerable<T> sample = default) where T : DbEntity, IIndexed, new()
=> new IndexedCollection<T>(sample ?? Enumerable.Range(0, 10).Select(x => new T() { Id = x }));
}
}

125
Nadeko.Tests/KwumTests.cs Normal file
View file

@ -0,0 +1,125 @@
using System.Linq;
using NadekoBot.Core.Common;
using NUnit.Framework;
namespace Nadeko.Tests
{
public class KwumTests
{
[Test]
public void TestDefaultHashCode()
{
var num = default(kwum);
Assert.AreEqual(0, num.GetHashCode());
}
[Test]
public void TestEqualGetHashCode()
{
var num1 = new kwum("234");
var num2 = new kwum("234");
Assert.AreEqual(num1.GetHashCode(), num2.GetHashCode());
}
[Test]
public void TestNotEqualGetHashCode()
{
var num1 = new kwum("234");
var num2 = new kwum("235");
Assert.AreNotEqual(num1.GetHashCode(), num2.GetHashCode());
}
[Test]
public void TestLongEqualGetHashCode()
{
var num1 = new kwum("hgbkhdbk");
var num2 = new kwum("hgbkhdbk");
Assert.AreEqual(num1.GetHashCode(), num2.GetHashCode());
}
[Test]
public void TestEqual()
{
var num1 = new kwum("hgbkhd");
var num2 = new kwum("hgbkhd");
Assert.AreEqual(num1, num2);
}
[Test]
public void TestNotEqual()
{
var num1 = new kwum("hgbk5d");
var num2 = new kwum("hgbk4d");
Assert.AreNotEqual(num1, num2);
}
[Test]
public void TestParseValidValue()
{
var validValue = "234e";
Assert.True(kwum.TryParse(validValue, out _));
}
[Test]
public void TestParseInvalidValue()
{
var invalidValue = "1234";
Assert.False(kwum.TryParse(invalidValue, out _));
}
[Test]
public void TestCorrectParseValue()
{
var validValue = "qwerf4bm";
kwum.TryParse(validValue, out var parsedValue);
Assert.AreEqual(parsedValue, new kwum(validValue));
}
[Test]
public void TestToString()
{
var validValue = "46g5yh";
kwum.TryParse(validValue, out var parsedValue);
Assert.AreEqual(validValue, parsedValue.ToString());
}
[Test]
public void TestConversionsToFromInt()
{
var num = new kwum(10);
Assert.AreEqual(10, (int)num);
Assert.AreEqual(num, (kwum)10);
}
[Test]
public void TestConverstionsToString()
{
var num = new kwum(10);
Assert.AreEqual("c", num.ToString());
num = new kwum(123);
Assert.AreEqual("5v", num.ToString());
// leading zeros have no meaning
Assert.AreEqual(new kwum("22225v"), num);
}
[Test]
public void TestMaxValue()
{
var num = new kwum(int.MaxValue - 1);
Assert.AreEqual("3zzzzzy", num.ToString());
num = new kwum(int.MaxValue);
Assert.AreEqual("3zzzzzz", num.ToString());
}
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NadekoBot.Core\NadekoBot.Core.csproj" />
</ItemGroup>
</Project>

136
Nadeko.Tests/PubSubTests.cs Normal file
View file

@ -0,0 +1,136 @@
using System.Threading.Tasks;
using NadekoBot.Core.Common;
using NUnit.Framework;
using NUnit.Framework.Internal;
namespace Nadeko.Tests
{
public class PubSubTests
{
[Test]
public async Task Test_EventPubSub_PubSub()
{
TypedKey<int> key = "test_key";
var expected = new Randomizer().Next();
var pubsub = new EventPubSub();
await pubsub.Sub(key, data =>
{
Assert.AreEqual(expected, data);
Assert.Pass();
return default;
});
await pubsub.Pub(key, expected);
Assert.Fail("Event not registered");
}
[Test]
public async Task Test_EventPubSub_MeaninglessUnsub()
{
TypedKey<int> key = "test_key";
var expected = new Randomizer().Next();
var pubsub = new EventPubSub();
await pubsub.Sub(key, data =>
{
Assert.AreEqual(expected, data);
Assert.Pass();
return default;
});
await pubsub.Unsub(key, _ => default);
await pubsub.Pub(key, expected);
Assert.Fail("Event not registered");
}
[Test]
public async Task Test_EventPubSub_MeaninglessUnsubThatLooksTheSame()
{
TypedKey<int> key = "test_key";
var expected = new Randomizer().Next();
var pubsub = new EventPubSub();
await pubsub.Sub(key, data =>
{
Assert.AreEqual(expected, data);
Assert.Pass();
return default;
});
await pubsub.Unsub(key, data =>
{
Assert.AreEqual(expected, data);
Assert.Pass();
return default;
});
await pubsub.Pub(key, expected);
Assert.Fail("Event not registered");
}
[Test]
public async Task Test_EventPubSub_MeaningfullUnsub()
{
TypedKey<int> key = "test_key";
var pubsub = new EventPubSub();
ValueTask Action(int data)
{
Assert.Fail("Event is raised when it shouldn't be");
return default;
}
await pubsub.Sub(key, Action);
await pubsub.Unsub(key, Action);
await pubsub.Pub(key, 0);
Assert.Pass();
}
[Test]
public async Task Test_EventPubSub_ObjectData()
{
TypedKey<byte[]> key = "test_key";
var pubsub = new EventPubSub();
var localData = new byte[1];
ValueTask Action(byte[] data)
{
Assert.AreEqual(localData, data);
Assert.Pass();
return default;
}
await pubsub.Sub(key, Action);
await pubsub.Pub(key, localData);
Assert.Fail("Event not raised");
}
[Test]
public async Task Test_EventPubSub_MultiSubUnsub()
{
TypedKey<object> key = "test_key";
var pubsub = new EventPubSub();
var localData = new object();
int successCounter = 0;
ValueTask Action1(object data)
{
Assert.AreEqual(localData, data);
successCounter+=10;
return default;
}
ValueTask Action2(object data)
{
Assert.AreEqual(localData, data);
successCounter++;
return default;
}
await pubsub.Sub(key, Action1); // + 10 \
await pubsub.Sub(key, Action2); // + 1 - + = 12
await pubsub.Sub(key, Action2); // + 1 /
await pubsub.Unsub(key, Action2); // - 1/
await pubsub.Pub(key, localData);
Assert.AreEqual(successCounter, 11, "Not all events are raised.");
}
}
}

25
Nadeko.Tests/Random.cs Normal file
View file

@ -0,0 +1,25 @@
using System;
using System.Text;
using NadekoBot.Common.Yml;
using NUnit.Framework;
namespace Nadeko.Tests
{
public class RandomTests
{
[SetUp]
public void Setup()
{
Console.OutputEncoding = Encoding.UTF8;
}
[Test]
public void Utf8CodepointsToEmoji()
{
var point = @"0001F338";
var hopefullyEmoji = YamlHelper.UnescapeUnicodeCodePoint(point);
Assert.AreEqual("🌸", hopefullyEmoji, hopefullyEmoji);
}
}
}

View file

@ -0,0 +1,20 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace NadekoBot.Common
{
public class AsyncLazy<T> : Lazy<Task<T>>
{
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(); }
}
}

View file

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using Discord.Commands;
using NadekoBot.Core.Services.Impl;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class AliasesAttribute : AliasAttribute
{
public AliasesAttribute([CallerMemberName] string memberName = "")
: base(CommandNameLoadHelper.GetAliasesFor(memberName))
{
}
}
}

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

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace NadekoBot.Common.Attributes
{
public static class CommandNameLoadHelper
{
private static YamlDotNet.Serialization.IDeserializer _deserializer
= new YamlDotNet.Serialization.Deserializer();
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
? 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
? aliases[0]
: methodName;
return toReturn;
}
}
}

View file

@ -0,0 +1,16 @@
using System;
using System.Runtime.CompilerServices;
using Discord.Commands;
using NadekoBot.Core.Services.Impl;
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

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

View file

@ -0,0 +1,14 @@
using System;
using Discord.Commands;
namespace NadekoBot.Common.Attributes
{
[AttributeUsage(AttributeTargets.Class)]
sealed class NadekoModuleAttribute : GroupAttribute
{
public NadekoModuleAttribute(string moduleName) : base(moduleName)
{
}
}
}

View file

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

View file

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

View file

@ -0,0 +1,38 @@
using Discord.Commands;
using NadekoBot.Core.Services;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Core.Common.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class RatelimitAttribute : PreconditionAttribute
{
public int Seconds { get; }
public RatelimitAttribute(int seconds)
{
if (seconds <= 0)
throw new ArgumentOutOfRangeException(nameof(seconds));
Seconds = seconds;
}
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
if (Seconds == 0)
return Task.FromResult(PreconditionResult.FromSuccess());
var cache = services.GetService<IDataCache>();
var rem = cache.TryAddRatelimit(context.User.Id, command.Name, Seconds);
if(rem == null)
return Task.FromResult(PreconditionResult.FromSuccess());
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
return Task.FromResult(PreconditionResult.FromError(msgContent));
}
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.Runtime.CompilerServices;
using Discord.Commands;
using NadekoBot.Core.Services.Impl;
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

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

View file

@ -0,0 +1,26 @@
namespace NadekoBot.Common
{
public enum BotConfigEditType
{
/// <summary>
/// The amount of currency awarded to the winner of the trivia game.
/// Default is 0.
/// </summary>
TriviaCurrencyReward,
/// <summary>
/// Users can't start trivia games which have smaller win requirement than specified by this setting.
/// Default is 0.
/// </summary>
MinimumTriviaWinReq,
/// <summary>
/// The amount of XP the user receives when they send a message (which is not too short).
/// Default is 3.
/// </summary>
XpPerMessage,
/// <summary>
/// This value represents how often the user can receive XP from sending messages.
/// Default is 5.
/// </summary>
XpMinutesTimeout,
}
}

View file

@ -0,0 +1,126 @@
using Discord;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using System;
namespace NadekoBot.Common
{
public class CREmbed
{
public CREmbedAuthor Author { get; set; }
public string PlainText { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Url { get; set; }
public CREmbedFooter Footer { get; set; }
public string Thumbnail { get; set; }
public string Image { get; set; }
public CREmbedField[] Fields { get; set; }
public uint Color { get; set; } = 7458112;
public bool IsValid =>
IsEmbedValid || !string.IsNullOrWhiteSpace(PlainText);
public bool IsEmbedValid =>
!string.IsNullOrWhiteSpace(Title) ||
!string.IsNullOrWhiteSpace(Description) ||
!string.IsNullOrWhiteSpace(Url) ||
!string.IsNullOrWhiteSpace(Thumbnail) ||
!string.IsNullOrWhiteSpace(Image) ||
(Footer != null && (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl))) ||
(Fields != null && Fields.Length > 0);
public EmbedBuilder ToEmbed()
{
var embed = new EmbedBuilder();
if (!string.IsNullOrWhiteSpace(Title))
embed.WithTitle(Title);
if (!string.IsNullOrWhiteSpace(Description))
embed.WithDescription(Description);
if (Url != null && Uri.IsWellFormedUriString(Url, UriKind.Absolute))
embed.WithUrl(Url);
embed.WithColor(new Discord.Color(Color));
if (Footer != null)
embed.WithFooter(efb =>
{
efb.WithText(Footer.Text);
if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute))
efb.WithIconUrl(Footer.IconUrl);
});
if (Thumbnail != null && Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute))
embed.WithThumbnailUrl(Thumbnail);
if (Image != null && Uri.IsWellFormedUriString(Image, UriKind.Absolute))
embed.WithImageUrl(Image);
if (Author != null && !string.IsNullOrWhiteSpace(Author.Name))
{
if (!Uri.IsWellFormedUriString(Author.IconUrl, UriKind.Absolute))
Author.IconUrl = null;
if (!Uri.IsWellFormedUriString(Author.Url, UriKind.Absolute))
Author.Url = null;
embed.WithAuthor(Author.Name, Author.IconUrl, Author.Url);
}
if (Fields != null)
foreach (var f in Fields)
{
if (!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value))
embed.AddField(efb => efb.WithName(f.Name).WithValue(f.Value).WithIsInline(f.Inline));
}
return embed;
}
public static bool TryParse(string input, out CREmbed embed)
{
embed = null;
if (string.IsNullOrWhiteSpace(input) || !input.Trim().StartsWith('{'))
return false;
try
{
var crembed = JsonConvert.DeserializeObject<CREmbed>(input);
if (crembed.Fields != null && crembed.Fields.Length > 0)
foreach (var f in crembed.Fields)
{
f.Name = f.Name.TrimTo(256);
f.Value = f.Value.TrimTo(1024);
}
if (!crembed.IsValid)
return false;
embed = crembed;
return true;
}
catch
{
return false;
}
}
}
public class CREmbedField
{
public string Name { get; set; }
public string Value { get; set; }
public bool Inline { get; set; }
}
public class CREmbedFooter
{
public string Text { get; set; }
public string IconUrl { get; set; }
[JsonProperty("icon_url")]
private string Icon_Url { set => IconUrl = value; }
}
public class CREmbedAuthor
{
public string Name { get; set; }
public string IconUrl { get; set; }
[JsonProperty("icon_url")]
private string Icon_Url { set => IconUrl = value; }
public string Url { get; set; }
}
}

View file

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

View file

@ -0,0 +1,772 @@
// 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 NadekoBot.Common.Collections
{
/// <summary>
/// Represents a thread-safe hash-based unique collection.
/// </summary>
/// <typeparam name="T">The type of the items in the collection.</typeparam>
/// <remarks>
/// All public members of <see cref="ConcurrentHashSet{T}"/> are thread-safe and may be used
/// concurrently from multiple threads.
/// </remarks>
[DebuggerDisplay("Count = {Count}")]
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T>
{
private const int DefaultCapacity = 31;
private const int MaxLockNumber = 1024;
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>
public bool IsEmpty
{
get
{
var acquiredLocks = 0;
try
{
AcquireAllLocks(ref acquiredLocks);
for (var i = 0; i < _tables.CountPerLock.Length; i++)
{
if (_tables.CountPerLock[i] != 0)
{
return false;
}
}
}
finally
{
ReleaseLocks(0, acquiredLocks);
}
return true;
}
}
/// <summary>
/// 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, DefaultCapacity, true, EqualityComparer<T>.Default)
{
}
/// <summary>
/// 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
/// 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>
public ConcurrentHashSet(int concurrencyLevel, int capacity)
: this(concurrencyLevel, capacity, false, EqualityComparer<T>.Default)
{
}
/// <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
/// 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
/// the new
/// <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)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{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>
/// <exception cref="T:System.ArgumentNullException"><paramref name="comparer"/> is a null reference.</exception>
public ConcurrentHashSet(IEqualityComparer<T> 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
/// 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
/// 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
/// (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 == null) throw new ArgumentNullException(nameof(collection));
InitializeFromCollection(collection);
}
/// <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 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>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="collection"/> is a null reference.
/// -or-
/// <paramref name="comparer"/> is a null reference.
/// </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="concurrencyLevel"/> is less than 1.
/// </exception>
public ConcurrentHashSet(int concurrencyLevel, IEnumerable<T> collection, IEqualityComparer<T> comparer)
: this(concurrencyLevel, DefaultCapacity, false, comparer)
{
if (collection == null) throw new ArgumentNullException(nameof(collection));
if (comparer == null) throw new ArgumentNullException(nameof(comparer));
InitializeFromCollection(collection);
}
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{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
/// 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>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="concurrencyLevel"/> is less than 1. -or-
/// <paramref name="capacity"/> is less than 0.
/// </exception>
/// <exception cref="T:System.ArgumentNullException"><paramref name="comparer"/> is a null reference.</exception>
public ConcurrentHashSet(int concurrencyLevel, int capacity, IEqualityComparer<T> comparer)
: this(concurrencyLevel, capacity, false, 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));
// 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 object();
}
var countPerLock = new int[locks.Length];
var buckets = new Node[capacity];
_tables = new Tables(buckets, locks, countPerLock);
_growLockArray = growLockArray;
_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>
public void Clear()
{
var locksAcquired = 0;
try
{
AcquireAllLocks(ref locksAcquired);
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
{
ReleaseLocks(0, locksAcquired);
}
}
/// <summary>
/// Determines whether the <see cref="ConcurrentHashSet{T}"/> contains the specified
/// item.
/// </summary>
/// <param name="item">The item to locate in the <see cref="ConcurrentHashSet{T}"/>.</param>
/// <returns>true if the <see cref="ConcurrentHashSet{T}"/> contains the item; otherwise, false.</returns>
public bool Contains(T 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 tables = _tables;
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 tables.Buckets[bucketNo]);
while (current != null)
{
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
{
return true;
}
current = current.Next;
}
return false;
}
/// <summary>
/// Attempts to remove the item from the <see cref="ConcurrentHashSet{T}"/>.
/// </summary>
/// <param name="item">The item to remove.</param>
/// <returns>true if an item was removed successfully; otherwise, false.</returns>
public bool TryRemove(T item)
{
var hashcode = _comparer.GetHashCode(item);
while (true)
{
var tables = _tables;
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length);
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 (tables != _tables)
{
continue;
}
Node previous = null;
for (var current = tables.Buckets[bucketNo]; current != null; current = current.Next)
{
Debug.Assert((previous == null && current == tables.Buckets[bucketNo]) || previous.Next == current);
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
{
if (previous == null)
{
Volatile.Write(ref tables.Buckets[bucketNo], current.Next);
}
else
{
previous.Next = current.Next;
}
tables.CountPerLock[lockNo]--;
return true;
}
previous = current;
}
}
return false;
}
}
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 == 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;
}
}
private bool AddInternal(T item, int hashcode, bool acquireLock)
{
while (true)
{
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(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 (tables != _tables)
{
continue;
}
// Try to find this item in the bucket
Node previous = null;
for (var current = tables.Buckets[bucketNo]; current != null; current = current.Next)
{
Debug.Assert((previous == 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 tables.Buckets[bucketNo], new Node(item, hashcode, tables.Buckets[bucketNo]));
checked
{
tables.CountPerLock[lockNo]++;
}
//
// If the number of elements guarded by this lock has exceeded the budget, resize the bucket table.
// 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 (tables.CountPerLock[lockNo] > _budget)
{
resizeDesired = true;
}
}
finally
{
if (lockTaken)
Monitor.Exit(tables.Locks[lockNo]);
}
//
// The fact that we got here means that we just performed an insertion. If necessary, we will grow the table.
//
// Concurrency notes:
// - Notice that we are not holding any locks at when calling GrowTable. This is necessary to prevent deadlocks.
// - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0
// and then verify that the table we passed to it as the argument is still the current table.
//
if (resizeDesired)
{
GrowTable(tables);
}
return true;
}
}
private static int GetBucket(int hashcode, int bucketCount)
{
var bucketNo = (hashcode & 0x7fffffff) % bucketCount;
Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount);
return bucketNo;
}
private static void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount)
{
bucketNo = (hashcode & 0x7fffffff) % bucketCount;
lockNo = bucketNo % lockCount;
Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount);
Debug.Assert(lockNo >= 0 && lockNo < lockCount);
}
private void GrowTable(Tables tables)
{
const int maxArrayLength = 0X7FEFFFFF;
var locksAcquired = 0;
try
{
// The thread that first obtains _locks[0] will be the one doing the resize operation
AcquireLocks(0, 1, ref locksAcquired);
// Make sure nobody resized the table while we were waiting for lock 0:
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 < 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 < tables.Buckets.Length / 4)
{
_budget = 2 * _budget;
if (_budget < 0)
{
_budget = int.MaxValue;
}
return;
}
// Compute the new table size. We find the smallest integer larger than twice the previous table size, and not divisible by
// 2,3,5 or 7. We can consider a different table-sizing policy in the future.
var newLength = 0;
var maximizeTableSize = false;
try
{
checked
{
// Double the size of the buckets table and add one, so that we have an odd integer.
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;
}
if (maximizeTableSize)
{
newLength = maxArrayLength;
// We want to make sure that GrowTable will not be called again, since table is at the maximum size.
// To achieve that, we set the budget to int.MaxValue.
//
// (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;
}
// Now acquire all other locks for the table
AcquireLocks(1, tables.Locks.Length, ref locksAcquired);
var newLocks = tables.Locks;
// Add more locks
if (_growLockArray && tables.Locks.Length < MaxLockNumber)
{
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 < tables.Buckets.Length; i++)
{
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);
newBuckets[newBucketNo] = new Node(current.Item, current.Hashcode, newBuckets[newBucketNo]);
checked
{
newCountPerLock[newLockNo]++;
}
current = next;
}
}
// Adjust the budget
_budget = Math.Max(1, newBuckets.Length / newLocks.Length);
// Replace tables with the new versions
_tables = new Tables(newBuckets, newLocks, newCountPerLock);
}
finally
{
// Release all locks that we took earlier
ReleaseLocks(0, locksAcquired);
}
}
public int RemoveWhere(Func<T, bool> predicate)
{
var elems = this.Where(predicate);
var removed = 0;
foreach (var elem in elems)
{
if (this.TryRemove(elem))
removed++;
}
return removed;
}
private void AcquireAllLocks(ref int locksAcquired)
{
// First, acquire lock 0
AcquireLocks(0, 1, ref locksAcquired);
// 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);
}
private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired)
{
Debug.Assert(fromInclusive <= toExclusive);
var locks = _tables.Locks;
for (var i = fromInclusive; i < toExclusive; i++)
{
var lockTaken = false;
try
{
Monitor.Enter(locks[i], ref lockTaken);
}
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]);
}
}
private void CopyToItems(T[] array, int index)
{
var buckets = _tables.Buckets;
for (var i = 0; i < buckets.Length; i++)
{
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
{
public readonly Node[] Buckets;
public readonly object[] Locks;
public volatile int[] CountPerLock;
public Tables(Node[] buckets, object[] locks, int[] countPerLock)
{
Buckets = buckets;
Locks = locks;
CountPerLock = countPerLock;
}
}
private sealed class Node
{
public readonly T Item;
public readonly int Hashcode;
public volatile Node Next;
public Node(T item, int hashcode, Node next)
{
Item = item;
Hashcode = hashcode;
Next = next;
}
}
}
}

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

@ -0,0 +1,141 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NadekoBot.Core.Services.Database.Models;
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 int IndexOf(T item) => item.Index;
public IndexedCollection()
{
Source = new List<T>();
}
public IndexedCollection(IEnumerable<T> source)
{
lock (_locker)
{
Source = source.OrderBy(x => x.Index).ToList();
UpdateIndexes();
}
}
public void UpdateIndexes()
{
lock (_locker)
{
for (var i = 0; i < Source.Count; i++)
{
if (Source[i].Index != i)
Source[i].Index = i;
}
}
}
public static implicit operator List<T>(IndexedCollection<T> x) =>
x.Source;
public List<T> ToList() => Source.ToList();
public IEnumerator<T> GetEnumerator() =>
Source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
Source.GetEnumerator();
public void Add(T item)
{
lock (_locker)
{
item.Index = Source.Count;
Source.Add(item);
}
}
public virtual void Clear()
{
lock (_locker)
{
Source.Clear();
}
}
public bool Contains(T item)
{
lock (_locker)
{
return Source.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex)
{
lock (_locker)
{
Source.CopyTo(array, arrayIndex);
}
}
public virtual bool Remove(T item)
{
bool removed;
lock (_locker)
{
if (removed = Source.Remove(item))
{
for (int i = 0; i < Source.Count; i++)
{
if (Source[i].Index != i)
Source[i].Index = i;
}
}
}
return removed;
}
public virtual void Insert(int index, T item)
{
lock (_locker)
{
Source.Insert(index, item);
for (int i = index; i < Source.Count; i++)
{
Source[i].Index = i;
}
}
}
public virtual void RemoveAt(int index)
{
lock (_locker)
{
Source.RemoveAt(index);
for (int i = index; i < Source.Count; i++)
{
Source[i].Index = i;
}
}
}
public virtual T this[int index]
{
get { return Source[index]; }
set
{
lock (_locker)
{
value.Index = index;
Source[index] = value;
}
}
}
}
}

View file

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

View file

@ -0,0 +1,166 @@
using System.Collections.Generic;
using System.Globalization;
using NadekoBot.Common.Yml;
using SixLabors.ImageSharp.PixelFormats;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
namespace NadekoBot.Core.Common.Configs
{
public sealed class BotConfig
{
[Comment(@"DO NOT CHANGE")]
public int Version { get; set; }
[Comment(@"Most commands, when executed, have a small colored line
next to the response. The color depends whether the command
is completed, errored or in progress (pending)
Color settings below are for the color of those lines.
To get color's hex, you can go here https://htmlcolorcodes.com/
and copy the hex code fo your selected color (marked as #)")]
public ColorConfig Color { get; set; }
[Comment("Default bot language. It has to be in the list of supported languages (.langli)")]
public CultureInfo DefaultLocale { get; set; }
[Comment(@"Style in which executed commands will show up in the console.
Allowed values: Simple, Normal, None")]
public ConsoleOutputType ConsoleOutputType { get; set; }
// [Comment(@"For what kind of updates will the bot check.
// Allowed values: Release, Commit, None")]
// public UpdateCheckType CheckForUpdates { get; set; }
// [Comment(@"How often will the bot check for updates, in hours")]
// public int CheckUpdateInterval { get; set; }
[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),
or all owners? (this might cause the bot to lag if there's a lot of owners specified)")]
public bool ForwardToAllOwners { get; set; }
[Comment(@"When a user DMs the bot with a message which is not a command
they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot.
Supports embeds. How it looks: https://puu.sh/B0BLV.png")]
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
public string DmHelpText { get; set; }
[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; }
[Comment(@"Which string will be used to recognize the commands")]
public string Prefix { get; set; }
[Comment(@"Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
1st user who joins will get greeted immediately
If more users join within the next 5 seconds, they will be greeted in groups of 5.
This will cause %user.mention% and other placeholders to be replaced with multiple users.
Keep in mind this might break some of your embeds - for example if you have %user.avatar% in the thumbnail,
it will become invalid, as it will resolve to a list of avatars of grouped users.
note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some
servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited,
and (slightly) reduce the greet spam in those servers.")]
public bool GroupGreets { get; set; }
[Comment(@"Whether the bot will rotate through all specified statuses.
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()
{
Version = 1;
var color = new ColorConfig();
Color = color;
DefaultLocale = new CultureInfo("en-US");
ConsoleOutputType = ConsoleOutputType.Normal;
ForwardMessages = false;
ForwardToAllOwners = false;
DmHelpText = @"{""description"": ""Type `%prefix%h` for help.""}";
HelpText = @"{
""title"": ""To invite me to your server, use this link"",
""description"": ""https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303"",
""color"": 53380,
""thumbnail"": ""https://i.imgur.com/nKYyqMK.png"",
""fields"": [
{
""name"": ""Useful help commands"",
""value"": ""`%bot.prefix%modules` Lists all bot modules.
`%prefix%h CommandName` Shows some help about a specific command.
`%prefix%commands ModuleName` Lists all commands in a module."",
""inline"": false
},
{
""name"": ""List of all Commands"",
""value"": ""https://nadeko.bot/commands"",
""inline"": false
},
{
""name"": ""Nadeko Support Server"",
""value"": ""https://discord.nadeko.bot/ "",
""inline"": true
}
]
}";
var blocked = new BlockedConfig();
Blocked = blocked;
Prefix = ".";
RotateStatuses = false;
GroupGreets = false;
}
}
public class BlockedConfig
{
public HashSet<string> Commands { get; set; }
public HashSet<string> Modules { get; set; }
public BlockedConfig()
{
Modules = new HashSet<string>();
Commands = new HashSet<string>();
}
}
public class ColorConfig
{
[Comment(@"Color used for embed responses when command successfully executes")]
public Rgba32 Ok { get; set; }
[Comment(@"Color used for embed responses when command has an error")]
public Rgba32 Error { get; set; }
[Comment(@"Color used for embed responses while command is doing work or is in progress")]
public Rgba32 Pending { get; set; }
public ColorConfig()
{
Ok = Rgba32.ParseHex("00e584");
Error = Rgba32.ParseHex("ee281f");
Pending = Rgba32.ParseHex("faa61a");
}
}
public enum ConsoleOutputType
{
Normal = 0,
Simple = 1,
None = 2,
}
}

View file

@ -0,0 +1,18 @@
namespace NadekoBot.Core.Common.Configs
{
/// <summary>
/// Base interface for available config serializers
/// </summary>
public interface IConfigSeria
{
/// <summary>
/// Serialize the object to string
/// </summary>
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

@ -0,0 +1,43 @@
using NadekoBot.Core.Services;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Discord;
namespace NadekoBot.Core.Common
{
public class DownloadTracker : INService
{
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.
/// </summary>
/// <param name="guild">Guild to check and potentially download users from</param>
/// <returns>Task representing download state</returns>
public async Task EnsureUsersDownloadedAsync(IGuild guild)
{
await downloadUsersSemaphore.WaitAsync();
try
{
var now = DateTime.UtcNow;
// download once per hour at most
var added = LastDownloads.AddOrUpdate(
guild.Id,
now,
(key, old) => (now - old) > TimeSpan.FromHours(1) ? now : old);
// means that this entry was just added - download the users
if (added == now)
await guild.DownloadUsersAsync();
}
finally
{
downloadUsersSemaphore.Release();
}
}
}
}

View file

@ -0,0 +1,69 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Core.Common;
using NadekoBot.Core.Modules.Music;
using NadekoBot.Core.Services;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Music.Resolvers;
using NadekoBot.Modules.Music.Services;
namespace NadekoBot.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBotStringsServices(this IServiceCollection services)
=> services
.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
.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.GetService<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;
}
}
}

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

@ -0,0 +1,15 @@
using System;
namespace NadekoBot.Core.Common
{
public static class Helpers
{
public static void ReadErrorAndExit(int exitCode)
{
if (!Console.IsInputRedirected)
Console.ReadKey();
Environment.Exit(exitCode);
}
}
}

View file

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

View file

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

View file

@ -0,0 +1,49 @@
using System;
namespace NadekoBot.Core.Common
{
public class ImageUrls
{
public int Version { get; set; } = 2;
public CoinData Coins { get; set; }
public Uri[] Currency { get; set; }
public Uri[] Dice { get; set; }
public RategirlData Rategirl { get; set; }
public XpData Xp { get; set; }
//new
public RipData Rip { get; set; }
public SlotData Slots { get; set; }
public class RipData
{
public Uri Bg { get; set; }
public Uri Overlay { get; set; }
}
public class SlotData
{
public Uri[] Emojis { get; set; }
public Uri[] Numbers { get; set; }
public Uri Bg { get; set; }
}
public class CoinData
{
public Uri[] Heads { get; set; }
public Uri[] Tails { get; set; }
}
public class RategirlData
{
public Uri Matrix { get; set; }
public Uri Dot { get; set; }
}
public class XpData
{
public Uri Bg { get; set; }
}
}
}

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.Core.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

@ -0,0 +1,98 @@
using System;
using System.Runtime.CompilerServices;
namespace NadekoBot.Core.Common
{
// needs proper invalid input check (character array input out of range)
// needs negative number support
public readonly struct kwum : IEquatable<kwum>
{
private readonly int _value;
private const string ValidCharacters = "23456789abcdefghijkmnpqrstuvwxyz";
public kwum(int num)
=> _value = num;
public kwum(in char c)
{
if (!IsValidChar(c))
throw new ArgumentException("Character needs to be a valid kwum character.", nameof(c));
_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++)
{
var c = input[index];
if (!IsValidChar(c))
throw new ArgumentException("All characters need to be a valid kwum characters.", nameof(input));
_value += ValidCharacters.IndexOf(c) * (int)Math.Pow(ValidCharacters.Length, input.Length - index - 1);
}
}
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 kwum(input);
return true;
}
public static kwum operator +(kwum left, kwum right)
=> new kwum(left._value + right._value);
public static bool operator ==(kwum left, kwum right)
=> left._value == right._value;
public static bool operator !=(kwum left, kwum right)
=> !(left == right);
public static implicit operator long(kwum kwum)
=> kwum._value;
public static implicit operator int(kwum kwum)
=> kwum._value;
public static implicit operator kwum(int num)
=> new kwum(num);
public static bool IsValidChar(char c)
=> ValidCharacters.Contains(c);
public override string ToString()
{
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] = ValidCharacters[(int)rem];
}
return new string(chars);
}
public override bool Equals(object obj)
=> obj is kwum kw && kw == this;
public bool Equals(kwum other)
=> other == this;
public override int GetHashCode()
{
return _value.GetHashCode();
}
}
}

View file

@ -0,0 +1,14 @@
using CommandLine;
namespace NadekoBot.Core.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

@ -0,0 +1,57 @@
using System;
using System.Net;
using System.Runtime.CompilerServices;
using Discord.Net;
using Serilog;
namespace NadekoBot.Core.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");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Handle(HttpException ex)
{
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");
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("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");
break;
case HttpStatusCode.ServiceUnavailable:
case HttpStatusCode.InternalServerError:
Log.Error("Discord is having internal issues. Please, try again later");
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");
break;
default:
Log.Warning("An error occurred while attempting to connect to Discord");
break;
}
Log.Fatal(ex.ToString());
}
}
}

View file

@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
namespace NadekoBot.Common.ModuleBehaviors
{
/// <summary>
/// Implemented by modules which block execution before anything is executed
/// </summary>
public interface IEarlyBehavior
{
int Priority { get; }
ModuleBehaviorType BehaviorType { get; }
Task<bool> RunBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg);
}
public enum ModuleBehaviorType
{
Blocker,
Executor,
}
}

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

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

View file

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
namespace NadekoBot.Common.ModuleBehaviors
{
/// <summary>
/// Last thing to be executed, won't stop further executions
/// </summary>
public interface ILateExecutor
{
Task LateExecute(DiscordSocketClient client, 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

@ -0,0 +1,16 @@
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
/// </summary>
public interface IReadyExecutor
{
/// <summary>
/// Executed when bot is ready
/// </summary>
public Task OnReadyAsync();
}
}

View file

@ -0,0 +1,155 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using System.Globalization;
using System.Threading.Tasks;
namespace NadekoBot.Modules
{
public abstract class NadekoModule : ModuleBase
{
protected CultureInfo _cultureInfo { get; set; }
public IBotStrings Strings { get; set; }
public CommandHandler CmdHandler { get; set; }
public ILocalization Localization { get; set; }
public string Prefix => CmdHandler.GetPrefix(ctx.Guild);
protected ICommandContext ctx => Context;
protected NadekoModule()
{
}
protected override void BeforeExecute(CommandInfo cmd)
{
_cultureInfo = Localization.GetCultureInfo(ctx.Guild?.Id);
}
protected string GetText(string key) =>
Strings.GetText(key, _cultureInfo);
protected string GetText(string key, params object[] args) =>
Strings.GetText(key, _cultureInfo, args);
public Task<IUserMessage> ErrorLocalizedAsync(string textKey, params object[] args)
{
var text = GetText(textKey, args);
return ctx.Channel.SendErrorAsync(text);
}
public Task<IUserMessage> ReplyErrorLocalizedAsync(string textKey, params object[] args)
{
var text = GetText(textKey, args);
return ctx.Channel.SendErrorAsync(Format.Bold(ctx.User.ToString()) + " " + text);
}
public Task<IUserMessage> ReplyPendingLocalizedAsync(string textKey, params object[] args)
{
var text = GetText(textKey, args);
return ctx.Channel.SendPendingAsync(Format.Bold(ctx.User.ToString()) + " " + text);
}
public Task<IUserMessage> ConfirmLocalizedAsync(string textKey, params object[] args)
{
var text = GetText(textKey, args);
return ctx.Channel.SendConfirmAsync(text);
}
public Task<IUserMessage> ReplyConfirmLocalizedAsync(string textKey, params object[] args)
{
var text = GetText(textKey, args);
return ctx.Channel.SendConfirmAsync(Format.Bold(ctx.User.ToString()) + " " + text);
}
public async Task<bool> PromptUserConfirmAsync(EmbedBuilder embed)
{
embed
.WithPendingColor()
.WithFooter("yes/no");
var msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
try
{
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
{
var _ = Task.Run(() => msg.DeleteAsync());
}
}
// TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ?
public async Task<string> GetUserInputAsync(ulong userId, ulong channelId)
{
var userInputTask = new TaskCompletionSource<string>();
var dsc = (DiscordSocketClient)ctx.Client;
try
{
dsc.MessageReceived += MessageReceived;
if ((await Task.WhenAny(userInputTask.Task, Task.Delay(10000)).ConfigureAwait(false)) != userInputTask.Task)
{
return null;
}
return await userInputTask.Task.ConfigureAwait(false);
}
finally
{
dsc.MessageReceived -= MessageReceived;
}
Task MessageReceived(SocketMessage arg)
{
var _ = Task.Run(() =>
{
if (!(arg is SocketUserMessage userMsg) ||
!(userMsg.Channel is ITextChannel chan) ||
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;
}
}
}
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

@ -0,0 +1,74 @@
using System;
using System.Security.Cryptography;
namespace NadekoBot.Common
{
public class NadekoRandom : Random
{
readonly RandomNumberGenerator _rng;
public NadekoRandom() : base()
{
_rng = RandomNumberGenerator.Create();
}
public override int Next()
{
var bytes = new byte[sizeof(int)];
_rng.GetBytes(bytes);
return Math.Abs(BitConverter.ToInt32(bytes, 0));
}
public override int Next(int maxValue)
{
if (maxValue <= 0)
throw new ArgumentOutOfRangeException(nameof(maxValue));
var bytes = new byte[sizeof(int)];
_rng.GetBytes(bytes);
return Math.Abs(BitConverter.ToInt32(bytes, 0)) % maxValue;
}
public override int Next(int minValue, int maxValue)
{
if (minValue > maxValue)
throw new ArgumentOutOfRangeException(nameof(maxValue));
if (minValue == maxValue)
return minValue;
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;
}
public long NextLong(long minValue, long maxValue)
{
if (minValue > maxValue)
throw new ArgumentOutOfRangeException(nameof(maxValue));
if (minValue == maxValue)
return minValue;
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;
}
public override void NextBytes(byte[] 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);
}
public override double NextDouble()
{
var bytes = new byte[sizeof(double)];
_rng.GetBytes(bytes);
return BitConverter.ToDouble(bytes, 0);
}
}
}

View file

@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
namespace NadekoBot.Common
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class NoPublicBotAttribute : PreconditionAttribute
{
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"));
#else
return Task.FromResult(PreconditionResult.FromSuccess());
#endif
}
}
}

View file

@ -0,0 +1,24 @@
using CommandLine;
namespace NadekoBot.Core.Common
{
public static class OptionsParser
{
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
{
using (var p = new Parser(x =>
{
x.HelpWriter = null;
}))
{
var res = p.ParseArguments<T>(args);
options = res.MapResult(x => x, x => options);
options.NormalizeOptions();
return (options, res.Tag == ParserResultType.Parsed);
}
}
}
}

View file

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

View file

@ -0,0 +1,41 @@
using Newtonsoft.Json;
namespace NadekoBot.Core.Common
{
public class OsuUserBests
{
[JsonProperty("beatmap_id")] public string BeatmapId { get; set; }
[JsonProperty("score_id")] public string ScoreId { get; set; }
[JsonProperty("score")] public string Score { get; set; }
[JsonProperty("maxcombo")] public string Maxcombo { get; set; }
[JsonProperty("count50")] public double Count50 { get; set; }
[JsonProperty("count100")] public double Count100 { get; set; }
[JsonProperty("count300")] public double Count300 { get; set; }
[JsonProperty("countmiss")] public int Countmiss { get; set; }
[JsonProperty("countkatu")] public double Countkatu { get; set; }
[JsonProperty("countgeki")] public double Countgeki { get; set; }
[JsonProperty("perfect")] public string Perfect { get; set; }
[JsonProperty("enabled_mods")] public int EnabledMods { get; set; }
[JsonProperty("user_id")] public string UserId { get; set; }
[JsonProperty("date")] public string Date { get; set; }
[JsonProperty("rank")] public string Rank { get; set; }
[JsonProperty("pp")] public double Pp { get; set; }
[JsonProperty("replay_available")] public string ReplayAvailable { get; set; }
}
}

View file

@ -0,0 +1,25 @@
using System;
namespace NadekoBot.Common
{
public static class PlatformHelper
{
private const int ProcessorCountRefreshIntervalMs = 30000;
private static volatile int _processorCount;
private static volatile int _lastProcessorCountRefreshTicks;
public static int ProcessorCount {
get {
var now = Environment.TickCount;
if (_processorCount == 0 || (now - _lastProcessorCountRefreshTicks) >= ProcessorCountRefreshIntervalMs)
{
_processorCount = Environment.ProcessorCount;
_lastProcessorCountRefreshTicks = now;
}
return _processorCount;
}
}
}
}

View file

@ -0,0 +1,8 @@
namespace NadekoBot.Core.Common.Pokemon
{
public class PokemonNameId
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View file

@ -0,0 +1,40 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace NadekoBot.Core.Common.Pokemon
{
public class SearchPokemon
{
public class GenderRatioClass
{
public float M { get; set; }
public float F { get; set; }
}
public class BaseStatsClass
{
public int HP { get; set; }
public int ATK { get; set; }
public int DEF { get; set; }
public int SPA { get; set; }
public int SPD { get; set; }
public int SPE { get; set; }
public override string ToString() => $@"💚**HP:** {HP,-4} ⚔**ATK:** {ATK,-4} 🛡**DEF:** {DEF,-4}
**SPA:** {SPA,-4} 🎇**SPD:** {SPD,-4} 💨**SPE:** {SPE,-4}";
}
[JsonProperty("num")]
public int Id { get; set; }
public string Species { get; set; }
public string[] Types { get; set; }
public GenderRatioClass GenderRatio { get; set; }
public BaseStatsClass BaseStats { get; set; }
public Dictionary<string, string> Abilities { get; set; }
public float HeightM { get; set; }
public float WeightKg { get; set; }
public string Color { get; set; }
public string[] Evos { get; set; }
public string[] EggGroups { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace NadekoBot.Core.Common.Pokemon
{
public class SearchPokemonAbility
{
public string Desc { get; set; }
public string ShortDesc { get; set; }
public string Name { get; set; }
public float Rating { get; set; }
}
}

View file

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Core.Common
{
public class EventPubSub : IPubSub
{
private readonly Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>> _actions
= new Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>>();
private readonly object locker = new object();
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{
Func<object, ValueTask> localAction = obj => action((TData) obj);
lock(locker)
{
Dictionary<Delegate, List<Func<object, ValueTask>>> keyActions;
if (!_actions.TryGetValue(key.Key, out keyActions))
{
keyActions = new Dictionary<Delegate, List<Func<object, ValueTask>>>();
_actions[key.Key] = keyActions;
}
List<Func<object, ValueTask>> sameActions;
if (!keyActions.TryGetValue(action, out sameActions))
{
sameActions = new List<Func<object, ValueTask>>();
keyActions[action] = sameActions;
}
sameActions.Add(localAction);
return Task.CompletedTask;
}
}
public Task Pub<TData>(in TypedKey<TData> key, TData data)
{
lock (locker)
{
if(_actions.TryGetValue(key.Key, out var actions))
{
// if this class ever gets used, this needs to be properly implemented
// 1. ignore all valuetasks which are completed
// 2. return task.whenall all other tasks
return Task.WhenAll(actions
.SelectMany(kvp => kvp.Value)
.Select(action => action(data).AsTask()));
}
return Task.CompletedTask;
}
}
public Task Unsub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{
lock (locker)
{
// get subscriptions for this action
if (_actions.TryGetValue(key.Key, out var actions))
{
var hashCode = action.GetHashCode();
// get subscriptions which have the same action hash code
// note: having this as a list allows for multiple subscriptions of
// the same insance's/static method
if (actions.TryGetValue(action, out var sameActions))
{
// remove last subscription
sameActions.RemoveAt(sameActions.Count - 1);
// if the last subscription was the only subscription
// we can safely remove this action's dictionary entry
if (sameActions.Count == 0)
{
actions.Remove(action);
// if our dictionary has no more elements after
// removing the entry
// it's safe to remove it from the key's subscriptions
if (actions.Count == 0)
{
_actions.Remove(key.Key);
}
}
}
}
return Task.CompletedTask;
}
}
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Threading.Tasks;
namespace NadekoBot.Core.Common
{
public interface IPubSub
{
public Task Pub<TData>(in TypedKey<TData> key, TData data);
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action);
}
}

View file

@ -0,0 +1,8 @@
namespace NadekoBot.Core.Common
{
public interface ISeria
{
byte[] Serialize<T>(T data);
T Deserialize<T>(byte[] data);
}
}

View file

@ -0,0 +1,28 @@
using System.Text.Json;
using NadekoBot.Core.Common.JsonConverters;
namespace NadekoBot.Core.Common
{
public class JsonSeria : ISeria
{
private JsonSerializerOptions serializerOptions = new JsonSerializerOptions()
{
Converters =
{
new Rgba32Converter(),
new CultureInfoConverter(),
}
};
public byte[] Serialize<T>(T data)
=> JsonSerializer.SerializeToUtf8Bytes(data, serializerOptions);
public T Deserialize<T>(byte[] data)
{
if (data is null)
return default;
return JsonSerializer.Deserialize<T>(data, serializerOptions);
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
using NadekoBot.Core.Services;
using NadekoBot.Extensions;
using Serilog;
using StackExchange.Redis;
namespace NadekoBot.Core.Common
{
public sealed class RedisPubSub : IPubSub
{
private readonly ConnectionMultiplexer _multi;
private readonly ISeria _serializer;
private readonly IBotCredentials _creds;
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds)
{
_multi = multi;
_serializer = serializer;
_creds = creds;
}
public Task Pub<TData>(in TypedKey<TData> key, TData data)
{
var serialized = _serializer.Serialize(data);
return _multi.GetSubscriber().PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget);
}
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{
var eventName = key.Key;
return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", async (ch, data) =>
{
try
{
var dataObj = _serializer.Deserialize<TData>(data);
await action(dataObj);
}
catch (Exception ex)
{
Log.Error($"Error handling the event {eventName}: {ex.Message}");
}
});
}
}
}

View file

@ -0,0 +1,29 @@
namespace NadekoBot.Core.Common
{
public readonly struct TypedKey<TData>
{
public readonly string Key;
public TypedKey(in string key)
{
Key = key;
}
public static implicit operator TypedKey<TData>(in string input)
=> new TypedKey<TData>(input);
public static implicit operator string(in TypedKey<TData> input)
=> input.Key;
public static bool operator ==(in TypedKey<TData> left, in TypedKey<TData> right)
=> left.Key == right.Key;
public static bool operator !=(in TypedKey<TData> left, in TypedKey<TData> right)
=> !(left == right);
public override bool Equals(object obj)
=> obj is TypedKey<TData> o && o == this;
public override int GetHashCode() => Key?.GetHashCode() ?? 0;
public override string ToString() => Key;
}
}

View file

@ -0,0 +1,38 @@
using System.Text.RegularExpressions;
using NadekoBot.Common.Yml;
using NadekoBot.Core.Common.Configs;
using YamlDotNet.Serialization;
namespace NadekoBot.Core.Common
{
public class YamlSeria : IConfigSeria
{
private readonly ISerializer _serializer;
private readonly IDeserializer _deserializer;
private static readonly Regex CodePointRegex
= new Regex(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
RegexOptions.Compiled);
public YamlSeria()
{
_serializer = Yaml.Serializer;
_deserializer = Yaml.Deserializer;
}
public string Serialize<T>(T obj)
{
var escapedOutput = _serializer.Serialize(obj);
var output = CodePointRegex.Replace(escapedOutput, me =>
{
var str = me.Groups["code"].Value;
var newString = YamlHelper.UnescapeUnicodeCodePoint(str);
return newString;
});
return output;
}
public T Deserialize<T>(string data)
=> _deserializer.Deserialize<T>(data);
}
}

View file

@ -0,0 +1,235 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Music.Services;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NadekoBot.Core.Common;
namespace NadekoBot.Common.Replacements
{
public class ReplacementBuilder
{
private static readonly Regex rngRegex = new Regex("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%", RegexOptions.Compiled);
private ConcurrentDictionary<string, Func<string>> _reps = new ConcurrentDictionary<string, Func<string>>();
private ConcurrentDictionary<Regex, Func<Match, string>> _regex = new ConcurrentDictionary<Regex, Func<Match, string>>();
public ReplacementBuilder()
{
WithRngRegex();
}
public ReplacementBuilder WithDefault(IUser usr, IMessageChannel ch, SocketGuild g, DiscordSocketClient client)
{
return this.WithUser(usr)
.WithChannel(ch)
.WithServer(client, g)
.WithClient(client);
}
public ReplacementBuilder WithDefault(ICommandContext ctx) =>
WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client);
public ReplacementBuilder WithMention(DiscordSocketClient client)
{
/*OBSOLETE*/
_reps.TryAdd("%mention%", () => $"<@{client.CurrentUser.Id}>");
/*NEW*/
_reps.TryAdd("%bot.mention%", () => client.CurrentUser.Mention);
return this;
}
public ReplacementBuilder WithClient(DiscordSocketClient client)
{
WithMention(client);
/*OBSOLETE*/
_reps.TryAdd("%shardid%", () => client.ShardId.ToString());
_reps.TryAdd("%time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
/*NEW*/
_reps.TryAdd("%bot.status%", () => client.Status.ToString());
_reps.TryAdd("%bot.latency%", () => client.Latency.ToString());
_reps.TryAdd("%bot.name%", () => client.CurrentUser.Username);
_reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString());
_reps.TryAdd("%bot.time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
_reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator);
_reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString());
_reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl()?.ToString());
WithStats(client);
return this;
}
public ReplacementBuilder WithServer(DiscordSocketClient client, SocketGuild g)
{
/*OBSOLETE*/
_reps.TryAdd("%sid%", () => g == null ? "DM" : g.Id.ToString());
_reps.TryAdd("%server%", () => g == null ? "DM" : g.Name);
_reps.TryAdd("%members%", () => g != null && g is SocketGuild sg ? sg.MemberCount.ToString() : "?");
_reps.TryAdd("%server_time%", () =>
{
TimeZoneInfo to = TimeZoneInfo.Local;
if (g != null)
{
if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
}
return TimeZoneInfo.ConvertTime(DateTime.UtcNow,
TimeZoneInfo.Utc,
to).ToString("HH:mm ") + to.StandardName.GetInitials();
});
/*NEW*/
_reps.TryAdd("%server.id%", () => g == null ? "DM" : g.Id.ToString());
_reps.TryAdd("%server.name%", () => g == null ? "DM" : g.Name);
_reps.TryAdd("%server.members%", () => g != null && g is SocketGuild sg ? sg.MemberCount.ToString() : "?");
_reps.TryAdd("%server.time%", () =>
{
TimeZoneInfo to = TimeZoneInfo.Local;
if (g != null)
{
if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
}
return TimeZoneInfo.ConvertTime(DateTime.UtcNow,
TimeZoneInfo.Utc,
to).ToString("HH:mm ") + to.StandardName.GetInitials();
});
return this;
}
public ReplacementBuilder WithChannel(IMessageChannel ch)
{
/*OBSOLETE*/
_reps.TryAdd("%channel%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name);
_reps.TryAdd("%chname%", () => ch.Name);
_reps.TryAdd("%cid%", () => ch?.Id.ToString());
/*NEW*/
_reps.TryAdd("%channel.mention%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name);
_reps.TryAdd("%channel.name%", () => ch.Name);
_reps.TryAdd("%channel.id%", () => ch.Id.ToString());
_reps.TryAdd("%channel.created%", () => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy"));
_reps.TryAdd("%channel.nsfw%", () => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-");
_reps.TryAdd("%channel.topic%", () => (ch as ITextChannel)?.Topic ?? "-");
return this;
}
public ReplacementBuilder WithUser(IUser user)
{
// /*OBSOLETE*/
// _reps.TryAdd("%user%", () => user.Mention);
// _reps.TryAdd("%userfull%", () => user.ToString());
// _reps.TryAdd("%username%", () => user.Username);
// _reps.TryAdd("%userdiscrim%", () => user.Discriminator);
// _reps.TryAdd("%useravatar%", () => user.RealAvatarUrl()?.ToString());
// _reps.TryAdd("%id%", () => user.Id.ToString());
// _reps.TryAdd("%uid%", () => user.Id.ToString());
// /*NEW*/
// _reps.TryAdd("%user.mention%", () => user.Mention);
// _reps.TryAdd("%user.fullname%", () => user.ToString());
// _reps.TryAdd("%user.name%", () => user.Username);
// _reps.TryAdd("%user.discrim%", () => user.Discriminator);
// _reps.TryAdd("%user.avatar%", () => user.RealAvatarUrl()?.ToString());
// _reps.TryAdd("%user.id%", () => user.Id.ToString());
// _reps.TryAdd("%user.created_time%", () => user.CreatedAt.ToString("HH:mm"));
// _reps.TryAdd("%user.created_date%", () => user.CreatedAt.ToString("dd.MM.yyyy"));
// _reps.TryAdd("%user.joined_time%", () => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-");
// _reps.TryAdd("%user.joined_date%", () => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-");
WithManyUsers(new[] {user});
return this;
}
public ReplacementBuilder WithManyUsers(IEnumerable<IUser> users)
{
/*OBSOLETE*/
_reps.TryAdd("%user%", () => string.Join(" ", users.Select(user => user.Mention)));
_reps.TryAdd("%userfull%", () => string.Join(" ", users.Select(user => user.ToString())));
_reps.TryAdd("%username%", () => string.Join(" ", users.Select(user => user.Username)));
_reps.TryAdd("%userdiscrim%", () => string.Join(" ", users.Select(user => user.Discriminator)));
_reps.TryAdd("%useravatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl()?.ToString())));
_reps.TryAdd("%id%", () => string.Join(" ", users.Select(user => user.Id.ToString())));
_reps.TryAdd("%uid%", () => string.Join(" ", users.Select(user => user.Id.ToString())));
/*NEW*/
_reps.TryAdd("%user.mention%", () => string.Join(" ", users.Select(user => user.Mention)));
_reps.TryAdd("%user.fullname%", () => string.Join(" ", users.Select(user => user.ToString())));
_reps.TryAdd("%user.name%", () => string.Join(" ", users.Select(user => user.Username)));
_reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator)));
_reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl()?.ToString())));
_reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString())));
_reps.TryAdd("%user.created_time%", () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm"))));
_reps.TryAdd("%user.created_date%", () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy"))));
_reps.TryAdd("%user.joined_time%", () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-")));
_reps.TryAdd("%user.joined_date%", () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-")));
return this;
}
private ReplacementBuilder WithStats(DiscordSocketClient c)
{
/*OBSOLETE*/
_reps.TryAdd("%servers%", () => c.Guilds.Count.ToString());
#if !GLOBAL_NADEKO
_reps.TryAdd("%users%", () => c.Guilds.Sum(s => s.Users.Count).ToString());
#endif
/*NEW*/
_reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString());
#if !GLOBAL_NADEKO
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(s => s.Users.Count).ToString());
#endif
_reps.TryAdd("%shard.id%", () => c.ShardId.ToString());
return this;
}
public ReplacementBuilder WithRngRegex()
{
var rng = new NadekoRandom();
_regex.TryAdd(rngRegex, (match) =>
{
if (!int.TryParse(match.Groups["from"].ToString(), out var from))
from = 0;
if (!int.TryParse(match.Groups["to"].ToString(), out var to))
to = 0;
if (from == 0 && to == 0)
return rng.Next(0, 11).ToString();
if (from >= to)
return string.Empty;
return rng.Next(from, to + 1).ToString();
});
return this;
}
public ReplacementBuilder WithOverride(string key, Func<string> output)
{
_reps.AddOrUpdate(key, output, delegate { return output; });
return this;
}
public Replacer Build()
{
return new Replacer(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray());
}
public ReplacementBuilder WithProviders(IEnumerable<IPlaceholderProvider> phProviders)
{
foreach (var provider in phProviders)
{
foreach (var ovr in provider.GetPlaceholders())
{
_reps.TryAdd(ovr.Name, ovr.Func);
}
}
return this;
}
}
}

View file

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace NadekoBot.Common.Replacements
{
public class Replacer
{
private readonly IEnumerable<(string Key, Func<string> Text)> _replacements;
private readonly IEnumerable<(Regex Regex, Func<Match, string> Replacement)> _regex;
public Replacer(IEnumerable<(string, Func<string>)> replacements, IEnumerable<(Regex, Func<Match, string>)> regex)
{
_replacements = replacements;
_regex = regex;
}
public string Replace(string input)
{
if (string.IsNullOrWhiteSpace(input))
return input;
foreach (var (Key, Text) in _replacements)
{
if (input.Contains(Key))
input = input.Replace(Key, Text(), StringComparison.InvariantCulture);
}
foreach (var item in _regex)
{
input = item.Regex.Replace(input, (m) => item.Replacement(m));
}
return input;
}
public CREmbed Replace(CREmbed embedData)
{
embedData.PlainText = Replace(embedData.PlainText);
embedData.Description = Replace(embedData.Description);
embedData.Title = Replace(embedData.Title);
embedData.Thumbnail = Replace(embedData.Thumbnail);
embedData.Image = Replace(embedData.Image);
if (embedData.Author != null)
{
embedData.Author.Name = Replace(embedData.Author.Name);
embedData.Author.IconUrl = Replace(embedData.Author.IconUrl);
}
if (embedData.Fields != null)
foreach (var f in embedData.Fields)
{
f.Name = Replace(f.Name);
f.Value = Replace(f.Value);
}
if (embedData.Footer != null)
{
embedData.Footer.Text = Replace(embedData.Footer.Text);
embedData.Footer.IconUrl = Replace(embedData.Footer.IconUrl);
}
return embedData;
}
}
}

View file

@ -0,0 +1,16 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
namespace NadekoBot.Common
{
public class RequireObjectPropertiesContractResolver : DefaultContractResolver
{
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
contract.ItemRequired = Required.DisallowNull;
return contract;
}
}
}

View file

@ -0,0 +1,22 @@
using System;
using Discord;
namespace NadekoBot.Common.ShardCom
{
public class ShardComMessage
{
public int ShardId { get; set; }
public ConnectionState ConnectionState { get; set; }
public int Guilds { get; set; }
public DateTime Time { get; set; }
public ShardComMessage Clone() =>
new ShardComMessage
{
ShardId = ShardId,
ConnectionState = ConnectionState,
Guilds = Guilds,
Time = Time,
};
}
}

View file

@ -0,0 +1,28 @@
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NadekoBot.Core.Services;
namespace NadekoBot.Common.ShardCom
{
public class ShardComServer
{
private readonly IDataCache _cache;
public ShardComServer(IDataCache cache)
{
_cache = cache;
}
public void Start()
{
var sub = _cache.Redis.GetSubscriber();
sub.SubscribeAsync("shardcoord_send", (ch, data) =>
{
var _ = OnDataReceived(JsonConvert.DeserializeObject<ShardComMessage>(data));
}, StackExchange.Redis.CommandFlags.FireAndForget);
}
public event Func<ShardComMessage, Task> OnDataReceived = delegate { return Task.CompletedTask; };
}
}

View file

@ -0,0 +1,63 @@
using System;
namespace NadekoBot.Core.Common
{
public struct ShmartNumber : IEquatable<ShmartNumber>
{
public long Value { get; }
public string Input { get; }
public ShmartNumber(long val, string input = null)
{
Value = val;
Input = input;
}
public static implicit operator ShmartNumber(long num)
{
return new ShmartNumber(num);
}
public static implicit operator long(ShmartNumber num)
{
return num.Value;
}
public static implicit operator ShmartNumber(int num)
{
return new ShmartNumber(num);
}
public override string ToString()
{
return Value.ToString();
}
public override bool Equals(object obj)
{
return obj is ShmartNumber sn
? Equals(sn)
: false;
}
public bool Equals(ShmartNumber other)
{
return other.Value == Value;
}
public override int GetHashCode()
{
return Value.GetHashCode() ^ Input.GetHashCode(StringComparison.InvariantCulture);
}
public static bool operator ==(ShmartNumber left, ShmartNumber right)
{
return left.Equals(right);
}
public static bool operator !=(ShmartNumber left, ShmartNumber right)
{
return !(left == right);
}
}
}

View file

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

View file

@ -0,0 +1,9 @@
namespace NadekoBot.Common.TypeReaders
{
public enum AddRemove
{
Add = int.MinValue,
Rem = int.MinValue + 1,
Rm = int.MinValue + 1,
}
}

View file

@ -0,0 +1,87 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Discord.Commands;
using NadekoBot.Core.Services;
using NadekoBot.Modules.CustomReactions.Services;
using NadekoBot.Core.Common.TypeReaders;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Common.TypeReaders
{
public class CommandTypeReader : NadekoTypeReader<CommandInfo>
{
public CommandTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
var _cmds = services.GetService<CommandService>();
var _cmdHandler = services.GetService<CommandHandler>();
input = input.ToUpperInvariant();
var prefix = _cmdHandler.GetPrefix(context.Guild);
if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture))
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found."));
input = input.Substring(prefix.Length);
var cmd = _cmds.Commands.FirstOrDefault(c =>
c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input));
if (cmd == null)
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found."));
return Task.FromResult(TypeReaderResult.FromSuccess(cmd));
}
}
public class CommandOrCrTypeReader : NadekoTypeReader<CommandOrCrInfo>
{
private readonly DiscordSocketClient _client;
private readonly CommandService _cmds;
public CommandOrCrTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
_client = client;
_cmds = cmds;
}
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
input = input.ToUpperInvariant();
var _crs = services.GetService<CustomReactionsService>();
if (_crs.ReactionExists(context.Guild?.Id, input))
{
return TypeReaderResult.FromSuccess(new CommandOrCrInfo(input, CommandOrCrInfo.Type.Custom));
}
var cmd = await new CommandTypeReader(_client, _cmds).ReadAsync(context, input, services).ConfigureAwait(false);
if (cmd.IsSuccess)
{
return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name, CommandOrCrInfo.Type.Normal));
}
return TypeReaderResult.FromError(CommandError.ParseFailed, "No such command or cr found.");
}
}
public class CommandOrCrInfo
{
public enum Type
{
Normal,
Custom,
}
public string Name { get; set; }
public Type CmdType { get; set; }
public bool IsCustom => CmdType == Type.Custom;
public CommandOrCrInfo(string input, Type type)
{
this.Name = input;
this.CmdType = type;
}
}
}

View file

@ -0,0 +1,56 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Core.Common.TypeReaders;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Common.TypeReaders
{
public class GuildDateTimeTypeReader : NadekoTypeReader<GuildDateTime>
{
public GuildDateTimeTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
var gdt = Parse(services, context.Guild.Id, input);
if(gdt == null)
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format."));
return Task.FromResult(TypeReaderResult.FromSuccess(gdt));
}
public static GuildDateTime Parse(IServiceProvider services, ulong guildId, string input)
{
var _gts = services.GetService<GuildTimezoneService>();
if (!DateTime.TryParse(input, out var dt))
return null;
var tz = _gts.GetTimeZoneOrUtc(guildId);
return new GuildDateTime(tz, dt);
}
}
public class GuildDateTime
{
public TimeZoneInfo Timezone { get; }
public DateTime CurrentGuildTime { get; }
public DateTime InputTime { get; }
public DateTime InputTimeUtc { get; }
private GuildDateTime() { }
public GuildDateTime(TimeZoneInfo guildTimezone, DateTime inputTime)
{
var now = DateTime.UtcNow;
Timezone = guildTimezone;
CurrentGuildTime = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.Utc, Timezone);
InputTime = inputTime;
InputTimeUtc = TimeZoneInfo.ConvertTime(inputTime, Timezone, TimeZoneInfo.Utc);
}
}
}

View file

@ -0,0 +1,33 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Core.Common.TypeReaders;
using Discord;
namespace NadekoBot.Common.TypeReaders
{
public class GuildTypeReader : NadekoTypeReader<IGuild>
{
private readonly DiscordSocketClient _client;
public GuildTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
_client = client;
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider _)
{
input = input.Trim().ToUpperInvariant();
var guilds = _client.Guilds;
var guild = guilds.FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == input) ?? //by id
guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == input); //by name
if (guild != null)
return Task.FromResult(TypeReaderResult.FromSuccess(guild));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No guild by that name or Id found"));
}
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.WebSocket;
namespace NadekoBot.Core.Common.TypeReaders
{
public class KwumTypeReader : NadekoTypeReader<kwum>
{
public KwumTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
if (kwum.TryParse(input, out var val))
return Task.FromResult(TypeReaderResult.FromSuccess(val));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid kwum"));
}
}
}

View file

@ -0,0 +1,27 @@
namespace NadekoBot.Common.TypeReaders.Models
{
public class PermissionAction
{
public static PermissionAction Enable => new PermissionAction(true);
public static PermissionAction Disable => new PermissionAction(false);
public bool Value { get; }
public PermissionAction(bool value)
{
this.Value = value;
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
return this.Value == ((PermissionAction)obj).Value;
}
public override int GetHashCode() => Value.GetHashCode();
}
}

View file

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace NadekoBot.Core.Common.TypeReaders.Models
{
public class StoopidTime
{
public string Input { get; set; }
public TimeSpan Time { get; set; }
private static readonly Regex _regex = new Regex(
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
RegexOptions.Compiled | RegexOptions.Multiline);
private StoopidTime() { }
public static StoopidTime FromInput(string input)
{
var m = _regex.Match(input);
if (m.Length == 0)
{
throw new ArgumentException("Invalid string input format.");
}
string output = "";
var namesAndValues = new Dictionary<string, int>();
foreach (var groupName in _regex.GetGroupNames())
{
if (groupName == "0") continue;
if (!int.TryParse(m.Groups[groupName].Value, out var value))
{
namesAndValues[groupName] = 0;
continue;
}
if (value < 1)
{
throw new ArgumentException($"Invalid {groupName} value.");
}
namesAndValues[groupName] = value;
output += m.Groups[groupName].Value + " " + groupName + " ";
}
var ts = new TimeSpan(30 * namesAndValues["months"] +
7 * namesAndValues["weeks"] +
namesAndValues["days"],
namesAndValues["hours"],
namesAndValues["minutes"],
namesAndValues["seconds"]);
if (ts > TimeSpan.FromDays(90))
{
throw new ArgumentException("Time is too long.");
}
return new StoopidTime()
{
Input = input,
Time = ts,
};
}
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Core.Common.TypeReaders;
using Discord.WebSocket;
namespace NadekoBot.Common.TypeReaders
{
public class ModuleTypeReader : NadekoTypeReader<ModuleInfo>
{
private readonly CommandService _cmds;
public ModuleTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
_cmds = cmds;
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider _)
{
input = input.ToUpperInvariant();
var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key;
if (module == null)
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found."));
return Task.FromResult(TypeReaderResult.FromSuccess(module));
}
}
public class ModuleOrCrTypeReader : NadekoTypeReader<ModuleOrCrInfo>
{
private readonly CommandService _cmds;
public ModuleOrCrTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
_cmds = cmds;
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider _)
{
input = input.ToUpperInvariant();
var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key;
if (module == null && input != "ACTUALCUSTOMREACTIONS")
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found."));
return Task.FromResult(TypeReaderResult.FromSuccess(new ModuleOrCrInfo
{
Name = input,
}));
}
}
public class ModuleOrCrInfo
{
public string Name { get; set; }
}
}

View file

@ -0,0 +1,18 @@
using Discord.Commands;
using Discord.WebSocket;
namespace NadekoBot.Core.Common.TypeReaders
{
public abstract class NadekoTypeReader<T> : TypeReader
{
private readonly DiscordSocketClient _client;
private readonly CommandService _cmds;
private NadekoTypeReader() { }
protected NadekoTypeReader(DiscordSocketClient client, CommandService cmds)
{
_client = client;
_cmds = cmds;
}
}
}

View file

@ -0,0 +1,47 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.TypeReaders.Models;
using NadekoBot.Core.Common.TypeReaders;
namespace NadekoBot.Common.TypeReaders
{
/// <summary>
/// Used instead of bool for more flexible keywords for true/false only in the permission module
/// </summary>
public class PermissionActionTypeReader : NadekoTypeReader<PermissionAction>
{
public PermissionActionTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider _)
{
input = input.ToUpperInvariant();
switch (input)
{
case "1":
case "T":
case "TRUE":
case "ENABLE":
case "ENABLED":
case "ALLOW":
case "PERMIT":
case "UNBAN":
return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Enable));
case "0":
case "F":
case "FALSE":
case "DENY":
case "DISABLE":
case "DISABLED":
case "DISALLOW":
case "BAN":
return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Disable));
default:
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Did not receive a valid boolean value"));
}
}
}
}

View file

@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.WebSocket;
using SixLabors.ImageSharp;
namespace NadekoBot.Core.Common.TypeReaders
{
public class Rgba32TypeReader : NadekoTypeReader<Color>
{
public Rgba32TypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
}
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
await Task.Yield();
input = input.Replace("#", "", StringComparison.InvariantCulture);
try
{
return TypeReaderResult.FromSuccess(Color.ParseHex(input));
}
catch
{
return TypeReaderResult.FromError(CommandError.ParseFailed, "Parameter is not a valid color hex.");
}
}
}
}

View file

@ -0,0 +1,109 @@
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Core.Services;
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NadekoBot.Core.Modules.Gambling.Services;
namespace NadekoBot.Core.Common.TypeReaders
{
public class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
{
public ShmartNumberTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
}
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(input))
return TypeReaderResult.FromError(CommandError.ParseFailed, "Input is empty.");
var i = input.Trim().ToUpperInvariant();
i = i.Replace("K", "000");
//can't add m because it will conflict with max atm
if (TryHandlePercentage(services, context, i, out var num))
return TypeReaderResult.FromSuccess(new ShmartNumber(num, i));
try
{
var expr = new NCalc.Expression(i, NCalc.EvaluateOptions.IgnoreCase);
expr.EvaluateParameter += (str, ev) => EvaluateParam(str, ev, context, services);
var lon = (long)(decimal.Parse(expr.Evaluate().ToString()));
return TypeReaderResult.FromSuccess(new ShmartNumber(lon, input));
}
catch (Exception)
{
return TypeReaderResult.FromError(CommandError.ParseFailed, $"Invalid input: {input}");
}
}
private static void EvaluateParam(string name, NCalc.ParameterArgs args, ICommandContext ctx, IServiceProvider svc)
{
switch (name.ToUpperInvariant())
{
case "PI":
args.Result = Math.PI;
break;
case "E":
args.Result = Math.E;
break;
case "ALL":
case "ALLIN":
args.Result = Cur(svc, ctx);
break;
case "HALF":
args.Result = Cur(svc, ctx) / 2;
break;
case "MAX":
args.Result = Max(svc, ctx);
break;
default:
break;
}
}
private static readonly Regex percentRegex = new Regex(@"^((?<num>100|\d{1,2})%)$", RegexOptions.Compiled);
private static long Cur(IServiceProvider services, ICommandContext ctx)
{
var _db = services.GetService<DbService>();
long cur;
using (var uow = _db.GetDbContext())
{
cur = uow.DiscordUsers.GetUserCurrency(ctx.User.Id);
uow.SaveChanges();
}
return cur;
}
private static long Max(IServiceProvider services, ICommandContext ctx)
{
var settings = services.GetService<GamblingConfigService>().Data;
var max = settings.MaxBet;
return max == 0
? Cur(services, ctx)
: max;
}
private static bool TryHandlePercentage(IServiceProvider services, ICommandContext ctx, string input, out long num)
{
num = 0;
var m = percentRegex.Match(input);
if (m.Captures.Count != 0)
{
if (!long.TryParse(m.Groups["num"].ToString(), out var percent))
return false;
num = (long)(Cur(services, ctx) * (percent / 100.0f));
return true;
}
return false;
}
}
}

View file

@ -0,0 +1,30 @@
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Core.Common.TypeReaders.Models;
using System;
using System.Threading.Tasks;
namespace NadekoBot.Core.Common.TypeReaders
{
public class StoopidTimeTypeReader : NadekoTypeReader<StoopidTime>
{
public StoopidTimeTypeReader(DiscordSocketClient client, CommandService cmds) : base(client, cmds)
{
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
if (string.IsNullOrWhiteSpace(input))
return Task.FromResult(TypeReaderResult.FromError(CommandError.Unsuccessful, "Input is empty."));
try
{
var time = StoopidTime.FromInput(input);
return Task.FromResult(TypeReaderResult.FromSuccess(time));
}
catch (Exception ex)
{
return Task.FromResult(TypeReaderResult.FromError(CommandError.Exception, ex.Message));
}
}
}
}

View file

@ -0,0 +1,14 @@
using System;
namespace NadekoBot.Common.Yml
{
public class CommentAttribute : Attribute
{
public string Comment { get; }
public CommentAttribute(string comment)
{
Comment = comment;
}
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.TypeInspectors;
namespace NadekoBot.Common.Yml
{
public class CommentGatheringTypeInspector : TypeInspectorSkeleton
{
private readonly ITypeInspector innerTypeDescriptor;
public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor)
{
this.innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException("innerTypeDescriptor");
}
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
{
return innerTypeDescriptor
.GetProperties(type, container)
.Select(d => new CommentsPropertyDescriptor(d));
}
private sealed class CommentsPropertyDescriptor : IPropertyDescriptor
{
private readonly IPropertyDescriptor baseDescriptor;
public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor)
{
this.baseDescriptor = baseDescriptor;
Name = baseDescriptor.Name;
}
public string Name { get; set; }
public Type Type { get { return baseDescriptor.Type; } }
public Type TypeOverride {
get { return baseDescriptor.TypeOverride; }
set { baseDescriptor.TypeOverride = value; }
}
public int Order { get; set; }
public ScalarStyle ScalarStyle {
get { return baseDescriptor.ScalarStyle; }
set { baseDescriptor.ScalarStyle = value; }
}
public bool CanWrite { get { return baseDescriptor.CanWrite; } }
public void Write(object target, object value)
{
baseDescriptor.Write(target, value);
}
public T GetCustomAttribute<T>() where T : Attribute
{
return baseDescriptor.GetCustomAttribute<T>();
}
public IObjectDescriptor Read(object target)
{
var comment = baseDescriptor.GetCustomAttribute<CommentAttribute>();
return comment != null
? new CommentsObjectDescriptor(baseDescriptor.Read(target), comment.Comment)
: baseDescriptor.Read(target);
}
}
}
}

View file

@ -0,0 +1,24 @@
using System;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
namespace NadekoBot.Common.Yml
{
public sealed class CommentsObjectDescriptor : IObjectDescriptor
{
private readonly IObjectDescriptor innerDescriptor;
public CommentsObjectDescriptor(IObjectDescriptor innerDescriptor, string comment)
{
this.innerDescriptor = innerDescriptor;
this.Comment = comment;
}
public string Comment { get; private set; }
public object Value { get { return innerDescriptor.Value; } }
public Type Type { get { return innerDescriptor.Type; } }
public Type StaticType { get { return innerDescriptor.StaticType; } }
public ScalarStyle ScalarStyle { get { return innerDescriptor.ScalarStyle; } }
}
}

View file

@ -0,0 +1,26 @@
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.ObjectGraphVisitors;
namespace NadekoBot.Common.Yml
{
public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor
{
public CommentsObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor)
: base(nextVisitor)
{
}
public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
{
var commentsDescriptor = value as CommentsObjectDescriptor;
if (commentsDescriptor != null && !string.IsNullOrWhiteSpace(commentsDescriptor.Comment))
{
context.Emit(new Comment(commentsDescriptor.Comment.Replace("\n", "\n# "), false));
}
return base.EnterMapping(key, value, context);
}
}
}

View file

@ -0,0 +1,32 @@
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.EventEmitters;
namespace NadekoBot.Common.Yml
{
public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
{
public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter)
: base(nextEmitter) { }
public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
{
if (typeof(string).IsAssignableFrom(eventInfo.Source.Type))
{
string value = eventInfo.Source.Value as string;
if (!string.IsNullOrEmpty(value))
{
bool isMultiLine = value.IndexOfAny(new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0;
if (isMultiLine)
eventInfo = new ScalarEventInfo(eventInfo.Source)
{
Style = ScalarStyle.Literal,
};
}
}
nextEmitter.Emit(eventInfo, emitter);
}
}
}

View file

@ -0,0 +1,52 @@
using System;
using System.Globalization;
using SixLabors.ImageSharp.PixelFormats;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
namespace NadekoBot.Common.Yml
{
public class Rgba32Converter : IYamlTypeConverter
{
public bool Accepts(Type type)
{
return type == typeof(Rgba32);
}
public object ReadYaml(IParser parser, Type type)
{
var scalar = parser.Consume<Scalar>();
var result = Rgba32.ParseHex(scalar.Value);
return result;
}
public void WriteYaml(IEmitter emitter, object value, Type type)
{
var color = (Rgba32)value;
var val = (uint) (color.B << 0 | color.G << 8 | color.R << 16);
emitter.Emit(new Scalar(val.ToString("X6").ToLower()));
}
}
public class CultureInfoConverter : IYamlTypeConverter
{
public bool Accepts(Type type)
{
return type == typeof(CultureInfo);
}
public object ReadYaml(IParser parser, Type type)
{
var scalar = parser.Consume<Scalar>();
var result = new CultureInfo(scalar.Value);
return result;
}
public void WriteYaml(IEmitter emitter, object value, Type type)
{
var ci = (CultureInfo)value;
emitter.Emit(new Scalar(ci.Name));
}
}
}

View file

@ -0,0 +1,28 @@
using System;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
namespace NadekoBot.Common.Yml
{
public class UriConverter : IYamlTypeConverter
{
public bool Accepts(Type type)
{
return type == typeof(Uri);
}
public object ReadYaml(IParser parser, Type type)
{
var scalar = parser.Consume<Scalar>();
var result = new Uri(scalar.Value);
return result;
}
public void WriteYaml(IEmitter emitter, object value, Type type)
{
var uri = (Uri)value;
emitter.Emit(new Scalar(uri.ToString()));
}
}
}

View file

@ -0,0 +1,25 @@
using YamlDotNet.Serialization;
namespace NadekoBot.Common.Yml
{
public class Yaml
{
public static ISerializer Serializer => new SerializerBuilder()
.WithTypeInspector(inner => new CommentGatheringTypeInspector(inner))
.WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor))
.WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
.WithIndentedSequences()
.WithTypeConverter(new Rgba32Converter())
.WithTypeConverter(new CultureInfoConverter())
.WithTypeConverter(new UriConverter())
.Build();
public static IDeserializer Deserializer => new DeserializerBuilder()
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
.WithTypeConverter(new Rgba32Converter())
.WithTypeConverter(new CultureInfoConverter())
.WithTypeConverter(new UriConverter())
.Build();
}
}

View file

@ -0,0 +1,58 @@
namespace NadekoBot.Common.Yml
{
public class YamlHelper
{
// https://github.com/aaubry/YamlDotNet/blob/0f4cc205e8b2dd8ef6589d96de32bf608a687c6f/YamlDotNet/Core/Scanner.cs#L1687
/// <summary>
/// This is modified code from yamldotnet's repo which handles parsing unicode code points
/// it is needed as yamldotnet doesn't support unescaped unicode characters
/// </summary>
/// <param name="point">Unicode code point</param>
/// <returns>Actual character</returns>
public static string UnescapeUnicodeCodePoint(string point)
{
var character = 0;
// Scan the character value.
foreach(var c in point)
{
if (!IsHex(c))
{
return point;
}
character = (character << 4) + AsHex(c);
}
// Check the value and write the character.
if (character >= 0xD800 && character <= 0xDFFF || character > 0x10FFFF)
{
return point;
}
return char.ConvertFromUtf32(character);
}
public static bool IsHex(char c)
{
return
(c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f');
}
public static int AsHex(char c)
{
if (c <= '9')
{
return c - '0';
}
if (c <= 'F')
{
return c - 'A' + 10;
}
return c - 'a' + 10;
}
}
}

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