Overhaul the IRC implementation.
* Simplified UI plumbing. * Improves handling of errors and kicks. * Persists chat history between session. * Fixes leaks of the old widget tree when exiting. * A few small UI polish improvements.
This commit is contained in:
371
OpenRA.Game/GlobalChat.cs
Normal file
371
OpenRA.Game/GlobalChat.cs
Normal file
@@ -0,0 +1,371 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Meebey.SmartIrc4net;
|
||||
using OpenRA;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Chat
|
||||
{
|
||||
public enum ChatConnectionStatus { Disconnected, Connecting, Connected, Disconnecting, Joined, Error }
|
||||
public enum ChatMessageType { Message, Notification }
|
||||
|
||||
public sealed class ChatUser
|
||||
{
|
||||
public readonly string Name;
|
||||
public bool IsOp;
|
||||
public bool IsVoiced;
|
||||
|
||||
public ChatUser(string name, bool isOp, bool isVoice)
|
||||
{
|
||||
Name = name;
|
||||
IsOp = isOp;
|
||||
IsVoiced = isVoice;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ChatMessage
|
||||
{
|
||||
static long nextUID;
|
||||
|
||||
public readonly DateTime Time;
|
||||
public readonly ChatMessageType Type;
|
||||
public readonly string Nick;
|
||||
public readonly string Message;
|
||||
public readonly string UID;
|
||||
|
||||
public ChatMessage(DateTime time, ChatMessageType type, string nick, string message)
|
||||
{
|
||||
Time = time;
|
||||
Type = type;
|
||||
Nick = nick;
|
||||
Message = message;
|
||||
|
||||
UID = Interlocked.Increment(ref nextUID).ToString();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var time = Time.ToString(Game.Settings.Chat.TimestampFormat);
|
||||
if (Type == ChatMessageType.Notification)
|
||||
return "{0} {1}".F(time, Message);
|
||||
|
||||
return "{0} {1}: {2}".F(time, Nick, Message);
|
||||
}
|
||||
}
|
||||
|
||||
public class GlobalChat : IDisposable
|
||||
{
|
||||
readonly IrcClient client = new IrcClient();
|
||||
volatile Channel channel;
|
||||
|
||||
public readonly ObservableSortedDictionary<string, ChatUser> Users = new ObservableSortedDictionary<string, ChatUser>(StringComparer.InvariantCultureIgnoreCase);
|
||||
public readonly ObservableList<ChatMessage> History = new ObservableList<ChatMessage>();
|
||||
|
||||
volatile string topic;
|
||||
public string Topic { get { return topic; } }
|
||||
|
||||
volatile ChatConnectionStatus connectionStatus = ChatConnectionStatus.Disconnected;
|
||||
public ChatConnectionStatus ConnectionStatus { get { return connectionStatus; } }
|
||||
|
||||
public GlobalChat()
|
||||
{
|
||||
client.Encoding = System.Text.Encoding.UTF8;
|
||||
client.SendDelay = 100;
|
||||
client.ActiveChannelSyncing = true;
|
||||
|
||||
client.OnConnecting += OnConnecting;
|
||||
client.OnConnected += OnConnected;
|
||||
client.OnDisconnecting += OnDisconnecting;
|
||||
client.OnDisconnected += OnDisconnected;
|
||||
client.OnError += OnError;
|
||||
client.OnKick += OnKick;
|
||||
|
||||
client.OnRawMessage += (_, e) => Game.RunAfterTick(() => Log.Write("irc", e.Data.RawMessage));
|
||||
client.OnJoin += OnJoin;
|
||||
client.OnChannelActiveSynced += OnChannelActiveSynced;
|
||||
client.OnTopic += (_, e) => topic = e.Topic;
|
||||
client.OnTopicChange += (_, e) => topic = e.NewTopic;
|
||||
client.OnNickChange += OnNickChange;
|
||||
|
||||
client.OnChannelMessage += (_, e) => AddMessage(e.Data.Nick, e.Data.Message);
|
||||
client.OnOp += (_, e) => SetUserOp(e.Whom, true);
|
||||
client.OnDeop += (_, e) => SetUserOp(e.Whom, false);
|
||||
client.OnVoice += (_, e) => SetUserVoiced(e.Whom, true);
|
||||
client.OnDevoice += (_, e) => SetUserVoiced(e.Whom, false);
|
||||
client.OnPart += OnPart;
|
||||
client.OnQuit += OnQuit;
|
||||
}
|
||||
|
||||
void SetUserOp(string whom, bool isOp)
|
||||
{
|
||||
Game.RunAfterTick(() =>
|
||||
{
|
||||
ChatUser user;
|
||||
if (Users.TryGetValue(whom, out user))
|
||||
user.IsOp = isOp;
|
||||
});
|
||||
}
|
||||
|
||||
void SetUserVoiced(string whom, bool isVoiced)
|
||||
{
|
||||
Game.RunAfterTick(() =>
|
||||
{
|
||||
ChatUser user;
|
||||
if (Users.TryGetValue(whom, out user))
|
||||
user.IsVoiced = isVoiced;
|
||||
});
|
||||
}
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
if (client.IsConnected)
|
||||
return;
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Connect(Game.Settings.Chat.Hostname, Game.Settings.Chat.Port);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
connectionStatus = ChatConnectionStatus.Error;
|
||||
AddNotification(e.Message);
|
||||
Game.RunAfterTick(() => Log.Write("irc", e.ToString()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
client.Listen();
|
||||
}) { Name = "IrcListenThread" }.Start();
|
||||
}
|
||||
|
||||
void AddNotification(string text)
|
||||
{
|
||||
var message = new ChatMessage(DateTime.Now, ChatMessageType.Notification, null, text);
|
||||
Game.RunAfterTick(() =>
|
||||
{
|
||||
History.Add(message);
|
||||
Log.Write("irc", text);
|
||||
});
|
||||
}
|
||||
|
||||
void AddMessage(string nick, string text)
|
||||
{
|
||||
var message = new ChatMessage(DateTime.Now, ChatMessageType.Message, nick, text);
|
||||
Game.RunAfterTick(() =>
|
||||
{
|
||||
History.Add(message);
|
||||
Log.Write("irc", text);
|
||||
});
|
||||
}
|
||||
|
||||
void OnConnecting(object sender, EventArgs e)
|
||||
{
|
||||
AddNotification("Connecting to {0}:{1}...".F(Game.Settings.Chat.Hostname, Game.Settings.Chat.Port));
|
||||
connectionStatus = ChatConnectionStatus.Connecting;
|
||||
}
|
||||
|
||||
void OnConnected(object sender, EventArgs e)
|
||||
{
|
||||
AddNotification("Connected.");
|
||||
connectionStatus = ChatConnectionStatus.Connected;
|
||||
|
||||
// Guard against settings.yaml modification
|
||||
var nick = SanitizedName(Game.Settings.Chat.Nickname);
|
||||
if (nick != Game.Settings.Chat.Nickname)
|
||||
Game.RunAfterTick(() => Game.Settings.Chat.Nickname = nick);
|
||||
|
||||
client.Login(nick, "in-game IRC client", 0, "OpenRA");
|
||||
client.RfcJoin("#" + Game.Settings.Chat.Channel);
|
||||
}
|
||||
|
||||
void OnDisconnecting(object sender, EventArgs e)
|
||||
{
|
||||
if (connectionStatus != ChatConnectionStatus.Error)
|
||||
connectionStatus = ChatConnectionStatus.Disconnecting;
|
||||
}
|
||||
|
||||
void OnDisconnected(object sender, EventArgs e)
|
||||
{
|
||||
Game.RunAfterTick(Users.Clear);
|
||||
|
||||
// Keep the chat window open if there is an error
|
||||
// It will be cleared by the Disconnect button
|
||||
if (connectionStatus != ChatConnectionStatus.Error)
|
||||
{
|
||||
Game.RunAfterTick(History.Clear);
|
||||
topic = null;
|
||||
connectionStatus = ChatConnectionStatus.Disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
void OnError(object sender, ErrorEventArgs e)
|
||||
{
|
||||
// Ignore any errors that happen during disconnect
|
||||
if (connectionStatus != ChatConnectionStatus.Disconnecting)
|
||||
{
|
||||
connectionStatus = ChatConnectionStatus.Error;
|
||||
AddNotification("Error: " + e.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void OnKick(object sender, KickEventArgs e)
|
||||
{
|
||||
Disconnect();
|
||||
connectionStatus = ChatConnectionStatus.Error;
|
||||
AddNotification("Error: You were kicked from the chat by {0}".F(e.Who));
|
||||
}
|
||||
|
||||
void OnJoin(object sender, JoinEventArgs e)
|
||||
{
|
||||
if (e.Who == client.Nickname || e.Channel != channel.Name)
|
||||
return;
|
||||
|
||||
AddNotification("{0} joined the chat.".F(e.Who));
|
||||
Game.RunAfterTick(() => Users.Add(e.Who, new ChatUser(e.Who, false, false)));
|
||||
}
|
||||
|
||||
void OnChannelActiveSynced(object sender, IrcEventArgs e)
|
||||
{
|
||||
channel = client.GetChannel(e.Data.Channel);
|
||||
AddNotification("{0} users online".F(channel.Users.Count));
|
||||
connectionStatus = ChatConnectionStatus.Joined;
|
||||
|
||||
foreach (DictionaryEntry user in channel.Users)
|
||||
{
|
||||
var u = (ChannelUser)user.Value;
|
||||
Game.RunAfterTick(() => Users.Add(u.Nick, new ChatUser(u.Nick, u.IsOp, u.IsVoice)));
|
||||
}
|
||||
}
|
||||
|
||||
void OnNickChange(object sender, NickChangeEventArgs e)
|
||||
{
|
||||
AddNotification("{0} is now known as {1}.".F(e.OldNickname, e.NewNickname));
|
||||
|
||||
Game.RunAfterTick(() =>
|
||||
{
|
||||
ChatUser user;
|
||||
if (!Users.TryGetValue(e.OldNickname, out user))
|
||||
return;
|
||||
|
||||
Users.Remove(e.OldNickname);
|
||||
Users.Add(e.NewNickname, new ChatUser(e.NewNickname, user.IsOp, user.IsVoiced));
|
||||
});
|
||||
}
|
||||
|
||||
void OnQuit(object sender, QuitEventArgs e)
|
||||
{
|
||||
AddNotification("{0} left the chat.".F(e.Who));
|
||||
Game.RunAfterTick(() => Users.Remove(e.Who));
|
||||
}
|
||||
|
||||
void OnPart(object sender, PartEventArgs e)
|
||||
{
|
||||
if (e.Data.Channel != channel.Name)
|
||||
return;
|
||||
|
||||
AddNotification("{0} left the chat.".F(e.Who));
|
||||
Game.RunAfterTick(() => Users.Remove(e.Who));
|
||||
}
|
||||
|
||||
public string SanitizedName(string dirty)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dirty))
|
||||
return null;
|
||||
|
||||
// There is no need to mangle the nick if it is already valid
|
||||
if (Rfc2812.IsValidNickname(dirty))
|
||||
return dirty;
|
||||
|
||||
// TODO: some special chars are allowed as well, but not at every position
|
||||
var clean = new string(dirty.Where(c => char.IsLetterOrDigit(c)).ToArray());
|
||||
|
||||
if (string.IsNullOrEmpty(clean))
|
||||
return null;
|
||||
|
||||
if (char.IsDigit(clean[0]))
|
||||
return SanitizedName(clean.Substring(1));
|
||||
|
||||
// Source: https://tools.ietf.org/html/rfc2812#section-1.2.1
|
||||
if (clean.Length > 9)
|
||||
clean = clean.Substring(0, 9);
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
public bool IsValidNickname(string name)
|
||||
{
|
||||
return Rfc2812.IsValidNickname(name);
|
||||
}
|
||||
|
||||
public void SendMessage(string text)
|
||||
{
|
||||
if (connectionStatus != ChatConnectionStatus.Joined)
|
||||
return;
|
||||
|
||||
// Guard against a last-moment disconnection
|
||||
try
|
||||
{
|
||||
client.SendMessage(SendType.Message, channel.Name, text);
|
||||
AddMessage(client.Nickname, text);
|
||||
}
|
||||
catch (NotConnectedException) { }
|
||||
}
|
||||
|
||||
public bool TrySetNickname(string nick)
|
||||
{
|
||||
// TODO: This is inconsistent with the other check
|
||||
if (Rfc2812.IsValidNickname(nick))
|
||||
{
|
||||
client.RfcNick(nick);
|
||||
Game.Settings.Chat.Nickname = nick;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
// Error is an alias for disconnect, but keeps the panel open
|
||||
// so that clients can see the error
|
||||
if (connectionStatus == ChatConnectionStatus.Error)
|
||||
{
|
||||
Game.RunAfterTick(History.Clear);
|
||||
topic = null;
|
||||
connectionStatus = ChatConnectionStatus.Disconnected;
|
||||
}
|
||||
else
|
||||
connectionStatus = ChatConnectionStatus.Disconnecting;
|
||||
|
||||
if (!client.IsConnected)
|
||||
return;
|
||||
|
||||
client.RfcQuit(Game.Settings.Chat.QuitMessage);
|
||||
|
||||
AddNotification("Disconnecting from {0}...".F(client.Address));
|
||||
|
||||
Game.RunAfterTick(() => Game.Settings.Chat.ConnectAutomatically = false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (client.IsConnected)
|
||||
client.Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user