diff --git a/Dockerfile b/Dockerfile index e6a50a2..e7664c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ COPY src/ ./ RUN dotnet publish -c Release -r linux-musl-x64 --self-contained true -o /out FROM alpine:3.23 -RUN apk add --no-cache ca-certificates tzdata libgcc libstdc++ +RUN apk add --no-cache ca-certificates tzdata libgcc libstdc++ && mkdir /data && chown 65532:65532 /data COPY --from=build /out/Beetlejuice /usr/local/bin/beetlejuice USER 65532:65532 ENTRYPOINT ["/usr/local/bin/beetlejuice"] diff --git a/src/Program.cs b/src/Program.cs index 1366a23..5add336 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -13,8 +13,12 @@ string Env(string key, string? fallback = null) => Environment.GetEnvironmentVar ?? throw new InvalidOperationException($"Missing {key}"); string discordToken = Env("DISCORD_TOKEN"); if (discordToken == "REPLACE_IN_COOLIFY") { Console.WriteLine("Set DISCORD_TOKEN in Coolify, then restart Beetlejuice."); await Task.Delay(Timeout.Infinite); return; } -ulong guildId = ulong.Parse(Env("DISCORD_GUILD_ID")); -var roles = Env("DISCORD_ALLOWED_ROLE_IDS", "").Split(',', StringSplitOptions.RemoveEmptyEntries).Select(ulong.Parse).ToHashSet(); +const ulong initialUser = 227696176412098560; +string whitelistPath = Env("WHITELIST_PATH", "/data/whitelist"); +Directory.CreateDirectory(Path.GetDirectoryName(whitelistPath)!); +var whitelist = File.Exists(whitelistPath) ? File.ReadAllLines(whitelistPath).Select(ulong.Parse).ToHashSet() : [initialUser]; +if (!File.Exists(whitelistPath)) File.WriteAllLines(whitelistPath, whitelist.Select(x => x.ToString())); +var whitelistLock = new object(); string rconPassword = Env("RCON_PASSWORD"); if (rconPassword.Length != 64) throw new InvalidOperationException("RCON_PASSWORD must be 64 characters"); var zone = TimeZoneInfo.FindSystemTimeZoneById(Env("AUTO_RESTART_TIMEZONE", "Australia/Brisbane")); @@ -30,8 +34,22 @@ builder.Services.AddDiscordGateway(o => { o.Token = discordToken; o.Intents = Ga .AddApplicationCommands(); var host = builder.Build(); -bool Allowed(ApplicationCommandContext c) => c.Interaction.GuildId == guildId && c.User is GuildInteractionUser u && - (roles.Count == 0 || u.RoleIds.Any(roles.Contains)); +bool Allowed(ApplicationCommandContext c) { lock (whitelistLock) return whitelist.Contains(c.User.Id); } +string Whitelist(ApplicationCommandContext c, string discordId, bool add) +{ + if (c.User.Id != initialUser) return "Not authorised."; + if (!ulong.TryParse(discordId, out ulong id)) return "Invalid Discord ID."; + if (!add && id == initialUser) return "The whitelist owner cannot be removed."; + lock (whitelistLock) + { + bool changed = add ? whitelist.Add(id) : whitelist.Remove(id); + if (!changed) return add ? "Already whitelisted." : "Not whitelisted."; + string temp = whitelistPath + ".tmp"; + File.WriteAllLines(temp, whitelist.Order().Select(x => x.ToString())); + File.Move(temp, whitelistPath, true); + return $"{(add ? "Added" : "Removed")} <@{id}>."; + } +} async Task Command(string command, bool allowDisconnect = false) { var address = (await Dns.GetHostAddressesAsync(Env("RCON_HOST", "zomboid"))).First(x => x.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); @@ -129,6 +147,11 @@ host.AddSlashCommand("restart", "Restart when no players are online", (Applicati Allowed(c) ? Manual() : Task.FromResult("Not authorised.")); host.AddSlashCommand("players", "List online players", async (ApplicationCommandContext c) => !Allowed(c) ? "Not authorised." : await PlayerList()); +host.AddSlashCommandGroup("whitelist", "Manage bot access", group => +{ + group.AddSubCommand("add", "Add a Discord user", (string discordId, ApplicationCommandContext c) => Whitelist(c, discordId, true)); + group.AddSubCommand("del", "Remove a Discord user", (string discordId, ApplicationCommandContext c) => Whitelist(c, discordId, false)); +}); async Task Status() { try { return $"Server online; {(await Players()).Count} player(s)."; } catch { return "Server unavailable."; } } async Task PlayerList()