ircsharp/IRCStates/Server.cs

942 lines
31 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Globalization;
using System.Linq;
using IRCTokens;
namespace IRCStates
{
public class Server
{
public const string WhoType = "525"; // randomly generated
private readonly StatefulDecoder _decoder;
private 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; }
public ISupport ISupport { get; set; }
public bool HasCap { get; set; }
public override string ToString()
{
return $"Server(name={Name})";
}
public string CaseFold(string str)
{
return Casemap.CaseFold(ISupport.CaseMapping, str);
}
private bool CaseFoldEquals(string s1, string s2)
{
return CaseFold(s1) == CaseFold(s2);
}
private bool IsMe(string nickname)
{
return CaseFold(nickname) == NickNameLower;
}
private bool HasUser(string nickname)
{
return Users.ContainsKey(CaseFold(nickname));
}
private User AddUser(string nickname, string nicknameLower)
{
var user = CreateUser(nickname, nicknameLower);
Users[nicknameLower] = user;
return user;
}
private User CreateUser(string nickname, string nicknameLower)
{
var user = new User();
user.SetNickName(nickname, nicknameLower);
return user;
}
private bool IsChannel(string target)
{
return !string.IsNullOrEmpty(target) &&
ISupport.ChanTypes.Contains(target[0].ToString(CultureInfo.InvariantCulture));
}
public bool HasChannel(string name)
{
return Channels.ContainsKey(CaseFold(name));
}
private Channel GetChannel(string name)
{
return HasChannel(name) ? Channels[name] : null;
}
private ChannelUser UserJoin(Channel channel, User user)
{
var channelUser = new ChannelUser();
user.Channels.Add(CaseFold(channel.Name));
channel.Users[user.NickNameLower] = channelUser;
return channelUser;
}
private void SelfHostmask(Hostmask hostmask)
{
NickName = hostmask.NickName;
if (hostmask.UserName != null) UserName = hostmask.UserName;
if (hostmask.HostName != null) HostName = hostmask.HostName;
}
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 = Channels[channelLower];
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);
}
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);
}
}
}
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))).ToList();
}
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;
}
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.ContainsKey(nicknameLower))
{
var user = Users[nicknameLower];
emit.User = user;
user.RealName = realname;
}
return emit;
}
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.ContainsKey(nicknameLower))
{
var user = Users[nicknameLower];
emit.User = user;
user.Away = away;
}
return emit;
}
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.ContainsKey(nicknameLower))
{
var user = Users[nicknameLower];
emit.User = user;
user.Account = account;
}
return emit;
}
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.Substring(1);
if (AgreedCaps.Contains(k)) AgreedCaps.Remove(k);
}
else if (!AgreedCaps.Contains(key) && AvailableCaps.ContainsKey(key))
{
AgreedCaps.Add(key);
}
break;
}
return emit;
}
private Emit HandleLoggedIn(Line line)
{
SelfHostmask(new Hostmask(line.Params[1]));
Account = line.Params[2];
return new Emit();
}
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.ContainsKey(nicknameLower))
{
var user = Users[nicknameLower];
emit.User = user;
user.UserName = username;
user.HostName = hostname;
}
return emit;
}
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;
}
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;
}
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;
}
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();
}
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 nickLower = CaseFold(line.Hostmask.NickName);
if (IsMe(nickLower))
{
emit.SelfSource = true;
SelfHostmask(line.Hostmask);
}
var user = HasUser(nickLower)
? Users[nickLower]
: AddUser(line.Hostmask.NickName, nickLower);
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.Substring(1);
}
else
break;
}
emit.Target = line.Params[0];
if (IsChannel(target) && HasChannel(target))
{
emit.Channel = Channels[CaseFold(target)];
}
else if (IsMe(target))
{
emit.SelfTarget = true;
}
return emit;
}
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();
}
private Emit HandleChannelModeIs(Line line)
{
var emit = new Emit();
if (HasChannel(line.Params[1]))
{
var channel = Channels[CaseFold(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;
}
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;
}
private Emit HandleTopicTime(Line line)
{
var emit = new Emit();
var channelLower = CaseFold(line.Params[1]);
if (Channels.ContainsKey(channelLower))
{
var channel = Channels[channelLower];
emit.Channel = channel;
channel.TopicSetter = line.Params[2];
channel.TopicTime = DateTimeOffset
.FromUnixTimeSeconds(int.Parse(line.Params[3], CultureInfo.InvariantCulture)).DateTime;
}
return emit;
}
private Emit HandleTopicNumeric(Line line)
{
var emit = new Emit();
var channelLower = CaseFold(line.Params[1]);
if (Channels.ContainsKey(channelLower))
{
var channel = Channels[channelLower];
emit.Channel = channel;
Channels[channelLower].Topic = line.Params[2];
}
return emit;
}
private Emit HandleTopic(Line line)
{
var emit = new Emit();
var channelLower = CaseFold(line.Params[0]);
if (Channels.ContainsKey(channelLower))
{
var channel = Channels[channelLower];
emit.Channel = channel;
channel.Topic = line.Params[1];
channel.TopicSetter = line.Hostmask.ToString();
channel.TopicTime = DateTime.UtcNow;
}
return emit;
}
private Emit HandleCreationTime(Line line)
{
var emit = new Emit();
var channelLower = CaseFold(line.Params[1]);
if (Channels.ContainsKey(channelLower))
{
var channel = Channels[channelLower];
emit.Channel = channel;
channel.Created = DateTimeOffset
.FromUnixTimeSeconds(int.Parse(line.Params[2], CultureInfo.InvariantCulture)).DateTime;
}
return emit;
}
private Emit HandleNames(Line line)
{
var emit = new Emit();
var channelLower = CaseFold(line.Params[2]);
if (!Channels.ContainsKey(channelLower)) return emit;
var channel = Channels[channelLower];
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.Substring(modes.Length));
var nickLower = CaseFold(hostmask.NickName);
if (!Users.ContainsKey(nickLower)) AddUser(hostmask.NickName, nickLower);
var user = Users[nickLower];
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(nickLower)) SelfHostmask(hostmask);
foreach (var mode in modes.Select(c => c.ToString(CultureInfo.InvariantCulture)))
if (!channelUser.Modes.Contains(mode))
channelUser.Modes.Add(mode);
}
return emit;
}
private Emit HandleError(Line line)
{
Users.Clear();
Channels.Clear();
return new Emit();
}
private Emit HandleQuit(Line line)
{
var emit = new Emit();
var nickLower = CaseFold(line.Hostmask.NickName);
if (line.Params.Any()) emit.Text = line.Params[0];
if (IsMe(nickLower) || line.Source == null)
{
emit.Self = true;
Users.Clear();
Channels.Clear();
}
else if (Users.ContainsKey(nickLower))
{
var user = Users[nickLower];
Users.Remove(nickLower);
emit.User = user;
foreach (var channel in user.Channels.Select(c => Channels[c]))
channel.Users.Remove(user.NickNameLower);
}
return emit;
}
private Emit HandleLoggedOut(Line line)
{
Account = null;
SelfHostmask(new Hostmask(line.Params[1]));
return new Emit();
}
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 kickerLower = CaseFold(line.Hostmask.NickName);
if (IsMe(kickerLower)) emit.SelfSource = true;
emit.UserSource = Users.ContainsKey(kickerLower)
? Users[kickerLower]
: CreateUser(line.Hostmask.NickName, kickerLower);
}
return emit;
}
private Emit HandlePart(Line line)
{
var (emit, user) = UserPart(line, line.Hostmask.NickName, line.Params[0], 1);
if (user != null)
{
emit.User = user;
if (IsMe(user.NickName)) emit.Self = true;
}
return emit;
}
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 channelLower = CaseFold(line.Params[0]);
var nickLower = CaseFold(line.Hostmask.NickName);
// handle own join
if (IsMe(nickLower))
{
emit.Self = true;
if (!HasChannel(channelLower))
{
var channel = new Channel();
channel.SetName(line.Params[0], channelLower);
Channels[channelLower] = channel;
}
SelfHostmask(line.Hostmask);
if (extended)
{
Account = account;
RealName = realname;
}
}
if (HasChannel(channelLower))
{
var channel = Channels[channelLower];
emit.Channel = channel;
if (!HasUser(nickLower)) AddUser(line.Hostmask.NickName, nickLower);
var user = Users[nickLower];
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;
}
private Emit HandleNick(Line line)
{
var nick = line.Params[0];
var nickLower = CaseFold(line.Hostmask.NickName);
var emit = new Emit();
if (Users.ContainsKey(nickLower))
{
var user = Users[nickLower];
Users.Remove(nickLower);
emit.User = user;
var oldNickLower = user.NickNameLower;
var newNickLower = CaseFold(nick);
user.SetNickName(nick, newNickLower);
Users[newNickLower] = user;
foreach (var channelLower in user.Channels)
{
var channel = Channels[channelLower];
var channelUser = channel.Users[oldNickLower];
channel.Users.Remove(oldNickLower);
channel.Users[newNickLower] = channelUser;
}
}
if (IsMe(nickLower))
{
emit.Self = true;
NickName = nick;
NickNameLower = CaseFold(nick);
}
return emit;
}
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;
}
private Emit HandleISupport(Line line)
{
ISupport = new ISupport();
ISupport.Parse(line.Params);
return new Emit();
}
private Emit HandleWelcome(Line line)
{
NickName = line.Params[0];
NickNameLower = CaseFold(line.Params[0]);
Registered = true;
return new Emit();
}
}
}