Moved Discord bot configuration into appsettings and added moderation commands.

This commit is contained in:
Hipposgrumm 2025-05-31 18:13:35 -06:00
parent ec5621d904
commit 21e0a3f81a
11 changed files with 574 additions and 166 deletions

2
.gitignore vendored
View File

@ -5,4 +5,4 @@ src/bin
src/obj
src/Properties
src/sodoffmmo.csproj.user
/src/discord_bot.json
/src/discord_bot_token

View File

@ -12,7 +12,7 @@ class ChatMessageHandler : CommandHandler {
if (ManagementCommandProcessor.ProcessCommand(message, client))
return Task.CompletedTask;
if (CheckIllegalName(client)) return Task.CompletedTask;
if (client.TempMuted) {
if (client.Muted) {
ClientMuted(client);
return Task.CompletedTask;
}

View File

@ -88,6 +88,8 @@ class LoginHandler : CommandHandler
client.PlayerData.Role = info.Role;
client.PlayerData.VikingId = info.Id;
client.GameVersion = info.Version;
client.Muted = Client.MutedList.ContainsKey(info.Id);
client.Banned = Client.BannedList.ContainsKey(info.Id);
return true;
}
} catch (Exception ex) {

View File

@ -9,17 +9,23 @@ public class Client {
static int id;
static object lck = new();
// VikingId, Reason (nullable)
public static readonly Dictionary<int, string?> MutedList = new();
public static readonly Dictionary<int, string?> BannedList = new();
public int ClientID { get; private set; }
public PlayerData PlayerData { get; set; } = new();
public Room? Room { get; private set; }
public Room? ReturnRoomOnPardon { get; private set; } = null;
public bool OldApi { get; set; } = false;
public bool TempMuted { get; set; } = false;
public uint GameVersion { get; set; }
public bool Muted { get; set; } = false;
public bool Banned { get; set; } = false;
private readonly Socket socket;
SocketBuffer socketBuffer = new();
private volatile bool scheduledDisconnect = false;
private readonly object clientLock = new();
public uint GameVersion { get; set; }
public Client(Socket clientSocket) {
socket = clientSocket;
@ -53,7 +59,12 @@ public class Client {
// set variable player data as not valid, but do not reset all player data
PlayerData.IsValid = false;
if (Room != null) {
bool transfer = Configuration.DiscordBotConfig != null && Room != null && room != null &&
Configuration.DiscordBotConfig.Merges.Values
.Any(rooms => rooms.Contains(Room.Name) && rooms.Contains(room.Name));
ReturnRoomOnPardon = room; // This is needed so that users are put where they're supposed to when they're unbanned.
if (Room != null && (!Banned || !Room.Name.StartsWith("BannedUserRoom_"))) {
Console.WriteLine($"Leave room: {Room.Name} (id={Room.Id}, size={Room.ClientsCount}) IID: {ClientID}");
Room.RemoveClient(this);
@ -61,19 +72,41 @@ public class Client {
data.Add("r", Room.Id);
data.Add("u", ClientID);
Room.Send(NetworkObject.WrapObject(0, 1004, data).Serialize());
DiscordManager.SendPlayerBasedMessage("", ":red_square: {1} has left the room.", Room, PlayerData);
if (!transfer) DiscordManager.SendPlayerBasedMessage("", ":red_square: {1} has left the room.", Room, PlayerData);
}
// set new room (null when SetRoom is used as LeaveRoom)
Room = room;
if (Banned) {
if (room != null) {
Room = Room.GetOrAdd("BannedUserRoom_" + ClientID, autoRemove: true);
if (Room.Clients.Contains(this)) {
// Run all addclient things but no duplicate.
Room.AddClient(this); // Appends to end.
Room.RemoveClient(this); // Removes first instance.
} else {
Room.AddClient(this);
}
Send(Room.SubscribeRoom());
} else {
Room?.RemoveClient(this);
Room = null;
}
} else {
// set new room (null when SetRoom is used as LeaveRoom)
Room? oldRoom = Room;
Room = room;
if (Room != null) {
Console.WriteLine($"Join room: {Room.Name} RoomID (id={Room.Id}, size={Room.ClientsCount}) IID: {ClientID}");
Room.AddClient(this);
if (Room != null) {
Console.WriteLine($"Join room: {Room.Name} RoomID (id={Room.Id}, size={Room.ClientsCount}) IID: {ClientID}");
Room.AddClient(this);
Send(Room.SubscribeRoom());
if (Room.Name != "LIMBO") UpdatePlayerUserVariables(); // do not update user vars if room is limbo
DiscordManager.SendPlayerBasedMessage("", ":green_square: {1} has joined the room.", Room, PlayerData);
Send(Room.SubscribeRoom());
if (Room.Name != "LIMBO") UpdatePlayerUserVariables(); // do not update user vars if room is limbo
if (transfer) {
DiscordManager.SendPlayerBasedMessage("", ":cyclone: {1} transferred from "+$"{oldRoom!.Name} to {room!.Name}.", Room, PlayerData);
} else {
DiscordManager.SendPlayerBasedMessage("", ":green_square: {1} has joined the room.", Room, PlayerData);
}
}
}
}
}

View File

@ -5,6 +5,7 @@ using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
@ -12,6 +13,7 @@ namespace sodoffmmo.Core;
internal static class Configuration {
public static ServerConfiguration ServerConfiguration { get; private set; } = new ServerConfiguration();
public static DiscordBotConfig? DiscordBotConfig { get; private set; } = null;
public static SortedDictionary<int, object> Emoticons = new();
public static SortedDictionary<int, object> Actions = new();
@ -33,15 +35,21 @@ internal static class Configuration {
ServerConfiguration.Authentication = AuthenticationMode.Disabled;
}
// Emoticons (the emojis that float above your head)
SortAndVersion(config.GetSection("Emoticons"), Emoticons);
// Actions (such as dances)
SortAndVersion(config.GetSection("Actions"), Actions);
// In EMD, there are seperate actions when in car that share IDs with normal actions.
// Since this is easy to check MMO-side (SPM uservar), we can just store these seperate
SortAndVersion(config.GetSection("EMDCarActions"), EMDCarActions);
// Canned Chat options (which can vary game to game)
SortAndVersion(config.GetSection("CannedChat"), CannedChat);
DiscordBotConfig = config.GetSection("DiscordBot").Get<DiscordBotConfig>();
if (DiscordBotConfig != null) {
// Emoticons (the emojis that float above your head)
SortAndVersion(config.GetSection("Emoticons"), Emoticons);
// Actions (such as dances)
SortAndVersion(config.GetSection("Actions"), Actions);
// In EMD, there are seperate actions when in car that share IDs with normal actions.
// Since this is easy to check MMO-side (SPM uservar), we can just store these seperate
SortAndVersion(config.GetSection("EMDCarActions"), EMDCarActions);
// Canned Chat options (which can vary game to game)
SortAndVersion(config.GetSection("CannedChat"), CannedChat);
}
}
// While version checking isn't exactly necessary for Actions when using vanilla files, it's nice to support it anyway.
@ -97,6 +105,15 @@ internal sealed class ServerConfiguration {
public string BypassToken { get; set; } = "";
}
internal sealed class DiscordBotConfig {
public bool Disabled { get; set; }
public ulong Server { get; set; }
public ulong Channel { get; set; }
public ulong RoleModerator { get; set; }
public ulong RoleAdmin { get; set; }
public Dictionary<string, string[]> Merges { get; set; }
}
public enum AuthenticationMode {
Disabled, Optional, RequiredForChat, Required
}

View File

@ -104,8 +104,13 @@ public class Room {
NetworkObject obj = new();
NetworkArray roomInfo = new();
roomInfo.Add(Id);
roomInfo.Add(Name); // Room Name
roomInfo.Add(Group); // Group Name
if (Name.StartsWith("BannedUserRoom_")) {
roomInfo.Add("BannedUserRoom"); // Room Name
roomInfo.Add("BannedUserRoom"); // Group Name
} else {
roomInfo.Add(Name); // Room Name
roomInfo.Add(Group); // Group Name
}
roomInfo.Add(true); // is game
roomInfo.Add(false); // is hidden
roomInfo.Add(false); // is password protected
@ -136,8 +141,13 @@ public class Room {
NetworkArray r1 = new();
r1.Add(Id);
r1.Add(Name); // Room Name
r1.Add(Group); // Group Name
if (Name.StartsWith("BannedUserRoom_")) {
r1.Add("BannedUserRoom"); // Room Name
r1.Add("BannedUserRoom"); // Group Name
} else {
r1.Add(Name); // Room Name
r1.Add(Group); // Group Name
}
r1.Add(true);
r1.Add(false);
r1.Add(false);

View File

@ -15,8 +15,8 @@ class TempMuteCommand : IManagementCommand {
client.Send(Utils.BuildServerSideMessage($"TempMute: user {arguments[0]} not found", "Server"));
return;
}
target.TempMuted = !target.TempMuted;
if (target.TempMuted)
target.Muted = !target.Muted;
if (target.Muted)
client.Send(Utils.BuildServerSideMessage($"TempMute: {arguments[0]} has been temporarily muted", "Server"));
else
client.Send(Utils.BuildServerSideMessage($"TempMute: {arguments[0]} has been unmuted", "Server"));

View File

@ -6,87 +6,77 @@ using System.Collections.Concurrent;
using sodoffmmo.Core;
using Discord.Rest;
using sodoffmmo.Data;
using System.Runtime.CompilerServices;
using System.Numerics;
namespace sodoffmmo.Management;
class DiscordManager {
private static readonly string token_file;
private static readonly ConcurrentQueue<QueuedMessage> queue = new();
private static DiscordSocketClient client;
private static BotConfig config;
private static SocketTextChannel channel;
static DiscordManager() {
DirectoryInfo dir = new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "src"));
while (dir.Parent != null) {
if (dir.Name == "src" && dir.Exists) break;
dir = dir.Parent;
}
if (dir.Parent == null) dir = new DirectoryInfo(Directory.GetCurrentDirectory());
token_file = Path.Combine(dir.FullName, "discord_bot.json");
}
private static readonly ConcurrentQueue<QueuedMessage> MessageQueue = new();
private static DiscordSocketClient BotClient;
private static SocketTextChannel BotChannel;
public static void Initialize() {
try {
// This approach is taken because I don't want to accidentally
// push my bot token because it was in appsettings. The
// json handled here has been added to gitignore.
if (File.Exists(token_file)) {
Task.Run(() => RunBot(JsonSerializer.Deserialize<BotConfig>(File.ReadAllText(token_file))));
} else {
Log("Discord Integration not started because the file containing the token was not found.\nPut the token in \""+token_file+"\" and restart the server to use Discord Integration.", ConsoleColor.Yellow);
File.WriteAllText(token_file, JsonSerializer.Serialize(new BotConfig {
Token = ""
}, new JsonSerializerOptions {
WriteIndented = true
}));
if (Configuration.DiscordBotConfig?.Disabled == false) { // Bot config isn't null or disabled.
try {
string botTokenPath = Path.Combine(Path.GetDirectoryName(typeof(Configuration).Assembly.Location), "discord_bot_token");
if (File.Exists(botTokenPath)) {
string? token = new StreamReader(botTokenPath).ReadLine();
if (token != null) {
if (Configuration.DiscordBotConfig.Server != 0) {
if (Configuration.DiscordBotConfig.Channel != 0) {
Task.Run(() => RunBot(token));
return;
}
Log("Discord bot was not started because the ServerID isn't set. Configure it in appsettings.json or disable it altogether.", ConsoleColor.Yellow);
return;
}
Log("Discord bot was not started because the ChannelID isn't set. Configure it in appsettings.json or disable it altogether.", ConsoleColor.Yellow);
return;
}
}
Log("Discord bot was not started because the token wasn't found. Create the file containing the bot token (see instructions src/appsettings.json) or disable it altogether in appsettings.json.", ConsoleColor.Yellow);
} catch (Exception e) {
Log(e.ToString(), ConsoleColor.Red);
}
} catch (Exception e) {
Log(e.ToString(), ConsoleColor.Red);
}
}
private static async Task RunBot(BotConfig? config) {
if (config == null) {
Log("Bot config didn't deserialize correctly. Discord Integration is not active.", ConsoleColor.Yellow);
return;
}
client = new DiscordSocketClient(new DiscordSocketConfig {
private static async Task RunBot(string token) {
BotClient = new DiscordSocketClient(new DiscordSocketConfig {
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers | GatewayIntents.MessageContent,
AlwaysDownloadUsers = true
});
client.Ready += async () => {
channel = client.GetGuild(config.Server).GetTextChannel(config.Channel);
BotClient.Ready += async () => {
BotChannel = BotClient.GetGuild(Configuration.DiscordBotConfig!.Server).GetTextChannel(Configuration.DiscordBotConfig!.Channel);
};
client.MessageReceived += OnDiscordMessage;
await client.LoginAsync(TokenType.Bot, config.Token);
DiscordManager.config = config;
Log("Loaded bot config from "+token_file+" and bot is running.");
await client.StartAsync();
BotClient.MessageReceived += OnDiscordMessage;
await BotClient.LoginAsync(TokenType.Bot, token);
await BotClient.StartAsync();
Log("Discord bot is running.");
while (true) {
try {
if (!queue.IsEmpty && queue.TryDequeue(out QueuedMessage message)) {
SocketTextChannel? destination = channel;
if (!MessageQueue.IsEmpty && MessageQueue.TryDequeue(out QueuedMessage message)) {
SocketTextChannel? destination = BotChannel;
if (message.room != null) {
if (message.room.Name == "LIMBO") continue;
string room = message.room.Name;
destination = channel.Threads.SingleOrDefault(x => x.Name == room);
if (message.room.Name == "LIMBO" || message.room.Name.StartsWith("BannedUserRoom_")) continue; // Don't log stuff in LIMBO or Banned User.
string room = message.roomName!;
destination = BotChannel.Threads.SingleOrDefault(x => x.Name == room);
if (destination == null) {
// A discord channel can have up to 1000 active threads
// and an unlimited number of archived threads.
if (message.room.AutoRemove) {
// Set temporary rooms (such as user rooms) to shorter archive times.
destination = await channel.CreateThreadAsync(room, autoArchiveDuration: ThreadArchiveDuration.OneHour);
destination = await BotChannel.CreateThreadAsync(room, autoArchiveDuration: ThreadArchiveDuration.OneHour);
} else {
// Persistent rooms should stay around for longer.
destination = await channel.CreateThreadAsync(room, autoArchiveDuration: ThreadArchiveDuration.OneWeek);
destination = await BotChannel.CreateThreadAsync(room, autoArchiveDuration: ThreadArchiveDuration.OneWeek);
}
}
}
await destination.SendMessageAsync(message.msg);
}
} catch (Exception e) {
} catch (Exception e) { // In case something goes wrong for any reason, don't "kill" the bot.
Log(e.ToString(), ConsoleColor.Red);
}
}
@ -95,65 +85,331 @@ class DiscordManager {
// For handling commands sent directly in the channel.
private static async Task OnDiscordMessage(SocketMessage arg) {
// Check that message is from a user in the correct place.
bool roomChannel = arg.Channel is SocketThreadChannel thr && thr.ParentChannel.Id == config.Channel;
bool roomChannel = arg.Channel is SocketThreadChannel thr && thr.ParentChannel.Id == BotChannel.Id;
if (!(
arg.Channel.Id == config.Channel ||
roomChannel
roomChannel ||
arg.Channel.Id == BotChannel.Id
) ||
arg is not SocketUserMessage socketMessage ||
socketMessage.Author.IsBot
) return;
// Get the room if it exists.
Room? room = (roomChannel && Room.Exists(arg.Channel.Name)) ? Room.Get(arg.Channel.Name) : null;
// Perms
Role role = Role.User;
if (arg.Author is IGuildUser user) {
if (user.RoleIds.Contains(Configuration.DiscordBotConfig!.RoleAdmin)) {
role = Role.Admin;
} else if (user.RoleIds.Contains(Configuration.DiscordBotConfig!.RoleModerator)) {
role = Role.Admin;
}
}
string[] message = socketMessage.Content.Split(' ');
if (message.Length >= 1) {
string[] everywhereCommands = new string[] {
// Denotes commands that work identically regardless
// of whether you're in a room thread.
"!tempmute <int: vikingid> [string... reason] - Mute someone until they reconnect."
// Denotes commands that work the same or similarly
// regardless of whether you're in a room thread.
"!freechat <bool: enabled> - Enable/Disable free chat in all rooms.",
"!tempmute <int: vikingid> [string... reason] - Mute someone (until they reconnect or go elsewhere).",
"!untempmute <int: vikingid> - Revoke a tempmute.",
"!mute <int: vikingid> [string... reason] - Mute a vikingid (currently forgotten after server restart due to technical restrictions, but this will probably be fixed in the future).",
"!unmute <int: vikingid> - Revoke a mute.",
"!ban <int: vikingid> [string... reason] - Ban a vikingid from MMO (currently forgotten after server restart due to technical restrictions, but this will probably be fixed in the future).",
"!unban <int: vikingid> - Revoke a ban.",
"!pardon <int: vikingid> - Alias for !unban",
"!mutelist - Get a list of muted users.",
"!banlist - Get a list of banned users.",
};
bool un_command = message[0].StartsWith("!un");
if (message[0] == "!tempmute" || message[0] == "!untempmute") {
if (message.Length > 1 && int.TryParse(message[1], out int id)) {
foreach (Room room in Room.AllRooms()) {
foreach (Client player in room.Clients) {
if (player.PlayerData.VikingId == id) {
if (player.TempMuted != un_command) {
await arg.Channel.SendMessageAsync($"This player is {(un_command ? "not" : "already")} tempmuted.", messageReference: arg.Reference);
} else {
player.TempMuted = !un_command;
string reason = string.Empty;
string msg = un_command ? "Un-tempmuted" : "Tempmuted";
msg += $" {player.PlayerData.DiplayName}.";
if (message.Length >= 2) {
reason = string.Join(' ', message, 2, message.Length - 2);
msg += " Reason: "+reason;
}
await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
}
goto tempmute_search_loop_end;
}
if (message[0] == "!freechat") {
if (role < Role.Moderator) {
await arg.Channel.SendMessageAsync("You don't have permission to use this!", messageReference: arg.Reference);
return;
}
// freechat command
if (message.Length > 1) {
bool enable;
if (!bool.TryParse(message[1], out enable)) {
if ((message[1][0] & 0b11011111) == 'Y') {
enable = true;
} else if ((message[1][0] & 0b11011111) == (int)'N') {
enable = false;
} else {
await arg.Channel.SendMessageAsync("Input a boolean or yes/no value!", messageReference: arg.Reference);
return;
}
}
tempmute_search_loop_end:;// StackOverflow said to do it like this (no labeled break like in Java).
Configuration.ServerConfiguration.EnableChat = enable;
await arg.Channel.SendMessageAsync($"Chat {(enable?"en":"dis")}abled!", messageReference: arg.Reference);
} else {
await arg.Channel.SendMessageAsync("Input a value!", messageReference: arg.Reference);
}
} else if (message[0] == "!tempmute") {
if (role < Role.Moderator) {
await arg.Channel.SendMessageAsync("You don't have permission to use this!", messageReference: arg.Reference);
return;
}
// tempmute command
if (message.Length > 1 && int.TryParse(message[1], out int id)) {
IEnumerable<Client> vikings = FindViking(id, room);
if (room != null && !vikings.Any()) vikings = FindViking(id); // Plan B
if (vikings.Any()) {
bool sent = false;
string? reason = null;
if (message.Length > 2) {
reason = string.Join(' ', message, 2, message.Length - 2);
}
foreach (Client player in vikings) {
if (player.Muted) {
if (Client.MutedList.TryGetValue(id, out string? rreason)) {
string msg = "This user is already muted.";
if (rreason != null && rreason.Length > 0) msg += " Reason: "+rreason;
if (!sent) await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
} else {
if (!sent) await arg.Channel.SendMessageAsync("This user is already tempmuted.", messageReference: arg.Reference);
}
} else {
player.Muted = true;
string msg = $"Tempmuted {player.PlayerData.DiplayName}";
if (reason != null) {
msg += ". Reason: "+reason;
}
if (!sent) await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
}
sent = true;
}
} else {
await arg.Channel.SendMessageAsync("User with that ID is not online.", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("Input a valid vikingid!", messageReference: arg.Reference);
}
} else if (roomChannel) {
if (message[0] == "!help") {
await arg.Channel.SendMessageAsync(string.Join(Environment.NewLine, new string[] {
"### List of commands:",
"!help - Prints the help message. That's this message right here!",
"!list - Lists all players in this room.",
"!say <string... message> - Say something in this room."
}.Concat(everywhereCommands)
));
} else if (message[0] == "!list") {
if (!Room.Exists(arg.Channel.Name)) {
await arg.Channel.SendMessageAsync("This room is currently unloaded.", messageReference: arg.Reference);
} else if (message[0] == "!untempmute") {
if (role < Role.Moderator) {
await arg.Channel.SendMessageAsync("You don't have permission to use this!", messageReference: arg.Reference);
return;
}
// un-tempmute command
if (message.Length > 1 && int.TryParse(message[1], out int id)) {
IEnumerable<Client> vikings = FindViking(id, room);
if (room != null && !vikings.Any()) vikings = FindViking(id); // Plan B
if (vikings.Any()) {
bool sent = false;
foreach (Client player in vikings) {
if (player.Muted) {
if (Client.MutedList.ContainsKey(id)) {
if (!sent) await arg.Channel.SendMessageAsync("This user is true muted. To unmute them, use !unmute", messageReference: arg.Reference);
} else {
player.Muted = false;
if (!sent) await arg.Channel.SendMessageAsync($"Un-tempmuted {player.PlayerData.DiplayName}.", messageReference: arg.Reference);
}
} else {
if (!sent) await arg.Channel.SendMessageAsync("This user is not muted.", messageReference: arg.Reference);
}
sent = true;
}
} else {
Room room = Room.Get(arg.Channel.Name);
await arg.Channel.SendMessageAsync("User with that ID is not online.", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("Input a valid vikingid!", messageReference: arg.Reference);
}
} else if (message[0] == "!mute") {
if (role < Role.Moderator) {
await arg.Channel.SendMessageAsync("You don't have permission to use this!", messageReference: arg.Reference);
return;
}
// true mute command
if (message.Length > 1 && int.TryParse(message[1], out int id)) {
string? name = null;
IEnumerable<Client> vikings = FindViking(id, room);
if (room != null && !vikings.Any()) vikings = FindViking(id); // Plan B
foreach (Client player in vikings) {
name = player.PlayerData.DiplayName;
player.Muted = true;
}
if (Client.MutedList.TryGetValue(id, out string? rreason)) {
string msg = "This user is already muted.";
if (rreason != null && rreason.Length > 0) msg += " Reason: "+rreason;
await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
} else {
string? reason = null;
if (message.Length > 2) {
reason = string.Join(' ', message, 2, message.Length - 2);
}
Client.MutedList.Add(id, reason);
string msg = $"Muted *VikingID {id}*";
if (name != null) msg = $"Muted {name}";
if (reason != null) {
msg += ". Reason: " + reason;
}
await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("Input a valid vikingid!", messageReference: arg.Reference);
}
} else if (message[0] == "!unmute") {
if (role < Role.Moderator) {
await arg.Channel.SendMessageAsync("You don't have permission to use this!", messageReference: arg.Reference);
return;
}
// unmute command
if (message.Length > 1 && int.TryParse(message[1], out int id)) {
string? name = null;
IEnumerable<Client> vikings = FindViking(id, room);
if (room != null && !vikings.Any()) vikings = FindViking(id); // Plan B
if (vikings.Any()) {
foreach (Client player in vikings) {
if (player.Muted) {
name = player.PlayerData.DiplayName;
player.Muted = false;
}
}
}
if (Client.MutedList.Remove(id) || name != null) { // name is only set if there is an unmute
if (name != null) {
await arg.Channel.SendMessageAsync($"Unmuted {name}.", messageReference: arg.Reference);
} else {
await arg.Channel.SendMessageAsync($"Unmuted *VikingID {id}*.", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("This user is not muted.", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("Input a valid vikingid!", messageReference: arg.Reference);
}
} else if (message[0] == "!ban") {
if (role < Role.Moderator) {
await arg.Channel.SendMessageAsync("You don't have permission to use this!", messageReference: arg.Reference);
return;
}
// ban command
if (message.Length > 1 && int.TryParse(message[1], out int id)) {
string? name = null;
foreach (Client player in FindViking(id)) {
name = player.PlayerData.DiplayName;
player.Banned = true;
player.SetRoom(player.Room); // This will run the client through the 'if Banned' conditions.
}
if (Client.BannedList.TryGetValue(id, out string? rreason)) {
string msg = "This user is already banned.";
if (rreason != null && rreason.Length > 0) msg += " Reason: "+rreason;
await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
} else {
string? reason = null;
if (message.Length > 2) {
reason = string.Join(' ', message, 2, message.Length - 2);
}
Client.BannedList.Add(id, reason);
string msg = $"Banned *VikingID {id}*";
if (name != null) msg = $"Banned {name}";
if (reason != null) {
msg += ". Reason: " + reason;
}
await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("Input a valid vikingid!", messageReference: arg.Reference);
}
} else if (message[0] == "!unban" || message[0] == "!pardon") {
if (role < Role.Moderator) {
await arg.Channel.SendMessageAsync("You don't have permission to use this!", messageReference: arg.Reference);
return;
}
// unban command
if (message.Length > 1 && int.TryParse(message[1], out int id)) {
if (Client.BannedList.Remove(id)) {
string? playername = null;
foreach (Room rooom in Room.AllRooms()) {
foreach (Client player in rooom.Clients) {
if (player.PlayerData.VikingId == id) {
playername = player.PlayerData.DiplayName;
player.Banned = false;
player.SetRoom(player.ReturnRoomOnPardon); // Put the player back.
}
}
}
if (playername != null) {
await arg.Channel.SendMessageAsync($"Unbanned {playername}.", messageReference: arg.Reference);
} else {
await arg.Channel.SendMessageAsync($"Unbanned *VikingID {id}*.", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("This user is not banned.", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("Input a valid vikingid!", messageReference: arg.Reference);
}
} else if (message[0] == "!mutelist") {
// mutelist command
if (Client.MutedList.Count > 0) {
Dictionary<int, string> clients = new();
foreach (Room rooom in Room.AllRooms()) {
foreach (Client player in rooom.Clients) {
int id = player.PlayerData.VikingId;
if (Client.MutedList.ContainsKey(id)) {
clients.TryAdd(id, player.PlayerData.DiplayName);
}
}
}
string msg = "Muted Players:";
foreach (KeyValuePair<int, string?> play in Client.MutedList) {
if (clients.TryGetValue(play.Key, out string? name)) {
msg += $"\n* {SanitizeString(name ?? "")} ||VikingId: {play.Key}||";
} else {
msg += $"\n* *Offline Viking {play.Key}*";
}
if (play.Value != null) {
msg += $"; Reason: {play.Value}";
}
}
await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
} else {
await arg.Channel.SendMessageAsync("No-one is muted.", messageReference: arg.Reference);
}
} else if (message[0] == "!banlist") {
// mutelist command
if (Client.BannedList.Count > 0) {
Dictionary<int, string> clients = new();
foreach (Room rooom in Room.AllRooms()) {
foreach (Client player in rooom.Clients) {
int id = player.PlayerData.VikingId;
if (Client.BannedList.ContainsKey(id)) {
clients.TryAdd(id, player.PlayerData.DiplayName);
}
}
}
string msg = "Banned Players:";
foreach (KeyValuePair<int, string?> play in Client.BannedList) {
if (clients.TryGetValue(play.Key, out string? name)) {
msg += $"\n* {SanitizeString(name ?? "")} ||VikingId: {play.Key}||";
} else {
msg += $"\n* *Offline Viking {play.Key}*";
}
if (play.Value != null) {
msg += $"; Reason: {play.Value}";
}
}
await arg.Channel.SendMessageAsync(msg, messageReference: arg.Reference);
} else {
await arg.Channel.SendMessageAsync("No-one is banned.", messageReference: arg.Reference);
}
} else if (roomChannel) {
if (room != null) {
if (message[0] == "!help") {
// help command (room ver.)
await arg.Channel.SendMessageAsync(string.Join(Environment.NewLine+"* ", new string[] {
"### List of commands:",
"!help - Prints the help message. That's this message right here!",
"!list - Lists all players in this room.",
"!say <string... message> - Say something in this room."
}.Concat(everywhereCommands)
));
} else if (message[0] == "!list") {
// list command (room ver.)
if (room.ClientsCount > 0) {
string msg = "Players in Room:\n";
string msg = "Players in Room:";
foreach (Client player in room.Clients) {
string vikingid;
if (player.PlayerData.VikingId != 0) {
@ -161,19 +417,21 @@ class DiscordManager {
} else {
vikingid = "||(Not Authenticated!)||";
}
msg += $"* {SanitizeString(player.PlayerData.DiplayName)} {vikingid}\n";
msg += $"\n* {SanitizeString(player.PlayerData.DiplayName)} {vikingid}";
if (player.Muted) {
msg += " (:mute: Muted)";
}
}
SendMessage(msg, room); // Sent like this in case it's longer than 2000 characters (handled by this method).
} else {
await arg.Channel.SendMessageAsync("This room is empty.", messageReference: arg.Reference);
}
}
} else if (message[0] == "!say") {
if (message.Length > 1) {
// Due to client-based restrictions, this probably only works in SoD and maybe MaM.
// Unless we want to create an MMOAvatar instance for the server "user".
if (Room.Exists(arg.Channel.Name)) {
Room room = Room.Get(arg.Channel.Name);
} else if (message[0] == "!say") {
// say command
if (message.Length > 1) {
// Due to client-based restrictions, this probably only works in SoD and maybe MaM.
// Unless we want to create an MMOAvatar instance for the server "user".
// This is because the code for chatlogging is tied to ChatBubble and only ChatBubble.
if (room.ClientsCount > 0) {
room.Send(Utils.BuildChatMessage(Guid.Empty.ToString(), string.Join(' ', message, 1, message.Length-1), "Server"));
await arg.AddReactionAsync(Emote.Parse(":white_check_mark:"));
@ -181,14 +439,15 @@ class DiscordManager {
await arg.Channel.SendMessageAsync("There is no-one in this room to see your message.", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("This room is currently unloaded.", messageReference: arg.Reference);
await arg.Channel.SendMessageAsync("You didn't include any message content!", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("You didn't include any message content!", messageReference: arg.Reference);
}
} else {
await arg.Channel.SendMessageAsync("This room is currently unloaded.", messageReference: arg.Reference);
}
} else {
if (message[0] == "!help") {
// help command (global)
await arg.Channel.SendMessageAsync(string.Join(Environment.NewLine, new string[] {
"### List of commands:",
"!help - Prints the help message. That's this message right here!",
@ -199,7 +458,8 @@ class DiscordManager {
.Append("*Using !help in a room thread yields a different set of commands. Try using !help in a room thread!*")
));
} else if (message[0] == "!rooms") {
IEnumerable<RestThreadChannel> threads = await channel.GetActiveThreadsAsync();
// rooms command
IEnumerable<RestThreadChannel> threads = await BotChannel.GetActiveThreadsAsync();
Console.WriteLine(threads.Count());
ulong uid = socketMessage.Author.Id;
foreach (RestThreadChannel thread in threads) {
@ -211,10 +471,11 @@ class DiscordManager {
await ping.DeleteAsync();
}
} else if (message[0] == "!list") {
// list command (global)
string msg = string.Empty;
foreach (Room room in Room.AllRooms()) {
foreach (Client player in room.Clients) {
if (msg == string.Empty) msg = "Players Online:\n";
foreach (Room rooom in Room.AllRooms()) {
foreach (Client player in rooom.Clients) {
if (msg == string.Empty) msg = "Players Online:";
string vikingid;
if (player.PlayerData.VikingId != 0) {
@ -222,7 +483,10 @@ class DiscordManager {
} else {
vikingid = "||(Not Authenticated!)||";
}
msg += $"* {SanitizeString(player.PlayerData.DiplayName)} {vikingid} ({room.Name})\n";
msg += $"\n* {SanitizeString(player.PlayerData.DiplayName)} {vikingid} ({rooom.Name})";
if (player.Muted) {
msg += " (:mute: Muted)";
}
}
}
if (msg == string.Empty) {
@ -241,13 +505,14 @@ class DiscordManager {
/// </summary>
/// <param name="msg">Message to send</param>
/// <param name="room">Room to send message in - can be null</param>
public static void SendMessage(string msg, Room? room=null) {
if (client != null) {
public static void SendMessage(string msg, Room? room=null, string? roomName=null) {
if (BotClient != null) {
while (msg.Length > 0) {
string piece = msg;
if (msg.Length > 2000) { // Discord character limit is 2000.
// Find a good place to break the message, if possible.
int breakIndex = msg.LastIndexOfAny(new char[2] {' ','\n'}, 2000);
// Find a good place to break the message, if possible. Otherwise revert to 2000.
int breakIndex = msg.LastIndexOf('\n', 2000);
if (breakIndex <= 0) breakIndex = msg.LastIndexOf(' ', 2000);
if (breakIndex <= 0) breakIndex = 2000;
// Split the first part of the message and recycle the rest.
piece = msg.Substring(0, breakIndex);
@ -256,9 +521,10 @@ class DiscordManager {
// Exit the loop when the time comes.
msg = string.Empty;
}
queue.Enqueue(new QueuedMessage {
MessageQueue.Enqueue(new QueuedMessage {
msg = piece,
room = room
room = room,
roomName = roomName ?? room?.Name
});
}
}
@ -277,29 +543,41 @@ class DiscordManager {
/// <param name="player">PlayerData to associate the message with - can be null</param>
public static void SendPlayerBasedMessage(string msg, string surround="{1} **-->** {0}", Room? room=null, PlayerData? player=null) {
try {
if (client != null) {
if (BotClient != null) {
// We need to sanitize things first so that people can't
// use Discord's formatting features inside a chat message.
msg = SanitizeString(msg);
string name = SanitizeString(player?.DiplayName ?? "");
// Define for merged rooms.
string? roomName = room?.Name;
if (roomName != null) {
foreach (KeyValuePair<string, string[]> pair in Configuration.DiscordBotConfig!.Merges) {
if (pair.Value.Contains(roomName)) {
roomName = pair.Key;
if (roomName != room!.Name) surround = $"({room!.Name}) "+surround;
break;
}
}
}
if (player != null) {
if (player.VikingId != 0) {
surround = "||VikingId: "+player.VikingId+"||\n"+surround;
surround = $"||VikingId: {player.VikingId}||\n{surround}";
} else {
surround = "||(Not Authenticated!)||\n"+surround;
surround = $"||(Not Authenticated!)||\n{surround}";
}
}
// Send the sanitized message, headed with the viking id (if authenticated).
SendMessage(string.Format(surround, msg, name), room);
SendMessage(string.Format(surround, msg, name), room, roomName);
}
} catch (Exception e) {
Log(e.ToString(), ConsoleColor.Red);
}
}
public static string SanitizeString(string str) {
private static string SanitizeString(string str) {
string sanitizedStr = "";
for (int i=0;i<str.Length;i++) {
char c = str[i];
@ -327,23 +605,36 @@ class DiscordManager {
return sanitizedStr;
}
class BotConfig {
[JsonPropertyName("// Token")]
private string __TokenComment { get; set; } = "This is your bot's token. DO NOT SHARE THIS WITH ANYBODY!!!";
public string Token { get; set; }
[JsonPropertyName("// Server")]
private string __ServerComment { get; set; } = "This is the Server ID that the bot connects to.";
public ulong Server { get; set; }
[JsonPropertyName("// Channel")]
private string __ChannelComment { get; set; } = "This is the Channel ID where bot logs things and can run admin commands from.";
public ulong Channel { get; set; }
/// <summary>
/// Search all rooms for a vikingid, or null if not found.
/// This method currently returns a list because multiple clients can log in with the same viking at once. This should get fixed properly in the future.
/// </summary>
/// <param name="id">Viking ID</param>
/// <returns>Clients for ID. If there are none then return an empty list.</returns>
private static IEnumerable<Client> FindViking(int id, Room? room=null) {
List<Client> clients = new();
if (room != null) {
foreach (Client player in room.Clients) {
if (player.PlayerData.VikingId == id) {
clients.Add(player);
}
}
return clients;
}
foreach (Room rooom in Room.AllRooms()) {
foreach (Client player in rooom.Clients) {
if (player.PlayerData.VikingId == id) {
clients.Add(player);
}
}
}
return clients;
}
private class QueuedMessage {
public string msg;
public Room? room;
public string? roomName;
}
private static void Log(string msg, ConsoleColor? color=null) {

View File

@ -68,5 +68,49 @@
"// BypassToken": "Token allowed to connect without authentication",
"BypassToken": ""
},
"DiscordBot": {
"// DiscordBot": "This allows you to set up a Discord bot that logs all MMO interactions to a Discord channel.",
"// DiscordBot_": "You are not required to set this up. The server will run without it. Set the value below to true to disable the warning (and the bot).",
"Disabled": false,
"// DiscordBot+": "In the bot settings, enable the 'Server Members' and 'Message Content' intents. Otherwise, (some/all) commands may not work properly/at all.",
"// BotToken": "DO NOT PUT YOUR BOT TOKEN HERE! If you push your bot token to GitHub it may endanger your Discord account!",
"// BotToken_": "Place your token in a file named 'discord_bot_token' in this directory. Git is configured here to not include that file. DO NOT SHARE YOUR BOT TOKEN!",
"// How To Get IDs": "The below options require IDs used by Discord's API. You can find these by enabling developer mode (User Settings > Advanced > Developer Mode) and then right clicking the thing and select 'Copy ____ ID' (bottom-most option).",
"// Server": "This is the ID of the server the bot will operate in. Make sure the bot is in this server.",
"Server": 846864309454897214,
"// Channel": "This is the ID of the channel that the bot logs activity and accepts moderation commands. Make sure the bot has access to this channel.",
"Channel": 1130548490041303070,
"// RoleModerator": "ID of the role that will be moderator. This role can perform moderative actions such as muting and banning players.",
"RoleModerator": 1378174063599550554,
"// RoleAdmin": "ID of the role that will be admin. This role can perform dangerous actions and moderative actions.",
"RoleAdmin": 1378174063599550554,
"// Merges": "Merge multi-zone rooms into one channel.",
"Merges": {
"MainStreet": [
"MainStreet",
"MainStreetFunZone"
],
"LoungeInt": [
"LoungeInt",
"LoungeIntUpper"
],
"DWMadEurope": [
"DWMadEuropeItaly",
"DWMadEuropeParis",
"DWMadEuropeAlps"
],
"DWMadNYCentralPark": [
"DWMadNYCentralPark",
"DWMadNYCentralParkFunZone"
]
}
}
}

View File

@ -1,4 +1,12 @@
{
"// Usage": { "_": "(This example section isn't read. It can therefore be used for comments/documentation.)",
"// Desc": "Actions, emoticons, and canned chat messages are sent as IDs. This file maps these IDs to meaningful representations.",
"// Desc2": "If adding/changing any of these emotes, you should update this file accordingly.",
"450": { "_ID": "ID of the emote; this should be unique to the emote and must be a 32-bit integer.",
"a0000000": "I'm in School of Dragons!", "_Version": "Override for game with ClientVersion greater than or equal to this. For instance, this targets Min_SoD. Representation is in hexadecimal and is NOT case-sensitive. There can be multiple of these.",
"0": "I'm in a JumpStart Game!", "_Zero": "There should be something at 0 to handle all lower ClientVersions, lest it show up as unknown."
}
},
"Emoticons": {
"1": {
"04000001": ":grimacing:",

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@ -24,5 +24,8 @@
<None Update="emote_data.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="discord_bot_token" Condition="Exists('discord_bot_token')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>