diff --git a/.gitignore b/.gitignore index a4b3f4b..bc52bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /avatars/ /backups/ /database/ +/.idea/ nb*.xml \ No newline at end of file diff --git a/src/main/java/com/jagrosh/vortex/automod/AutoMod.java b/src/main/java/com/jagrosh/vortex/automod/AutoMod.java index ab5f8d7..cf0f233 100644 --- a/src/main/java/com/jagrosh/vortex/automod/AutoMod.java +++ b/src/main/java/com/jagrosh/vortex/automod/AutoMod.java @@ -28,6 +28,7 @@ import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -208,19 +209,19 @@ // ignore users vortex cant interact with if(!member.getGuild().getSelfMember().canInteract(member)) return false; - + // ignore users that can kick if(member.hasPermission(Permission.KICK_MEMBERS)) return false; - + // ignore users that can ban if(member.hasPermission(Permission.BAN_MEMBERS)) return false; - + // ignore users that can manage server if(member.hasPermission(Permission.MANAGE_SERVER)) return false; - + // if a channel is specified, ignore users that can manage messages in that channel if(channel!=null && (member.hasPermission(channel, Permission.MESSAGE_MANAGE) || vortex.getDatabase().ignores.isIgnored(channel))) return false; @@ -260,10 +261,14 @@ AutomodSettings settings = vortex.getDatabase().automod.getSettings(message.getGuild()); if(settings==null) return; - + // check the channel for channel-specific settings boolean preventSpam = message.getTextChannel().getTopic()==null || !message.getTextChannel().getTopic().toLowerCase().contains("{spam}"); - boolean preventInvites = message.getTextChannel().getTopic()==null || !message.getTextChannel().getTopic().toLowerCase().contains("{invites}"); + boolean preventInvites = (message.getTextChannel().getTopic()==null || !message.getTextChannel().getTopic().toLowerCase().contains("{invites}")) + && settings.inviteStrikes > 0; + + List inviteWhitelist = !preventInvites ? Collections.emptyList() + : vortex.getDatabase().inviteWhitelist.readWhitelist(message.getGuild()); boolean shouldDelete = false; String shouldChannelMute = null; @@ -389,7 +394,7 @@ } // anti-invite - if(settings.inviteStrikes > 0 && preventInvites) + if(preventInvites) { List invites = new ArrayList<>(); Matcher m = INVITES.matcher(message.getContentRaw()); @@ -399,7 +404,7 @@ for(String inviteCode : invites) { long gid = inviteResolver.resolve(message.getJDA(), inviteCode); - if(gid != message.getGuild().getIdLong()) + if(gid != message.getGuild().getIdLong() && !inviteWhitelist.contains(gid)) { strikeTotal += settings.inviteStrikes; reason.append(", Advertising"); @@ -462,7 +467,7 @@ } // now, lets resolve links, but async - if(!shouldDelete && settings.resolveUrls && (settings.inviteStrikes>0 || settings.refStrikes>0)) + if(!shouldDelete && settings.resolveUrls && (preventInvites || settings.refStrikes>0)) { List links = new LinkedList<>(); Matcher m = LINK.matcher(message.getContentRaw()); @@ -481,9 +486,10 @@ redirects = urlResolver.findRedirects(link); for(String resolved: redirects) { - if(settings.inviteStrikes>0 && resolved.matches(INVITE_LINK)) + if(preventInvites && resolved.matches(INVITE_LINK)) { - if(inviteResolver.resolve(message.getJDA(), resolved.replaceAll(INVITE_LINK, "$1")) != message.getGuild().getIdLong()) + long invite = inviteResolver.resolve(message.getJDA(), resolved.replaceAll(INVITE_LINK, "$1")); + if(invite != message.getGuild().getIdLong() && !inviteWhitelist.contains(invite)) containsInvite = true; } if(settings.refStrikes>0) @@ -493,7 +499,7 @@ } } - if((containsInvite || settings.inviteStrikes<1) && (containsRef || settings.refStrikes<1)) + if((containsInvite || !preventInvites) && (containsRef || settings.refStrikes<1)) break; } int rstrikeTotal = (containsInvite ? settings.inviteStrikes : 0) + (containsRef ? settings.refStrikes : 0); diff --git a/src/main/java/com/jagrosh/vortex/commands/automod/AntiinviteCmd.java b/src/main/java/com/jagrosh/vortex/commands/automod/AntiinviteCmd.java index 4bf0519..2face8c 100644 --- a/src/main/java/com/jagrosh/vortex/commands/automod/AntiinviteCmd.java +++ b/src/main/java/com/jagrosh/vortex/commands/automod/AntiinviteCmd.java @@ -37,9 +37,10 @@ this.guildOnly = true; this.aliases = new String[]{"antinvite","anti-invite"}; this.category = new Category("AutoMod"); - this.arguments = ""; + this.arguments = ""; this.help = "sets strikes for posting invites"; this.userPermissions = new Permission[]{Permission.MANAGE_SERVER}; + this.children = new Command[] {new WhitelistInvitesCmd(vortex)}; } @Override diff --git a/src/main/java/com/jagrosh/vortex/commands/automod/WhitelistInvitesCmd.java b/src/main/java/com/jagrosh/vortex/commands/automod/WhitelistInvitesCmd.java new file mode 100644 index 0000000..ba6bf09 --- /dev/null +++ b/src/main/java/com/jagrosh/vortex/commands/automod/WhitelistInvitesCmd.java @@ -0,0 +1,152 @@ +/* + * Copyright 2018 John Grosh (john.a.grosh@gmail.com). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jagrosh.vortex.commands.automod; + +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.vortex.Vortex; +import net.dv8tion.jda.core.Permission; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Michael Ritter (Kantenkugel) + */ +public class WhitelistInvitesCmd extends Command +{ + private final Vortex vortex; + private final static String DESCRIPTION = "Used to add/remove guilds from the invite whitelist. " + + "Invites to whitelisted guilds are completely ignored by the automod.\n" + + "Valid options are `ADD GUILD_ID[ GUILD_ID...]`, `REMOVE GUILD_ID[ GUILD_ID...]` and `SHOW`"; + + public WhitelistInvitesCmd(Vortex vortex) + { + this.vortex = vortex; + this.name = "whitelist"; + this.guildOnly = true; + this.category = new Category("AutoMod"); + this.arguments = ""; + this.help = "if strikes for invites are enabled, add/remove whitelisted guilds"; + this.userPermissions = new Permission[]{Permission.MANAGE_SERVER}; + } + + @Override + protected void execute(CommandEvent event) + { + String[] args = event.getArgs().toLowerCase().split("\\s+"); + switch(args[0]) + { + case "show": + handleShow(event, args); + break; + case "add": + handleAdd(event, args); + break; + case "remove": + handleRemove(event, args); + break; + default: + event.replyWarning(DESCRIPTION); + break; + } + } + + private void handleShow(CommandEvent event, String[] args) + { + if(args.length > 1) + { + event.replyWarning(DESCRIPTION); + return; + } + List currentWL = vortex.getDatabase().inviteWhitelist.readWhitelist(event.getGuild()); + event.replySuccess("Whitelisted Guild IDs:\n" + (currentWL.isEmpty() ? "None" : + currentWL.stream().map(String::valueOf).collect(Collectors.joining(", ")))); + } + + private void handleAdd(CommandEvent event, String[] args) + { + if(args.length <= 1) + { + event.replyWarning(DESCRIPTION); + return; + } + List guildIds = readIds(args); + if(guildIds == null) + { + event.replyWarning("Invalid Guild-ID(s) provided!"); + return; + } + if(guildIds.size() > 1) + { + vortex.getDatabase().inviteWhitelist.addAllToWhitelist(event.getGuild(), guildIds); + } + else if(!vortex.getDatabase().inviteWhitelist.addToWhitelist(event.getGuild(), guildIds.get(0))) + { + event.replyWarning("Given Guild was already whitelisted"); + return; + } + event.replySuccess("Whitelist has been modified"); + } + + private void handleRemove(CommandEvent event, String[] args) + { + if(args.length <= 1) + { + event.replyWarning(DESCRIPTION); + return; + } + List guildIds = readIds(args); + if(guildIds == null) + { + event.replyWarning("Invalid Guild-ID(s) provided!"); + return; + } + if(guildIds.size() > 1) + { + vortex.getDatabase().inviteWhitelist.removeAllFromWhitelist(event.getGuild(), guildIds); + } + else if(!vortex.getDatabase().inviteWhitelist.removeFromWhitelist(event.getGuild(), guildIds.get(0))) + { + event.replyWarning("Given Guild was not whitelisted"); + return; + } + event.replySuccess("Whitelist has been modified"); + } + + private List readIds(String[] args) + { + List guildIds = new ArrayList<>(args.length - 1); + try + { + for(int i = 1; i < args.length; i++) + { + long parsedId = Long.parseUnsignedLong(args[i]); + if(parsedId < 10_000_000_000_000_000L) //plausibility check + { + return null; + } + guildIds.add(parsedId); + } + } + catch(NumberFormatException ex) + { + return null; + } + return guildIds; + } +} diff --git a/src/main/java/com/jagrosh/vortex/database/Database.java b/src/main/java/com/jagrosh/vortex/database/Database.java index b2e0f5c..a3b8c9d 100644 --- a/src/main/java/com/jagrosh/vortex/database/Database.java +++ b/src/main/java/com/jagrosh/vortex/database/Database.java @@ -33,6 +33,7 @@ public final TempMuteManager tempmutes; public final TempBanManager tempbans; public final PremiumManager premium; + public final InviteWhitelistManager inviteWhitelist; public Database(String host, String user, String pass) throws Exception { @@ -47,6 +48,7 @@ tempmutes = new TempMuteManager(this); tempbans = new TempBanManager(this); premium = new PremiumManager(this); + inviteWhitelist = new InviteWhitelistManager(this); init(); } diff --git a/src/main/java/com/jagrosh/vortex/database/managers/InviteWhitelistManager.java b/src/main/java/com/jagrosh/vortex/database/managers/InviteWhitelistManager.java new file mode 100644 index 0000000..39deca3 --- /dev/null +++ b/src/main/java/com/jagrosh/vortex/database/managers/InviteWhitelistManager.java @@ -0,0 +1,133 @@ +package com.jagrosh.vortex.database.managers; + +import com.jagrosh.easysql.DataManager; +import com.jagrosh.easysql.DatabaseConnector; +import com.jagrosh.easysql.SQLColumn; +import com.jagrosh.easysql.columns.LongColumn; +import com.jagrosh.vortex.utils.FixedCache; +import net.dv8tion.jda.core.entities.Guild; +import org.slf4j.LoggerFactory; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class InviteWhitelistManager extends DataManager +{ + + public static final SQLColumn GUILD_ID = new LongColumn("GUILD_ID", false, 0L); + public static final SQLColumn WHITELIST_ID = new LongColumn("WL_ID", false, 0L); + + private final FixedCache> cache = new FixedCache<>(1000); + + public InviteWhitelistManager(DatabaseConnector connector) + { + super(connector, "INVITE_WL"); + } + + @Override + protected String primaryKey() + { + return GUILD_ID+", "+WHITELIST_ID; + } + + public boolean addToWhitelist(Guild guild, long whitelistId) + { + invalidateCache(guild); + return readWrite(selectAll(GUILD_ID.is(guild.getId()) + " AND " + WHITELIST_ID.is(whitelistId)), rs -> + { + if(rs.next()) + return false; + rs.moveToInsertRow(); + GUILD_ID.updateValue(rs, guild.getIdLong()); + WHITELIST_ID.updateValue(rs, whitelistId); + rs.insertRow(); + return true; + }); + } + + public void addAllToWhitelist(Guild guild, Collection whitelistIds) + { + invalidateCache(guild); + Set ids = new HashSet<>(whitelistIds); + readWrite(selectAll(String.format("%s AND %s IN (%s)", + GUILD_ID.is(guild.getId()), WHITELIST_ID.name, + ids.stream().map(String::valueOf).collect(Collectors.joining(",")))),rs -> + { + while(rs.next()) + { + ids.remove(WHITELIST_ID.getValue(rs)); + } + for(long id : ids) + { + rs.moveToInsertRow(); + GUILD_ID.updateValue(rs, guild.getIdLong()); + WHITELIST_ID.updateValue(rs, id); + rs.insertRow(); + } + }); + } + + public boolean removeFromWhitelist(Guild guild, long whitelistId) + { + invalidateCache(guild); + return readWrite(selectAll(GUILD_ID.is(guild.getId())+" AND "+WHITELIST_ID.is(whitelistId)), rs -> + { + if(rs.next()) + { + rs.deleteRow(); + return true; + } + return false; + }); + } + + public void removeAllFromWhitelist(Guild guild, Collection whitelistIds) + { + invalidateCache(guild); + try + { + PreparedStatement stmt = getConnection().prepareStatement("DELETE FROM " + getTableName() + " WHERE " + + GUILD_ID.name + " = ? AND " + WHITELIST_ID.name + " IN (" + + IntStream.range(0, whitelistIds.size()).mapToObj(i -> "?").collect(Collectors.joining(",")) + ')'); + int paramIndex = 0; + stmt.setLong(++paramIndex, guild.getIdLong()); + for(Long whitelistId : whitelistIds) + { + stmt.setLong(++paramIndex, whitelistId); + } + stmt.executeUpdate(); + } + catch(SQLException e) + { + LoggerFactory.getLogger(DatabaseConnector.class).error("Exception in SQL: "+e); + } + } + + public List readWhitelist(Guild guild) + { + if(cache.contains(guild.getIdLong())) + return cache.get(guild.getIdLong()); + List whitelist = read(selectAll(GUILD_ID.is(guild.getId())), rs -> + { + List list = new LinkedList<>(); + while(rs.next()) + list.add(WHITELIST_ID.getValue(rs)); + return Collections.unmodifiableList(list); + }); + cache.put(guild.getIdLong(), whitelist); + return whitelist; + } + + private void invalidateCache(Guild guild) + { + cache.pull(guild.getIdLong()); + } +}