mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-26 14:37:44 +01:00
378 lines
9.5 KiB
C#
378 lines
9.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using TGS.Interface;
|
|
using Meebey.SmartIrc4net;
|
|
|
|
namespace TGS.Server.ChatProviders
|
|
{
|
|
/// <summary>
|
|
/// <see cref="IChatProvider"/> for internet relay chat
|
|
/// </summary>
|
|
sealed class IRCChatProvider : IChatProvider
|
|
{
|
|
/// <summary>
|
|
/// Header used to mark that a channel is actually a query message
|
|
/// </summary>
|
|
const string PrivateMessageMarker = "---PRIVATE-MSG---";
|
|
/// <summary>
|
|
/// The irc client
|
|
/// </summary>
|
|
IrcFeatures irc;
|
|
/// <summary>
|
|
/// Used for multithreading safety
|
|
/// </summary>
|
|
object IRCLock = new object();
|
|
|
|
/// <summary>
|
|
/// The setup info for the provider
|
|
/// </summary>
|
|
IRCSetupInfo IRCConfig;
|
|
|
|
/// <inheritdoc />
|
|
public event OnChatMessage OnChatMessage;
|
|
|
|
/// <inheritdoc />
|
|
public ChatSetupInfo ProviderInfo()
|
|
{
|
|
return IRCConfig;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Construct a <see cref="IRCChatProvider"/>
|
|
/// </summary>
|
|
/// <param name="info">The <see cref="ChatSetupInfo"/></param>
|
|
public IRCChatProvider(ChatSetupInfo info)
|
|
{
|
|
IRCConfig = new IRCSetupInfo(info);
|
|
irc = new IrcFeatures()
|
|
{
|
|
SupportNonRfc = true,
|
|
CtcpUserInfo = Server.VersionString,
|
|
AutoRejoin = true,
|
|
AutoRejoinOnKick = true,
|
|
AutoRelogin = true,
|
|
AutoRetry = true,
|
|
AutoRetryLimit = 5,
|
|
AutoRetryDelay = 5,
|
|
ActiveChannelSyncing = true,
|
|
};
|
|
irc.OnChannelMessage += Irc_OnChannelMessage;
|
|
irc.OnQueryMessage += Irc_OnQueryMessage;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string SendMessageDirect(string message, string channel)
|
|
{
|
|
try
|
|
{
|
|
if (!Connected())
|
|
return "Disconnected.";
|
|
lock (IRCLock)
|
|
{
|
|
SendType st;
|
|
if (channel.Contains(PrivateMessageMarker))
|
|
{
|
|
channel = channel.Replace(PrivateMessageMarker, "");
|
|
st = SendType.Notice;
|
|
}
|
|
else
|
|
st = SendType.Message;
|
|
irc.SendMessage(st, channel, message);
|
|
}
|
|
return null;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return e.ToString();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string SetProviderInfo(ChatSetupInfo info)
|
|
{
|
|
var convertedInfo = (IRCSetupInfo)info;
|
|
var serverChange = convertedInfo.URL != IRCConfig.URL || convertedInfo.Port != IRCConfig.Port;
|
|
IRCConfig = convertedInfo;
|
|
if (!IRCConfig.Enabled)
|
|
{
|
|
Disconnect();
|
|
return null;
|
|
}
|
|
else if (serverChange || !Connected())
|
|
return Reconnect();
|
|
|
|
if (IRCConfig.Nickname != irc.Nickname)
|
|
irc.RfcNick(convertedInfo.Nickname);
|
|
Login();
|
|
JoinChannels();
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a message is considered sent from a chat admin
|
|
/// </summary>
|
|
/// <param name="e">The <see cref="IrcMessageData"/></param>
|
|
/// <returns><see langword="true"/> if <paramref name="e"/> was sent by a chat admin, <see langword="false"/> otherwise</returns>
|
|
private bool CheckAdmin(IrcMessageData e)
|
|
{
|
|
if (IRCConfig.AdminsAreSpecial)
|
|
{
|
|
if (e.Channel == null)
|
|
return false;
|
|
var user = (NonRfcChannelUser)irc.GetChannelUser(e.Channel, e.Nick);
|
|
if (user != null)
|
|
switch (IRCConfig.AuthLevel)
|
|
{
|
|
case IRCMode.Voice:
|
|
if (user.IsVoice)
|
|
return true;
|
|
goto case IRCMode.Halfop;
|
|
case IRCMode.Halfop:
|
|
if (user.IsHalfop)
|
|
return true;
|
|
goto case IRCMode.Op;
|
|
case IRCMode.Op:
|
|
if (user.IsOp)
|
|
return true;
|
|
goto case IRCMode.Owner;
|
|
case IRCMode.Owner:
|
|
if (user.IsOwner)
|
|
return true;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var lowerNick = e.Nick.ToLower();
|
|
foreach (var I in IRCConfig.AdminList)
|
|
if (lowerNick == I.ToLower())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the bot recieves a query message
|
|
/// </summary>
|
|
/// <param name="sender">The sender of the event (usually <see cref="irc"/>)</param>
|
|
/// <param name="e">The <see cref="IrcEventArgs"/></param>
|
|
private void Irc_OnQueryMessage(object sender, IrcEventArgs e)
|
|
{
|
|
OnChatMessage(this, e.Data.Nick, e.Data.Nick + PrivateMessageMarker, e.Data.Message, CheckAdmin(e.Data), true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a channel the bot is in recieves a message
|
|
/// </summary>
|
|
/// <param name="sender">The sender of the event (usually <see cref="irc"/>)</param>
|
|
/// <param name="e">The <see cref="IrcEventArgs"/></param>
|
|
private void Irc_OnChannelMessage(object sender, IrcEventArgs e)
|
|
{
|
|
var formattedMessage = e.Data.Message.Trim();
|
|
|
|
var splits = new List<string>(formattedMessage.Split(' '));
|
|
var test = splits[0];
|
|
if (test.Length > 1 && (test[test.Length - 1] == ':' || test[test.Length - 1] == ','))
|
|
test = test.Substring(0, test.Length - 1);
|
|
test = test.ToLower();
|
|
if (test != irc.Nickname.ToLower() && test != "!tgs")
|
|
return;
|
|
|
|
splits.RemoveAt(0);
|
|
formattedMessage = String.Join(" ", splits);
|
|
|
|
OnChatMessage(this, e.Data.Nick, e.Data.Channel, formattedMessage, CheckAdmin(e.Data), IRCConfig.AdminChannels.Contains(e.Data.Channel.ToLower()));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Joins all channels specified in <see cref="IRCConfig"/>
|
|
/// </summary>
|
|
void JoinChannels()
|
|
{
|
|
var hs = new HashSet<string>(); //for unique inserts
|
|
foreach (var I in IRCConfig.AdminChannels)
|
|
hs.Add(I);
|
|
foreach (var I in IRCConfig.DevChannels)
|
|
hs.Add(I);
|
|
foreach (var I in IRCConfig.GameChannels)
|
|
hs.Add(I);
|
|
foreach (var I in IRCConfig.WatchdogChannels)
|
|
hs.Add(I);
|
|
var ToPart = new List<string>();
|
|
foreach (var I in irc.JoinedChannels)
|
|
if (!hs.Remove(I))
|
|
ToPart.Add(I);
|
|
foreach (var I in ToPart)
|
|
irc.RfcPart(I);
|
|
foreach (var I in hs)
|
|
irc.RfcJoin(I);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a login query to <see cref="IRCSetupInfo.AuthTarget"/> with message <see cref="IRCSetupInfo.AuthMessage"/>
|
|
/// </summary>
|
|
void Login()
|
|
{
|
|
lock (IRCLock)
|
|
{
|
|
if (IRCConfig.AuthTarget != null)
|
|
irc.SendMessage(SendType.Message, IRCConfig.AuthTarget, IRCConfig.AuthMessage);
|
|
}
|
|
}
|
|
/// <inheritdoc />
|
|
public string Connect()
|
|
{
|
|
if (Connected() || !IRCConfig.Enabled)
|
|
return null;
|
|
lock (IRCLock)
|
|
{
|
|
try
|
|
{
|
|
try
|
|
{
|
|
irc.Connect(IRCConfig.URL, IRCConfig.Port);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return "IRC server unreachable: " + e.ToString();
|
|
}
|
|
|
|
try
|
|
{
|
|
irc.Login(IRCConfig.Nickname, IRCConfig.Nickname);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return "Bot name is already taken: " + e.ToString();
|
|
}
|
|
Login();
|
|
JoinChannels();
|
|
|
|
new Thread(new ThreadStart(IRCListen)) { IsBackground = true }.Start();
|
|
return null;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return e.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs the <see cref="irc"/> listener in a safe loop
|
|
/// </summary>
|
|
void IRCListen()
|
|
{
|
|
Thread.CurrentThread.Name = "IRC Listener Thread";
|
|
while (irc != null && Connected())
|
|
try
|
|
{
|
|
irc.Listen();
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string Reconnect()
|
|
{
|
|
Disconnect();
|
|
return Connect();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Disconnect()
|
|
{
|
|
try
|
|
{
|
|
lock (IRCLock)
|
|
{
|
|
if (irc.IsConnected)
|
|
{
|
|
irc.RfcQuit("Twas meant to be...");
|
|
irc.Disconnect();
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
/// <inheritdoc />
|
|
public bool Connected()
|
|
{
|
|
lock (IRCLock)
|
|
{
|
|
return irc != null && irc.IsConnected;
|
|
}
|
|
}
|
|
/// <inheritdoc />
|
|
public void SendMessage(string message, MessageType mt)
|
|
{
|
|
if (!Connected())
|
|
return;
|
|
lock (IRCLock)
|
|
{
|
|
string cids = "";
|
|
for (var I = 0; I < irc.JoinedChannels.Count; ++I)
|
|
{
|
|
var cid = irc.JoinedChannels[I];
|
|
bool SendToThisChannel = (mt.HasFlag(MessageType.AdminInfo) && IRCConfig.AdminChannels.Contains(cid))
|
|
|| (mt.HasFlag(MessageType.DeveloperInfo) && IRCConfig.DevChannels.Contains(cid))
|
|
|| (mt.HasFlag(MessageType.GameInfo) && IRCConfig.GameChannels.Contains(cid))
|
|
|| (mt.HasFlag(MessageType.WatchdogInfo) && IRCConfig.WatchdogChannels.Contains(cid));
|
|
if (SendToThisChannel)
|
|
cids += I > 0 ? ',' + cid : cid;
|
|
}
|
|
irc.SendMessage(SendType.Message, cids, message);
|
|
}
|
|
}
|
|
|
|
|
|
#region IDisposable Support
|
|
/// <summary>
|
|
/// To detect redundant <see cref="Dispose()"/> calls
|
|
/// </summary>
|
|
private bool disposedValue = false;
|
|
|
|
/// <summary>
|
|
/// Implements the <see cref="IDisposable"/> pattern. Calls <see cref="Disconnect"/> and sets <see cref="irc"/> to <see langword="null"/>
|
|
/// </summary>
|
|
/// <param name="disposing"><see langword="true"/> if <see cref="Dispose()"/> was called manually, <see langword="false"/> if it was from the finalizer</param>
|
|
void Dispose(bool disposing)
|
|
{
|
|
if (!disposedValue)
|
|
{
|
|
if (disposing)
|
|
{
|
|
// TODO: dispose managed state (managed objects).
|
|
Disconnect();
|
|
irc = null;
|
|
}
|
|
|
|
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
|
|
// TODO: set large fields to null.
|
|
|
|
disposedValue = true;
|
|
}
|
|
}
|
|
|
|
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
|
|
// ~TGIRCChatProvider() {
|
|
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
|
// Dispose(false);
|
|
// }
|
|
|
|
// This code added to correctly implement the disposable pattern.
|
|
/// <summary>
|
|
/// Implements the <see cref="IDisposable"/> pattern
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
|
|
Dispose(true);
|
|
// TODO: uncomment the following line if the finalizer is overridden above.
|
|
// GC.SuppressFinalize(this);
|
|
}
|
|
#endregion
|
|
}
|
|
}
|