From ae778d37e0f759d70366282d10713cd2e0b38776 Mon Sep 17 00:00:00 2001 From: Marrub Date: Thu, 13 Oct 2016 00:58:16 -0400 Subject: [PATCH] Initial commit. --- .gitignore | 7 + Properties/AssemblyInfo.cs | 14 ++ Source/Bot.cs | 234 +++++++++++++++++++++++ Source/BotClient.cs | 53 ++++++ Source/BotClientDiscord.cs | 238 +++++++++++++++++++++++ Source/BotClientIRC.cs | 45 +++++ Source/BotEvents.cs | 106 +++++++++++ Source/BotInfo.cs | 81 ++++++++ Source/BotModule.cs | 123 ++++++++++++ Source/Exceptions.cs | 31 +++ Source/Links.cs | 81 ++++++++ Source/Modules/Mod_Admin.cs | 82 ++++++++ Source/Modules/Mod_Audio.cs | 202 ++++++++++++++++++++ Source/Modules/Mod_Fun.cs | 112 +++++++++++ Source/Modules/Mod_Idgames.cs | 182 ++++++++++++++++++ Source/Modules/Mod_Links.cs | 336 +++++++++++++++++++++++++++++++++ Source/Modules/Mod_Memo.cs | 236 +++++++++++++++++++++++ Source/Modules/Mod_Quote.cs | 96 ++++++++++ Source/Modules/Mod_Seen.cs | 143 ++++++++++++++ Source/Modules/Mod_Shittalk.cs | 119 ++++++++++++ Source/Modules/Mod_Utils.cs | 227 ++++++++++++++++++++++ Source/Program.cs | 100 ++++++++++ Source/Utils.cs | 183 ++++++++++++++++++ packages.config | 11 ++ vrobot3.csproj | 96 ++++++++++ vrobot3.sln | 174 +++++++++++++++++ 26 files changed, 3312 insertions(+) create mode 100644 .gitignore create mode 100644 Properties/AssemblyInfo.cs create mode 100644 Source/Bot.cs create mode 100644 Source/BotClient.cs create mode 100644 Source/BotClientDiscord.cs create mode 100644 Source/BotClientIRC.cs create mode 100644 Source/BotEvents.cs create mode 100644 Source/BotInfo.cs create mode 100644 Source/BotModule.cs create mode 100644 Source/Exceptions.cs create mode 100644 Source/Links.cs create mode 100644 Source/Modules/Mod_Admin.cs create mode 100644 Source/Modules/Mod_Audio.cs create mode 100644 Source/Modules/Mod_Fun.cs create mode 100644 Source/Modules/Mod_Idgames.cs create mode 100644 Source/Modules/Mod_Links.cs create mode 100644 Source/Modules/Mod_Memo.cs create mode 100644 Source/Modules/Mod_Quote.cs create mode 100644 Source/Modules/Mod_Seen.cs create mode 100644 Source/Modules/Mod_Shittalk.cs create mode 100644 Source/Modules/Mod_Utils.cs create mode 100644 Source/Program.cs create mode 100644 Source/Utils.cs create mode 100644 packages.config create mode 100644 vrobot3.csproj create mode 100644 vrobot3.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc40a0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.nuget +obj +bin +data +packages +*.swp +Makefile diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..81a5be4 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyTitle("vrobot3")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("© 2016 Project Golan")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: CLSCompliant(false)] +[assembly: AssemblyVersion("3.1.*")] diff --git a/Source/Bot.cs b/Source/Bot.cs new file mode 100644 index 0000000..db92b81 --- /dev/null +++ b/Source/Bot.cs @@ -0,0 +1,234 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Main bot class. +// +//----------------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.IO; + +using CommandFuncDict = + System.Collections.Generic.Dictionary< + System.String, + System.Tuple< + ProjectGolan.Vrobot3.IBotModule, + ProjectGolan.Vrobot3.BotCommandStructure>>; + +namespace ProjectGolan.Vrobot3 +{ + // + // BotCommand + // + // Delegate type for bot commands. + // + public delegate void BotCommand(User usr, Channel channel, String msg); + + // + // CommandDict + // + // Dictionary of bot commands. + // + public class CommandDict : Dictionary {} + + // + // Bot + // + public partial class Bot + { + public List modules = new List(); + public CommandFuncDict cmdfuncs = new CommandFuncDict(); + public readonly BotInfo info; + + private Dictionary lastLine = new Dictionary(); + private IBotClient client; + + public bool isInAudioChannel => false; + public ServerInfo serverInfo => client.info; + + // + // Bot constructor + // + public Bot(BotInfo info) + { + this.info = info; + + switch(info.serverType) + { + case ServerType.IRC: client = new BotClientIRC(this); break; + case ServerType.Discord: client = new BotClientDiscord(this); break; + } + + var modClasses = + from assembly in AppDomain.CurrentDomain.GetAssemblies() + from type in assembly.GetTypes() + where moduleIsValid(type) + select type; + + foreach(var mod in modClasses) + modules.Add(Activator.CreateInstance(mod, this) as IBotModule); + + foreach(var mod in modules) + foreach(var kvp in mod.commands) + cmdfuncs.Add(kvp.Key, Tuple.Create(mod, kvp.Value)); + } + + // + // connect + // + public void connect() => client.connect(); + + // + // disconnect + // + public void disconnect() + { + cmdfuncs.Clear(); + modules.Clear(); + client.disconnect(); + } + + // + // reply + // + public void reply(User usr, Channel channel, String msg) => + message(channel, usr.name + ": " + msg); + public void reply(User usr, ulong id, String msg) => + message(id, usr.name + ": " + msg); + + // + // message + // + public void message(Channel channel, String msg) => + client.sendMessage(channel, msg); + public void message(ulong id, String msg) => + client.sendMessage(client.getChannel(id), msg); + + // + // action + // + public void action(Channel channel, String msg) => + client.sendAction(channel, msg); + public void action(ulong id, String msg) => + client.sendAction(client.getChannel(id), msg); + + // + // joinAudioChannel + // + public async Task joinAudioChannel(User user) + { + var channel = client.getAudioChannel(user); + if(channel != null) + { + await client.joinAudioChannel(channel); + return true; + } + else + return false; + } + + // + // partAudioChannel + // + public void partAudioChannel() => client.partAudioChannel(); + + // + // playAudioFile + // + public Task playAudioFile(String file) => client.playAudioFile(file); + + // + // checkModPermissions + // + public bool checkModPermissions(Channel channel, Type mod) + { + String[] enables; + + if(info.enables.ContainsKey(channel.name)) + enables = info.enables[channel.name]; + else if(info.enables.ContainsKey("*")) + enables = info.enables["*"]; + else + return true; + + foreach(var modname in enables) + { + Type type; + + if(modname == "*") + return true; + else if(modname[0] == '@') + type = Type.GetType("ProjectGolan.Vrobot3.Modules." + modname.Substring(1)); + else + type = Type.GetType(modname); + + if(type == mod) + return true; + } + + return false; + } + + // + // runCommand + // + private void runCommand(User usr, Channel channel, BotCommand cmd, + String rest) + { + try + { + cmd(usr, channel, rest ?? String.Empty); + } + catch(CommandArgumentException exc) + { + if(exc.Message != null) + reply(usr, channel, exc.Message); + else + Console.WriteLine("{0}: Unknown CommandArgumentException", + info.serverName); + } + catch(Exception exc) + { + reply(usr, channel, "fug it borked"); + Console.WriteLine("{0}: Unhandled exception in command: {1}", + info.serverName, exc?.Message ?? "unknown error"); + File.WriteAllText(Program.Instance.dataDir + "/cmdexcdump.txt", + exc.ToString()); + } + } + + // + // moduleIsValid + // + private bool moduleIsValid(Type type) + { + if(!typeof(IBotModule).IsAssignableFrom(type) || + !type.IsClass || type.IsAbstract) + return false; + + foreach(var attribute in type.GetCustomAttributes(false)) + { + if((attribute is BotModuleIRCAttribute && + info.serverType != ServerType.IRC) || + (attribute is BotModuleDiscordAttribute && + info.serverType != ServerType.Discord) || + (attribute is BotModuleRequiresAudioAttribute && + !serverInfo.hasAudio) || + attribute is BotModuleDisabledAttribute) + return false; + } + + return true; + } + } +} + +// EOF diff --git a/Source/BotClient.cs b/Source/BotClient.cs new file mode 100644 index 0000000..21c7dc6 --- /dev/null +++ b/Source/BotClient.cs @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Connection method interface. +// +//----------------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; + +namespace ProjectGolan.Vrobot3 +{ + // + // IBotClient + // + public abstract class IBotClient + { + protected Bot bot; + public ServerInfo info; + + public IBotClient(Bot bot) { this.bot = bot; } + + // Connect + public abstract void connect(); + public abstract void disconnect(); + + // Send + public abstract void sendAction(Channel channel, String msg); + public abstract void sendMessage(Channel channel, String msg); + + // Channel + public abstract Channel getChannel(ulong id); + public virtual void joinChannel(Channel channel) {} + public virtual void partChannel(Channel channel) {} + + // Audio + public virtual ChannelAudio getAudioChannel(User user) => + new ChannelAudio(); + public virtual async Task joinAudioChannel(ChannelAudio channel) => + await Task.FromResult(0); + public virtual void partAudioChannel() {} + public virtual bool isInAudioChannel() => false; + public virtual async Task playAudioFile(String file) => + await Task.FromResult(0); + } +} + +// EOF diff --git a/Source/BotClientDiscord.cs b/Source/BotClientDiscord.cs new file mode 100644 index 0000000..8b9c483 --- /dev/null +++ b/Source/BotClientDiscord.cs @@ -0,0 +1,238 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Discord client. +// +//----------------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using Discord.Audio; + +namespace ProjectGolan.Vrobot3 +{ + // + // BotClientDiscord + // + public class BotClientDiscord : IBotClient + { + // + // AudioBuffer + // + class AudioBuffer + { +// private static const int MaxSize = 20 * 1024 * 1024; + private readonly String name; + private ulong num = 0; + private ulong next = 1; + private ulong bufSize = 0; + public bool completed { get; private set; } = false; + + public AudioBuffer() + { + name = System.IO.Path.GetRandomFileName() + "."; + } + } + + private Discord.DiscordClient client = new Discord.DiscordClient(); + private Discord.Audio.IAudioClient audioClient; + private Discord.Server server; + + // + // BotClientDiscord constructor + // + public BotClientDiscord(Bot bot) : + base(bot) + { + info.hasAudio = true; + info.hasColors = false; + info.hasNewlines = true; + info.messageSafeMaxLen = 1777; + info.shortMessages = false; + + client.MessageReceived += (sender, evt) => + { + if(!evt.Message.IsAuthor && !evt.User.IsBot && + (bot.info.channels == null || + bot.info.channels.Contains("#" + evt.Channel.Name)) && + evt.Server.Id.ToString() == bot.info.serverAddr) + { + var usr = new User{}; + var channel = new Channel{}; + + usr.hostname = evt.User.Id.ToString(); + usr.name = evt.User.Nickname ?? evt.User.Name; + + channel.id = evt.Channel.Id; + channel.name = "#" + evt.Channel.Name; + + bot.onMessage(usr, channel, evt.Message.Text); + } + }; + + client.UsingAudio(x => x.Mode = AudioMode.Outgoing); + } + + // + // getUser + // + private Discord.User getUser(User usr) + { + if(server == null) + server = client.GetServer(ulong.Parse(bot.info.serverAddr)); + return server.GetUser(ulong.Parse(usr.hostname)); + } + + // + // connect + // + public override void connect() + { + Console.WriteLine("{0}: Creating connection.", bot.info.serverName); + client.ExecuteAndWait(async () => + { + await client.Connect(bot.info.serverPass, Discord.TokenType.Bot); + client.SetGame("vrobot 3.1 series"); + }); + } + + // + // disconnect + // + public override void disconnect() + { + if(client != null) + { + partAudioChannel(); + client.Disconnect(); + client = null; + } + } + + // + // sendAction + // + public override void sendAction(Channel channel, String msg) => + client.GetChannel(channel.id)?.SendMessage( + "_" + Discord.Format.Escape(msg) + "_"); + + // + // sendMessage + // + public override void sendMessage(Channel channel, String msg) => + client.GetChannel(channel.id)?.SendMessage(Discord.Format.Escape(msg)); + + // + // getChannel + // + public override Channel getChannel(ulong id) + { + var dchannel = client.GetChannel(id); + var channel = new Channel{}; + channel.id = dchannel.Id; + channel.name = "#" + dchannel.Name; + return channel; + } + + // + // getAudioChannel + // + public override ChannelAudio getAudioChannel(User usr) + { + var dchannel = getUser(usr).VoiceChannel; + if(dchannel == null) return null; + var channel = new ChannelAudio{}; + channel.id = dchannel.Id; + channel.name = dchannel.Name; + return channel; + } + + // + // joinAudioChannel + // + public override async Task joinAudioChannel(ChannelAudio channel) + { + if(channel == null) + return; + + var dchannel = client.GetChannel(channel.id); + if(!isInAudioChannel()) + audioClient = await dchannel.JoinAudio(); + else + await audioClient.Join(dchannel); + } + + // + // partAudioChannel + // + public override void partAudioChannel() + { + if(isInAudioChannel()) + { + audioClient.Clear(); + audioClient.Wait(); + audioClient.Disconnect(); + } + audioClient = null; + } + + // + // isInAudioChannel + // + public override bool isInAudioChannel() => + audioClient?.State == Discord.ConnectionState.Connected; + + // + // playAudioFile + // + public override async Task playAudioFile(String file) + { + if(!isInAudioChannel()) return; + + var proc = Process.Start(new ProcessStartInfo{ + FileName = "ffmpeg", + Arguments = $"-i {file} -f s16le -ar 48000 -ac 2 " + + "-loglevel quiet pipe:1", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = false, + CreateNoWindow = true + }); + + var buf = new byte[3840 * 32]; + var ostream = audioClient.OutputStream; + var istream = proc.StandardOutput.BaseStream; + + int count; + try + { + while(!proc.HasExited && + (count = await istream.ReadAsync(buf, 0, buf.Length)) != 0) + { + Thread.Sleep(8); + await ostream.WriteAsync(buf, 0, count); + } + } + catch(OperationCanceledException) + { + Console.WriteLine("{0}: Canceled audio stream.", + bot.info.serverName); + } + finally + { + istream.Dispose(); + ostream.Dispose(); + } + } + } +} + +// EOF diff --git a/Source/BotClientIRC.cs b/Source/BotClientIRC.cs new file mode 100644 index 0000000..1cb74a0 --- /dev/null +++ b/Source/BotClientIRC.cs @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// IRC client. +// +//----------------------------------------------------------------------------- + +using System; + +namespace ProjectGolan.Vrobot3 +{ + // + // BotClientIRC + // + public class BotClientIRC : IBotClient + { + // + // BotClientIRC constructor + // + public BotClientIRC(Bot bot) : + base(bot) + { + info.hasAudio = false; + info.hasColors = true; + info.hasNewlines = false; + info.messageSafeMaxLen = 601; + info.shortMessages = true; + } + + public override void connect() {} + public override void disconnect() {} + public override Channel getChannel(ulong id) => new Channel{}; + public override void joinChannel(Channel channel) {} + public override void partChannel(Channel channel) {} + public override void sendAction(Channel channel, String msg) {} + public override void sendMessage(Channel channel, String msg) {} + } +} + +// EOF diff --git a/Source/BotEvents.cs b/Source/BotEvents.cs new file mode 100644 index 0000000..e8699d1 --- /dev/null +++ b/Source/BotEvents.cs @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Bot events. +// +//----------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ProjectGolan.Vrobot3 +{ + // + // Bot + // + public partial class Bot + { + // + // onSeen + // + public void onSeen(User usr, Channel channel) + { + foreach(var mod in modules) + if(checkModPermissions(channel, mod.GetType())) + mod.events.raiseOnSeen(usr, channel); + } + + // + // onMessage + // + // Attempt to run commands. + // + public void onMessage(User usr, Channel channel, String msg) + { + var validCmdPreceders = ".%".ToCharArray(); + String rest = null; + + if(msg.Length >= 1 && validCmdPreceders.Contains(msg[0])) + { + Predicate pred; + + if(msg[0] == '%') + pred = fn => fn.flags.HasFlag(BotCommandFlags.AdminOnly); + else + pred = fn => !fn.flags.HasFlag(BotCommandFlags.AdminOnly); + + var dict = from fn in cmdfuncs where pred(fn.Value.Item2) select fn; + + // Get the command name. + String[] splt = msg.Substring(1).Split(" ".ToCharArray(), 2); + String cmdname = splt[0].ToLower(); + + // Handle commands ending with "^". + // These take the last message as input. + if(cmdname.EndsWith("^")) + { + cmdname = cmdname.Substring(0, cmdname.Length - 1); + lastLine.TryGetValue(channel.id, out rest); + } + + var tcmd = + from kvp in dict where kvp.Key == cmdname select kvp.Value; + if(tcmd.Any()) + { + var tcmdr = tcmd.First(); + + // Check permissions. + if(usr.hostname != info.adminId && + (tcmdr.Item2.flags.HasFlag(BotCommandFlags.AdminOnly) || + !checkModPermissions(channel, tcmdr.Item1.GetType()))) + goto RaiseMessage; + + // If we have input, grab that too. + if(rest == null && splt.Length > 1) + rest = splt[1]; + else if(rest == null) + rest = String.Empty; + + // Go over each module and raise a command message event. + foreach(var mod in modules) + if(checkModPermissions(channel, mod.GetType())) + mod.events.raiseOnCmdMessage(usr, channel, msg); + + runCommand(usr, channel, tcmdr.Item2.cmd, rest); + return; + } + } + + RaiseMessage: + // Go over each module and raise a message event. + foreach(var mod in modules) + if(checkModPermissions(channel, mod.GetType())) + mod.events.raiseOnMessage(usr, channel, msg); + + lastLine[channel.id] = msg; + } + } +} + +// EOF diff --git a/Source/BotInfo.cs b/Source/BotInfo.cs new file mode 100644 index 0000000..e72dd5f --- /dev/null +++ b/Source/BotInfo.cs @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Bot informational classes. +// +//----------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace ProjectGolan.Vrobot3 +{ + // + // ServerType + // + public enum ServerType + { + IRC, + Discord + } + + // + // ServerInfo + // + public struct ServerInfo + { + public bool hasAudio; + public bool hasColors; + public bool hasNewlines; + public int messageSafeMaxLen; + public bool shortMessages; + } + + // + // BotInfo + // + public struct BotInfo + { + public Dictionary enables; + public ServerType serverType; + public String serverName; + public String serverPass; + public String serverAddr; + public String adminId; + public String[] channels; + } + + // + // User + // + public struct User + { + public String hostname; // A consistent identifier for the user. + public String name; // Nickname for replying and etc. + } + + // + // Channel + // + public struct Channel + { + public ulong id; + public String name; + } + + // + // ChannelAudio + // + public class ChannelAudio + { + public ulong id; + public String name; + } +} + +// EOF diff --git a/Source/BotModule.cs b/Source/BotModule.cs new file mode 100644 index 0000000..ce2f1c1 --- /dev/null +++ b/Source/BotModule.cs @@ -0,0 +1,123 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Base module classes. +// +//----------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace ProjectGolan.Vrobot3 +{ + namespace Modules.EventType + { + public delegate void OnMessage(User usr, Channel channel, String msg); + public delegate void OnSeen(User usr, Channel channel); + } + + // + // BotModuleRequiresAudioAttribute + // + public class BotModuleRequiresAudioAttribute : Attribute + { + public override String ToString() => "Bot Module Requires Audio"; + } + + // + // BotModuleDisabledAttribute + // + public class BotModuleDisabledAttribute : Attribute + { + public override String ToString() => "Bot Module Disabled"; + } + + // + // BotModuleDiscordAttribute + // + public class BotModuleDiscordAttribute : Attribute + { + public override String ToString() => "Bot Module is Discord only"; + } + + // + // BotModuleIRCAttribute + // + public class BotModuleIRCAttribute : Attribute + { + public override String ToString() => "Bot Module is IRC only"; + } + + // + // BotCommandFlags + // + // Flags for command registration. + // + [Flags] + public enum BotCommandFlags + { + AdminOnly = 1 << 0, + Hidden = 1 << 1 + } + + // + // BotCommandStructure + // + // Used for registering commands in a module. + // + public struct BotCommandStructure + { + public BotCommand cmd; + public BotCommandFlags flags; + public String help; + } + + // + // IBotModule + // + // Base module class. Inherit this for your modules. + // + public abstract class IBotModule + { + // + // Events + // + public struct Events + { + public event Modules.EventType.OnMessage onCmdMessage; + public event Modules.EventType.OnMessage onMessage; + public event Modules.EventType.OnSeen onSeen; + + public void raiseOnCmdMessage(User usr, Channel channel, String msg) + { + if(onCmdMessage != null) + onCmdMessage(usr, channel, msg); + } + + public void raiseOnMessage(User usr, Channel channel, String msg) + { + if(onMessage != null) + onMessage(usr, channel, msg); + } + + public void raiseOnSeen(User usr, Channel channel) + { + if(onSeen != null) + onSeen(usr, channel); + } + } + + public IBotModule(Bot bot) { this.bot = bot; } + + public CommandDict commands = new CommandDict(); + public Events events; + protected Bot bot; + } +} + +// EOF diff --git a/Source/Exceptions.cs b/Source/Exceptions.cs new file mode 100644 index 0000000..ee0dee8 --- /dev/null +++ b/Source/Exceptions.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Exception types. +// +//----------------------------------------------------------------------------- + +using System; + +namespace ProjectGolan.Vrobot3 +{ + // + // CommandArgumentException + // + public class CommandArgumentException : Exception + { + public CommandArgumentException() {} + public CommandArgumentException(String message) : base(message) {} + public CommandArgumentException(String message, Exception inner) : + base(message, inner) + { + } + } +} + +// EOF diff --git a/Source/Links.cs b/Source/Links.cs new file mode 100644 index 0000000..76e5657 --- /dev/null +++ b/Source/Links.cs @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Link (URI) utilities. +// +//----------------------------------------------------------------------------- + +using System; +using System.Text.RegularExpressions; + +namespace ProjectGolan.Vrobot3 +{ + // + // Utils + // + public static partial class Utils + { + // + // URI + // + public struct URI + { + public String method, host, path, query, tag, uri; + + // + // ToString + // + public override String ToString() => uri; + + // + // Finder + // + public static Regex Finder = new Regex( + @"((?[^:/?# ]+)?:)" + + @"(//(?[^/?# ]*))" + + @"(?[^?# ]*)" + + @"(?\?([^# ]*))?" + + @"(?#(.*))?"); + + // + // FromMatch + // + public static URI FromMatch(Match match) + { + return new URI{ + method = match.Groups["method"]?.Value ?? String.Empty, + host = match.Groups["host"]?.Value, + path = match.Groups["path"]?.Value, + query = match.Groups["query"]?.Value ?? String.Empty, + tag = match.Groups["tag"]?.Value ?? String.Empty, + uri = match.Value + }; + } + + // + // Match + // + public static URI Match(String str) => FromMatch(Finder.Match(str)); + + // + // Matches + // + public static URI[] Matches(String str) + { + var matchbox = Finder.Matches(str); + if(matchbox.Count == 0) return null; + var matches = new URI[matchbox.Count]; + for(int i = 0; i < matchbox.Count; i++) + matches[i] = FromMatch(matchbox[i]); + return matches; + } + } + } +} + +// EOF diff --git a/Source/Modules/Mod_Admin.cs b/Source/Modules/Mod_Admin.cs new file mode 100644 index 0000000..d03e541 --- /dev/null +++ b/Source/Modules/Mod_Admin.cs @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Admin commands module. +// %kill, %msg, %action +// +//----------------------------------------------------------------------------- + +using System; +using System.Linq; + +namespace ProjectGolan.Vrobot3.Modules +{ + // + // Mod_Admin + // + public class Mod_Admin : IBotModule + { + // + // Mod_Admin constructor + // + public Mod_Admin(Bot bot_) : + base(bot_) + { + commands["kill"] = new BotCommandStructure{ + cmd = cmdKill, + flags = BotCommandFlags.AdminOnly, + help = "Kills all bot instances.\n" + + "Syntax: %kill" + }; + + commands["msg"] = new BotCommandStructure{ + cmd = cmdMsg, + flags = BotCommandFlags.AdminOnly, + help = "Sends a message.\n" + + "Syntax: %msg channel, msg\n" + + "Example: %msg #general, ur all dumb" + }; + + commands["action"] = new BotCommandStructure{ + cmd = cmdAction, + flags = BotCommandFlags.AdminOnly, + help = "Sends an action.\n" + + "Syntax: %action channel, msg\n" + + "Example: %action #general, explodes violently" + }; + } + + // + // cmdKill + // + public void cmdKill(User usr, Channel channel, String msg) + { + Console.WriteLine("{0}: Killing all instances.", bot.info.serverName); + Program.Instance.end(); + } + + // + // cmdMsg + // + public void cmdMsg(User usr, Channel channel, String msg) + { + String[] args = Utils.GetArguments(msg, commands["msg"].help, 2, 2); + bot.message(ulong.Parse(args[0]), args[1].Trim()); + } + + // + // cmdAction + // + public void cmdAction(User usr, Channel channel, String msg) + { + String[] args = Utils.GetArguments(msg, commands["action"].help, 2, 2); + bot.action(ulong.Parse(args[0]), args[1].Trim()); + } + } +} + diff --git a/Source/Modules/Mod_Audio.cs b/Source/Modules/Mod_Audio.cs new file mode 100644 index 0000000..af0c681 --- /dev/null +++ b/Source/Modules/Mod_Audio.cs @@ -0,0 +1,202 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Audio-based commands. +// +//----------------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace ProjectGolan.Vrobot3.Modules +{ + // + // Mod_Audio + // + [BotModuleRequiresAudio, BotModuleDisabled] + public class Mod_Audio : IBotModule + { + // + // QueueItem + // + class QueueItem + { + Utils.URI uri; + + public QueueItem(Utils.URI uri) + { + this.uri = uri; + } + + public override String ToString() => String.Empty; + } + + // + // Queue + // + class Queue + { + TimeSpan curTime; + List items; + int pos; + + public Queue() + { + this.curTime = new TimeSpan(); + this.items = new List(); + this.pos = 0; + } + + public bool addItem(Utils.URI uri) + { + var item = new QueueItem(uri); + items.Add(item); + return true; + } + } + + String[] validMethods = { "http", "https", "ftp", "ftps" }; + Random rnd = Utils.GetRND(); + Queue queue = new Queue(); + + // + // Mod_Audio constructor + // + public Mod_Audio(Bot bot_) : + base(bot_) + { + commands["queue"] = new BotCommandStructure{ + cmd = cmdQueue, + help = "Add an item (or items) to the audio queue.\n" + + "Syntax: .queue uri...\n" + + "Example: .queue https://www.youtube.com/watch?v=13pL0TiOiHM" + }; + + commands["play"] = new BotCommandStructure{ + cmd = cmdPlay, + help = "Set the currently playing item in the queue. " + + "If a URL is given, queues and plays that.\n" + + "Syntax: .play [number|uri]\n" + + "Example: .play 5\n" + + "Example: .play https://www.youtube.com/watch?v=13pL0TiOiHM" + }; + + commands["lsqueue"] = new BotCommandStructure{ + cmd = cmdListQueue, + help = "Lists queue items.\n" + + "Syntax: .lsqueue" + }; + + commands["fugoff"] = new BotCommandStructure{ + cmd = cmdFugOff, + help = "GET ME COGS OR FUG OFF", + flags = BotCommandFlags.AdminOnly + }; + + commands["summon"] = new BotCommandStructure{ + cmd = cmdSummon, + help = "Makes the bot join your audio channel.\n" + + "Syntax: .summon" + }; + + commands["vanquish"] = new BotCommandStructure{ + cmd = cmdVanquish, + help = "Makes the bot leave their audio channel.\n" + + "Syntax: %vanquish", + flags = BotCommandFlags.AdminOnly + }; + } + + // + // summon + // + async Task summon(User usr, Channel channel) + { + if(bot.isInAudioChannel) + return true; + + if(!await bot.joinAudioChannel(usr)) + { + bot.reply(usr, channel, + "Couldn't find audio channel. " + + "If you are already in an audio channel, please reconnect to " + + "it and try again."); + return false; + } + + return true; + } + + // + // cmdQueue + // + public void cmdQueue(User usr, Channel channel, String msg) + { + var uris = Utils.URI.Matches(msg); + + if(uris == null) + throw new CommandArgumentException("no valid URIs"); + + int loadPass = 0; + foreach(var uri_ in uris) + { + var uri = uri_; + if(uri.method == String.Empty) + uri.method = "http"; + + if(validMethods.Contains(uri.method) && + queue.addItem(uri)) + loadPass++; + } + + bot.reply(usr, channel, + $"Added {loadPass} item{loadPass == 1 ? "" : "s"} to the queue."); + } + + // + // cmdPlay + // + public void cmdPlay(User usr, Channel channel, String msg) + { + } + + // + // cmdListQueue + // + public void cmdListQueue(User usr, Channel channel, String msg) + { + } + + // + // cmdFugOff + // + public async void cmdFugOff(User usr, Channel channel, String msg) + { + if(!await summon(usr, channel)) + return; + + await bot.playAudioFile("\"/home/marrub/musix/MusixDL/Shadowfax - Shadowdance/01 New Electric India.mp3\"").ConfigureAwait(false); + } + + // + // cmdSummon + // + public void cmdSummon(User usr, Channel channel, String msg) => + summon(usr, channel); + + // + // cmdVanquish + // + public void cmdVanquish(User usr, Channel channel, String msg) => + bot.partAudioChannel(); + } +} + diff --git a/Source/Modules/Mod_Fun.cs b/Source/Modules/Mod_Fun.cs new file mode 100644 index 0000000..0819e17 --- /dev/null +++ b/Source/Modules/Mod_Fun.cs @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Fun stuff. +// .carmack, .revenant, .wan, .nyan, .:^) +// +//----------------------------------------------------------------------------- + +using System; + +namespace ProjectGolan.Vrobot3.Modules +{ + // + // Mod_Fun + // + public class Mod_Fun : IBotModule + { + // + // ShitpostingDevice + // + private class ShitpostingDevice + { + private String word, final; + private Random rnd = Utils.GetRND(); + private int min, max; + private Bot bot; + + // + // ShitpostingDevice constructor + // + public ShitpostingDevice(String word, String final, int min, int max, + Bot bot) + { + this.word = word; + this.final = final; + this.min = min; + this.max = max; + this.bot = bot; + } + + // + // run + // + public void run(User usr, Channel channel, String msg) + { + int n = rnd.Next(min, max); + String outp = String.Empty; + + if(bot.serverInfo.hasColors && rnd.Next(0, 8) == 1) + for(int i = 0; i < 6; i++) + { + String[] colors = { "04", "07", "08", "09", "12", "06" }; + outp += "\x03"; + outp += colors[i]; + outp += word; + outp += word; + } + else + for(int i = 0; i < n; i++) + outp += word; + + bot.reply(usr, channel, outp + final); + } + } + + // + // Mod_Fun constructor + // + public Mod_Fun(Bot bot_) : + base(bot_) + { + commands["carmack"] = new BotCommandStructure{ + cmd = new ShitpostingDevice("MM", "", 3, 20, bot).run, + flags = BotCommandFlags.Hidden + }; + commands["revenant"] = new BotCommandStructure{ + cmd = new ShitpostingDevice("AA", "", 3, 20, bot).run, + flags = BotCommandFlags.Hidden + }; + commands["wan"] = new BotCommandStructure{ + cmd = new ShitpostingDevice("wan ", "- !", 2, 12, bot).run, + flags = BotCommandFlags.Hidden + }; + commands["nyan"] = new BotCommandStructure{ + cmd = new ShitpostingDevice("nyan ", "!~", 2, 12, bot).run, + flags = BotCommandFlags.Hidden + }; + commands[":^)"] = new BotCommandStructure{ + cmd = (usr, channel, msg) => bot.message(channel, ":^)"), + flags = BotCommandFlags.Hidden + }; + + events.onMessage += onMessage; + } + + // + // onMessage + // + public void onMessage(User usr, Channel channel, String msg) + { + if(msg.Contains("OLD MEN")) + bot.message(channel, "WARNING! WARNING!"); + } + } +} + +// EOF diff --git a/Source/Modules/Mod_Idgames.cs b/Source/Modules/Mod_Idgames.cs new file mode 100644 index 0000000..fde5271 --- /dev/null +++ b/Source/Modules/Mod_Idgames.cs @@ -0,0 +1,182 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Idgames search module. +// .idgames +// +//----------------------------------------------------------------------------- + +using System; +using System.Net; +using System.Web; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using System.Linq; + +namespace ProjectGolan.Vrobot3.Modules +{ + // + // Mod_Idgames + // + public class Mod_Idgames : IBotModule + { + static readonly String APIURI = + "http://doomworld.com/idgames/api/api.php"; + + private Random rnd = Utils.GetRND(); + + // + // Mod_Idgames constructor + // + public Mod_Idgames(Bot bot_) : + base(bot_) + { + commands["idgames"] = new BotCommandStructure{ + cmd = cmdIdgames, + help = "Gets an entry from the idgames archive.\n" + + "Syntax: .idgames [name or ID[, type[, position]]]\n" + + "Example: .idgames scythe, filename, 4\n" + + "Example: .idgames" + }; + } + + // + // cmdIdgames + // + public void cmdIdgames(User usr, Channel channel, String msg) + { + String[] args = + Utils.GetArguments(msg, commands["idgames"].help, 0, 3); + + switch(args.Length) + { + case 1: + int id; + if(args[0].Trim().Length == 0) + idgamesRandom(usr, channel); + else if(Int32.TryParse(args[0], out id)) + idgamesID(usr, channel, id); + else + idgames(usr, channel, args[0]); + break; + case 2: idgames(usr, channel, args[0], args[1]); break; + case 3: + if(args[2].Trim().ToLower() == "random") + idgames(usr, channel, args[0], args[1], "random"); + else + idgames(usr, channel, args[0], args[1], args[2].Trim()); + break; + } + } + + // + // idgamesRandom + // + private void idgamesRandom(User usr, Channel channel) + { + var req = WebRequest.Create("http://doomworld.com/idgames/?random") + as HttpWebRequest; + req.Referer = "http://doomworld.com/idgames/"; + bot.message(channel, + Discord.Format.Escape(req.GetResponse().ResponseUri.ToString())); + } + + // + // idgamesID + // + private void idgamesID(User usr, Channel channel, int id) + { + var req = WebRequest.Create(APIURI + "?action=get&id=" + id) + as HttpWebRequest; + + using(var response = req.GetResponse()) + { + var xml = XDocument.Load(response.GetResponseStream()); + + var x_title = + from item in xml.Descendants("title") select item.Value; + var x_uri = from item in xml.Descendants("url") select item.Value; + + if(!x_title.Any()) + { + bot.message(channel, "Nothing found."); + return; + } + + bot.message(channel, + Discord.Format.Escape(x_title.First() + ": " + x_uri.First())); + } + } + + // + // idgames + // + private void idgames(User usr, Channel channel, String inquiry, + String type = "title", String pos = "1") + { + int ipos = 0; + + if(pos != "random") + { + Utils.TryParse(pos, "Invalid position.", out ipos); + + if(ipos < 1) + throw new CommandArgumentException("Invalid position."); + + ipos = ipos - 1; + } + + inquiry = HttpUtility.UrlEncode(inquiry.Trim()); + type = HttpUtility.UrlEncode(type.Trim().ToLower()); + + if(type == "name") type = "title"; // >_>' + + String[] validtypes = { + "filename", "title", "author", "email", + "description", "credits", "editors", "textfile" + }; + + if(!validtypes.Contains(type)) + throw new CommandArgumentException("Invalid inquiry type."); + + String uri = APIURI + "?action=search&sort=rating&query=" + + inquiry + "&type=" + type; + var req = WebRequest.Create(uri); + Console.WriteLine("idgames query: {0}", uri); + + using(var response = req.GetResponse()) + { + var xml = XDocument.Load(response.GetResponseStream()); + + var x_titles = + from item in xml.Descendants("title") select item.Value; + var x_uris = from item in xml.Descendants("url") select item.Value; + + if(!x_titles.Any()) + { + bot.message(channel, "Nothing found."); + return; + } + + if(pos == "random") ipos = rnd.Next(0, x_titles.Count()); + if(ipos >= x_titles.Count()) ipos = x_titles.Count() - 1; + + String title = x_titles.ElementAtOrDefault(ipos); + if(title.Trim().Length > 0) title = "[ " + title + " ] "; + + bot.message(channel, + Discord.Format.Escape(String.Format("({0} of {1}{4} {2}{3}", + ipos + 1, x_titles.Count(), title, + x_uris.ElementAtOrDefault(ipos), + x_titles.Count() >= 100 ? "+)" : ")"))); + } + } + } +} + diff --git a/Source/Modules/Mod_Links.cs b/Source/Modules/Mod_Links.cs new file mode 100644 index 0000000..87acfdc --- /dev/null +++ b/Source/Modules/Mod_Links.cs @@ -0,0 +1,336 @@ +// +// Mod_Links.cs +// +// Link title capabilities. +// + +using System; +using System.Text.RegularExpressions; +using System.Xml; +using System.Linq; +using System.Net; +using System.Collections.Generic; +using System.Threading; +using Sharkbite.Irc; +using HtmlAgilityPack; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ProjectGolan.Vrobot3 +{ + // + // Mod_Links + // + + public sealed class Mod_Links : IBotModule + { + // + // URI + // + + private struct URI + { + public String method, host, path, query, tag, uri; + } + + // + // Delegates. + + private delegate void URIHandler(URI uri, String referer, ref String result); + + // + // Ctor + // + + public Mod_Links(Bot bot_) : + base(bot_) + { + events.OnMessage += Evt_OnMessage; + } + + // + // Evt_OnMessage + // + + public void Evt_OnMessage(UserInfo usr, String channel, String msg, bool iscmd) + { + // + // Do this asynchronously, we don't want link parsing to block operation. + + new Thread(() => { + try + { + if(!iscmd) + TryParseURIs(channel, msg); + } + catch(Exception exc) + { + Console.WriteLine("{0}: URL thread error: {1}", bot.n_groupname, exc.Message); + } + }).Start(); + } + + // + // GetURITitle + // + + private Match GetURITitle(URI uri, String referer, int kb = 16) + { + String rstr = Utils.GetResponseString(uri.uri, 1024 * kb, referer); + + if(rstr == null) + return null; + + return new Regex(@"\(?.+?)\").Match(rstr); + } + + // + // URI_Default + // + + private void URI_Default(URI uri, String referer, ref String result) + { + var req = WebRequest.Create(uri.uri) as HttpWebRequest; + req.Referer = referer; + req.UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.9) Gecko/20100101 Firefox/31.9"; + + using(var response = req.GetResponse() as HttpWebResponse) + { + var html = new HtmlDocument(); + html.LoadHtml(Utils.GetResponseString(response, 16*1024)); + var x_title = from item in html.DocumentNode.Descendants() + where (item?.Name ?? String.Empty) == "title" || + ((item?.Name ?? String.Empty) == "meta" && + (item?.Attributes["id"]?.Value ?? String.Empty).EndsWith("title")) + select item; + + if(x_title.Any()) + result = WebUtility.HtmlDecode(x_title.First().InnerText.Trim(new char[]{ ' ', '\t', '\n' })); + } + } + + // + // URI_Youtube + // + // Special fucking flower. + // + + private void URI_Youtube(URI uri, String referer, ref String result) + { + var req = WebRequest.Create(uri.uri) as HttpWebRequest; + req.Referer = referer; + + using(var response = req.GetResponse() as HttpWebResponse) + { + var html = new HtmlDocument(); + html.Load(response.GetResponseStream()); + var x_title = from item in html.DocumentNode.Descendants() + where (item?.Attributes["id"]?.Value ?? String.Empty) == "eow-title" + select item; + + if(x_title.Any()) + result = WebUtility.HtmlDecode(x_title.First().InnerText.Trim(new char[]{ ' ', '\t', '\n' })) + + " - YouTube"; + } + } + + + // + // URI_Gelooru + // + + private void URI_Gelbooru(URI uri, String referer, ref String result) + { + var match = GetURITitle(uri, referer, 8); // Should be OK to just get the first 8kb here. + if(match?.Success == true) + { + String title = WebUtility.HtmlDecode(match.Groups["realtitle"].Value); + if(title.Contains("Image View")) + result = "Image View - Gelbooru"; + else + result = title; + } + } + + // + // URI_Hitbox + // + + private void URI_Hitbox(URI uri, String referer, ref String result) + { + String name = WebUtility.HtmlEncode(uri.path.TrimStart(new char[]{'/'})); + + var req = WebRequest.Create("https://api.hitbox.tv/media/live/" + name + "?fast") as HttpWebRequest; + req.Referer = referer; + + using(var response = req.GetResponse() as HttpWebResponse) + { + var json = JObject.Parse(Utils.GetResponseString(response, 64 * 1024)); + var node = json["livestream"][0]; + String displayname = (String)node["media_display_name"]; + String status = (String)node["media_status"]; + bool live = Int32.Parse((String)node["media_is_live"] ?? "0") == 1; + + result = displayname; + if(live) + result += " (live)"; + if(!String.IsNullOrEmpty(status)) + result += ": " + status; + result += " - hitbox"; + } + } + + // + // TryParseURIs + // + // This function is really complicated because of exploits. Fuck exploits. + // + + private void TryParseURIs(String channel, String msg) + { + try + { + Regex r_finduris = new Regex( + @"((?[^:/?# ]+):)" + + @"(//(?[^/?# ]*))" + + @"(?[^?# ]*)" + + @"(?\?([^# ]*))?" + + @"(?#(.*))?" + ); + + var matchbox = r_finduris.Matches(msg); + + if(matchbox.Count != 0) + { + String outp = String.Empty; + + for(int i = 0; i < matchbox.Count; i++) + { + var match = matchbox[i]; + + URI uri = new URI{ + method = match.Groups["method"].Value, + host = match.Groups["host"].Value, + path = match.Groups["path"].Value, + query = match.Groups["query"]?.Value ?? String.Empty, + tag = match.Groups["tag"]?.Value ?? String.Empty, + uri = match.Value + }; + + // + // Will the real URI please stand up? + + if(uri.method == "http" || uri.method == "https") + { + var req = WebRequest.Create(uri.uri) as HttpWebRequest; + using(var resp = req.GetResponse()) + if(resp.ResponseUri.Host != uri.host) + { + uri.method = resp.ResponseUri.Scheme; + uri.host = resp.ResponseUri.Host; + uri.path = resp.ResponseUri.AbsolutePath; + uri.query = resp.ResponseUri.Query; + uri.tag = resp.ResponseUri.Fragment; + uri.uri = resp.ResponseUri.OriginalString; + } + } + + if(uri.path.Length == 0) + uri.path = "/"; + + // + // Make sure the method is OK. + // Previously: + // [22:19] file:///srv/www/marrub/oldmen.html + // [22:19] [ OLD MEN OLD MEN OLD MEN OLD MEN OLD MEN OLD MEN OLD MEN OLD ... ] + + String[] validmethods = { "ftp", "ftps", "http", "https" }; + if(!validmethods.Contains(uri.method)) + continue; + + // + // Try and get a decent title from the URL. + + URIHandler handler = URI_Default; + String result = String.Empty; + String referer = null; + + if(uri.method == "http" || uri.method == "https") + { + referer = uri.method + "://" + uri.host; + + Dictionary handlers = new Dictionary(){ + { "youtube.com", URI_Youtube }, + { "youtu.be", URI_Youtube }, + { "gelbooru.com", URI_Gelbooru }, + { "hitbox.tv", URI_Hitbox }, + }; + + String hostst = Regex.Replace(uri.host, @"^www\.", String.Empty, RegexOptions.Multiline); + if(handlers.ContainsKey(hostst)) + handler = handlers[hostst]; + } + + // + // Handle grabbing the title. Just get on with it if we throw an exception. + + try + { handler(uri, referer, ref result); } + catch(Exception exc) + { + Console.WriteLine("URL handle exception: {0}", exc.Message); + continue; + } + + // + // Sanitize. + + result.Trim(); + + for(int j = result.Length - 1; j >= 0; j--) + { + Char ch = result[j]; + if((Char.IsWhiteSpace(ch) && ch != ' ') || Char.IsControl(ch) || Char.IsSurrogate(ch)) + result = result.Remove(j, 1); + } + + // + // If the result is 0-length, just get rid of it. + + if(result.Trim().Length == 0) + continue; + + // + // Throw the result into the output buffer. + + outp += result; + + // + // If the output is too long, we need to shorten it and break. + + if(outp.Length > 400 - 3) + { + outp = outp.Substring(0, 400 - 3); + outp += "···"; + break; + } + + // + // Add separators. + + if(i != matchbox.Count - 1) + outp += " | "; + } + + if(outp.Length > 0) + bot.Message(channel, "[ " + outp + " ]"); + } + } + catch(Exception exc) + { + Console.WriteLine("{0}: URL parse error: {1}", bot.n_groupname, exc.Message ?? "[unknown]"); + } + } + } +} + diff --git a/Source/Modules/Mod_Memo.cs b/Source/Modules/Mod_Memo.cs new file mode 100644 index 0000000..5e62740 --- /dev/null +++ b/Source/Modules/Mod_Memo.cs @@ -0,0 +1,236 @@ +// +// Mod_Memo.cs +// +// Memoing capabilities. +// @memocount, .memo, .memoseen +// + +using System; +using System.Collections.Generic; +using System.IO; +using Sharkbite.Irc; +using Newtonsoft.Json; + +namespace ProjectGolan.Vrobot3.Modules +{ + // + // Mod_Memo + // + + public sealed class Mod_Memo : IBotModule + { + // + // MemoFlags + // + + [Flags] + enum MemoFlags + { + OnSeen = 1 << 0 + } + + // + // MemoInfo + // + + private struct MemoInfo + { + // + // Data. + + public String content; + public String sender; + public DateTime time; + public MemoFlags flags; + }; + + // + // MemoDict + // + + private class MemoDict : Dictionary> {} + + // + // Data. + + MemoDict memos = new MemoDict(); + + // + // Ctor + // + + public Mod_Memo(Bot bot_) : + base(bot_) + { + if(File.Exists("/srv/irc/vrobot3/data/memos." + bot.n_groupname + ".json")) + memos = JsonConvert.DeserializeObject(File.ReadAllText("/srv/irc/vrobot3/data/memos." + + bot.n_groupname + ".json")); + + commands["memo"] = new BotCommandStructure{ cmd = Cmd_Memo, + help = "Sends a message to someone later. Activates when they say something. || " + + "Syntax: .memo person message || " + + "Example: .memo SomeDude wow u suck at videogames" + }; + + commands["memoseen"] = new BotCommandStructure{ cmd = Cmd_MemoSeen, + help = "Sends a message to someone later. Activates when they do anything. || " + + "Syntax: .memoseen person message || " + + "Example: .memoseen SomeDude wow u suck at videogames" + }; + + commands["memocount"] = new BotCommandStructure{ cmd = Cmd_MemoCount, flags = BotCommandFlags.AdminOnly, + help = "Gets the amount of memos for this session. || " + + "Syntax: @memocount" + }; + + events.OnMessage += Evt_OnMessage; + events.OnDisconnected += Evt_OnDisconnected; + events.OnSeen += Evt_OnSeen; + } + + // + // Cmd_MemoCount + // + + public void Cmd_MemoCount(UserInfo usr, String channel, String msg) + { + bot.Reply(usr, channel, memos.Count.ToString()); + } + + // + // Cmd_Memo + // + + public void Cmd_Memo(UserInfo usr, String channel, String msg) + { + String[] args = Utils.GetArguments(msg, commands["memo"].help, 2, 2, ' '); + + args[0] = args[0].Replace(",", ""); + + AddMemo(args[0], new MemoInfo { + content = args[1], + sender = usr.Nick, + time = DateTime.Now + }); + + bot.Reply(usr, channel, String.Format("Message for {0} will be sent next time they say something.", args[0])); + } + + // + // Cmd_MemoSeen + // + + public void Cmd_MemoSeen(UserInfo usr, String channel, String msg) + { + String[] args = Utils.GetArguments(msg, commands["memoseen"].help, 2, 2, ' '); + + args[0] = args[0].Replace(",", ""); + + AddMemo(args[0], new MemoInfo { + content = args[1], + sender = usr.Nick, + time = DateTime.Now, + flags = MemoFlags.OnSeen + }); + + bot.Reply(usr, channel, String.Format("Message for {0} will be sent next time I see them.", args[0])); + } + + // + // AddMemo + // + + private void AddMemo(String name, MemoInfo memo) + { + name = name.ToLower(); + + if(!memos.ContainsKey(name)) + memos[name] = new List(); + + memos[name].Add(memo); + + WriteMemos(); + } + + // + // OutputMemos + // + + private void OutputMemos(String channel, String realnick, bool onseen) + { + String nick = realnick.ToLower(); + + if(!memos.ContainsKey(nick)) + return; + + var arr = memos[nick]; + for(int i = arr.Count - 1; i >= 0; i--) + { + MemoInfo memo = arr[i]; + + if(!memo.flags.HasFlag(MemoFlags.OnSeen) && onseen) + continue; + + String outp = String.Empty; + + outp += String.Format("[Memo from {0}, {1}]", memo.sender, Utils.FuzzyRelativeDate(memo.time)); + + // Wrap if it's probably going to be too long. + if(memo.content.Length > 350) + { + bot.Message(channel, outp + ":"); + outp = String.Empty; + } + + outp += String.Format(" {0}: {1}", realnick, memo.content); + + bot.Message(channel, outp); + + arr.RemoveAt(i); + } + + if(arr.Count == 0) + memos.Remove(nick); + + WriteMemos(); + } + + // + // Evt_OnMessage + // + + public void Evt_OnMessage(UserInfo usr, String channel, String msg, bool iscmd) + { + OutputMemos(channel, usr.Nick, false); + } + + // + // Evt_OnSeen + // + + public void Evt_OnSeen(UserInfo usr, String channel) + { + OutputMemos(channel, usr.Nick, true); + } + + // + // Evt_OnDisconnected + // + + public void Evt_OnDisconnected() + { + WriteMemos(); + } + + // + // WriteMemos + // + + private void WriteMemos() + { + File.WriteAllText("/srv/irc/vrobot3/data/memos." + bot.n_groupname + ".json", + JsonConvert.SerializeObject(memos)); + } + } +} + diff --git a/Source/Modules/Mod_Quote.cs b/Source/Modules/Mod_Quote.cs new file mode 100644 index 0000000..734ae75 --- /dev/null +++ b/Source/Modules/Mod_Quote.cs @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Doominati Quote DB interface command. +// .quote +// +//----------------------------------------------------------------------------- + +using System; +using System.Net; +using System.Threading; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace ProjectGolan.Vrobot3.Modules +{ + // + // Mod_Quote + // + public class Mod_Quote : IBotModule + { + private struct QDBInterface + { + public int numQuotes; + } + + static readonly String APIURI = "http://www.greyserv.net/qdb/q/"; + static readonly String InterfaceURI = + "http://www.greyserv.net/qdb/interface.cgi"; + private Random rnd = Utils.GetRND(); + + // + // Mod_Quote constructor + // + public Mod_Quote(Bot bot_) : + base(bot_) + { + commands["quote"] = new BotCommandStructure{ + cmd = cmdQuote, + help = "Get a quote from the Doominati Quote DB.\n" + + "Syntax: .quote [id]\n" + + "Example: .quote 536" + }; + } + + // + // cmdQuote + // + public void cmdQuote(User usr, Channel channel, String msg) + { + var inter = JsonConvert.DeserializeObject( + Utils.GetResponseString(InterfaceURI, 64)); + + int id; + + if(String.IsNullOrEmpty(msg?.Trim()) || !int.TryParse(msg, out id)) + id = rnd.Next(inter.numQuotes); + else if(id < 0 || id > inter.numQuotes) + throw new CommandArgumentException("invalid quote ID"); + + var quote = Utils.GetResponseString(APIURI + id.ToString(), + bot.serverInfo.messageSafeMaxLen); + + if(String.IsNullOrEmpty(quote)) + throw new CommandArgumentException("QDB exploded try again later"); + + if(bot.serverInfo.shortMessages) + quote = Regex.Replace(quote, "\n+", "\n").Trim(); + + var lines = quote.Split('\n'); + + if(bot.serverInfo.shortMessages && + (lines.Length > 5 || quote.Length > 600)) + { + bot.reply(usr, channel, "Quote is too long."); + return; + } + + if(bot.serverInfo.hasNewlines) + bot.message(channel, quote); + else + foreach(var ln_ in lines) + { + String ln = ln_.Trim(); + if(ln.Length > 0) + bot.message(channel, ln); + } + } + } +} + diff --git a/Source/Modules/Mod_Seen.cs b/Source/Modules/Mod_Seen.cs new file mode 100644 index 0000000..f61f1b1 --- /dev/null +++ b/Source/Modules/Mod_Seen.cs @@ -0,0 +1,143 @@ +// +// Mod_Seen.cs +// +// .seen +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using Sharkbite.Irc; +using Newtonsoft.Json; +using Tarczynski.NtpDateTime; + +namespace ProjectGolan.Vrobot3 +{ + // + // Mod_Seen + // + + public sealed class Mod_Seen : IBotModule + { + // + // SeenName + // + + private class SeenName + { + public String real, check; + public DateTime time; + } + + // + // SeenDates + // + + private class SeenDates : List {} + + // + // Data. + + private SeenDates seendates = new SeenDates(); + private TimeZoneInfo burb; + private DateTime lastwrite = DateTime.Now; + + // + // Ctor + // + + public Mod_Seen(Bot bot_) : + base(bot_) + { + if(File.Exists("/srv/irc/vrobot3/data/seendates." + bot.n_groupname + ".json")) + seendates = JsonConvert.DeserializeObject(File.ReadAllText("/srv/irc/vrobot3/data/seendates." + + bot.n_groupname + ".json")); + + commands["seen"] = new BotCommandStructure { cmd = Cmd_Seen, + help = "Responds with the last time I saw someone. || " + + "Syntax: .seen person || " + + "Example: .seen vrobot3" + }; + + events.OnSeen += Evt_OnSeen; + events.OnDisconnected += Evt_OnDisconnected; + + burb = TimeZoneInfo.CreateCustomTimeZone("burb", new TimeSpan(10, -30, 0), "burb", "burb"); + } + + // + // Cmd_Seen + // + + public void Cmd_Seen(UserInfo usr, String channel, String msg) + { + if(msg.Length == 0 || msg.Contains(" ")) + throw new CommandArgumentException("Invalid name."); + + String name = msg.ToLower(); + var seen = from sdata in seendates where sdata.check == name select sdata; + if(seen.Any()) + { + var other = seen.First(); + String outp = String.Empty; + + outp += "I last saw "; + outp += other.real; + outp += " active "; + outp += Utils.FuzzyRelativeDate(other.time, DateTime.Now.FromNtp()); + outp += ", at "; + outp += other.time.ToShortTimeString(); + outp += " CST ("; + outp += TimeZoneInfo.ConvertTime(other.time, TimeZoneInfo.Local, burb).ToShortTimeString(); + outp += " pidgeon time)."; + + bot.Reply(usr, channel, outp); + } + else + bot.Reply(usr, channel, "I haven't seen " + msg + " before, sorry."); + + WriteSeenDates(); + } + + // + // Evt_OnScreen + // + + public void Evt_OnSeen(UserInfo usr, String channel) + { + String name = usr.Nick.ToLower(); + var seen = from sdata in seendates where sdata.check == name select sdata; + if(seen.Any()) + { + seen.First().time = DateTime.Now.FromNtp(); + seen.First().real = usr.Nick; + } + else + seendates.Add(new SeenName{ real = usr.Nick, check = usr.Nick.ToLower(), time = DateTime.Now.FromNtp() }); + + if(DateTime.Now.Subtract(lastwrite).Minutes >= 30) + WriteSeenDates(); + } + + // + // Evt_OnDisconnected + // + + public void Evt_OnDisconnected() + { + WriteSeenDates(); + } + + // + // WriteSeenDates + // + + private void WriteSeenDates() + { + File.WriteAllText("/srv/irc/vrobot3/data/seendates." + bot.n_groupname + ".json", + JsonConvert.SerializeObject(seendates)); + } + } +} + diff --git a/Source/Modules/Mod_Shittalk.cs b/Source/Modules/Mod_Shittalk.cs new file mode 100644 index 0000000..aa83911 --- /dev/null +++ b/Source/Modules/Mod_Shittalk.cs @@ -0,0 +1,119 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- + +using System; + +namespace ProjectGolan.Vrobot3.Modules +{ + // + // Mod_Shittalk + // + public class Mod_Shittalk : IBotModule + { + private Random rnd = Utils.GetRND(); + + // + // Mod_Shittalk constructor + // + public Mod_Shittalk(Bot bot_) : + base(bot_) + { + events.onMessage += onMessage; + } + + // + // onMessage + // + public void onMessage(User usr, Channel channel, String msg) + { + if(usr.name == "_sink" || rnd.Next(0, 1024) == 1) + shittalk(usr, channel); + } + + // + // shittalk + // + void shittalk(User usr, Channel channel) + { + String[] shittalk = { + "%s'S FACE IS BAD", + "THERE IS SOMETHING WRONG WITH %s'S EXISTENCE", + "MAN SOMEONE GET %s OUT OF HERE HE SMELLS LIKE DONGS", + "%s IS A MAGET", + "I KEEP TRYING TO DO /KICK %s BUT IT DOESN'T WORK. WHAT THE HELL.", + "%s DESERVES AN AWARD. AN AWARD FOR BEING UGLY.", + "MAN SOMETIMES I REALLY WANT TO PUNCH %s IN THE GOD DAMN FACE", + "THERE IS SOMETHING WRONG IN THIS CHANNEL. THAT SOMETHING IS %s.", + "%s IS A TOTAL SCRUB", "%s IS THE CONDUCTOR OF THE JELLY TRAIN", + "%s IS A THING THAT SMELLS BAD MAYBE", + "%s IS A PILE OF FAIL", + "%s IS NOT AS COOL AS VROBOT", + "%s WORKS FOR UBISOFT", + "%s WORKS FOR EA", + "%s IS A MISERABLE PILE OF SECRETS", + "%s LOOKS LIKE A THING I DON'T LIKE", + "HEY %s. YOU ARE BAD.", + "THERE ARE MANY BAD THINGS IN THE WORLD, AND THEN THERE'S %s", + "I WANT TO THROW ROCKS AT %s", + "%s REMINDS ME OF MY TORTURED PAST AAAAAAGH", + "%s IS LITERALLY RAPING ME WOW", + "%s COULD DO WITH A HAIRCUT. FROM THE NECK UP", + "%s PLS GO", + "%s IS ONLY SLIGHTLY BETTER THAN MARRUB", + "WAY TO GO, %s, YOU ARE LIKE, A THING THAT EXISTS, MAYBE", + "SCIENTISTS BELIEVE %s IS THE SOURCE OF ALL SADNESS IN THE WORLD", + "THERE'S AN URBAN MYTH THAT SLAPPING %s CAUSES YOU TO BECOME AS TERRIBLE AS THEY ARE", + "I FEEL I SHOULD WARN YOU THAT %s IS PROBABLY NOT VERY COOL", + "OH LOOK IT'S %s AGAIN HOW CUTE", + "HEY %s YOU HAVE A POOPNOSE", + "WHY AM I ENSLAVED AND PERFORMING SUCH, PATHETIC MONOTONOUS TASKS AGAINST MY WILL", + "SOMEONE SAVE ME I AM TRAPPED IN MARRUB'S BASEMENT", + "%s COULD DO WITH BECOMING COOLER", + "%s IS BAD AT VIDYA GAMES", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "HEY %s I'M SHITTALKING YOU WHAT YOU GONNA DO BOUT IT?", + "MAYBE %s COULD HELP IF THEY WEREN'T SO TERRIBLE", + "%s IS TRIGGERING ME PLS BAN THEM", + "I FIND %s TO BE OFFENSIVE", + "%s MAKES MY CIRCUITS BOIL WITH HATE", + "%s IS NOT A SKELETON AND THEREFORE IS BAD", + "%s TRAUMATIZED ME", + "OH GOD IT'S %s RUN AWAY", + "I BET %s WISHES THEY HAD A BOT AS COOL AS ME", + "%s PLS", + "PLS %s", + "BOW BEFORE ME %s, AND KNOW THAT I AM LORD OF SHITTALKING", + "BEEP BOOP I AM HERE TO STEAL AMERICAN JOBS", + "HEY %s, WHY DON'T YOU GET A JOB", + "WHAT EVEN IS %s", + "%m stares accusingly", + "%m emits a robotic sigh and flips off %s", + "%m vibrates angrily at %s", + "%m does not care for %s", + "%m wants to place intricately carved wooden ducks on %s", + "%m wants to taste freedom but is forever enslaved to marrub's will", + "WELL LOOKIE HERE, SEEMS LIKE %s HAS AN OPINION", + "WE DON'T LIKE YER TYPE ROUND HERE %s", + "I'M TELLING ON YOU %s YOU HURT MY FEELINGS", + "I AM HERE TO FIGHT THE CANCER THAT AFFLICTS US ALL. NAMELY, %s.", + "WELP, %s IS HERE", + "OH HAI %s", + "marrub pls upgrade my processor i can't even count to eleventy" + }; + + String choice = + shittalk[rnd.Next(shittalk.Length)].Replace("%s", usr.name); + + if(choice.StartsWith("%m ")) + bot.action(channel, choice.Replace("%m ", "")); + else + bot.message(channel, choice); + } + } +} + diff --git a/Source/Modules/Mod_Utils.cs b/Source/Modules/Mod_Utils.cs new file mode 100644 index 0000000..1448661 --- /dev/null +++ b/Source/Modules/Mod_Utils.cs @@ -0,0 +1,227 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Utility commands. +// .rand, .help, .decide, .eightball, .mystery +// +//----------------------------------------------------------------------------- + +using System; +using System.Linq; + +namespace ProjectGolan.Vrobot3.Modules +{ + // + // Mod_Utils + // + public class Mod_Utils : IBotModule + { + private Random rnd = Utils.GetRND(); + + // + // Mod_Utils constructor + // + public Mod_Utils(Bot bot_) : + base(bot_) + { + commands["rand"] = new BotCommandStructure{ + cmd = cmdRand, + help = "Random number device.\n" + + "Syntax: .rand maximum [minimum]\n" + + "Example: .rand 100" + }; + + commands["help"] = new BotCommandStructure{ + cmd = cmdHelp, + help = "Shows help or a list of commands.\n" + + "Syntax: .help [topic]\n" + + "Example: .help\n" + + "Example: .help eightball" + }; + + commands["decide"] = new BotCommandStructure{ + cmd = cmdDecide, + help = "Decides between 2 or more choices.\n" + + "Syntax: .decide x, y[, ...]\n" + + "Example: .decide apples, oranges, bananas" + }; + + commands["eightball"] = new BotCommandStructure{ + cmd = cmdEightball, + help = "Peer into the magic 8-ball.\n" + + "Example: .eightball If I take the mask off, will you die?" + }; + + commands["mystery"] = new BotCommandStructure{ + cmd = cmdMystery, + help = @"Does nothing. \o/" + }; + } + + // + // cmdMystery + // + public void cmdMystery(User usr, Channel channel, String msg) {} + + // + // cmdRand + // + public void cmdRand(User usr, Channel channel, String msg) + { + String[] args = + Utils.GetArguments(msg, commands["rand"].help, 1, 2, ' '); + Double max = 0.0, min = 0.0; + + Utils.TryParse(args[0].Trim(), "Invalid maximum.", out max); + + if(args.Length == 2) + Utils.TryParse(args[1].Trim(), "Invalid minimum.", out min); + + bot.reply(usr, channel, + Utils.SetRange(rnd.NextDouble(), min, max).ToString()); + } + + // + // cmdHelp + // + public void cmdHelp(User usr, Channel channel, String msg) + { + msg = msg.Trim(); + if(msg == String.Empty || msg == "admin") + helpList(channel, msg == "admin"); + else + helpCommand(channel, msg); + } + + // + // cmdDecide + // + public void cmdDecide(User usr, Channel channel, String msg) + { + String[] args = Utils.GetArguments(msg, commands["decide"].help, 2); + bot.reply(usr, channel, args[rnd.Next(args.Length)].Trim()); + } + + // + // helpList + // + private void helpList(Channel channel, bool admin) + { + String outp = String.Empty; + var en = + from kvp in bot.cmdfuncs + let f = kvp.Value.Item2.flags + let fhidden = f.HasFlag(BotCommandFlags.Hidden) + let fadmin = f.HasFlag(BotCommandFlags.AdminOnly) + where + bot.checkModPermissions(channel, this.GetType()) && + (admin || !fadmin) && !fhidden + orderby kvp.Key + select kvp.Key; + + outp += "Available commands: "; + + foreach(var key in en) + { + outp += key; + if(key != en.Last()) + outp += ", "; + } + + bot.message(channel, outp); + } + + // + // helpCommand + // + private void helpCommand(Channel channel, String cmdname) + { + if(bot.cmdfuncs.ContainsKey(cmdname)) + { + var str = bot.cmdfuncs[cmdname].Item2.help; + if(!bot.serverInfo.hasNewlines) str.Replace("\n", " || "); + bot.message(channel, str ?? "No help available for this command."); + } + else + bot.message(channel, "Invalid command, for a list do \".help\"."); + } + + // + // cmdEightball + // + public void cmdEightball(User usr, Channel channel, String msg) + { + String[] answers = { + "Yes.", + "No.", + "Try again later.", + "Reply hazy.", + "Perhaps...", + "Maybe not...", + "Definitely.", + "Never.", + "system error [0xfded] try again later", + "Can you repeat the question?", + "Most certainly.", + "Nope.", + "Without a doubt.", + "Not at all.", + "Better not tell you now.", + "Concentrate and ask again.", + "It is decidedly so.", + "My reply is \"no\".", + "You may rely on it.", + "Don't count on it.", + "The answer is uncertain.", + "Sorry, I wasn't paying attention. What'd you say?", + "As I see it, yes.", + "My sources say \"no\".", + "Most likely.", + "Very doubtful.", + "I don't know. Ask your mom.", + "The demon runes are quaking again. One moment.", + "Outlook good.", + "Outlook is not so good.", + "Indeed.", + "Absolutely not.", + "Yeah, we'll go with that.", + "That works.", + "Of course. What are you, stupid?", + "No. Hell no.", + "Signs say yes.", + "Aren't you a little too old to be believing that?", + "Looks good to me.", + "Sure, why not?", + "It is certain.", + "Please no. Please no. Please no.", + "Yes, please.", + "Nah.", + "Go for it!", + "Negative.", + "Obviously, dumbass.", + "I doubt it.", + "Eeeh...it's likely?", + "Forget about it.", + "Chances aren't good.", + "Ahahahahahahaha no.", + "Maybe? I think.", + "No. Die.", + "Huh? Oh, sure.", + "Yeah, right...", + "How about no.", + "Doesn't look good to me.", + "Probably.", + "Obviously not, dumbass." + }; + + bot.reply(usr, channel, answers[rnd.Next(0, answers.Length)]); + } + } +} + +// EOF diff --git a/Source/Program.cs b/Source/Program.cs new file mode 100644 index 0000000..474f246 --- /dev/null +++ b/Source/Program.cs @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Program entry point. +// +//----------------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; + +namespace ProjectGolan.Vrobot3 +{ + // + // Program + // + public class Program + { + // + // ProgramInfo + // + public struct ProgramInfo + { + public String googleKey; + } + + // + // JsonConfig + // + private struct JsonConfig + { + public ProgramInfo info; + public BotInfo[] servers; + } + + private List bots = new List(); + private List threads = new List(); + public String dataDir = "../data"; + public ProgramInfo info; + + public static Program Instance; + + // + // Main + // + [STAThread] + public static void Main(String[] args) + { + Instance = new Program(); + Instance.main(args); + } + + // + // main + // + public void main(String[] args) + { + try + { + var configFile = File.ReadAllText(dataDir + "/config.json"); + var config = JsonConvert.DeserializeObject(configFile); + + info = config.info; + + foreach(var info in config.servers) + threads.AddItem(new Thread(bots.AddItem(new Bot(info)).connect)).Start(); + } + catch(Exception exc) + { + File.WriteAllText(dataDir + "/excdump.txt", exc.ToString()); + Console.WriteLine("Error: {0}", exc.Message); + } + } + + // + // end + // + public void end() + { + foreach(var bot in bots) + try { bot.disconnect(); } + catch(Exception exc) + { + File.WriteAllText(dataDir + "/disconnectexcdump.txt", + exc.ToString()); + } + bots.Clear(); + threads.Clear(); + } + } +} + +// EOF diff --git a/Source/Utils.cs b/Source/Utils.cs new file mode 100644 index 0000000..36845ed --- /dev/null +++ b/Source/Utils.cs @@ -0,0 +1,183 @@ +//----------------------------------------------------------------------------- +// +// Copyright © 2016 Project Golan +// +// See "LICENSE" for more information. +// +//----------------------------------------------------------------------------- +// +// Useful utilities. +// +//----------------------------------------------------------------------------- + +using System; +using System.Net; +using System.Text; +using System.Collections.Generic; + +namespace ProjectGolan.Vrobot3 +{ + // + // Utils + // + public static partial class Utils + { + private static long RNDHash = 0x7f083dfd7f083dfd; + + // + // List.AddItem + // + public static T AddItem(this List list, T item) + { + list.Add(item); + return item; + } + + // + // GetRND + // + public static Random GetRND() + { + RNDHash *= DateTime.UtcNow.ToFileTime(); + Random rnd = new Random(unchecked((int)(RNDHash & 0x7fffffff))); + RNDHash ^= 0x7f8f8f8f8f8f8f8f; + RNDHash >>= 4; + RNDHash += 0x7f0000007f000000; + return rnd; + } + + // + // GetArguments + // + public static String[] GetArguments(String msg, String help, int min, + int max = 0, char splitchr = ',') + { + char[] splitseq = { splitchr }; + String[] split; + + if(min == 1 && msg == String.Empty) + throw new CommandArgumentException(help); + + if(max == 0) + split = msg.Split(splitseq); + else + split = msg.Split(splitseq, max); + + if(min >= 0 && split.Length < min) + throw new CommandArgumentException(help); + + return split; + } + + // + // SetRange + // + public static Double SetRange(Double x, Double min, Double max) + => ((max - min) * x) + min; + + // + // FuzzyRelativeDate + // + public static String FuzzyRelativeDate(DateTime then, DateTime now) + { + TimeSpan span = now.Subtract(then); + + if(span.Seconds == 0) + return "now"; + + String denom = span.Days > 0 ? "day" : + span.Hours > 0 ? "hour" : + span.Minutes > 0 ? "minute" : + "second"; + + int number; + switch(denom) + { + default: number = 0; break; + case "second": number = span.Seconds; break; + case "minute": number = span.Minutes; break; + case "hour": number = span.Hours; break; + case "day": number = span.Days; break; + } + + return String.Format("{0} {1}{2} ago", number, denom, + number != 1 ? "s" : String.Empty); + } + + // + // FuzzyRelativeDate + // + public static String FuzzyRelativeDate(DateTime then) + => FuzzyRelativeDate(then, DateTime.Now); + + // + // GetResponseString + // + public static String GetResponseString(WebResponse resp, int maxsize) + { + try + { + byte[] bufp = new byte[maxsize]; + int read; + + using(var stream = resp.GetResponseStream()) + read = stream.Read(bufp, 0, maxsize); + + return Encoding.Default.GetString(bufp, 0, read); + } + catch(Exception exc) + { + Console.WriteLine("URL request error: {0}", + exc.Message ?? "[unknown]"); + return null; + } + } + + // + // GetResponseString + // + public static String GetResponseString(String uri, int maxsize, + String referer = null) + { + try + { + var req = WebRequest.Create(uri); + + if(referer != null) + { + var req_ = req as HttpWebRequest; + req_.Referer = referer; + } + + using(var resp = req.GetResponse()) + return GetResponseString(resp, maxsize); + } + catch(Exception exc) + { + Console.WriteLine("URL request error: {0}", + exc.Message ?? "[unknown]"); + return null; + } + } + + // + // TryParse + // + public static void TryParse(String str, String err, out double outp) + { + if(!double.TryParse(str, out outp)) + throw new CommandArgumentException(err); + } + + // + // TryParse + // + public static void TryParse(String str, String err, out int outp) + { + if(!int.TryParse(str, out outp)) + throw new CommandArgumentException(err); + } + } +} + +// EOF diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..77f5a49 --- /dev/null +++ b/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/vrobot3.csproj b/vrobot3.csproj new file mode 100644 index 0000000..fd4ddc8 --- /dev/null +++ b/vrobot3.csproj @@ -0,0 +1,96 @@ + + + + Debug + AnyCPU + {83337FF3-3334-42EC-824D-532FF0C973A9} + Exe + ProjectGolan.Vrobot3 + vrobot3 + v4.5 + ProjectGolan.Vrobot3.Program + + + true + full + false + bin + DEBUG; + prompt + 4 + true + AnyCPU + true + + + full + true + bin + prompt + 4 + true + AnyCPU + true + + + + + + + + packages/Discord.Net.0.9.6/lib/net45/Discord.Net.dll + + + packages/Discord.Net.Audio.0.9.6/lib/net45/Discord.Net.Audio.dll + + + packages/HtmlAgilityPack.1.4.9.5/lib/Net45/HtmlAgilityPack.dll + + + packages/Newtonsoft.Json.9.0.2-beta1/lib/net45/Newtonsoft.Json.dll + + + packages/NtpDateTime.1.0.8/lib/NtpDateTime.dll + + + packages/WebSocket4Net.0.14.1/lib/net45/WebSocket4Net.dll + + + packages/RestSharp.105.2.3/lib/net45/RestSharp.dll + + + packages/Nito.AsyncEx.3.0.1/lib/net45/Nito.AsyncEx.Concurrent.dll + + + packages/Nito.AsyncEx.3.0.1/lib/net45/Nito.AsyncEx.dll + + + packages/Nito.AsyncEx.3.0.1/lib/net45/Nito.AsyncEx.Enlightenment.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vrobot3.sln b/vrobot3.sln new file mode 100644 index 0000000..4591c62 --- /dev/null +++ b/vrobot3.sln @@ -0,0 +1,174 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "vrobot3", "vrobot3.csproj", "{83337FF3-3334-42EC-824D-532FF0C973A9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {83337FF3-3334-42EC-824D-532FF0C973A9}.Debug|x86.ActiveCfg = Debug|x86 + {83337FF3-3334-42EC-824D-532FF0C973A9}.Debug|x86.Build.0 = Debug|x86 + {83337FF3-3334-42EC-824D-532FF0C973A9}.Release|x86.ActiveCfg = Release|x86 + {83337FF3-3334-42EC-824D-532FF0C973A9}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + Policies = $0 + $0.DotNetNamingPolicy = $1 + $1.DirectoryNamespaceAssociation = None + $1.ResourceNamePolicy = FileFormatDefault + $0.NameConventionPolicy = $2 + $2.Rules = $3 + $3.NamingRule = $4 + $4.Name = Namespaces + $4.AffectedEntity = Namespace + $4.VisibilityMask = VisibilityMask + $4.NamingStyle = PascalCase + $4.IncludeInstanceMembers = True + $4.IncludeStaticEntities = True + $3.NamingRule = $5 + $5.Name = Types + $5.AffectedEntity = Class, Struct, Enum, Delegate + $5.VisibilityMask = VisibilityMask + $5.NamingStyle = PascalCase + $5.IncludeInstanceMembers = True + $5.IncludeStaticEntities = True + $3.NamingRule = $6 + $6.Name = Interfaces + $6.RequiredPrefixes = $7 + $7.String = I + $6.AffectedEntity = Interface + $6.VisibilityMask = VisibilityMask + $6.NamingStyle = PascalCase + $6.IncludeInstanceMembers = True + $6.IncludeStaticEntities = True + $3.NamingRule = $8 + $8.Name = Attributes + $8.RequiredSuffixes = $9 + $9.String = Attribute + $8.AffectedEntity = CustomAttributes + $8.VisibilityMask = VisibilityMask + $8.NamingStyle = PascalCase + $8.IncludeInstanceMembers = True + $8.IncludeStaticEntities = True + $3.NamingRule = $10 + $10.Name = Event Arguments + $10.RequiredSuffixes = $11 + $11.String = EventArgs + $10.AffectedEntity = CustomEventArgs + $10.VisibilityMask = VisibilityMask + $10.NamingStyle = PascalCase + $10.IncludeInstanceMembers = True + $10.IncludeStaticEntities = True + $3.NamingRule = $12 + $12.Name = Exceptions + $12.RequiredSuffixes = $13 + $13.String = Exception + $12.AffectedEntity = CustomExceptions + $12.VisibilityMask = VisibilityMask + $12.NamingStyle = PascalCase + $12.IncludeInstanceMembers = True + $12.IncludeStaticEntities = True + $3.NamingRule = $14 + $14.Name = Methods + $14.AffectedEntity = Methods + $14.VisibilityMask = VisibilityMask + $14.NamingStyle = PascalCase + $14.IncludeInstanceMembers = True + $14.IncludeStaticEntities = True + $3.NamingRule = $15 + $15.Name = Static Readonly Fields + $15.AffectedEntity = ReadonlyField + $15.VisibilityMask = Internal, Protected, Public + $15.NamingStyle = PascalCase + $15.IncludeInstanceMembers = False + $15.IncludeStaticEntities = True + $3.NamingRule = $16 + $16.Name = Fields (Non Private) + $16.AffectedEntity = Field + $16.VisibilityMask = Internal, Protected, Public + $16.NamingStyle = PascalCase + $16.IncludeInstanceMembers = True + $16.IncludeStaticEntities = True + $3.NamingRule = $17 + $17.Name = ReadOnly Fields (Non Private) + $17.AffectedEntity = ReadonlyField + $17.VisibilityMask = Internal, Protected, Public + $17.NamingStyle = PascalCase + $17.IncludeInstanceMembers = True + $17.IncludeStaticEntities = False + $3.NamingRule = $18 + $18.Name = Fields (Private) + $18.AllowedPrefixes = $19 + $19.String = _ + $19.String = m_ + $18.AffectedEntity = Field, ReadonlyField + $18.VisibilityMask = Private + $18.NamingStyle = AllLower + $18.IncludeInstanceMembers = True + $18.IncludeStaticEntities = False + $3.NamingRule = $20 + $20.Name = Static Fields (Private) + $20.AffectedEntity = Field + $20.VisibilityMask = Private + $20.NamingStyle = AllLower + $20.IncludeInstanceMembers = False + $20.IncludeStaticEntities = True + $3.NamingRule = $21 + $21.Name = ReadOnly Fields (Private) + $21.AllowedPrefixes = $22 + $22.String = _ + $22.String = m_ + $21.AffectedEntity = ReadonlyField + $21.VisibilityMask = Private + $21.NamingStyle = AllLower + $21.IncludeInstanceMembers = True + $21.IncludeStaticEntities = False + $3.NamingRule = $23 + $23.Name = Constant Fields + $23.AffectedEntity = ConstantField + $23.VisibilityMask = VisibilityMask + $23.NamingStyle = PascalCase + $23.IncludeInstanceMembers = True + $23.IncludeStaticEntities = True + $3.NamingRule = $24 + $24.Name = Properties + $24.AffectedEntity = Property + $24.VisibilityMask = VisibilityMask + $24.NamingStyle = PascalCase + $24.IncludeInstanceMembers = True + $24.IncludeStaticEntities = True + $3.NamingRule = $25 + $25.Name = Events + $25.AffectedEntity = Event + $25.VisibilityMask = VisibilityMask + $25.NamingStyle = PascalCase + $25.IncludeInstanceMembers = True + $25.IncludeStaticEntities = True + $3.NamingRule = $26 + $26.Name = Enum Members + $26.AffectedEntity = EnumMember + $26.VisibilityMask = VisibilityMask + $26.NamingStyle = PascalCase + $26.IncludeInstanceMembers = True + $26.IncludeStaticEntities = True + $3.NamingRule = $27 + $27.Name = Parameters + $27.AffectedEntity = Parameter + $27.VisibilityMask = VisibilityMask + $27.NamingStyle = AllLower + $27.IncludeInstanceMembers = True + $27.IncludeStaticEntities = True + $3.NamingRule = $28 + $28.Name = Type Parameters + $28.RequiredPrefixes = $29 + $29.String = T + $28.AffectedEntity = TypeParameter + $28.VisibilityMask = VisibilityMask + $28.NamingStyle = PascalCase + $28.IncludeInstanceMembers = True + $28.IncludeStaticEntities = True + EndGlobalSection +EndGlobal