Merge remote-tracking branch 'refs/remotes/Kwoth/master'

This commit is contained in:
appelemac 2016-04-02 22:14:16 +02:00
commit d82cd19b7b
16 changed files with 634 additions and 235 deletions

View File

@ -0,0 +1,120 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Classes.Music
{
/// <summary>
/// 💩
/// </summary>
public class PoopyBuffer
{
private readonly byte[] ringBuffer;
public int WritePosition { get; private set; } = 0;
public int ReadPosition { get; private set; } = 0;
public int ContentLength => (WritePosition >= ReadPosition ?
WritePosition - ReadPosition :
(BufferSize - ReadPosition) + WritePosition);
public int BufferSize { get; }
private readonly object readWriteLock = new object();
public PoopyBuffer(int size)
{
if (size <= 0)
throw new ArgumentException();
BufferSize = size;
ringBuffer = new byte[size];
}
public int Read(byte[] buffer, int count)
{
if (buffer.Length < count)
throw new ArgumentException();
//Console.WriteLine($"***\nRead: {ReadPosition}\nWrite: {WritePosition}\nContentLength:{ContentLength}\n***");
lock (readWriteLock)
{
//read as much as you can if you're reading too much
if (count > ContentLength)
count = ContentLength;
//if nothing to read, return 0
if (WritePosition == ReadPosition)
return 0;
// if buffer is in the "normal" state, just read
if (WritePosition > ReadPosition)
{
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, count);
ReadPosition += count;
//Console.WriteLine($"Read only normally1 {count}[{ReadPosition - count} to {ReadPosition}]");
return count;
}
//else ReadPos <Writepos
// buffer is in its inverted state
// A: if i can read as much as possible without hitting the buffer.length, read that
if (count + ReadPosition <= BufferSize)
{
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, count);
ReadPosition += count;
//Console.WriteLine($"Read only normally2 {count}[{ReadPosition - count} to {ReadPosition}]");
return count;
}
// B: if i can't read as much, read to the end,
var readNormaly = BufferSize - ReadPosition;
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, readNormaly);
//Console.WriteLine($"Read normaly {count}[{ReadPosition} to {ReadPosition + readNormaly}]");
//then read the remaining amount from the start
var readFromStart = count - readNormaly;
Buffer.BlockCopy(ringBuffer, 0, buffer, readNormaly, readFromStart);
//Console.WriteLine($"Read From start {readFromStart}[{0} to {readFromStart}]");
ReadPosition = readFromStart;
return count;
}
}
public async Task WriteAsync(byte[] buffer, int count, CancellationToken cancelToken)
{
if (count > buffer.Length)
throw new ArgumentException();
while (ContentLength + count > BufferSize)
{
await Task.Delay(20, cancelToken);
if (cancelToken.IsCancellationRequested)
return;
}
//the while above assures that i cannot write past readposition with my write, so i don't have to check
// *unless its multithreaded or task is not awaited
lock (readWriteLock)
{
// if i can just write without hitting buffer.length, do it
if (WritePosition + count < BufferSize)
{
Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, count);
WritePosition += count;
//Console.WriteLine($"Wrote only normally {count}[{WritePosition - count} to {WritePosition}]");
return;
}
// otherwise, i have to write to the end, then write the rest from the start
var wroteNormaly = BufferSize - WritePosition;
Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, wroteNormaly);
//Console.WriteLine($"Wrote normally {wroteNormaly}[{WritePosition} to {BufferSize}]");
var wroteFromStart = count - wroteNormaly;
Buffer.BlockCopy(buffer, wroteNormaly, ringBuffer, 0, wroteFromStart);
//Console.WriteLine($"and from start {wroteFromStart} [0 to {wroteFromStart}");
WritePosition = wroteFromStart;
}
}
}
}

View File

