diff --git a/.env-example b/.env-example index ec20db94..c6691cee 100644 --- a/.env-example +++ b/.env-example @@ -4,5 +4,6 @@ CLIPTOK_ANTIPHISHING_ENDPOINT=useyourimagination CLOUDFLARED_TOKEN=ignoreifnotrelevant USERNAME_CHECK_ENDPOINT=https://api.example.com/username CLIPTALK_WEBHOOK=https://discord.com +REACTION_LOG_WEBHOOK=https://discord.com UPTIME_KUMA_PUSH_URL= TS_AUTHKEY=tskey-auth-asdfg-asdfghj \ No newline at end of file diff --git a/Commands/DebugCmds.cs b/Commands/DebugCmds.cs index d3752820..67498bfa 100644 --- a/Commands/DebugCmds.cs +++ b/Commands/DebugCmds.cs @@ -286,8 +286,11 @@ await ctx.RespondAsync( var response = $"**Overrides for {user.Mention}:**\n\n"; foreach (var overwrite in overwrites) { + var allowedPermissions = string.IsNullOrWhiteSpace(overwrite.Value.Allowed.ToString("name")) ? "none" : overwrite.Value.Allowed.ToString("name"); + var deniedPermissions = string.IsNullOrWhiteSpace(overwrite.Value.Denied.ToString("name")) ? "none" : overwrite.Value.Denied.ToString("name"); + response += - $"<#{overwrite.Key}>:\n**Allowed**: {overwrite.Value.Allowed}\n**Denied**: {overwrite.Value.Denied}\n\n"; + $"<#{overwrite.Key}>:\n**Allowed**: {allowedPermissions}\n**Denied**: {deniedPermissions}\n\n"; } if (response.Length > 2000) @@ -473,23 +476,112 @@ public async Task Apply(TextCommandContext ctx, await msg.ModifyAsync(x => x.Content = $"{Program.cfgjson.Emoji.Success} Successfully applied {numAppliedOverrides}/{dictionary.Count} overrides for {user.Mention}!"); } - } + + [Command("dump")] + [Description("Dump all of a channel's overrides from Discord or the database.")] + [IsBotOwner] + public class DumpChannelOverrides + { + [DefaultGroupCommand] + [Command("discord")] + [Description("Dump all of a channel's overrides as they exist on the Discord channel. Does not read from db.")] + public async Task DumpFromDiscord(CommandContext ctx, + [Description("The channel to dump overrides for.")] DiscordChannel channel) + { + var overwrites = channel.PermissionOverwrites; - [Command("dumpchanneloverrides")] - [Description("Dump all of a channel's overrides. This pulls from Discord, not the database.")] - [IsBotOwner] - public async Task DumpChannelOverrides(TextCommandContext ctx, - [Description("The channel to dump overrides for.")] DiscordChannel channel) - { - var overwrites = channel.PermissionOverwrites; + string output = ""; + foreach (var overwrite in overwrites) + { + output += $"{JsonConvert.SerializeObject(overwrite)}\n"; + } - string output = ""; - foreach (var overwrite in overwrites) - { - output += $"{JsonConvert.SerializeObject(overwrite)}\n"; + await ctx.RespondAsync($"Dump from Discord:\n{await StringHelpers.CodeOrHasteBinAsync(output, "json")}"); + } + + [Command("db")] + [TextAlias("database")] + [Description("Dump all of a channel's overrides as they are stored in the db.")] + public async Task DumpFromDb(CommandContext ctx, + [Description("The channel to dump overrides for.")] DiscordChannel channel) + { + List overwrites = new(); + try + { + var allOverwrites = await Program.db.HashGetAllAsync("overrides"); + foreach (var overwrite in allOverwrites) { + var overwriteDict = JsonConvert.DeserializeObject>(overwrite.Value); + if (overwriteDict is null) continue; + if (overwriteDict.TryGetValue(channel.Id, out var value)) + overwrites.Add(value); + } + } + catch (Exception ex) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Something went wrong while trying to fetch the overrides for {channel.Mention}!" + + " There are overrides in the database but I could not parse them. Check the database manually for details."); + + Program.discord.Logger.LogError(ex, "Failed to read overrides from db for 'debug overrides dump'!"); + + return; + } + + if (overwrites.Count == 0) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} No overrides found for {channel.Mention} in the database!"); + return; + } + + string output = ""; + foreach (var overwrite in overwrites) + { + output += $"{JsonConvert.SerializeObject(overwrite)}\n"; + } + + await ctx.RespondAsync($"Dump from db:\n{await StringHelpers.CodeOrHasteBinAsync(output, "json")}"); + } } + + [Command("cleanup")] + [Aliases("clean", "prune")] + [Description("Removes overrides from the db for channels that no longer exist.")] + [IsBotOwner] + public async Task CleanUpOverrides(CommandContext ctx) + { + var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + var removedOverridesCount = 0; + + var dbOverwrites = await Program.db.HashGetAllAsync("overrides"); + foreach (var userOverwrites in dbOverwrites) + { + var overwriteDict = JsonConvert.DeserializeObject>(userOverwrites.Value); + foreach (var overwrite in overwriteDict) + { + bool channelExists = Program.discord.Guilds.Any(g => g.Value.Channels.Any(c => c.Key == overwrite.Key)); - await ctx.RespondAsync(await StringHelpers.CodeOrHasteBinAsync(output, "json")); + if (!channelExists) + { + // Channel no longer exists, remove the override + overwriteDict.Remove(overwrite.Key); + removedOverridesCount++; + } + } + + // Write back to db + // If the user now has no overrides, remove them from the db entirely + if (overwriteDict.Count == 0) + { + await Program.db.HashDeleteAsync("overrides", userOverwrites.Name); + } + else + { + // Otherwise, update the user's overrides in the db + await Program.db.HashSetAsync("overrides", userOverwrites.Name, JsonConvert.SerializeObject(overwriteDict)); + } + } + + await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Done! Cleaned up {removedOverridesCount} overrides."); + } } [Command("dmchannel")] diff --git a/Constants/RegexConstants.cs b/Constants/RegexConstants.cs index 5651805b..8882a7ff 100644 --- a/Constants/RegexConstants.cs +++ b/Constants/RegexConstants.cs @@ -9,5 +9,11 @@ public class RegexConstants readonly public static Regex bold_rx = new("\\*\\*(.*?)\\*\\*"); readonly public static Regex discord_link_rx = new(@".*discord(?:app)?.com\/channels\/((?:@)?[a-z0-9]*)\/([0-9]*)(?:\/)?([0-9]*)"); readonly public static Regex channel_rx = new("<#([0-9]+)>"); + readonly public static Regex warn_msg_rx = new($"{Program.cfgjson.Emoji.Warning} <@!?[0-9]+> was warned"); + readonly public static Regex auto_warn_msg_rx = new($"{Program.cfgjson.Emoji.Denied} <@!?[0-9]+> was automatically warned"); + readonly public static Regex mute_msg_rx = new($"{Program.cfgjson.Emoji.Muted} <@!?[0-9]+> has been muted"); + readonly public static Regex unmute_msg_rx = new($"{Program.cfgjson.Emoji.Information} Successfully unmuted"); + readonly public static Regex ban_msg_rx = new($"{Program.cfgjson.Emoji.Banned} <@!?[0-9]+> has been banned"); + readonly public static Regex unban_msg_rx = new($"{Program.cfgjson.Emoji.Unbanned} Successfully unbanned"); } } diff --git a/Events/ChannelEvents.cs b/Events/ChannelEvents.cs index 8d44533d..08e4af50 100644 --- a/Events/ChannelEvents.cs +++ b/Events/ChannelEvents.cs @@ -2,6 +2,14 @@ { public class ChannelEvents { + public static async Task ChannelCreated(DiscordClient _, ChannelCreatedEventArgs e) + { + // see comment on ChannelUpdated + + var timestamp = DateTime.Now; + Tasks.EventTasks.PendingChannelCreateEvents.Add(timestamp, e); + } + public static async Task ChannelUpdated(DiscordClient _, ChannelUpdatedEventArgs e) { // Add this event to the pending events list. These are handled in a task later, see Tasks/EventTasks/HandlePendingChannelUpdateEventsAsync @@ -12,7 +20,7 @@ public static async Task ChannelUpdated(DiscordClient _, ChannelUpdatedEventArgs public static async Task ChannelDeleted(DiscordClient client, ChannelDeletedEventArgs e) { - // see above + // see comment on ChannelUpdated var timestamp = DateTime.Now; Tasks.EventTasks.PendingChannelDeleteEvents.Add(timestamp, e); diff --git a/Events/MemberEvents.cs b/Events/MemberEvents.cs index 450e3941..77d57768 100644 --- a/Events/MemberEvents.cs +++ b/Events/MemberEvents.cs @@ -105,33 +105,43 @@ public static async Task GuildMemberRemoved(DiscordClient client, GuildMemberRem if (e.Guild.Id != cfgjson.ServerID) return; - var muteRole = await e.Guild.GetRoleAsync(cfgjson.MutedRole); + // Attempt to check if member is cached + bool isMemberCached = client.Guilds[e.Guild.Id].Members.ContainsKey(e.Member.Id); - DiscordRole tqsMuteRole = default; - if (cfgjson.TqsMutedRole != 0) - tqsMuteRole = await e.Guild.GetRoleAsync(cfgjson.TqsMutedRole); + if (isMemberCached) + { + // Only check mute role against db entry if we know the member's roles are accurate. + // If the member is not cached, we will think they have no roles when they might actually be muted! + // Then we would be falsely removing their mute entry. - var userMute = await db.HashGetAsync("mutes", e.Member.Id); + var muteRole = await e.Guild.GetRoleAsync(cfgjson.MutedRole); - if (!userMute.IsNull && !e.Member.Roles.Contains(muteRole) & !e.Member.Roles.Contains(tqsMuteRole)) - db.HashDeleteAsync("mutes", e.Member.Id); + DiscordRole tqsMuteRole = default; + if (cfgjson.TqsMutedRole != 0) + tqsMuteRole = await e.Guild.GetRoleAsync(cfgjson.TqsMutedRole); - if ((e.Member.Roles.Contains(muteRole) || e.Member.Roles.Contains(tqsMuteRole)) && userMute.IsNull) - { - MemberPunishment newMute = new() + var userMute = await db.HashGetAsync("mutes", e.Member.Id); + + if (!userMute.IsNull && !e.Member.Roles.Contains(muteRole) & !e.Member.Roles.Contains(tqsMuteRole)) + db.HashDeleteAsync("mutes", e.Member.Id); + + if ((e.Member.Roles.Contains(muteRole) || e.Member.Roles.Contains(tqsMuteRole)) && userMute.IsNull) { - MemberId = e.Member.Id, - ModId = discord.CurrentUser.Id, - ServerId = e.Guild.Id, - ExpireTime = null, - ActionTime = DateTime.Now - }; - - db.HashSetAsync("mutes", e.Member.Id, JsonConvert.SerializeObject(newMute)); - } + MemberPunishment newMute = new() + { + MemberId = e.Member.Id, + ModId = discord.CurrentUser.Id, + ServerId = e.Guild.Id, + ExpireTime = null, + ActionTime = DateTime.Now + }; + + db.HashSetAsync("mutes", e.Member.Id, JsonConvert.SerializeObject(newMute)); + } - if (!userMute.IsNull && !e.Member.Roles.Contains(muteRole) && !e.Member.Roles.Contains(tqsMuteRole)) - db.HashDeleteAsync("mutes", e.Member.Id); + if (!userMute.IsNull && !e.Member.Roles.Contains(muteRole) && !e.Member.Roles.Contains(tqsMuteRole)) + db.HashDeleteAsync("mutes", e.Member.Id); + } string rolesStr = "None"; diff --git a/Events/MessageEvent.cs b/Events/MessageEvent.cs index 5ffbe772..24d5ec1c 100644 --- a/Events/MessageEvent.cs +++ b/Events/MessageEvent.cs @@ -378,7 +378,7 @@ public static async Task MessageHandlerAsync(DiscordClient client, MockDiscordMe // still warn anyway } - DiscordMessage msg = await WarningHelpers.SendPublicWarningMessageAndDeleteInfringingMessageAsync(message, $"{{Program.cfgjson.Emoji.Denied}} {{message.Author.Mention}} was automatically warned: **{{reason.Replace(\"`\", \"\\\\`\").Replace(\"*\", \"\\\\*\")}}**", wasAutoModBlock, 1); + DiscordMessage msg = await WarningHelpers.SendPublicWarningMessageAndDeleteInfringingMessageAsync(message, $"{Program.cfgjson.Emoji.Denied} {message.Author.Mention} was automatically warned: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**", wasAutoModBlock, 1); var warning = await WarningHelpers.GiveWarningAsync(message.Author, client.CurrentUser, reason, contextMessage: msg, channel, " automatically "); await InvestigationsHelpers.SendInfringingMessaageAsync("investigations", message, reason, warning.ContextLink, wasAutoModBlock: wasAutoModBlock); match = true; diff --git a/Events/ReactionEvent.cs b/Events/ReactionEvent.cs index 889330f5..5951ab58 100644 --- a/Events/ReactionEvent.cs +++ b/Events/ReactionEvent.cs @@ -1,4 +1,5 @@ using static Cliptok.Program; +using static Cliptok.Constants.RegexConstants; namespace Cliptok.Events { @@ -6,11 +7,34 @@ public class ReactionEvent { public static async Task OnReaction(DiscordClient _, MessageReactionAddedEventArgs e) { - if (e.Emoji.Id != cfgjson.HeartosoftId || e.Channel.IsPrivate || e.Guild.Id != cfgjson.ServerID) + // Ignore DMs and other servers + if (e.Channel.IsPrivate || e.Guild.Id != cfgjson.ServerID) return; DiscordMessage targetMessage = await e.Channel.GetMessageAsync(e.Message.Id); + // Remove reactions from warning/mute/ban messages + + if (targetMessage.Author.Id == discord.CurrentUser.Id && + warn_msg_rx.IsMatch(targetMessage.Content) || + auto_warn_msg_rx.IsMatch(targetMessage.Content) || + mute_msg_rx.IsMatch(targetMessage.Content) || + unmute_msg_rx.IsMatch(targetMessage.Content) || + ban_msg_rx.IsMatch(targetMessage.Content) || + unban_msg_rx.IsMatch(targetMessage.Content)) + { + await targetMessage.DeleteReactionAsync(e.Emoji, e.User); + var emoji = e.Emoji.Id != 0 ? $"[{e.Emoji.Name}](<{e.Emoji.Url}>)" : e.Emoji.ToString(); + await LogChannelHelper.LogMessageAsync("reactions", $"<:WindowsRecycleBin:824380487920910348> Removed reaction {emoji} from {e.Message.JumpLink} by {e.User.Mention}"); + return; + } + + // Remove self-heartosofts + + if (e.Emoji.Id != cfgjson.HeartosoftId) + return; + + // Avoid starboard race conditions await Task.Delay(1000); if (targetMessage.Author.Id == e.User.Id) diff --git a/Lists/scams.txt b/Lists/scams.txt index 71ff5007..8447d7f2 100644 --- a/Lists/scams.txt +++ b/Lists/scams.txt @@ -533,3 +533,9 @@ repackme8 JPYVouTegYI nolerawin.com ?promo=TOWER10 +nicholaswallace23 +you will reimburse me 10% of your profits when you receive +interested on how to start earning $100k +Free deposit 100$ +Promocode:Open2024 +gambler.icu diff --git a/Program.cs b/Program.cs index 01bf139a..d07ed3c5 100644 --- a/Program.cs +++ b/Program.cs @@ -224,6 +224,7 @@ static async Task Main(string[] _) .HandleThreadMembersUpdated(ThreadEvents.Discord_ThreadMembersUpdated) .HandleGuildBanRemoved(UnbanEvent.OnUnban) .HandleVoiceStateUpdated(VoiceEvents.VoiceStateUpdate) + .HandleChannelCreated(ChannelEvents.ChannelCreated) .HandleChannelUpdated(ChannelEvents.ChannelUpdated) .HandleChannelDeleted(ChannelEvents.ChannelDeleted) .HandleAutoModerationRuleExecuted(AutoModEvents.AutoModerationRuleExecuted) @@ -254,6 +255,7 @@ static async Task Main(string[] _) Tasks.ReminderTasks.CheckRemindersAsync(), Tasks.RaidmodeTasks.CheckRaidmodeAsync(cfgjson.ServerID), Tasks.LockdownTasks.CheckUnlocksAsync(), + Tasks.EventTasks.HandlePendingChannelCreateEventsAsync(), Tasks.EventTasks.HandlePendingChannelUpdateEventsAsync(), Tasks.EventTasks.HandlePendingChannelDeleteEventsAsync(), ]; diff --git a/Tasks/EventTasks.cs b/Tasks/EventTasks.cs index cad8dc6e..eeafa31d 100644 --- a/Tasks/EventTasks.cs +++ b/Tasks/EventTasks.cs @@ -2,9 +2,164 @@ namespace Cliptok.Tasks { public class EventTasks { + public static Dictionary PendingChannelCreateEvents = new(); public static Dictionary PendingChannelUpdateEvents = new(); public static Dictionary PendingChannelDeleteEvents = new(); + // todo(milkshake): combine create & update handlers to reduce duplicate code + public static async Task HandlePendingChannelCreateEventsAsync() + { + bool success = false; + + try + { + foreach (var pendingEvent in PendingChannelCreateEvents) + { + // This is the timestamp on this event, used to identify it / keep events in order in the list + var timestamp = pendingEvent.Key; + + // This is a set of ChannelCreatedEventArgs for the event we are processing + var e = pendingEvent.Value; + + try + { + // Sync channel overwrites with db so that they can be restored when a user leaves & rejoins. + + // Get the current channel overwrites + var currentChannelOverwrites = e.Channel.PermissionOverwrites; + + // Get the db overwrites + var dbOverwrites = await Program.db.HashGetAllAsync("overrides"); + + // Compare the two and sync them, prioritizing overwrites on channel over stored overwrites + + foreach (var userOverwrites in dbOverwrites) + { + var overwriteDict = + JsonConvert.DeserializeObject>(userOverwrites + .Value); + + // If the db overwrites are not in the current channel overwrites, remove them from the db. + + foreach (var overwrite in overwriteDict) + { + // (if overwrite is for a different channel, skip) + if (overwrite.Key != e.Channel.Id) continue; + + // (if current overwrite is in the channel, skip) + // checking individual properties here because sometimes they are the same but the one from Discord has + // other properties like Discord (DiscordClient) that I don't care about and will wrongly mark the overwrite as different + if (currentChannelOverwrites.Any(a => CompareOverwrites(a, overwrite.Value))) + continue; + + // If it looks like the member left, do NOT remove their overrides. + + // Try to fetch member. If it fails, they are not in the guild. If this is a voice channel, remove the override. + // (if they are not in the guild & this is not a voice channel, skip; otherwise, code below handles removal) + bool isMemberInServer = await IsMemberInServer((ulong)userOverwrites.Name, e.Guild); + if (!isMemberInServer && e.Channel.Type != DiscordChannelType.Voice) + continue; + + // User could be fetched, so they are in the server and their override was removed. Remove from db. + // (or user could not be fetched & this is a voice channel; remove) + + var overrides = await Program.db.HashGetAsync("overrides", userOverwrites.Name); + var dict = JsonConvert + .DeserializeObject>(overrides); + dict.Remove(e.Channel.Id); + if (dict.Count > 0) + await Program.db.HashSetAsync("overrides", userOverwrites.Name, + JsonConvert.SerializeObject(dict)); + else + { + await Program.db.HashDeleteAsync("overrides", userOverwrites.Name); + } + } + } + + foreach (var overwrite in currentChannelOverwrites) + { + // Ignore role overrides because we aren't storing those + if (overwrite.Type == DiscordOverwriteType.Role) continue; + + // If the current channel overwrites are not in the db, add them to the db. + + // Pull out db overwrites into list + + var dbOverwriteRaw = await Program.db.HashGetAllAsync("overrides"); + var dbOverwriteList = new List>(); + + foreach (var dbOverwrite in dbOverwriteRaw) + { + var dict = JsonConvert.DeserializeObject>(dbOverwrite.Value); + dbOverwriteList.Add(dict); + } + + // If the overwrite is already in the db for this channel, skip + if (dbOverwriteList.Any(dbOverwriteSet => dbOverwriteSet.ContainsKey(e.Channel.Id) && CompareOverwrites(dbOverwriteSet[e.Channel.Id], overwrite))) + continue; + + if ((await Program.db.HashKeysAsync("overrides")).Any(a => a == overwrite.Id.ToString())) + { + // User has an overwrite in the db; add this one to their list of overrides without + // touching existing ones + + var overwrites = await Program.db.HashGetAsync("overrides", overwrite.Id); + + if (!string.IsNullOrWhiteSpace(overwrites)) + { + var dict = + JsonConvert.DeserializeObject>(overwrites); + + if (dict is not null) + { + dict.Add(e.Channel.Id, overwrite); + + if (dict.Count > 0) + await Program.db.HashSetAsync("overrides", overwrite.Id, + JsonConvert.SerializeObject(dict)); + else + await Program.db.HashDeleteAsync("overrides", overwrite.Id); + } + } + } + else + { + // User doesn't have any overrides in db, so store new dictionary + + await Program.db.HashSetAsync("overrides", + overwrite.Id, JsonConvert.SerializeObject(new Dictionary + { { e.Channel.Id, overwrite } })); + } + } + + PendingChannelCreateEvents.Remove(timestamp); + success = true; + } + catch (InvalidOperationException ex) + { + Program.discord.Logger.LogDebug(ex, "Failed to enumerate channel overwrites for channel {channel}; this usually means the permissions were changed while processing a channel event. Will try again on next task run.", e.Channel.Id); + } + catch (Exception ex) + { + // Log the exception + Program.discord.Logger.LogWarning(ex, + "Failed to process pending channel create event for channel {channel}", e.Channel.Id); + + // Always remove the event from the pending list, even if we failed to process it + PendingChannelCreateEvents.Remove(timestamp); + } + } + } + catch (InvalidOperationException ex) + { + Program.discord.Logger.LogDebug(ex, "Failed to enumerate pending channel create events; this usually means a Channel Create event was just added to the list, or one was processed and removed from the list. Will try again on next task run."); + } + + Program.discord.Logger.LogDebug(Program.CliptokEventID, "Checked pending channel create events at {time} with result: {success}", DateTime.Now, success); + return success; + } + public static async Task HandlePendingChannelUpdateEventsAsync() { bool success = false; @@ -54,8 +209,8 @@ public static async Task HandlePendingChannelUpdateEventsAsync() // Try to fetch member. If it fails, they are not in the guild. If this is a voice channel, remove the override. // (if they are not in the guild & this is not a voice channel, skip; otherwise, code below handles removal) - if (!e.Guild.Members.ContainsKey((ulong)userOverwrites.Name) && - e.ChannelAfter.Type != DiscordChannelType.Voice) + bool isMemberInServer = await IsMemberInServer((ulong)userOverwrites.Name, e.Guild); + if (!isMemberInServer && e.ChannelAfter.Type != DiscordChannelType.Voice) continue; // User could be fetched, so they are in the server and their override was removed. Remove from db. @@ -241,5 +396,28 @@ private static bool CompareOverwrites(DiscordOverwrite a, DiscordOverwrite b) return a.Allowed == b.Allowed && a.Denied == b.Denied && a.Id == b.Id && a.Type == b.Type && a.CreationTimestamp == b.CreationTimestamp; } + + private static async Task IsMemberInServer(ulong userId, DiscordGuild guild) + { + bool isMemberInServer = false; + + // Check cache first + if (guild.Members.ContainsKey(userId)) + return true; + + // If the user isn't cached, try fetching them to confirm + try + { + await guild.GetMemberAsync(userId); + isMemberInServer = true; + } + catch (DSharpPlus.Exceptions.NotFoundException) + { + // Member is not in the server + // isMemberInServer is already false + } + + return isMemberInServer; + } } } \ No newline at end of file diff --git a/config.json b/config.json index 6e07ab7d..2f9f1206 100644 --- a/config.json +++ b/config.json @@ -287,6 +287,10 @@ }, "nicknames": { "channelId": 1280688061528674314 + }, + "reactions": { + "webhookEnvVar": "REACTION_LOG_WEBHOOK", + "channelId": 1311084236702482542 } }, "botOwners": [ diff --git a/docker-compose.yml b/docker-compose.yml index 2de7923e..a47b1e27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,8 @@ services: # Overrides your configs Redis options for use with Docker Compose # I don't advise changing this unless you have a strange setup - REDIS_DOCKER_OVERRIDE=true + # uncomment this for bespoke setups + network_mode: service:tailscale redis: image: 'redis:7.2-alpine' restart: always @@ -56,6 +58,8 @@ services: image: tailscale/tailscale:latest volumes: - ./data/ts-state:/var/lib/tailscale + devices: + - /dev/net/tun:/dev/net/tun environment: - TS_AUTHKEY=${TS_AUTHKEY} - TS_STATE_DIR=/var/lib/tailscale @@ -65,4 +69,3 @@ services: - net_admin - sys_module restart: always - network_mode: service:bot