1150 lines
37 KiB
C#
1150 lines
37 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using IRCTokens;
|
|
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
|
|
// ReSharper disable MemberCanBePrivate.Global
|
|
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
|
// ReSharper disable CommentTypo
|
|
// ReSharper disable IdentifierTypo
|
|
|
|
namespace IRCStates
|
|
{
|
|
public class Server
|
|
{
|
|
public const string WhoType = "525"; // randomly generated
|
|
private readonly StatefulDecoder _decoder;
|
|
|
|
private readonly Dictionary<string, string> _tempCaps;
|
|
|
|
public Server(string name)
|
|
{
|
|
Name = name;
|
|
Registered = false;
|
|
Modes = new List<string>();
|
|
Motd = new List<string>();
|
|
_decoder = new StatefulDecoder();
|
|
Users = new Dictionary<string, User>();
|
|
Channels = new Dictionary<string, Channel>();
|
|
ISupport = new ISupport();
|
|
HasCap = false;
|
|
_tempCaps = new Dictionary<string, string>();
|
|
AvailableCaps = new Dictionary<string, string>();
|
|
AgreedCaps = new List<string>();
|
|
}
|
|
|
|
public string Name { get; set; }
|
|
public string NickName { get; set; }
|
|
public string NickNameLower { get; set; }
|
|
public string UserName { get; set; }
|
|
public string HostName { get; set; }
|
|
public string RealName { get; set; }
|
|
public string Account { get; set; }
|
|
public string Away { get; set; }
|
|
|
|
public bool Registered { get; set; }
|
|
public List<string> Modes { get; set; }
|
|
public List<string> Motd { get; set; }
|
|
public Dictionary<string, User> Users { get; set; }
|
|
public Dictionary<string, Channel> Channels { get; set; }
|
|
public Dictionary<string, string> AvailableCaps { get; set; }
|
|
public List<string> AgreedCaps { get; set; }
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
public ISupport ISupport { get; set; }
|
|
public bool HasCap { get; set; }
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"Server(name={Name})";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use <see cref="ISupport"/>'s case mapping to convert to lowercase
|
|
/// </summary>
|
|
/// <param name="str"></param>
|
|
/// <returns></returns>
|
|
public string CaseFold(string str)
|
|
{
|
|
return Casemap.CaseFold(ISupport.CaseMapping, str);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the current nickname this client?
|
|
/// </summary>
|
|
/// <param name="nickname"></param>
|
|
/// <returns></returns>
|
|
private bool IsMe(string nickname)
|
|
{
|
|
return CaseFold(nickname) == NickNameLower;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check for a user - not case-sensitive
|
|
/// </summary>
|
|
/// <param name="nickname"></param>
|
|
/// <returns></returns>
|
|
private bool HasUser(string nickname)
|
|
{
|
|
return Users.ContainsKey(CaseFold(nickname));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get existing user by case-insensitive nickname
|
|
/// </summary>
|
|
/// <param name="nickname"></param>
|
|
/// <returns></returns>
|
|
private User GetUser(string nickname)
|
|
{
|
|
return HasUser(nickname) ? Users[CaseFold(nickname)] : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create and add user
|
|
/// </summary>
|
|
/// <param name="nickname"></param>
|
|
/// <returns></returns>
|
|
private User AddUser(string nickname)
|
|
{
|
|
var user = CreateUser(nickname);
|
|
Users[CaseFold(nickname)] = user;
|
|
return user;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build a new <see cref="User"/> and update correct case-mapped nick
|
|
/// </summary>
|
|
/// <param name="nickname"></param>
|
|
/// <returns></returns>
|
|
private User CreateUser(string nickname)
|
|
{
|
|
var user = new User();
|
|
user.SetNickName(nickname, CaseFold(nickname));
|
|
return user;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the channel a valid ISupport type?
|
|
/// </summary>
|
|
/// <param name="target"></param>
|
|
/// <returns></returns>
|
|
private bool IsChannel(string target)
|
|
{
|
|
return !string.IsNullOrEmpty(target) &&
|
|
ISupport.ChanTypes.Contains(target[0].ToString(CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the channel known to this client?
|
|
/// </summary>
|
|
/// <param name="name"></param>
|
|
/// <returns></returns>
|
|
public bool HasChannel(string name)
|
|
{
|
|
return IsChannel(name) && Channels.ContainsKey(CaseFold(name));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the channel if it's known to us
|
|
/// </summary>
|
|
/// <param name="name"></param>
|
|
/// <returns></returns>
|
|
private Channel GetChannel(string name)
|
|
{
|
|
return HasChannel(name) ? Channels[CaseFold(name)] : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a <see cref="User"/> to a <see cref="Channel"/>
|
|
/// </summary>
|
|
/// <param name="channel"></param>
|
|
/// <param name="user"></param>
|
|
/// <returns>the <see cref="ChannelUser"/> that was added</returns>
|
|
private ChannelUser UserJoin(Channel channel, User user)
|
|
{
|
|
var channelUser = new ChannelUser();
|
|
user.Channels.Add(CaseFold(channel.Name));
|
|
channel.Users[user.NickNameLower] = channelUser;
|
|
return channelUser;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set own <see cref="NickName"/>, <see cref="UserName"/>, and <see cref="HostName"/>
|
|
/// from a given <see cref="Hostmask"/>
|
|
/// </summary>
|
|
/// <param name="hostmask"></param>
|
|
private void SelfHostmask(Hostmask hostmask)
|
|
{
|
|
NickName = hostmask.NickName;
|
|
if (hostmask.UserName != null) UserName = hostmask.UserName;
|
|
if (hostmask.HostName != null) HostName = hostmask.HostName;
|
|
}
|
|
|
|
private void SelfHostmask(string raw)
|
|
{
|
|
SelfHostmask(new Hostmask(raw));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a user from a channel. Used to handle PART and KICK
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <param name="nickName"></param>
|
|
/// <param name="channelName"></param>
|
|
/// <param name="reasonIndex"></param>
|
|
/// <returns></returns>
|
|
private (Emit, User) UserPart(Line line, string nickName, string channelName, int reasonIndex)
|
|
{
|
|
var emit = new Emit();
|
|
var channelLower = CaseFold(channelName);
|
|
if (line.Params.Count >= reasonIndex + 1) emit.Text = line.Params[reasonIndex];
|
|
|
|
User user = null;
|
|
if (HasChannel(channelName))
|
|
{
|
|
var channel = GetChannel(channelName);
|
|
emit.Channel = channel;
|
|
var nickLower = CaseFold(nickName);
|
|
if (HasUser(nickLower))
|
|
{
|
|
user = Users[nickLower];
|
|
user.Channels.Remove(channelLower);
|
|
channel.Users.Remove(nickLower);
|
|
if (!user.Channels.Any()) Users.Remove(nickLower);
|
|
}
|
|
|
|
if (IsMe(nickName))
|
|
{
|
|
Channels.Remove(channelLower);
|
|
foreach (var userToRemove in channel.Users.Keys.Select(u => Users[u]))
|
|
{
|
|
userToRemove.Channels.Remove(channelLower);
|
|
if (!userToRemove.Channels.Any()) Users.Remove(userToRemove.NickNameLower);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (emit, user);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update modes on a <see cref="Channel"/> given modes and parameters
|
|
/// </summary>
|
|
/// <param name="channel"></param>
|
|
/// <param name="modes"></param>
|
|
/// <param name="parameters"></param>
|
|
private void SetChannelModes(Channel channel, IEnumerable<(bool, string)> modes, IList<string> parameters)
|
|
{
|
|
foreach (var (add, c) in modes)
|
|
{
|
|
var listMode = ISupport.ChanModes.ListModes.Contains(c);
|
|
if (ISupport.Prefix.Modes.Contains(c))
|
|
{
|
|
var nicknameLower = CaseFold(parameters.First());
|
|
parameters.RemoveAt(0);
|
|
if (!HasUser(nicknameLower)) continue;
|
|
|
|
var channelUser = channel.Users[nicknameLower];
|
|
if (add)
|
|
{
|
|
if (!channelUser.Modes.Contains(c)) channelUser.Modes.Add(c);
|
|
}
|
|
else if (channelUser.Modes.Contains(c))
|
|
{
|
|
channelUser.Modes.Remove(c);
|
|
}
|
|
}
|
|
else if (add && (listMode ||
|
|
ISupport.ChanModes.SettingBModes.Contains(c) ||
|
|
ISupport.ChanModes.SettingCModes.Contains(c)))
|
|
{
|
|
channel.AddMode(c, parameters.First(), listMode);
|
|
parameters.RemoveAt(0);
|
|
}
|
|
else if (!add && (listMode || ISupport.ChanModes.SettingBModes.Contains(c)))
|
|
{
|
|
channel.RemoveMode(c, parameters.First());
|
|
parameters.RemoveAt(0);
|
|
}
|
|
else if (add)
|
|
{
|
|
channel.AddMode(c, null, false);
|
|
}
|
|
else
|
|
{
|
|
channel.RemoveMode(c, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle incoming bytes
|
|
/// </summary>
|
|
/// <param name="data"></param>
|
|
/// <param name="length"></param>
|
|
/// <returns>parsed lines and emits</returns>
|
|
/// <exception cref="ServerDisconnectedException"></exception>
|
|
public IEnumerable<(Line, Emit)> Receive(byte[] data, int length)
|
|
{
|
|
if (data == null) return null;
|
|
|
|
var lines = _decoder.Push(data, length);
|
|
if (lines == null) throw new ServerDisconnectedException();
|
|
|
|
return lines.Select(l => (l, Parse(l)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delegate a <see cref="Line"/> to the correct handler
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
public Emit Parse(Line line)
|
|
{
|
|
if (line == null) return null;
|
|
|
|
var emit = line.Command switch
|
|
{
|
|
Numeric.RPL_WELCOME => HandleWelcome(line),
|
|
Numeric.RPL_ISUPPORT => HandleISupport(line),
|
|
Numeric.RPL_MOTDSTART => HandleMotd(line),
|
|
Numeric.RPL_MOTD => HandleMotd(line),
|
|
Commands.Nick => HandleNick(line),
|
|
Commands.Join => HandleJoin(line),
|
|
Commands.Part => HandlePart(line),
|
|
Commands.Kick => HandleKick(line),
|
|
Commands.Quit => HandleQuit(line),
|
|
Commands.Error => HandleError(line),
|
|
Numeric.RPL_NAMREPLY => HandleNames(line),
|
|
Numeric.RPL_CREATIONTIME => HandleCreationTime(line),
|
|
Commands.Topic => HandleTopic(line),
|
|
Numeric.RPL_TOPIC => HandleTopicNumeric(line),
|
|
Numeric.RPL_TOPICWHOTIME => HandleTopicTime(line),
|
|
Commands.Mode => HandleMode(line),
|
|
Numeric.RPL_CHANNELMODEIS => HandleChannelModeIs(line),
|
|
Numeric.RPL_UMODEIS => HandleUModeIs(line),
|
|
Commands.Privmsg => HandleMessage(line),
|
|
Commands.Notice => HandleMessage(line),
|
|
Commands.Tagmsg => HandleMessage(line),
|
|
Numeric.RPL_VISIBLEHOST => HandleVisibleHost(line),
|
|
Numeric.RPL_WHOREPLY => HandleWhoReply(line),
|
|
Numeric.RPL_WHOSPCRPL => HandleWhox(line),
|
|
Numeric.RPL_WHOISUSER => HandleWhoIsUser(line),
|
|
Commands.Chghost => HandleChghost(line),
|
|
Commands.Setname => HandleSetname(line),
|
|
Commands.Away => HandleAway(line),
|
|
Commands.Account => HandleAccount(line),
|
|
Commands.Cap => HandleCap(line),
|
|
Numeric.RPL_LOGGEDIN => HandleLoggedIn(line),
|
|
Numeric.RPL_LOGGEDOUT => HandleLoggedOut(line),
|
|
_ => null
|
|
};
|
|
|
|
if (emit != null)
|
|
emit.Command = line.Command;
|
|
else
|
|
emit = new Emit();
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles SETNAME command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleSetname(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
var realname = line.Params[0];
|
|
var nicknameLower = CaseFold(line.Hostmask.NickName);
|
|
|
|
if (IsMe(nicknameLower))
|
|
{
|
|
emit.Self = true;
|
|
RealName = realname;
|
|
}
|
|
|
|
if (Users.TryGetValue(nicknameLower, out var user))
|
|
{
|
|
emit.User = user;
|
|
user.RealName = realname;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles AWAY command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleAway(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
var away = line.Params.FirstOrDefault();
|
|
var nicknameLower = CaseFold(line.Hostmask.NickName);
|
|
|
|
if (IsMe(nicknameLower))
|
|
{
|
|
emit.Self = true;
|
|
Away = away;
|
|
}
|
|
|
|
if (Users.TryGetValue(nicknameLower, out var user))
|
|
{
|
|
emit.User = user;
|
|
user.Away = away;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles ACCOUNT command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleAccount(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
var account = line.Params[0].Trim('*');
|
|
var nicknameLower = CaseFold(line.Hostmask.NickName);
|
|
|
|
if (IsMe(nicknameLower))
|
|
{
|
|
emit.Self = true;
|
|
Account = account;
|
|
}
|
|
|
|
if (Users.TryGetValue(nicknameLower, out var user))
|
|
{
|
|
emit.User = user;
|
|
user.Account = account;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles CAP command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleCap(Line line)
|
|
{
|
|
HasCap = true;
|
|
var subcommand = line.Params[1].ToUpperInvariant();
|
|
var multiline = line.Params[2] == "*";
|
|
var caps = line.Params[multiline ? 3 : 2];
|
|
|
|
var tokens = new Dictionary<string, string>();
|
|
var tokensStr = new List<string>();
|
|
foreach (var cap in caps.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
tokensStr.Add(cap);
|
|
var kv = cap.Split('=', 2);
|
|
tokens[kv[0]] = kv.Length > 1 ? kv[1] : string.Empty;
|
|
}
|
|
|
|
var emit = new Emit {Subcommand = subcommand, Finished = !multiline, Tokens = tokensStr};
|
|
|
|
switch (subcommand)
|
|
{
|
|
case "LS":
|
|
_tempCaps.UpdateWith(tokens);
|
|
if (!multiline)
|
|
{
|
|
AvailableCaps.UpdateWith(_tempCaps);
|
|
_tempCaps.Clear();
|
|
}
|
|
|
|
break;
|
|
case "NEW":
|
|
AvailableCaps.UpdateWith(tokens);
|
|
break;
|
|
case "DEL":
|
|
foreach (var key in tokens.Keys.Where(key => AvailableCaps.ContainsKey(key)))
|
|
{
|
|
AvailableCaps.Remove(key);
|
|
if (AgreedCaps.Contains(key)) AgreedCaps.Remove(key);
|
|
}
|
|
|
|
break;
|
|
case "ACK":
|
|
foreach (var key in tokens.Keys)
|
|
if (key.StartsWith('-'))
|
|
{
|
|
var k = key[1..];
|
|
if (AgreedCaps.Contains(k)) AgreedCaps.Remove(k);
|
|
}
|
|
else if (!AgreedCaps.Contains(key) && AvailableCaps.ContainsKey(key))
|
|
{
|
|
AgreedCaps.Add(key);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_LOGGEDIN numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleLoggedIn(Line line)
|
|
{
|
|
SelfHostmask(new Hostmask(line.Params[1]));
|
|
Account = line.Params[2];
|
|
return new Emit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles CHGHOST command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleChghost(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
var username = line.Params[0];
|
|
var hostname = line.Params[1];
|
|
var nicknameLower = CaseFold(line.Hostmask.NickName);
|
|
|
|
if (IsMe(nicknameLower))
|
|
{
|
|
emit.Self = true;
|
|
UserName = username;
|
|
HostName = hostname;
|
|
}
|
|
|
|
if (Users.TryGetValue(nicknameLower, out var user))
|
|
{
|
|
emit.User = user;
|
|
user.UserName = username;
|
|
user.HostName = hostname;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_WHOISUSER numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleWhoIsUser(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
var nickname = line.Params[1];
|
|
var username = line.Params[2];
|
|
var hostname = line.Params[3];
|
|
var realname = line.Params[5];
|
|
|
|
if (IsMe(nickname))
|
|
{
|
|
emit.Self = true;
|
|
UserName = username;
|
|
HostName = hostname;
|
|
RealName = realname;
|
|
}
|
|
|
|
if (HasUser(nickname))
|
|
{
|
|
var user = Users[CaseFold(nickname)];
|
|
emit.User = user;
|
|
user.UserName = username;
|
|
user.HostName = hostname;
|
|
user.RealName = realname;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_WHOSPCRPL numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleWhox(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
if (line.Params[1] == WhoType && line.Params.Count == 8)
|
|
{
|
|
var nickname = line.Params[5];
|
|
var username = line.Params[2];
|
|
var hostname = line.Params[4];
|
|
var realname = line.Params[7];
|
|
var account = line.Params[6] == "0" ? null : line.Params[6];
|
|
|
|
if (IsMe(nickname))
|
|
{
|
|
emit.Self = true;
|
|
UserName = username;
|
|
HostName = hostname;
|
|
RealName = realname;
|
|
Account = account;
|
|
}
|
|
|
|
if (HasUser(nickname))
|
|
{
|
|
var user = Users[CaseFold(nickname)];
|
|
emit.User = user;
|
|
user.UserName = username;
|
|
user.HostName = hostname;
|
|
user.RealName = realname;
|
|
user.Account = account;
|
|
}
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_WHOREPLY numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleWhoReply(Line line)
|
|
{
|
|
var emit = new Emit {Target = line.Params[1]};
|
|
var nickname = line.Params[5];
|
|
var username = line.Params[2];
|
|
var hostname = line.Params[3];
|
|
var realname = line.Params[7].Split(' ', 2)[1];
|
|
|
|
if (IsMe(nickname))
|
|
{
|
|
emit.Self = true;
|
|
UserName = username;
|
|
HostName = hostname;
|
|
RealName = realname;
|
|
}
|
|
|
|
if (HasUser(nickname))
|
|
{
|
|
var user = Users[CaseFold(nickname)];
|
|
emit.User = user;
|
|
user.UserName = username;
|
|
user.HostName = hostname;
|
|
user.RealName = realname;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_VISIBLEHOST numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleVisibleHost(Line line)
|
|
{
|
|
var split = line.Params[1].Split('@', 2);
|
|
switch (split.Length)
|
|
{
|
|
case 1:
|
|
HostName = split[0];
|
|
break;
|
|
case 2:
|
|
HostName = split[1];
|
|
UserName = split[0];
|
|
break;
|
|
}
|
|
|
|
return new Emit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles PRIVMSG, NOTICE, and TAGMSG commands
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleMessage(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
var message = line.Params.Count > 1 ? line.Params[1] : null;
|
|
if (message != null) emit.Text = message;
|
|
|
|
var nick = CaseFold(line.Hostmask.NickName);
|
|
if (IsMe(nick))
|
|
{
|
|
emit.SelfSource = true;
|
|
SelfHostmask(line.Hostmask);
|
|
}
|
|
|
|
var user = GetUser(nick) ?? AddUser(nick);
|
|
emit.User = user;
|
|
|
|
if (line.Hostmask.UserName != null) user.UserName = line.Hostmask.UserName;
|
|
if (line.Hostmask.HostName != null) user.HostName = line.Hostmask.HostName;
|
|
|
|
var target = line.Params[0];
|
|
var statusMsg = new List<string>();
|
|
while (target.Length > 0)
|
|
{
|
|
var t = target[0].ToString(CultureInfo.InvariantCulture);
|
|
if (ISupport.StatusMsg.Contains(t))
|
|
{
|
|
statusMsg.Add(t);
|
|
target = target[1..];
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
emit.Target = line.Params[0];
|
|
|
|
if (IsChannel(target) && HasChannel(target))
|
|
emit.Channel = GetChannel(target);
|
|
else if (IsMe(target)) emit.SelfTarget = true;
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_UMODEIS numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleUModeIs(Line line)
|
|
{
|
|
foreach (var c in line.Params[1]
|
|
.TrimStart('+')
|
|
.Select(m => m.ToString(CultureInfo.InvariantCulture))
|
|
.Where(m => !Modes.Contains(m)))
|
|
Modes.Add(c);
|
|
|
|
return new Emit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_CHANNELMODEIS numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleChannelModeIs(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
if (HasChannel(line.Params[1]))
|
|
{
|
|
var channel = GetChannel(line.Params[1]);
|
|
emit.Channel = channel;
|
|
var modes = line.Params[2]
|
|
.TrimStart('+')
|
|
.Select(p => (true, p.ToString(CultureInfo.InvariantCulture)));
|
|
var parameters = line.Params.Skip(3).ToList();
|
|
SetChannelModes(channel, modes, parameters);
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles MODE command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleMode(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
var target = line.Params[0];
|
|
var modeString = line.Params[1];
|
|
var parameters = line.Params.Skip(2).ToList();
|
|
|
|
var modifier = '+';
|
|
var modes = new List<(bool, string)>();
|
|
var tokens = new List<string>();
|
|
|
|
foreach (var c in modeString)
|
|
if (new[] {'+', '-'}.Contains(c))
|
|
{
|
|
modifier = c;
|
|
}
|
|
else
|
|
{
|
|
modes.Add((modifier == '+', c.ToString(CultureInfo.InvariantCulture)));
|
|
tokens.Add($"{modifier}{c}");
|
|
}
|
|
|
|
emit.Tokens = tokens;
|
|
|
|
if (IsMe(target))
|
|
{
|
|
emit.SelfTarget = true;
|
|
foreach (var (add, c) in modes)
|
|
if (add && !Modes.Contains(c))
|
|
Modes.Add(c);
|
|
else if (Modes.Contains(c)) Modes.Remove(c);
|
|
}
|
|
else if (HasChannel(target))
|
|
{
|
|
var channel = GetChannel(CaseFold(target));
|
|
emit.Channel = channel;
|
|
SetChannelModes(channel, modes, parameters);
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_TOPICWHOTIME numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleTopicTime(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
if (HasChannel(line.Params[1]))
|
|
{
|
|
var channel = GetChannel(line.Params[1]);
|
|
emit.Channel = channel;
|
|
channel.TopicSetter = line.Params[2];
|
|
channel.TopicTime = DateTimeOffset
|
|
.FromUnixTimeSeconds(int.Parse(line.Params[3], CultureInfo.InvariantCulture)).DateTime;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_TOPIC numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleTopicNumeric(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
if (HasChannel(line.Params[1]))
|
|
{
|
|
var channel = GetChannel(line.Params[1]);
|
|
emit.Channel = channel;
|
|
channel.Topic = line.Params[2];
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles TOPIC command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleTopic(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
if (HasChannel(line.Params[0]))
|
|
{
|
|
var channel = GetChannel(line.Params[0]);
|
|
emit.Channel = channel;
|
|
channel.Topic = line.Params[1];
|
|
channel.TopicSetter = line.Hostmask.ToString();
|
|
channel.TopicTime = DateTime.UtcNow;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_CREATIONTIME numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleCreationTime(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
if (HasChannel(line.Params[1]))
|
|
{
|
|
var channel = GetChannel(line.Params[1]);
|
|
emit.Channel = channel;
|
|
channel.Created = DateTimeOffset
|
|
.FromUnixTimeSeconds(int.Parse(line.Params[2], CultureInfo.InvariantCulture)).DateTime;
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_NAMREPLY numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleNames(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
if (!HasChannel(line.Params[2])) return emit;
|
|
|
|
var channel = GetChannel(line.Params[2]);
|
|
emit.Channel = channel;
|
|
var nicknames = line.Params[3].Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
var users = new List<User>();
|
|
emit.Users = users;
|
|
|
|
foreach (var nick in nicknames)
|
|
{
|
|
var modes = "";
|
|
foreach (var c in nick)
|
|
{
|
|
var mode = ISupport.Prefix.FromPrefix(c);
|
|
if (mode != null)
|
|
modes += mode;
|
|
else
|
|
break;
|
|
}
|
|
|
|
var hostmask = new Hostmask(nick[modes.Length..]);
|
|
var user = GetUser(hostmask.NickName) ?? AddUser(hostmask.NickName);
|
|
|
|
users.Add(user);
|
|
var channelUser = UserJoin(channel, user);
|
|
|
|
if (hostmask.UserName != null) user.UserName = hostmask.UserName;
|
|
if (hostmask.HostName != null) user.HostName = hostmask.HostName;
|
|
|
|
if (IsMe(hostmask.NickName)) SelfHostmask(hostmask);
|
|
|
|
foreach (var mode in modes.Select(c => c.ToString(CultureInfo.InvariantCulture)))
|
|
if (!channelUser.Modes.Contains(mode))
|
|
channelUser.Modes.Add(mode);
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles ERROR command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleError(Line line)
|
|
{
|
|
Users.Clear();
|
|
Channels.Clear();
|
|
return new Emit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles QUIT command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleQuit(Line line)
|
|
{
|
|
var emit = new Emit();
|
|
var nick = line.Hostmask.NickName;
|
|
if (line.Params.Any()) emit.Text = line.Params[0];
|
|
|
|
if (IsMe(nick) || line.Source == null)
|
|
{
|
|
emit.Self = true;
|
|
Users.Clear();
|
|
Channels.Clear();
|
|
}
|
|
else if (HasUser(nick))
|
|
{
|
|
var user = GetUser(nick);
|
|
Users.Remove(user.NickNameLower);
|
|
emit.User = user;
|
|
foreach (var channel in user.Channels.Select(c => Channels[c]))
|
|
channel.Users.Remove(user.NickNameLower);
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_LOGGEDOUT numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleLoggedOut(Line line)
|
|
{
|
|
Account = null;
|
|
SelfHostmask(line.Params[1]);
|
|
return new Emit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles KICK command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleKick(Line line)
|
|
{
|
|
var (emit, kicked) = UserPart(line, line.Params[1], line.Params[0], 2);
|
|
if (kicked != null)
|
|
{
|
|
emit.UserTarget = kicked;
|
|
if (IsMe(kicked.NickName)) emit.Self = true;
|
|
|
|
var kicker = line.Hostmask.NickName;
|
|
if (IsMe(kicker)) emit.SelfSource = true;
|
|
|
|
emit.UserSource = GetUser(kicker) ?? CreateUser(kicker);
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles PART command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandlePart(Line line)
|
|
{
|
|
var (emit, user) = UserPart(line, line.Hostmask.NickName, line.Params[0], 1);
|
|
if (user != null)
|
|
{
|
|
emit.User = user;
|
|
emit.Self = IsMe(user.NickName);
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles JOIN command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleJoin(Line line)
|
|
{
|
|
var extended = line.Params.Count == 3;
|
|
var account = extended ? line.Params[1].Trim('*') : null;
|
|
var realname = extended ? line.Params[2] : null;
|
|
var emit = new Emit();
|
|
|
|
var channelName = line.Params[0];
|
|
var nick = line.Hostmask.NickName;
|
|
|
|
// handle own join
|
|
if (IsMe(nick))
|
|
{
|
|
emit.Self = true;
|
|
if (!HasChannel(channelName))
|
|
{
|
|
var channel = new Channel();
|
|
channel.SetName(channelName, CaseFold(channelName));
|
|
Channels[CaseFold(channelName)] = channel;
|
|
}
|
|
|
|
SelfHostmask(line.Hostmask);
|
|
if (extended)
|
|
{
|
|
Account = account;
|
|
RealName = realname;
|
|
}
|
|
}
|
|
|
|
if (HasChannel(channelName))
|
|
{
|
|
var channel = GetChannel(channelName);
|
|
emit.Channel = channel;
|
|
|
|
if (!HasUser(nick)) AddUser(nick);
|
|
|
|
var user = GetUser(nick);
|
|
emit.User = user;
|
|
if (line.Hostmask.UserName != null) user.UserName = line.Hostmask.UserName;
|
|
if (line.Hostmask.HostName != null) user.HostName = line.Hostmask.HostName;
|
|
if (extended)
|
|
{
|
|
user.Account = account;
|
|
user.RealName = realname;
|
|
}
|
|
|
|
UserJoin(channel, user);
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles NICK command
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleNick(Line line)
|
|
{
|
|
var newNick = line.Params[0];
|
|
var oldNick = line.Hostmask.NickName;
|
|
|
|
var emit = new Emit();
|
|
|
|
if (HasUser(oldNick))
|
|
{
|
|
var user = GetUser(oldNick);
|
|
var oldNickLower = user.NickNameLower;
|
|
var newNickLower = CaseFold(newNick);
|
|
|
|
emit.User = user;
|
|
Users.Remove(oldNickLower);
|
|
Users[newNickLower] = user;
|
|
user.SetNickName(newNick, newNickLower);
|
|
|
|
foreach (var channelLower in user.Channels)
|
|
{
|
|
var channel = GetChannel(channelLower);
|
|
var channelUser = channel.Users[oldNickLower];
|
|
channel.Users.Remove(oldNickLower);
|
|
channel.Users[newNickLower] = channelUser;
|
|
}
|
|
}
|
|
|
|
if (IsMe(oldNick))
|
|
{
|
|
emit.Self = true;
|
|
NickName = newNick;
|
|
NickNameLower = CaseFold(newNick);
|
|
}
|
|
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_MOTDSTART and RPL_MOTD numerics
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleMotd(Line line)
|
|
{
|
|
if (line.Command == Numeric.RPL_MOTDSTART) Motd.Clear();
|
|
|
|
var emit = new Emit {Text = line.Params[1]};
|
|
Motd.Add(line.Params[1]);
|
|
return emit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_ISUPPORT numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleISupport(Line line)
|
|
{
|
|
ISupport = new ISupport();
|
|
ISupport.Parse(line.Params);
|
|
return new Emit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RPL_WELCOME numeric
|
|
/// </summary>
|
|
/// <param name="line"></param>
|
|
/// <returns></returns>
|
|
private Emit HandleWelcome(Line line)
|
|
{
|
|
NickName = line.Params[0];
|
|
NickNameLower = CaseFold(line.Params[0]);
|
|
Registered = true;
|
|
return new Emit();
|
|
}
|
|
}
|
|
}
|