Music Commands
This commit is contained in:
@@ -6,13 +6,13 @@
|
|||||||
"compilationOptions": {},
|
"compilationOptions": {},
|
||||||
"targets": {
|
"targets": {
|
||||||
".NETCoreApp,Version=v6.0": {
|
".NETCoreApp,Version=v6.0": {
|
||||||
"MusicCommands/1.0.0": {
|
"Music Commands/1.0.0": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"PluginManager": "1.0.0",
|
"PluginManager": "1.0.0",
|
||||||
"YoutubeExplode": "6.2.0"
|
"YoutubeExplode": "6.2.0"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"MusicCommands.dll": {}
|
"Music Commands.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AngleSharp/0.17.0": {
|
"AngleSharp/0.17.0": {
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"libraries": {
|
"libraries": {
|
||||||
"MusicCommands/1.0.0": {
|
"Music Commands/1.0.0": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
BIN
BUILDS/net6.0/Music Commands.dll
Normal file
BIN
BUILDS/net6.0/Music Commands.dll
Normal file
Binary file not shown.
Binary file not shown.
@@ -4,7 +4,7 @@
|
|||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<BaseOutputPath>bin\</BaseOutputPath>
|
<BaseOutputPath></BaseOutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
34
MusicCommands/AudioFile.cs
Normal file
34
MusicCommands/AudioFile.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using AngleSharp.Dom;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MusicCommands
|
||||||
|
{
|
||||||
|
internal class AudioFile
|
||||||
|
{
|
||||||
|
internal string Name { get; set; }
|
||||||
|
internal string Url { get; set; }
|
||||||
|
|
||||||
|
internal AudioFile(string name, string url)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task DownloadAudioFile()
|
||||||
|
{
|
||||||
|
Process proc = new Process();
|
||||||
|
proc.StartInfo.FileName = "MusicDownloader.exe";
|
||||||
|
proc.StartInfo.Arguments = $"{Url},{Name}";
|
||||||
|
proc.StartInfo.UseShellExecute = false;
|
||||||
|
proc.StartInfo.RedirectStandardOutput = true;
|
||||||
|
|
||||||
|
proc.Start();
|
||||||
|
await proc.WaitForExitAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,5 +8,6 @@ internal static class Data
|
|||||||
internal static IAudioClient audioClient = null;
|
internal static IAudioClient audioClient = null;
|
||||||
internal static IVoiceChannel voiceChannel = null;
|
internal static IVoiceChannel voiceChannel = null;
|
||||||
|
|
||||||
internal static MusicPlayer CurrentlyRunning = null;
|
internal static MusicPlayer MusicPlayer = null;
|
||||||
|
internal static MusicPlaylist Playlist = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ internal class Leave : DBCommand
|
|||||||
{
|
{
|
||||||
if (Data.audioClient is not null && Data.voiceChannel is not null)
|
if (Data.audioClient is not null && Data.voiceChannel is not null)
|
||||||
{
|
{
|
||||||
Data.CurrentlyRunning.Stop();
|
Data.Playlist.ClearQueue();
|
||||||
Data.CurrentlyRunning = null;
|
Data.MusicPlayer.isPlaying = false;
|
||||||
await Data.audioClient.StopAsync();
|
await Data.audioClient.StopAsync();
|
||||||
await Data.voiceChannel.DisconnectAsync();
|
await Data.voiceChannel.DisconnectAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Nullable>warnings</Nullable>
|
<Nullable>warnings</Nullable>
|
||||||
<BaseOutputPath>bin\</BaseOutputPath>
|
<BaseOutputPath>bin\</BaseOutputPath>
|
||||||
|
<AssemblyName>Music Commands</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
|||||||
@@ -9,17 +9,56 @@ namespace MusicCommands;
|
|||||||
|
|
||||||
internal class MusicPlayer
|
internal class MusicPlayer
|
||||||
{
|
{
|
||||||
|
private Stream outputStream { get; }
|
||||||
|
|
||||||
|
internal bool isPlaying, isPaused;
|
||||||
|
|
||||||
|
public MusicPlayer(Stream outputChannel)
|
||||||
|
{
|
||||||
|
outputStream = outputChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Play(Stream source, int byteSize)
|
||||||
|
{
|
||||||
|
isPlaying = true;
|
||||||
|
while (isPlaying)
|
||||||
|
{
|
||||||
|
if (isPaused)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var bits = new byte[byteSize];
|
||||||
|
var read = await source.ReadAsync(bits, 0, byteSize);
|
||||||
|
if (read == 0)
|
||||||
|
break;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await outputStream.WriteAsync(bits, 0, read);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await source.FlushAsync();
|
||||||
|
await source.DisposeAsync();
|
||||||
|
source.Close();
|
||||||
|
await outputStream.FlushAsync();
|
||||||
|
isPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
public MusicPlayer(Stream input, Stream output)
|
public MusicPlayer(Stream input, Stream output)
|
||||||
{
|
{
|
||||||
inputStream = input;
|
inputStream = input;
|
||||||
outputStream = output;
|
outputStream = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MusicPlayer(Stream output)
|
|
||||||
{
|
|
||||||
inputStream = null;
|
|
||||||
outputStream = output;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stream inputStream { get; } // from FFMPEG
|
public Stream inputStream { get; } // from FFMPEG
|
||||||
public Stream outputStream { get; } // to Voice Channel
|
public Stream outputStream { get; } // to Voice Channel
|
||||||
@@ -32,69 +71,13 @@ internal class MusicPlayer
|
|||||||
_stop = true;
|
_stop = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartSendAudioFromLink(string URL)
|
public async Task StartSendAudio(int bsize)
|
||||||
{
|
|
||||||
/* using (HttpClient client = new HttpClient())
|
|
||||||
using (HttpResponseMessage response = await client.GetAsync(URL))
|
|
||||||
using (var content = response.Content)
|
|
||||||
{
|
|
||||||
await (await content.ReadAsStreamAsync()).CopyToAsync(outputStream);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
Stream ms = new MemoryStream();
|
|
||||||
var bsize = 512;
|
|
||||||
new Thread(async delegate(object o)
|
|
||||||
{
|
|
||||||
var response = await new HttpClient().GetAsync(URL);
|
|
||||||
using (var stream = await response.Content.ReadAsStreamAsync())
|
|
||||||
{
|
|
||||||
var buffer = new byte[bsize];
|
|
||||||
int read;
|
|
||||||
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
|
|
||||||
{
|
|
||||||
var pos = ms.Position;
|
|
||||||
ms.Position = ms.Length;
|
|
||||||
ms.Write(buffer, 0, read);
|
|
||||||
ms.Position = pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).Start();
|
|
||||||
Console.Write("Reading data: ");
|
|
||||||
while (ms.Length < bsize * 10)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
Console.Title = "Reading data: " + ms.Length + " bytes read of " + bsize * 10;
|
|
||||||
Console.Write(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("\nDone");
|
|
||||||
ms.Position = 0;
|
|
||||||
|
|
||||||
_stop = false;
|
|
||||||
Paused = false;
|
|
||||||
|
|
||||||
while (!_stop)
|
|
||||||
{
|
|
||||||
if (Paused) continue;
|
|
||||||
var buffer = new byte[bsize];
|
|
||||||
var read = await ms.ReadAsync(buffer, 0, buffer.Length);
|
|
||||||
if (read > 0)
|
|
||||||
await outputStream.WriteAsync(buffer, 0, read);
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StartSendAudio()
|
|
||||||
{
|
{
|
||||||
Paused = false;
|
Paused = false;
|
||||||
_stop = false;
|
_stop = false;
|
||||||
while (!_stop)
|
while (!_stop)
|
||||||
{
|
{
|
||||||
if (Paused) continue;
|
if (Paused) continue;
|
||||||
var bsize = 512;
|
|
||||||
var buffer = new byte[bsize];
|
var buffer = new byte[bsize];
|
||||||
var bcount = await inputStream.ReadAsync(buffer, 0, bsize);
|
var bcount = await inputStream.ReadAsync(buffer, 0, bsize);
|
||||||
if (bcount <= 0)
|
if (bcount <= 0)
|
||||||
@@ -114,5 +97,5 @@ internal class MusicPlayer
|
|||||||
Functions.WriteLogFile(ex.ToString());
|
Functions.WriteLogFile(ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|||||||
25
MusicCommands/MusicPlaylist.cs
Normal file
25
MusicCommands/MusicPlaylist.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MusicCommands
|
||||||
|
{
|
||||||
|
internal class MusicPlaylist
|
||||||
|
{
|
||||||
|
internal MusicPlaylist()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Initialized playlist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Queue<AudioFile> QueueList = new();
|
||||||
|
|
||||||
|
public void Enqueue(AudioFile query) => QueueList.Enqueue(query);
|
||||||
|
public void ClearQueue() => QueueList.Clear();
|
||||||
|
|
||||||
|
public int Count => QueueList.Count;
|
||||||
|
public AudioFile GetNextSong => QueueList.Dequeue();
|
||||||
|
public AudioFile WhatIsNext => QueueList.Peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ internal class Pause : DBCommand
|
|||||||
{
|
{
|
||||||
public string Command => "pause";
|
public string Command => "pause";
|
||||||
|
|
||||||
public string Description => "Pause the music";
|
public string Description => "Pause/Unpause the music that is currently running";
|
||||||
|
|
||||||
public string Usage => "pause";
|
public string Usage => "pause";
|
||||||
|
|
||||||
@@ -20,6 +20,6 @@ internal class Pause : DBCommand
|
|||||||
|
|
||||||
public void Execute(SocketCommandContext context, SocketMessage message, DiscordSocketClient client, bool isDM)
|
public void Execute(SocketCommandContext context, SocketMessage message, DiscordSocketClient client, bool isDM)
|
||||||
{
|
{
|
||||||
Data.CurrentlyRunning.Paused = true;
|
Data.MusicPlayer.isPaused = !Data.MusicPlayer.isPaused;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Discord;
|
using Discord;
|
||||||
using Discord.Audio;
|
using Discord.Audio;
|
||||||
using Discord.Commands;
|
using Discord.Commands;
|
||||||
@@ -42,28 +41,47 @@ internal class Play : DBCommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string url = splitted[1];
|
var url = splitted[1];
|
||||||
path += $"{Functions.CreateMD5(url)}";
|
path += $"{Functions.CreateMD5(url)}";
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
break;
|
{
|
||||||
//await context.Channel.SendMessageAsync("Searching for " + url);
|
Data.Playlist.Enqueue(new AudioFile(path, null));
|
||||||
await GetMusicAudio(url, path);
|
|
||||||
//await context.Channel.SendMessageAsync("Playing: " + url);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string searchString = Functions.MergeStrings(splitted, 1);
|
var file = new AudioFile(path, url);
|
||||||
|
await file.DownloadAudioFile();
|
||||||
|
Data.Playlist.Enqueue(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var searchString = splitted.MergeStrings(1);
|
||||||
path += $"{Functions.CreateMD5(searchString)}";
|
path += $"{Functions.CreateMD5(searchString)}";
|
||||||
|
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
break;
|
{
|
||||||
|
Data.Playlist.Enqueue(new AudioFile(path, null));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
await context.Channel.SendMessageAsync("Searching for " + searchString);
|
await context.Channel.SendMessageAsync("Searching for " + searchString);
|
||||||
await GetMusicAudio(searchString, path);
|
var file = new AudioFile(path, searchString);
|
||||||
|
await file.DownloadAudioFile();
|
||||||
|
Data.Playlist.Enqueue(file);
|
||||||
|
if (Data.MusicPlayer is null)
|
||||||
await context.Channel.SendMessageAsync("Playing: " + searchString);
|
await context.Channel.SendMessageAsync("Playing: " + searchString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Data.MusicPlayer is not null)
|
||||||
|
{
|
||||||
|
await context.Channel.SendMessageAsync("Enqueued your request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
while (false);
|
while (false);
|
||||||
|
|
||||||
|
|
||||||
Data.voiceChannel = (context.User as IGuildUser)?.VoiceChannel;
|
Data.voiceChannel = (context.User as IGuildUser)?.VoiceChannel;
|
||||||
|
|
||||||
if (Data.voiceChannel == null)
|
if (Data.voiceChannel == null)
|
||||||
@@ -72,16 +90,29 @@ internal class Play : DBCommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.audioClient = await Data.voiceChannel.ConnectAsync(true);
|
if (Data.audioClient is null)
|
||||||
|
|
||||||
|
|
||||||
using (var ffmpeg = CreateStream(path))
|
|
||||||
using (var output = ffmpeg.StandardOutput.BaseStream)
|
|
||||||
using (var discord = Data.audioClient.CreatePCMStream(AudioApplication.Mixed))
|
|
||||||
{
|
{
|
||||||
if (Data.CurrentlyRunning != null) Data.CurrentlyRunning.Stop();
|
Data.audioClient = await Data.voiceChannel.ConnectAsync(true);
|
||||||
Data.CurrentlyRunning = new MusicPlayer(output, discord);
|
Data.MusicPlayer = null;
|
||||||
await Data.CurrentlyRunning.StartSendAudio();
|
}
|
||||||
|
|
||||||
|
|
||||||
|
using (var discordChanneAudioOutStream = Data.audioClient.CreatePCMStream(AudioApplication.Mixed))
|
||||||
|
{
|
||||||
|
if (Data.MusicPlayer is null)
|
||||||
|
Data.MusicPlayer = new MusicPlayer(discordChanneAudioOutStream);
|
||||||
|
while (Data.Playlist.Count > 0)
|
||||||
|
{
|
||||||
|
var nowPlaying = Data.Playlist.GetNextSong;
|
||||||
|
using (var ffmpeg = CreateStream(nowPlaying.Name))
|
||||||
|
using (var ffmpegOutputBaseStream = ffmpeg.StandardOutput.BaseStream)
|
||||||
|
{
|
||||||
|
await Data.MusicPlayer.Play(ffmpegOutputBaseStream, 1024);
|
||||||
|
Console.WriteLine("Finished playing from" + nowPlaying.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Data.MusicPlayer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,16 +120,4 @@ internal class Play : DBCommand
|
|||||||
{
|
{
|
||||||
return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1", UseShellExecute = false, RedirectStandardOutput = true });
|
return Process.Start(new ProcessStartInfo { FileName = "ffmpeg", Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1", UseShellExecute = false, RedirectStandardOutput = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GetMusicAudio(string url, string location)
|
|
||||||
{
|
|
||||||
Process proc = new Process();
|
|
||||||
proc.StartInfo.FileName = "MusicDownloader.exe";
|
|
||||||
proc.StartInfo.Arguments = $"{url},{location}";
|
|
||||||
proc.StartInfo.UseShellExecute = false;
|
|
||||||
proc.StartInfo.RedirectStandardOutput = true;
|
|
||||||
|
|
||||||
proc.Start();
|
|
||||||
await proc.WaitForExitAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
31
MusicCommands/Skip.cs
Normal file
31
MusicCommands/Skip.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Discord.Commands;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using PluginManager.Interfaces;
|
||||||
|
|
||||||
|
namespace MusicCommands
|
||||||
|
{
|
||||||
|
public class Skip : DBCommand
|
||||||
|
{
|
||||||
|
public string Command => "skip";
|
||||||
|
|
||||||
|
public string Description => "skip the music that is currently running";
|
||||||
|
|
||||||
|
public string Usage => "skip";
|
||||||
|
|
||||||
|
public bool canUseDM => false;
|
||||||
|
|
||||||
|
public bool canUseServer => true;
|
||||||
|
|
||||||
|
public bool requireAdmin => false;
|
||||||
|
|
||||||
|
public void Execute(SocketCommandContext context, SocketMessage message, DiscordSocketClient client, bool isDM)
|
||||||
|
{
|
||||||
|
Data.MusicPlayer.isPlaying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using Discord.Commands;
|
|
||||||
using Discord.WebSocket;
|
|
||||||
using PluginManager.Interfaces;
|
|
||||||
|
|
||||||
namespace MusicCommands;
|
|
||||||
|
|
||||||
internal class Unpause : DBCommand
|
|
||||||
{
|
|
||||||
public string Command => "unpause";
|
|
||||||
|
|
||||||
public string Description => "Unpause the music";
|
|
||||||
|
|
||||||
public string Usage => "unpause";
|
|
||||||
|
|
||||||
public bool canUseDM => false;
|
|
||||||
|
|
||||||
public bool canUseServer => true;
|
|
||||||
|
|
||||||
public bool requireAdmin => false;
|
|
||||||
|
|
||||||
public void Execute(SocketCommandContext context, SocketMessage message, DiscordSocketClient client, bool isDM)
|
|
||||||
{
|
|
||||||
Data.CurrentlyRunning.Paused = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
MusicCommands/queue.cs
Normal file
31
MusicCommands/queue.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Discord.Commands;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using PluginManager.Interfaces;
|
||||||
|
|
||||||
|
namespace MusicCommands
|
||||||
|
{
|
||||||
|
public class queue : DBCommand
|
||||||
|
{
|
||||||
|
public string Command => "queue";
|
||||||
|
|
||||||
|
public string Description => "check queue";
|
||||||
|
|
||||||
|
public string Usage => "queue";
|
||||||
|
|
||||||
|
public bool canUseDM => false;
|
||||||
|
|
||||||
|
public bool canUseServer => true;
|
||||||
|
|
||||||
|
public bool requireAdmin => false;
|
||||||
|
|
||||||
|
public async void Execute(SocketCommandContext context, SocketMessage message, DiscordSocketClient client, bool isDM)
|
||||||
|
{
|
||||||
|
await context.Channel.SendMessageAsync($"You have {Data.Playlist.Count} items in queue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user