Utility and nsfw work

This commit is contained in:
Master Kwoth
2017-05-23 01:59:31 +02:00
parent d08bc60be5
commit 2df415341c
83 changed files with 1495 additions and 1308 deletions

View File

@@ -1,20 +0,0 @@
using System;
namespace NadekoBot.Modules.Music.Classes
{
class PlaylistFullException : Exception
{
public PlaylistFullException(string message) : base(message)
{
}
public PlaylistFullException() : base("Queue is full.") { }
}
class SongNotFoundException : Exception
{
public SongNotFoundException(string message) : base(message)
{
}
public SongNotFoundException() : base("Song is not found.") { }
}
}

View File

@@ -1,15 +0,0 @@
using Discord;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Classes
{
public static class MusicExtensions
{
public static EmbedAuthorBuilder WithMusicIcon(this EmbedAuthorBuilder eab) =>
eab.WithIconUrl("http://i.imgur.com/nhKS3PT.png");
}
}

View File

@@ -1,295 +0,0 @@
using Discord.Audio;
using NadekoBot.Extensions;
using NLog;
using System;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
namespace NadekoBot.Modules.Music.Classes
{
public class SongInfo
{
public string Provider { get; set; }
public MusicType ProviderType { get; set; }
public string Query { get; set; }
public string Title { get; set; }
public string Uri { get; set; }
public string AlbumArt { get; set; }
}
public class Song
{
public SongInfo SongInfo { get; }
public MusicPlayer MusicPlayer { get; set; }
private string _queuerName;
public string QueuerName { get{
return Discord.Format.Sanitize(_queuerName);
} set { _queuerName = value; } }
public TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
public TimeSpan CurrentTime => TimeSpan.FromSeconds(BytesSent / (float)_frameBytes / (1000 / (float)_milliseconds));
private const int _milliseconds = 20;
private const int _samplesPerFrame = (48000 / 1000) * _milliseconds;
private const int _frameBytes = 3840; //16-bit, 2 channels
private ulong BytesSent { get; set; }
//pwetty
public string PrettyProvider =>
$"{(SongInfo.Provider ?? "???")}";
public string PrettyFullTime => PrettyCurrentTime + " / " + PrettyTotalTime;
public string PrettyName => $"**[{SongInfo.Title.TrimTo(65)}]({SongUrl})**";
public string PrettyInfo => $"{MusicPlayer.PrettyVolume} | {PrettyTotalTime} | {PrettyProvider} | {QueuerName}";
public string PrettyFullName => $"{PrettyName}\n\t\t`{PrettyTotalTime} | {PrettyProvider} | {QueuerName}`";
public string PrettyCurrentTime {
get {
var time = CurrentTime.ToString(@"mm\:ss");
var hrs = (int)CurrentTime.TotalHours;
if (hrs > 0)
return hrs + ":" + time;
else
return time;
}
}
public string PrettyTotalTime {
get
{
if (TotalTime == TimeSpan.Zero)
return "(?)";
if (TotalTime == TimeSpan.MaxValue)
return "∞";
var time = TotalTime.ToString(@"mm\:ss");
var hrs = (int)TotalTime.TotalHours;
if (hrs > 0)
return hrs + ":" + time;
return time;
}
}
public string Thumbnail {
get {
switch (SongInfo.ProviderType)
{
case MusicType.Radio:
return "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png"; //test links
case MusicType.Normal:
//todo have videoid in songinfo from the start
var videoId = Regex.Match(SongInfo.Query, "<=v=[a-zA-Z0-9-]+(?=&)|(?<=[0-9])[^&\n]+|(?<=v=)[^&\n]+");
return $"https://img.youtube.com/vi/{ videoId }/0.jpg";
case MusicType.Local:
return "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png"; //test links
case MusicType.Soundcloud:
return SongInfo.AlbumArt;
default:
return "";
}
}
}
public string SongUrl {
get {
switch (SongInfo.ProviderType)
{
case MusicType.Normal:
return SongInfo.Query;
case MusicType.Soundcloud:
return SongInfo.Query;
case MusicType.Local:
return $"https://google.com/search?q={ WebUtility.UrlEncode(SongInfo.Title).Replace(' ', '+') }";
case MusicType.Radio:
return $"https://google.com/search?q={SongInfo.Title}";
default:
return "";
}
}
}
public int SkipTo { get; set; }
private readonly Logger _log;
public Song(SongInfo songInfo)
{
SongInfo = songInfo;
_log = LogManager.GetCurrentClassLogger();
}
public Song Clone()
{
var s = new Song(SongInfo)
{
MusicPlayer = MusicPlayer,
QueuerName = QueuerName
};
return s;
}
public async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
{
BytesSent = (ulong) SkipTo * 3840 * 50;
var filename = Path.Combine(Music.MusicDataPath, DateTime.Now.UnixTimestamp().ToString());
var inStream = new SongBuffer(MusicPlayer, filename, SongInfo, SkipTo, _frameBytes * 100);
var bufferTask = inStream.BufferSong(cancelToken).ConfigureAwait(false);
try
{
var attempt = 0;
var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 1.MiB()); //Fast connection can do this easy
var finished = false;
var count = 0;
var sw = new Stopwatch();
var slowconnection = false;
sw.Start();
while (!finished)
{
var t = await Task.WhenAny(prebufferingTask, Task.Delay(2000, cancelToken));
if (t != prebufferingTask)
{
count++;
if (count == 10)
{
slowconnection = true;
prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken, 20.MiB());
_log.Warn("Slow connection buffering more to ensure no disruption, consider hosting in cloud");
continue;
}
if (inStream.BufferingCompleted && count == 1)
{
_log.Debug("Prebuffering canceled. Cannot get any data from the stream.");
return;
}
else
{
continue;
}
}
else if (prebufferingTask.IsCanceled)
{
_log.Debug("Prebuffering canceled. Cannot get any data from the stream.");
return;
}
finished = true;
}
sw.Stop();
_log.Debug("Prebuffering successfully completed in " + sw.Elapsed);
var outStream = voiceClient.CreatePCMStream(AudioApplication.Music);
int nextTime = Environment.TickCount + _milliseconds;
byte[] buffer = new byte[_frameBytes];
while (!cancelToken.IsCancellationRequested && //song canceled for whatever reason
!(MusicPlayer.MaxPlaytimeSeconds != 0 && CurrentTime.TotalSeconds >= MusicPlayer.MaxPlaytimeSeconds)) // or exceedded max playtime
{
//Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------");
var read = await inStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
//await inStream.CopyToAsync(voiceClient.OutputStream);
if (read < _frameBytes)
_log.Debug("read {0}", read);
unchecked
{
BytesSent += (ulong)read;
}
if (read < _frameBytes)
{
if (read == 0)
{
if (inStream.BufferingCompleted)
break;
if (attempt++ == 20)
{
MusicPlayer.SongCancelSource.Cancel();
break;
}
if (slowconnection)
{
_log.Warn("Slow connection has disrupted music, waiting a bit for buffer");
await Task.Delay(1000, cancelToken).ConfigureAwait(false);
nextTime = Environment.TickCount + _milliseconds;
}
else
{
await Task.Delay(100, cancelToken).ConfigureAwait(false);
nextTime = Environment.TickCount + _milliseconds;
}
}
else
attempt = 0;
}
else
attempt = 0;
while (MusicPlayer.Paused)
{
await Task.Delay(200, cancelToken).ConfigureAwait(false);
nextTime = Environment.TickCount + _milliseconds;
}
buffer = AdjustVolume(buffer, MusicPlayer.Volume);
if (read != _frameBytes) continue;
nextTime = unchecked(nextTime + _milliseconds);
int delayMillis = unchecked(nextTime - Environment.TickCount);
if (delayMillis > 0)
await Task.Delay(delayMillis, cancelToken).ConfigureAwait(false);
await outStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
}
}
finally
{
await bufferTask;
inStream.Dispose();
}
}
private async Task CheckPrebufferingAsync(SongBuffer inStream, CancellationToken cancelToken, long size)
{
while (!inStream.BufferingCompleted && inStream.Length < size)
{
await Task.Delay(100, cancelToken);
}
_log.Debug("Buffering successfull");
}
//aidiakapi ftw
public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume)
{
if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples;
// 16-bit precision for the multiplication
var volumeFixed = (int)Math.Round(volume * 65536d);
var count = audioSamples.Length / 2;
fixed (byte* srcBytes = audioSamples)
{
var src = (short*)srcBytes;
for (var i = count; i != 0; i--, src++)
*src = (short)(((*src) * volumeFixed) >> 16);
}
return audioSamples;
}
}
}

View File