@ -10,116 +10,17 @@ using System.Threading;
using System.Threading.Tasks;
using VideoLibrary;
namespace NadekoBot.Classes.Music {
public class SongInfo {
namespace NadekoBot.Classes.Music
{
public class SongInfo
{
public string Provider { get; internal set; }
public MusicType ProviderType { get; internal set; }
public string Title { get; internal set; }
public string Uri { get; internal set; }
}
/// <summary>
/// 💩
/// </summary>
public class PoopyBuffer {
private readonly byte[] ringBuffer;
public int WritePosition { get; private set; } = 0;
public int ReadPosition { get; private set; } = 0;
public int ContentLength => (WritePosition >= ReadPosition ?
WritePosition - ReadPosition :
(BufferSize - ReadPosition) + WritePosition);
public int BufferSize { get; }
private readonly object readWriteLock = new object();
public PoopyBuffer(int size) {
if (size <= 0)
throw new ArgumentException();
BufferSize = size;
ringBuffer = new byte[size];
}
public int Read(byte[] buffer, int count) {
if (buffer.Length < count)
throw new ArgumentException();
//Console.WriteLine($"***\nRead: {ReadPosition}\nWrite: {WritePosition}\nContentLength:{ContentLength}\n***");
lock (readWriteLock) {
//read as much as you can if you're reading too much
if (count > ContentLength)
count = ContentLength;
//if nothing to read, return 0
if (WritePosition == ReadPosition)
return 0;
// if buffer is in the "normal" state, just read
if (WritePosition > ReadPosition) {
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, count);
ReadPosition += count;
//Console.WriteLine($"Read only normally1 {count}[{ReadPosition - count} to {ReadPosition}]");
return count;
}
//else ReadPos <Writepos
// buffer is in its inverted state
// A: if i can read as much as possible without hitting the buffer.length, read that
if (count + ReadPosition <= BufferSize) {
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, count);
ReadPosition += count;
//Console.WriteLine($"Read only normally2 {count}[{ReadPosition - count} to {ReadPosition}]");
return count;
}
// B: if i can't read as much, read to the end,
var readNormaly = BufferSize - ReadPosition;
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, readNormaly);
//Console.WriteLine($"Read normaly {count}[{ReadPosition} to {ReadPosition + readNormaly}]");
//then read the remaining amount from the start
var readFromStart = count - readNormaly;
Buffer.BlockCopy(ringBuffer, 0, buffer, readNormaly, readFromStart);
//Console.WriteLine($"Read From start {readFromStart}[{0} to {readFromStart}]");
ReadPosition = readFromStart;
return count;
}
}
public async Task WriteAsync(byte[] buffer, int count, CancellationToken cancelToken) {
if (count > buffer.Length)
throw new ArgumentException();
while (ContentLength + count > BufferSize) {
await Task.Delay(20, cancelToken);
if (cancelToken.IsCancellationRequested)
return;
}
//the while above assures that i cannot write past readposition with my write, so i don't have to check
// *unless its multithreaded or task is not awaited
lock (readWriteLock) {
// if i can just write without hitting buffer.length, do it
if (WritePosition + count < BufferSize) {
Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, count);
WritePosition += count;
//Console.WriteLine($"Wrote only normally {count}[{WritePosition - count} to {WritePosition}]");
return;
}
// otherwise, i have to write to the end, then write the rest from the start
var wroteNormaly = BufferSize - WritePosition;
Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, wroteNormaly);
//Console.WriteLine($"Wrote normally {wroteNormaly}[{WritePosition} to {BufferSize}]");
var wroteFromStart = count - wroteNormaly;
Buffer.BlockCopy(buffer, wroteNormaly, ringBuffer, 0, wroteFromStart);
//Console.WriteLine($"and from start {wroteFromStart} [0 to {wroteFromStart}");
WritePosition = wroteFromStart;
}
}
}
public class Song {
public class Song
{
public StreamState State { get; internal set; }
public string PrettyName =>
$"**【 {SongInfo.Title.TrimTo(55)} 】**`{(SongInfo.Provider ?? "-")}`";
@ -130,22 +31,27 @@ namespace NadekoBot.Classes.Music {
private bool prebufferingComplete { get; set; } = false;
public MusicPlayer MusicPlayer { get; set; }
public string PrettyCurrentTime() {
public string PrettyCurrentTime()
{
var time = TimeSpan.FromSeconds(bytesSent / 3840 / 50);
return $"【{(int)time.TotalMinutes}m {time.Seconds}s】";
}
private ulong bytesSent { get; set; } = 0;
private Song(SongInfo songInfo) {
private Song(SongInfo songInfo)
{
this.SongInfo = songInfo;
}
private Task BufferSong(CancellationToken cancelToken) =>
Task.Run(async () => {
Task.Factory.StartNew(async () =>
{
Process p = null;
try {
p = Process.Start(new ProcessStartInfo {
try
{
p = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet",
UseShellExecute = false,
@ -156,11 +62,15 @@ namespace NadekoBot.Classes.Music {
const int blockSize = 3840;
var buffer = new byte[blockSize];
var attempt = 0;
while (!cancelToken.IsCancellationRequested) {
while (!cancelToken.IsCancellationRequested)
{
var read = 0;
try {
try
{
read = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, blockSize, cancelToken);
} catch {
}
catch
{
return;
}
if (read == 0)
@ -168,7 +78,8 @@ namespace NadekoBot.Classes.Music {
break;
else
await Task.Delay(100, cancelToken);
else {
else
{
attempt = 0;
await Task.Delay(5, cancelToken);
}
@ -176,24 +87,35 @@ namespace NadekoBot.Classes.Music {
if (songBuffer.ContentLength > 2.MB())
prebufferingComplete = true;
}
} catch (Exception ex) {
}
catch (Exception ex)
{
Console.WriteLine($"Buffering errored: {ex.Message}");
} finally {
}
finally
{
Console.WriteLine($"Buffering done." + $" [{songBuffer.ContentLength}]");
if (p != null) {
try {
if (p != null)
{
try
{
p.Kill();
} catch { }
}
catch { }
p.Dispose();
}
}
});
}, TaskCreationOptions.LongRunning);
internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) {
internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
{
var bufferTask = new ConfiguredTaskAwaitable();
try {
try
{
bufferTask = BufferSong(cancelToken).ConfigureAwait(false);
} catch (Exception ex) {
}
catch (Exception ex)
{
var clr = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"ERR BUFFER START : {ex.Message}\n{ex}");
@ -202,26 +124,31 @@ namespace NadekoBot.Classes.Music {
var bufferAttempts = 0;
const int waitPerAttempt = 500;
var toAttemptTimes = SongInfo.ProviderType != MusicType.Normal ? 5 : 9;
while (!prebufferingComplete && bufferAttempts++ < toAttemptTimes) {
while (!prebufferingComplete && bufferAttempts++ < toAttemptTimes)
{
await Task.Delay(waitPerAttempt, cancelToken);
}
cancelToken.ThrowIfCancellationRequested();
Console.WriteLine($"Prebuffering done? in {waitPerAttempt * bufferAttempts}");
const int blockSize = 3840;
var attempt = 0;
while (!cancelToken.IsCancellationRequested) {
while (!cancelToken.IsCancellationRequested)
{
//Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------");
byte[] buffer = new byte[blockSize];
var read = songBuffer.Read(buffer, blockSize);
unchecked {
unchecked
{
bytesSent += (ulong)read;
}
if (read == 0)
if (attempt++ == 20) {
if (attempt++ == 20)
{
voiceClient.Wait();
Console.WriteLine($"Song finished. [{songBuffer.ContentLength}]");
break;
} else
}
else
await Task.Delay(100, cancelToken);
else
attempt = 0;
@ -239,11 +166,13 @@ namespace NadekoBot.Classes.Music {
}
//stackoverflow ftw
private static byte[] AdjustVolume(byte[] audioSamples, float volume) {
private static byte[] AdjustVolume(byte[] audioSamples, float volume)
{
if (Math.Abs(volume - 1.0f) < 0.01f)
return audioSamples;
var array = new byte[audioSamples.Length];
for (var i = 0; i < array.Length; i += 2) {
for (var i = 0; i < array.Length; i += 2)
{
// convert byte pair to int
short buf1 = audioSamples[i + 1];
@ -263,35 +192,43 @@ namespace NadekoBot.Classes.Music {
return array;
}
public static async Task<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal) {
public static async Task<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
if (musicType != MusicType.Local && IsRadioLink(query)) {
if (musicType != MusicType.Local && IsRadioLink(query))
{
musicType = MusicType.Radio;
query = await HandleStreamContainers(query) ?? query;
}
try {
switch (musicType) {
try
{
switch (musicType)
{
case MusicType.Local:
return new Song(new SongInfo {
return new Song(new SongInfo
{
Uri = "\"" + Path.GetFullPath(query) + "\"",
Title = Path.GetFileNameWithoutExtension(query),
Provider = "Local File",
ProviderType = musicType,
});
case MusicType.Radio:
return new Song(new SongInfo {
return new Song(new SongInfo
{
Uri = query,
Title = $"{query}",
Provider = "Radio Stream",
ProviderType = musicType,
});
}
if (SoundCloud.Default.IsSoundCloudLink(query)) {
if (SoundCloud.Default.IsSoundCloudLink(query))
{
var svideo = await SoundCloud.Default.GetVideoAsync(query);
return new Song(new SongInfo {
return new Song(new SongInfo
{
Title = svideo.FullName,
Provider = "SoundCloud",
Uri = svideo.StreamLink,
@ -310,76 +247,99 @@ namespace NadekoBot.Classes.Music {
if (video == null) // do something with this error
throw new Exception("Could not load any video elements based on the query.");
return new Song(new SongInfo {
return new Song(new SongInfo
{
Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube"
Provider = "YouTube",
Uri = video.Uri,
ProviderType = musicType,
});
} catch (Exception ex) {
}
catch (Exception ex)
{
Console.WriteLine($"Failed resolving the link.{ex.Message}");
return null;
}
}
private static async Task<string> HandleStreamContainers(string query) {
private static async Task<string> HandleStreamContainers(string query)
{
string file = null;
try {
try
{
file = await SearchHelper.GetResponseStringAsync(query);
} catch {
}
catch
{
return query;
}
if (query.Contains(".pls")) {
if (query.Contains(".pls"))
{
//File1=http://armitunes.com:8000/
//Regex.Match(query)
try {
try
{
var m = Regex.Match(file, "File1=(?<url>.*?)\\n");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
} catch {
}
catch
{
Console.WriteLine($"Failed reading .pls:\n{file}");
return null;
}
}
if (query.Contains(".m3u")) {
if (query.Contains(".m3u"))
{
/*
# This is a comment
C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
*/
try {
try
{
var m = Regex.Match(file, "(?<url>^[^#].*)", RegexOptions.Multiline);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
} catch {
}
catch
{
Console.WriteLine($"Failed reading .m3u:\n{file}");
return null;
}
}
if (query.Contains(".asx")) {
if (query.Contains(".asx"))
{
//<ref href="http://armitunes.com:8000"/>
try {
try
{
var m = Regex.Match(file, "<ref href=\"(?<url>.*?)\"");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
} catch {
}
catch
{
Console.WriteLine($"Failed reading .asx:\n{file}");
return null;
}
}
if (query.Contains(".xspf")) {
if (query.Contains(".xspf"))
{
/*
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<track><location>file:///mp3s/song_1.mp3</location></track>
*/
try {
try
{
var m = Regex.Match(file, "<location>(?<url>.*?)</location>");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
} catch {
}
catch
{
Console.WriteLine($"Failed reading .xspf:\n{file}");
return null;
}

View File

@ -1,35 +1,41 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using System.Collections.Concurrent;
using Discord;
using Discord.Commands;
using NadekoBot.Modules;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using TriviaGame = NadekoBot.Classes.Trivia.TriviaGame;
namespace NadekoBot.Commands {
internal class Trivia : DiscordCommand {
namespace NadekoBot.Commands
{
internal class Trivia : DiscordCommand
{
public static ConcurrentDictionary<ulong, TriviaGame> RunningTrivias = new ConcurrentDictionary<ulong, TriviaGame>();
public Func<CommandEventArgs, Task> DoFunc() => async e => {
public Func<CommandEventArgs, Task> DoFunc() => async e =>
{
TriviaGame trivia;
if (!RunningTrivias.TryGetValue(e.Server.Id, out trivia)) {
if (!RunningTrivias.TryGetValue(e.Server.Id, out trivia))
{
var triviaGame = new TriviaGame(e);
if (RunningTrivias.TryAdd(e.Server.Id, triviaGame))
await e.Channel.SendMessage("**Trivia game started!**\nFirst player to get to 10 points wins! You have 30 seconds per question.\nUse command `tq` if game was started by accident.**");
else
await triviaGame.StopGame();
} else
}
else
await e.Channel.SendMessage("Trivia game is already running on this server.\n" + trivia.CurrentQuestion);
};
internal override void Init(CommandGroupBuilder cgb) {
internal override void Init(CommandGroupBuilder cgb)
{
cgb.CreateCommand(Module.Prefix + "t")
.Description("Starts a game of trivia.")
.Do(DoFunc());
cgb.CreateCommand(Module.Prefix + "tl")
.Description("Shows a current trivia leaderboard.")
.Do(async e=> {
.Do(async e =>
{
TriviaGame trivia;
if (RunningTrivias.TryGetValue(e.Server.Id, out trivia))
await e.Channel.SendMessage(trivia.GetLeaderboard());
@ -39,11 +45,14 @@ namespace NadekoBot.Commands {
cgb.CreateCommand(Module.Prefix + "tq")
.Description("Quits current trivia after current question.")
.Do(async e=> {
.Do(async e =>
{
TriviaGame trivia;
if (RunningTrivias.TryGetValue(e.Server.Id, out trivia)) {
if (RunningTrivias.TryGetValue(e.Server.Id, out trivia))
{
await trivia.StopGame();
} else
}
else
await e.Channel.SendMessage("No trivia is running on this server.");
});
}

View File

@ -148,8 +148,6 @@ namespace NadekoBot.Modules.Administration.Commands
str += $"`New name:` **{e.After.Name}**";
else if (e.Before.AvatarUrl != e.After.AvatarUrl)
str += $"`New Avatar:` {e.After.AvatarUrl}";
else if (e.Before.Status != e.After.Status)
str += $"Status `{e.Before.Status}` -> `{e.After.Status}`";
else
return;
await ch.SendMessage(str);

View File

@ -20,6 +20,7 @@ namespace NadekoBot.Modules.Administration.Commands
public ulong RepeatingServerId { get; set; }
public ulong RepeatingChannelId { get; set; }
public Message lastMessage { get; set; } = null;
public string RepeatingMessage { get; set; }
public int Interval { get; set; }
@ -34,7 +35,13 @@ namespace NadekoBot.Modules.Administration.Commands
{
try
{
await ch.SendMessage(msg);
if (lastMessage != null)
await lastMessage.Delete();
}
catch { }
try
{
lastMessage = await ch.SendMessage(msg);
}
catch { }
}

View File

@ -74,6 +74,10 @@ namespace NadekoBot.Modules.Administration.Commands
internal override void Init(CommandGroupBuilder cgb)
{
cgb.CreateCommand(Module.Prefix + "remind")
.Description("Sends a message to you or a channel after certain amount of time. " +
"First argument is me/here/'channelname'. Second argument is time in a descending order (mo>w>d>h>m) example: 1w5d3h10m. " +
"Third argument is a (multiword)message. " +
"\n**Usage**: `.remind me 1d5h Do something` or `.remind #general Start now!`")
.Parameter("meorchannel", ParameterType.Required)
.Parameter("time", ParameterType.Required)
.Parameter("message", ParameterType.Unparsed)

View File

@ -0,0 +1,125 @@
using Discord;
using Discord.Commands;
using NadekoBot.Commands;
using System.Text;
using System.Timers;
using static NadekoBot.Modules.Games.Commands.Bomberman;
namespace NadekoBot.Modules.Games.Commands
{
class Bomberman : DiscordCommand
{
public Field[,] board = new Field[15, 15];
public BombermanPlayer[] players = new BombermanPlayer[4];
public Channel gameChannel = null;
public Message godMsg = null;
public int curI = 5;
public int curJ = 5;
public Bomberman(DiscordModule module) : base(module)
{
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
board[i, j] = new Field();
}
}
NadekoBot.Client.MessageReceived += (s, e) =>
{
if (e.Channel != gameChannel ||
e.User.Id == NadekoBot.Client.CurrentUser.Id)
return;
if (e.Message.Text == "w")
{
board[curI - 1, curJ] = board[curI--, curJ];
board[curI + 1, curJ].player = null;
}
else if (e.Message.Text == "s")
{
board[curI + 1, curJ] = board[curI++, curJ];
board[curI - 1, curJ].player = null;
}
else if (e.Message.Text == "a")
{
board[curI, curJ - 1] = board[curI, curJ--];
board[curI, curJ + 1].player = null;
}
else if (e.Message.Text == "d")
{
board[curI, curJ + 1] = board[curI, curJ++];
board[curI, curJ - 1].player = null;
}
e.Message.Delete();
};
var t = new Timer();
t.Elapsed += async (s, e) =>
{
if (gameChannel == null)
return;
var boardStr = new StringBuilder();
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
boardStr.Append(board[i, j].ToString());
}
boardStr.AppendLine();
}
if (godMsg.Id != 0)
await godMsg.Edit(boardStr.ToString());
};
t.Interval = 1000;
t.Start();
}
internal override void Init(CommandGroupBuilder cgb)
{
//cgb.CreateCommand(Module.Prefix + "bomb")
// .Description("Bomberman start")
// .Do(async e =>
// {
// if (gameChannel != null)
// return;
// godMsg = await e.Channel.SendMessage("GAME START IN 1 SECOND....");
// gameChannel = e.Channel;
// players[0] = new BombermanPlayer
// {
// User = e.User,
// };
// board[5, 5].player = players[0];
// });
}
public class BombermanPlayer
{
public User User = null;
public string Icon = "👳";
internal void MoveLeft()
{
}
}
}
internal struct Field
{
public BombermanPlayer player;
public override string ToString() => player?.Icon ?? "⬜";
}
}

View File

@ -2,21 +2,23 @@
using Discord.Modules;
using NadekoBot.Commands;
using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Commands;
using System;
using System.Linq;
namespace NadekoBot.Modules
namespace NadekoBot.Modules.Games
{
internal class Games : DiscordModule
internal class GamesModule : DiscordModule
{
private readonly Random rng = new Random();
public Games()
public GamesModule()
{
commands.Add(new Trivia(this));
commands.Add(new SpeedTyping(this));
commands.Add(new PollCommand(this));
commands.Add(new PlantPick(this));
commands.Add(new Bomberman(this));
//commands.Add(new BetrayGame(this));
}

View File

@ -71,7 +71,7 @@ namespace NadekoBot.Modules
cgb.CreateCommand("n")
.Alias("next")
.Alias("skip")
.Description("Goes to the next song in the queue.")
.Description("Goes to the next song in the queue.**Usage**: `!m n`")
.Do(e =>
{
MusicPlayer musicPlayer;
@ -81,7 +81,7 @@ namespace NadekoBot.Modules
cgb.CreateCommand("s")
.Alias("stop")
.Description("Stops the music and clears the playlist. Stays in the channel.")
.Description("Stops the music and clears the playlist. Stays in the channel.\n**Usage**: `!m s`")
.Do(async e =>
{
await Task.Run(() =>
@ -94,7 +94,8 @@ namespace NadekoBot.Modules
cgb.CreateCommand("d")
.Alias("destroy")
.Description("Completely stops the music and unbinds the bot from the channel. (may cause weird behaviour)")
.Description("Completely stops the music and unbinds the bot from the channel. " +
"(may cause weird behaviour)\n**Usage**: `!m d`")
.Do(async e =>
{
await Task.Run(() =>
@ -107,7 +108,7 @@ namespace NadekoBot.Modules
cgb.CreateCommand("p")
.Alias("pause")
.Description("Pauses or Unpauses the song.")
.Description("Pauses or Unpauses the song.\n**Usage**: `!m p`")
.Do(async e =>
{
MusicPlayer musicPlayer;
@ -121,7 +122,8 @@ namespace NadekoBot.Modules
cgb.CreateCommand("q")
.Alias("yq")
.Description("Queue a song using keywords or a link. Bot will join your voice channel. **You must be in a voice channel**.\n**Usage**: `!m q Dream Of Venice`")
.Description("Queue a song using keywords or a link. Bot will join your voice channel." +
"**You must be in a voice channel**.\n**Usage**: `!m q Dream Of Venice`")
.Parameter("query", ParameterType.Unparsed)
.Do(async e =>
{
@ -130,7 +132,7 @@ namespace NadekoBot.Modules
cgb.CreateCommand("lq")
.Alias("ls").Alias("lp")
.Description("Lists up to 15 currently queued songs.")
.Description("Lists up to 15 currently queued songs.\n**Usage**: `!m lq`")
.Do(async e =>
{
MusicPlayer musicPlayer;
@ -158,7 +160,7 @@ namespace NadekoBot.Modules
cgb.CreateCommand("np")
.Alias("playing")
.Description("Shows the song currently playing.")
.Description("Shows the song currently playing.\n**Usage**: `!m np`")
.Do(async e =>
{
MusicPlayer musicPlayer;
@ -172,7 +174,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("vol")
.Description("Sets the music volume 0-150%")
.Description("Sets the music volume 0-150%\n**Usage**: `!m vol 50`")
.Parameter("val", ParameterType.Required)
.Do(async e =>
{
@ -192,7 +194,8 @@ namespace NadekoBot.Modules
cgb.CreateCommand("dv")
.Alias("defvol")
.Description("Sets the default music volume when music playback is started (0-100). Does not persist through restarts.\n**Usage**: !m dv 80")
.Description("Sets the default music volume when music playback is started (0-100)." +
" Does not persist through restarts.\n**Usage**: `!m dv 80`")
.Parameter("val", ParameterType.Required)
.Do(async e =>
{
@ -208,7 +211,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("min").Alias("mute")
.Description("Sets the music volume to 0%")
.Description("Sets the music volume to 0%\n**Usage**: `!m min`")
.Do(e =>
{
MusicPlayer musicPlayer;
@ -218,7 +221,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("max")
.Description("Sets the music volume to 100% (real max is actually 150%).")
.Description("Sets the music volume to 100% (real max is actually 150%).\n**Usage**: `!m max`")
.Do(e =>
{
MusicPlayer musicPlayer;
@ -228,7 +231,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("half")
.Description("Sets the music volume to 50%.")
.Description("Sets the music volume to 50%.\n**Usage**: `!m half`")
.Do(e =>
{
MusicPlayer musicPlayer;
@ -238,7 +241,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("sh")
.Description("Shuffles the current playlist.")
.Description("Shuffles the current playlist.\n**Usage**: `!m sh`")
.Do(async e =>
{
MusicPlayer musicPlayer;
@ -254,18 +257,8 @@ namespace NadekoBot.Modules
await e.Channel.SendMessage("🎵 `Songs shuffled.`");
});
cgb.CreateCommand("setgame")
.Description("Sets the game of the bot to the number of songs playing. **Owner only**")
.AddCheck(SimpleCheckers.OwnerOnly())
.Do(async e =>
{
await e.Channel.SendMessage("❗This command is deprecated. " +
"Use:\n `.ropl`\n `.adpl %playing% songs, %queued% queued.` instead.\n " +
"It even persists through restarts.");
});
cgb.CreateCommand("pl")
.Description("Queues up to 25 songs from a youtube playlist specified by a link, or keywords.")
.Description("Queues up to 25 songs from a youtube playlist specified by a link, or keywords.\n**Usage**: `!m pl playlist link or name`")
.Parameter("playlist", ParameterType.Unparsed)
.Do(async e =>
{
@ -292,7 +285,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("lopl")
.Description("Queues up to 50 songs from a directory. **Owner Only!**")
.Description("Queues up to 50 songs from a directory. **Owner Only!**\n**Usage**: `!m lopl C:/music/classical`")
.Parameter("directory", ParameterType.Unparsed)
.AddCheck(Classes.Permissions.SimpleCheckers.OwnerOnly())
.Do(async e =>
@ -313,7 +306,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("radio").Alias("ra")
.Description("Queues a radio stream from a link. It can be a direct mp3 radio stream, .m3u, .pls .asx or .xspf")
.Description("Queues a radio stream from a link. It can be a direct mp3 radio stream, .m3u, .pls .asx or .xspf\n**Usage**: `!m ra radio link here`")
.Parameter("radio_link", ParameterType.Required)
.Do(async e =>
{
@ -326,7 +319,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("lo")
.Description("Queues a local file by specifying a full path. **Owner Only!**")
.Description("Queues a local file by specifying a full path. **Owner Only!**\n**Usage**: `!m ra C:/music/mysong.mp3`")
.Parameter("path", ParameterType.Unparsed)
.AddCheck(Classes.Permissions.SimpleCheckers.OwnerOnly())
.Do(async e =>
@ -338,7 +331,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("mv")
.Description("Moves the bot to your voice channel. (works only if music is already playing)")
.Description("Moves the bot to your voice channel. (works only if music is already playing)\n**Usage**: `!m mv`")
.Do(e =>
{
MusicPlayer musicPlayer;
@ -349,7 +342,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("rm")
.Description("Remove a song by its # in the queue, or 'all' to remove whole queue.")
.Description("Remove a song by its # in the queue, or 'all' to remove whole queue.\n**Usage**: `!m rm 5`")
.Parameter("num", ParameterType.Required)
.Do(async e =>
{
@ -378,7 +371,7 @@ namespace NadekoBot.Modules
});
cgb.CreateCommand("cleanup")
.Description("Cleans up hanging voice connections. **Owner Only!**")
.Description("Cleans up hanging voice connections. **Owner Only!**\n**Usage**: `!m cleanup`")
.AddCheck(SimpleCheckers.OwnerOnly())
.Do(e =>
{
@ -397,7 +390,7 @@ namespace NadekoBot.Modules
cgb.CreateCommand("rcs")
.Alias("repeatcurrentsong")
.Description("Toggles repeat of current song.")
.Description("Toggles repeat of current song.\n**Usage**: `!m rcs`")
.Do(async e =>
{
MusicPlayer musicPlayer;
@ -414,7 +407,7 @@ namespace NadekoBot.Modules
cgb.CreateCommand("rpl")
.Alias("repeatplaylist")
.Description("Toggles repeat of all songs in the queue (every song that finishes is added to the end of the queue).")
.Description("Toggles repeat of all songs in the queue (every song that finishes is added to the end of the queue).\n**Usage**: `!m rpl`")
.Do(async e =>
{
MusicPlayer musicPlayer;

View File

@ -0,0 +1,162 @@
using Discord.Commands;
using NadekoBot.Commands;
using ScaredFingers.UnitsConversion;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Search.Commands
{
class ConverterCommand : DiscordCommand
{
public ConverterCommand(DiscordModule module) : base(module)
{
if (unitTables == null)
{
CultureInfo ci = new CultureInfo("en-US");
Thread.CurrentThread.CurrentCulture = ci;
unitTables = new List<UnitTable>();
unitTables.Add(UnitTable.LengthTable);
unitTables.Add(UnitTable.TemperatureTable);
unitTables.Add(UnitTable.VolumeTable);
unitTables.Add(UnitTable.WeightTable);
reInitCurrencyConverterTable();
}
}
internal override void Init(CommandGroupBuilder cgb)
{
cgb.CreateCommand(Module.Prefix + "convert")
.Description("Convert quantities from>to. Like `~convert m>km 1000`")
.Parameter("from-to", ParameterType.Required)
.Parameter("quantity", ParameterType.Optional)
.Do(ConvertFunc());
cgb.CreateCommand(Module.Prefix + "convertlist")
.Description("List of the convertable dimensions and currencies.")
.Do(ConvertListFunc());
}
private Func<CommandEventArgs, Task> ConvertListFunc() =>
async e =>
{
reInitCurrencyConverterTable();
string msg = "";
foreach (var tmpTable in unitTables)
{
int i = 1;
while (tmpTable.IsKnownUnit(i))
{
msg += tmpTable.GetUnitName(i) + " (" + tmpTable.GetUnitSymbol(i) + "); ";
i++;
}
msg += "\n";
}
foreach (var curr in exchangeRateProvider.Currencies)
{
msg += curr + "; ";
}
await e.Channel.SendMessage(msg);
};
private Func<CommandEventArgs, Task> ConvertFunc() =>
async e =>
{
try
{
await e.Channel.SendIsTyping();
string from = e.GetArg("from-to").ToLowerInvariant().Split('>')[0];
string to = e.GetArg("from-to").ToLowerInvariant().Split('>')[1];
float quantity = 1.0f;
if (!float.TryParse(e.GetArg("quantity"), out quantity))
{
quantity = 1.0f;
}
int fromCode, toCode = 0;
UnitTable table = null;
ResolveUnitCodes(from, to, out table, out fromCode, out toCode);
if (table != null)
{
Unit inUnit = new Unit(fromCode, quantity, table);
Unit outUnit = inUnit.Convert(toCode);
await e.Channel.SendMessage(inUnit.ToString() + " = " + outUnit.ToString());
}
else
{
reInitCurrencyConverterTable();
Unit inUnit = currTable.CreateUnit(quantity, from.ToUpperInvariant());
Unit outUnit = inUnit.Convert(currTable.CurrencyCode(to.ToUpperInvariant()));
await e.Channel.SendMessage(inUnit.ToString() + " = " + outUnit.ToString());
}
}
catch //(Exception ex)
{
//Console.WriteLine(ex.ToString());
await e.Channel.SendMessage("Bad input format, or sth went wrong... Try to list them with `" + Module.Prefix + "`convertlist");
}
};
private void reInitCurrencyConverterTable()
{
if (lastChanged == null || lastChanged.DayOfYear != DateTime.Now.DayOfYear)
{
exchangeRateProvider = new WebExchangeRatesProvider();
currTable = new CurrencyExchangeTable(exchangeRateProvider);
lastChanged = DateTime.Now;
}
}
private void ResolveUnitCodes(string from, string to, out UnitTable table, out int fromCode, out int toCode)
{
foreach (var tmpTable in unitTables)
{
int f = LookupUnit(tmpTable, from);
int t = LookupUnit(tmpTable, to);
if (f > 0 && t > 0)
{
table = tmpTable;
fromCode = f;
toCode = t;
return;
}
}
table = null;
fromCode = 0;
toCode = 0;
}
private int LookupUnit(UnitTable table, string lookup)
{
string wellformedLookup = lookup.ToLowerInvariant().Replace("°", "");
int i = 1;
while (table.IsKnownUnit(i))
{
if (wellformedLookup == table.GetUnitName(i).ToLowerInvariant().Replace("°", "") ||
wellformedLookup == table.GetUnitPlural(i).ToLowerInvariant().Replace("°", "") ||
wellformedLookup == table.GetUnitSymbol(i).ToLowerInvariant().Replace("°", ""))
{
return i;
}
i++;
}
return 0;
}
private static List<UnitTable> unitTables;
private static CurrencyExchangeRatesProvider exchangeRateProvider;
private static CurrencyExchangeTable currTable;
private static DateTime lastChanged;
}
}

View File

@ -5,6 +5,7 @@ using NadekoBot.Classes.IMDB;
using NadekoBot.Classes.JSONModels;
using NadekoBot.Commands;
using NadekoBot.Extensions;
using NadekoBot.Modules.Search.Commands;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
@ -23,6 +24,7 @@ namespace NadekoBot.Modules
{
commands.Add(new LoLCommands(this));
commands.Add(new StreamNotifications(this));
commands.Add(new ConverterCommand(this));
rng = new Random();
}

View File

@ -7,6 +7,7 @@ using NadekoBot.Commands;
using NadekoBot.Modules;
using NadekoBot.Modules.Administration;
using NadekoBot.Modules.Gambling;
using NadekoBot.Modules.Games;
using NadekoBot.Modules.Pokemon;
using NadekoBot.Modules.Translator;
using Newtonsoft.Json;
@ -163,7 +164,7 @@ namespace NadekoBot
modules.Add(new PermissionModule(), "Permissions", ModuleFilter.None);
modules.Add(new Conversations(), "Conversations", ModuleFilter.None);
modules.Add(new GamblingModule(), "Gambling", ModuleFilter.None);
modules.Add(new Games(), "Games", ModuleFilter.None);
modules.Add(new GamesModule(), "Games", ModuleFilter.None);
modules.Add(new Music(), "Music", ModuleFilter.None);
modules.Add(new Searches(), "Searches", ModuleFilter.None);
modules.Add(new NSFW(), "NSFW", ModuleFilter.None);

View File

@ -97,6 +97,10 @@
<HintPath>..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="ScaredFingers.UnitsConversion, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>lib\ScaredFingers.UnitsConversion.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
@ -130,6 +134,7 @@
<Compile Include="Classes\JSONModels\_JSONModels.cs" />
<Compile Include="Classes\Leet.cs" />
<Compile Include="Classes\Music\MusicControls.cs" />
<Compile Include="Classes\Music\PoopyBuffer.cs" />
<Compile Include="Classes\Music\Song.cs" />
<Compile Include="Classes\Music\StreamRequest.cs" />
<Compile Include="Classes\Music\SoundCloud.cs" />
@ -186,7 +191,8 @@
<Compile Include="Modules\Conversations.cs" />
<Compile Include="Modules\DiscordModule.cs" />
<Compile Include="Modules\Gambling\GamblingModule.cs" />
<Compile Include="Modules\Games.cs" />
<Compile Include="Modules\Games\Commands\Bomberman.cs" />
<Compile Include="Modules\Games\GamesModule.cs" />
<Compile Include="Modules\Help.cs" />
<Compile Include="Modules\Music.cs" />
<Compile Include="Commands\PollCommand.cs" />
@ -217,6 +223,7 @@
<Compile Include="Modules\Pokemon\PokemonTypes\NormalType.cs" />
<Compile Include="Modules\Pokemon\PokeStats.cs" />
<Compile Include="Modules\Searches.cs" />
<Compile Include="Modules\Search\Commands\ConverterCommand.cs" />
<Compile Include="Modules\Translator\Helpers\GoogleTranslator.cs" />
<Compile Include="Modules\Translator\TranslateCommand.cs" />
<Compile Include="Modules\Translator\TranslatorModule.cs" />
@ -478,6 +485,9 @@
<Name>Discord.Net</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="lib\ScaredFingers.UnitsConversion.dll" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

Binary file not shown.

Binary file not shown.

View File

@ -2,7 +2,7 @@
######You can donate on paypal: `nadekodiscordbot@gmail.com` or Bitcoin `17MZz1JAqME39akMLrVT4XBPffQJ2n1EPa`
#NadekoBot List Of Commands
Version: `NadekoBot v0.9.5933.23628`
Version: `NadekoBot v0.9.5936.28974`
### Administration
Command and aliases | Description | Usage
----------------|--------------|-------
@ -31,7 +31,7 @@ Command and aliases | Description | Usage
`.lsar` | Lits all self-assignable roles.
`.iam` | Adds a role to you that you choose. Role must be on a list of self-assignable roles. | .iam Gamer
`.iamn`, `.iamnot` | Removes a role to you that you choose. Role must be on a list of self-assignable roles. | .iamn Gamer
`.remind` |
`.remind` | Sends a message to you or a channel after certain amount of time. First argument is me/here/'channelname'. Second argument is time in a descending order (mo>w>d>h>m) example: 1w5d3h10m. Third argument is a (multiword)message. | `.remind me 1d5h Do something` or `.remind #general Start now!`
`.sr`, `.setrole` | Sets a role for a given user. | .sr @User Guest
`.rr`, `.removerole` | Removes a role from a given user. | .rr @User Admin
`.r`, `.role`, `.cr` | Creates a role with a given name. | .r Awesome Role
@ -166,7 +166,7 @@ Command and aliases | Description | Usage
`$draw` | Draws a card from the deck.If you supply number [x], she draws up to 5 cards from the deck. | $draw [x]
`$shuffle`, `$sh` | Reshuffles all cards back into the deck.
`$flip` | Flips coin(s) - heads or tails, and shows an image. | `$flip` or `$flip 3`
`$roll` | Rolls 2 dice from 0-10. If you supply a number [x] it rolls up to 30 normal dice. | $roll [x]
`$roll` | Rolls 0-100. If you supply a number [x] it rolls up to 30 normal dice. If you split 2 numbers with letter d (xdy) it will roll x dice from 1 to y. | $roll or $roll 7 or $roll 3d5
`$nroll` | Rolls in a given range. | `$nroll 5` (rolls 0-5) or `$nroll 5-15`
`$raffle` | Prints a name and ID of a random user from the online list from the (optional) role.
`$$$` | Check how much NadekoFlowers you have.
@ -185,6 +185,8 @@ Command and aliases | Description | Usage
`>typeadd` | Adds a new article to the typing contest. Owner only.
`>poll` | Creates a poll, only person who has manage server permission can do it. | >poll Question?;Answer1;Answ 2;A_3
`>pollend` | Stops active poll on this server and prints the results in this channel.
`>pick` | Picks a flower planted in this channel.
`>plant` | Spend a flower to plant it in this channel. (If bot is restarted or crashes, flower will be lost)
`>choose` | Chooses a thing from a list of things | >choose Get up;Sleep;Sleep more
`>8ball` | Ask the 8ball a yes/no question.
`>rps` | Play a game of rocket paperclip scissors with Nadeko. | >rps scissors
@ -193,27 +195,28 @@ Command and aliases | Description | Usage
### Music
Command and aliases | Description | Usage
----------------|--------------|-------
`!m n`, `!m next`, `!m skip` | Goes to the next song in the queue.
`!m s`, `!m stop` | Stops the music and clears the playlist. Stays in the channel.
`!m d`, `!m destroy` | Completely stops the music and unbinds the bot from the channel. (may cause weird behaviour)
`!m p`, `!m pause` | Pauses or Unpauses the song.
`!m n`, `!m next`, `!m skip` | Goes to the next song in the queue. | `!m n`
`!m s`, `!m stop` | Stops the music and clears the playlist. Stays in the channel. | `!m s`
`!m d`, `!m destroy` | Completely stops the music and unbinds the bot from the channel. (may cause weird behaviour) | `!m d`
`!m p`, `!m pause` | Pauses or Unpauses the song. | `!m p`
`!m q`, `!m yq` | Queue a song using keywords or a link. Bot will join your voice channel.**You must be in a voice channel**. | `!m q Dream Of Venice`
`!m lq`, `!m ls`, `!m lp` | Lists up to 15 currently queued songs.
`!m np`, `!m playing` | Shows the song currently playing.
`!m vol` | Sets the music volume 0-150%
`!m dv`, `!m defvol` | Sets the default music volume when music playback is started (0-100). Does not persist through restarts. | !m dv 80
`!m min`, `!m mute` | Sets the music volume to 0%
`!m max` | Sets the music volume to 100% (real max is actually 150%).
`!m half` | Sets the music volume to 50%.
`!m sh` | Shuffles the current playlist.
`!m setgame` | Sets the game of the bot to the number of songs playing. **Owner only**
`!m pl` | Queues up to 25 songs from a youtube playlist specified by a link, or keywords.
`!m lopl` | Queues up to 50 songs from a directory. **Owner Only!**
`!m radio`, `!m ra` | Queues a radio stream from a link. It can be a direct mp3 radio stream, .m3u, .pls .asx or .xspf
`!m lo` | Queues a local file by specifying a full path. **Owner Only!**
`!m mv` | Moves the bot to your voice channel. (works only if music is already playing)
`!m rm` | Remove a song by its # in the queue, or 'all' to remove whole queue.
`!m cleanup` | Cleans up hanging voice connections. **Owner Only!**
`!m lq`, `!m ls`, `!m lp` | Lists up to 15 currently queued songs. | `!m lq`
`!m np`, `!m playing` | Shows the song currently playing. | `!m np`
`!m vol` | Sets the music volume 0-150% | `!m vol 50`
`!m dv`, `!m defvol` | Sets the default music volume when music playback is started (0-100). Does not persist through restarts. | `!m dv 80`
`!m min`, `!m mute` | Sets the music volume to 0% | `!m min`
`!m max` | Sets the music volume to 100% (real max is actually 150%). | `!m max`
`!m half` | Sets the music volume to 50%. | `!m half`
`!m sh` | Shuffles the current playlist. | `!m sh`
`!m pl` | Queues up to 25 songs from a youtube playlist specified by a link, or keywords. | `!m pl playlist link or name`
`!m lopl` | Queues up to 50 songs from a directory. **Owner Only!** | `!m lopl C:/music/classical`
`!m radio`, `!m ra` | Queues a radio stream from a link. It can be a direct mp3 radio stream, .m3u, .pls .asx or .xspf | `!m ra radio link here`
`!m lo` | Queues a local file by specifying a full path. **Owner Only!** | `!m ra C:/music/mysong.mp3`
`!m mv` | Moves the bot to your voice channel. (works only if music is already playing) | `!m mv`
`!m rm` | Remove a song by its # in the queue, or 'all' to remove whole queue. | `!m rm 5`
`!m cleanup` | Cleans up hanging voice connections. **Owner Only!** | `!m cleanup`
`!m rcs`, `!m repeatcurrentsong` | Toggles repeat of current song. | `!m rcs`
`!m rpl`, `!m repeatplaylist` | Toggles repeat of all songs in the queue (every song that finishes is added to the end of the queue). | `!m rpl`
### Searches
Command and aliases | Description | Usage
@ -225,6 +228,8 @@ Command and aliases | Description | Usage
`~beam`, `~bm` | Notifies this channel when a certain user starts streaming. | ~beam SomeStreamer
`~removestream`, `~rms` | Removes notifications of a certain streamer on this channel. | ~rms SomeGuy
`~liststreams`, `~ls` | Lists all streams you are following on this server. | ~ls
`~convert` | Convert quantities from>to. Like `~convert m>km 1000`
`~convertlist` | List of the convertable dimensions and currencies.
`~we` | Shows weather data for a specified city and a country BOTH ARE REQUIRED. Weather api is very random if you make a mistake.
`~yt` | Searches youtubes and shows the first result
`~ani`, `~anime`, `~aq` | Queries anilist for an anime and shows the first result.
@ -243,6 +248,7 @@ Command and aliases | Description | Usage
`~yomama`, `~ym` | Shows a random joke from <http://api.yomomma.info/>
`~randjoke`, `~rj` | Shows a random joke from <http://tambal.azurewebsites.net/joke/random>
`~chucknorris`, `~cn` | Shows a random chucknorris joke from <http://tambal.azurewebsites.net/joke/random>
`~mi`, `magicitem` | Shows a random magicitem from <https://1d4chan.org/wiki/List_of_/tg/%27s_magic_items>
`~revav` | Returns a google reverse image search for someone's avatar.
### NSFW