FragLand/terracord

View on GitHub
Terracord/Terracord.cs

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * Terracord.cs - A Discord <-> Terraria bridge plugin for TShock
 * Copyright (C) 2019-2021 Lloyd Dilley
 * http://www.frag.land/
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

using System;
using System.Threading;
using System.Text.RegularExpressions;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;
using TShockAPI.Hooks;

namespace FragLand.TerracordPlugin
{
  [ApiVersion(2, 1)]
  public class Terracord:TerrariaPlugin
  {
    ///<summary>
    /// Plugin name
    /// </summary>
    public override string Name => "Terracord";

    /// <summary>
    /// Plugin version
    /// </summary>
    public override Version Version => new Version(1, 3, 1);

    /// <summary>
    /// Plugin author(s)
    /// </summary>
    public override string Author => "Lloyd Dilley";

    /// <summary>
    /// Plugin description
    /// </summary>
    public override string Description => "A Discord <-> Terraria bridge plugin for TShock";

    // Plugin version
    public const string PluginVersion = "1.3.1";
    // Discord bot client
    private readonly Discord discord;
    // Plugin start time
    public static readonly DateTime startTime = DateTime.Now;

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="game">TShock game</param>
    public Terracord(Main game):base(game)
    {
      // Parse terracord.xml configuration file
      Config.Parse();
      // Populate emoji dictionary
      //Util.PopulateEmojiDict();
      // Initialize Discord bot
      discord = new Discord();
    }

    /// <summary>
    /// Plugin initialization
    /// </summary>
    public override void Initialize()
    {
      ServerApi.Hooks.GameInitialize.Register(this, OnInitialize);
      ServerApi.Hooks.GamePostInitialize.Register(this, OnPostInitialize);
      ServerApi.Hooks.ServerJoin.Register(this, OnJoin);
      ServerApi.Hooks.ServerBroadcast.Register(this, OnBroadcast);
      //ServerApi.Hooks.ServerChat.Register(this, OnChat);
      PlayerHooks.PlayerChat += OnChat;
      ServerApi.Hooks.ServerLeave.Register(this, OnLeave);
      GeneralHooks.ReloadEvent += OnReload;
    }

    /// <summary>
    /// Plugin destruction 
    /// </summary>
    protected override void Dispose(bool disposing)
    {
      if(disposing)
      {
        Util.Log("Relay shutting down.", Util.Severity.Info);
        discord.Send(Config.UnavailableText);
        discord.SetTopic(Config.OfflineTopic).ConfigureAwait(true);
        Thread.Sleep(1000); // allow time for topic to be set above
        ServerApi.Hooks.GameInitialize.Deregister(this, OnInitialize);
        ServerApi.Hooks.GamePostInitialize.Deregister(this, OnPostInitialize);
        ServerApi.Hooks.ServerJoin.Deregister(this, OnJoin);
        ServerApi.Hooks.ServerBroadcast.Deregister(this, OnBroadcast);
        //ServerApi.Hooks.ServerChat.Deregister(this, OnChat);
        PlayerHooks.PlayerChat -= OnChat;
        ServerApi.Hooks.ServerLeave.Deregister(this, OnLeave);
        GeneralHooks.ReloadEvent -= OnReload;
        discord.Client.Dispose();
      }
      base.Dispose(disposing);
    }

    /// <summary>
    /// Called when TShock is initialized
    /// </summary>
    /// <param name="args">event arguments passed by hook</param>
    private void OnInitialize(EventArgs args)
    {
      Util.Log("Server has started.", Util.Severity.Info);
    }

    /// <summary>
    /// Called after TShock is initialized
    /// </summary>
    /// <param name="args">event arguments passed by hook</param>
    private void OnPostInitialize(EventArgs args)
    {
      // Launch Discord bot in an asynchronous context
      // The line below blocks and prevents TShock console input when await Task.Delay(-1) is used in Connect()
      discord.Connect().GetAwaiter().GetResult();
      // Execute synchronously instead and use discard to suppress await warning
      //_ = discord.Connect();
    }

    /// <summary>
    /// Called when a player joins the server
    /// </summary>
    /// <param name="args">event arguments passed by hook</param>
    private void OnJoin(JoinEventArgs args)
    {
      PlayerEventNotify(args, Config.JoinText);
    }

    /// <summary>
    /// Called when a broadcast message is intercepted
    /// </summary>
    /// <param name="args">event arguments passed by hook</param>
    private void OnBroadcast(ServerBroadcastEventArgs args)
    {
      // Do not relay game broadcasts to Discord if this option is enabled
      if(Config.SilenceBroadcasts)
        return;

      // Filter broadcast messages based on content
      if(Util.FilterBroadcast($"{args.Message}"))
        return;

      Util.Log($"Server broadcast: {Util.ConvertItems(args.Message.ToString())}", Util.Severity.Info);
      discord.Send(Config.BroadcastText.Replace("$message", args.Message.ToString()));
    }

