Browse Source

Initial commit.

master
Marrub 4 years ago
commit
ae778d37e0
26 changed files with 3312 additions and 0 deletions
  1. +7
    -0
      .gitignore
  2. +14
    -0
      Properties/AssemblyInfo.cs
  3. +234
    -0
      Source/Bot.cs
  4. +53
    -0
      Source/BotClient.cs
  5. +238
    -0
      Source/BotClientDiscord.cs
  6. +45
    -0
      Source/BotClientIRC.cs
  7. +106
    -0
      Source/BotEvents.cs
  8. +81
    -0
      Source/BotInfo.cs
  9. +123
    -0
      Source/BotModule.cs
  10. +31
    -0
      Source/Exceptions.cs
  11. +81
    -0
      Source/Links.cs
  12. +82
    -0
      Source/Modules/Mod_Admin.cs
  13. +202
    -0
      Source/Modules/Mod_Audio.cs
  14. +112
    -0
      Source/Modules/Mod_Fun.cs
  15. +182
    -0
      Source/Modules/Mod_Idgames.cs
  16. +336
    -0
      Source/Modules/Mod_Links.cs
  17. +236
    -0
      Source/Modules/Mod_Memo.cs
  18. +96
    -0
      Source/Modules/Mod_Quote.cs
  19. +143
    -0
      Source/Modules/Mod_Seen.cs
  20. +119
    -0
      Source/Modules/Mod_Shittalk.cs
  21. +227
    -0
      Source/Modules/Mod_Utils.cs
  22. +100
    -0
      Source/Program.cs
  23. +183
    -0
      Source/Utils.cs
  24. +11
    -0
      packages.config
  25. +96
    -0
      vrobot3.csproj
  26. +174
    -0
      vrobot3.sln

+ 7
- 0
.gitignore View File

@@ -0,0 +1,7 @@
.nuget
obj
bin
data
packages
*.swp
Makefile

+ 14
- 0
Properties/AssemblyInfo.cs View File

@@ -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.*")]

+ 234
- 0
Source/Bot.cs View File

@@ -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<String, BotCommandStructure> {}

//
// Bot
//
public partial class Bot
{
public List<IBotModule> modules = new List<IBotModule>();
public CommandFuncDict cmdfuncs = new CommandFuncDict();
public readonly BotInfo info;

private Dictionary<ulong, String> lastLine = new Dictionary<ulong, String>();
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<bool> 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

+ 53
- 0
Source/BotClient.cs View File

@@ -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

+ 238
- 0
Source/BotClientDiscord.cs View File

@@ -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

+ 45
- 0
Source/BotClientIRC.cs View File

@@ -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

+ 106
- 0
Source/BotEvents.cs View File

@@ -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<BotCommandStructure> 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

+ 81
- 0
Source/BotInfo.cs View File

@@ -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<String, String[]> 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

+ 123
- 0
Source/BotModule.cs View File

@@ -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

+ 31
- 0
Source/Exceptions.cs View File

@@ -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

+ 81
- 0
Source/Links.cs View File

@@ -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(
@"((?<method>[^:/?# ]+)?:)" +
@"(//(?<host>[^/?# ]*))" +
@"(?<path>[^?# ]*)" +
@"(?<query>\?([^# ]*))?" +
@"(?<tag>#(.*))?");

//
// 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

+ 82
- 0
Source/Modules/Mod_Admin.cs View File

@@ -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());
}
}
}


+ 202
- 0
Source/Modules/Mod_Audio.cs View File

@@ -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<QueueItem> items;
int pos;

public Queue()
{
this.curTime = new TimeSpan();
this.items = new List<QueueItem>();
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<bool> 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();
}
}


+ 112
- 0
Source/Modules/Mod_Fun.cs View File

@@ -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

+ 182
- 0
Source/Modules/Mod_Idgames.cs View File

@@ -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 ? "+)" : ")")));
}
}
}
}


+ 336
- 0
Source/Modules/Mod_Links.cs View File

@@ -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(@"\<title\>(?<realtitle>.+?)\</title\>").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(
@"((?<method>[^:/?# ]+):)" +
@"(//(?<host>[^/?# ]*))" +
@"(?<path>[^?# ]*)" +
@"(?<query>\?([^# ]*))?" +
@"(?<tag>#(.*))?"
);

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] <marrub> file:///srv/www/marrub/oldmen.html
// [22:19] <vrobot3> [ 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<String, URIHandler> handlers = new Dictionary<String, URIHandler>(){
{ "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]");
}
}
}
}


+ 236
- 0
Source/Modules/Mod_Memo.cs View File

@@ -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<String, List<MemoInfo>> {}

//
// 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<MemoDict>(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<MemoInfo>();

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();
}