@@ -1,219 +0,0 @@
using NadekoBot.Extensions;
using NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Classes
{
/// <summary>
/// Create a buffer for a song file. It will create multiples files to ensure, that radio don't fill up disk space.
/// It also help for large music by deleting files that are already seen.
/// </summary>
class SongBuffer : Stream
{
public SongBuffer(MusicPlayer musicPlayer, string basename, SongInfo songInfo, int skipTo, int maxFileSize)
{
MusicPlayer = musicPlayer;
Basename = basename;
SongInfo = songInfo;
SkipTo = skipTo;
MaxFileSize = maxFileSize;
CurrentFileStream = new FileStream(this.GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
_log = LogManager.GetCurrentClassLogger();
}
MusicPlayer MusicPlayer { get; }
private string Basename { get; }
private SongInfo SongInfo { get; }
private int SkipTo { get; }
private int MaxFileSize { get; } = 2.MiB();
private long FileNumber = -1;
private long NextFileToRead = 0;
public bool BufferingCompleted { get; private set; } = false;
private ulong CurrentBufferSize = 0;
private FileStream CurrentFileStream;
private Logger _log;
public Task BufferSong(CancellationToken cancelToken) =>
Task.Run(async () =>
{
Process p = null;
FileStream outStream = null;
try
{
p = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-ss {SkipTo} -i {SongInfo.Uri} -f s16le -ar 48000 -vn -ac 2 pipe:1 -loglevel quiet",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = false,
CreateNoWindow = true,
});
byte[] buffer = new byte[81920];
int currentFileSize = 0;
ulong prebufferSize = 100ul.MiB();
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
while (!p.HasExited) //Also fix low bandwidth
{
int bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false);
if (currentFileSize >= MaxFileSize)
{
try
{
outStream.Dispose();
}
catch { }
outStream = new FileStream(Basename + "-" + ++FileNumber, FileMode.Append, FileAccess.Write, FileShare.Read);
currentFileSize = bytesRead;
}
else
{
currentFileSize += bytesRead;
}
CurrentBufferSize += Convert.ToUInt64(bytesRead);
await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
while (CurrentBufferSize > prebufferSize)
await Task.Delay(100, cancelToken);
}
BufferingCompleted = true;
}
catch (System.ComponentModel.Win32Exception)
{
var oldclr = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(@"You have not properly installed or configured FFMPEG.
Please install and configure FFMPEG to play music.
Check the guides for your platform on how to setup ffmpeg correctly:
Windows Guide: https://goo.gl/OjKk8F
Linux Guide: https://goo.gl/ShjCUo");
Console.ForegroundColor = oldclr;
}
catch (Exception ex)
{
Console.WriteLine($"Buffering stopped: {ex.Message}");
}
finally
{
if (outStream != null)
outStream.Dispose();
Console.WriteLine($"Buffering done.");
if (p != null)
{
try
{
p.Kill();
}
catch { }
p.Dispose();
}
}
});
/// <summary>
/// Return the next file to read, and delete the old one
/// </summary>
/// <returns>Name of the file to read</returns>
private string GetNextFile()
{
string filename = Basename + "-" + NextFileToRead;
if (NextFileToRead != 0)
{
try
{
CurrentBufferSize -= Convert.ToUInt64(new FileInfo(Basename + "-" + (NextFileToRead - 1)).Length);
File.Delete(Basename + "-" + (NextFileToRead - 1));
}
catch { }
}
NextFileToRead++;
return filename;
}
private bool IsNextFileReady()
{
return NextFileToRead <= FileNumber;
}
private void CleanFiles()
{
for (long i = NextFileToRead - 1; i <= FileNumber; i++)
{
try
{
File.Delete(Basename + "-" + i);
}
catch { }
}
}
//Stream part
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => (long)CurrentBufferSize;
public override long Position { get; set; } = 0;
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)
{
int read = CurrentFileStream.Read(buffer, offset, count);
if (read < count)
{
if (!BufferingCompleted || IsNextFileReady())
{
CurrentFileStream.Dispose();
CurrentFileStream = new FileStream(GetNextFile(), FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
read += CurrentFileStream.Read(buffer, read + offset, count - read);
}
if (read < count)
Array.Clear(buffer, read, count - read);
}
return read;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public new void Dispose()
{
CurrentFileStream.Dispose();
MusicPlayer.SongCancelSource.Cancel();
CleanFiles();
base.Dispose();
}
}
}

View File

@@ -1,215 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NLog;
using VideoLibrary;
namespace NadekoBot.Modules.Music.Classes
{
public static class SongHandler
{
private static readonly Logger _log = LogManager.GetCurrentClassLogger();
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))
{
musicType = MusicType.Radio;
query = await HandleStreamContainers(query).ConfigureAwait(false) ?? query;
}
try
{
switch (musicType)
{
case MusicType.Local:
return new Song(new SongInfo
{
Uri = "\"" + Path.GetFullPath(query) + "\"",
Title = Path.GetFileNameWithoutExtension(query),
Provider = "Local File",
ProviderType = musicType,
Query = query,
});
case MusicType.Radio:
return new Song(new SongInfo
{
Uri = query,
Title = $"{query}",
Provider = "Radio Stream",
ProviderType = musicType,
Query = query
})
{ TotalTime = TimeSpan.MaxValue };
}
if (SoundCloud.Default.IsSoundCloudLink(query))
{
var svideo = await SoundCloud.Default.ResolveVideoAsync(query).ConfigureAwait(false);
return new Song(new SongInfo
{
Title = svideo.FullName,
Provider = "SoundCloud",
Uri = svideo.StreamLink,
ProviderType = musicType,
Query = svideo.TrackLink,
AlbumArt = svideo.artwork_url,
})
{ TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) };
}
if (musicType == MusicType.Soundcloud)
{
var svideo = await SoundCloud.Default.GetVideoByQueryAsync(query).ConfigureAwait(false);
return new Song(new SongInfo
{
Title = svideo.FullName,
Provider = "SoundCloud",
Uri = svideo.StreamLink,
ProviderType = MusicType.Soundcloud,
Query = svideo.TrackLink,
AlbumArt = svideo.artwork_url,
})
{ TotalTime = TimeSpan.FromMilliseconds(svideo.Duration) };
}
var link = (await NadekoBot.Google.GetVideosByKeywordsAsync(query).ConfigureAwait(false)).FirstOrDefault();
if (string.IsNullOrWhiteSpace(link))
throw new OperationCanceledException("Not a valid youtube query.");
var allVideos = await Task.Run(async () => { try { return await YouTube.Default.GetAllVideosAsync(link).ConfigureAwait(false); } catch { return Enumerable.Empty<YouTubeVideo>(); } }).ConfigureAwait(false);
var videos = allVideos.Where(v => v.AdaptiveKind == AdaptiveKind.Audio);
var video = videos
.Where(v => v.AudioBitrate < 256)
.OrderByDescending(v => v.AudioBitrate)
.FirstOrDefault();
if (video == null) // do something with this error
throw new Exception("Could not load any video elements based on the query.");
var m = Regex.Match(query, @"\?t=(?<t>\d*)");
int gotoTime = 0;
if (m.Captures.Count > 0)
int.TryParse(m.Groups["t"].ToString(), out gotoTime);
var song = new Song(new SongInfo
{
Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube"
Provider = "YouTube",
Uri = await video.GetUriAsync().ConfigureAwait(false),
Query = link,
ProviderType = musicType,
});
song.SkipTo = gotoTime;
return song;
}
catch (Exception ex)
{
_log.Warn($"Failed resolving the link.{ex.Message}");
_log.Warn(ex);
return null;
}
}
private static async Task<string> HandleStreamContainers(string query)
{
string file = null;
try
{
using (var http = new HttpClient())
{
file = await http.GetStringAsync(query).ConfigureAwait(false);
}
}
catch
{
return query;
}
if (query.Contains(".pls"))
{
//File1=http://armitunes.com:8000/
//Regex.Match(query)
try
{
var m = Regex.Match(file, "File1=(?<url>.*?)\\n");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .pls:\n{file}");
return null;
}
}
if (query.Contains(".m3u"))
{
/*
# This is a comment
C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
*/
try
{
var m = Regex.Match(file, "(?<url>^[^#].*)", RegexOptions.Multiline);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .m3u:\n{file}");
return null;
}
}
if (query.Contains(".asx"))
{
//<ref href="http://armitunes.com:8000"/>
try
{
var m = Regex.Match(file, "<ref href=\"(?<url>.*?)\"");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .asx:\n{file}");
return null;
}
}
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
{
var m = Regex.Match(file, "<location>(?<url>.*?)</location>");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
}
catch
{
_log.Warn($"Failed reading .xspf:\n{file}");
return null;
}
}
return query;
}
private static bool IsRadioLink(string query) =>
(query.StartsWith("http") ||
query.StartsWith("ww"))
&&
(query.Contains(".pls") ||
query.Contains(".m3u") ||
query.Contains(".asx") ||
query.Contains(".xspf"));
}
}

View File

@@ -1,142 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Music.Classes
{
public class SoundCloud
{
private static readonly SoundCloud _instance = new SoundCloud();
public static SoundCloud Default => _instance;
static SoundCloud() { }
public SoundCloud() { }
public async Task<SoundCloudVideo> ResolveVideoAsync(string url)
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentNullException(nameof(url));
if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.SoundCloudClientId))
throw new ArgumentNullException(nameof(NadekoBot.Credentials.SoundCloudClientId));
string response = "";
using (var http = new HttpClient())
{
response = await http.GetStringAsync($"http://api.soundcloud.com/resolve?url={url}&client_id={NadekoBot.Credentials.SoundCloudClientId}").ConfigureAwait(false);
}
var responseObj = Newtonsoft.Json.JsonConvert.DeserializeObject<SoundCloudVideo>(response);
if (responseObj?.Kind != "track")
throw new InvalidOperationException("Url is either not a track, or it doesn't exist.");
return responseObj;
}
public bool IsSoundCloudLink(string url) =>
System.Text.RegularExpressions.Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)");
public async Task<SoundCloudVideo> GetVideoByQueryAsync(string query)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.SoundCloudClientId))
throw new ArgumentNullException(nameof(NadekoBot.Credentials.SoundCloudClientId));
var response = "";
using (var http = new HttpClient())
{
response = await http.GetStringAsync($"http://api.soundcloud.com/tracks?q={Uri.EscapeDataString(query)}&client_id={NadekoBot.Credentials.SoundCloudClientId}").ConfigureAwait(false);
}
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo[]>(response).Where(s => s.Streamable).FirstOrDefault();
if (responseObj?.Kind != "track")
throw new InvalidOperationException("Query yielded no results.");
return responseObj;
}
}
public class SoundCloudVideo
{
public string Kind { get; set; } = "";
public long Id { get; set; } = 0;
public SoundCloudUser User { get; set; } = new SoundCloudUser();
public string Title { get; set; } = "";
[JsonIgnore]
public string FullName => User.Name + " - " + Title;
public bool Streamable { get; set; } = false;
public int Duration { get; set; }
[JsonProperty("permalink_url")]
public string TrackLink { get; set; } = "";
public string artwork_url { get; set; } = "";
[JsonIgnore]
public string StreamLink => $"https://api.soundcloud.com/tracks/{Id}/stream?client_id={NadekoBot.Credentials.SoundCloudClientId}";
}
public class SoundCloudUser
{
[Newtonsoft.Json.JsonProperty("username")]
public string Name { get; set; }
}
/*
{"kind":"track",
"id":238888167,
"created_at":"2015/12/24 01:04:52 +0000",
"user_id":43141975,
"duration":120852,
"commentable":true,
"state":"finished",
"original_content_size":4834829,
"last_modified":"2015/12/24 01:17:59 +0000",
"sharing":"public",
"tag_list":"Funky",
"permalink":"18-fd",
"streamable":true,
"embeddable_by":"all",
"downloadable":false,
"purchase_url":null,
"label_id":null,
"purchase_title":null,
"genre":"Disco",
"title":"18 Ж",
"description":"",
"label_name":null,
"release":null,
"track_type":null,
"key_signature":null,
"isrc":null,
"video_url":null,
"bpm":null,
"release_year":null,
"release_month":null,
"release_day":null,
"original_format":"mp3",
"license":"all-rights-reserved",
"uri":"https://api.soundcloud.com/tracks/238888167",
"user":{
"id":43141975,
"kind":"user",
"permalink":"mrb00gi",
"username":"Mrb00gi",
"last_modified":"2015/12/01 16:06:57 +0000",
"uri":"https://api.soundcloud.com/users/43141975",
"permalink_url":"http://soundcloud.com/mrb00gi",
"avatar_url":"https://a1.sndcdn.com/images/default_avatar_large.png"
},
"permalink_url":"http://soundcloud.com/mrb00gi/18-fd",
"artwork_url":null,
"waveform_url":"https://w1.sndcdn.com/gsdLfvEW1cUK_m.png",
"stream_url":"https://api.soundcloud.com/tracks/238888167/stream",
"playback_count":7,
"download_count":0,
"favoritings_count":1,
"comment_count":0,
"attachments_uri":"https://api.soundcloud.com/tracks/238888167/attachments"}
*/
}

