diff --git a/.deploy.yml b/.deploy.yml
deleted file mode 100644
index 5fb394d..0000000
--- a/.deploy.yml
+++ /dev/null
@@ -1,38 +0,0 @@
----
-- name: Geekbot Deploy
- hosts: all
- remote_user: geekbot
- vars:
- ansible_port: 65432
- ansible_python_interpreter: /usr/bin/python3
- tasks:
- - name: Login to Gitlab Docker Registry
- 'community.docker.docker_login':
- registry_url: "{{ lookup('env', 'CI_REGISTRY') }}"
- username: "{{ lookup('env', 'CI_REGISTRY_USER') }}"
- password: "{{ lookup('env', 'CI_REGISTRY_PASSWORD') }}"
- reauthorize: yes
- - name: Replace Prod Container
- 'community.docker.docker_container':
- name: GeekbotProd
- image: "{{ lookup('env', 'IMAGE_TAG') }}"
- recreate: yes
- pull: yes
- restart_policy: always
- keep_volumes: no
- ports:
- - "12995:12995"
- env:
- GEEKBOT_DB_HOST: "{{ lookup('env', 'GEEKBOT_DB_HOST') }}"
- GEEKBOT_DB_USER: "{{ lookup('env', 'GEEKBOT_DB_USER') }}"
- GEEKBOT_DB_PASSWORD: "{{ lookup('env', 'GEEKBOT_DB_PASSWORD') }}"
- GEEKBOT_DB_PORT: "{{ lookup('env', 'GEEKBOT_DB_PORT') }}"
- GEEKBOT_DB_DATABASE: "{{ lookup('env', 'GEEKBOT_DB_DATABASE') }}"
- GEEKBOT_DB_REQUIRE_SSL: "true"
- GEEKBOT_DB_TRUST_CERT: "true"
- GEEKBOT_SUMOLOGIC: "{{ lookup('env', 'GEEKBOT_SUMOLOCIG') }}"
- GEEKBOT_SENTRY: "{{ lookup('env', 'GEEKBOT_SENTRY') }}"
- GEEKBOT_DB_REDSHIFT_COMPAT: "true"
- - name: Cleanup Old Container
- 'community.docker.docker_prune':
- images: yes
diff --git a/.gitignore b/.gitignore
index 066c4b4..c99d9e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,12 @@
-/*/**/bin
-/*/**/obj
-src/Bot/tmp/
-src/Bot/Logs/*
-!/src/Bot/Logs/.keep
+Geekbot.net/bin
+Geekbot.net/obj
+Geekbot.net/tmp/
+Tests/bin
+Tests/obj
+Backup/
.vs/
+UpgradeLog.htm
.idea
.vscode
-Geekbot.net.sln.DotSettings.user
-app
+Geekbot.net/Logs/*
+!/Geekbot.net/Logs/.keep
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a774f5e..3928542 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,69 +1,54 @@
-stages:
- - build
- - docker
- - deploy
- - ops
-
-variables:
- VERSION: 4.4.0-V$CI_COMMIT_SHORT_SHA
- IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
-
-Build:
- stage: build
- image: mcr.microsoft.com/dotnet/sdk:6.0
- artifacts:
- expire_in: 1h
- paths:
- - app
- script:
- - dotnet restore
- - dotnet test tests
- - dotnet publish --version-suffix "$VERSION" -r linux-x64 -c Release -p:DebugType=embedded --no-self-contained -o ./app ./src/Startup/
-
-Package:
- stage: docker
- image: docker
- only:
- - master
- services:
- - docker:stable-dind
- script:
- - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- - docker build -t $IMAGE_TAG .
- - docker push $IMAGE_TAG
-
-Deploy:
- stage: deploy
- image: quay.io/ansible/ansible-runner:stable-2.12-latest
- only:
- - master
- variables:
- ANSIBLE_NOCOWS: 1
- before_script:
- - mkdir /root/.ssh
- - cp $SSH_PRIVATE_KEY /root/.ssh/id_ed25519
- - cp $SSH_PUBLIC_KEY /root/.ssh/id_ed25519.pub
- - chmod -R 600 /root/.ssh
- - ssh-keyscan -p 65432 $PROD_IP > /root/.ssh/known_hosts
- script:
- - ansible-galaxy collection install -r ansible-requirements.yml
- - ansible-playbook -i $PROD_IP, .deploy.yml
-
-Sentry:
- stage: ops
- image: getsentry/sentry-cli
- allow_failure: true
- only:
- - master
- script:
- - sentry-cli releases new -p geekbot $VERSION
- - sentry-cli releases set-commits --auto $VERSION
- - sentry-cli releases deploys $VERSION new -e Production
-
-Github Mirror:
- stage: ops
- image: runebaas/rsync-ssh-git
- only:
- - master
- script:
- - git push https://runebaas:$TOKEN@github.com/pizzaandcoffee/Geekbot.net.git origin/master:master -f
+stages:
+ - build
+ - deploy
+
+before_script:
+ - set -e
+ - set -u
+ - set -o pipefail
+
+build:
+ stage: build
+ image: microsoft/dotnet:2.0.3-sdk-stretch
+ variables:
+ NUGET_PACKAGES: "${CI_PROJECT_DIR}/.nugetcache"
+ cache:
+ paths:
+ - .nugetcache
+ artifacts:
+ expire_in: 1h
+ paths:
+ - Geekbot.net/Binaries/
+ script:
+ - dotnet restore
+ - dotnet test Tests
+ - dotnet publish --configuration Release -o Binaries ./
+
+deploy:
+ stage: deploy
+ image: instrumentisto/rsync-ssh
+ only:
+ - master
+ dependencies:
+ - build
+ environment:
+ name: Production
+ url: https://discordapp.com/oauth2/authorize?client_id=171249478546882561&scope=bot&permissions=1416834054
+ before_script:
+ - eval $(ssh-agent -s)
+ - mkdir -p ~/.ssh
+ - '[[ -f /.dockerenv ]] && echo -e "Host *\n StrictHostKeyChecking no" > ~/.ssh/config'
+ - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
+ - chmod 700 ~/.ssh
+ script:
+ - rsync -rav -e "ssh -p 65432" ./Geekbot.net/Binaries/* www-data@31.220.42.224:$DEPPATH
+ - ssh -p 65432 www-data@31.220.42.224 "sudo systemctl restart geekbot.service"
+
+mirror:
+ stage: deploy
+ image: bravissimolabs/alpine-git:latest
+ only:
+ - master
+ script:
+ - git push https://runebaas:$TOKEN@github.com/pizzaandcoffee/Geekbot.net.git origin/master:master -f
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 39529ff..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,7 +0,0 @@
-FROM mcr.microsoft.com/dotnet/aspnet:6.0
-
-COPY ./app /app/
-
-EXPOSE 12995/tcp
-WORKDIR /app
-ENTRYPOINT ./Geekbot
diff --git a/Geekbot.net.sln b/Geekbot.net.sln
index f33d887..5011ad4 100644
--- a/Geekbot.net.sln
+++ b/Geekbot.net.sln
@@ -3,19 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.0.0
MinimumVisualStudioVersion = 10.0.0.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "tests\Tests.csproj", "{4CAF5F02-EFFE-4FDA-BD44-EEADDBA9600E}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Geekbot.net", "Geekbot.net/Geekbot.net.csproj", "{FDCB3D92-E7B5-47BB-A9B5-CFAEFA57CDB4}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Core\Core.csproj", "{47671723-52A9-4668-BBC5-2BA76AE3B288}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "src\Web\Web.csproj", "{0A63D5DC-6325-4F53-8ED2-9843239B76CC}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bot", "src\Bot\Bot.csproj", "{DBF79896-9F7F-443D-B336-155E276DFF16}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Commands", "src\Commands\Commands.csproj", "{7C771DFE-912A-4276-B0A6-047E09603F1E}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Interactions", "src\Interactions\Interactions.csproj", "{FF6859D9-C539-4910-BE1E-9ECFED2F46FA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Startup", "src\Startup\Startup.csproj", "{A691B018-4B19-4A7A-A0F6-DBB17641254F}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{4CAF5F02-EFFE-4FDA-BD44-EEADDBA9600E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -23,34 +13,14 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {FDCB3D92-E7B5-47BB-A9B5-CFAEFA57CDB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FDCB3D92-E7B5-47BB-A9B5-CFAEFA57CDB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FDCB3D92-E7B5-47BB-A9B5-CFAEFA57CDB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FDCB3D92-E7B5-47BB-A9B5-CFAEFA57CDB4}.Release|Any CPU.Build.0 = Release|Any CPU
{4CAF5F02-EFFE-4FDA-BD44-EEADDBA9600E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CAF5F02-EFFE-4FDA-BD44-EEADDBA9600E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4CAF5F02-EFFE-4FDA-BD44-EEADDBA9600E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CAF5F02-EFFE-4FDA-BD44-EEADDBA9600E}.Release|Any CPU.Build.0 = Release|Any CPU
- {47671723-52A9-4668-BBC5-2BA76AE3B288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {47671723-52A9-4668-BBC5-2BA76AE3B288}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {47671723-52A9-4668-BBC5-2BA76AE3B288}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {47671723-52A9-4668-BBC5-2BA76AE3B288}.Release|Any CPU.Build.0 = Release|Any CPU
- {0A63D5DC-6325-4F53-8ED2-9843239B76CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0A63D5DC-6325-4F53-8ED2-9843239B76CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0A63D5DC-6325-4F53-8ED2-9843239B76CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0A63D5DC-6325-4F53-8ED2-9843239B76CC}.Release|Any CPU.Build.0 = Release|Any CPU
- {DBF79896-9F7F-443D-B336-155E276DFF16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DBF79896-9F7F-443D-B336-155E276DFF16}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DBF79896-9F7F-443D-B336-155E276DFF16}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DBF79896-9F7F-443D-B336-155E276DFF16}.Release|Any CPU.Build.0 = Release|Any CPU
- {7C771DFE-912A-4276-B0A6-047E09603F1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7C771DFE-912A-4276-B0A6-047E09603F1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7C771DFE-912A-4276-B0A6-047E09603F1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7C771DFE-912A-4276-B0A6-047E09603F1E}.Release|Any CPU.Build.0 = Release|Any CPU
- {FF6859D9-C539-4910-BE1E-9ECFED2F46FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FF6859D9-C539-4910-BE1E-9ECFED2F46FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FF6859D9-C539-4910-BE1E-9ECFED2F46FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FF6859D9-C539-4910-BE1E-9ECFED2F46FA}.Release|Any CPU.Build.0 = Release|Any CPU
- {A691B018-4B19-4A7A-A0F6-DBB17641254F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A691B018-4B19-4A7A-A0F6-DBB17641254F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A691B018-4B19-4A7A-A0F6-DBB17641254F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A691B018-4B19-4A7A-A0F6-DBB17641254F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Geekbot.net.sln.DotSettings b/Geekbot.net.sln.DotSettings
deleted file mode 100644
index 6d439b8..0000000
--- a/Geekbot.net.sln.DotSettings
+++ /dev/null
@@ -1,12 +0,0 @@
-
- NEVER
- 200
- True
- True
- True
- True
- True
- True
- True
- True
- True
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Admin.cs b/Geekbot.net/Commands/Admin.cs
new file mode 100644
index 0000000..0b76b4b
--- /dev/null
+++ b/Geekbot.net/Commands/Admin.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Text;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ [Group("admin")]
+ [RequireUserPermission(GuildPermission.Administrator)]
+ public class Admin : ModuleBase
+ {
+ private readonly DiscordSocketClient _client;
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+ private readonly ITranslationHandler _translation;
+
+ public Admin(IDatabase redis, DiscordSocketClient client, IErrorHandler errorHandler,
+ ITranslationHandler translationHandler)
+ {
+ _redis = redis;
+ _client = client;
+ _errorHandler = errorHandler;
+ _translation = translationHandler;
+ }
+
+ [Command("welcome", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Set a Welcome Message (use '$user' to mention the new joined user).")]
+ public async Task SetWelcomeMessage([Remainder] [Summary("message")] string welcomeMessage)
+ {
+ _redis.HashSet($"{Context.Guild.Id}:Settings", new[] {new HashEntry("WelcomeMsg", welcomeMessage)});
+ var formatedMessage = welcomeMessage.Replace("$user", Context.User.Mention);
+ await ReplyAsync("Welcome message has been changed\r\nHere is an example of how it would look:\r\n" +
+ formatedMessage);
+ }
+
+ [Command("modchannel", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Set a channel for moderation purposes")]
+ public async Task selectModChannel([Summary("#Channel")] ISocketMessageChannel channel)
+ {
+ try
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("Successfully saved mod channel, you can now do the following");
+ sb.AppendLine("- `!admin showleave true` - send message to mod channel when someone leaves");
+ sb.AppendLine("- `!admin showdel true` - send message to mod channel when someone deletes a message");
+ await channel.SendMessageAsync(sb.ToString());
+ _redis.HashSet($"{Context.Guild.Id}:Settings",
+ new[] {new HashEntry("ModChannel", channel.Id.ToString())});
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context, "That channel doesn't seem to be valid");
+ }
+ }
+
+ [Command("showleave", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Notify modchannel when someone leaves")]
+ public async Task showLeave([Summary("true/false")] bool enabled)
+ {
+ var modChannelId = ulong.Parse(_redis.HashGet($"{Context.Guild.Id}:Settings", "ModChannel"));
+ try
+ {
+ var modChannel = (ISocketMessageChannel) _client.GetChannel(modChannelId);
+ if (enabled)
+ {
+ await modChannel.SendMessageAsync("Saved - now sending messages here when someone leaves");
+ _redis.HashSet($"{Context.Guild.Id}:Settings", new[] {new HashEntry("ShowLeave", true)});
+ }
+ else
+ {
+ await modChannel.SendMessageAsync("Saved - stopping sending messages here when someone leaves");
+ _redis.HashSet($"{Context.Guild.Id}:Settings", new[] {new HashEntry("ShowLeave", false)});
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ "Modchannel doesn't seem to exist, please set one with `!admin modchannel [channelId]`");
+ }
+ }
+
+ [Command("showdel", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Notify modchannel when someone deletes a message")]
+ public async Task showDelete([Summary("true/false")] bool enabled)
+ {
+ var modChannelId = ulong.Parse(_redis.HashGet($"{Context.Guild.Id}:Settings", "ModChannel"));
+ try
+ {
+ var modChannel = (ISocketMessageChannel) _client.GetChannel(modChannelId);
+ if (enabled)
+ {
+ await modChannel.SendMessageAsync(
+ "Saved - now sending messages here when someone deletes a message");
+ _redis.HashSet($"{Context.Guild.Id}:Settings", new[] {new HashEntry("ShowDelete", true)});
+ }
+ else
+ {
+ await modChannel.SendMessageAsync(
+ "Saved - stopping sending messages here when someone deletes a message");
+ _redis.HashSet($"{Context.Guild.Id}:Settings", new[] {new HashEntry("ShowDelete", false)});
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ "Modchannel doesn't seem to exist, please set one with `!admin modchannel [channelId]`");
+ }
+ }
+
+ [Command("setlang", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Change the bots language")]
+ public async Task setLanguage([Summary("language")] string languageRaw)
+ {
+ try
+ {
+ var language = languageRaw.ToUpper();
+ var success = _translation.SetLanguage(Context.Guild.Id, language);
+ if (success)
+ {
+ var trans = _translation.GetDict(Context);
+ await ReplyAsync(trans["NewLanguageSet"]);
+ return;
+ }
+
+ await ReplyAsync(
+ $"That doesn't seem to be a supported language\r\nSupported Languages are {string.Join(", ", _translation.GetSupportedLanguages())}");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command("lang", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Change the bots language")]
+ public async Task getLanguage()
+ {
+ try
+ {
+ var trans = _translation.GetDict(Context);
+ await ReplyAsync(trans["GetLanguage"]);
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bot/Commands/Utils/AvatarGetter.cs b/Geekbot.net/Commands/AvatarGetter.cs
similarity index 54%
rename from src/Bot/Commands/Utils/AvatarGetter.cs
rename to Geekbot.net/Commands/AvatarGetter.cs
index 458eec8..9e31b6b 100644
--- a/src/Bot/Commands/Utils/AvatarGetter.cs
+++ b/Geekbot.net/Commands/AvatarGetter.cs
@@ -2,12 +2,11 @@
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
-using Geekbot.Core;
-using Geekbot.Core.ErrorHandling;
+using Geekbot.net.Lib;
-namespace Geekbot.Bot.Commands.Utils
+namespace Geekbot.net.Commands
{
- public class AvatarGetter : TransactionModuleBase
+ public class AvatarGetter : ModuleBase
{
private readonly IErrorHandler _errorHandler;
@@ -17,18 +16,19 @@ namespace Geekbot.Bot.Commands.Utils
}
[Command("avatar", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
[Summary("Get someones avatar")]
- public async Task GetAvatar([Remainder, Summary("@someone")] IUser user = null)
+ public async Task getAvatar([Remainder] [Summary("user")] IUser user = null)
{
try
{
- user ??= Context.User;
- var url = user.GetAvatarUrl(ImageFormat.Auto, 1024);
+ if (user == null) user = Context.User;
+ var url = user.GetAvatarUrl().Replace("128", "1024");
await ReplyAsync(url);
}
catch (Exception e)
{
- await _errorHandler.HandleCommandException(e, Context);
+ _errorHandler.HandleCommandException(e, Context);
}
}
}
diff --git a/Geekbot.net/Commands/BattleTag.cs b/Geekbot.net/Commands/BattleTag.cs
new file mode 100644
index 0000000..cbc5ecc
--- /dev/null
+++ b/Geekbot.net/Commands/BattleTag.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.Commands
+{
+ [Group("battletag")]
+ public class BattleTag : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IUserRepository _userRepository;
+
+ public BattleTag(IErrorHandler errorHandler, IUserRepository userRepository)
+ {
+ _errorHandler = errorHandler;
+ _userRepository = userRepository;
+ }
+
+ [Command(RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Games)]
+ [Summary("Get your battletag")]
+ public async Task BattleTagCmd()
+ {
+ try
+ {
+ var tag = _userRepository.getUserSetting(Context.User.Id, "BattleTag");
+ if (!string.IsNullOrEmpty(tag))
+ await ReplyAsync($"Your BattleTag is {tag}");
+ else
+ await ReplyAsync("You haven't set your BattleTag, set it with `!battletag user#1234`");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command(RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Games)]
+ [Summary("Save your battletag")]
+ public async Task BattleTagCmd([Summary("Battletag")] string tag)
+ {
+ try
+ {
+ if (isValidTag(tag))
+ {
+ _userRepository.saveUserSetting(Context.User.Id, "BattleTag", tag);
+ await ReplyAsync("Saved!");
+ }
+ else
+ {
+ await ReplyAsync("That doesn't seem to be a valid battletag");
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ public static bool isValidTag(string tag)
+ {
+ var splited = tag.Split("#");
+ if (splited.Length != 2) return false;
+ if (!int.TryParse(splited[1], out var discriminator)) return false;
+ if (splited[1].Length == 4 || splited[1].Length == 5) return true;
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Cat.cs b/Geekbot.net/Commands/Cat.cs
new file mode 100644
index 0000000..2120fe7
--- /dev/null
+++ b/Geekbot.net/Commands/Cat.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using Newtonsoft.Json;
+
+namespace Geekbot.net.Commands
+{
+ public class Cat : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+
+ public Cat(IErrorHandler errorHandler)
+ {
+ _errorHandler = errorHandler;
+ }
+
+ [Command("cat", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Return a random image of a cat.")]
+ public async Task Say()
+ {
+ try
+ {
+ using (var client = new HttpClient())
+ {
+ try
+ {
+ client.BaseAddress = new Uri("http://random.cat");
+ var response = await client.GetAsync("/meow.php");
+ response.EnsureSuccessStatusCode();
+
+ var stringResponse = await response.Content.ReadAsStringAsync();
+ var catFile = JsonConvert.DeserializeObject(stringResponse);
+ var eb = new EmbedBuilder();
+ eb.ImageUrl = catFile.file;
+ await ReplyAsync("", false, eb.Build());
+ }
+ catch (HttpRequestException e)
+ {
+ await ReplyAsync($"Seems like the dog cought the cat (error occured)\r\n{e.Message}");
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private class CatResponse
+ {
+ public string file { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Changelog.cs b/Geekbot.net/Commands/Changelog.cs
new file mode 100644
index 0000000..e060152
--- /dev/null
+++ b/Geekbot.net/Commands/Changelog.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Geekbot.net.Lib;
+using Newtonsoft.Json;
+
+namespace Geekbot.net.Commands
+{
+ public class Changelog : ModuleBase
+ {
+ private readonly DiscordSocketClient _client;
+ private readonly IErrorHandler _errorHandler;
+
+ public Changelog(IErrorHandler errorHandler, DiscordSocketClient client)
+ {
+ _errorHandler = errorHandler;
+ _client = client;
+ }
+
+ [Command("changelog", RunMode = RunMode.Async)]
+ [Alias("updates")]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Show the latest 5 updates")]
+ public async Task getChangelog()
+ {
+ try
+ {
+ using (var client = new HttpClient())
+ {
+ client.BaseAddress = new Uri("https://api.github.com");
+ client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent",
+ "http://developer.github.com/v3/#user-agent-required");
+ var response = await client.GetAsync("/repos/pizzaandcoffee/geekbot.net/commits");
+ response.EnsureSuccessStatusCode();
+
+ var stringResponse = await response.Content.ReadAsStringAsync();
+ var commits = JsonConvert.DeserializeObject>(stringResponse);
+ var eb = new EmbedBuilder();
+ eb.WithColor(new Color(143, 165, 102));
+ eb.WithAuthor(new EmbedAuthorBuilder
+ {
+ IconUrl = _client.CurrentUser.GetAvatarUrl(),
+ Name = "Latest Updates",
+ Url = "https://geekbot.pizzaandcoffee.rocks/updates"
+ });
+ var sb = new StringBuilder();
+ foreach (var commit in commits.Take(10))
+ sb.AppendLine($"- {commit.commit.message} ({commit.commit.author.date:yyyy-MM-dd})");
+ eb.Description = sb.ToString();
+ eb.WithFooter(new EmbedFooterBuilder
+ {
+ Text = $"List generated from github commits on {DateTime.Now:yyyy-MM-dd}"
+ });
+ await ReplyAsync("", false, eb.Build());
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private class Commit
+ {
+ public string sha { get; set; }
+ public CommitInfo commit { get; set; }
+ public Uri html_url { get; set; }
+ }
+
+ private class CommitInfo
+ {
+ public commitAuthor author { get; set; }
+ public string message { get; set; }
+ }
+
+ private class commitAuthor
+ {
+ public string name { get; set; }
+ public string email { get; set; }
+ public DateTimeOffset date { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/CheckEm.cs b/Geekbot.net/Commands/CheckEm.cs
new file mode 100644
index 0000000..11d0cef
--- /dev/null
+++ b/Geekbot.net/Commands/CheckEm.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using Geekbot.net.Lib.Media;
+
+namespace Geekbot.net.Commands
+{
+ public class CheckEm : ModuleBase
+ {
+ private readonly IMediaProvider _checkEmImages;
+ private readonly IErrorHandler _errorHandler;
+ private readonly Random _rnd;
+
+ public CheckEm(Random RandomClient, IMediaProvider mediaProvider, IErrorHandler errorHandler)
+ {
+ _rnd = RandomClient;
+ _checkEmImages = mediaProvider;
+ _errorHandler = errorHandler;
+ }
+
+ [Command("checkem", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Check for dubs")]
+ public async Task MuhDubs()
+ {
+ try
+ {
+ var number = _rnd.Next(10000000, 99999999);
+ var dubtriqua = "";
+
+ var ns = GetIntArray(number);
+ if (ns[7] == ns[6])
+ {
+ dubtriqua = "DUBS";
+ if (ns[6] == ns[5])
+ {
+ dubtriqua = "TRIPS";
+ if (ns[5] == ns[4])
+ dubtriqua = "QUADS";
+ }
+ }
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Check em {Context.User.Mention}");
+ sb.AppendLine($"**{number}**");
+ if (!string.IsNullOrEmpty(dubtriqua))
+ sb.AppendLine($":tada: {dubtriqua} :tada:");
+ sb.AppendLine(_checkEmImages.getCheckem());
+
+ await ReplyAsync(sb.ToString());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private int[] GetIntArray(int num)
+ {
+ var listOfInts = new List();
+ while (num > 0)
+ {
+ listOfInts.Add(num % 10);
+ num = num / 10;
+ }
+
+ listOfInts.Reverse();
+ return listOfInts.ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Choose.cs b/Geekbot.net/Commands/Choose.cs
new file mode 100644
index 0000000..214a1a8
--- /dev/null
+++ b/Geekbot.net/Commands/Choose.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.Commands
+{
+ public class Choose : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly Random _rnd;
+ private readonly ITranslationHandler _translation;
+
+ public Choose(Random RandomClient, IErrorHandler errorHandler, ITranslationHandler translation)
+ {
+ _rnd = RandomClient;
+ _errorHandler = errorHandler;
+ _translation = translation;
+ }
+
+ [Command("choose", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Let the bot choose for you, seperate options with a semicolon.")]
+ public async Task Command([Remainder] [Summary("option1;option2")]
+ string choices)
+ {
+ try
+ {
+ var transDict = _translation.GetDict(Context);
+ var choicesArray = choices.Split(';');
+ var choice = _rnd.Next(choicesArray.Length);
+ await ReplyAsync(string.Format(transDict["Choice"], choicesArray[choice]));
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/ContextHandler.cs b/Geekbot.net/Commands/ContextHandler.cs
new file mode 100644
index 0000000..4e978b2
--- /dev/null
+++ b/Geekbot.net/Commands/ContextHandler.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+
+namespace Geekbot.net.Commands
+{
+ public class ContextHandler : IContextHandler
+ {
+ private Dictionary _store;
+ private readonly IDiscordClient _client;
+ private readonly CommandService _commandService;
+ private readonly IServiceProvider _servicesProvider;
+
+ public ContextHandler(IDiscordClient client, CommandService commandService, IServiceProvider servicesProvider)
+ {
+ _store = new Dictionary();
+ _commandService = commandService;
+ _servicesProvider = servicesProvider;
+ _client = client;
+ }
+
+ public ContextReference HasContext(IUserMessage message)
+ {
+ if (_store.ContainsKey(message.Author.Id)) return ContextReference.User;
+
+ if (_store.ContainsKey(message.Channel.Id)) return ContextReference.Channel;
+
+ return ContextReference.None;
+ }
+
+ public void SaveContext(ContextReference type, ICommandContext context, string commandName)
+ {
+ var contextStore = new ContextStore()
+ {
+ CommandName = commandName
+ };
+ var id = GetId(type, context.Message);
+ _store.Add(id, contextStore);
+ }
+
+ public async void ExecuteOnContext(ContextReference type, IUserMessage message)
+ {
+ var id = GetId(type, message);
+ var obj = _store[id];
+ var context = new CommandContext(_client, message);
+ var rest = await _commandService.ExecuteAsync(context, $"{obj.CommandSearch} {context.Message?.Content}", _servicesProvider);
+ if (!rest.IsSuccess)
+ {
+ await context.Channel.SendMessageAsync(rest.ErrorReason);
+ }
+ }
+
+ public void ClearContext(ulong id)
+ {
+ _store.Remove(id);
+ }
+
+ private ulong GetId(ContextReference type, IUserMessage message)
+ {
+ if (type == ContextReference.Channel)
+ {
+ return message.Author.Id;
+ }
+
+ if (type == ContextReference.User)
+ {
+ return message.Author.Id;
+ }
+
+ throw new Exception("No Context Object Found");
+ }
+
+ private class ContextStore
+ {
+ public string CommandName { get; set; }
+ public string CommandSearch => $"{CommandName} ctx";
+ }
+ }
+
+ public enum ContextReference
+ {
+ User,
+ Channel,
+ None
+ }
+
+ public interface IContextHandler
+ {
+ ContextReference HasContext(IUserMessage message);
+ void SaveContext(ContextReference type, ICommandContext context, string commandName);
+ void ExecuteOnContext(ContextReference type, IUserMessage message);
+ void ClearContext(ulong id);
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Dice.cs b/Geekbot.net/Commands/Dice.cs
new file mode 100644
index 0000000..9195be8
--- /dev/null
+++ b/Geekbot.net/Commands/Dice.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.Commands
+{
+ public class Dice : ModuleBase
+ {
+ private readonly Random _rnd;
+
+ public Dice(Random RandomClient)
+ {
+ _rnd = RandomClient;
+ }
+
+ [Command("dice", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Roll a dice.")]
+ public async Task RollCommand([Remainder] [Summary("diceType")] string diceType = "1d20")
+ {
+ var splitedDices = diceType.Split("+");
+ var dices = new List();
+ var mod = 0;
+ foreach (var i in splitedDices)
+ {
+ var dice = toDice(i);
+ if (dice.sides != 0 && dice.times != 0)
+ {
+ dices.Add(dice);
+ }
+ else if (dice.mod != 0)
+ {
+ if (mod != 0)
+ {
+ await ReplyAsync("You can only have one mod");
+ return;
+ }
+
+ mod = dice.mod;
+ }
+ }
+
+ if (!dices.Any())
+ {
+ await ReplyAsync(
+ "That is not a valid dice, examples are: 1d20, 1d6, 2d6, 1d6+2, 1d6+2d8+1d20+6, etc...");
+ return;
+ }
+
+
+ if (dices.Any(d => d.times > 20))
+ {
+ await ReplyAsync("You can't throw more than 20 dices");
+ return;
+ }
+
+ if (dices.Any(d => d.sides > 120))
+ {
+ await ReplyAsync("A dice can't have more than 120 sides");
+ return;
+ }
+
+ var rep = new StringBuilder();
+ rep.AppendLine($":game_die: {Context.User.Mention}");
+ rep.Append("**Result:** ");
+ var resultStrings = new List();
+ var total = 0;
+ var extraText = "";
+ foreach (var dice in dices)
+ {
+ var results = new List();
+ for (var i = 0; i < dice.times; i++)
+ {
+ var roll = _rnd.Next(1, dice.sides);
+ total += roll;
+ results.Add(roll);
+ if (roll == dice.sides) extraText = "**Critical Hit!**";
+ if (roll == 1) extraText = "**Critical Fail!**";
+ }
+
+ resultStrings.Add($"{dice.diceType} ({string.Join(",", results)})");
+ }
+
+ rep.Append(string.Join(" + ", resultStrings));
+ if (mod != 0)
+ {
+ rep.Append($" + {mod}");
+ total += mod;
+ }
+
+ rep.AppendLine();
+ rep.AppendLine($"**Total:** {total}");
+ if (extraText != "") rep.AppendLine(extraText);
+ await ReplyAsync(rep.ToString());
+ }
+
+ private DiceTypeDto toDice(string dice)
+ {
+ var diceParts = dice.Split('d');
+ if (diceParts.Length == 2
+ && int.TryParse(diceParts[0], out var times)
+ && int.TryParse(diceParts[1], out var max))
+ return new DiceTypeDto
+ {
+ diceType = dice,
+ times = times,
+ sides = max
+ };
+ if (dice.Length == 1
+ && int.TryParse(diceParts[0], out var mod))
+ return new DiceTypeDto
+ {
+ mod = mod
+ };
+ return new DiceTypeDto();
+ }
+ }
+
+ internal class DiceTypeDto
+ {
+ public string diceType { get; set; }
+ public int times { get; set; }
+ public int sides { get; set; }
+ public int mod { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Dog.cs b/Geekbot.net/Commands/Dog.cs
new file mode 100644
index 0000000..485b7f0
--- /dev/null
+++ b/Geekbot.net/Commands/Dog.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using Newtonsoft.Json;
+
+namespace Geekbot.net.Commands
+{
+ public class Dog : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+
+ public Dog(IErrorHandler errorHandler)
+ {
+ _errorHandler = errorHandler;
+ }
+
+ [Command("dog", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Return a random image of a dog.")]
+ public async Task Say()
+ {
+ try
+ {
+ using (var client = new HttpClient())
+ {
+ try
+ {
+ client.BaseAddress = new Uri("http://random.dog");
+ var response = await client.GetAsync("/woof.json");
+ response.EnsureSuccessStatusCode();
+
+ var stringResponse = await response.Content.ReadAsStringAsync();
+ var dogFile = JsonConvert.DeserializeObject(stringResponse);
+ var eb = new EmbedBuilder();
+ eb.ImageUrl = dogFile.url;
+ await ReplyAsync("", false, eb.Build());
+ }
+ catch (HttpRequestException e)
+ {
+ await ReplyAsync($"Seems like the dog got lost (error occured)\r\n{e.Message}");
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private class DogResponse
+ {
+ public string url { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/EightBall.cs b/Geekbot.net/Commands/EightBall.cs
new file mode 100644
index 0000000..5cadee5
--- /dev/null
+++ b/Geekbot.net/Commands/EightBall.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.Commands
+{
+ public class EightBall : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly Random _rnd;
+
+ public EightBall(Random RandomClient, IErrorHandler errorHandler)
+ {
+ _rnd = RandomClient;
+ _errorHandler = errorHandler;
+ }
+
+ [Command("8ball", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Ask 8Ball a Question.")]
+ public async Task Ball([Remainder] [Summary("Question")] string echo)
+ {
+ try
+ {
+ var replies = new List
+ {
+ "It is certain",
+ "It is decidedly so",
+ "Without a doubt",
+ "Yes, definitely",
+ "You may rely on it",
+ "As I see it, yes",
+ "Most likely",
+ "Outlook good",
+ "Yes",
+ "Signs point to yes",
+ "Reply hazy try again",
+ "Ask again later",
+ "Better not tell you now",
+ "Cannot predict now",
+ "Concentrate and ask again",
+ "Don't count on it",
+ "My reply is no",
+ "My sources say no",
+ "Outlook not so good",
+ "Very doubtful"
+ };
+
+ var answer = _rnd.Next(replies.Count);
+ await ReplyAsync(replies[answer]);
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bot/Commands/Utils/Emojify.cs b/Geekbot.net/Commands/Emojify.cs
similarity index 51%
rename from src/Bot/Commands/Utils/Emojify.cs
rename to Geekbot.net/Commands/Emojify.cs
index a513710..7bdbf8a 100644
--- a/src/Bot/Commands/Utils/Emojify.cs
+++ b/Geekbot.net/Commands/Emojify.cs
@@ -1,26 +1,29 @@
-using Discord.Commands;
-using Geekbot.Core;
-using Geekbot.Core.Converters;
-using Geekbot.Core.ErrorHandling;
+using System;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
-namespace Geekbot.Bot.Commands.Utils
+namespace Geekbot.net.Commands
{
- public class Emojify : TransactionModuleBase
+ public class Emojify : ModuleBase
{
+ private readonly IEmojiConverter _emojiConverter;
private readonly IErrorHandler _errorHandler;
- public Emojify(IErrorHandler errorHandler)
+ public Emojify(IErrorHandler errorHandler, IEmojiConverter emojiConverter)
{
_errorHandler = errorHandler;
+ _emojiConverter = emojiConverter;
}
[Command("emojify", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
[Summary("Emojify text")]
public async Task Dflt([Remainder] [Summary("text")] string text)
{
try
{
- var emojis = EmojiConverter.TextToEmoji(text);
+ var emojis = _emojiConverter.textToEmoji(text);
if (emojis.Length > 1999)
{
await ReplyAsync("I can't take that much at once!");
@@ -29,18 +32,10 @@ namespace Geekbot.Bot.Commands.Utils
await ReplyAsync($"{Context.User.Username}#{Context.User.Discriminator} said:");
await ReplyAsync(emojis);
- try
- {
- await Context.Message.DeleteAsync();
- }
- catch
- {
- // bot may not have enough permission, doesn't matter if it fails
- }
}
catch (Exception e)
{
- await _errorHandler.HandleCommandException(e, Context);
+ _errorHandler.HandleCommandException(e, Context);
}
}
}
diff --git a/src/Bot/Commands/Randomness/Fortune.cs b/Geekbot.net/Commands/Fortune.cs
similarity index 69%
rename from src/Bot/Commands/Randomness/Fortune.cs
rename to Geekbot.net/Commands/Fortune.cs
index 1157603..1335139 100644
--- a/src/Bot/Commands/Randomness/Fortune.cs
+++ b/Geekbot.net/Commands/Fortune.cs
@@ -1,11 +1,11 @@
using System.Threading.Tasks;
using Discord.Commands;
-using Geekbot.Core;
-using Geekbot.Core.Media;
+using Geekbot.net.Lib;
+using Geekbot.net.Lib.Media;
-namespace Geekbot.Bot.Commands.Randomness
+namespace Geekbot.net.Commands
{
- public class Fortune : TransactionModuleBase
+ public class Fortune : ModuleBase
{
private readonly IFortunesProvider _fortunes;
@@ -15,6 +15,7 @@ namespace Geekbot.Bot.Commands.Randomness
}
[Command("fortune", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
[Summary("Get a random fortune")]
public async Task GetAFortune()
{
diff --git a/Geekbot.net/Commands/Gdq.cs b/Geekbot.net/Commands/Gdq.cs
new file mode 100644
index 0000000..b191d6c
--- /dev/null
+++ b/Geekbot.net/Commands/Gdq.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.Commands
+{
+ public class Gdq : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+
+ public Gdq(IErrorHandler errorHandler)
+ {
+ _errorHandler = errorHandler;
+ }
+
+ [Command("gdq", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Games)]
+ [Summary("Get a quote from the GDQ donation generator.")]
+ public async Task getQuote()
+ {
+ try
+ {
+ using (var client = new WebClient())
+ {
+ var url = new Uri("http://taskinoz.com/gdq/api/");
+ var response = client.DownloadString(url);
+
+ await ReplyAsync(response);
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Google.cs b/Geekbot.net/Commands/Google.cs
new file mode 100644
index 0000000..b10bc02
--- /dev/null
+++ b/Geekbot.net/Commands/Google.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Discord.Net;
+using Geekbot.net.Lib;
+using Newtonsoft.Json;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ public class Google : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+
+ public Google(IErrorHandler errorHandler, IDatabase redis)
+ {
+ _errorHandler = errorHandler;
+ _redis = redis;
+ }
+
+ [Command("google", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Google Something.")]
+ public async Task askGoogle([Remainder, Summary("SearchText")] string searchText)
+ {
+ try
+ {
+ using (var client = new WebClient())
+ {
+ var apiKey = _redis.StringGet("googleGraphKey");
+ if (!apiKey.HasValue)
+ {
+ await ReplyAsync("No Google API key has been set, please contact my owner");
+ return;
+ }
+
+ var url = new Uri($"https://kgsearch.googleapis.com/v1/entities:search?languages=en&limit=1&query={searchText}&key={apiKey}");
+ var responseString = client.DownloadString(url);
+ var response = Utf8Json.JsonSerializer.Deserialize(responseString);
+
+ if (!response.itemListElement.Any())
+ {
+ await ReplyAsync("No results were found...");
+ return;
+ }
+
+ var data = response.itemListElement.First().result;
+ var eb = new EmbedBuilder();
+ eb.Title = data.name;
+ if(!string.IsNullOrEmpty(data.description)) eb.WithDescription(data.description);
+ if(!string.IsNullOrEmpty(data.detailedDescription?.url)) eb.WithUrl(data.detailedDescription.url);
+ if(!string.IsNullOrEmpty(data.detailedDescription?.articleBody)) eb.AddField("Details", data.detailedDescription.articleBody);
+ if(!string.IsNullOrEmpty(data.image?.contentUrl)) eb.WithThumbnailUrl(data.image.contentUrl);
+
+ await ReplyAsync("", false, eb.Build());
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ public class GoogleKGApiResponse
+ {
+ public List itemListElement { get; set; }
+
+ public class GoogleKGApiElement
+ {
+ public GoogleKGApiResult result { get; set; }
+ public double resultScore { get; set; }
+ }
+
+ public class GoogleKGApiResult
+ {
+ public string name { get; set; }
+ public string description { get; set; }
+ public GoogleKGApiImage image { get; set; }
+ public GoogleKGApiDetailed detailedDescription { get; set; }
+ }
+
+ public class GoogleKGApiImage
+ {
+ public string contentUrl { get; set; }
+ public string url { get; set; }
+ }
+
+ public class GoogleKGApiDetailed
+ {
+ public string articleBody { get; set; }
+ public string url { get; set; }
+ public string license { get; set; }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bot/Commands/User/GuildInfo.cs b/Geekbot.net/Commands/GuildInfo.cs
similarity index 57%
rename from src/Bot/Commands/User/GuildInfo.cs
rename to Geekbot.net/Commands/GuildInfo.cs
index c063d89..2ca76c6 100644
--- a/src/Bot/Commands/User/GuildInfo.cs
+++ b/Geekbot.net/Commands/GuildInfo.cs
@@ -3,32 +3,28 @@ using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
-using Geekbot.Bot.CommandPreconditions;
-using Geekbot.Core;
-using Geekbot.Core.Database;
-using Geekbot.Core.ErrorHandling;
-using Geekbot.Core.Extensions;
-using Geekbot.Core.Levels;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
-namespace Geekbot.Bot.Commands.User
+namespace Geekbot.net.Commands
{
- public class GuildInfo : TransactionModuleBase
+ public class GuildInfo : ModuleBase
{
private readonly IErrorHandler _errorHandler;
- private readonly DatabaseContext _database;
private readonly ILevelCalc _levelCalc;
+ private readonly IDatabase _redis;
- public GuildInfo(DatabaseContext database, ILevelCalc levelCalc, IErrorHandler errorHandler)
+ public GuildInfo(IDatabase redis, ILevelCalc levelCalc, IErrorHandler errorHandler)
{
- _database = database;
+ _redis = redis;
_levelCalc = levelCalc;
_errorHandler = errorHandler;
}
[Command("serverstats", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Statistics)]
[Summary("Show some info about the bot.")]
- [DisableInDirectMessage]
- public async Task GetInfo()
+ public async Task getInfo()
{
try
{
@@ -41,10 +37,8 @@ namespace Geekbot.Bot.Commands.User
var created = Context.Guild.CreatedAt;
var age = Math.Floor((DateTime.Now - created).TotalDays);
- var messages = _database.Messages
- .Where(e => e.GuildId == Context.Guild.Id.AsLong())
- .Sum(e => e.MessageCount);
- var level = _levelCalc.GetLevel(messages);
+ var messages = _redis.HashGet($"{Context.Guild.Id}:Messages", 0.ToString());
+ var level = _levelCalc.GetLevel((int) messages);
eb.AddField("Server Age", $"{created.Day}/{created.Month}/{created.Year} ({age} days)");
eb.AddInlineField("Level", level)
@@ -54,8 +48,15 @@ namespace Geekbot.Bot.Commands.User
}
catch (Exception e)
{
- await _errorHandler.HandleCommandException(e, Context);
+ _errorHandler.HandleCommandException(e, Context);
}
}
+
+ public static string FirstCharToUpper(string input)
+ {
+ if (string.IsNullOrEmpty(input))
+ throw new ArgumentException("ARGH!");
+ return input.First().ToString().ToUpper() + input.Substring(1);
+ }
}
}
\ No newline at end of file
diff --git a/src/Bot/Commands/Utils/Help.cs b/Geekbot.net/Commands/Help.cs
similarity index 67%
rename from src/Bot/Commands/Utils/Help.cs
rename to Geekbot.net/Commands/Help.cs
index 7aa9aff..aa2cc57 100644
--- a/src/Bot/Commands/Utils/Help.cs
+++ b/Geekbot.net/Commands/Help.cs
@@ -1,14 +1,12 @@
using System;
using System.Text;
using System.Threading.Tasks;
-using Discord;
using Discord.Commands;
-using Geekbot.Core;
-using Geekbot.Core.ErrorHandling;
+using Geekbot.net.Lib;
-namespace Geekbot.Bot.Commands.Utils
+namespace Geekbot.net.Commands
{
- public class Help : TransactionModuleBase
+ public class Help : ModuleBase
{
private readonly IErrorHandler _errorHandler;
@@ -18,6 +16,7 @@ namespace Geekbot.Bot.Commands.Utils
}
[Command("help", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
[Summary("List all Commands")]
public async Task GetHelp()
{
@@ -27,13 +26,12 @@ namespace Geekbot.Bot.Commands.Utils
sb.AppendLine("For a list of all commands, please visit the following page");
sb.AppendLine("https://geekbot.pizzaandcoffee.rocks/commands");
- var dm = await Context.User.CreateDMChannelAsync(RequestOptions.Default);
+ var dm = await Context.User.GetOrCreateDMChannelAsync();
await dm.SendMessageAsync(sb.ToString());
- await Context.Message.AddReactionAsync(new Emoji("✅"));
}
catch (Exception e)
{
- await _errorHandler.HandleCommandException(e, Context);
+ _errorHandler.HandleCommandException(e, Context);
}
}
}
diff --git a/src/Bot/Commands/Utils/Info.cs b/Geekbot.net/Commands/Info.cs
similarity index 66%
rename from src/Bot/Commands/Utils/Info.cs
rename to Geekbot.net/Commands/Info.cs
index 912528d..e8c35dc 100644
--- a/src/Bot/Commands/Utils/Info.cs
+++ b/Geekbot.net/Commands/Info.cs
@@ -5,26 +5,28 @@ using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
-using Geekbot.Core;
-using Geekbot.Core.ErrorHandling;
-using Geekbot.Core.Extensions;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
-namespace Geekbot.Bot.Commands.Utils
+namespace Geekbot.net.Commands
{
- public class Info : TransactionModuleBase
+ public class Info : ModuleBase
{
private readonly DiscordSocketClient _client;
private readonly CommandService _commands;
private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
- public Info(IErrorHandler errorHandler, DiscordSocketClient client, CommandService commands)
+ public Info(IDatabase redis, IErrorHandler errorHandler, DiscordSocketClient client, CommandService commands)
{
+ _redis = redis;
_errorHandler = errorHandler;
_client = client;
_commands = commands;
}
[Command("info", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
[Summary("Get Information about the bot")]
public async Task BotInfo()
{
@@ -32,30 +34,31 @@ namespace Geekbot.Bot.Commands.Utils
{
var eb = new EmbedBuilder();
- var appInfo = await _client.GetApplicationInfoAsync();
-
eb.WithAuthor(new EmbedAuthorBuilder()
- .WithIconUrl(appInfo.IconUrl)
- .WithName($"{Constants.Name} V{Constants.BotVersion()}"));
+ .WithIconUrl(_client.CurrentUser.GetAvatarUrl())
+ .WithName($"{Constants.Name} V{Constants.BotVersion}"));
+ var botOwner = await Context.Guild.GetUserAsync(ulong.Parse(_redis.StringGet("botOwner")));
var uptime = DateTime.Now.Subtract(Process.GetCurrentProcess().StartTime);
eb.AddInlineField("Bot Name", _client.CurrentUser.Username);
- eb.AddInlineField("Bot Owner", $"{appInfo.Owner.Username}#{appInfo.Owner.Discriminator}");
- eb.AddInlineField("Library", $"Discord.NET {Constants.LibraryVersion()}");
+ eb.AddInlineField("Bot Owner", $"{botOwner.Username}#{botOwner.Discriminator}");
+ eb.AddInlineField("Library", "Discord.NET V1.0.2");
eb.AddInlineField("Uptime", $"{uptime.Days}D {uptime.Hours}H {uptime.Minutes}M {uptime.Seconds}S");
eb.AddInlineField("Servers", Context.Client.GetGuildsAsync().Result.Count);
eb.AddInlineField("Total Commands", _commands.Commands.Count());
+
eb.AddField("Website", "https://geekbot.pizzaandcoffee.rocks/");
await ReplyAsync("", false, eb.Build());
}
catch (Exception e)
{
- await _errorHandler.HandleCommandException(e, Context);
+ _errorHandler.HandleCommandException(e, Context);
}
}
[Command("uptime", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
[Summary("Get the Bot Uptime")]
public async Task BotUptime()
{
@@ -66,7 +69,7 @@ namespace Geekbot.Bot.Commands.Utils
}
catch (Exception e)
{
- await _errorHandler.HandleCommandException(e, Context);
+ _errorHandler.HandleCommandException(e, Context);
}
}
}
diff --git a/Geekbot.net/Commands/Karma.cs b/Geekbot.net/Commands/Karma.cs
new file mode 100644
index 0000000..1affa82
--- /dev/null
+++ b/Geekbot.net/Commands/Karma.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ public class Karma : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+ private readonly ITranslationHandler _translation;
+
+ public Karma(IDatabase redis, IErrorHandler errorHandler, ITranslationHandler translation)
+ {
+ _redis = redis;
+ _errorHandler = errorHandler;
+ _translation = translation;
+ }
+
+ [Command("good", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Karma)]
+ [Summary("Increase Someones Karma")]
+ public async Task Good([Summary("@someone")] IUser user)
+ {
+ try
+ {
+ var transDict = _translation.GetDict(Context);
+ var lastKarmaFromRedis = _redis.HashGet($"{Context.Guild.Id}:KarmaTimeout", Context.User.Id.ToString());
+ var lastKarma = ConvertToDateTimeOffset(lastKarmaFromRedis.ToString());
+ if (user.Id == Context.User.Id)
+ {
+ await ReplyAsync(string.Format(transDict["CannotChangeOwn"], Context.User.Username));
+ }
+ else if (TimeoutFinished(lastKarma))
+ {
+ await ReplyAsync(string.Format(transDict["WaitUntill"], Context.User.Username,
+ GetTimeLeft(lastKarma)));
+ }
+ else
+ {
+ var newKarma = _redis.HashIncrement($"{Context.Guild.Id}:Karma", user.Id.ToString());
+ _redis.HashSet($"{Context.Guild.Id}:KarmaTimeout",
+ new[] {new HashEntry(Context.User.Id.ToString(), DateTimeOffset.Now.ToString("u"))});
+
+ var eb = new EmbedBuilder();
+ eb.WithAuthor(new EmbedAuthorBuilder()
+ .WithIconUrl(user.GetAvatarUrl())
+ .WithName(user.Username));
+
+ eb.WithColor(new Color(138, 219, 146));
+ eb.Title = transDict["Increased"];
+ eb.AddInlineField(transDict["By"], Context.User.Username);
+ eb.AddInlineField(transDict["Amount"], "+1");
+ eb.AddInlineField(transDict["Current"], newKarma);
+ await ReplyAsync("", false, eb.Build());
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command("bad", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Karma)]
+ [Summary("Decrease Someones Karma")]
+ public async Task Bad([Summary("@someone")] IUser user)
+ {
+ try
+ {
+ var transDict = _translation.GetDict(Context);
+ var lastKarmaFromRedis = _redis.HashGet($"{Context.Guild.Id}:KarmaTimeout", Context.User.Id.ToString());
+ var lastKarma = ConvertToDateTimeOffset(lastKarmaFromRedis.ToString());
+ if (user.Id == Context.User.Id)
+ {
+ await ReplyAsync(string.Format(transDict["CannotChangeOwn"], Context.User.Username));
+ }
+ else if (TimeoutFinished(lastKarma))
+ {
+ await ReplyAsync(string.Format(transDict["WaitUntill"], Context.User.Username,
+ GetTimeLeft(lastKarma)));
+ }
+ else
+ {
+ var newKarma = _redis.HashDecrement($"{Context.Guild.Id}:Karma", user.Id.ToString());
+ _redis.HashSet($"{Context.Guild.Id}:KarmaTimeout",
+ new[] {new HashEntry(Context.User.Id.ToString(), DateTimeOffset.Now.ToString())});
+
+ var eb = new EmbedBuilder();
+ eb.WithAuthor(new EmbedAuthorBuilder()
+ .WithIconUrl(user.GetAvatarUrl())
+ .WithName(user.Username));
+
+ eb.WithColor(new Color(138, 219, 146));
+ eb.Title = transDict["Decreased"];
+ eb.AddInlineField(transDict["By"], Context.User.Username);
+ eb.AddInlineField(transDict["Amount"], "-1");
+ eb.AddInlineField(transDict["Current"], newKarma);
+ await ReplyAsync("", false, eb.Build());
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private DateTimeOffset ConvertToDateTimeOffset(string dateTimeOffsetString)
+ {
+ if (string.IsNullOrEmpty(dateTimeOffsetString))
+ return DateTimeOffset.Now.Subtract(new TimeSpan(7, 18, 0, 0));
+ return DateTimeOffset.Parse(dateTimeOffsetString);
+ }
+
+ private bool TimeoutFinished(DateTimeOffset lastKarma)
+ {
+ return lastKarma.AddMinutes(3) > DateTimeOffset.Now;
+ }
+
+ private string GetTimeLeft(DateTimeOffset lastKarma)
+ {
+ var dt = lastKarma.AddMinutes(3).Subtract(DateTimeOffset.Now);
+ return $"{dt.Minutes} Minutes and {dt.Seconds} Seconds";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/MagicTheGathering.cs b/Geekbot.net/Commands/MagicTheGathering.cs
new file mode 100644
index 0000000..e2702b1
--- /dev/null
+++ b/Geekbot.net/Commands/MagicTheGathering.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using MtgApiManager.Lib.Service;
+
+namespace Geekbot.net.Commands
+{
+ public class Magicthegathering : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+
+ public Magicthegathering(IErrorHandler errorHandler)
+ {
+ _errorHandler = errorHandler;
+ }
+
+ [Command("mtg", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Games)]
+ [Summary("Find a Magic The Gathering Card.")]
+ public async Task getCard([Remainder] [Summary("name")] string cardName)
+ {
+ try
+ {
+ var service = new CardService();
+ var result = service.Where(x => x.Name, cardName);
+
+ var card = result.All().Value.FirstOrDefault();
+ if (card == null)
+ {
+ await ReplyAsync("I couldn't find that card...");
+ return;
+ }
+
+ var eb = new EmbedBuilder();
+ eb.Title = card.Name;
+ eb.Description = card.Type;
+
+ if (card.Colors != null) eb.WithColor(GetColor(card.Colors));
+
+ if (card.ImageUrl != null) eb.ImageUrl = card.ImageUrl.ToString();
+
+ if (!string.IsNullOrEmpty(card.Text)) eb.AddField("Text", card.Text);
+
+ if (!string.IsNullOrEmpty(card.Flavor)) eb.AddField("Flavor", card.Flavor);
+ if (!string.IsNullOrEmpty(card.SetName)) eb.AddInlineField("Set", card.SetName);
+ if (!string.IsNullOrEmpty(card.Power)) eb.AddInlineField("Power", card.Power);
+ if (!string.IsNullOrEmpty(card.Loyalty)) eb.AddInlineField("Loyality", card.Loyalty);
+ if (!string.IsNullOrEmpty(card.Toughness)) eb.AddInlineField("Thoughness", card.Toughness);
+
+ if (!string.IsNullOrEmpty(card.ManaCost)) eb.AddInlineField("Cost", card.ManaCost);
+ if (!string.IsNullOrEmpty(card.Rarity)) eb.AddInlineField("Rarity", card.Rarity);
+
+ if (card.Legalities != null)
+ eb.AddField("Legality", string.Join(", ", card.Legalities.Select(e => e.Format)));
+
+ await ReplyAsync("", false, eb.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private Color GetColor(IEnumerable colors)
+ {
+ var color = colors.FirstOrDefault();
+ switch (color)
+ {
+ case "Black":
+ return new Color(177, 171, 170);
+ case "White":
+ return new Color(255, 252, 214);
+ case "Blue":
+ return new Color(156, 189, 204);
+ case "Red":
+ return new Color(204, 156, 140);
+ case "Green":
+ return new Color(147, 181, 159);
+ default:
+ return new Color(255, 252, 214);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Mod.cs b/Geekbot.net/Commands/Mod.cs
new file mode 100644
index 0000000..c927b0d
--- /dev/null
+++ b/Geekbot.net/Commands/Mod.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Text;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ [Group("mod")]
+ [RequireUserPermission(GuildPermission.KickMembers)]
+ [RequireUserPermission(GuildPermission.ManageMessages)]
+ [RequireUserPermission(GuildPermission.ManageRoles)]
+ public class Mod : ModuleBase
+ {
+ private readonly DiscordSocketClient _client;
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+ private readonly IUserRepository _userRepository;
+
+ public Mod(IUserRepository userRepositry, IErrorHandler errorHandler, IDatabase redis,
+ DiscordSocketClient client)
+ {
+ _userRepository = userRepositry;
+ _errorHandler = errorHandler;
+ _redis = redis;
+ _client = client;
+ }
+
+ [Command("namehistory", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("See past usernames of an user")]
+ public async Task usernameHistory([Summary("@user")] IUser user)
+ {
+ try
+ {
+ var userRepo = _userRepository.Get(user.Id);
+ var sb = new StringBuilder();
+ sb.AppendLine($":bust_in_silhouette: {user.Username} has been known as:");
+ foreach (var name in userRepo.UsedNames) sb.AppendLine($"- `{name}`");
+ await ReplyAsync(sb.ToString());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ $"I don't have enough permissions to give {user.Username} that role");
+ }
+ }
+
+ [Command("kick", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Ban a user")]
+ public async Task kick([Summary("@user")] IUser userNormal,
+ [Summary("reason")] [Remainder] string reason = "none")
+ {
+ try
+ {
+ var user = (IGuildUser) userNormal;
+ if (reason == "none") reason = "No reason provided";
+ await user.GetOrCreateDMChannelAsync().Result.SendMessageAsync(
+ $"You have been kicked from {Context.Guild.Name} for the following reason: \"{reason}\"");
+ await user.KickAsync();
+ try
+ {
+ var modChannelId = ulong.Parse(_redis.HashGet($"{Context.Guild.Id}:Settings", "ModChannel"));
+ var modChannel = (ISocketMessageChannel) _client.GetChannel(modChannelId);
+ var eb = new EmbedBuilder();
+ eb.Title = ":x: User Kicked";
+ eb.AddInlineField("User", user.Username);
+ eb.AddInlineField("By Mod", Context.User.Username);
+ eb.AddField("Reason", reason);
+ await modChannel.SendMessageAsync("", false, eb.Build());
+ }
+ catch
+ {
+ await ReplyAsync($"{user.Username} was kicked for the following reason: \"{reason}\"");
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context, "I don't have enough permissions to kick someone");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Overwatch.cs b/Geekbot.net/Commands/Overwatch.cs
new file mode 100644
index 0000000..10dc459
--- /dev/null
+++ b/Geekbot.net/Commands/Overwatch.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using OverwatchAPI;
+using OverwatchAPI.Config;
+using Serilog;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ [Group("ow")]
+ public class Overwatch : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IUserRepository _userRepository;
+
+ public Overwatch(IErrorHandler errorHandler, IDatabase redis, IUserRepository userRepository)
+ {
+ _errorHandler = errorHandler;
+ _userRepository = userRepository;
+ }
+
+ [Command("profile", RunMode = RunMode.Async)]
+ [Summary("Get someones overwatch profile. EU on PC only. Default battletag is your own (if set).")]
+ [Remarks(CommandCategories.Games)]
+ public async Task owProfile()
+ {
+ try
+ {
+ var tag = _userRepository.getUserSetting(Context.User.Id, "BattleTag");
+ if (string.IsNullOrEmpty(tag))
+ {
+ await ReplyAsync("You have no battle Tag saved, use `!battletag`");
+ return;
+ }
+
+ var profile = await createProfile(tag);
+ if (profile == null)
+ {
+ await ReplyAsync("That player doesn't seem to exist");
+ return;
+ }
+
+ await ReplyAsync("", false, profile.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command("profile", RunMode = RunMode.Async)]
+ [Summary("Get someones overwatch profile. EU on PC only. Default battletag is your own (if set).")]
+ [Remarks(CommandCategories.Games)]
+ public async Task owProfile([Summary("BattleTag")] string tag)
+ {
+ try
+ {
+ if (!BattleTag.isValidTag(tag))
+ {
+ await ReplyAsync("That doesn't seem to be a valid battletag...");
+ return;
+ }
+
+ var profile = await createProfile(tag);
+ if (profile == null)
+ {
+ await ReplyAsync("That player doesn't seem to exist");
+ return;
+ }
+
+ await ReplyAsync("", false, profile.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command("profile", RunMode = RunMode.Async)]
+ [Summary("Get someones overwatch profile. EU on PC only.")]
+ [Remarks(CommandCategories.Games)]
+ public async Task owProfile([Summary("@someone")] IUser user)
+ {
+ try
+ {
+ var tag = _userRepository.getUserSetting(user.Id, "BattleTag");
+ if (string.IsNullOrEmpty(tag))
+ {
+ await ReplyAsync("This user didn't set a battletag");
+ return;
+ }
+
+ var profile = await createProfile(tag);
+ if (profile == null)
+ {
+ await ReplyAsync("That player doesn't seem to exist");
+ return;
+ }
+
+ await ReplyAsync("", false, profile.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private async Task createProfile(string battletag)
+ {
+ var owConfig = new OverwatchConfig.Builder().WithRegions(Region.Eu).WithPlatforms(Platform.Pc);
+ using (var owClient = new OverwatchClient(owConfig))
+ {
+ var player = await owClient.GetPlayerAsync(battletag);
+ if (player.Username == null) return null;
+ var eb = new EmbedBuilder();
+ eb.WithAuthor(new EmbedAuthorBuilder()
+ .WithIconUrl(player.ProfilePortraitUrl)
+ .WithName(player.Username));
+ eb.Url = player.ProfileUrl;
+ eb.AddInlineField("Level", player.PlayerLevel);
+ eb.AddInlineField("Current Rank",
+ player.CompetitiveRank > 0 ? player.CompetitiveRank.ToString() : "Unranked");
+
+ return eb;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Owner.cs b/Geekbot.net/Commands/Owner.cs
new file mode 100644
index 0000000..c4adb25
--- /dev/null
+++ b/Geekbot.net/Commands/Owner.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Geekbot.net.Lib;
+using Serilog;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ [Group("owner")]
+ [RequireUserPermission(GuildPermission.Administrator)]
+ public class Owner : ModuleBase
+ {
+ private readonly DiscordSocketClient _client;
+ private readonly IErrorHandler _errorHandler;
+ private readonly IGeekbotLogger _logger;
+ private readonly IDatabase _redis;
+ private readonly IUserRepository _userRepository;
+
+ public Owner(IDatabase redis, DiscordSocketClient client, IGeekbotLogger logger, IUserRepository userRepositry,
+ IErrorHandler errorHandler)
+ {
+ _redis = redis;
+ _client = client;
+ _logger = logger;
+ _userRepository = userRepositry;
+ _errorHandler = errorHandler;
+ }
+
+ [Command("youtubekey", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Set the youtube api key")]
+ public async Task SetYoutubeKey([Summary("API Key")] string key)
+ {
+ var botOwner = Context.Guild.GetUserAsync(ulong.Parse(_redis.StringGet("botOwner"))).Result;
+ if (!Context.User.Id.ToString().Equals(botOwner.Id.ToString()))
+ {
+ await ReplyAsync(
+ $"Sorry, only the botowner can do this ({botOwner.Username}#{botOwner.Discriminator})");
+ return;
+ }
+
+ _redis.StringSet("youtubeKey", key);
+ await ReplyAsync("Apikey has been set");
+ }
+
+ [Command("game", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Set the game that the bot is playing")]
+ public async Task SetGame([Remainder] [Summary("Game")] string key)
+ {
+ var botOwner = Context.Guild.GetUserAsync(ulong.Parse(_redis.StringGet("botOwner"))).Result;
+ if (!Context.User.Id.ToString().Equals(botOwner.Id.ToString()))
+ {
+ await ReplyAsync(
+ $"Sorry, only the botowner can do this ({botOwner.Username}#{botOwner.Discriminator})");
+ return;
+ }
+
+ _redis.StringSet("Game", key);
+ await _client.SetGameAsync(key);
+ _logger.Information("Geekbot", $"Changed game to {key}");
+ await ReplyAsync($"Now Playing {key}");
+ }
+
+ [Command("popuserrepo", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Populate user cache")]
+ public async Task popUserRepoCommand()
+ {
+ try
+ {
+ var botOwner = Context.Guild.GetUserAsync(ulong.Parse(_redis.StringGet("botOwner"))).Result;
+ if (!Context.User.Id.ToString().Equals(botOwner.Id.ToString()))
+ {
+ await ReplyAsync(
+ $"Sorry, only the botowner can do this ({botOwner.Username}#{botOwner.Discriminator})");
+ return;
+ }
+ }
+ catch (Exception)
+ {
+ await ReplyAsync(
+ $"Sorry, only the botowner can do this");
+ return;
+ }
+
+ var success = 0;
+ var failed = 0;
+ try
+ {
+ _logger.Warning("UserRepository", "Populating User Repositry");
+ await ReplyAsync("Starting Population of User Repository");
+ foreach (var guild in _client.Guilds)
+ {
+ _logger.Information("UserRepository", $"Populating users from {guild.Name}");
+ foreach (var user in guild.Users)
+ {
+ var succeded = await _userRepository.Update(user);
+ var inc = succeded ? success++ : failed++;
+ }
+ }
+
+ _logger.Warning("UserRepository", "Finished Updating User Repositry");
+ await ReplyAsync(
+ $"Successfully Populated User Repository with {success} Users in {_client.Guilds.Count} Guilds (Failed: {failed})");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ "Couldn't complete User Repository, see console for more info");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Bot/Commands/Utils/Ping.cs b/Geekbot.net/Commands/Ping.cs
similarity index 68%
rename from src/Bot/Commands/Utils/Ping.cs
rename to Geekbot.net/Commands/Ping.cs
index ee751cd..c41700e 100644
--- a/src/Bot/Commands/Utils/Ping.cs
+++ b/Geekbot.net/Commands/Ping.cs
@@ -1,13 +1,14 @@
using System.Threading.Tasks;
using Discord.Commands;
-using Geekbot.Core;
+using Geekbot.net.Lib;
-namespace Geekbot.Bot.Commands.Utils
+namespace Geekbot.net.Commands
{
- public class Ping : TransactionModuleBase
+ public class Ping : ModuleBase
{
[Command("👀", RunMode = RunMode.Async)]
[Summary("Look at the bot.")]
+ [Remarks(CommandCategories.Fun)]
public async Task Eyes()
{
await ReplyAsync("S... Stop looking at me... baka!");
diff --git a/src/Bot/Commands/Games/Pokedex.cs b/Geekbot.net/Commands/Pokedex.cs
similarity index 66%
rename from src/Bot/Commands/Games/Pokedex.cs
rename to Geekbot.net/Commands/Pokedex.cs
index 325ee8c..2b43693 100644
--- a/src/Bot/Commands/Games/Pokedex.cs
+++ b/Geekbot.net/Commands/Pokedex.cs
@@ -3,14 +3,12 @@ using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
-using Geekbot.Core;
-using Geekbot.Core.ErrorHandling;
-using Geekbot.Core.Extensions;
+using Geekbot.net.Lib;
using PokeAPI;
-namespace Geekbot.Bot.Commands.Games
+namespace Geekbot.net.Commands
{
- public class Pokedex : TransactionModuleBase
+ public class Pokedex : ModuleBase
{
private readonly IErrorHandler _errorHandler;
@@ -20,8 +18,9 @@ namespace Geekbot.Bot.Commands.Games
}
[Command("pokedex", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
[Summary("A Pokedex Tool")]
- public async Task GetPokemon([Summary("pokemon-name")] string pokemonName)
+ public async Task GetPokemon([Summary("pokemonName")] string pokemonName)
{
try
{
@@ -37,38 +36,38 @@ namespace Geekbot.Bot.Commands.Games
return;
}
- var embed = await PokemonEmbedBuilder(pokemon);
+ var embed = await pokemonEmbedBuilder(pokemon);
await ReplyAsync("", false, embed.Build());
}
catch (Exception e)
{
- await _errorHandler.HandleCommandException(e, Context);
+ _errorHandler.HandleCommandException(e, Context);
}
}
- private async Task PokemonEmbedBuilder(Pokemon pokemon)
+ private async Task pokemonEmbedBuilder(Pokemon pokemon)
{
var eb = new EmbedBuilder();
var species = await DataFetcher.GetApiObject(pokemon.ID);
- eb.Title = $"#{pokemon.ID} {ToUpper(pokemon.Name)}";
+ eb.Title = $"#{pokemon.ID} {toUpper(pokemon.Name)}";
eb.Description = species.FlavorTexts[1].FlavorText;
eb.ThumbnailUrl = pokemon.Sprites.FrontMale ?? pokemon.Sprites.FrontFemale;
- eb.AddInlineField(GetSingularOrPlural(pokemon.Types.Length, "Type"),
- string.Join(", ", pokemon.Types.Select(t => ToUpper(t.Type.Name))));
- eb.AddInlineField(GetSingularOrPlural(pokemon.Abilities.Length, "Ability"),
- string.Join(", ", pokemon.Abilities.Select(t => ToUpper(t.Ability.Name))));
+ eb.AddInlineField(getSingularOrPlural(pokemon.Types.Length, "Type"),
+ string.Join(", ", pokemon.Types.Select(t => toUpper(t.Type.Name))));
+ eb.AddInlineField(getSingularOrPlural(pokemon.Abilities.Length, "Ability"),
+ string.Join(", ", pokemon.Abilities.Select(t => toUpper(t.Ability.Name))));
eb.AddInlineField("Height", pokemon.Height);
eb.AddInlineField("Weight", pokemon.Mass);
return eb;
}
- private string GetSingularOrPlural(int lenght, string word)
+ private string getSingularOrPlural(int lenght, string word)
{
if (lenght == 1) return word;
return word.EndsWith("y") ? $"{word.Remove(word.Length - 1)}ies" : $"{word}s";
}
- private string ToUpper(string s)
+ private string toUpper(string s)
{
if (string.IsNullOrEmpty(s)) return string.Empty;
return char.ToUpper(s[0]) + s.Substring(1);
diff --git a/Geekbot.net/Commands/Poll.cs b/Geekbot.net/Commands/Poll.cs
new file mode 100644
index 0000000..51b6742
--- /dev/null
+++ b/Geekbot.net/Commands/Poll.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using Newtonsoft.Json;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ [Group("poll")]
+ public class Poll : ModuleBase
+ {
+ private readonly IEmojiConverter _emojiConverter;
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+ private readonly IUserRepository _userRepository;
+
+ public Poll(IErrorHandler errorHandler, IDatabase redis, IEmojiConverter emojiConverter,
+ IUserRepository userRepository)
+ {
+ _errorHandler = errorHandler;
+ _redis = redis;
+ _emojiConverter = emojiConverter;
+ _userRepository = userRepository;
+ }
+
+ [Command(RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Check status of the current poll")]
+ public async Task Dflt()
+ {
+ try
+ {
+ var currentPoll = GetCurrentPoll();
+ if (currentPoll.Question == null || currentPoll.IsFinshed)
+ {
+ await ReplyAsync(
+ "There is no poll in this channel ongoing at the moment\r\nYou can create one with `!poll create question;option1;option2;option3`");
+ return;
+ }
+
+ await ReplyAsync("There is a poll running at the moment");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command("create", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Create a poll")]
+ public async Task Create([Remainder] [Summary("question;option1;option2")]
+ string rawPollString)
+ {
+ try
+ {
+ var currentPoll = GetCurrentPoll();
+ if (currentPoll.Question != null && !currentPoll.IsFinshed)
+ {
+ await ReplyAsync("You have not finished you last poll yet. To finish it use `!poll end`");
+ return;
+ }
+
+ var pollList = rawPollString.Split(';').ToList();
+ if (pollList.Count <= 2)
+ {
+ await ReplyAsync(
+ "You need a question with atleast 2 options, a valid creation would look like this `question;option1;option2`");
+ return;
+ }
+
+ var eb = new EmbedBuilder();
+ eb.Title = $"Poll by {Context.User.Username}";
+ var question = pollList[0];
+ eb.Description = question;
+ pollList.RemoveAt(0);
+ var i = 1;
+ pollList.ForEach(option =>
+ {
+ eb.AddInlineField($"Option {_emojiConverter.numberToEmoji(i)}", option);
+ i++;
+ });
+ var pollMessage = await ReplyAsync("", false, eb.Build());
+ i = 1;
+ pollList.ForEach(option =>
+ {
+ pollMessage.AddReactionAsync(new Emoji(_emojiConverter.numberToEmoji(i)));
+ i++;
+ });
+ var poll = new PollData
+ {
+ Creator = Context.User.Id,
+ MessageId = pollMessage.Id,
+ IsFinshed = false,
+ Question = question,
+ Options = pollList
+ };
+ var pollJson = JsonConvert.SerializeObject(poll);
+ _redis.HashSet($"{Context.Guild.Id}:Polls", new[] {new HashEntry(Context.Channel.Id, pollJson)});
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command("end", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("End the current poll")]
+ public async Task End()
+ {
+ try
+ {
+ var currentPoll = GetCurrentPoll();
+ if (currentPoll.Question == null || currentPoll.IsFinshed)
+ {
+ await ReplyAsync("There is no ongoing poll at the moment");
+ return;
+ }
+
+ var results = await getPollResults(currentPoll);
+ var sb = new StringBuilder();
+ sb.AppendLine("**Poll Results**");
+ sb.AppendLine(currentPoll.Question);
+ foreach (var result in results) sb.AppendLine($"{result.VoteCount} - {result.Option}");
+ await ReplyAsync(sb.ToString());
+ currentPoll.IsFinshed = true;
+ var pollJson = JsonConvert.SerializeObject(currentPoll);
+ _redis.HashSet($"{Context.Guild.Id}:Polls", new[] {new HashEntry(Context.Channel.Id, pollJson)});
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private PollData GetCurrentPoll()
+ {
+ try
+ {
+ var currentPoll = _redis.HashGet($"{Context.Guild.Id}:Polls", Context.Channel.Id);
+ return JsonConvert.DeserializeObject(currentPoll.ToString());
+ }
+ catch
+ {
+ return new PollData();
+ }
+ }
+
+ private async Task> getPollResults(PollData poll)
+ {
+ var message = (IUserMessage) await Context.Channel.GetMessageAsync(poll.MessageId);
+ var results = new List();
+ foreach (var r in message.Reactions)
+ try
+ {
+ var option = int.Parse(r.Key.Name.ToCharArray()[0].ToString());
+ var result = new PollResult
+ {
+ Option = poll.Options[option - 1],
+ VoteCount = r.Value.ReactionCount
+ };
+ results.Add(result);
+ }
+ catch {}
+
+ results.Sort((x, y) => y.VoteCount.CompareTo(x.VoteCount));
+ return results;
+ }
+
+ private class PollData
+ {
+ public ulong Creator { get; set; }
+ public ulong MessageId { get; set; }
+ public bool IsFinshed { get; set; }
+ public string Question { get; set; }
+ public List Options { get; set; }
+ }
+
+ private class PollResult
+ {
+ public string Option { get; set; }
+ public int VoteCount { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Quote.cs b/Geekbot.net/Commands/Quote.cs
new file mode 100644
index 0000000..c3bb235
--- /dev/null
+++ b/Geekbot.net/Commands/Quote.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using Newtonsoft.Json;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ [Group("quote")]
+ public class Quote : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+
+ public Quote(IDatabase redis, IErrorHandler errorHandler, Random random)
+ {
+ _redis = redis;
+ _errorHandler = errorHandler;
+ }
+
+ [Command]
+ [Remarks(CommandCategories.Quotes)]
+ [Summary("Return a random quoute from the database")]
+ public async Task getRandomQuote()
+ {
+ try
+ {
+ var randomQuotes = _redis.SetMembers($"{Context.Guild.Id}:Quotes");
+ var randomNumber = new Random().Next(randomQuotes.Length - 1);
+ var randomQuote = randomQuotes[randomNumber];
+ var quote = JsonConvert.DeserializeObject(randomQuote);
+ var embed = quoteBuilder(quote, randomNumber + 1);
+ await ReplyAsync("", false, embed.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context, "Whoops, seems like the quote was to edgy to return");
+ }
+ }
+
+ [Command("save")]
+ [Remarks(CommandCategories.Quotes)]
+ [Summary("Save a quote from the last sent message by @user")]
+ public async Task saveQuote([Summary("@user")] IUser user)
+ {
+ try
+ {
+ if (user.Id == Context.Message.Author.Id)
+ {
+ await ReplyAsync("You can't save your own quotes...");
+ return;
+ }
+
+ if (user.IsBot)
+ {
+ await ReplyAsync("You can't save quotes by a bot...");
+ return;
+ }
+
+ var lastMessage = await getLastMessageByUser(user);
+ var quote = createQuoteObject(lastMessage);
+ var quoteStore = JsonConvert.SerializeObject(quote);
+ _redis.SetAdd($"{Context.Guild.Id}:Quotes", quoteStore);
+ var embed = quoteBuilder(quote);
+ await ReplyAsync("**Quote Added**", false, embed.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ "I counldn't find a quote from that user :disappointed:");
+ }
+ }
+
+ [Command("save")]
+ [Remarks(CommandCategories.Quotes)]
+ [Summary("Save a quote from a message id")]
+ public async Task saveQuote([Summary("messageId")] ulong messageId)
+ {
+ try
+ {
+ var message = await Context.Channel.GetMessageAsync(messageId);
+ if (message.Author.Id == Context.Message.Author.Id)
+ {
+ await ReplyAsync("You can't save your own quotes...");
+ return;
+ }
+
+ if (message.Author.IsBot)
+ {
+ await ReplyAsync("You can't save quotes by a bot...");
+ return;
+ }
+
+ var quote = createQuoteObject(message);
+ var quoteStore = JsonConvert.SerializeObject(quote);
+ _redis.SetAdd($"{Context.Guild.Id}:Quotes", quoteStore);
+ var embed = quoteBuilder(quote);
+ await ReplyAsync("**Quote Added**", false, embed.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ "I couldn't find a message with that id :disappointed:");
+ }
+ }
+
+ [Command("make")]
+ [Remarks(CommandCategories.Quotes)]
+ [Summary("Create a quote from the last sent message by @user")]
+ public async Task returnSpecifiedQuote([Summary("@user")] IUser user)
+ {
+ try
+ {
+ var lastMessage = await getLastMessageByUser(user);
+ var quote = createQuoteObject(lastMessage);
+ var embed = quoteBuilder(quote);
+ await ReplyAsync("", false, embed.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ "I counldn't find a quote from that user :disappointed:");
+ }
+ }
+
+ [Command("make")]
+ [Remarks(CommandCategories.Quotes)]
+ [Summary("Create a quote from a message id")]
+ public async Task returnSpecifiedQuote([Summary("messageId")] ulong messageId)
+ {
+ try
+ {
+ var message = await Context.Channel.GetMessageAsync(messageId);
+ var quote = createQuoteObject(message);
+ var embed = quoteBuilder(quote);
+ await ReplyAsync("", false, embed.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ "I couldn't find a message with that id :disappointed:");
+ }
+ }
+
+ [Command("remove")]
+ [RequireUserPermission(GuildPermission.KickMembers)]
+ [RequireUserPermission(GuildPermission.ManageMessages)]
+ [RequireUserPermission(GuildPermission.ManageRoles)]
+ [Remarks(CommandCategories.Quotes)]
+ [Summary("Remove a quote (required mod permissions)")]
+ public async Task removeQuote([Summary("quoteId")] int id)
+ {
+ try
+ {
+ var quotes = _redis.SetMembers($"{Context.Guild.Id}:Quotes");
+ var success = _redis.SetRemove($"{Context.Guild.Id}:Quotes", quotes[id - 1]);
+ if (success)
+ {
+ var quote = JsonConvert.DeserializeObject(quotes[id - 1]);
+ var embed = quoteBuilder(quote);
+ await ReplyAsync($"**Removed #{id}**", false, embed.Build());
+ }
+ else
+ {
+ await ReplyAsync($"I couldn't find a quote with that id :disappointed:");
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context,
+ "I couldn't find a quote with that id :disappointed:");
+ }
+ }
+
+ private async Task getLastMessageByUser(IUser user)
+ {
+ var list = Context.Channel.GetMessagesAsync().Flatten();
+ await list;
+ return list.Result
+ .First(msg => msg.Author.Id == user.Id
+ && msg.Embeds.Count == 0
+ && msg.Id != Context.Message.Id
+ && !msg.Content.ToLower().StartsWith("!"));
+ }
+
+ private EmbedBuilder quoteBuilder(QuoteObject quote, int id = 0)
+ {
+ var user = Context.Client.GetUserAsync(quote.userId).Result;
+ var eb = new EmbedBuilder();
+ eb.WithColor(new Color(143, 167, 232));
+ eb.Title = id == 0 ? "" : $"#{id} | ";
+ eb.Title += $"{user.Username} @ {quote.time.Day}.{quote.time.Month}.{quote.time.Year}";
+ eb.Description = quote.quote;
+ eb.ThumbnailUrl = user.GetAvatarUrl();
+ if (quote.image != null) eb.ImageUrl = quote.image;
+ return eb;
+ }
+
+ private QuoteObject createQuoteObject(IMessage message)
+ {
+ string image;
+ try
+ {
+ image = message.Attachments.First().Url;
+ }
+ catch (Exception)
+ {
+ image = null;
+ }
+
+ return new QuoteObject
+ {
+ userId = message.Author.Id,
+ time = message.Timestamp.DateTime,
+ quote = message.Content,
+ image = image
+ };
+ }
+ }
+
+ public class QuoteObject
+ {
+ public ulong userId { get; set; }
+ public string quote { get; set; }
+ public DateTime time { get; set; }
+ public string image { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/RandomAnimals.cs b/Geekbot.net/Commands/RandomAnimals.cs
new file mode 100644
index 0000000..29930eb
--- /dev/null
+++ b/Geekbot.net/Commands/RandomAnimals.cs
@@ -0,0 +1,58 @@
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using Geekbot.net.Lib.Media;
+
+namespace Geekbot.net.Commands
+{
+ public class RandomAnimals : ModuleBase
+ {
+ private readonly IMediaProvider _mediaProvider;
+
+ public RandomAnimals(IMediaProvider mediaProvider)
+ {
+ _mediaProvider = mediaProvider;
+ }
+
+ [Command("panda", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Get a random panda image")]
+ public async Task panda()
+ {
+ await ReplyAsync(_mediaProvider.getPanda());
+ }
+
+ [Command("croissant", RunMode = RunMode.Async)]
+ [Alias("gipfeli")]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Get a random croissant image")]
+ public async Task croissant()
+ {
+ await ReplyAsync(_mediaProvider.getCrossant());
+ }
+
+ [Command("pumpkin", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Get a random pumpkin image")]
+ public async Task pumpkin()
+ {
+ await ReplyAsync(_mediaProvider.getPumpkin());
+ }
+
+ [Command("squirrel", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Get a random squirrel image")]
+ public async Task squirrel()
+ {
+ await ReplyAsync(_mediaProvider.getSquirrel());
+ }
+
+ [Command("turtle", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Randomness)]
+ [Summary("Get a random turtle image")]
+ public async Task turtle()
+ {
+ await ReplyAsync(_mediaProvider.getTurtle());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Rank.cs b/Geekbot.net/Commands/Rank.cs
new file mode 100644
index 0000000..516c2aa
--- /dev/null
+++ b/Geekbot.net/Commands/Rank.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Discord.WebSocket;
+using Geekbot.net.Lib;
+using Serilog;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ public class Rank : ModuleBase
+ {
+ private readonly IEmojiConverter _emojiConverter;
+ private readonly IErrorHandler _errorHandler;
+ private readonly IGeekbotLogger _logger;
+ private readonly IDatabase _redis;
+ private readonly IUserRepository _userRepository;
+ private readonly DiscordSocketClient _client;
+
+ public Rank(IDatabase redis, IErrorHandler errorHandler, IGeekbotLogger logger, IUserRepository userRepository,
+ IEmojiConverter emojiConverter, DiscordSocketClient client)
+ {
+ _redis = redis;
+ _errorHandler = errorHandler;
+ _logger = logger;
+ _userRepository = userRepository;
+ _emojiConverter = emojiConverter;
+ _client = client;
+ }
+
+ [Command("rank", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Statistics)]
+ [Summary("get user top 10 in messages or karma")]
+ public async Task RankCmd([Summary("type")] string typeUnformated = "messages",
+ [Summary("amount")] int amount = 10)
+ {
+ try
+ {
+ var type = typeUnformated.ToCharArray().First().ToString().ToUpper() + typeUnformated.Substring(1);
+
+ if (!type.Equals("Messages") && !type.Equals("Karma") && !type.Equals("Rolls"))
+ {
+ await ReplyAsync("Valid types are '`messages`' '`karma`', '`rolls`'");
+ return;
+ }
+
+ var replyBuilder = new StringBuilder();
+
+ if (amount > 20)
+ {
+ replyBuilder.AppendLine(":warning: Limiting to 20");
+ amount = 20;
+ }
+
+ var messageList = _redis.HashGetAll($"{Context.Guild.Id}:{type}");
+ var sortedList = messageList.OrderByDescending(e => e.Value).ToList();
+ var guildMessages = (int) sortedList.First().Value;
+ sortedList.Remove(sortedList.Single(e => e.Name.ToString().Equals(_client.CurrentUser.Id.ToString())));
+ if (type == "Messages") sortedList.RemoveAt(0);
+
+ var highscoreUsers = new Dictionary();
+ var listLimiter = 1;
+ var failedToRetrieveUser = false;
+ foreach (var user in sortedList)
+ {
+ if (listLimiter > amount) break;
+ try
+ {
+ var guildUser = _userRepository.Get((ulong) user.Name);
+ if (guildUser.Username != null)
+ {
+ highscoreUsers.Add(new RankUserPolyfill
+ {
+ Username = guildUser.Username,
+ Discriminator = guildUser.Discriminator
+ }, (int) user.Value);
+ }
+ else
+ {
+ highscoreUsers.Add(new RankUserPolyfill
+ {
+ Id = user.Name
+ }, (int) user.Value);
+ failedToRetrieveUser = true;
+ }
+
+ listLimiter++;
+ }
+ catch (Exception e)
+ {
+ _logger.Warning("Geekbot", $"Could not retrieve user {user.Name}", e);
+ }
+ }
+
+ if (failedToRetrieveUser) replyBuilder.AppendLine(":warning: Couldn't get all userdata\n");
+ replyBuilder.AppendLine($":bar_chart: **{type} Highscore for {Context.Guild.Name}**");
+ var highscorePlace = 1;
+ foreach (var user in highscoreUsers)
+ {
+ replyBuilder.Append(highscorePlace < 11
+ ? $"{_emojiConverter.numberToEmoji(highscorePlace)} "
+ : $"`{highscorePlace}.` ");
+
+ replyBuilder.Append(user.Key.Username != null
+ ? $"**{user.Key.Username}#{user.Key.Discriminator}**"
+ : $"**{user.Key.Id}**");
+
+ switch (type)
+ {
+ case "Messages":
+ var percent = Math.Round((double) (100 * user.Value) / guildMessages, 2);
+ replyBuilder.Append($" - {percent}% of total - {user.Value} messages");
+ break;
+ case "Karma":
+ replyBuilder.Append($" - {user.Value} Karma");
+ break;
+ case "Rolls":
+ replyBuilder.Append($" - {user.Value} Guessed");
+ break;
+ }
+
+ replyBuilder.Append("\n");
+
+ highscorePlace++;
+ }
+
+ await ReplyAsync(replyBuilder.ToString());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+
+ internal class RankUserPolyfill
+ {
+ public string Username { get; set; }
+ public string Discriminator { get; set; }
+ public string Id { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Role.cs b/Geekbot.net/Commands/Role.cs
new file mode 100644
index 0000000..77e917f
--- /dev/null
+++ b/Geekbot.net/Commands/Role.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Discord.Net;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ [Group("role")]
+ public class Role : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+
+ public Role(IErrorHandler errorHandler, IDatabase redis)
+ {
+ _errorHandler = errorHandler;
+ _redis = redis;
+ }
+
+ [Command(RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Get a list of all available roles.")]
+ public async Task getAllRoles()
+ {
+ try
+ {
+ var roles = _redis.HashGetAll($"{Context.Guild.Id}:RoleWhitelist");
+ if (roles.Length == 0)
+ {
+ await ReplyAsync("There are no roles configured for this server");
+ return;
+ }
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"**Self Service Roles on {Context.Guild.Name}**");
+ sb.AppendLine("To get a role, use `!role name`");
+ foreach (var role in roles) sb.AppendLine($"- {role.Name}");
+ await ReplyAsync(sb.ToString());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command(RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Get a role by mentioning it.")]
+ public async Task giveRole([Summary("roleNickname")] string roleNameRaw)
+ {
+ try
+ {
+ var roleName = roleNameRaw.ToLower();
+ if (_redis.HashExists($"{Context.Guild.Id}:RoleWhitelist", roleName))
+ {
+ var guildUser = (IGuildUser) Context.User;
+ var roleId = ulong.Parse(_redis.HashGet($"{Context.Guild.Id}:RoleWhitelist", roleName));
+ var role = Context.Guild.Roles.First(r => r.Id == roleId);
+ if (role == null)
+ {
+ await ReplyAsync("That role doesn't seem to exist");
+ return;
+ }
+
+ if (guildUser.RoleIds.Contains(roleId))
+ {
+ await guildUser.RemoveRoleAsync(role);
+ await ReplyAsync($"Removed you from {role.Name}");
+ return;
+ }
+
+ await guildUser.AddRoleAsync(role);
+ await ReplyAsync($"Added you to {role.Name}");
+ return;
+ }
+
+ await ReplyAsync("That role doesn't seem to exist");
+ }
+ catch (HttpException e)
+ {
+ _errorHandler.HandleHttpException(e, Context);
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [RequireUserPermission(GuildPermission.Administrator)]
+ [Command("add", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Add a role to the whitelist.")]
+ public async Task addRole([Summary("@role")] IRole role, [Summary("alias")] string roleName)
+ {
+ try
+ {
+ if (role.IsManaged)
+ {
+ await ReplyAsync("You can't add a role that is managed by discord");
+ return;
+ }
+
+ if (role.Permissions.ManageRoles
+ || role.Permissions.Administrator
+ || role.Permissions.ManageGuild
+ || role.Permissions.BanMembers
+ || role.Permissions.KickMembers)
+ {
+ await ReplyAsync(
+ "Woah, i don't think you want to add that role to self service as it contains some dangerous permissions");
+ return;
+ }
+
+ _redis.HashSet($"{Context.Guild.Id}:RoleWhitelist",
+ new[] {new HashEntry(roleName.ToLower(), role.Id.ToString())});
+ await ReplyAsync($"Added {role.Name} to the whitelist");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [RequireUserPermission(GuildPermission.Administrator)]
+ [Command("remove", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Remove a role from the whitelist.")]
+ public async Task removeRole([Summary("roleNickname")] string roleName)
+ {
+ try
+ {
+ _redis.HashDelete($"{Context.Guild.Id}:RoleWhitelist", roleName);
+ await ReplyAsync($"Removed {roleName} from the whitelist");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Roll.cs b/Geekbot.net/Commands/Roll.cs
new file mode 100644
index 0000000..33cdf67
--- /dev/null
+++ b/Geekbot.net/Commands/Roll.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ public class Roll : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+ private readonly Random _rnd;
+ private readonly ITranslationHandler _translation;
+
+ public Roll(IDatabase redis, Random RandomClient, IErrorHandler errorHandler, ITranslationHandler translation)
+ {
+ _redis = redis;
+ _rnd = RandomClient;
+ _translation = translation;
+ _errorHandler = errorHandler;
+ }
+
+ [Command("roll", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Fun)]
+ [Summary("Guess which number the bot will roll (1-100")]
+ public async Task RollCommand([Remainder] [Summary("guess")] string stuff = "noGuess")
+ {
+ try
+ {
+ var number = _rnd.Next(1, 100);
+ var guess = 1000;
+ int.TryParse(stuff, out guess);
+ var transDict = _translation.GetDict(Context);
+ if (guess <= 100 && guess > 0)
+ {
+ var prevRoll = _redis.HashGet($"{Context.Guild.Id}:RollsPrevious", Context.Message.Author.Id);
+ if (!prevRoll.IsNullOrEmpty && prevRoll.ToString() == guess.ToString())
+ {
+ await ReplyAsync(string.Format(transDict["NoPrevGuess"], Context.Message.Author.Mention));
+ return;
+ }
+
+ _redis.HashSet($"{Context.Guild.Id}:RollsPrevious",
+ new[] {new HashEntry(Context.Message.Author.Id, guess)});
+ await ReplyAsync(string.Format(transDict["Rolled"], Context.Message.Author.Mention, number, guess));
+ if (guess == number)
+ {
+ await ReplyAsync(string.Format(transDict["Gratz"], Context.Message.Author));
+ _redis.HashIncrement($"{Context.Guild.Id}:Rolls", Context.User.Id.ToString());
+ }
+ }
+ else
+ {
+ await ReplyAsync(string.Format(transDict["RolledNoGuess"], Context.Message.Author.Mention, number));
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Say.cs b/Geekbot.net/Commands/Say.cs
new file mode 100644
index 0000000..d2bc22e
--- /dev/null
+++ b/Geekbot.net/Commands/Say.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.Commands
+{
+ [Group("say")]
+ public class Say : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IContextHandler _contextHandler;
+
+ public Say(IErrorHandler errorHandler, IContextHandler contextHandler)
+ {
+ _errorHandler = errorHandler;
+ _contextHandler = contextHandler;
+ }
+
+ [RequireUserPermission(GuildPermission.Administrator)]
+ [Command("record", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Say Something.")]
+ public async Task recordEcho()
+ {
+ try
+ {
+ _contextHandler.SaveContext(ContextReference.User, Context, "say");
+ await ReplyAsync("Recording...");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [RequireUserPermission(GuildPermission.Administrator)]
+ [Command("ctx", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Say Something.")]
+ public async Task recordEcho([Remainder] [Summary("What?")] string echo)
+ {
+ Console.WriteLine("actually got here...");
+ _contextHandler.ClearContext(Context.User.Id);
+ await Context.Channel.SendMessageAsync(echo);
+ }
+
+ [RequireUserPermission(GuildPermission.Administrator)]
+ [Command("ss", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Admin)]
+ [Summary("Say Something.")]
+ public async Task Echo([Remainder] [Summary("What?")] string echo)
+ {
+ try
+ {
+ await Context.Message.DeleteAsync();
+ await ReplyAsync(echo);
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Ship.cs b/Geekbot.net/Commands/Ship.cs
new file mode 100644
index 0000000..7b85eb0
--- /dev/null
+++ b/Geekbot.net/Commands/Ship.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ public class Ship : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+ private readonly Random _rnd;
+
+ public Ship(IDatabase redis, Random randomClient, IErrorHandler errorHandler)
+ {
+ _redis = redis;
+ _rnd = randomClient;
+ _errorHandler = errorHandler;
+ }
+
+ [Command("Ship", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Fun)]
+ [Summary("Ask the Shipping meter")]
+ public async Task Command([Summary("@User1")] IUser user1, [Summary("@User2")] IUser user2)
+ {
+ try
+ {
+ var dbstring = "";
+ if (user1.Id > user2.Id)
+ dbstring = $"{user1.Id}-{user2.Id}";
+ else
+ dbstring = $"{user2.Id}-{user1.Id}";
+
+ var dbval = _redis.HashGet($"{Context.Guild.Id}:Ships", dbstring);
+ var shippingRate = 0;
+ if (dbval.IsNullOrEmpty)
+ {
+ shippingRate = _rnd.Next(1, 100);
+ _redis.HashSet($"{Context.Guild.Id}:Ships", dbstring, shippingRate);
+ }
+ else
+ {
+ shippingRate = int.Parse(dbval.ToString());
+ }
+
+ var reply = ":heartpulse: **Matchmaking** :heartpulse:\r\n";
+ reply = reply + $":two_hearts: {user1.Mention} :heart: {user2.Mention} :two_hearts:\r\n";
+ reply = reply + $"0% [{BlockCounter(shippingRate)}] 100% - {DeterminateSuccess(shippingRate)}";
+ await ReplyAsync(reply);
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private string DeterminateSuccess(int rate)
+ {
+ if (rate < 20)
+ return "Not gonna happen";
+ if (rate >= 20 && rate < 40)
+ return "Not such a good idea";
+ if (rate >= 40 && rate < 60)
+ return "There might be a chance";
+ if (rate >= 60 && rate < 80)
+ return "Almost a match, but could work";
+ if (rate >= 80)
+ return "It's a match";
+ return "a";
+ }
+
+ private string BlockCounter(int rate)
+ {
+ var amount = Math.Floor(decimal.Floor(rate / 10));
+ Console.WriteLine(amount);
+ var blocks = "";
+ for (var i = 1; i <= 10; i++)
+ if (i <= amount)
+ {
+ blocks = blocks + ":white_medium_small_square:";
+ if (i == amount)
+ blocks = blocks + $" {rate}% ";
+ }
+ else
+ {
+ blocks = blocks + ":black_medium_small_square:";
+ }
+
+ return blocks;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Slap.cs b/Geekbot.net/Commands/Slap.cs
new file mode 100644
index 0000000..134752c
--- /dev/null
+++ b/Geekbot.net/Commands/Slap.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ public class Slap : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly Random _random;
+ private readonly IDatabase _redis;
+
+ public Slap(IErrorHandler errorHandler, Random random, IDatabase redis)
+ {
+ _errorHandler = errorHandler;
+ _random = random;
+ _redis = redis;
+ }
+
+ [Command("slap", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Fun)]
+ [Summary("slap someone")]
+ public async Task Slapper([Summary("@user")] IUser user)
+ {
+ try
+ {
+ if (user.Id == Context.User.Id)
+ {
+ await ReplyAsync("Why would you slap yourself?");
+ return;
+ }
+
+ var things = new List()
+ {
+ "thing",
+ "rubber chicken",
+ "leek stick",
+ "large trout",
+ "flat hand",
+ "strip of bacon",
+ "feather",
+ "piece of pizza",
+ "moldy banana",
+ "sharp retort",
+ "printed version of wikipedia",
+ "panda paw",
+ "spiked sledgehammer",
+ "monstertruck",
+ "dirty toilet brush",
+ "sleeping seagull",
+ "sunflower",
+ "mousepad",
+ "lolipop",
+ "bottle of rum"
+ };
+
+ _redis.HashIncrement($"{Context.Guild.Id}:SlapsRecieved", user.Id.ToString());
+ _redis.HashIncrement($"{Context.Guild.Id}:SlapsGiven", Context.User.Id.ToString());
+
+ await ReplyAsync($"{Context.User.Username} slapped {user.Username} with a {things[_random.Next(things.Count - 1)]}");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Stats.cs b/Geekbot.net/Commands/Stats.cs
new file mode 100644
index 0000000..67b76b9
--- /dev/null
+++ b/Geekbot.net/Commands/Stats.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ public class Stats : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly ILevelCalc _levelCalc;
+ private readonly IDatabase _redis;
+
+ public Stats(IDatabase redis, IErrorHandler errorHandler, ILevelCalc levelCalc)
+ {
+ _redis = redis;
+ _errorHandler = errorHandler;
+ _levelCalc = levelCalc;
+ }
+
+ [Command("stats", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Statistics)]
+ [Summary("Get information about this user")]
+ public async Task User([Summary("@someone")] IUser user = null)
+ {
+ try
+ {
+ var userInfo = user ?? Context.Message.Author;
+ var userGuildInfo = (IGuildUser) userInfo;
+ var createdAt = userInfo.CreatedAt;
+ var joinedAt = userGuildInfo.JoinedAt.Value;
+ var age = Math.Floor((DateTime.Now - createdAt).TotalDays);
+ var joinedDayAgo = Math.Floor((DateTime.Now - joinedAt).TotalDays);
+
+ var messages = (int) _redis.HashGet($"{Context.Guild.Id}:Messages", userInfo.Id.ToString());
+ var guildMessages = (int) _redis.HashGet($"{Context.Guild.Id}:Messages", 0.ToString());
+ var level = _levelCalc.GetLevel(messages);
+
+ var percent = Math.Round((double) (100 * messages) / guildMessages, 2);
+
+ var eb = new EmbedBuilder();
+ eb.WithAuthor(new EmbedAuthorBuilder()
+ .WithIconUrl(userInfo.GetAvatarUrl())
+ .WithName(userInfo.Username));
+ eb.WithColor(new Color(221, 255, 119));
+
+ var karma = _redis.HashGet($"{Context.Guild.Id}:Karma", userInfo.Id.ToString());
+ var correctRolls = _redis.HashGet($"{Context.Guild.Id}:Rolls", userInfo.Id.ToString());
+
+ eb.AddInlineField("Discordian Since",
+ $"{createdAt.Day}.{createdAt.Month}.{createdAt.Year} ({age} days)")
+ .AddInlineField("Joined Server",
+ $"{joinedAt.Day}.{joinedAt.Month}.{joinedAt.Year} ({joinedDayAgo} days)")
+ .AddInlineField("Karma", karma.ToString() ?? "0")
+ .AddInlineField("Level", level)
+ .AddInlineField("Messages Sent", messages)
+ .AddInlineField("Server Total", $"{percent}%");
+
+ if (!correctRolls.IsNullOrEmpty)
+ eb.AddInlineField("Guessed Rolls", correctRolls);
+
+ await ReplyAsync("", false, eb.Build());
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/UrbanDictionary.cs b/Geekbot.net/Commands/UrbanDictionary.cs
new file mode 100644
index 0000000..573b289
--- /dev/null
+++ b/Geekbot.net/Commands/UrbanDictionary.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using Newtonsoft.Json;
+
+namespace Geekbot.net.Commands
+{
+ public class UrbanDictionary : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+
+ public UrbanDictionary(IErrorHandler errorHandler)
+ {
+ _errorHandler = errorHandler;
+ }
+
+ [Command("urban", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Lookup something on urban dictionary")]
+ public async Task urbanDefine([Remainder] [Summary("word")] string word)
+ {
+ try
+ {
+ using (var client = new HttpClient())
+ {
+ client.BaseAddress = new Uri("https://api.urbandictionary.com");
+ var response = await client.GetAsync($"/v0/define?term={word}");
+ response.EnsureSuccessStatusCode();
+
+ var stringResponse = await response.Content.ReadAsStringAsync();
+ var definitions = JsonConvert.DeserializeObject(stringResponse);
+ if (definitions.list.Count == 0)
+ {
+ await ReplyAsync("That word hasn't been defined...");
+ return;
+ }
+
+ var definition = definitions.list.First(e => !string.IsNullOrWhiteSpace(e.example));
+
+ var eb = new EmbedBuilder();
+ eb.WithAuthor(new EmbedAuthorBuilder
+ {
+ Name = definition.word,
+ Url = definition.permalink
+ });
+ eb.WithColor(new Color(239, 255, 0));
+ eb.Description = definition.definition;
+ eb.AddField("Example", definition.example ?? "(no example given...)");
+ eb.AddInlineField("Upvotes", definition.thumbs_up);
+ eb.AddInlineField("Downvotes", definition.thumbs_down);
+ if (definitions.tags.Length > 0) eb.AddField("Tags", string.Join(", ", definitions.tags));
+
+ await ReplyAsync("", false, eb.Build());
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ private class UrbanResponse
+ {
+ public string[] tags { get; set; }
+ public string result_type { get; set; }
+ public List list { get; set; }
+ }
+
+ private class UrbanListItem
+ {
+ public string definition { get; set; }
+ public string permalink { get; set; }
+ public string thumbs_up { get; set; }
+ public string author { get; set; }
+ public string word { get; set; }
+ public string defid { get; set; }
+ public string current_vote { get; set; }
+ public string example { get; set; }
+ public string thumbs_down { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Voice.cs b/Geekbot.net/Commands/Voice.cs
new file mode 100644
index 0000000..cda1f8b
--- /dev/null
+++ b/Geekbot.net/Commands/Voice.cs
@@ -0,0 +1,102 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.Commands
+{
+ public class Voice : ModuleBase
+ {
+ private readonly IAudioUtils _audioUtils;
+ private readonly IErrorHandler _errorHandler;
+
+ public Voice(IErrorHandler errorHandler, IAudioUtils audioUtils)
+ {
+ _errorHandler = errorHandler;
+ _audioUtils = audioUtils;
+ }
+
+ [Command("join")]
+ public async Task JoinChannel()
+ {
+ try
+ {
+ // Get the audio channel
+ var channel = (Context.User as IGuildUser)?.VoiceChannel;
+ if (channel == null)
+ {
+ await Context.Channel.SendMessageAsync(
+ "User must be in a voice channel, or a voice channel must be passed as an argument.");
+ return;
+ }
+
+ // For the next step with transmitting audio, you would want to pass this Audio Client in to a service.
+ var audioClient = await channel.ConnectAsync();
+ _audioUtils.StoreAudioClient(Context.Guild.Id, audioClient);
+ await ReplyAsync($"Connected to {channel.Name}");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command("disconnect")]
+ public async Task DisconnectChannel()
+ {
+ try
+ {
+ var audioClient = _audioUtils.GetAudioClient(Context.Guild.Id);
+ if (audioClient == null)
+ {
+ await Context.Channel.SendMessageAsync("I'm not in a voice channel at the moment");
+ return;
+ }
+
+ await audioClient.StopAsync();
+ await ReplyAsync("Disconnected from channel!");
+ _audioUtils.Cleanup(Context.Guild.Id);
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ _audioUtils.Cleanup(Context.Guild.Id);
+ }
+ }
+
+ [Command("ytplay")]
+ public async Task ytplay(string url)
+ {
+ try
+ {
+ if (!url.Contains("youtube"))
+ {
+ await ReplyAsync("I can only play youtube videos");
+ return;
+ }
+ var audioClient = _audioUtils.GetAudioClient(Context.Guild.Id);
+ if (audioClient == null)
+ {
+ await ReplyAsync("I'm not in a voice channel at the moment");
+ return;
+ }
+
+ var message = await Context.Channel.SendMessageAsync("Just a second, i'm still a bit slow at this");
+ var ffmpeg = _audioUtils.CreateStreamFromYoutube(url, Context.Guild.Id);
+ var output = ffmpeg.StandardOutput.BaseStream;
+ await message.ModifyAsync(msg => msg.Content = "**Playing!** Please note that this feature is experimental");
+ var discord = audioClient.CreatePCMStream(Discord.Audio.AudioApplication.Mixed);
+ await output.CopyToAsync(discord);
+ await discord.FlushAsync();
+ _audioUtils.Cleanup(Context.Guild.Id);
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ _audioUtils.Cleanup(Context.Guild.Id);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/Youtube.cs b/Geekbot.net/Commands/Youtube.cs
new file mode 100644
index 0000000..305052b
--- /dev/null
+++ b/Geekbot.net/Commands/Youtube.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Threading.Tasks;
+using Discord.Commands;
+using Geekbot.net.Lib;
+using Google.Apis.Services;
+using Google.Apis.YouTube.v3;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Commands
+{
+ public class Youtube : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IDatabase _redis;
+
+ public Youtube(IDatabase redis, IErrorHandler errorHandler)
+ {
+ _redis = redis;
+ _errorHandler = errorHandler;
+ }
+
+ [Command("yt", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Search for something on youtube.")]
+ public async Task Yt([Remainder] [Summary("Title")] string searchQuery)
+ {
+ var key = _redis.StringGet("youtubeKey");
+ if (key.IsNullOrEmpty)
+ {
+ await ReplyAsync("No youtube key set, please tell my senpai to set one");
+ return;
+ }
+
+ try
+ {
+ var youtubeService = new YouTubeService(new BaseClientService.Initializer
+ {
+ ApiKey = key.ToString(),
+ ApplicationName = GetType().ToString()
+ });
+
+ var searchListRequest = youtubeService.Search.List("snippet");
+ searchListRequest.Q = searchQuery;
+ searchListRequest.MaxResults = 2;
+
+ var searchListResponse = await searchListRequest.ExecuteAsync();
+
+ var result = searchListResponse.Items[0];
+
+ await ReplyAsync(
+ $"\"{result.Snippet.Title}\" from \"{result.Snippet.ChannelTitle}\" https://youtu.be/{result.Id.VideoId}");
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Commands/mal.cs b/Geekbot.net/Commands/mal.cs
new file mode 100644
index 0000000..22b3d4a
--- /dev/null
+++ b/Geekbot.net/Commands/mal.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Threading.Tasks;
+using System.Web;
+using Discord;
+using Discord.Commands;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.Commands
+{
+ public class mal : ModuleBase
+ {
+ private readonly IErrorHandler _errorHandler;
+ private readonly IMalClient _malClient;
+
+ public mal(IMalClient malClient, IErrorHandler errorHandler)
+ {
+ _malClient = malClient;
+ _errorHandler = errorHandler;
+ }
+
+ [Command("anime", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Show Info about an Anime.")]
+ public async Task searchAnime([Remainder] [Summary("AnimeName")] string animeName)
+ {
+ try
+ {
+ if (_malClient.isLoggedIn())
+ {
+ var anime = await _malClient.getAnime(animeName);
+ if (anime != null)
+ {
+ var eb = new EmbedBuilder();
+
+ var description = HttpUtility.HtmlDecode(anime.Synopsis)
+ .Replace("
", "")
+ .Replace("[i]", "*")
+ .Replace("[/i]", "*");
+
+ eb.Title = anime.Title;
+ eb.Description = description;
+ eb.ImageUrl = anime.Image;
+ eb.AddInlineField("Premiered", $"{anime.StartDate}");
+ eb.AddInlineField("Ended", anime.EndDate == "0000-00-00" ? "???" : anime.EndDate);
+ eb.AddInlineField("Status", anime.Status);
+ eb.AddInlineField("Episodes", anime.Episodes);
+ eb.AddInlineField("MAL Score", anime.Score);
+ eb.AddInlineField("Type", anime.Type);
+ eb.AddField("MAL Link", $"https://myanimelist.net/anime/{anime.Id}");
+
+ await ReplyAsync("", false, eb.Build());
+ }
+ else
+ {
+ await ReplyAsync("No anime found with that name...");
+ }
+ }
+ else
+ {
+ await ReplyAsync(
+ "Unfortunally i'm not connected to MyAnimeList.net, please tell my senpai to connect me");
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+
+ [Command("manga", RunMode = RunMode.Async)]
+ [Remarks(CommandCategories.Helpers)]
+ [Summary("Show Info about a Manga.")]
+ public async Task searchManga([Remainder] [Summary("MangaName")] string mangaName)
+ {
+ try
+ {
+ if (_malClient.isLoggedIn())
+ {
+ var manga = await _malClient.getManga(mangaName);
+ if (manga != null)
+ {
+ var eb = new EmbedBuilder();
+
+ var description = HttpUtility.HtmlDecode(manga.Synopsis)
+ .Replace("
", "")
+ .Replace("[i]", "*")
+ .Replace("[/i]", "*");
+
+ eb.Title = manga.Title;
+ eb.Description = description;
+ eb.ImageUrl = manga.Image;
+ eb.AddInlineField("Premiered", $"{manga.StartDate}");
+ eb.AddInlineField("Ended", manga.EndDate == "0000-00-00" ? "???" : manga.EndDate);
+ eb.AddInlineField("Status", manga.Status);
+ eb.AddInlineField("Volumes", manga.Volumes);
+ eb.AddInlineField("Chapters", manga.Chapters);
+ eb.AddInlineField("MAL Score", manga.Score);
+ eb.AddField("MAL Link", $"https://myanimelist.net/manga/{manga.Id}");
+
+ await ReplyAsync("", false, eb.Build());
+ }
+ else
+ {
+ await ReplyAsync("No manga found with that name...");
+ }
+ }
+ else
+ {
+ await ReplyAsync(
+ "Unfortunally i'm not connected to MyAnimeList.net, please tell my senpai to connect me");
+ }
+ }
+ catch (Exception e)
+ {
+ _errorHandler.HandleCommandException(e, Context);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Geekbot.net.csproj b/Geekbot.net/Geekbot.net.csproj
new file mode 100755
index 0000000..82b605a
--- /dev/null
+++ b/Geekbot.net/Geekbot.net.csproj
@@ -0,0 +1,73 @@
+
+
+ Exe
+ netcoreapp2.0
+ derp.ico
+ 1.1.0
+ Pizza and Coffee Studios
+ Pizza and Coffee Studios
+ A Discord bot
+ https://github.com/pizzaandcoffee/Geekbot.net
+ NU1701
+
+
+
+ 1.0.2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1.2.6
+
+
+
+
+ 4.3.0
+
+
+ 4.3.0
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
\ No newline at end of file
diff --git a/Geekbot.net/Handlers.cs b/Geekbot.net/Handlers.cs
new file mode 100644
index 0000000..fc0f871
--- /dev/null
+++ b/Geekbot.net/Handlers.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections;
+using System.Text;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Geekbot.net.Commands;
+using Geekbot.net.Lib;
+using Serilog;
+using StackExchange.Redis;
+
+namespace Geekbot.net
+{
+ public class Handlers
+ {
+ private readonly IDiscordClient _client;
+ private readonly IGeekbotLogger _logger;
+ private readonly IDatabase _redis;
+ private readonly IServiceProvider _servicesProvider;
+ private readonly CommandService _commands;
+ private readonly IUserRepository _userRepository;
+ private readonly IContextHandler _contextHandler;
+
+ public Handlers(IDiscordClient client, IGeekbotLogger logger, IDatabase redis, IServiceProvider servicesProvider, CommandService commands, IUserRepository userRepository, IContextHandler contextHandler)
+ {
+ _client = client;
+ _logger = logger;
+ _redis = redis;
+ _servicesProvider = servicesProvider;
+ _commands = commands;
+ _userRepository = userRepository;
+ _contextHandler = contextHandler;
+ }
+
+ //
+ // Incoming Messages
+ //
+
+ public Task RunCommand(SocketMessage messageParam)
+ {
+ try
+ {
+ if (!(messageParam is SocketUserMessage message)) return Task.CompletedTask;
+ if (message.Author.IsBot) return Task.CompletedTask;
+ var argPos = 0;
+ var lowCaseMsg = message.ToString().ToLower();
+ if (lowCaseMsg.Equals("ping") || lowCaseMsg.StartsWith("ping "))
+ {
+ message.Channel.SendMessageAsync("pong");
+ return Task.CompletedTask;
+ }
+ if (lowCaseMsg.StartsWith("hui"))
+ {
+ message.Channel.SendMessageAsync("hui!!!");
+ return Task.CompletedTask;
+ }
+
+ var contextType = _contextHandler.HasContext(message);
+ if (contextType != ContextReference.None)
+ {
+ _contextHandler.ExecuteOnContext(contextType, message);
+ return Task.CompletedTask;
+ }
+ if (!(message.HasCharPrefix('!', ref argPos) ||
+ message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return Task.CompletedTask;
+ var context = new CommandContext(_client, message);
+ var commandExec = _commands.ExecuteAsync(context, argPos, _servicesProvider);
+ return Task.CompletedTask;
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Geekbot", "Failed to run commands", e);
+ return Task.CompletedTask;
+ }
+ }
+
+ public Task UpdateStats(SocketMessage message)
+ {
+ try
+ {
+ if (message == null) return Task.CompletedTask;
+ if (message.Channel.Name.StartsWith('@'))
+ {
+ _logger.Information("Message", "DM-Channel - {message.Channel.Name} - {message.Content}");
+ return Task.CompletedTask;
+ }
+ var channel = (SocketGuildChannel) message.Channel;
+
+ _redis.HashIncrementAsync($"{channel.Guild.Id}:Messages", message.Author.Id.ToString());
+ _redis.HashIncrementAsync($"{channel.Guild.Id}:Messages", 0.ToString());
+
+ if (message.Author.IsBot) return Task.CompletedTask;
+ _logger.Information("Message", message.Content, SimpleConextConverter.ConvertSocketMessage(message));
+// _logger.Information($"[Message] {channel.Guild.Name} ({channel.Guild.Id}) - {message.Channel} ({message.Channel.Id}) - {message.Author.Username}#{message.Author.Discriminator} ({message.Author.Id}) - {message.Content}");
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Message", "Could not process message stats", e);
+ }
+ return Task.CompletedTask;
+ }
+
+ //
+ // User Stuff
+ //
+
+ public Task UserJoined(SocketGuildUser user)
+ {
+ try
+ {
+ if (!user.IsBot)
+ {
+ var message = _redis.HashGet($"{user.Guild.Id}:Settings", "WelcomeMsg");
+ if (!message.IsNullOrEmpty)
+ {
+ message = message.ToString().Replace("$user", user.Mention);
+ user.Guild.DefaultChannel.SendMessageAsync(message);
+ }
+ }
+ _userRepository.Update(user);
+ _logger.Information("Geekbot", $"{user.Username} ({user.Id}) joined {user.Guild.Name} ({user.Guild.Id})");
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Geekbot", "Failed to send welcome message", e);
+ }
+ return Task.CompletedTask;
+ }
+
+ public Task UserUpdated(SocketUser oldUser, SocketUser newUser)
+ {
+ _userRepository.Update(newUser);
+ return Task.CompletedTask;
+ }
+
+ public async Task UserLeft(SocketGuildUser user)
+ {
+ try
+ {
+ var sendLeftEnabled = _redis.HashGet($"{user.Guild.Id}:Settings", "ShowLeave");
+ if (sendLeftEnabled.ToString() == "1")
+ {
+ var modChannel = ulong.Parse(_redis.HashGet($"{user.Guild.Id}:Settings", "ModChannel"));
+ if (!string.IsNullOrEmpty(modChannel.ToString()))
+ {
+ var modChannelSocket = (ISocketMessageChannel) await _client.GetChannelAsync(modChannel);
+ await modChannelSocket.SendMessageAsync($"{user.Username}#{user.Discriminator} left the server");
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Geekbot", "Failed to send leave message", e);
+ }
+ _logger.Information("Geekbot", $"{user.Username} ({user.Id}) joined {user.Guild.Name} ({user.Guild.Id})");
+ }
+
+ //
+ // Message Stuff
+ //
+
+ public async Task MessageDeleted(Cacheable message, ISocketMessageChannel channel)
+ {
+ try
+ {
+ var guild = ((IGuildChannel) channel).Guild;
+ var sendLeftEnabled = _redis.HashGet($"{guild.Id}:Settings", "ShowDelete");
+ if (sendLeftEnabled.ToString() == "1")
+ {
+ var modChannel = ulong.Parse(_redis.HashGet($"{guild.Id}:Settings", "ModChannel"));
+ if (!string.IsNullOrEmpty(modChannel.ToString()) && modChannel != channel.Id)
+ {
+ var modChannelSocket = (ISocketMessageChannel) await _client.GetChannelAsync(modChannel);
+ var sb = new StringBuilder();
+ if (message.Value != null)
+ {
+ sb.AppendLine(
+ $"The following message from {message.Value.Author.Username}#{message.Value.Author.Discriminator} was deleted in <#{channel.Id}>");
+ sb.AppendLine(message.Value.Content);
+ }
+ else
+ {
+ sb.AppendLine("Someone deleted a message, the message was not cached...");
+ }
+ await modChannelSocket.SendMessageAsync(sb.ToString());
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Geekbot", "Failed to send delete message...", e);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/AudioClientCache.cs b/Geekbot.net/Lib/AudioClientCache.cs
new file mode 100644
index 0000000..d43b98a
--- /dev/null
+++ b/Geekbot.net/Lib/AudioClientCache.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Security.Cryptography;
+using Discord.Audio;
+using Discord.Net;
+
+namespace Geekbot.net.Lib
+{
+ public class AudioUtils : IAudioUtils
+ {
+ private string _tempFolderPath;
+ private Dictionary _audioClients;
+
+ public AudioUtils()
+ {
+ _audioClients = new Dictionary();
+ _tempFolderPath = Path.GetFullPath("./tmp/");
+ if (Directory.Exists(_tempFolderPath))
+ {
+ Directory.Delete(_tempFolderPath, true);
+ }
+ Directory.CreateDirectory(_tempFolderPath);
+ }
+
+ public IAudioClient GetAudioClient(ulong guildId)
+ {
+ return _audioClients[guildId];
+ }
+
+ public void StoreAudioClient(ulong guildId, IAudioClient client)
+ {
+ _audioClients[guildId] = client;
+ }
+
+ public Process CreateStreamFromFile(string path)
+ {
+ var ffmpeg = new ProcessStartInfo
+ {
+ FileName = "ffmpeg",
+ Arguments = $"-i {path} -ac 2 -f s16le -ar 48000 pipe:1",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ };
+ return Process.Start(ffmpeg);
+ }
+
+ public Process CreateStreamFromYoutube(string url, ulong guildId)
+ {
+ var ytdlMediaUrl = GetYoutubeMediaUrl(url);
+ DownloadMediaUrl(ytdlMediaUrl, guildId);
+ return CreateStreamFromFile($"{_tempFolderPath}{guildId}");
+ }
+
+ public void Cleanup(ulong guildId)
+ {
+ File.Delete($"{_tempFolderPath}{guildId}");
+ }
+
+ private string GetYoutubeMediaUrl(string url)
+ {
+ var ytdl = new ProcessStartInfo()
+ {
+ FileName = "youtube-dl",
+ Arguments = $"-f bestaudio -g {url}",
+ UseShellExecute = false,
+ RedirectStandardOutput = true
+ };
+ var output = Process.Start(ytdl).StandardOutput.ReadToEnd();
+ if (string.IsNullOrWhiteSpace(output))
+ {
+ throw new Exception("Could not get Youtube Media URL");
+ }
+ return output;
+ }
+
+ private void DownloadMediaUrl(string url, ulong guildId)
+ {
+ using (var web = new WebClient())
+ {
+ web.DownloadFile(url, $"{_tempFolderPath}{guildId}");
+ }
+// var ffmpeg = new ProcessStartInfo
+// {
+// FileName = "ffmpeg",
+// Arguments = $"-i \"{_tempFolderPath}{guildId}\" -c:a mp3 -b:a 256k {_tempFolderPath}{guildId}.mp3",
+// UseShellExecute = false,
+// RedirectStandardOutput = true,
+// };
+// Process.Start(ffmpeg).WaitForExit();
+// File.Delete($"{_tempFolderPath}{guildId}");
+ return;
+ }
+ }
+
+ public interface IAudioUtils
+ {
+ IAudioClient GetAudioClient(ulong guildId);
+ void StoreAudioClient(ulong guildId, IAudioClient client);
+ Process CreateStreamFromFile(string path);
+ Process CreateStreamFromYoutube(string url, ulong guildId);
+ void Cleanup(ulong guildId);
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/CommandCategories.cs b/Geekbot.net/Lib/CommandCategories.cs
new file mode 100644
index 0000000..ca7ea58
--- /dev/null
+++ b/Geekbot.net/Lib/CommandCategories.cs
@@ -0,0 +1,15 @@
+namespace Geekbot.net.Lib
+{
+ public class CommandCategories
+ {
+ public const string Randomness = "Randomness";
+ public const string Karma = "Karma";
+ public const string Quotes = "Quotes";
+ public const string Fun = "Fun";
+ public const string Statistics = "Statistics";
+ public const string Helpers = "Helpers";
+ public const string Games = "Games";
+ public const string Admin = "Admin";
+ public const string Uncategorized = "Uncategorized";
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/Constants.cs b/Geekbot.net/Lib/Constants.cs
new file mode 100644
index 0000000..db5c875
--- /dev/null
+++ b/Geekbot.net/Lib/Constants.cs
@@ -0,0 +1,9 @@
+namespace Geekbot.net.Lib
+{
+ public class Constants
+ {
+ public const string Name = "Geekbot";
+ public const double BotVersion = 3.5;
+ public const double ApiVersion = 1;
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/EmojiConverter.cs b/Geekbot.net/Lib/EmojiConverter.cs
new file mode 100644
index 0000000..60e75f4
--- /dev/null
+++ b/Geekbot.net/Lib/EmojiConverter.cs
@@ -0,0 +1,99 @@
+using System.Collections;
+using System.Text;
+
+namespace Geekbot.net.Lib
+{
+ public class EmojiConverter : IEmojiConverter
+ {
+ public string numberToEmoji(int number)
+ {
+ if (number == 10)
+ {
+ return "🔟";
+ }
+ var emojiMap = new string[]
+ {
+ ":zero:",
+ ":one:",
+ ":two:",
+ ":three:",
+ ":four:",
+ ":five:",
+ ":six:",
+ ":seven:",
+ ":eight:",
+ ":nine:",
+ };
+ var numbers = number.ToString().ToCharArray();
+ var returnString = new StringBuilder();
+ foreach (var n in numbers)
+ {
+ returnString.Append(emojiMap[int.Parse(n.ToString())]);
+ }
+ return returnString.ToString();
+ }
+
+ public string textToEmoji(string text)
+ {
+ var emojiMap = new Hashtable
+ {
+ ['A'] = ":regional_indicator_a:",
+ ['B'] = ":b:",
+ ['C'] = ":regional_indicator_c:",
+ ['D'] = ":regional_indicator_d:",
+ ['E'] = ":regional_indicator_e:",
+ ['F'] = ":regional_indicator_f:",
+ ['G'] = ":regional_indicator_g:",
+ ['H'] = ":regional_indicator_h:",
+ ['I'] = ":regional_indicator_i:",
+ ['J'] = ":regional_indicator_j:",
+ ['K'] = ":regional_indicator_k:",
+ ['L'] = ":regional_indicator_l:",
+ ['M'] = ":regional_indicator_m:",
+ ['N'] = ":regional_indicator_n:",
+ ['O'] = ":regional_indicator_o:",
+ ['P'] = ":regional_indicator_p:",
+ ['Q'] = ":regional_indicator_q:",
+ ['R'] = ":regional_indicator_r:",
+ ['S'] = ":regional_indicator_s:",
+ ['T'] = ":regional_indicator_t:",
+ ['U'] = ":regional_indicator_u:",
+ ['V'] = ":regional_indicator_v:",
+ ['W'] = ":regional_indicator_w:",
+ ['X'] = ":regional_indicator_x:",
+ ['Y'] = ":regional_indicator_y:",
+ ['Z'] = ":regional_indicator_z:",
+ ['!'] = ":exclamation:",
+ ['?'] = ":question:",
+ ['#'] = ":hash:",
+ ['*'] = ":star2:",
+ ['+'] = ":heavy_plus_sign:",
+ ['0'] = ":zero:",
+ ['1'] = ":one:",
+ ['2'] = ":two:",
+ ['3'] = ":three:",
+ ['4'] = ":four:",
+ ['5'] = ":five:",
+ ['6'] = ":six:",
+ ['7'] = ":seven:",
+ ['8'] = ":eight:",
+ ['9'] = ":nine:",
+ [' '] = " "
+ };
+ var letters = text.ToUpper().ToCharArray();
+ var returnString = new StringBuilder();
+ foreach (var n in letters)
+ {
+ var emoji = emojiMap[n] ?? n;
+ returnString.Append(emoji);
+ }
+ return returnString.ToString();
+ }
+ }
+
+ public interface IEmojiConverter
+ {
+ string numberToEmoji(int number);
+ string textToEmoji(string text);
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/ErrorHandler.cs b/Geekbot.net/Lib/ErrorHandler.cs
new file mode 100644
index 0000000..332a824
--- /dev/null
+++ b/Geekbot.net/Lib/ErrorHandler.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Net;
+using System.Runtime.InteropServices.ComTypes;
+using System.Security.Principal;
+using Discord.Commands;
+using Discord.Net;
+using Nancy.Extensions;
+using Serilog;
+using SharpRaven;
+using SharpRaven.Data;
+using SharpRaven.Utilities;
+using Utf8Json;
+
+namespace Geekbot.net.Lib
+{
+ public class ErrorHandler : IErrorHandler
+ {
+ private readonly IGeekbotLogger _logger;
+ private readonly ITranslationHandler _translation;
+ private readonly IRavenClient _raven;
+
+ public ErrorHandler(IGeekbotLogger logger, ITranslationHandler translation)
+ {
+ _logger = logger;
+ _translation = translation;
+
+ var sentryDsn = Environment.GetEnvironmentVariable("SENTRY");
+ if (!string.IsNullOrEmpty(sentryDsn))
+ {
+ _raven = new RavenClient(sentryDsn);
+ _logger.Information("Geekbot", $"Command Errors will be logged to Sentry: {sentryDsn}");
+ }
+ else
+ {
+ _raven = null;
+ }
+ }
+
+ public void HandleCommandException(Exception e, ICommandContext Context, string errorMessage = "def")
+ {
+ try
+ {
+ var errorString = errorMessage == "def" ? _translation.GetString(Context.Guild.Id, "errorHandler", "SomethingWentWrong") : errorMessage;
+ var errorObj = SimpleConextConverter.ConvertContext(Context);
+ _logger.Error("Geekbot", "An error ocured", e, errorObj);
+ if (!string.IsNullOrEmpty(errorMessage))
+ {
+ Context.Channel.SendMessageAsync(errorString);
+ }
+
+ if (e.Message.Contains("50013")) return;
+ if (e.Message.Contains("50007")) return;
+ if (_raven == null) return;
+
+ var sentryEvent = new SentryEvent(e)
+ {
+ Tags =
+ {
+ ["discord_server"] = errorObj.Guild.Name,
+ ["discord_user"] = errorObj.User.Name
+ },
+ Message = errorObj.Message.Content,
+ Extra = errorObj
+ };
+ _raven.Capture(sentryEvent);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Geekbot", "Errorception", ex);
+ }
+ }
+
+ public async void HandleHttpException(HttpException e, ICommandContext Context)
+ {
+ var errorStrings = _translation.GetDict(Context, "httpErrors");
+ switch(e.HttpCode)
+ {
+ case HttpStatusCode.Forbidden:
+ await Context.Channel.SendMessageAsync(errorStrings["403"]);
+ break;
+ }
+ }
+
+
+ }
+
+ public interface IErrorHandler
+ {
+ void HandleCommandException(Exception e, ICommandContext Context, string errorMessage = "def");
+ void HandleHttpException(HttpException e, ICommandContext Context);
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/GeekbotLogger.cs b/Geekbot.net/Lib/GeekbotLogger.cs
new file mode 100644
index 0000000..18e8c54
--- /dev/null
+++ b/Geekbot.net/Lib/GeekbotLogger.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Threading.Tasks;
+using Serilog;
+using Utf8Json;
+using Utf8Json.Formatters;
+using Utf8Json.Resolvers;
+
+namespace Geekbot.net.Lib
+{
+ public class GeekbotLogger : IGeekbotLogger
+ {
+ private readonly ILogger _serilog;
+ public GeekbotLogger()
+ {
+ _serilog = LoggerFactory.createLogger();
+ //JsonSerializer.SetDefaultResolver(StandardResolver.AllowPrivateExcludeNullSnakeCase);
+ Information("Geekbot", "Using GeekbotLogger");
+ }
+
+ public void Debug(string source, string message, object extra = null)
+ {
+ HandleLogObject("Debug", source, message, null, extra);
+ }
+
+ public void Information(string source, string message, object extra = null)
+ {
+ HandleLogObject("Information", source, message, null, extra);
+ }
+
+ public void Warning(string source, string message, Exception stackTrace = null, object extra = null)
+ {
+ HandleLogObject("Warning", source, message, stackTrace, extra);
+ }
+
+ public void Error(string source, string message, Exception stackTrace, object extra = null)
+ {
+ HandleLogObject("Error", source, message, stackTrace, extra);
+ }
+
+ private Task HandleLogObject(string type, string source, string message, Exception stackTrace = null, object extra = null)
+ {
+ var logJson = CreateLogObject(type, source, message, null, extra);
+ // fuck serilog
+ _serilog.Information(logJson + "}");
+ return Task.CompletedTask;
+ }
+
+ private string CreateLogObject(string type, string source, string message, Exception stackTrace = null, object extra = null)
+ {
+ var logObject = new GeekbotLoggerObject()
+ {
+ Timestamp = DateTime.Now,
+ Type = type,
+ Source = source,
+ Message = message,
+ StackTrace = stackTrace,
+ Extra = extra
+ };
+ return JsonSerializer.ToJsonString(logObject);
+ }
+ }
+
+ public class GeekbotLoggerObject
+ {
+ public DateTime Timestamp { get; set; }
+ public string Type { get; set; }
+ public string Source { get; set; }
+ public string Message { get; set; }
+ public Exception StackTrace { get; set; }
+ public object Extra { get; set; }
+ }
+
+ public interface IGeekbotLogger
+ {
+ void Debug(string source, string message, object extra = null);
+ void Information(string source, string message, object extra = null);
+ void Warning(string source, string message, Exception stackTrace = null, object extra = null);
+ void Error(string source, string message, Exception stackTrace, object extra = null);
+ }
+}
\ No newline at end of file
diff --git a/src/Core/Levels/LevelCalc.cs b/Geekbot.net/Lib/LevelCalc.cs
similarity index 56%
rename from src/Core/Levels/LevelCalc.cs
rename to Geekbot.net/Lib/LevelCalc.cs
index 8203f97..d075754 100644
--- a/src/Core/Levels/LevelCalc.cs
+++ b/Geekbot.net/Lib/LevelCalc.cs
@@ -2,11 +2,11 @@
using System.Collections.Generic;
using System.Linq;
-namespace Geekbot.Core.Levels
+namespace Geekbot.net.Lib
{
public class LevelCalc : ILevelCalc
{
- private readonly int[] _levels;
+ private int[] _levels;
public LevelCalc()
{
@@ -20,9 +20,20 @@ namespace Geekbot.Core.Levels
_levels = levels.ToArray();
}
- public int GetLevel(int? messages)
+ public int GetLevel(int messages)
{
- return 1 + _levels.TakeWhile(level => !(level > messages)).Count();
+ var returnVal = 1;
+ foreach (var level in _levels)
+ {
+ if (level > messages) break;
+ returnVal++;
+ }
+ return returnVal;
}
}
+
+ public interface ILevelCalc
+ {
+ int GetLevel(int experience);
+ }
}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/LoggerFactory.cs b/Geekbot.net/Lib/LoggerFactory.cs
new file mode 100644
index 0000000..789bdc6
--- /dev/null
+++ b/Geekbot.net/Lib/LoggerFactory.cs
@@ -0,0 +1,28 @@
+using System;
+using Serilog;
+using Serilog.Formatting.Json;
+using Serilog.Sinks.SumoLogic;
+
+namespace Geekbot.net.Lib
+{
+ public class LoggerFactory
+ {
+ public static ILogger createLogger()
+ {
+ var loggerCreation = new LoggerConfiguration();
+ var template = "{Message}{NewLine}";
+ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GEEKBOT_SUMO")))
+ {
+ Console.WriteLine("Logging Geekbot Logs to Sumologic");
+ loggerCreation.WriteTo.SumoLogic(Environment.GetEnvironmentVariable("GEEKBOT_SUMO"),
+ outputTemplate: template);
+ }
+ else
+ {
+ loggerCreation.WriteTo.LiterateConsole(outputTemplate: template);
+ loggerCreation.WriteTo.RollingFile("Logs/geekbot-{Date}.txt", shared: true, outputTemplate: template);
+ }
+ return loggerCreation.CreateLogger();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/MalClient.cs b/Geekbot.net/Lib/MalClient.cs
new file mode 100644
index 0000000..bc09036
--- /dev/null
+++ b/Geekbot.net/Lib/MalClient.cs
@@ -0,0 +1,78 @@
+using System.Threading.Tasks;
+using MyAnimeListSharp.Auth;
+using MyAnimeListSharp.Core;
+using MyAnimeListSharp.Facade.Async;
+using Serilog;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Lib
+{
+ public class MalClient : IMalClient
+ {
+ private readonly IDatabase _redis;
+ private readonly IGeekbotLogger _logger;
+ private ICredentialContext _credentials;
+ private AnimeSearchMethodsAsync _animeSearch;
+ private MangaSearchMethodsAsync _mangaSearch;
+
+ public MalClient(IDatabase redis, IGeekbotLogger logger)
+ {
+ _redis = redis;
+ _logger = logger;
+ reloadClient();
+ }
+
+ public bool reloadClient()
+ {
+ var malCredentials = _redis.HashGetAll("malCredentials");
+ if (malCredentials.Length != 0)
+ {
+ _credentials = new CredentialContext();
+ foreach (var c in malCredentials)
+ {
+ switch (c.Name)
+ {
+ case "Username":
+ _credentials.UserName = c.Value;
+ break;
+ case "Password":
+ _credentials.Password = c.Value;
+ break;
+ }
+ }
+ _animeSearch = new AnimeSearchMethodsAsync(_credentials);
+ _mangaSearch = new MangaSearchMethodsAsync(_credentials);
+ _logger.Debug("Geekbot", "Logged in to MAL");
+ return true;
+ }
+ _logger.Debug("Geekbot", "No MAL Credentials Set!");
+ return false;
+
+ }
+
+ public bool isLoggedIn()
+ {
+ return _credentials != null;
+ }
+
+ public async Task getAnime(string query)
+ {
+ var response = await _animeSearch.SearchDeserializedAsync(query);
+ return response.Entries.Count == 0 ? null : response.Entries[0];
+ }
+
+ public async Task getManga(string query)
+ {
+ var response = await _mangaSearch.SearchDeserializedAsync(query);
+ return response.Entries.Count == 0 ? null : response.Entries[0];
+ }
+ }
+
+ public interface IMalClient
+ {
+ bool reloadClient();
+ bool isLoggedIn();
+ Task getAnime(string query);
+ Task getManga(string query);
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/Media/FortunesProvider.cs b/Geekbot.net/Lib/Media/FortunesProvider.cs
new file mode 100644
index 0000000..f536074
--- /dev/null
+++ b/Geekbot.net/Lib/Media/FortunesProvider.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using Serilog;
+
+namespace Geekbot.net.Lib.Media
+{
+ internal class FortunesProvider : IFortunesProvider
+ {
+ private readonly string[] fortuneArray;
+ private readonly Random rnd;
+ private readonly int totalFortunes;
+
+ public FortunesProvider(Random rnd, IGeekbotLogger logger)
+ {
+ var path = Path.GetFullPath("./Storage/fortunes");
+ if (File.Exists(path))
+ {
+ var rawFortunes = File.ReadAllText(path);
+ fortuneArray = rawFortunes.Split("%");
+ totalFortunes = fortuneArray.Length;
+ this.rnd = rnd;
+ logger.Debug("Geekbot", "Loaded {totalFortunes} Fortunes");
+ }
+ else
+ {
+ logger.Information("Geekbot", $"Fortunes File not found at {path}");
+ }
+ }
+
+ public string GetRandomFortune()
+ {
+ return fortuneArray[rnd.Next(0, totalFortunes)];
+ }
+ }
+
+ public interface IFortunesProvider
+ {
+ string GetRandomFortune();
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/Media/MediaProvider.cs b/Geekbot.net/Lib/Media/MediaProvider.cs
new file mode 100644
index 0000000..ce5a0cd
--- /dev/null
+++ b/Geekbot.net/Lib/Media/MediaProvider.cs
@@ -0,0 +1,117 @@
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Serilog;
+
+namespace Geekbot.net.Lib.Media
+{
+ public class MediaProvider : IMediaProvider
+ {
+ private readonly Random _random;
+ private readonly IGeekbotLogger _logger;
+ private string[] _checkemImages;
+ private string[] _pandaImages;
+ private string[] _croissantImages;
+ private string[] _squirrelImages;
+ private string[] _pumpkinImages;
+ private string[] _turtlesImages;
+
+ public MediaProvider(Random rnd, IGeekbotLogger logger)
+ {
+ _random = rnd;
+ _logger = logger;
+
+ logger.Information("Geekbot", "Loading Media Files");
+
+ LoadCheckem();
+ LoadPandas();
+ BakeCroissants();
+ LoadSquirrels();
+ LoadPumpkins();
+ LoadTurtles();
+ }
+
+ private void LoadCheckem()
+ {
+ var rawLinks = File.ReadAllText(Path.GetFullPath("./Storage/checkEmPics"));
+ _checkemImages = rawLinks.Split("\n");
+ _logger.Debug("Geekbot", $"Loaded {_checkemImages.Length} CheckEm Images");
+ }
+
+ private void LoadPandas()
+ {
+ var rawLinks = File.ReadAllText(Path.GetFullPath("./Storage/pandas"));
+ _pandaImages = rawLinks.Split("\n");
+ _logger.Debug("Geekbot", $"Loaded {_pandaImages.Length} Panda Images");
+ }
+
+ private void BakeCroissants()
+ {
+ var rawLinks = File.ReadAllText(Path.GetFullPath("./Storage/croissant"));
+ _croissantImages = rawLinks.Split("\n");
+ _logger.Debug("Geekbot", $"Loaded {_croissantImages.Length} Croissant Images");
+ }
+
+ private void LoadSquirrels()
+ {
+ var rawLinks = File.ReadAllText(Path.GetFullPath("./Storage/squirrel"));
+ _squirrelImages = rawLinks.Split("\n");
+ _logger.Debug("Geekbot", $"Loaded {_squirrelImages.Length} Squirrel Images");
+ }
+
+ private void LoadPumpkins()
+ {
+ var rawLinks = File.ReadAllText(Path.GetFullPath("./Storage/pumpkin"));
+ _pumpkinImages = rawLinks.Split("\n");
+ _logger.Debug("Geekbot", $"Loaded {_pumpkinImages.Length} Pumpkin Images");
+ }
+
+ private void LoadTurtles()
+ {
+ var rawLinks = File.ReadAllText(Path.GetFullPath("./Storage/turtles"));
+ _turtlesImages = rawLinks.Split("\n");
+ _logger.Debug("Geekbot", $"Loaded {_turtlesImages.Length} Turtle Images");
+ }
+
+ public string getCheckem()
+ {
+ return _checkemImages[_random.Next(0, _checkemImages.Length)];
+ }
+
+ public string getPanda()
+ {
+ return _pandaImages[_random.Next(0, _pandaImages.Length)];
+ }
+
+ public string getCrossant()
+ {
+ return _croissantImages[_random.Next(0, _croissantImages.Length)];
+ }
+
+ public string getSquirrel()
+ {
+ return _squirrelImages[_random.Next(0, _squirrelImages.Length)];
+ }
+
+ public string getPumpkin()
+ {
+ return _pumpkinImages[_random.Next(0, _pumpkinImages.Length)];
+ }
+
+ public string getTurtle()
+ {
+ return _turtlesImages[_random.Next(0, _turtlesImages.Length)];
+ }
+ }
+
+ public interface IMediaProvider
+ {
+ string getCheckem();
+ string getPanda();
+ string getCrossant();
+ string getSquirrel();
+ string getPumpkin();
+ string getTurtle();
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/SimpleConextConverter.cs b/Geekbot.net/Lib/SimpleConextConverter.cs
new file mode 100644
index 0000000..3dbc58b
--- /dev/null
+++ b/Geekbot.net/Lib/SimpleConextConverter.cs
@@ -0,0 +1,97 @@
+using System;
+using Discord.Commands;
+using Discord.WebSocket;
+
+namespace Geekbot.net.Lib
+{
+ public class SimpleConextConverter
+ {
+ public static MessageDto ConvertContext(ICommandContext context)
+ {
+ return new MessageDto()
+ {
+ Message = new MessageDto.MessageContent()
+ {
+ Content = context.Message.Content,
+ Id = context.Message.Id.ToString(),
+ Attachments = context.Message.Attachments.Count,
+ ChannelMentions = context.Message.MentionedChannelIds.Count,
+ UserMentions = context.Message.MentionedUserIds.Count,
+ RoleMentions = context.Message.MentionedRoleIds.Count
+ },
+ User = new MessageDto.IdAndName()
+ {
+ Id = context.User.Id.ToString(),
+ Name = $"{context.User.Username}#{context.User.Discriminator}"
+ },
+ Guild = new MessageDto.IdAndName()
+ {
+ Id = context.Guild.Id.ToString(),
+ Name = context.Guild.Name
+ },
+ Channel = new MessageDto.IdAndName()
+ {
+ Id = context.Channel.Id.ToString(),
+ Name = context.Channel.Name
+ }
+ };
+ }
+ public static MessageDto ConvertSocketMessage(SocketMessage message)
+ {
+ var channel = (SocketGuildChannel) message.Channel;
+ return new MessageDto()
+ {
+ Message = new MessageDto.MessageContent()
+ {
+ Content = message.Content,
+ Id = message.Id.ToString(),
+ Attachments = message.Attachments.Count,
+ ChannelMentions = message.MentionedChannels.Count,
+ UserMentions = message.MentionedUsers.Count,
+ RoleMentions = message.MentionedRoles.Count
+ },
+ User = new MessageDto.IdAndName()
+ {
+ Id = message.Author.Id.ToString(),
+ Name = $"{message.Author.Username}#{message.Author.Discriminator}"
+ },
+ Guild = new MessageDto.IdAndName()
+ {
+ Id = channel.Guild.Id.ToString(),
+ Name = channel.Guild.Name
+ },
+ Channel = new MessageDto.IdAndName()
+ {
+ Id = channel.Id.ToString(),
+ Name = channel.Name
+ },
+ };
+ }
+
+ }
+
+
+ public class MessageDto
+ {
+ public MessageContent Message { get; set; }
+ public IdAndName User { get; set; }
+ public IdAndName Guild { get; set; }
+ public IdAndName Channel { get; set; }
+
+ public class MessageContent
+ {
+ public string Content { get; set; }
+ public string Id { get; set; }
+ public int Attachments { get; set; }
+ public int ChannelMentions { get; set; }
+ public int UserMentions { get; set; }
+ public int RoleMentions { get; set; }
+ }
+
+ public class IdAndName
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/TranslationHandler.cs b/Geekbot.net/Lib/TranslationHandler.cs
new file mode 100644
index 0000000..cdacac8
--- /dev/null
+++ b/Geekbot.net/Lib/TranslationHandler.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Discord.Commands;
+using Discord.WebSocket;
+using Serilog;
+using StackExchange.Redis;
+
+namespace Geekbot.net.Lib
+{
+ public class TranslationHandler : ITranslationHandler
+ {
+ private readonly IGeekbotLogger _logger;
+ private readonly IDatabase _redis;
+ private Dictionary>> _translations;
+ private Dictionary _serverLanguages;
+ private List _supportedLanguages;
+
+ public TranslationHandler(IReadOnlyCollection clientGuilds, IDatabase redis, IGeekbotLogger logger)
+ {
+ _logger = logger;
+ _redis = redis;
+ _logger.Information("Geekbot", "Loading Translations");
+ LoadTranslations();
+ LoadServerLanguages(clientGuilds);
+ }
+
+ private void LoadTranslations()
+ {
+ try
+ {
+ var translationFile = File.ReadAllText(Path.GetFullPath("./Storage/Translations.json"));
+ var rawTranslations = Utf8Json.JsonSerializer.Deserialize>>>(translationFile);
+ var sortedPerLanguage = new Dictionary>>();
+ foreach (var command in rawTranslations)
+ {
+ foreach (var str in command.Value)
+ {
+ foreach (var lang in str.Value)
+ {
+ if (!sortedPerLanguage.ContainsKey(lang.Key))
+ {
+ var commandDict = new Dictionary>();
+ var strDict = new Dictionary();
+ strDict.Add(str.Key, lang.Value);
+ commandDict.Add(command.Key, strDict);
+ sortedPerLanguage.Add(lang.Key, commandDict);
+ }
+ if (!sortedPerLanguage[lang.Key].ContainsKey(command.Key))
+ {
+ var strDict = new Dictionary();
+ strDict.Add(str.Key, lang.Value);
+ sortedPerLanguage[lang.Key].Add(command.Key, strDict);
+ }
+ if (!sortedPerLanguage[lang.Key][command.Key].ContainsKey(str.Key))
+ {
+ sortedPerLanguage[lang.Key][command.Key].Add(str.Key, lang.Value);
+ }
+ }
+ }
+ }
+ _translations = sortedPerLanguage;
+
+ _supportedLanguages = new List();
+ foreach (var lang in sortedPerLanguage)
+ {
+ _supportedLanguages.Add(lang.Key);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Geekbot", "Failed to load Translations", e);
+ Environment.Exit(110);
+ }
+ }
+
+ private void LoadServerLanguages(IReadOnlyCollection clientGuilds)
+ {
+ _serverLanguages = new Dictionary();
+ foreach (var guild in clientGuilds)
+ {
+ var language = _redis.HashGet($"{guild.Id}:Settings", "Language");
+ if (string.IsNullOrEmpty(language) || !_supportedLanguages.Contains(language))
+ {
+ _serverLanguages[guild.Id] = "EN";
+ }
+ else
+ {
+ _serverLanguages[guild.Id] = language.ToString();
+ }
+ }
+ }
+
+ public string GetString(ulong guildId, string command, string stringName)
+ {
+ var translation = _translations[_serverLanguages[guildId]][command][stringName];
+ if (!string.IsNullOrWhiteSpace(translation)) return translation;
+ translation = _translations[command][stringName]["EN"];
+ if (string.IsNullOrWhiteSpace(translation))
+ {
+ _logger.Warning("Geekbot", $"No translation found for {command} - {stringName}");
+ }
+ return translation;
+ }
+
+ public Dictionary GetDict(ICommandContext context)
+ {
+ try
+ {
+ var command = context.Message.Content.Split(' ').First().TrimStart('!').ToLower();
+ return _translations[_serverLanguages[context.Guild.Id]][command];
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Geekbot", "lol nope", e);
+ return new Dictionary();
+ }
+ }
+
+ public Dictionary GetDict(ICommandContext context, string command)
+ {
+ try
+ {
+ return _translations[_serverLanguages[context.Guild.Id]][command];
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Geekbot", "lol nope", e);
+ return new Dictionary();
+ }
+ }
+
+ public bool SetLanguage(ulong guildId, string language)
+ {
+ try
+ {
+ if (!_supportedLanguages.Contains(language)) return false;
+ _redis.HashSet($"{guildId}:Settings", new HashEntry[]{ new HashEntry("Language", language), });
+ _serverLanguages[guildId] = language;
+ return true;
+ }
+ catch (Exception e)
+ {
+ _logger.Error("Geekbot", "Error while changing language", e);
+ return false;
+ }
+ }
+
+ public List GetSupportedLanguages()
+ {
+ return _supportedLanguages;
+ }
+ }
+
+ public interface ITranslationHandler
+ {
+ string GetString(ulong guildId, string command, string stringName);
+ Dictionary GetDict(ICommandContext context);
+ Dictionary GetDict(ICommandContext context, string command);
+ bool SetLanguage(ulong guildId, string language);
+ List GetSupportedLanguages();
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Lib/UserRepository.cs b/Geekbot.net/Lib/UserRepository.cs
new file mode 100644
index 0000000..b180ef9
--- /dev/null
+++ b/Geekbot.net/Lib/UserRepository.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Discord.WebSocket;
+using Serilog;
+using StackExchange.Redis;
+using Utf8Json;
+
+namespace Geekbot.net.Lib
+{
+ public class UserRepository : IUserRepository
+ {
+ private readonly IDatabase _redis;
+ private readonly IGeekbotLogger _logger;
+ public UserRepository(IDatabase redis, IGeekbotLogger logger)
+ {
+ _redis = redis;
+ _logger = logger;
+ }
+
+ public Task Update(SocketUser user)
+ {
+ try
+ {
+ var savedUser = Get(user.Id);
+ savedUser.Id = user.Id;
+ savedUser.Username = user.Username;
+ savedUser.Discriminator = user.Discriminator;
+ savedUser.AvatarUrl = user.GetAvatarUrl() ?? "0";
+ savedUser.IsBot = user.IsBot;
+ savedUser.Joined = user.CreatedAt;
+ if(savedUser.UsedNames == null) savedUser.UsedNames = new List();
+ if (!savedUser.UsedNames.Contains(user.Username))
+ {
+ savedUser.UsedNames.Add(user.Username);
+ }
+ Store(savedUser);
+
+ _logger.Information("UserRepository", "Updated User", savedUser);
+ return Task.FromResult(true);
+ }
+ catch (Exception e)
+ {
+ _logger.Warning("UserRepository", $"Failed to update user: {user.Username}#{user.Discriminator} ({user.Id})", e);
+ return Task.FromResult(false);
+ }
+ }
+
+ private void Store(UserRepositoryUser user)
+ {
+ _redis.HashSetAsync($"Users:{user.Id.ToString()}", new HashEntry[]
+ {
+ new HashEntry("Id", user.Id.ToString()),
+ new HashEntry("Username", user.Username),
+ new HashEntry("Discriminator", user.Discriminator),
+ new HashEntry("AvatarUrl", user.AvatarUrl),
+ new HashEntry("IsBot", user.IsBot),
+ new HashEntry("Joined", user.Joined.ToString()),
+ new HashEntry("UsedNames", JsonSerializer.Serialize(user.UsedNames)),
+ });
+ }
+
+ public UserRepositoryUser Get(ulong userId)
+ {
+ try
+ {
+ var user = _redis.HashGetAll($"Users:{userId.ToString()}");
+ for (int i = 1; i < 11; i++)
+ {
+ if (user.Length != 0) break;
+ user = _redis.HashGetAll($"Users:{(userId + (ulong) i).ToString()}");
+
+ }
+ var dto = new UserRepositoryUser();
+ foreach (var a in user.ToDictionary())
+ {
+ switch (a.Key)
+ {
+ case "Id":
+ dto.Id = ulong.Parse(a.Value);
+ break;
+ case "Username":
+ dto.Username = a.Value.ToString();
+ break;
+ case "Discriminator":
+ dto.Discriminator = a.Value.ToString();
+ break;
+ case "AvatarUrl":
+ dto.AvatarUrl = (a.Value != "0") ? a.Value.ToString() : null;
+ break;
+ case "IsBot":
+ dto.IsBot = a.Value == 1;
+ break;
+ case "Joined":
+ dto.Joined = DateTimeOffset.Parse(a.Value.ToString());
+ break;
+ case "UsedNames":
+ dto.UsedNames = JsonSerializer.Deserialize>(a.Value.ToString()) ?? new List();
+ break;
+ }
+ }
+ return dto;
+ }
+ catch (Exception e)
+ {
+ _logger.Warning("UserRepository", "Failed to get {userId} from repository", e);
+ return new UserRepositoryUser();
+ }
+ }
+
+ public string getUserSetting(ulong userId, string setting)
+ {
+ return _redis.HashGet($"Users:{userId}", setting);
+ }
+
+ public bool saveUserSetting(ulong userId, string setting, string value)
+ {
+ _redis.HashSet($"Users:{userId}", new HashEntry[]
+ {
+ new HashEntry(setting, value)
+ });
+ return true;
+ }
+ }
+
+ public class UserRepositoryUser
+ {
+ public ulong Id { get; set; }
+ public string Username { get; set; }
+ public string Discriminator { get; set; }
+ public string AvatarUrl { get; set; }
+ public bool IsBot { get; set; }
+ public DateTimeOffset Joined { get; set; }
+ public List UsedNames { get; set; }
+ }
+
+ public interface IUserRepository
+ {
+ Task Update(SocketUser user);
+ UserRepositoryUser Get(ulong userId);
+ string getUserSetting(ulong userId, string setting);
+ bool saveUserSetting(ulong userId, string setting, string value);
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Logs/.keep b/Geekbot.net/Logs/.keep
new file mode 100755
index 0000000..e69de29
diff --git a/Geekbot.net/Program.cs b/Geekbot.net/Program.cs
new file mode 100755
index 0000000..c7d2216
--- /dev/null
+++ b/Geekbot.net/Program.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Geekbot.net.Commands;
+using Geekbot.net.Lib;
+using Geekbot.net.Lib.Media;
+using Microsoft.Extensions.DependencyInjection;
+using Nancy.Hosting.Self;
+using Serilog;
+using StackExchange.Redis;
+
+namespace Geekbot.net
+{
+ internal class Program
+ {
+ private DiscordSocketClient client;
+ private CommandService commands;
+ private IDatabase redis;
+ private IServiceCollection services;
+ private IServiceProvider servicesProvider;
+ private RedisValue token;
+ private IGeekbotLogger logger;
+ private IUserRepository userRepository;
+ private string[] args;
+ private bool firstStart = false;
+
+ private static void Main(string[] args)
+ {
+ var logo = new StringBuilder();
+ logo.AppendLine(@" ____ _____ _____ _ ______ ___ _____");
+ logo.AppendLine(@" / ___| ____| ____| |/ / __ ) / _ \\_ _|");
+ logo.AppendLine(@"| | _| _| | _| | ' /| _ \| | | || |");
+ logo.AppendLine(@"| |_| | |___| |___| . \| |_) | |_| || |");
+ logo.AppendLine(@" \____|_____|_____|_|\_\____/ \___/ |_|");
+ logo.AppendLine("=========================================");
+ Console.WriteLine(logo.ToString());
+ var logger = new GeekbotLogger();
+ logger.Information("Geekbot", "Starting...");
+ try
+ {
+ new Program().MainAsync(args, logger).GetAwaiter().GetResult();
+ }
+ catch (Exception e)
+ {
+ logger.Error("Geekbot", "RIP", e);
+ }
+ }
+
+ private async Task MainAsync(string[] args, IGeekbotLogger logger)
+ {
+ this.logger = logger;
+ this.args = args;
+ logger.Information("Geekbot", "Initing Stuff");
+
+ client = new DiscordSocketClient(new DiscordSocketConfig
+ {
+ LogLevel = LogSeverity.Verbose,
+ MessageCacheSize = 1000
+ });
+ client.Log += DiscordLogger;
+ commands = new CommandService();
+
+ try
+ {
+ var redisMultiplexer = ConnectionMultiplexer.Connect("127.0.0.1:6379");
+ redis = redisMultiplexer.GetDatabase(6);
+ logger.Information("Redis", $"Connected to db {redis.Database}");
+ }
+ catch (Exception e)
+ {
+ logger.Error("Redis", "Redis Connection Failed", e);
+ Environment.Exit(102);
+ }
+
+ token = redis.StringGet("discordToken");
+ if (token.IsNullOrEmpty)
+ {
+ Console.Write("Your bot Token: ");
+ var newToken = Console.ReadLine();
+ redis.StringSet("discordToken", newToken);
+ redis.StringSet("Game", "Ping Pong");
+ token = newToken;
+ firstStart = true;
+ }
+
+ services = new ServiceCollection();
+
+ userRepository = new UserRepository(redis, logger);
+ var randomClient = new Random();
+ var fortunes = new FortunesProvider(randomClient, logger);
+ var mediaProvider = new MediaProvider(randomClient, logger);
+ var malClient = new MalClient(redis, logger);
+ var levelCalc = new LevelCalc();
+ var emojiConverter = new EmojiConverter();
+ var audioUtils = new AudioUtils();
+
+ services.AddSingleton(redis);
+ services.AddSingleton(logger);
+ services.AddSingleton(userRepository);
+ services.AddSingleton(levelCalc);
+ services.AddSingleton(emojiConverter);
+ services.AddSingleton(audioUtils);
+ services.AddSingleton(randomClient);
+ services.AddSingleton(fortunes);
+ services.AddSingleton(mediaProvider);
+ services.AddSingleton(malClient);
+
+ logger.Information("Geekbot", "Connecting to Discord");
+
+ await Login();
+
+ await Task.Delay(-1);
+ }
+
+ private async Task Login()
+ {
+ try
+ {
+ await client.LoginAsync(TokenType.Bot, token);
+ await client.StartAsync();
+ var isConneted = await isConnected();
+ if (isConneted)
+ {
+ await client.SetGameAsync(redis.StringGet("Game"));
+ logger.Information("Geekbot", $"Now Connected as {client.CurrentUser.Username} to {client.Guilds.Count} Servers");
+
+ logger.Information("Geekbot", "Registering Stuff");
+ var translationHandler = new TranslationHandler(client.Guilds, redis, logger);
+ var errorHandler = new ErrorHandler(logger, translationHandler);
+ await commands.AddModulesAsync(Assembly.GetEntryAssembly());
+ var contextHandler = new ContextHandler(client, commands, servicesProvider);
+ services.AddSingleton(commands);
+ services.AddSingleton(contextHandler);
+ services.AddSingleton(errorHandler);
+ services.AddSingleton(translationHandler);
+ services.AddSingleton(client);
+ servicesProvider = services.BuildServiceProvider();
+
+ var handlers = new Handlers(client, logger, redis, servicesProvider, commands, userRepository, contextHandler);
+
+ client.MessageReceived += handlers.RunCommand;
+ client.MessageReceived += handlers.UpdateStats;
+ client.MessageDeleted += handlers.MessageDeleted;
+ client.UserJoined += handlers.UserJoined;
+ client.UserUpdated += handlers.UserUpdated;
+ client.UserLeft += handlers.UserLeft;
+
+ if (firstStart || args.Contains("--reset"))
+ {
+ logger.Information("Geekbot", "Finishing setup");
+ await FinishSetup();
+ logger.Information("Geekbot", "Setup finished");
+ }
+ if (!args.Contains("--disable-api"))
+ {
+ startWebApi();
+ }
+
+ logger.Information("Geekbot", "Done and ready for use");
+ }
+ }
+ catch (Exception e)
+ {
+ logger.Error("Discord", "Could not connect...", e);
+ Environment.Exit(103);
+ }
+ }
+
+ private async Task isConnected()
+ {
+ while (!client.ConnectionState.Equals(ConnectionState.Connected))
+ await Task.Delay(25);
+ return true;
+ }
+
+ private void startWebApi()
+ {
+ logger.Information("API", "Starting Webserver");
+ var webApiUrl = new Uri("http://localhost:12995");
+ new NancyHost(webApiUrl).Start();
+ logger.Information("API", $"Webserver now running on {webApiUrl}");
+ }
+
+ private async Task FinishSetup()
+ {
+ var appInfo = await client.GetApplicationInfoAsync();
+ logger.Information("Setup", $"Just a moment while i setup everything {appInfo.Owner.Username}");
+ try
+ {
+ redis.StringSet("botOwner", appInfo.Owner.Id);
+ var req = HttpWebRequest.Create(appInfo.IconUrl);
+ using (var stream = req.GetResponse().GetResponseStream())
+ {
+ await client.CurrentUser.ModifyAsync(User =>
+ {
+ User.Avatar = new Image(stream);
+ User.Username = appInfo.Name.ToString();
+ });
+ }
+ logger.Information("Setup", "Everything done, enjoy!");
+ }
+ catch (Exception e)
+ {
+ logger.Warning("Setup", "Oha, it seems like something went wrong while running the setup, geekbot will work never the less though", e);
+ }
+ return Task.CompletedTask;
+ }
+
+ private Task DiscordLogger(LogMessage message)
+ {
+ var logMessage = $"[{message.Source}] {message.Message}";
+ switch (message.Severity)
+ {
+ case LogSeverity.Verbose:
+ case LogSeverity.Debug:
+ logger.Debug(message.Source, message.Message);
+ break;
+ case LogSeverity.Info:
+ logger.Information(message.Source, message.Message);
+ break;
+ case LogSeverity.Critical:
+ case LogSeverity.Error:
+ case LogSeverity.Warning:
+ if (logMessage.Contains("VOICE_STATE_UPDATE")) break;
+ logger.Error(message.Source, message.Message, message.Exception);
+ break;
+ default:
+ logger.Information(message.Source, $"{logMessage} --- {message.Severity}");
+ break;
+ }
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Storage/Translations.json b/Geekbot.net/Storage/Translations.json
new file mode 100644
index 0000000..2d3414e
--- /dev/null
+++ b/Geekbot.net/Storage/Translations.json
@@ -0,0 +1,100 @@
+{
+ "admin": {
+ "NewLanguageSet": {
+ "EN": "I will reply in english from now on",
+ "CHDE": "I werd ab jetzt uf schwiizerdüütsch antworte, äuuä"
+ },
+ "GetLanguage": {
+ "EN": "I'm talking english",
+ "CHDE": "I red schwiizerdüütsch"
+ }
+ },
+ "errorHandler": {
+ "SomethingWentWrong": {
+ "EN": "Something went wrong :confused:",
+ "CHDE": "Öppis isch schief gange :confused:"
+ }
+ },
+ "httpErrors": {
+ "403": {
+ "EN": "Seems like i don't have enough permission to that :confused:",
+ "CHDE": "Gseht danach us das ich nid gnueg recht han zum das mache :confused:"
+ }
+ },
+ "choose": {
+ "Choice": {
+ "EN": "I Choose **{0}**",
+ "CHDE": "I nimme **{0}**"
+ }
+ },
+ "good": {
+ "CannotChangeOwn": {
+ "EN": "Sorry {0}, but you can't give yourself karma",
+ "CHDE": "Sorry {0}, aber du chasch dr selber kei karma geh"
+ },
+ "WaitUntill": {
+ "EN": "Sorry {0}, but you have to wait {1} before you can give karma again...",
+ "CHDE": "Sorry {0}, aber du musch no {1} warte bisch d wieder karma chasch geh..."
+ },
+ "Increased": {
+ "EN": "Karma gained",
+ "CHDE": "Karma becho"
+ },
+ "By": {
+ "EN": "By",
+ "CHDE": "Vo"
+ },
+ "Amount": {
+ "EN": "Amount",
+ "CHDE": "Mengi"
+ },
+ "Current": {
+ "EN": "Current",
+ "CHDE": "Jetzt"
+ }
+ },
+ "bad": {
+ "CannotChangeOwn": {
+ "EN": "Sorry {0}, but you can't lower your own karma",
+ "CHDE": "Sorry {0}, aber du chasch dr din eigete karma nid weg neh"
+ },
+ "WaitUntill": {
+ "EN": "Sorry {0}, but you have to wait {1} before you can lower karma again...",
+ "CHDE": "Sorry {0}, aber du musch no {1} warte bisch d wieder karma chasch senke..."
+ },
+ "Decreased": {
+ "EN": "Karma lowered",
+ "CHDE": "Karma gsenkt"
+ },
+ "By": {
+ "EN": "By",
+ "CHDE": "Vo"
+ },
+ "Amount": {
+ "EN": "Amount",
+ "CHDE": "Mengi"
+ },
+ "Current": {
+ "EN": "Current",
+ "CHDE": "Jetzt"
+ }
+ },
+ "roll": {
+ "Rolled": {
+ "EN": "{0}, you rolled {1}, your guess was {2}",
+ "CHDE": "{0}, du hesch {1} grollt und hesch {2} grate"
+ },
+ "Gratz": {
+ "EN": "Congratulations {0}, your guess was correct!",
+ "CHDE": "Gratuliere {0}, du hesch richtig grate!"
+ },
+ "RolledNoGuess": {
+ "EN": "{0}, you rolled {1}",
+ "CHDE": "{0}, du hesch {1} grollt"
+ },
+ "NoPrevGuess": {
+ "EN": ":red_circle: {0}, you can't guess the same number again",
+ "CHDE": ":red_circle: {0}, du chasch nid nomol es gliche rate"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/Storage/checkEmPics b/Geekbot.net/Storage/checkEmPics
new file mode 100644
index 0000000..03be23e
--- /dev/null
+++ b/Geekbot.net/Storage/checkEmPics
@@ -0,0 +1,122 @@
+http://s19.postimg.org/pcq2kwzoj/4cb.png
+http://s19.postimg.org/cvetk0f4z/5_Dim_Dy6p.jpg
+http://s19.postimg.org/5hzfl1v37/1310151998600.jpg
+http://s19.postimg.org/53y3lgazn/1324181141954.jpg
+http://s19.postimg.org/724rjg3hf/1392512742365.png
+http://s19.postimg.org/3rgejkdk3/1393501296733.png
+http://s19.postimg.org/a6ffg8k9v/1401667341503.jpg
+http://s19.postimg.org/qiph5yylf/1419231572452.jpg
+http://s19.postimg.org/fqwi4m8ir/1427600681401.png
+http://s19.postimg.org/4c00zzw6b/1447813628974.png
+http://s19.postimg.org/uuio8puw3/b5_3q_ycaaavxtf.jpg
+http://s19.postimg.org/bghu913fn/check_em_by_boyboy99100_d57xp3y.png
+http://s19.postimg.org/s1pgooujn/l_Hkppjs.jpg
+http://s19.postimg.org/m08itft0j/checkem.jpg
+https://old.postimg.org/image/6vx33rb1b/
+https://old.postimg.org/image/wxiaz1mov/
+https://old.postimg.org/image/azqfizx27/
+https://old.postimg.org/image/6iy2kbiu7/
+https://old.postimg.org/image/k8slt45y7/
+https://old.postimg.org/image/t7ruxmplr/
+https://old.postimg.org/image/ssbzqvean/
+https://old.postimg.org/image/kbchfy9lr/
+https://old.postimg.org/image/dl0lk9btr/
+https://old.postimg.org/image/e5k80oufz/
+https://old.postimg.org/image/er005baqn/
+https://old.postimg.org/image/bfk2uzcin/
+https://old.postimg.org/image/556fp0jkv/
+https://old.postimg.org/image/i0efbryu7/
+https://old.postimg.org/image/943n7u87z/
+https://old.postimg.org/image/xn5op5cm7/
+https://old.postimg.org/image/3l5p4d0kf/
+https://old.postimg.org/image/5boq5ui3j/
+https://old.postimg.org/image/ru082bqcf/
+https://old.postimg.org/image/ytea1oqan/
+https://old.postimg.org/image/vu7dekgtb/
+https://old.postimg.org/image/hl7qwi2an/
+https://old.postimg.org/image/5aescfg9r/
+https://old.postimg.org/image/9gzmrrfvj/
+https://old.postimg.org/image/50bv6tr1b/
+https://old.postimg.org/image/afkl7silb/
+https://old.postimg.org/image/nrdsgzllr/
+https://old.postimg.org/image/s32e5zsin/
+https://old.postimg.org/image/5sej60v8f/
+https://old.postimg.org/image/lgfqctau7/
+https://old.postimg.org/image/tn7q4e0wv/
+https://old.postimg.org/image/8612arz1b/
+https://old.postimg.org/image/w5tf52mn3/
+https://old.postimg.org/image/zdxwi48wv/
+https://old.postimg.org/image/lphwghd0f/
+https://old.postimg.org/image/uzu0k0nq7/
+https://old.postimg.org/image/3vqzsxjbz/
+https://old.postimg.org/image/5d7uqqyov/
+https://old.postimg.org/image/dntnyku8v/
+https://old.postimg.org/image/dsxf891jz/
+https://old.postimg.org/image/3nyrioizj/
+https://old.postimg.org/image/6zx2bzaqn/
+https://old.postimg.org/image/wu6v1raqn/
+https://old.postimg.org/image/hb9f4n2fz/
+https://old.postimg.org/image/p7yhqm3a7/
+https://old.postimg.org/image/oelvxzx9b/
+https://old.postimg.org/image/vcq03xvdr/
+https://old.postimg.org/image/b08t1yqlb/
+https://old.postimg.org/image/6yrpwayan/
+https://old.postimg.org/image/btleukwm7/
+https://old.postimg.org/image/62ztuldzz/
+https://old.postimg.org/image/w3iq9pxr3/
+https://old.postimg.org/image/byp6493xb/
+https://old.postimg.org/image/xp2lf9xcv/
+https://old.postimg.org/image/j9p9u49pb/
+https://old.postimg.org/image/hvxmytafz/
+https://old.postimg.org/image/5eqzbnfa7/
+https://old.postimg.org/image/do2uq290f/
+https://old.postimg.org/image/54o261q1r/
+https://old.postimg.org/image/94qm4jr4v/
+https://old.postimg.org/image/lee88y0pr/
+https://old.postimg.org/image/bncb58cv3/
+https://old.postimg.org/image/5246j7me7/
+https://old.postimg.org/image/4uby8ym1r/
+https://old.postimg.org/image/qn996tj4v/
+https://old.postimg.org/image/c1dn4twyn/
+https://old.postimg.org/image/6rd9ra23j/
+https://lehcark14.files.wordpress.com/2008/08/botan16.jpg
+http://i.imgur.com/p9vALew.jpg
+http://i.imgur.com/4a9l2Rm.png
+http://i.imgur.com/RNtixMQ.jpg
+https://pbs.twimg.com/media/Cro9aIGUEAAkXCP.jpg
+http://s16.postimg.org/empvloimd/Check_em_Guts.png
+https://s18.postimg.io/qgbhe7u09/1424491645996.gif
+http://s19.postimg.org/hhemlt7xf/3eb.jpg
+http://s19.postimg.org/cwsg6vo83/8aa.png
+http://s19.postimg.org/rh9j1pj6r/28mohl4.png
+http://s19.postimg.org/zba4n3qzn/86d.jpg
+http://s19.postimg.org/cb3hart5v/2016_09_16_08_58_45.png
+http://s19.postimg.org/m9ofx92lf/bb1.jpg
+http://s19.postimg.org/maydqo4f7/e8b.jpg
+http://s19.postimg.org/yqzoy5n4z/fbe.png
+http://s19.postimg.org/xd822unvn/giphy.gif
+http://s19.postimg.org/c4udlf9er/l_TU3eup.jpg
+https://66.media.tumblr.com/cc893a0ee40d73d083da3df4bdaf45cc/tumblr_mx8psiFduG1t1g1k8o1_500.gif
+http://i.imgur.com/swbXHSy.gif
+http://img1.reactor.cc/pics/post/full/Anime-Touhou-Project-Yakumo-Yukari-%D0%A0%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D1%8F-1303807.jpeg
+http://i.imgur.com/ftGLHE0.png
+http://i.imgur.com/JELDhKQ.png
+http://imgur.com/yBJound
+http://i.imgur.com/f7gAVPJ.png
+http://i.imgur.com/HxWyo2Z.jpg
+http://i.imgur.com/8Eb9CxQ.png
+http://i.imgur.com/kOECcjz.png
+http://i.imgur.com/MJLu7oJ.jpg
+http://i.imgur.com/itG3rPM.jpg
+http://i.imgur.com/G83Go9t.jpg
+http://i.imgur.com/jI2dBnU.jpg
+http://i.imgur.com/FtALzg0.jpg
+http://i.imgur.com/GwZpJEv.gif
+http://i.imgur.com/TYGRD3B.gif
+http://i.imgur.com/P6TxLS3.png
+http://i.imgur.com/phTVTdn.jpg
+http://i.imgur.com/thhR6UE.jpg
+http://i.imgur.com/KbROufx.jpg
+http://i.imgur.com/sQqWbcm.jpg
+http://i.imgur.com/YYpis53.png
+http://i.imgur.com/kwaRd54.gif
\ No newline at end of file
diff --git a/Geekbot.net/Storage/croissant b/Geekbot.net/Storage/croissant
new file mode 100644
index 0000000..281b790
--- /dev/null
+++ b/Geekbot.net/Storage/croissant
@@ -0,0 +1,17 @@
+https://i2.wp.com/epicureandculture.com/wp-content/uploads/2014/12/shutterstock_172040546.jpg
+http://www.bakespace.com/images/large/5d79070cf21b2f33c3a1dd4336cb27d2.jpeg
+http://food.fnr.sndimg.com/content/dam/images/food/fullset/2015/5/7/1/SD1B43_croissants-recipe_s4x3.jpg.rend.hgtvcom.616.462.suffix/1431052139248.jpeg
+http://img.taste.com.au/u-Bwjfm_/taste/2016/11/mini-croissants-with-3-fillings-14692-1.jpeg
+https://media.newyorker.com/photos/590974702179605b11ad8096/16:9/w_1200,h_630,c_limit/Gopnik-TheMurkyMeaningsofStraightenedOutCroissants.jpg
+http://bt.static-redmouse.ch/sites/bielertagblatt.ch/files/styles/bt_article_showroom_landscape/hash/84/c9/84c9aed08415265911ec05c46d25d3ef.jpg?itok=hP5PnHaT
+https://www.dermann.at/wp-content/uploads/Schokocroissant_HPBild_1400x900px.jpeg
+https://www.bettybossi.ch/static/rezepte/x/bb_bkxx060101_0360a_x.jpg
+http://www.engel-beck.ch/uploads/pics/tete-de-moine-gipfel-.jpg
+https://storage.cpstatic.ch/storage/og_image/laugengipfel--425319.jpg
+https://www.backhaus-kutzer.de/fileadmin/templates/Resources/Public/img/produkte/suesses-gebaeck/Milchhoernchen.png
+https://www.kuechengoetter.de/uploads/media/1000x524/00/36390-vanillekipferl-0.jpg?v=1-0
+https://c1.staticflickr.com/3/2835/10874180753_2b2916e3ce_b.jpg
+http://www.mistercool.ch/wp-content/uploads/2017/02/Gipfel-mit-Cerealien-7168.png
+https://scontent-sea1-1.cdninstagram.com/t51.2885-15/s480x480/e35/c40.0.999.999/15099604_105396696611384_2866237281000226816_n.jpg?ig_cache_key=MTM4MzQxOTU1MDc5NjUxNzcwMA%3D%3D.2.c
+http://www.lecrobag.de/wp-content/uploads/2014/03/Wurst_2014_l.jpg
+https://www.thecookierookie.com/wp-content/uploads/2017/02/sheet-pan-chocolate-croissants-collage1.jpeg
\ No newline at end of file
diff --git a/src/Bot/Storage/fortunes b/Geekbot.net/Storage/fortunes
similarity index 100%
rename from src/Bot/Storage/fortunes
rename to Geekbot.net/Storage/fortunes
diff --git a/src/Bot/Storage/pandas b/Geekbot.net/Storage/pandas
similarity index 78%
rename from src/Bot/Storage/pandas
rename to Geekbot.net/Storage/pandas
index 6c6d725..9d5046c 100644
--- a/src/Bot/Storage/pandas
+++ b/Geekbot.net/Storage/pandas
@@ -4,9 +4,8 @@ https://nationalzoo.si.edu/sites/default/files/styles/slide_1400x700/public/supp
https://media4.s-nbcnews.com/j/newscms/2016_36/1685951/ss-160826-twip-05_8cf6d4cb83758449fd400c7c3d71aa1f.nbcnews-ux-2880-1000.jpg
https://ichef-1.bbci.co.uk/news/660/cpsprodpb/169F6/production/_91026629_gettyimages-519508400.jpg
https://cdn.history.com/sites/2/2017/03/GettyImages-157278376.jpg
+https://www.pandasinternational.org/wptemp/wp-content/uploads/2012/10/slider1.jpg
https://tctechcrunch2011.files.wordpress.com/2015/11/panda.jpg
http://www.nationalgeographic.com/content/dam/magazine/rights-exempt/2016/08/departments/panda-mania-12.jpg
http://animals.sandiegozoo.org/sites/default/files/2016-09/panda1_10.jpg
-http://kids.nationalgeographic.com/content/dam/kids/photos/animals/Mammals/A-G/giant-panda-eating.adapt.945.1.jpg
-https://static.independent.co.uk/s3fs-public/thumbnails/image/2015/10/08/15/Hong-Kong-pandas.jpg
-https://3sn4dm1qd6i72l8a4r2ig7fl-wpengine.netdna-ssl.com/wp-content/uploads/2016/11/panda_lunlun_ZA_2083-b.jpg
\ No newline at end of file
+http://kids.nationalgeographic.com/content/dam/kids/photos/animals/Mammals/A-G/giant-panda-eating.adapt.945.1.jpg
\ No newline at end of file
diff --git a/Geekbot.net/Storage/pumpkin b/Geekbot.net/Storage/pumpkin
new file mode 100644
index 0000000..4b8e6f2
--- /dev/null
+++ b/Geekbot.net/Storage/pumpkin
@@ -0,0 +1,23 @@
+https://i.pinimg.com/736x/0a/a7/8a/0aa78af25e114836e1a42585fb7b09ed--funny-pumpkins-pumkin-carving.jpg
+http://wdy.h-cdn.co/assets/16/31/980x1470/gallery-1470321728-shot-two-021.jpg
+https://i.pinimg.com/736x/6c/62/bf/6c62bfa73a19ffd9fc6f2d720d5e9764--cool-pumpkin-carving-carving-pumpkins.jpg
+http://images6.fanpop.com/image/photos/38900000/Jack-o-Lantern-halloween-38991566-500-415.jpg
+http://ghk.h-cdn.co/assets/15/37/1441834730-pumpkin-carve-2.jpg
+http://diy.sndimg.com/content/dam/images/diy/fullset/2011/7/26/1/iStock-10761186_halloween-pumpkin-in-garden_s4x3.jpg.rend.hgtvcom.966.725.suffix/1420851319631.jpeg
+http://ghk.h-cdn.co/assets/cm/15/11/54ffe537af882-snail-pumpkin-de.jpg
+https://www.digsdigs.com/photos/2009/10/100-halloween-pumpkin-carving-ideas-12.jpg
+http://diy.sndimg.com/content/dam/images/diy/fullset/2010/6/4/0/CI-Kyle-Nishioka_big-teeth-Jack-O-Lantern_s4x3.jpg.rend.hgtvcom.966.725.suffix/1420699522718.jpeg
+https://twistedsifter.files.wordpress.com/2011/10/most-amazing-pumpkin-carving-ray-villafane-10.jpg?w=521&h=739
+https://i.pinimg.com/736x/09/c4/b1/09c4b187b266c1f65332294f66009944--funny-pumpkins-halloween-pumpkins.jpg
+http://www.evilmilk.com/pictures/The_Pumpkin_Man.jpg
+http://cache.lovethispic.com/uploaded_images/blogs/13-Funny-Pumpkin-Carvings-5773-9.JPG
+http://ihappyhalloweenpictures.com/wp-content/uploads/2016/10/funny-halloween-pumpkin.jpg
+http://www.smallhomelove.com/wp-content/uploads/2012/08/leg-eating-pumpkin.jpg
+https://cdn.shopify.com/s/files/1/0773/6789/articles/Halloween_Feature_8ff7a7c4-2cb3-4584-a85f-5d4d1e6ca26e.jpg?v=1476211360
+http://4vector.com/i/free-vector-pumpkin-boy-color-version-clip-art_107714_Pumpkin_Boy_Color_Version_clip_art_hight.png
+https://i.pinimg.com/736x/59/8a/0f/598a0fbf789631b76c1ffd4443194d8e--halloween-pumpkins-fall-halloween.jpg
+https://i.pinimg.com/originals/8f/86/f9/8f86f95457467872b371ba697d341961.jpg
+http://nerdist.com/wp-content/uploads/2015/08/taleshalloween1.jpg
+http://www.designbolts.com/wp-content/uploads/2014/09/Scary-Pumpkin_Grin_stencil-Ideas.jpg
+http://vignette2.wikia.nocookie.net/scoobydoo/images/7/75/Pumpkin_monsters_%28Witch%27s_Ghost%29.png/revision/latest?cb=20140520070213
+https://taholtorf.files.wordpress.com/2013/10/36307-1920x1280.jpg
diff --git a/Geekbot.net/Storage/squirrel b/Geekbot.net/Storage/squirrel
new file mode 100644
index 0000000..2216465
--- /dev/null
+++ b/Geekbot.net/Storage/squirrel
@@ -0,0 +1,45 @@
+http://orig14.deviantart.net/6016/f/2010/035/c/b/first_squirrel_assassin_by_shotokanteddy.jpg
+https://thumbs-prod.si-cdn.com/eoEYA_2Hau4795uKoecUZZgz-3w=/800x600/filters:no_upscale()/https://public-media.smithsonianmag.com/filer/52/f9/52f93262-c29b-4a4f-b031-0c7ad145ed5f/42-33051942.jpg
+http://images5.fanpop.com/image/photos/30700000/Squirrel-squirrels-30710732-400-300.jpg
+https://www.lovethegarden.com/sites/default/files/files/Red%20%26%20Grey%20Squirrel%20picture%20side%20by%20side-LR.jpg
+http://i.dailymail.co.uk/i/pix/2016/02/24/16/158F7E7C000005DC-3462228-image-a-65_1456331226865.jpg
+http://2.bp.blogspot.com/-egfnMhUb8tg/T_dAIu1m6cI/AAAAAAAAPPU/v4x9q4WqWl8/s640/cute-squirrel-hey-watcha-thinkin-about.jpg
+https://upload.wikimedia.org/wikipedia/commons/thumb/1/1c/Squirrel_posing.jpg/287px-Squirrel_posing.jpg
+https://i.pinimg.com/736x/51/db/9b/51db9bad4a87d445d321923c7d56b501--red-squirrel-animal-kingdom.jpg
+https://metrouk2.files.wordpress.com/2016/10/ad_223291521.jpg?w=620&h=949&crop=1
+http://www.redsquirrelsunited.org.uk/wp-content/uploads/2016/07/layer-slider.jpg
+http://images.mentalfloss.com/sites/default/files/squirrel-hero.jpg?resize=1100x740
+https://i.pinimg.com/736x/ce/9c/59/ce9c5990b193046400d98724595cdaf3--red-squirrel-chipmunks.jpg
+https://www.brooklynpaper.com/assets/photos/40/30/dtg-squirrel-attacks-prospect-park-patrons-2017-07-28-bk01_z.jpg
+http://www.freakingnews.com/pictures/16000/Squirrel-Shark-16467.jpg
+http://img09.deviantart.net/5c1c/i/2013/138/0/6/barbarian_squirel_by_coucoucmoa-d64r9m4.jpg
+https://i.pinimg.com/736x/b4/5c/0d/b45c0d00b1a57e9f84f27f13cb019001--baby-squirrel-red-squirrel.jpg
+https://i.pinimg.com/736x/0f/75/87/0f7587bb613ab524763afe8c9a532e5c--cute-squirrel-squirrels.jpg
+http://cdn.images.express.co.uk/img/dynamic/128/590x/Grey-squirrel-828838.jpg
+http://www.lovethispic.com/uploaded_images/79964-Squirrel-Smelling-A-Flower.jpg
+https://i.pinimg.com/736x/23/d5/f9/23d5f9868f7d76c79c49bef53ae08f7f--squirrel-funny-red-squirrel.jpg
+http://stories.barkpost.com/wp-content/uploads/2016/01/squirrel-3-copy.jpg
+https://i.ytimg.com/vi/pzUs0DdzK3Y/hqdefault.jpg
+https://www.askideas.com/media/41/I-Swear-It-Wasnt-Me-Funny-Squirrel-Meme-Picture-For-Facebook.jpg
+https://i.pinimg.com/736x/2d/54/d8/2d54d8d2a9b3ab9d3e78544b75afd88e--funny-animal-pictures-humorous-pictures.jpg
+http://www.funny-animalpictures.com/media/content/items/images/funnysquirrels0012_O.jpg
+http://funny-pics.co/wp-content/uploads/funny-squirrel-and-coffee-picture.jpg
+https://pbs.twimg.com/media/Bi4Ij6CIgAAgEdZ.jpg
+http://www.funnyjunksite.com/pictures/wp-content/uploads/2015/06/Funny-Superman-Squirrels.jpg
+https://i.pinimg.com/736x/bf/35/00/bf3500104f8394909d116259d1f0575e--funny-squirrel-squirrel-girl.jpg
+http://quotespill.com/wp-content/uploads/2017/07/Squirrel-Meme-Draw-me-like-one-of-your-french-squirrrels-min.jpg
+https://i.pinimg.com/736x/e2/16/bb/e216bba53f80fc8e0111d371e9850159--funny-squirrels-cute-squirrel.jpg
+https://i.pinimg.com/736x/52/43/c9/5243c93377245be1f686218c266d775c--funny-squirrel-baby-squirrel.jpg
+https://i.pinimg.com/736x/0c/be/1d/0cbe1da8ad2c0cf3882a806b6fd88965--cute-pictures-funny-animal-pictures.jpg
+https://i.pinimg.com/736x/e5/08/67/e508670aa00ca3c896eccb81c4f6e2a8--funny-squirrel-baby-squirrel.jpg
+https://i.pinimg.com/736x/1c/7d/4f/1c7d4f067a10066aad802ce5ac468d71--group-boards-a-squirrel.jpg
+http://funny-pics.co/wp-content/uploads/funny-squirrel-on-a-branch.jpg
+http://loldamn.com/wp-content/uploads/2016/06/funny-squirrel-playing-water-bending.jpg
+https://cdn.trendhunterstatic.com/thumbs/squirrel-photography.jpeg
+https://i.pinimg.com/736x/d6/42/12/d64212cc6221916db4173962bf6c131a--cute-squirrel-baby-squirrel.jpg
+https://i.pinimg.com/236x/10/13/58/101358f2afc2c7d6b6a668046e7b8382--funny-animal-pictures-funny-animals.jpg
+https://i.pinimg.com/736x/da/0d/fe/da0dfe93bb26887795f906e8fa97d68e--secret-squirrel-cute-squirrel.jpg
+http://2.bp.blogspot.com/-HLieBqEuQoM/UDkRmeyzB5I/AAAAAAAABHs/RtsEynn5t6Y/s1600/hd-squirrel-wallpaper-with-a-brown-squirrel-eating-watermelon-wallpapers-backgrounds-pictures-photos.jpg
+http://www.city-data.com/forum/members/brenda-starz-328928-albums-brenda-s-funny-squirrel-comment-pic-s-pic5075-punk-squirrels.jpg
+http://img15.deviantart.net/9c50/i/2011/213/c/9/just_taking_it_easy_by_lou_in_canada-d42do3d.jpg
+http://3.bp.blogspot.com/-AwsSk76R2Is/USQa3-dszKI/AAAAAAAABUQ/KF_F8HbtP1U/w1200-h630-p-k-no-nu/crazySquirrel.jpg
diff --git a/src/Bot/Storage/turtles b/Geekbot.net/Storage/turtles
similarity index 69%
rename from src/Bot/Storage/turtles
rename to Geekbot.net/Storage/turtles
index aa0fbcf..9dbbf72 100644
--- a/src/Bot/Storage/turtles
+++ b/Geekbot.net/Storage/turtles
@@ -1,20 +1,21 @@
https://i.guim.co.uk/img/media/6b9be13031738e642f93f9271f3592044726a9b1/0_0_2863_1610/2863.jpg?w=640&h=360&q=55&auto=format&usm=12&fit=max&s=85f3b33cc158b5aa120c143dae1916ed
http://cf.ltkcdn.net/small-pets/images/std/212089-676x450-Turtle-feeding-on-leaf.jpg
+https://static1.squarespace.com/static/5369465be4b0507a1fd05af0/53767a6be4b0ad0822345e52/57e40ba4893fc031e05a018f/1498243318058/solvin.jpg?format=1500w
https://c402277.ssl.cf1.rackcdn.com/photos/419/images/story_full_width/HI_287338Hero.jpg?1433950119
https://www.cdc.gov/salmonella/agbeni-08-17/images/turtle.jpg
https://cdn.arstechnica.net/wp-content/uploads/2017/08/GettyImages-524757168.jpg
http://pmdvod.nationalgeographic.com/NG_Video/595/319/4504517_098_05_TOS_thumbnail_640x360_636296259676.jpg
+http://cdn1.arkive.org/media/7D/7D46329A-6ED2-4F08-909E-7B596417994A/Presentation.Large/Big-headed-turtle-close-up.jpg
http://s7d2.scene7.com/is/image/PetSmart/ARTHMB-CleaningYourTortoiseOrTurtlesHabitat-20160818?$AR1104$
https://fthmb.tqn.com/9VGWzK_GWlvrjxtdFPX6EJxOq24=/960x0/filters:no_upscale()/133605352-56a2bce53df78cf7727960db.jpg
+https://i.imgur.com/46QmzgF.jpg
https://www.wildgratitude.com/wp-content/uploads/2015/07/turtle-spirit-animal1.jpg
http://www.backwaterreptiles.com/images/turtles/red-eared-slider-turtle-for-sale.jpg
+https://i.pinimg.com/736x/f1/f4/13/f1f413d6d07912be6080c08b186630ac--happy-turtle-funny-stuff.jpg
+http://www.dupageforest.org/uploadedImages/Content/District_News/Nature_Stories/2016/Snapping%20Turtle%20Scott%20Plantier%20STP4793.jpg
http://turtlebackzoo.com/wp-content/uploads/2016/07/exhibit-headers_0008_SOUTH-AMERICA-600x400.jpg
+https://i.ytimg.com/vi/_YfYHFM3Das/maxresdefault.jpg
https://i.pinimg.com/736x/dd/4e/7f/dd4e7f2f921ac28b1d5a59174d477131--cute-baby-sea-turtles-adorable-turtles.jpg
http://kids.nationalgeographic.com/content/dam/kids/photos/animals/Reptiles/A-G/green-sea-turtle-closeup-underwater.adapt.945.1.jpg
+https://i.ytimg.com/vi/p4Jj9QZFJvw/hqdefault.jpg
https://fthmb.tqn.com/nirxHkH3jBAe74ife6fJJu6k6q8=/2121x1414/filters:fill(auto,1)/Red-eared-sliders-GettyImages-617946009-58fae8835f9b581d59a5bab6.jpg
-http://assets.worldwildlife.org/photos/167/images/original/MID_225023-circle-hawksbill-turtle.jpg?1345565600
-https://seaturtles.org/wp-content/uploads/2013/11/GRN-honuAnitaWintner2.jpg
-https://images2.minutemediacdn.com/image/upload/c_crop,h_2549,w_4536,x_0,y_237/v1560186367/shape/mentalfloss/istock-687398754.jpg?itok=QsiF5yHP
-https://c402277.ssl.cf1.rackcdn.com/photos/13028/images/story_full_width/seaturtle_spring2017.jpg?1485359391
-https://i2.wp.com/rangerrick.org/wp-content/uploads/2018/03/Turtle-Tale-RR-Jr-June-July-2017.jpg?fit=1156%2C650&ssl=1
-https://boyslifeorg.files.wordpress.com/2019/07/greenseaturtle.jpg
\ No newline at end of file
diff --git a/Geekbot.net/WebApi/HelpController.cs b/Geekbot.net/WebApi/HelpController.cs
new file mode 100644
index 0000000..6146352
--- /dev/null
+++ b/Geekbot.net/WebApi/HelpController.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using Nancy;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.WebApi
+{
+ public class HelpController : NancyModule
+ {
+ public HelpController()
+ {
+ Get("/v1/commands", args =>
+ {
+ var commands = getCommands().Result;
+
+ var commandList = new List();
+ foreach (var cmd in commands.Commands)
+ {
+ var cmdParamsObj = new List();
+ foreach (var cmdParam in cmd.Parameters)
+ {
+ var singleParamObj = new CommandParamDto()
+ {
+ Summary = cmdParam.Summary,
+ Default = cmdParam?.DefaultValue?.ToString() ?? null,
+ Type = cmdParam?.Type?.ToString()
+ };
+ cmdParamsObj.Add(singleParamObj);
+ }
+
+ var param = string.Join(", !", cmd.Aliases);
+ var cmdObj = new CommandDto()
+ {
+ Name = cmd.Name,
+ Summary = cmd.Summary,
+ Category = cmd.Remarks ?? CommandCategories.Uncategorized,
+ IsAdminCommand = (param.Contains("admin")),
+ Aliases = cmd.Aliases.ToArray(),
+ Params = cmdParamsObj
+ };
+ commandList.Add(cmdObj);
+ }
+ return Response.AsJson(commandList);
+
+ });
+ }
+
+ private async Task getCommands()
+ {
+ var commands = new CommandService();
+ await commands.AddModulesAsync(Assembly.GetEntryAssembly());
+ return commands;
+ }
+ }
+
+ public class CommandDto
+ {
+ public string Name { get; set; }
+ public string Category { get; set; }
+ public string Summary { get; set; }
+ public bool IsAdminCommand { get; set; }
+ public Array Aliases { get; set; }
+ public List Params { get; set; }
+ }
+
+ public class CommandParamDto
+ {
+ public string Summary { get; set; }
+ public string Default { get; set; }
+ public string Type { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/WebApi/StatusController.cs b/Geekbot.net/WebApi/StatusController.cs
new file mode 100644
index 0000000..2fd84f8
--- /dev/null
+++ b/Geekbot.net/WebApi/StatusController.cs
@@ -0,0 +1,29 @@
+using Nancy;
+using Geekbot.net.Lib;
+
+namespace Geekbot.net.WebApi
+{
+ public class StatusController : NancyModule
+ {
+ public StatusController()
+ {
+ Get("/", args =>
+ {
+ var responseBody = new ApiStatusDto()
+ {
+ GeekbotVersion = Constants.BotVersion.ToString(),
+ ApiVersion = Constants.ApiVersion.ToString(),
+ Status = "Online"
+ };
+ return Response.AsJson(responseBody);
+ });
+ }
+ }
+
+ public class ApiStatusDto
+ {
+ public string GeekbotVersion { get; set; }
+ public string ApiVersion { get; set; }
+ public string Status { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Geekbot.net/WebApi/WebConfig.cs b/Geekbot.net/WebApi/WebConfig.cs
new file mode 100644
index 0000000..f0b9ca3
--- /dev/null
+++ b/Geekbot.net/WebApi/WebConfig.cs
@@ -0,0 +1,23 @@
+using System.Diagnostics;
+using Nancy;
+using Nancy.Bootstrapper;
+using Nancy.TinyIoc;
+
+namespace Geekbot.net.WebApi
+{
+ public class WebConfig : DefaultNancyBootstrapper
+ {
+ protected override void RequestStartup(TinyIoCContainer container, IPipelines pipelines, NancyContext context)
+ {
+
+ //CORS Enable
+ pipelines.AfterRequest.AddItemToEndOfPipeline(ctx =>
+ {
+ ctx.Response.WithHeader("Access-Control-Allow-Origin", "*")
+ .WithHeader("Access-Control-Allow-Methods", "GET")
+ .WithHeader("Access-Control-Allow-Headers", "Accept, Origin, Content-type")
+ .WithHeader("Last-Modified", Process.GetCurrentProcess().StartTime.ToString());
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Startup/derp.ico b/Geekbot.net/derp.ico
similarity index 100%
rename from src/Startup/derp.ico
rename to Geekbot.net/derp.ico
diff --git a/Tests/Lib/EmojiConverter.test.cs b/Tests/Lib/EmojiConverter.test.cs
new file mode 100644
index 0000000..ca4ecbc
--- /dev/null
+++ b/Tests/Lib/EmojiConverter.test.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Geekbot.net.Lib;
+using Xunit;
+
+namespace Tests.Lib
+{
+ public class EmojiConverter_test
+ {
+ public static IEnumerable