    /// <summary>
    /// Called when a chat message is intercepted
    /// </summary>
    /// <param name="args">event arguments passed by hook</param>
    //private void OnChat(ServerChatEventArgs args)
    private void OnChat(PlayerChatEventArgs args)
    {
      // Do not relay game chat to Discord if this option is enabled
      if(Config.SilenceChat)
        return;

      // PlayerHooks.OnPlayerChat() already handles filtering commands and muted players.
      // Do not relay commands or messages from muted players
      //if(args.Text.StartsWith(TShock.Config.CommandSpecifier, StringComparison.InvariantCulture) || args.Text.StartsWith(TShock.Config.CommandSilentSpecifier, StringComparison.InvariantCulture) || TShock.Players[args.Who].mute)
      //  return;

      // Attempt to convert any channel mentions
      //string modifiedMessage = args.Text;
      string modifiedMessage = args.RawText;
      if(Regex.IsMatch(modifiedMessage, @"#.+"))
        modifiedMessage = Util.ConvertChannelMentions(modifiedMessage, discord.Client);

      // Attempt to convert any role/user mentions
      if(Regex.IsMatch(modifiedMessage, @"@.+"))
        modifiedMessage = Util.ConvertRoleUserMentions(modifiedMessage, discord.Client);

      // Check for game items and convert them to friendly names if found
      if(Regex.IsMatch(modifiedMessage, @"\[i(/p[0-9]+)?(/s[0-9]+)?:([0-9]+)\]"))
        modifiedMessage = Util.ConvertItems(modifiedMessage);

      // Convert emoticons to emojis if enabled
      if(Config.ConvertEmoticons)
        modifiedMessage = Util.ConvertEmoticons(modifiedMessage);

      if(Config.LogChat)
      {
        //Util.Log($"{TShock.Players[args.Who].Name} said: {modifiedMessage}", Util.Severity.Info);
        Util.Log($"{args.Player.Name} said: {modifiedMessage}", Util.Severity.Info);
      }
      //discord.Send($"**<{TShock.Players[args.Who].Name}>** {modifiedMessage}");
      string text = Config.PlayerText.Replace("$player_name", args.Player.Name);
      text = text.Replace("$group_name", args.Player.Group.Name);
      text = text.Replace("$message", modifiedMessage);
      discord.Send(text);
    }

    /// <summary>
    /// Called when a player leaves the server
    /// </summary>
    /// <param name="args">event arguments passed by hook</param>
    private void OnLeave(LeaveEventArgs args)
    {
      PlayerEventNotify(args, Config.LeaveText);
    }

    /// <summary>
    /// Called when the TShock reload command is issued
    /// </summary>
    /// <param name="args">event arguments passed by hook</param>
    private void OnReload(ReloadEventArgs args)
    {
      Util.Log("Reload triggered. Please note that Discord bot token changes require a restart to take effect.", Util.Severity.Info);
      Config.Parse();
      Util.Log("Reload complete.", Util.Severity.Info);
    }

    /// <summary>
    /// Sends Terraria player join/leave events to the Discord text channel
    /// </summary>
    /// <param name="eventArgs">event arguments</param>
    /// <param name="message">message</param>
    private void PlayerEventNotify(object eventArgs, string message)
    {
      try
      {
        // This check should help prevent unnecessary exceptions from being logged after TShock reaps incomplete connections
        if(eventArgs != null)
        {
          string playerName = null;
          int playerCount = TShock.Utils.GetActivePlayerCount();
          if(eventArgs is JoinEventArgs joinEventArgs)
          {
            playerName = TShock.Players[joinEventArgs.Who].Name;
            playerCount += 1; // needed since server active player count does not increment until OnJoin() event method returns
          }
          if(eventArgs is LeaveEventArgs leaveEventArgs)
          {
            playerName = TShock.Players[leaveEventArgs.Who].Name;
            playerCount -= 1; // needed since server active player count does not decrement until OnLeave() event method returns
          }
          if(!String.IsNullOrEmpty(playerName))
          {
            message = message.Replace("$player_name", playerName);
            string status = Config.BotGame.Replace("$server_name", TShock.Config.Settings.ServerName);
            status = status.Replace("$world_name", Terraria.Main.worldName);
            status = status.Replace("$player_count", playerCount.ToString());
            status = status.Replace("$player_slots", TShock.Config.Settings.MaxSlots.ToString());
            Discord.UpdateBotGame(discord.Client, status).ConfigureAwait(true);
            Util.Log(message, Util.Severity.Info);
            discord.Send(message);
          }
        }
      }
      catch(NullReferenceException nre)
      {
        if(Config.DebugMode)
        {
          Util.Log($"Exception caught after player joined or left TShock server: {nre.Message}", Util.Severity.Error);
          throw;
        }
      }
    }
  }
}