View File

@@ -23,8 +23,6 @@ namespace NadekoBot.Modules.Music
[DontAutoLoad]
public class Music : NadekoTopLevelModule
{
public const string MusicDataPath = "data/musicdata";
private static MusicService music;
static Music()

View File

@@ -11,15 +11,21 @@ using NadekoBot.Extensions;
using System.Xml;
using System.Threading;
using System.Collections.Concurrent;
using NadekoBot.Services.Searches;
namespace NadekoBot.Modules.NSFW
{
[NadekoModule("NSFW", "~")]
[NadekoModule("NSFW")]
public class NSFW : NadekoTopLevelModule
{
private static readonly ConcurrentDictionary<ulong, Timer> _autoHentaiTimers = new ConcurrentDictionary<ulong, Timer>();
private static readonly ConcurrentHashSet<ulong> _hentaiBombBlacklist = new ConcurrentHashSet<ulong>();
private readonly SearchesService _service;
public NSFW(SearchesService service)
{
_service = service;
}
private async Task InternalHentai(IMessageChannel channel, string tag, bool noError)
{
@@ -142,11 +148,11 @@ namespace NadekoBot.Modules.NSFW
#endif
[NadekoCommand, Usage, Description, Aliases]
public Task Yandere([Remainder] string tag = null)
=> InternalDapiCommand(tag, Searches.Searches.DapiSearchType.Yandere);
=> InternalDapiCommand(tag, DapiSearchType.Yandere);
[NadekoCommand, Usage, Description, Aliases]
public Task Konachan([Remainder] string tag = null)
=> InternalDapiCommand(tag, Searches.Searches.DapiSearchType.Konachan);
=> InternalDapiCommand(tag, DapiSearchType.Konachan);
[NadekoCommand, Usage, Description, Aliases]
public async Task E621([Remainder] string tag = null)
@@ -167,7 +173,7 @@ namespace NadekoBot.Modules.NSFW
[NadekoCommand, Usage, Description, Aliases]
public Task Rule34([Remainder] string tag = null)
=> InternalDapiCommand(tag, Searches.Searches.DapiSearchType.Rule34);
=> InternalDapiCommand(tag, DapiSearchType.Rule34);
[NadekoCommand, Usage, Description, Aliases]
public async Task Danbooru([Remainder] string tag = null)
@@ -210,7 +216,7 @@ namespace NadekoBot.Modules.NSFW
[NadekoCommand, Usage, Description, Aliases]
public Task Gelbooru([Remainder] string tag = null)
=> InternalDapiCommand(tag, Searches.Searches.DapiSearchType.Gelbooru);
=> InternalDapiCommand(tag, DapiSearchType.Gelbooru);
[NadekoCommand, Usage, Description, Aliases]
public async Task Boobs()
@@ -270,23 +276,23 @@ namespace NadekoBot.Modules.NSFW
}
});
public static Task<string> GetRule34ImageLink(string tag) =>
Searches.Searches.InternalDapiSearch(tag, Searches.Searches.DapiSearchType.Rule34);
public Task<string> GetRule34ImageLink(string tag) =>
_service.DapiSearch(tag, DapiSearchType.Rule34);
public static Task<string> GetYandereImageLink(string tag) =>
Searches.Searches.InternalDapiSearch(tag, Searches.Searches.DapiSearchType.Yandere);
public Task<string> GetYandereImageLink(string tag) =>
_service.DapiSearch(tag, DapiSearchType.Yandere);
public static Task<string> GetKonachanImageLink(string tag) =>
Searches.Searches.InternalDapiSearch(tag, Searches.Searches.DapiSearchType.Konachan);
public Task<string> GetKonachanImageLink(string tag) =>
_service.DapiSearch(tag, DapiSearchType.Konachan);
public static Task<string> GetGelbooruImageLink(string tag) =>
Searches.Searches.InternalDapiSearch(tag, Searches.Searches.DapiSearchType.Gelbooru);
public Task<string> GetGelbooruImageLink(string tag) =>
_service.DapiSearch(tag, DapiSearchType.Gelbooru);
public async Task InternalDapiCommand(string tag, Searches.Searches.DapiSearchType type)
public async Task InternalDapiCommand(string tag, DapiSearchType type)
{
tag = tag?.Trim() ?? "";
var url = await Searches.Searches.InternalDapiSearch(tag, type).ConfigureAwait(false);
var url = await _service.DapiSearch(tag, type).ConfigureAwait(false);
if (url == null)
await ReplyErrorLocalized("not_found").ConfigureAwait(false);

View File

@@ -1,6 +1,7 @@
using Discord;
using Discord.Commands;
using NadekoBot.Extensions;
using NadekoBot.Services;
using NLog;
using System;
using System.Globalization;
@@ -16,20 +17,23 @@ namespace NadekoBot.Modules
public readonly string ModuleTypeName;
public readonly string LowerModuleTypeName;
//todo :thinking:
public NadekoStrings _strings { get; set; }
public ILocalization _localization { get; set; }
protected NadekoTopLevelModule(bool isTopLevelModule = true)
{
//if it's top level module
ModuleTypeName = isTopLevelModule ? this.GetType().Name : this.GetType().DeclaringType.Name;
LowerModuleTypeName = ModuleTypeName.ToLowerInvariant();
if (!NadekoBot.ModulePrefixes.TryGetValue(ModuleTypeName, out Prefix))
Prefix = "?err?";
Prefix = NadekoBot.Prefix;
_log = LogManager.GetCurrentClassLogger();
}
protected override void BeforeExecute()
{
_cultureInfo = NadekoBot.Localization.GetCultureInfo(Context.Guild?.Id);
_cultureInfo =_localization.GetCultureInfo(Context.Guild?.Id);
_log.Info("Culture info is {0}", _cultureInfo);
}
@@ -53,46 +57,12 @@ namespace NadekoBot.Modules
// var text = NadekoBot.ResponsesResourceManager.GetString(textKey, cultureInfo);
// return Context.Channel.SendErrorAsync(title, text, url, footer);
//}
/// <summary>
/// Used as failsafe in case response key doesn't exist in the selected or default language.
/// </summary>
private static readonly CultureInfo _usCultureInfo = new CultureInfo("en-US");
public static string GetTextStatic(string key, CultureInfo cultureInfo, string lowerModuleTypeName)
{
var text = NadekoBot.Strings.GetString(lowerModuleTypeName + "_" + key, cultureInfo);
if (string.IsNullOrWhiteSpace(text))
{
LogManager.GetCurrentClassLogger().Warn(lowerModuleTypeName + "_" + key + " key is missing from " + cultureInfo + " response strings. PLEASE REPORT THIS.");
text = NadekoBot.Strings.GetString(lowerModuleTypeName + "_" + key, _usCultureInfo) ?? $"Error: dkey {lowerModuleTypeName + "_" + key} not found!";
if (string.IsNullOrWhiteSpace(text))
return "I can't tell you if the command is executed, because there was an error printing out the response. Key '" +
lowerModuleTypeName + "_" + key + "' " + "is missing from resources. Please report this.";
}
return text;
}
public static string GetTextStatic(string key, CultureInfo cultureInfo, string lowerModuleTypeName,
params object[] replacements)
{
try
{
return string.Format(GetTextStatic(key, cultureInfo, lowerModuleTypeName), replacements);
}
catch (FormatException)
{
return "I can't tell you if the command is executed, because there was an error printing out the response. Key '" +
lowerModuleTypeName + "_" + key + "' " + "is not properly formatted. Please report this.";
}
}
protected string GetText(string key) =>
GetTextStatic(key, _cultureInfo, LowerModuleTypeName);
_strings.GetText(key, _cultureInfo, LowerModuleTypeName);
protected string GetText(string key, params object[] replacements) =>
GetTextStatic(key, _cultureInfo, LowerModuleTypeName, replacements);
_strings.GetText(key, _cultureInfo, LowerModuleTypeName, replacements);
public Task<IUserMessage> ErrorLocalized(string textKey, params object[] replacements)
{

View File

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

View File

@@ -20,20 +20,6 @@ namespace NadekoBot.Modules.Permissions
[NadekoModule("Permissions", ";")]
public partial class Permissions : NadekoTopLevelModule
{
public class OldPermissionCache
{
public string PermRole { get; set; }
public bool Verbose { get; set; } = true;
public Permission RootPermission { get; set; }
}
public class PermissionCache
{
public string PermRole { get; set; }
public bool Verbose { get; set; } = true;
public PermissionsCollection<Permissionv2> Permissions { get; set; }
}
//guildid, root permission
public static ConcurrentDictionary<ulong, PermissionCache> Cache { get; } =
new ConcurrentDictionary<ulong, PermissionCache>();

View File

@@ -762,14 +762,6 @@ namespace NadekoBot.Modules.Searches
// }
//}
public enum DapiSearchType
{
Safebooru,
Gelbooru,
Konachan,
Rule34,
Yandere
}
public async Task InternalDapiCommand(IUserMessage umsg, string tag, DapiSearchType type)
{
@@ -787,55 +779,6 @@ namespace NadekoBot.Modules.Searches
.WithImageUrl(url)
.WithFooter(efb => efb.WithText(type.ToString()))).ConfigureAwait(false);
}
public static async Task<string> InternalDapiSearch(string tag, DapiSearchType type)
{
tag = tag?.Replace(" ", "_");
var website = "";
switch (type)
{
case DapiSearchType.Safebooru:
website = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
break;
case DapiSearchType.Gelbooru:
website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
break;
case DapiSearchType.Rule34:
website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
break;
case DapiSearchType.Konachan:
website = $"https://konachan.com/post.xml?s=post&q=index&limit=100&tags={tag}";
break;
case DapiSearchType.Yandere:
website = $"https://yande.re/post.xml?limit=100&tags={tag}";
break;
}
try
{
var toReturn = await Task.Run(async () =>
{
using (var http = new HttpClient())
{
http.AddFakeHeaders();
var data = await http.GetStreamAsync(website).ConfigureAwait(false);
var doc = new XmlDocument();
doc.Load(data);
var node = doc.LastChild.ChildNodes[new NadekoRandom().Next(0, doc.LastChild.ChildNodes.Count)];
var url = node.Attributes["file_url"].Value;
if (!url.StartsWith("http"))
url = "https:" + url;
return url;
}
}).ConfigureAwait(false);
return toReturn;
}
catch
{
return null;
}
}
public async Task<bool> ValidateQuery(IMessageChannel ch, string query)
{
if (!string.IsNullOrWhiteSpace(query)) return true;

View File

@@ -1,48 +1,32 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Services;
using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
namespace NadekoBot.Modules.Utility
{
public partial class Utility
{
public class CommandAliasEqualityComparer : IEqualityComparer<CommandAlias>
{
public bool Equals(CommandAlias x, CommandAlias y) => x.Trigger == y.Trigger;
public int GetHashCode(CommandAlias obj) => obj.Trigger.GetHashCode();
}
[Group]
public class CommandMapCommands : NadekoSubmodule
{
//guildId, (trigger, mapping)
public static ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>> AliasMaps { get; } = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>();
private readonly UtilityService _service;
private readonly DbHandler _db;
private readonly DiscordShardedClient _client;
static CommandMapCommands()
public CommandMapCommands(UtilityService service, DbHandler db, DiscordShardedClient client)
{
var eq = new CommandAliasEqualityComparer();
AliasMaps = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>(
NadekoBot.AllGuildConfigs.ToDictionary(
x => x.GuildId,
x => new ConcurrentDictionary<string, string>(x.CommandAliases
.Distinct(eq)
.ToDictionary(ca => ca.Trigger, ca => ca.Mapping))));
}
public static void Unload()
{
AliasMaps.Clear();
_service = service;
_db = db;
_client = client;
}
[NadekoCommand, Usage, Description, Aliases]
@@ -61,14 +45,14 @@ namespace NadekoBot.Modules.Utility
{
ConcurrentDictionary<string, string> maps;
string throwaway;
if (!AliasMaps.TryGetValue(Context.Guild.Id, out maps) ||
if (!_service.AliasMaps.TryGetValue(Context.Guild.Id, out maps) ||
!maps.TryRemove(trigger, out throwaway))
{
await ReplyErrorLocalized("alias_remove_fail", Format.Code(trigger)).ConfigureAwait(false);
return;
}
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
var config = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.CommandAliases));
var toAdd = new CommandAlias()
@@ -83,9 +67,9 @@ namespace NadekoBot.Modules.Utility
await ReplyConfirmLocalized("alias_removed", Format.Code(trigger)).ConfigureAwait(false);
return;
}
AliasMaps.AddOrUpdate(Context.Guild.Id, (_) =>
_service.AliasMaps.AddOrUpdate(Context.Guild.Id, (_) =>
{
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
var config = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.CommandAliases));
config.CommandAliases.Add(new CommandAlias()
@@ -100,7 +84,7 @@ namespace NadekoBot.Modules.Utility
});
}, (_, map) =>
{
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
var config = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.CommandAliases));
var toAdd = new CommandAlias()
@@ -131,7 +115,7 @@ namespace NadekoBot.Modules.Utility
return;
ConcurrentDictionary<string, string> maps;
if (!AliasMaps.TryGetValue(Context.Guild.Id, out maps) || !maps.Any())
if (!_service.AliasMaps.TryGetValue(Context.Guild.Id, out maps) || !maps.Any())
{
await ReplyErrorLocalized("aliases_none").ConfigureAwait(false);
return;
@@ -139,7 +123,7 @@ namespace NadekoBot.Modules.Utility
var arr = maps.ToArray();
await Context.Channel.SendPaginatedConfirmAsync(page + 1, (curPage) =>
await Context.Channel.SendPaginatedConfirmAsync(_client, page + 1, (curPage) =>
{
return new EmbedBuilder().WithOkColor()
.WithTitle(GetText("alias_list"))

View File

@@ -15,60 +15,13 @@ namespace NadekoBot.Modules.Utility
[Group]
public class CrossServerTextChannel : NadekoSubmodule
{
static CrossServerTextChannel()
private readonly UtilityService _service;
public CrossServerTextChannel(UtilityService service)
{
NadekoBot.Client.MessageReceived += Client_MessageReceived;
_service = service;
}
public static void Unload()
{
NadekoBot.Client.MessageReceived -= Client_MessageReceived;
}
private static async Task Client_MessageReceived(Discord.WebSocket.SocketMessage imsg)
{
try
{
if (imsg.Author.IsBot)
return;
var msg = imsg as IUserMessage;
if (msg == null)
return;
var channel = imsg.Channel as ITextChannel;
if (channel == null)
return;
if (msg.Author.Id == NadekoBot.Client.CurrentUser.Id) return;
foreach (var subscriber in Subscribers)
{
var set = subscriber.Value;
if (!set.Contains(channel))
continue;
foreach (var chan in set.Except(new[] { channel }))
{
try
{
await chan.SendMessageAsync(GetMessage(channel, (IGuildUser)msg.Author,
msg)).ConfigureAwait(false);
}
catch
{
// ignored
}
}
}
}
catch
{
// ignored
}
}
private static string GetMessage(ITextChannel channel, IGuildUser user, IUserMessage message) =>
$"**{channel.Guild.Name} | {channel.Name}** `{user.Username}`: " + message.Content.SanitizeMentions();
public static readonly ConcurrentDictionary<int, ConcurrentHashSet<ITextChannel>> Subscribers =
new ConcurrentDictionary<int, ConcurrentHashSet<ITextChannel>>();
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
@@ -76,7 +29,7 @@ namespace NadekoBot.Modules.Utility
{
var token = new NadekoRandom().Next();
var set = new ConcurrentHashSet<ITextChannel>();
if (Subscribers.TryAdd(token, set))
if (_service.Subscribers.TryAdd(token, set))
{
set.Add((ITextChannel) Context.Channel);
await ((IGuildUser) Context.User).SendConfirmAsync(GetText("csc_token"), token.ToString())
@@ -90,7 +43,7 @@ namespace NadekoBot.Modules.Utility
public async Task Jcsc(int token)
{
ConcurrentHashSet<ITextChannel> set;
if (!Subscribers.TryGetValue(token, out set))
if (!_service.Subscribers.TryGetValue(token, out set))
return;
set.Add((ITextChannel) Context.Channel);
await ReplyConfirmLocalized("csc_join").ConfigureAwait(false);
@@ -101,7 +54,7 @@ namespace NadekoBot.Modules.Utility
[RequireUserPermission(GuildPermission.ManageGuild)]
public async Task Lcsc()
{
foreach (var subscriber in Subscribers)
foreach (var subscriber in _service.Subscribers)
{
subscriber.Value.TryRemove((ITextChannel) Context.Channel);
}

View File

@@ -1,7 +1,9 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Services;
using System;
using System.Linq;
using System.Text;
@@ -14,6 +16,17 @@ namespace NadekoBot.Modules.Utility
[Group]
public class InfoCommands : NadekoSubmodule
{
private readonly DiscordShardedClient _client;
private readonly IStatsService _stats;
private readonly CommandHandler _ch;
public InfoCommands(DiscordShardedClient client, IStatsService stats, CommandHandler ch)
{
_client = client;
_stats = stats;
_ch = ch;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ServerInfo(string guildName = null)
@@ -24,7 +37,7 @@ namespace NadekoBot.Modules.Utility
if (string.IsNullOrWhiteSpace(guildName))
guild = channel.Guild;
else
guild = NadekoBot.Client.Guilds.FirstOrDefault(g => g.Name.ToUpperInvariant() == guildName.ToUpperInvariant());
guild = _client.Guilds.FirstOrDefault(g => g.Name.ToUpperInvariant() == guildName.ToUpperInvariant());
if (guild == null)
return;
var ownername = await guild.GetUserAsync(guild.OwnerId);
@@ -50,9 +63,9 @@ namespace NadekoBot.Modules.Utility
.AddField(fb => fb.WithName(GetText("features")).WithValue(features).WithIsInline(true))
.WithImageUrl(guild.IconUrl)
.WithColor(NadekoBot.OkColor);
if (guild.Emojis.Any())
if (guild.Emotes.Any())
{
embed.AddField(fb => fb.WithName(GetText("custom_emojis") + $"({guild.Emojis.Count})").WithValue(string.Join(" ", guild.Emojis.Shuffle().Take(20).Select(e => $"{e.Name} <:{e.Name}:{e.Id}>"))));
embed.AddField(fb => fb.WithName(GetText("custom_emojis") + $"({guild.Emotes.Count})").WithValue(string.Join(" ", guild.Emotes.Shuffle().Take(20).Select(e => $"{e.Name} <:{e.Name}:{e.Id}>"))));
}
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
@@ -101,36 +114,36 @@ namespace NadekoBot.Modules.Utility
embed.WithThumbnailUrl(user.RealAvatarUrl());
await Context.Channel.EmbedAsync(embed).ConfigureAwait(false);
}
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Activity(int page = 1)
{
const int activityPerPage = 15;
page -= 1;
if (page < 0)
return;
int startCount = page * activityPerPage;
StringBuilder str = new StringBuilder();
foreach (var kvp in NadekoBot.CommandHandler.UserMessagesSent.OrderByDescending(kvp => kvp.Value).Skip(page*activityPerPage).Take(activityPerPage))
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task Activity(int page = 1)
{
str.AppendLine(GetText("activity_line",
++startCount,
Format.Bold(kvp.Key.ToString()),
kvp.Value / NadekoBot.Stats.GetUptime().TotalSeconds, kvp.Value));
}
const int activityPerPage = 15;
page -= 1;
await Context.Channel.EmbedAsync(new EmbedBuilder()
.WithTitle(GetText("activity_page", page + 1))
.WithOkColor()
.WithFooter(efb => efb.WithText(GetText("activity_users_total",
NadekoBot.CommandHandler.UserMessagesSent.Count)))
.WithDescription(str.ToString()));
if (page < 0)
return;
int startCount = page * activityPerPage;
StringBuilder str = new StringBuilder();
foreach (var kvp in _ch.UserMessagesSent.OrderByDescending(kvp => kvp.Value).Skip(page * activityPerPage).Take(activityPerPage))
{
str.AppendLine(GetText("activity_line",
++startCount,
Format.Bold(kvp.Key.ToString()),
kvp.Value / _stats.GetUptime().TotalSeconds, kvp.Value));
}
await Context.Channel.EmbedAsync(new EmbedBuilder()
.WithTitle(GetText("activity_page", page + 1))
.WithOkColor()
.WithFooter(efb => efb.WithText(GetText("activity_users_total",
_ch.UserMessagesSent.Count)))
.WithDescription(str.ToString()));
}
}
}
}

View File

@@ -1,19 +1,17 @@
using Discord;
using Discord.Commands;
using Discord.Net;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Discord.WebSocket;
using NadekoBot.Modules.Utility.Models;
namespace NadekoBot.Modules.Utility
{
@@ -22,140 +20,15 @@ namespace NadekoBot.Modules.Utility
[Group]
public class RepeatCommands : NadekoSubmodule
{
//guildid/RepeatRunners
public static ConcurrentDictionary<ulong, ConcurrentQueue<RepeatRunner>> Repeaters { get; set; }
private readonly UtilityService _service;
private readonly DiscordShardedClient _client;
private readonly DbHandler _db;
private static bool _ready;
public class RepeatRunner
public RepeatCommands(UtilityService service, DiscordShardedClient client, DbHandler db)
{
private readonly Logger _log;
private CancellationTokenSource source { get; set; }
private CancellationToken token { get; set; }
public Repeater Repeater { get; }
public SocketGuild Guild { get; }
public ITextChannel Channel { get; private set; }
private IUserMessage oldMsg = null;
public RepeatRunner(Repeater repeater, ITextChannel channel = null)
{
_log = LogManager.GetCurrentClassLogger();
Repeater = repeater;
Channel = channel;
Guild = NadekoBot.Client.GetGuild(repeater.GuildId);
if(Guild!=null)
Task.Run(Run);
}
private async Task Run()
{
source = new CancellationTokenSource();
token = source.Token;
try
{
while (!token.IsCancellationRequested)
{
await Task.Delay(Repeater.Interval, token).ConfigureAwait(false);
await Trigger().ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
}
public async Task Trigger()
{
var toSend = "🔄 " + Repeater.Message;
//var lastMsgInChannel = (await Channel.GetMessagesAsync(2)).FirstOrDefault();
// if (lastMsgInChannel.Id == oldMsg?.Id) //don't send if it's the same message in the channel
// continue;
if (oldMsg != null)
try
{
await oldMsg.DeleteAsync();
}
catch
{
// ignored
}
try
{
if (Channel == null)
Channel = Guild.GetTextChannel(Repeater.ChannelId);
if (Channel != null)
oldMsg = await Channel.SendMessageAsync(toSend.SanitizeMentions()).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
_log.Warn("Missing permissions. Repeater stopped. ChannelId : {0}", Channel?.Id);
return;
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
{
_log.Warn("Channel not found. Repeater stopped. ChannelId : {0}", Channel?.Id);
return;
}
catch (Exception ex)
{
_log.Warn(ex);
}
}
public void Reset()
{
source.Cancel();
var _ = Task.Run(Run);
}
public void Stop()
{
source.Cancel();
}
public override string ToString() =>
$"{Channel?.Mention ?? $"<#{Repeater.ChannelId}>" } " +
$"| {(int) Repeater.Interval.TotalHours}:{Repeater.Interval:mm} " +
$"| {Repeater.Message.TrimTo(33)}";
}
static RepeatCommands()
{
var _ = Task.Run(async () =>
{
#if !GLOBAL_NADEKO
await Task.Delay(5000).ConfigureAwait(false);
#else
await Task.Delay(30000).ConfigureAwait(false);
#endif
//todo this is pretty terrible
Repeaters = new ConcurrentDictionary<ulong, ConcurrentQueue<RepeatRunner>>(NadekoBot.AllGuildConfigs
.ToDictionary(gc => gc.GuildId,
gc => new ConcurrentQueue<RepeatRunner>(gc.GuildRepeaters
.Select(gr => new RepeatRunner(gr))
.Where(x => x.Guild != null))));
_ready = true;
});
}
public static void Unload()
{
_ready = false;
foreach (var kvp in Repeaters)
{
RepeatRunner r;
while (kvp.Value.TryDequeue(out r))
{
r.Stop();
}
}
Repeaters.Clear();
_service = service;
_client = client;
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
@@ -163,11 +36,10 @@ namespace NadekoBot.Modules.Utility
[RequireUserPermission(GuildPermission.ManageMessages)]
public async Task RepeatInvoke(int index)
{
if (!_ready)
if (!_service.RepeaterReady)
return;
index -= 1;
ConcurrentQueue<RepeatRunner> rep;
if (!Repeaters.TryGetValue(Context.Guild.Id, out rep))
if (!_service.Repeaters.TryGetValue(Context.Guild.Id, out var rep))
{
await ReplyErrorLocalized("repeat_invoke_none").ConfigureAwait(false);
return;
@@ -193,14 +65,13 @@ namespace NadekoBot.Modules.Utility
[Priority(0)]
public async Task RepeatRemove(int index)
{
if (!_ready)
if (!_service.RepeaterReady)
return;
if (index < 1)
return;
index -= 1;
ConcurrentQueue<RepeatRunner> rep;
if (!Repeaters.TryGetValue(Context.Guild.Id, out rep))
if (!_service.Repeaters.TryGetValue(Context.Guild.Id, out var rep))
return;
var repeaterList = rep.ToList();
@@ -215,7 +86,7 @@ namespace NadekoBot.Modules.Utility
repeater.Stop();
repeaterList.RemoveAt(index);
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
var guildConfig = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(gc => gc.GuildRepeaters));
@@ -223,7 +94,7 @@ namespace NadekoBot.Modules.Utility
await uow.CompleteAsync().ConfigureAwait(false);
}
if (Repeaters.TryUpdate(Context.Guild.Id, new ConcurrentQueue<RepeatRunner>(repeaterList), rep))
if (_service.Repeaters.TryUpdate(Context.Guild.Id, new ConcurrentQueue<RepeatRunner>(repeaterList), rep))
await Context.Channel.SendConfirmAsync(GetText("message_repeater"),
GetText("repeater_stopped", index + 1) + $"\n\n{repeater}").ConfigureAwait(false);
}
@@ -234,7 +105,7 @@ namespace NadekoBot.Modules.Utility
[Priority(1)]
public async Task Repeat(int minutes, [Remainder] string message)
{
if (!_ready)
if (!_service.RepeaterReady)
return;
if (minutes < 1 || minutes > 10080)
return;
@@ -250,7 +121,7 @@ namespace NadekoBot.Modules.Utility
Message = message
};
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
var gc = uow.GuildConfigs.For(Context.Guild.Id, set => set.Include(x => x.GuildRepeaters));
@@ -261,9 +132,9 @@ namespace NadekoBot.Modules.Utility
await uow.CompleteAsync().ConfigureAwait(false);
}
var rep = new RepeatRunner(toAdd, (ITextChannel) Context.Channel);
var rep = new RepeatRunner(_client, toAdd, (ITextChannel) Context.Channel);
Repeaters.AddOrUpdate(Context.Guild.Id, new ConcurrentQueue<RepeatRunner>(new[] {rep}), (key, old) =>
_service.Repeaters.AddOrUpdate(Context.Guild.Id, new ConcurrentQueue<RepeatRunner>(new[] {rep}), (key, old) =>
{
old.Enqueue(rep);
return old;
@@ -282,10 +153,9 @@ namespace NadekoBot.Modules.Utility
[RequireUserPermission(GuildPermission.ManageMessages)]
public async Task RepeatList()
{
if (!_ready)
if (!_service.RepeaterReady)
return;
ConcurrentQueue<RepeatRunner> repRunners;
if (!Repeaters.TryGetValue(Context.Guild.Id, out repRunners))
if (!_service.Repeaters.TryGetValue(Context.Guild.Id, out var repRunners))
{
await ReplyConfirmLocalized("repeaters_none").ConfigureAwait(false);
return;

View File

@@ -22,16 +22,20 @@ namespace NadekoBot.Modules.Utility
[Group]
public class PatreonCommands : NadekoSubmodule
{
private static readonly PatreonThingy patreon;
//todo rename patreon thingy and move it to be a service, or a part of utility service
private readonly PatreonThingy patreon;
private readonly IBotCredentials _creds;
private readonly BotConfig _config;
private readonly DbHandler _db;
private readonly CurrencyHandler _currency;
static PatreonCommands()
public PatreonCommands(IBotCredentials creds, BotConfig config, DbHandler db, CurrencyHandler currency)
{
patreon = PatreonThingy.Instance;
}
public static void Unload()
{
patreon.Updater.Change(Timeout.Infinite, Timeout.Infinite);
_creds = creds;
_config = config;
_db = db;
_currency = currency;
patreon = PatreonThingy.GetInstance(creds, db, currency);
}
[NadekoCommand, Usage, Description, Aliases]
@@ -46,7 +50,7 @@ namespace NadekoBot.Modules.Utility
[NadekoCommand, Usage, Description, Aliases]
public async Task ClaimPatreonRewards()
{
if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.PatreonAccessToken))
if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken))
return;
if (DateTime.UtcNow.Day < 5)
{
@@ -65,11 +69,11 @@ namespace NadekoBot.Modules.Utility
if (amount > 0)
{
await ReplyConfirmLocalized("clpa_success", amount + NadekoBot.BotConfig.CurrencySign).ConfigureAwait(false);
await ReplyConfirmLocalized("clpa_success", amount + _config.CurrencySign).ConfigureAwait(false);
return;
}
var rem = (patreon.Interval - (DateTime.UtcNow - patreon.LastUpdate));
var helpcmd = Format.Code(NadekoBot.ModulePrefixes[typeof(Help.Help).Name] + "donate");
var helpcmd = Format.Code(NadekoBot.Prefix + "donate");
await Context.Channel.EmbedAsync(new EmbedBuilder().WithOkColor()
.WithDescription(GetText("clpa_fail"))
.AddField(efb => efb.WithName(GetText("clpa_fail_already_title")).WithValue(GetText("clpa_fail_already")))
@@ -83,8 +87,10 @@ namespace NadekoBot.Modules.Utility
public class PatreonThingy
{
public static PatreonThingy _instance = new PatreonThingy();
public static PatreonThingy Instance => _instance;
//todo quickly hacked while rewriting, fix this
private static PatreonThingy _instance = null;
public static PatreonThingy GetInstance(IBotCredentials creds, DbHandler db, CurrencyHandler cur)
=> _instance ?? (_instance = new PatreonThingy(creds, db, cur));
private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1);
@@ -96,10 +102,17 @@ namespace NadekoBot.Modules.Utility
private readonly Logger _log;
public readonly TimeSpan Interval = TimeSpan.FromHours(1);
private IBotCredentials _creds;
private readonly DbHandler _db;
private readonly CurrencyHandler _currency;
private PatreonThingy()
static PatreonThingy() { }
private PatreonThingy(IBotCredentials creds, DbHandler db, CurrencyHandler currency)
{
if (string.IsNullOrWhiteSpace(NadekoBot.Credentials.PatreonAccessToken))
_creds = creds;
_db = db;
_currency = currency;
if (string.IsNullOrWhiteSpace(creds.PatreonAccessToken))
return;
_log = LogManager.GetCurrentClassLogger();
Updater = new Timer(async (_) => await LoadPledges(), null, TimeSpan.Zero, Interval);
@@ -116,7 +129,7 @@ namespace NadekoBot.Modules.Utility
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Clear();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + NadekoBot.Credentials.PatreonAccessToken);
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + _creds.PatreonAccessToken);
var data = new PatreonData()
{
Links = new PatreonDataLinks()
@@ -170,7 +183,7 @@ namespace NadekoBot.Modules.Utility
var amount = data.Reward.attributes.amount_cents;
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
var users = uow._context.Set<RewardedUser>();
var usr = users.FirstOrDefault(x => x.PatreonUserId == data.User.id);
@@ -185,7 +198,7 @@ namespace NadekoBot.Modules.Utility
AmountRewardedThisMonth = amount,
});
await CurrencyHandler.AddCurrencyAsync(userId, "Patreon reward - new", amount, uow).ConfigureAwait(false);
await _currency.AddCurrencyAsync(userId, "Patreon reward - new", amount, uow).ConfigureAwait(false);
await uow.CompleteAsync().ConfigureAwait(false);
return amount;
@@ -197,7 +210,7 @@ namespace NadekoBot.Modules.Utility
usr.AmountRewardedThisMonth = amount;
usr.PatreonUserId = data.User.id;
await CurrencyHandler.AddCurrencyAsync(userId, "Patreon reward - recurring", amount, uow).ConfigureAwait(false);
await _currency.AddCurrencyAsync(userId, "Patreon reward - recurring", amount, uow).ConfigureAwait(false);
await uow.CompleteAsync().ConfigureAwait(false);
return amount;
@@ -211,7 +224,7 @@ namespace NadekoBot.Modules.Utility
usr.AmountRewardedThisMonth = amount;
usr.PatreonUserId = data.User.id;
await CurrencyHandler.AddCurrencyAsync(usr.UserId, "Patreon reward - update", toAward, uow).ConfigureAwait(false);
await _currency.AddCurrencyAsync(usr.UserId, "Patreon reward - update", toAward, uow).ConfigureAwait(false);
await uow.CompleteAsync().ConfigureAwait(false);
return toAward;

View File

@@ -17,6 +17,13 @@ namespace NadekoBot.Modules.Utility
[Group]
public class QuoteCommands : NadekoSubmodule
{
private readonly DbHandler _db;
public QuoteCommands(DbHandler db)
{
_db = db;
}
[NadekoCommand, Usage, Description, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ListQuotes(int page = 1)
@@ -27,7 +34,7 @@ namespace NadekoBot.Modules.Utility
return;
IEnumerable<Quote> quotes;
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
quotes = uow.Quotes.GetGroup(Context.Guild.Id, page * 16, 16);
}
@@ -50,7 +57,7 @@ namespace NadekoBot.Modules.Utility
keyword = keyword.ToUpperInvariant();
Quote quote;
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
quote =
await uow.Quotes.GetRandomQuoteByKeywordAsync(Context.Guild.Id, keyword).ConfigureAwait(false);
@@ -87,7 +94,7 @@ namespace NadekoBot.Modules.Utility
keyword = keyword.ToUpperInvariant();
Quote keywordquote;
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
keywordquote =
await uow.Quotes.SearchQuoteKeywordTextAsync(Context.Guild.Id, keyword, text)
@@ -108,7 +115,7 @@ namespace NadekoBot.Modules.Utility
if (id < 0)
return;
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
var qfromid = uow.Quotes.Get(id);
CREmbed crembed;
@@ -146,7 +153,7 @@ namespace NadekoBot.Modules.Utility
keyword = keyword.ToUpperInvariant();
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
uow.Quotes.Add(new Quote
{
@@ -169,7 +176,7 @@ namespace NadekoBot.Modules.Utility
var success = false;
string response;
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
var q = uow.Quotes.Get(id);
@@ -201,7 +208,7 @@ namespace NadekoBot.Modules.Utility
keyword = keyword.ToUpperInvariant();
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
uow.Quotes.RemoveAllByKeyword(Context.Guild.Id, keyword.ToUpperInvariant());

View File

@@ -19,89 +19,13 @@ namespace NadekoBot.Modules.Utility
[Group]
public class RemindCommands : NadekoSubmodule
{
readonly Regex _regex = new Regex(@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d)w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,2})h)?(?:(?<minutes>\d{1,2})m)?$",
RegexOptions.Compiled | RegexOptions.Multiline);
private readonly UtilityService _service;
private readonly DbHandler _db;
private static string remindMessageFormat { get; }
private static readonly IDictionary<string, Func<Reminder, string>> _replacements = new Dictionary<string, Func<Reminder, string>>
public RemindCommands(UtilityService service, DbHandler db)
{
{ "%message%" , (r) => r.Message },
{ "%user%", (r) => $"<@!{r.UserId}>" },
{ "%target%", (r) => r.IsPrivate ? "Direct Message" : $"<#{r.ChannelId}>"}
};
private new static readonly Logger _log;
private static readonly CancellationTokenSource cancelSource;
private static readonly CancellationToken cancelAllToken;
static RemindCommands()
{
_log = LogManager.GetCurrentClassLogger();
cancelSource = new CancellationTokenSource();
cancelAllToken = cancelSource.Token;
List<Reminder> reminders;
using (var uow = DbHandler.UnitOfWork())
{
reminders = uow.Reminders.GetAll().ToList();
}
remindMessageFormat = NadekoBot.BotConfig.RemindMessageFormat;
foreach (var r in reminders)
{
Task.Run(() => StartReminder(r, cancelAllToken));
}
}
public static void Unload()
{
if (!cancelSource.IsCancellationRequested)
cancelSource.Cancel();
}
private static async Task StartReminder(Reminder r, CancellationToken t)
{
var now = DateTime.Now;
var time = r.When - now;
if (time.TotalMilliseconds > int.MaxValue)
return;
await Task.Delay(time, t).ConfigureAwait(false);
try
{
IMessageChannel ch;
if (r.IsPrivate)
{
var user = NadekoBot.Client.GetGuild(r.ServerId).GetUser(r.ChannelId);
if(user == null)
return;
ch = await user.CreateDMChannelAsync().ConfigureAwait(false);
}
else
{
ch = NadekoBot.Client.GetGuild(r.ServerId)?.GetTextChannel(r.ChannelId);
}
if (ch == null)
return;
await ch.SendMessageAsync(
_replacements.Aggregate(remindMessageFormat,
(cur, replace) => cur.Replace(replace.Key, replace.Value(r)))
.SanitizeMentions()
).ConfigureAwait(false); //it works trust me
}
catch (Exception ex) { _log.Warn(ex); }
finally
{
using (var uow = DbHandler.UnitOfWork())
{
uow.Reminders.Remove(r);
await uow.CompleteAsync();
}
}
_service = service;
_db = db;
}
public enum MeOrHere
@@ -139,7 +63,7 @@ namespace NadekoBot.Modules.Utility
public async Task RemindInternal(ulong targetId, bool isPrivate, string timeStr, [Remainder] string message)
{
var m = _regex.Match(timeStr);
var m = _service.Remind.Regex.Match(timeStr);
if (m.Length == 0)
{
@@ -150,7 +74,7 @@ namespace NadekoBot.Modules.Utility
string output = "";
var namesAndValues = new Dictionary<string, int>();
foreach (var groupName in _regex.GetGroupNames())
foreach (var groupName in _service.Remind.Regex.GetGroupNames())
{
if (groupName == "0") continue;
int value;
@@ -191,7 +115,7 @@ namespace NadekoBot.Modules.Utility
ServerId = Context.Guild.Id
};
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
uow.Reminders.Add(rem);
await uow.CompleteAsync();
@@ -210,7 +134,7 @@ namespace NadekoBot.Modules.Utility
{
// ignored
}
await StartReminder(rem, cancelAllToken);
await _service.Remind.StartReminder(rem);
}
[NadekoCommand, Usage, Description, Aliases]
@@ -220,7 +144,7 @@ namespace NadekoBot.Modules.Utility
if (string.IsNullOrWhiteSpace(arg))
return;
using (var uow = DbHandler.UnitOfWork())
using (var uow = _db.UnitOfWork)
{
uow.BotConfig.GetOrCreate().RemindMessageFormat = arg.Trim();
await uow.CompleteAsync().ConfigureAwait(false);

View File

@@ -2,17 +2,8 @@
using Discord.Commands;
using NadekoBot.Attributes;
using NadekoBot.Extensions;
using NadekoBot.Modules.Utility.Commands.Models;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using Newtonsoft.Json;
using NLog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Utility
@@ -22,108 +13,17 @@ namespace NadekoBot.Modules.Utility
[Group]
public class UnitConverterCommands : NadekoSubmodule
{
public static List<ConvertUnit> Units { get; set; } = new List<ConvertUnit>();
private new static readonly Logger _log;
private static Timer _timer;
private static readonly TimeSpan _updateInterval = new TimeSpan(12, 0, 0);
private readonly UtilityService _service;
static UnitConverterCommands()
public UnitConverterCommands(UtilityService service)
{
_log = LogManager.GetCurrentClassLogger();
try
{
var data = JsonConvert.DeserializeObject<List<MeasurementUnit>>(File.ReadAllText("data/units.json")).Select(u => new ConvertUnit()
{
Modifier = u.Modifier,
UnitType = u.UnitType,
InternalTrigger = string.Join("|", u.Triggers)
}).ToArray();
using (var uow = DbHandler.UnitOfWork())
{
if (uow.ConverterUnits.Empty())
{
uow.ConverterUnits.AddRange(data);
uow.Complete();
}
}
Units = data.ToList();
}
catch (Exception ex)
{
_log.Warn("Could not load units: " + ex.Message);
}
_timer = new Timer(async (obj) => await UpdateCurrency(), null, _updateInterval, _updateInterval);
_service = service;
}
public static void Unload()
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
public static async Task UpdateCurrency()
{
try
{
var currencyRates = await UpdateCurrencyRates();
var unitTypeString = "currency";
var range = currencyRates.ConversionRates.Select(u => new ConvertUnit()
{
InternalTrigger = u.Key,
Modifier = u.Value,
UnitType = unitTypeString
}).ToArray();
var baseType = new ConvertUnit()
{
Triggers = new[] { currencyRates.Base },
Modifier = decimal.One,
UnitType = unitTypeString
};
var toRemove = Units.Where(u => u.UnitType == unitTypeString);
using (var uow = DbHandler.UnitOfWork())
{
uow.ConverterUnits.RemoveRange(toRemove.ToArray());
uow.ConverterUnits.Add(baseType);
uow.ConverterUnits.AddRange(range);
await uow.CompleteAsync().ConfigureAwait(false);
}
Units.RemoveAll(u => u.UnitType == unitTypeString);
Units.Add(baseType);
Units.AddRange(range);
_log.Info("Updated Currency");
}
catch
{
_log.Warn("Failed updating currency. Ignore this.");
}
}
//[NadekoCommand, Usage, Description, Aliases]
//[RequireContext(ContextType.Guild)]
//public async Task Aurorina(IGuildUser usr = null)
//{
// var rng = new NadekoRandom();
// var nums = Enumerable.Range(48, 10)
// .Concat(Enumerable.Range(65, 26))
// .Concat(Enumerable.Range(97, 26))
// .Concat(new[] {45, 46, 95})
// .ToArray();
// var token = String.Concat(new int[59]
// .Select(x => (char) nums[rng.Next(0, nums.Length)]));
// if (usr == null)
// await Context.Channel.SendConfirmAsync(token).ConfigureAwait(false);
// else
// await Context.Channel.SendConfirmAsync($"Token of user {usr} is `{token}`").ConfigureAwait(false);
//}
[NadekoCommand, Usage, Description, Aliases]
public async Task ConvertList()
{
var res = Units.GroupBy(x => x.UnitType)
var res = _service.Converter.Units.GroupBy(x => x.UnitType)
.Aggregate(new EmbedBuilder().WithTitle(GetText("convertlist"))
.WithColor(NadekoBot.OkColor),
(embed, g) => embed.AddField(efb =>
@@ -135,8 +35,8 @@ namespace NadekoBot.Modules.Utility
[NadekoCommand, Usage, Description, Aliases]
public async Task Convert(string origin, string target, decimal value)
{
var originUnit = Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(origin.ToLowerInvariant()));
var targetUnit = Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(target.ToLowerInvariant()));
var originUnit = _service.Converter.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(origin.ToLowerInvariant()));
var targetUnit = _service.Converter.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(target.ToLowerInvariant()));
if (originUnit == null || targetUnit == null)
{
await ReplyErrorLocalized("convert_not_found", Format.Bold(origin), Format.Bold(target)).ConfigureAwait(false);
@@ -189,14 +89,5 @@ namespace NadekoBot.Modules.Utility
await Context.Channel.SendConfirmAsync(GetText("convert", value, (originUnit.Triggers.First()).SnPl(value.IsInteger() ? (int)value : 2), res, (targetUnit.Triggers.First() + "s").SnPl(res.IsInteger() ? (int)res : 2)));
}
}
public static async Task<Rates> UpdateCurrencyRates()
{
using (var http = new HttpClient())
{
var res = await http.GetStringAsync("http://api.fixer.io/latest").ConfigureAwait(false);
return JsonConvert.DeserializeObject<Rates>(res);
}
}
}
}

View File

@@ -1,11 +0,0 @@
using System.Collections.Generic;
namespace NadekoBot.Modules.Utility.Commands.Models
{
public class MeasurementUnit
{
public List<string> Triggers { get; set; }
public string UnitType { get; set; }
public decimal Modifier { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace NadekoBot.Modules.Utility.Commands.Models
{
public class Rates
{
public string Base { get; set; }
public DateTime Date { get; set; }
[JsonProperty("rates")]
public Dictionary<string, decimal> ConversionRates { get; set; }
}
}

View File

@@ -0,0 +1,114 @@
using Discord;
using Discord.Net;
using Discord.WebSocket;
using Microsoft.Extensions.Logging;
using NadekoBot.Extensions;
using NadekoBot.Services.Database.Models;
using NLog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Utility.Models
{
public class RepeatRunner
{
private readonly Logger _log;
private CancellationTokenSource source { get; set; }
private CancellationToken token { get; set; }
public Repeater Repeater { get; }
public SocketGuild Guild { get; }
public ITextChannel Channel { get; private set; }
private IUserMessage oldMsg = null;
public RepeatRunner(DiscordShardedClient client, Repeater repeater, ITextChannel channel = null)
{
_log = LogManager.GetCurrentClassLogger();
Repeater = repeater;
Channel = channel;
//todo @.@ fix all of this
Guild = client.GetGuild(repeater.GuildId);
if (Guild != null)
Task.Run(Run);
}
private async Task Run()
{
source = new CancellationTokenSource();
token = source.Token;
try
{
while (!token.IsCancellationRequested)
{
await Task.Delay(Repeater.Interval, token).ConfigureAwait(false);
await Trigger().ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
}
public async Task Trigger()
{
var toSend = "🔄 " + Repeater.Message;
//var lastMsgInChannel = (await Channel.GetMessagesAsync(2)).FirstOrDefault();
// if (lastMsgInChannel.Id == oldMsg?.Id) //don't send if it's the same message in the channel
// continue;
if (oldMsg != null)
try
{
await oldMsg.DeleteAsync();
}
catch
{
// ignored
}
try
{
if (Channel == null)
Channel = Guild.GetTextChannel(Repeater.ChannelId);
if (Channel != null)
oldMsg = await Channel.SendMessageAsync(toSend.SanitizeMentions()).ConfigureAwait(false);
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
_log.Warn("Missing permissions. Repeater stopped. ChannelId : {0}", Channel?.Id);
return;
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
{
_log.Warn("Channel not found. Repeater stopped. ChannelId : {0}", Channel?.Id);
return;
}
catch (Exception ex)
{
_log.Warn(ex);
}
}
public void Reset()
{
source.Cancel();
var _ = Task.Run(Run);
}
public void Stop()
{
source.Cancel();
}
public override string ToString() =>
$"{Channel?.Mention ?? $"<#{Repeater.ChannelId}>" } " +
$"| {(int)Repeater.Interval.TotalHours}:{Repeater.Interval:mm} " +
$"| {Repeater.Message.TrimTo(33)}";
}
}

View File

@@ -20,15 +20,23 @@ using System.Diagnostics;
namespace NadekoBot.Modules.Utility
{
[NadekoModule("Utility", ".")]
[NadekoModule("Utility")]
public partial class Utility : NadekoTopLevelModule
{
private static ConcurrentDictionary<ulong, Timer> _rotatingRoleColors = new ConcurrentDictionary<ulong, Timer>();
private readonly DiscordShardedClient _client;
private readonly IStatsService _stats;
private readonly UtilityService _service;
//private readonly MusicService _music;
private readonly IBotCredentials _creds;
public static void Unload()
public Utility(UtilityService service, DiscordShardedClient client, IStatsService stats, IBotCredentials creds)
{
_rotatingRoleColors.ForEach(x => x.Value?.Change(Timeout.Infinite, Timeout.Infinite));
_rotatingRoleColors.Clear();
_client = client;
_stats = stats;
_service = service;
//_music = music;
_creds = creds;
}
//[NadekoCommand, Usage, Description, Aliases]
@@ -355,11 +363,11 @@ namespace NadekoBot.Modules.Utility
if (page < 1)
return;
var status = string.Join(", ", NadekoBot.Client.Shards.GroupBy(x => x.ConnectionState)
var status = string.Join(", ", _client.Shards.GroupBy(x => x.ConnectionState)
.Select(x => $"{x.Count()} {x.Key}")
.ToArray());
var allShardStrings = NadekoBot.Client.Shards
var allShardStrings = _client.Shards
.Select(x =>
GetText("shard_stats_txt", x.ShardId.ToString(),
Format.Bold(x.ConnectionState.ToString()), Format.Bold(x.Guilds.Count.ToString())))
@@ -367,7 +375,7 @@ namespace NadekoBot.Modules.Utility
await Context.Channel.SendPaginatedConfirmAsync(page, (curPage) =>
await Context.Channel.SendPaginatedConfirmAsync(_client, page, (curPage) =>
{
var str = string.Join("\n", allShardStrings.Skip(25 * (curPage - 1)).Take(25));
@@ -386,7 +394,7 @@ namespace NadekoBot.Modules.Utility
[NadekoCommand, Usage, Description, Aliases]
public async Task ShardId(IGuild guild)
{
var shardId = NadekoBot.Client.GetShardIdFor(guild);
var shardId = _client.GetShardIdFor(guild);
await Context.Channel.SendConfirmAsync(shardId.ToString()).ConfigureAwait(false);
}
@@ -394,10 +402,8 @@ namespace NadekoBot.Modules.Utility
[NadekoCommand, Usage, Description, Aliases]
public async Task Stats()
{
var stats = NadekoBot.Stats;
var shardId = Context.Guild != null
? NadekoBot.Client.GetShardIdFor(Context.Guild)
? _client.GetShardIdFor(Context.Guild)
: 0;
await Context.Channel.EmbedAsync(
@@ -405,21 +411,21 @@ namespace NadekoBot.Modules.Utility
.WithAuthor(eab => eab.WithName($"NadekoBot v{StatsService.BotVersion}")
.WithUrl("http://nadekobot.readthedocs.io/en/latest/")
.WithIconUrl("https://cdn.discordapp.com/avatars/116275390695079945/b21045e778ef21c96d175400e779f0fb.jpg"))
.AddField(efb => efb.WithName(GetText("author")).WithValue(stats.Author).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("botid")).WithValue(NadekoBot.Client.CurrentUser.Id.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("shard")).WithValue($"#{shardId} / {NadekoBot.Client.Shards.Count}").WithIsInline(true))
.AddField(efb => efb.WithName(GetText("commands_ran")).WithValue(stats.CommandsRan.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("messages")).WithValue($"{stats.MessageCounter} ({stats.MessagesPerSecond:F2}/sec)").WithIsInline(true))
.AddField(efb => efb.WithName(GetText("memory")).WithValue($"{stats.Heap} MB").WithIsInline(true))
.AddField(efb => efb.WithName(GetText("owner_ids")).WithValue(string.Join("\n", NadekoBot.Credentials.OwnerIds)).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("uptime")).WithValue(stats.GetUptimeString("\n")).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("author")).WithValue(_stats.Author).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("botid")).WithValue(_client.CurrentUser.Id.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("shard")).WithValue($"#{shardId} / {_client.Shards.Count}").WithIsInline(true))
.AddField(efb => efb.WithName(GetText("commands_ran")).WithValue(_stats.CommandsRan.ToString()).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("messages")).WithValue($"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)").WithIsInline(true))
.AddField(efb => efb.WithName(GetText("memory")).WithValue($"{_stats.Heap} MB").WithIsInline(true))
.AddField(efb => efb.WithName(GetText("owner_ids")).WithValue(string.Join("\n", _creds.OwnerIds)).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("uptime")).WithValue(_stats.GetUptimeString("\n")).WithIsInline(true))
.AddField(efb => efb.WithName(GetText("presence")).WithValue(
GetText("presence_txt",
NadekoBot.Client.Guilds.Count, stats.TextChannels, stats.VoiceChannels)).WithIsInline(true))
_client.Guilds.Count, _stats.TextChannels, _stats.VoiceChannels)).WithIsInline(true))
#if !GLOBAL_NADEKO
.WithFooter(efb => efb.WithText(GetText("stats_songs",
NadekoBot.MusicService.MusicPlayers.Count(mp => mp.Value.CurrentSong != null),
NadekoBot.MusicService.MusicPlayers.Sum(mp => mp.Value.Playlist.Count))))
//.WithFooter(efb => efb.WithText(GetText("stats_songs",
// _music.MusicPlayers.Count(mp => mp.Value.CurrentSong != null),
// _music.MusicPlayers.Sum(mp => mp.Value.Playlist.Count))))
#endif
);
}
@@ -427,7 +433,7 @@ namespace NadekoBot.Modules.Utility
[NadekoCommand, Usage, Description, Aliases]
public async Task Showemojis([Remainder] string emojis)
{
var tags = Context.Message.Tags.Where(t => t.Type == TagType.Emoji).Select(t => (Emoji)t.Value);
var tags = Context.Message.Tags.Where(t => t.Type == TagType.Emoji).Select(t => (Emote)t.Value);
var result = string.Join("\n", tags.Select(m => GetText("showemojis", m, m.Url)));
@@ -446,7 +452,7 @@ namespace NadekoBot.Modules.Utility
if (page < 0)
return;
var guilds = await Task.Run(() => NadekoBot.Client.Guilds.OrderBy(g => g.Name).Skip((page - 1) * 15).Take(15)).ConfigureAwait(false);
var guilds = await Task.Run(() => _client.Guilds.OrderBy(g => g.Name).Skip((page - 1) * 15).Take(15)).ConfigureAwait(false);
if (!guilds.Any())
{

View File

@@ -0,0 +1,319 @@
using Discord;
using Discord.WebSocket;
using NadekoBot.Extensions;
using NadekoBot.Modules.Utility.Models;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using Newtonsoft.Json;
using NLog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Utility
{
public class UtilityService
{
public ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>> AliasMaps { get; } = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>();
//messagerepeater
//guildid/RepeatRunners
public ConcurrentDictionary<ulong, ConcurrentQueue<RepeatRunner>> Repeaters { get; set; }
public bool RepeaterReady { get; private set; }
//remind
public RemindService Remind { get; }
//unit conversion
public ConverterService Converter { get; }
public UtilityService(IEnumerable<GuildConfig> guildConfigs, DiscordShardedClient client, BotConfig config, DbHandler db)
{
//commandmap
AliasMaps = new ConcurrentDictionary<ulong, ConcurrentDictionary<string, string>>(
guildConfigs.ToDictionary(
x => x.GuildId,
x => new ConcurrentDictionary<string, string>(x.CommandAliases
.Distinct(new CommandAliasEqualityComparer())
.ToDictionary(ca => ca.Trigger, ca => ca.Mapping))));
//crossesrver
_client = client;
_client.MessageReceived += Client_MessageReceived;
//messagerepeater
var _ = Task.Run(async () =>
{
#if !GLOBAL_NADEKO
await Task.Delay(5000).ConfigureAwait(false);
#else
await Task.Delay(30000).ConfigureAwait(false);
#endif
//todo this is pretty terrible :kms: no time
Repeaters = new ConcurrentDictionary<ulong, ConcurrentQueue<RepeatRunner>>(guildConfigs
.ToDictionary(gc => gc.GuildId,
gc => new ConcurrentQueue<RepeatRunner>(gc.GuildRepeaters
.Select(gr => new RepeatRunner(client, gr))
.Where(x => x.Guild != null))));
RepeaterReady = true;
});
//reminder
Remind = new RemindService(client, config, db);
//unit converter
Converter = new ConverterService(db);
}
private async Task Client_MessageReceived(Discord.WebSocket.SocketMessage imsg)
{
try
{
if (imsg.Author.IsBot)
return;
var msg = imsg as IUserMessage;
if (msg == null)
return;
var channel = imsg.Channel as ITextChannel;
if (channel == null)
return;
if (msg.Author.Id == _client.CurrentUser.Id) return;
foreach (var subscriber in Subscribers)
{
var set = subscriber.Value;
if (!set.Contains(channel))
continue;
foreach (var chan in set.Except(new[] { channel }))
{
try
{
await chan.SendMessageAsync(GetMessage(channel, (IGuildUser)msg.Author,
msg)).ConfigureAwait(false);
}
catch
{
// ignored
}
}
}
}
catch
{
// ignored
}
}
private string GetMessage(ITextChannel channel, IGuildUser user, IUserMessage message) =>
$"**{channel.Guild.Name} | {channel.Name}** `{user.Username}`: " + message.Content.SanitizeMentions();
public readonly ConcurrentDictionary<int, ConcurrentHashSet<ITextChannel>> Subscribers =
new ConcurrentDictionary<int, ConcurrentHashSet<ITextChannel>>();
private DiscordShardedClient _client;
}
public class ConverterService
{
public class MeasurementUnit
{
public List<string> Triggers { get; set; }
public string UnitType { get; set; }
public decimal Modifier { get; set; }
}
public class Rates
{
public string Base { get; set; }
public DateTime Date { get; set; }
[JsonProperty("rates")]
public Dictionary<string, decimal> ConversionRates { get; set; }
}
public List<ConvertUnit> Units { get; set; } = new List<ConvertUnit>();
private readonly Logger _log;
private Timer _timer;
private readonly TimeSpan _updateInterval = new TimeSpan(12, 0, 0);
private readonly DbHandler _db;
public ConverterService(DbHandler db)
{
_log = LogManager.GetCurrentClassLogger();
_db = db;
try
{
var data = JsonConvert.DeserializeObject<List<MeasurementUnit>>(File.ReadAllText("data/units.json")).Select(u => new ConvertUnit()
{
Modifier = u.Modifier,
UnitType = u.UnitType,
InternalTrigger = string.Join("|", u.Triggers)
}).ToArray();
using (var uow = _db.UnitOfWork)
{
if (uow.ConverterUnits.Empty())
{
uow.ConverterUnits.AddRange(data);
uow.Complete();
}
}
Units = data.ToList();
}
catch (Exception ex)
{
_log.Warn("Could not load units: " + ex.Message);
}
_timer = new Timer(async (obj) => await UpdateCurrency(), null, _updateInterval, _updateInterval);
}
public static async Task<Rates> UpdateCurrencyRates()
{
using (var http = new HttpClient())
{
var res = await http.GetStringAsync("http://api.fixer.io/latest").ConfigureAwait(false);
return JsonConvert.DeserializeObject<Rates>(res);
}
}
public async Task UpdateCurrency()
{
try
{
var currencyRates = await UpdateCurrencyRates();
var unitTypeString = "currency";
var range = currencyRates.ConversionRates.Select(u => new ConvertUnit()
{
InternalTrigger = u.Key,
Modifier = u.Value,
UnitType = unitTypeString
}).ToArray();
var baseType = new ConvertUnit()
{
Triggers = new[] { currencyRates.Base },
Modifier = decimal.One,
UnitType = unitTypeString
};
var toRemove = Units.Where(u => u.UnitType == unitTypeString);
using (var uow = _db.UnitOfWork)
{
uow.ConverterUnits.RemoveRange(toRemove.ToArray());
uow.ConverterUnits.Add(baseType);
uow.ConverterUnits.AddRange(range);
await uow.CompleteAsync().ConfigureAwait(false);
}
Units.RemoveAll(u => u.UnitType == unitTypeString);
Units.Add(baseType);
Units.AddRange(range);
_log.Info("Updated Currency");
}
catch
{
_log.Warn("Failed updating currency. Ignore this.");
}
}
}
public class RemindService
{
public readonly Regex Regex = new Regex(@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d)w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,2})h)?(?:(?<minutes>\d{1,2})m)?$",
RegexOptions.Compiled | RegexOptions.Multiline);
public string RemindMessageFormat { get; }
public readonly IDictionary<string, Func<Reminder, string>> _replacements = new Dictionary<string, Func<Reminder, string>>
{
{ "%message%" , (r) => r.Message },
{ "%user%", (r) => $"<@!{r.UserId}>" },
{ "%target%", (r) => r.IsPrivate ? "Direct Message" : $"<#{r.ChannelId}>"}
};
private readonly Logger _log;
private readonly CancellationTokenSource cancelSource;
private readonly CancellationToken cancelAllToken;
private readonly BotConfig _config;
private readonly DiscordShardedClient _client;
private readonly DbHandler _db;
public RemindService(DiscordShardedClient client, BotConfig config, DbHandler db)
{
_config = config;
_client = client;
_log = LogManager.GetCurrentClassLogger();
_db = db;
cancelSource = new CancellationTokenSource();
cancelAllToken = cancelSource.Token;
List<Reminder> reminders;
using (var uow = _db.UnitOfWork)
{
reminders = uow.Reminders.GetAll().ToList();
}
RemindMessageFormat = _config.RemindMessageFormat;
foreach (var r in reminders)
{
Task.Run(() => StartReminder(r));
}
}
public async Task StartReminder(Reminder r)
{
var t = cancelAllToken;
var now = DateTime.Now;
var time = r.When - now;
if (time.TotalMilliseconds > int.MaxValue)
return;
await Task.Delay(time, t).ConfigureAwait(false);
try
{
IMessageChannel ch;
if (r.IsPrivate)
{
var user = _client.GetGuild(r.ServerId).GetUser(r.ChannelId);
if (user == null)
return;
ch = await user.CreateDMChannelAsync().ConfigureAwait(false);
}
else
{
ch = _client.GetGuild(r.ServerId)?.GetTextChannel(r.ChannelId);
}
if (ch == null)
return;
await ch.SendMessageAsync(
_replacements.Aggregate(RemindMessageFormat,
(cur, replace) => cur.Replace(replace.Key, replace.Value(r)))
.SanitizeMentions()
).ConfigureAwait(false); //it works trust me
}
catch (Exception ex) { _log.Warn(ex); }
finally
{
using (var uow = _db.UnitOfWork)
{
uow.Reminders.Remove(r);
await uow.CompleteAsync();
}
}
}
}
public class CommandAliasEqualityComparer : IEqualityComparer<CommandAlias>
{
public bool Equals(CommandAlias x, CommandAlias y) => x.Trigger == y.Trigger;
public int GetHashCode(CommandAlias obj) => obj.Trigger.GetHashCode();
}
}