From 4ec4320207c39ece1517c180f46d123398e208d3 Mon Sep 17 00:00:00 2001 From: xdnw Date: Thu, 13 Jul 2023 07:18:57 +0100 Subject: [PATCH 1/3] abstract summarizer --- .../link/locutus/discord/gpt/GPTUtil.java | 13 +++++++----- .../discord/gpt/imps/ProcessSummarizer.java | 21 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/main/java/link/locutus/discord/gpt/GPTUtil.java b/src/main/java/link/locutus/discord/gpt/GPTUtil.java index c3d84187..8de179fe 100644 --- a/src/main/java/link/locutus/discord/gpt/GPTUtil.java +++ b/src/main/java/link/locutus/discord/gpt/GPTUtil.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Function; public class GPTUtil { @@ -46,18 +47,20 @@ public static int getTokens(String input, ModelType type) { return enc.encode(input).size(); } public static List getChunks(String input, ModelType type, int tokenSizeCap) { + EncodingRegistry registry = Encodings.newDefaultEncodingRegistry(); + Encoding enc = registry.getEncodingForModel(type); + return getChunks(input, tokenSizeCap, s -> enc.encode(s).size()); + } + + public static List getChunks(String input, int tokenSizeCap, Function getSize) { List result = new ArrayList<>(); String[] lines = input.split("[\r\n]+|\\.\\s"); - // get tokenizer - EncodingRegistry registry = Encodings.newDefaultEncodingRegistry(); - Encoding enc = registry.getEncodingForModel(type); - // get the tokens count for each line List tokensCount = new ArrayList<>(); for (String line : lines) { - int size = enc.encode(line).size(); + int size = getSize.apply(line); if (size > tokenSizeCap) { throw new IllegalArgumentException("Line exceeds token limit of " + tokenSizeCap); } diff --git a/src/main/java/link/locutus/discord/gpt/imps/ProcessSummarizer.java b/src/main/java/link/locutus/discord/gpt/imps/ProcessSummarizer.java index 9519b6e9..02dda9f1 100644 --- a/src/main/java/link/locutus/discord/gpt/imps/ProcessSummarizer.java +++ b/src/main/java/link/locutus/discord/gpt/imps/ProcessSummarizer.java @@ -16,14 +16,15 @@ import java.util.Base64; import java.util.List; -public class ProcessSummarizer implements ISummarizer { +public abstract class ProcessSummarizer implements ISummarizer { private final File file; private final String prompt; private final int promptTokens; private final ModelType model; private final File venvExe; + private final int tokenCap; - public ProcessSummarizer(File venvExe, File file) { + public ProcessSummarizer(File venvExe, File file, ModelType model, int tokenCap) { this.venvExe = venvExe; this.file = file; this.prompt = """ @@ -32,15 +33,21 @@ public ProcessSummarizer(File venvExe, File file) { {query} Concise summary:"""; - this.model = ModelType.GPT_4; - this.promptTokens = GPTUtil.getTokens(prompt.replace("{query}", ""), model); + this.model = model; + String queryLess = prompt.replace("{query}", ""); + this.promptTokens = queryLess.isEmpty() ? 0 : getSize(queryLess); + this.tokenCap = tokenCap; } + + public int getSize(String text) { + return GPTUtil.getTokens(text, model); + } + @Override public String summarize(String text) { - int cap = 4096 - 4; - int remaining = cap - promptTokens; + int remaining = tokenCap - promptTokens; List summaries = new ArrayList<>(); - for (String chunk : GPTUtil.getChunks(text, model, remaining)) { + for (String chunk : GPTUtil.getChunks(text, remaining, this::getSize)) { String result = summarizeChunk(chunk); summaries.add(result); } From ab125b7159f4ef414f75a7a171ab04816b7cc4fe Mon Sep 17 00:00:00 2001 From: xdnw Date: Sat, 15 Jul 2023 06:41:36 +0100 Subject: [PATCH 2/3] extract text --- build.gradle | 16 +- .../link/locutus/discord/gpt/GptHandler.java | 10 +- .../discord/gpt/imps/GPTSummarizer.java | 7 +- .../discord/gpt/imps/ProcessSummarizer.java | 2 +- .../locutus/discord/gpt/test/ExtractText.java | 173 ++++++++++++++++++ .../link/locutus/discord/util/MarkupUtil.java | 9 + 6 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 src/main/java/link/locutus/discord/gpt/test/ExtractText.java diff --git a/build.gradle b/build.gradle index df2d0acb..d14decb6 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,9 @@ dependencies { implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' implementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25' - implementation "org.xerial:sqlite-jdbc:3.30.1" + implementation 'com.google.http-client:google-http-client-jackson2:1.43.3' + + implementation('org.xerial:sqlite-jdbc:3.40.0.0') implementation "com.google.code.gson:gson:2.8.6" implementation 'org.jsoup:jsoup:1.12.1' implementation 'org.bitbucket.cowwoc:diff-match-patch:1.2' @@ -90,10 +92,11 @@ dependencies { implementation 'com.ptsmods:mysqlw:1.7' implementation 'org.openjdk.nashorn:nashorn-core:15.4' - implementation 'com.google.api-client:google-api-client:1.33.0' - implementation 'com.google.oauth-client:google-oauth-client-jetty:1.32.1' - implementation 'com.google.apis:google-api-services-sheets:v4-rev20210629-1.32.1' - implementation 'com.google.apis:google-api-services-drive:v3-rev20211107-1.32.1' + implementation 'com.google.api-client:google-api-client:2.0.0' + implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1' + implementation 'com.google.apis:google-api-services-sheets:v4-rev20220927-2.0.0' + implementation 'com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0' + implementation 'com.google.apis:google-api-services-docs:v1-rev20220609-2.0.0' implementation 'de.erichseifert.gral:gral-core:0.11' implementation 'org.kefirsf:kefirbb:1.5' @@ -133,7 +136,7 @@ dependencies { // https://mvnrepository.com/artifact/de.siegmar/fastcsv implementation group: 'de.siegmar', name: 'fastcsv', version: '2.2.0' - implementation('org.xerial:sqlite-jdbc:3.40.0.0') + implementation('org.hibernate:hibernate-core:6.1.5.Final') implementation('org.hibernate:hibernate-community-dialects:6.1.5.Final') @@ -141,7 +144,6 @@ dependencies { implementation("org.jooq:jooq-meta:3.17.5") implementation("org.jooq:jooq-codegen:3.17.5") - jooqGenerator 'org.xerial:sqlite-jdbc:3.36.0.3' implementation("com.theokanning.openai-gpt3-java:client:0.12.0") diff --git a/src/main/java/link/locutus/discord/gpt/GptHandler.java b/src/main/java/link/locutus/discord/gpt/GptHandler.java index 6e3fad9d..f2833a7d 100644 --- a/src/main/java/link/locutus/discord/gpt/GptHandler.java +++ b/src/main/java/link/locutus/discord/gpt/GptHandler.java @@ -58,18 +58,18 @@ public GptHandler() throws SQLException, ClassNotFoundException { this.embeddingDatabase = new AdaEmbedding(registry, service); // TODO change ^ that to mini - File gpt4freePath = new File("../gpt4free/mymain.py"); + File gpt3freePath = new File("../gpt4free/gpt3_5_turbo.py"); File venvExe = new File("../gpt4free/venv/Scripts/python.exe"); // ensure files exist - if (!gpt4freePath.exists()) { - throw new RuntimeException("gpt4free not found: " + gpt4freePath.getAbsolutePath()); + if (!gpt3freePath.exists()) { + throw new RuntimeException("gpt4free not found: " + gpt3freePath.getAbsolutePath()); } if (!venvExe.exists()) { throw new RuntimeException("venv not found: " + venvExe.getAbsolutePath()); } - this.summarizer = new ProcessSummarizer(venvExe, gpt4freePath); - this.text2text = new ProcessText2Text(venvExe, gpt4freePath); + this.summarizer = new ProcessSummarizer(venvExe, gpt3freePath, ModelType.GPT_3_5_TURBO, 8192); + this.text2text = new ProcessText2Text(venvExe, gpt3freePath); } public ProcessText2Text getText2text() { diff --git a/src/main/java/link/locutus/discord/gpt/imps/GPTSummarizer.java b/src/main/java/link/locutus/discord/gpt/imps/GPTSummarizer.java index 29bac4fb..d90a80a0 100644 --- a/src/main/java/link/locutus/discord/gpt/imps/GPTSummarizer.java +++ b/src/main/java/link/locutus/discord/gpt/imps/GPTSummarizer.java @@ -25,11 +25,14 @@ public GPTSummarizer(EncodingRegistry registry, OpenAiService service) { this.registry = registry; this.service = service; this.prompt = """ - Write a concise summary which preserves syntax, equations, arguments and constraints of the following: + You will be provided text from a user guide for the game Politics And War. + Take the information and organize it into dot points of factual knowledge (start each line with `- `). Do not make anything up. + Preserve syntax, formulas and precision. + Text: {query} - Concise summary:"""; + Fact summary:"""; this.model = ModelType.GPT_3_5_TURBO; this.chatEncoder = registry.getEncodingForModel(model); this.promptTokens = GPTUtil.getTokens(prompt.replace("{query}", ""), model); diff --git a/src/main/java/link/locutus/discord/gpt/imps/ProcessSummarizer.java b/src/main/java/link/locutus/discord/gpt/imps/ProcessSummarizer.java index 02dda9f1..45694823 100644 --- a/src/main/java/link/locutus/discord/gpt/imps/ProcessSummarizer.java +++ b/src/main/java/link/locutus/discord/gpt/imps/ProcessSummarizer.java @@ -16,7 +16,7 @@ import java.util.Base64; import java.util.List; -public abstract class ProcessSummarizer implements ISummarizer { +public class ProcessSummarizer implements ISummarizer { private final File file; private final String prompt; private final int promptTokens; diff --git a/src/main/java/link/locutus/discord/gpt/test/ExtractText.java b/src/main/java/link/locutus/discord/gpt/test/ExtractText.java new file mode 100644 index 00000000..167d73a7 --- /dev/null +++ b/src/main/java/link/locutus/discord/gpt/test/ExtractText.java @@ -0,0 +1,173 @@ +package link.locutus.discord.gpt.test; + +// Copyright 2019 Google LLC +// +// 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. + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; +import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.store.FileDataStoreFactory; +import com.google.api.services.docs.v1.Docs; +import com.google.api.services.docs.v1.DocsScopes; +import com.google.api.services.docs.v1.model.Body; +import com.google.api.services.docs.v1.model.Document; +import com.google.api.services.docs.v1.model.ParagraphElement; +import com.google.api.services.docs.v1.model.StructuralElement; +import com.google.api.services.docs.v1.model.TableCell; +import com.google.api.services.docs.v1.model.TableRow; +import com.google.api.services.docs.v1.model.TextRun; +import link.locutus.discord.util.MarkupUtil; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.GeneralSecurityException; +import java.util.Collections; +import java.util.List; + +public class ExtractText { + private static final String APPLICATION_NAME = "Google Docs API Extract Guide"; + private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); + protected static final String TOKENS_DIRECTORY_PATH = "config" + java.io.File.separator + "tokens2"; + private static final String CREDENTIALS_FILE_PATH = "config" + java.io.File.separator + "credentials-drive.json"; + + /** + * Global instance of the scopes required by this quickstart. If modifying these scopes, delete + * your previously saved tokens/ folder. + */ + private static final List SCOPES = + Collections.singletonList(DocsScopes.DOCUMENTS_READONLY); + + + public static java.io.File getCredentialsFile() { + return new java.io.File(CREDENTIALS_FILE_PATH); + } + + /** + * Creates an authorized Credential object. + * + * @param HTTP_TRANSPORT The network HTTP Transport. + * @return An authorized Credential object. + * @throws IOException If the credentials.json file cannot be found. + */ + private static Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) + throws IOException { + // Load client secrets. + // ensure file exists CREDENTIALS_FILE_PATH + if (!new java.io.File(CREDENTIALS_FILE_PATH).exists()) { + System.out.println("File not found: " + CREDENTIALS_FILE_PATH); + throw new IOException("File not found: " + CREDENTIALS_FILE_PATH); + } + // ensure new java.io.File(TOKENS_DIRECTORY_PATH) exists + if (!new java.io.File(TOKENS_DIRECTORY_PATH).exists()) { + if (!new java.io.File(TOKENS_DIRECTORY_PATH).mkdirs()) { + System.out.println("Unable to create directory: " + TOKENS_DIRECTORY_PATH); + throw new IOException("Unable to create directory: " + TOKENS_DIRECTORY_PATH); + } + } + + java.io.File file = getCredentialsFile(); + try (InputStream in = new FileInputStream(file)) { + GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in)); + + // Build flow and trigger user authorization request. + GoogleAuthorizationCodeFlow flow = + new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES) + .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIRECTORY_PATH))) + .setAccessType("offline") + .build(); + LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build(); + return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user"); + } + } + + /** + * Returns the text in the given ParagraphElement. + * + * @param element a ParagraphElement from a Google Doc + */ + private static String readParagraphElement(ParagraphElement element) { + TextRun run = element.getTextRun(); + if (run == null || run.getContent() == null) { + // The TextRun can be null if there is an inline object. + return ""; + } + return run.getContent(); + } + + /** + * Recurses through a list of Structural Elements to read a document's text where text may be in + * nested elements. + * + * @param elements a list of Structural Elements + */ + private static String readStructuralElements(List elements) { + StringBuilder sb = new StringBuilder(); + for (StructuralElement element : elements) { + if (element.getParagraph() != null) { + for (ParagraphElement paragraphElement : element.getParagraph().getElements()) { + sb.append(readParagraphElement(paragraphElement)); + } + } else if (element.getTable() != null) { + // The text in table cells are in nested Structural Elements and tables may be + // nested. + for (TableRow row : element.getTable().getTableRows()) { + for (TableCell cell : row.getTableCells()) { + sb.append(readStructuralElements(cell.getContent())); + } + } + } else if (element.getTableOfContents() != null) { + // The text in the TOC is also in a Structural Element. + sb.append(readStructuralElements(element.getTableOfContents().getContent())); + } + } + return sb.toString(); + } + + public static String getDocumentMarkdown(String documentId) throws IOException, GeneralSecurityException { + final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); + Docs service = + new Docs.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT)) + .setApplicationName(APPLICATION_NAME) + .build(); + + String url = "https://docs.google.com/feeds/download/documents/export/Export?id=" + documentId + "&exportFormat=html"; + HttpRequest request = service.getRequestFactory().buildGetRequest(new GenericUrl(url)); + request.getHeaders().setAuthorization("Bearer " + getCredentials(HTTP_TRANSPORT).getAccessToken()); + HttpResponse response = request.execute(); + String html = response.parseAsString(); + System.out.println(html); + // convert to markdown + String markdown = MarkupUtil.htmlToMarkdown(html); + // remove images + markdown = MarkupUtil.removeImages(markdown); + markdown = MarkupUtil.stripImageReferences(markdown); + // remove duplicate tabs and duplicate newlines + markdown = markdown.replaceAll("\t+", "\t").replaceAll("\n+", "\n"); + // replace html tables with csv + + + return markdown.trim(); + } +} diff --git a/src/main/java/link/locutus/discord/util/MarkupUtil.java b/src/main/java/link/locutus/discord/util/MarkupUtil.java index 7642fd5c..c2f4365f 100644 --- a/src/main/java/link/locutus/discord/util/MarkupUtil.java +++ b/src/main/java/link/locutus/discord/util/MarkupUtil.java @@ -23,6 +23,15 @@ public static String markdownUrl(String name, String url) { return String.format("[%s](%s)", name, url); } + public static String stripImageReferences(String markdown) { + Pattern pattern = Pattern.compile("\\[.*?]:\\s.*?(\\r?\\n|$)"); + return pattern.matcher(markdown).replaceAll(""); + } + + public static String removeImages(String markdown) { + return markdown.replaceAll("!\\[.*?]\\(.*?\\)", "").replaceAll("!\\[.*?]\\[\\]", ""); + } + public static String htmlUrl(String name, String url) { return String.format("%s", url, name); } From dd29e0cdf7117d03cf94fac94535a44a97995279 Mon Sep 17 00:00:00 2001 From: xdnw Date: Mon, 17 Jul 2023 02:43:10 +0100 Subject: [PATCH 3/3] Update autorole task --- .../java/link/locutus/discord/Locutus.java | 3 +- .../discord/commands/account/AutoRole.java | 64 +-- .../v2/impl/pw/commands/BankCommands.java | 21 +- .../v2/impl/pw/commands/ExchangeCommands.java | 10 - .../v2/impl/pw/commands/FACommands.java | 6 +- .../v2/impl/pw/commands/UtilityCommands.java | 80 ++-- .../java/link/locutus/discord/db/GuildDB.java | 10 +- .../link/locutus/discord/db/GuildHandler.java | 5 +- .../locutus/discord/db/entities/DBNation.java | 6 +- .../locutus/discord/db/guild/GuildKey.java | 10 +- .../discord/util/task/roles/AutoRoleInfo.java | 400 ++++++++++++++++ .../discord/util/task/roles/AutoRoleTask.java | 441 ++++++++---------- .../util/task/roles/DelegateAutoRoleTask.java | 28 +- .../util/task/roles/IAutoRoleTask.java | 12 +- .../util/update/NationUpdateProcessor.java | 4 +- 15 files changed, 698 insertions(+), 402 deletions(-) create mode 100644 src/main/java/link/locutus/discord/util/task/roles/AutoRoleInfo.java diff --git a/src/main/java/link/locutus/discord/Locutus.java b/src/main/java/link/locutus/discord/Locutus.java index e59aad76..6a6a341f 100644 --- a/src/main/java/link/locutus/discord/Locutus.java +++ b/src/main/java/link/locutus/discord/Locutus.java @@ -578,8 +578,7 @@ public void autoRole(DBNation nation) { Member member = guild.getMember(discordUser); if (member != null) { try { - db.getAutoRoleTask().autoRole(member, s -> { - }); + db.getAutoRoleTask().autoRole(member, nation, true); } catch (Throwable e) { e.printStackTrace(); } diff --git a/src/main/java/link/locutus/discord/commands/account/AutoRole.java b/src/main/java/link/locutus/discord/commands/account/AutoRole.java index d5545d05..74adefb3 100644 --- a/src/main/java/link/locutus/discord/commands/account/AutoRole.java +++ b/src/main/java/link/locutus/discord/commands/account/AutoRole.java @@ -4,21 +4,23 @@ import link.locutus.discord.apiv1.enums.Rank; import link.locutus.discord.commands.manager.Command; import link.locutus.discord.commands.manager.CommandCategory; +import link.locutus.discord.commands.manager.v2.binding.annotation.Switch; import link.locutus.discord.commands.manager.v2.command.IMessageIO; import link.locutus.discord.commands.manager.v2.impl.pw.CM; +import link.locutus.discord.commands.manager.v2.impl.pw.commands.UtilityCommands; import link.locutus.discord.db.GuildDB; import link.locutus.discord.db.entities.DBNation; import link.locutus.discord.db.guild.GuildKey; import link.locutus.discord.user.Roles; -import link.locutus.discord.util.RateLimitUtil; import link.locutus.discord.util.StringMan; import link.locutus.discord.util.TimeUtil; import link.locutus.discord.util.discord.DiscordUtil; +import link.locutus.discord.util.task.roles.AutoRoleInfo; import link.locutus.discord.util.task.roles.IAutoRoleTask; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.User; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import org.json.JSONObject; import java.util.ArrayList; import java.util.List; @@ -53,6 +55,7 @@ public String onCommand(Guild guild, IMessageIO channel, User author, DBNation m if (args.size() != 1) { return usage(args.size(), 1, channel); } + boolean force = flags.contains('f'); GuildDB db = Locutus.imp().getGuildDB(guild); if (db == null) return "No registered guild."; @@ -63,64 +66,17 @@ public String onCommand(Guild guild, IMessageIO channel, User author, DBNation m if (args.get(0).equalsIgnoreCase("*")) { if (!Roles.INTERNAL_AFFAIRS.has(author, guild)) return "No permission"; - Function func = i -> { - task.autoRoleAll(new Consumer<>() { - private boolean messaged = false; - - @Override - public void accept(String s) { - if (messaged) return; - messaged = true; - channel.send(s); - } - }); - return true; - }; - channel.send("Please wait..."); - func.apply(0L); - - if (db.hasAlliance()) { - for (Map.Entry entry : db.getMaskedNonMembers().entrySet()) { - response.append(entry.getKey().getAsMention()); - DBNation nation = DiscordUtil.getNation(entry.getKey().getUser()); - if (nation != null) { - String active = TimeUtil.secToTime(TimeUnit.MINUTES, nation.getActive_m()); - if (nation.getActive_m() > 10000) active = "**" + active + "**"; - response.append(nation.getName()).append(" | <").append(nation.getNationUrl()).append("> | ").append(active).append(" | ").append(Rank.byId(nation.getPosition())).append(" in the alliance:").append(nation.getAllianceName()); - } - response.append("- ").append(entry.getValue()); - response.append("\n"); - } - } - - + JSONObject command = CM.role.autoassign.cmd.create().toJson(); + return UtilityCommands.autoroleall(author, db, channel, command, force); } else { DBNation nation = DiscordUtil.parseNation(args.get(0)); - if (nation == null) return "That nation isn't registered: " + CM.register.cmd.toSlashMention() + ""; + if (nation == null) return "That nation isn't registered: `" + args.get(0) + "` see:" + CM.register.cmd.toSlashMention() + ""; User user = nation.getUser(); if (user == null) return "User is not registered."; Member member = db.getGuild().getMember(user); if (member == null) return "Member not found in guild: " + DiscordUtil.getFullUsername(user); - List output = new ArrayList<>(); - Consumer out = output::add; - task.autoRole(member, out); - - if (!output.isEmpty()) { - channel.send(StringMan.join(output, "\n")); - } + JSONObject command = CM.role.autorole.cmd.create(user.getAsMention()).toJson(); + return UtilityCommands.autorole(db, channel, command, member, force); } - - response.append("Done!"); - - if (db.getOrNull(GuildKey.AUTOROLE) == null) { - response.append("\n- AutoRole disabled. To enable it use: ").append(GuildKey.AUTOROLE.getCommandMention()); - } else response.append("\n- AutoRole Mode: ").append(db.getOrNull(GuildKey.AUTOROLE) + ""); - if (db.getOrNull(GuildKey.AUTONICK) == null) { - response.append("\n- AutoNick disabled. To enable it use: " + GuildKey.AUTONICK.getCommandMention() + ""); - } - else response.append("\n- AutoNick Mode: ").append(db.getOrNull(GuildKey.AUTONICK) + ""); - if (Roles.REGISTERED.toRole(db) == null) response.append("\n- Please set a registered role: " + CM.role.setAlias.cmd.create(Roles.REGISTERED.name(), "", null, null).toSlashCommand() + ""); - - return response.toString(); } } \ No newline at end of file diff --git a/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/BankCommands.java b/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/BankCommands.java index 18ccd5d6..4c9ca295 100644 --- a/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/BankCommands.java +++ b/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/BankCommands.java @@ -176,18 +176,9 @@ public String depositResources(@Me User author, @Me DBNation me, @Me Member memb @Arg("Deposit via the api") @Switch("u") boolean useApi, @Switch("f") boolean force) throws IOException, ExecutionException, InterruptedException, GeneralSecurityException { - if (mailResults && !Roles.MAIL.has(author, db.getGuild())) { - throw new IllegalArgumentException("No permission for `mailResults`. " + Roles.MAIL.toDiscordRoleNameElseInstructions(db.getGuild())); - } - if (dm && !Roles.MAIL.hasOnRoot(author)) { - throw new IllegalArgumentException("No permission for `dm`. " + Roles.MAIL.toDiscordRoleNameElseInstructions(db.getGuild())); - } if (customMessage != null && !dm && !mailResults) { throw new IllegalArgumentException("Cannot specify `customMessage` without specifying `dm` or `mailResults`"); } - if (useApi && !Roles.ECON.has(author, db.getGuild())) { - throw new IllegalArgumentException("No permission for `useApi`. " + Roles.ECON.toDiscordRoleNameElseInstructions(db.getGuild())); - } if (amount != null && (rawsDays != null || keepWarchestFactor != null || keepPerCity != null || keepTotal != null || unitResources != null)) { throw new IllegalArgumentException("Cannot specify `amount` to deposit with other deposit modes."); } @@ -200,7 +191,19 @@ public String depositResources(@Me User author, @Me DBNation me, @Me Member memb if (sheetAmounts != null && (rawsDays != null || keepWarchestFactor != null || keepPerCity != null || keepTotal != null || unitResources != null)) { throw new IllegalArgumentException("Cannot specify `sheetAmounts` to deposit with other deposit modes."); } + if (nations.isEmpty()) return "No nations found"; + + boolean isOther = nations.size() > 1 || me.getNation_id() != nations.iterator().next().getNation_id(); + if (mailResults && !Roles.MAIL.has(author, db.getGuild()) && isOther) { + throw new IllegalArgumentException("No permission for `mailResults`. " + Roles.MAIL.toDiscordRoleNameElseInstructions(db.getGuild())); + } + if (dm && !Roles.MAIL.hasOnRoot(author) && isOther) { + throw new IllegalArgumentException("No permission for `dm`. " + Roles.MAIL.toDiscordRoleNameElseInstructions(db.getGuild())); + } + if (useApi && isOther && !Roles.ECON.has(author, db.getGuild())) { + throw new IllegalArgumentException("No permission for `useApi`. " + Roles.ECON.toDiscordRoleNameElseInstructions(db.getGuild())); + } Set allowed = Roles.ECON_STAFF.getAllowedAccounts(author, db); Map statuses = new LinkedHashMap<>(); Map toKeepMap = new LinkedHashMap<>(); diff --git a/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/ExchangeCommands.java b/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/ExchangeCommands.java index b12e40f2..071b72bf 100644 --- a/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/ExchangeCommands.java +++ b/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/ExchangeCommands.java @@ -76,16 +76,6 @@ public String create(@Me Guild guild, @Me User user, @Me DBNation me, @Me IMessa return help; } - @Command(desc = "Autorole members for an exchange") - @RolePermission(guild = StockDB.ROOT_GUILD) - public String autoRole(@Me GuildDB db, @Me Guild guildl, @Me Member member, @Me DBNation me, StockDB stockDB, @Me Exchange exchange) { - exchange.autoRole(member); - exchange.autoRole(); - StringBuilder response = new StringBuilder(); - db.getAutoRoleTask().autoRole(member, f -> response.append(f).append("\n")); - return response.toString().trim(); - } - @Command(desc = "Destroy an exchange") @RolePermission(guild = StockDB.ROOT_GUILD) public String drop(@Me Guild guild, @Me IMessageIO io, @Me DBNation me, @Me JSONObject command, StockDB db, @Me Exchange exchange, @Switch("f") boolean force) { diff --git a/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/FACommands.java b/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/FACommands.java index 03ba965a..0c535253 100644 --- a/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/FACommands.java +++ b/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/FACommands.java @@ -292,12 +292,12 @@ public String embassy(@Me GuildDB db, @Me Guild guild, @Me User user, @Me DBNati Role role = aaRoles.get(nation.getAlliance_id()); if (role == null) { db.addCoalition(nation.getAlliance_id(), Coalition.MASKEDALLIANCES); - GuildDB.AutoRoleOption autoRoleValue = db.getOrNull(GuildKey.AUTOROLE); + GuildDB.AutoRoleOption autoRoleValue = db.getOrNull(GuildKey.AUTOROLE_ALLIANCES); if (autoRoleValue == null || autoRoleValue == GuildDB.AutoRoleOption.FALSE) { - return "AutoRole is disabled. See " + GuildKey.AUTOROLE.getCommandMention() + ""; + return "AutoRole is disabled. See " + GuildKey.AUTOROLE_ALLIANCES.getCommandMention() + ""; } db.getAutoRoleTask().syncDB(); - db.getAutoRoleTask().autoRole(member, f -> {}); + db.getAutoRoleTask().autoRole(member, nation, true); aaRoles = DiscordUtil.getAARoles(guild.getRoles()); role = aaRoles.get(nation.getAlliance_id()); if (role == null) { diff --git a/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/UtilityCommands.java b/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/UtilityCommands.java index 8963055f..fb6b8eda 100644 --- a/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/UtilityCommands.java +++ b/src/main/java/link/locutus/discord/commands/manager/v2/impl/pw/commands/UtilityCommands.java @@ -44,9 +44,9 @@ import link.locutus.discord.util.offshore.test.IAChannel; import link.locutus.discord.util.sheet.SpreadSheet; import link.locutus.discord.util.task.nation.MultiReport; +import link.locutus.discord.util.task.roles.AutoRoleInfo; import link.locutus.discord.util.task.roles.IAutoRoleTask; import link.locutus.discord.apiv1.domains.subdomains.attack.DBAttack; -import link.locutus.discord.apiv1.domains.subdomains.SAllianceContainer; import link.locutus.discord.apiv1.enums.AttackType; import link.locutus.discord.apiv1.enums.MilitaryUnit; import link.locutus.discord.apiv1.enums.Rank; @@ -1187,39 +1187,23 @@ public String ProjectCost(Set projects, @Default("false") boolean techn @Command(desc = "Auto rank users on discord") @RolePermission(Roles.INTERNAL_AFFAIRS) - public String autoroleall(@Me User author, @Me GuildDB db, @Me IMessageIO channel) { + public static String autoroleall(@Me User author, @Me GuildDB db, @Me IMessageIO channel, @Me JSONObject command, @Switch("f") boolean force) { IAutoRoleTask task = db.getAutoRoleTask(); task.syncDB(); - StringBuilder response = new StringBuilder(); - - Function func = new Function() { - @Override - public Boolean apply(Long i) { - task.autoRoleAll(new Consumer() { - private boolean messaged = false; - @Override - public void accept(String s) { - if (messaged) return; - messaged = true; - channel.send(s); - } - }); - return true; - } - }; - if (Roles.ADMIN.hasOnRoot(author) || true) { + AutoRoleInfo result = task.autoRoleAll(false); + if (force) { channel.send("Please wait..."); - func.apply(0L); - } else if (Roles.INTERNAL_AFFAIRS.has(author, db.getGuild())) { - String taskId = getClass().getSimpleName() + "." + db.getGuild().getId(); - Boolean result = TimeUtil.runTurnTask(taskId, func); - if (result != Boolean.TRUE) return "Task already ran this turn"; - } else { - return "No permission"; + result.execute(); + return result.getChangesAndErrorMessage(); } + String body = "`note: Results may differ if settings or users change`\n" + + result.getSyncDbResult() + "\n------\n" + result.toString(); + IMessageBuilder msg = channel.create().confirmation("Auto role all", body, command); + if (db.hasAlliance()) { + StringBuilder response = new StringBuilder(); for (Map.Entry entry : db.getMaskedNonMembers().entrySet()) { User user = entry.getKey().getUser(); response.append("`" + DiscordUtil.getFullUsername(user) + "`" + "`<@" + user.getIdLong() + ">`"); @@ -1232,42 +1216,32 @@ public void accept(String s) { response.append("- ").append(entry.getValue()); response.append("\n"); } + if (response.length() > 0) { + msg.append(response.toString()); + } } - if (db.getOrNull(GuildKey.AUTOROLE) == null) { - response.append("\n- AutoRole disabled. To enable it use: " + GuildKey.AUTOROLE.getCommandMention() + ""); - } - else response.append("\n- AutoRole Mode: ").append(db.getOrNull(GuildKey.AUTOROLE) + ""); - if (db.getOrNull(GuildKey.AUTONICK) == null) { - response.append("\n- AutoNick disabled. To enable it use: " + GuildKey.AUTONICK.getCommandMention() + ""); - } - else response.append("\n- AutoNick Mode: ").append(db.getOrNull(GuildKey.AUTONICK) + ""); - if (Roles.REGISTERED.toRole(db) == null) response.append("\n- Please set a registered role: " + CM.role.setAlias.cmd.create(Roles.REGISTERED.name(), "", null, null).toSlashCommand() + ""); - return response.toString(); + msg.send(); + return null; } @Command(desc = "Auto rank users on discord") - public String autorole(@Me GuildDB db, @Me IMessageIO channel, Member member) { + public static String autorole(@Me GuildDB db, @Me IMessageIO channel, @Me JSONObject command, Member member, @Switch("f") boolean force) { IAutoRoleTask task = db.getAutoRoleTask(); task.syncDB(); DBNation nation = DiscordUtil.getNation(member.getUser()); - Consumer out = channel::send; - if (nation == null) out.accept("That nation isn't registered: " + CM.register.cmd.toSlashMention() + ""); - task.autoRole(member, out); - - StringBuilder response = new StringBuilder("Done!"); - - if (db.getOrNull(GuildKey.AUTOROLE) == null) { - response.append("\n- AutoRole disabled. To enable it use: " + GuildKey.AUTOROLE.getCommandMention() + ""); - } - else response.append("\n- AutoRole Mode: ").append((Object) db.getOrNull(GuildKey.AUTOROLE)); - if (db.getOrNull(GuildKey.AUTONICK) == null) { - response.append("\n- AutoNick disabled. To enable it use: " + GuildKey.AUTONICK.getCommandMention() + ""); + if (nation == null) return "That nation isn't registered: " + CM.register.cmd.toSlashMention(); + AutoRoleInfo result = task.autoRole(member, nation, force); + if (force) { + result.execute(); + return result.getChangesAndErrorMessage(); } - else response.append("\n- AutoNick Mode: ").append((Object) db.getOrNull(GuildKey.AUTONICK)); - if (Roles.REGISTERED.toRole(db) == null) response.append("\n- Please set a registered role: " + CM.role.setAlias.cmd.create(Roles.REGISTERED.name(), "", null, null).toSlashCommand() + ""); - return response.toString(); + + String body = "`note: Results may differ if settings or users change`\n" + + result.getSyncDbResult() + "\n------\n" + result.toString(); + channel.create().confirmation("Auto role " + nation.getNation(), body, command).send(); + return null; } @RolePermission(value = {Roles.MILCOM, Roles.INTERNAL_AFFAIRS,Roles.ECON,Roles.FOREIGN_AFFAIRS}, any=true) diff --git a/src/main/java/link/locutus/discord/db/GuildDB.java b/src/main/java/link/locutus/discord/db/GuildDB.java index 1b414be5..8aeb929d 100644 --- a/src/main/java/link/locutus/discord/db/GuildDB.java +++ b/src/main/java/link/locutus/discord/db/GuildDB.java @@ -985,6 +985,12 @@ public void createTables() { } catch (SQLException e) { e.printStackTrace(); } + { + // Rename AUTOROLE to AUTOROLE_ALLIANCES in `key` + String updateKey = "UPDATE INFO SET key = 'AUTOROLE_ALLIANCES' WHERE key = 'AUTOROLE'"; + executeStmt(updateKey); + } + }; { String create = "CREATE TABLE IF NOT EXISTS `LOANS` (`loan_id` INTEGER PRIMARY KEY AUTOINCREMENT, `server` BIGINT NOT NULL, `message`, `receiver` INT NOT NULL, `resources` BLOB NOT NULL, `due` BIGINT NOT NULL, `repaid` BIGINT NOT NULL)"; @@ -2526,8 +2532,8 @@ public String toString() { public enum AutoRoleOption { FALSE("No roles given"), - ALL("Roles for the alliance"), - ALLIES("Roles for allies (e.g. if a coalition server)"), + ALL("Alliance roles created for all (see: `AUTOROLE_TOP_X`)"), + ALLIES("Alliance roles created for allies (see: `allies` coalition)"), ; private final String description; diff --git a/src/main/java/link/locutus/discord/db/GuildHandler.java b/src/main/java/link/locutus/discord/db/GuildHandler.java index f2232c11..f13292c7 100644 --- a/src/main/java/link/locutus/discord/db/GuildHandler.java +++ b/src/main/java/link/locutus/discord/db/GuildHandler.java @@ -414,17 +414,14 @@ public void onNationChangeColor(NationChangeColorEvent event) { public void onCityCreate(CityCreateEvent event) { DBNation nation = event.getNation(); if (nation != null) { - // Auto role User user = nation.getUser(); if (user != null) { Member member = guild.getMember(user); if (member != null) { - db.getAutoRoleTask().autoRoleCities(member, () -> nation, ignore1 -> {}, ignore2 -> {}); + db.getAutoRoleTask().autoRoleCities(member, nation); } } - - } } diff --git a/src/main/java/link/locutus/discord/db/entities/DBNation.java b/src/main/java/link/locutus/discord/db/entities/DBNation.java index 5b0f0f3f..dc8c1244 100644 --- a/src/main/java/link/locutus/discord/db/entities/DBNation.java +++ b/src/main/java/link/locutus/discord/db/entities/DBNation.java @@ -41,6 +41,7 @@ import link.locutus.discord.util.sheet.SpreadSheet; import link.locutus.discord.util.task.MailTask; import link.locutus.discord.util.task.multi.GetUid; +import link.locutus.discord.util.task.roles.AutoRoleInfo; import link.locutus.discord.util.trade.TradeManager; import link.locutus.discord.web.jooby.handler.CommandResult; import com.google.gson.JsonObject; @@ -371,9 +372,8 @@ public String register(User user, GuildDB db, boolean isNewRegistration) { if (member != null) { RateLimitUtil.complete(db.getGuild().addRoleToMember(user, role)); output.append("You have been assigned the role: " + role.getName()); - db.getAutoRoleTask().autoRole(member, s -> { - output.append("\n").append(s); - }); + AutoRoleInfo task = db.getAutoRoleTask().autoRole(member, this, true); + output.append("\n" + task.getChangesAndErrorMessage()); } else { member = db.getGuild().retrieveMember(user).complete(); output.append("Member " + DiscordUtil.getFullUsername(user) + " not found in guild: " + db.getGuild()); diff --git a/src/main/java/link/locutus/discord/db/guild/GuildKey.java b/src/main/java/link/locutus/discord/db/guild/GuildKey.java index c0e50297..3aa5ef34 100644 --- a/src/main/java/link/locutus/discord/db/guild/GuildKey.java +++ b/src/main/java/link/locutus/discord/db/guild/GuildKey.java @@ -999,12 +999,12 @@ public String help() { "See also: " + CM.role.clearNicks.cmd.toSlashMention(); } }; - public static GuildSetting AUTOROLE = new GuildEnumSetting(GuildSettingCategory.ROLE, GuildDB.AutoRoleOption.class) { + public static GuildSetting AUTOROLE_ALLIANCES = new GuildEnumSetting(GuildSettingCategory.ROLE, GuildDB.AutoRoleOption.class) { @NoFormat @Command(descMethod = "help") @RolePermission(Roles.ADMIN) public String AUTOROLE(@Me GuildDB db, @Me User user, GuildDB.AutoRoleOption mode) { - return AUTOROLE.setAndValidate(db, user, mode); + return AUTOROLE_ALLIANCES.setAndValidate(db, user, mode); } @Override @@ -1028,7 +1028,7 @@ public String AUTOROLE_ALLIANCE_RANK(@Me GuildDB db, @Me User user, Rank allianc public String help() { return "The ingame rank required to get an alliance role. (default: member) Options: " + StringMan.getString(Rank.values()); } - }.setupRequirements(f -> f.requires(AUTOROLE)); + }.setupRequirements(f -> f.requires(AUTOROLE_ALLIANCES)); public static GuildSetting AUTOROLE_TOP_X = new GuildIntegerSetting(GuildSettingCategory.ROLE) { @NoFormat @Command(descMethod = "help") @@ -1040,7 +1040,7 @@ public String AUTOROLE_TOP_X(@Me GuildDB db, @Me User user, Integer topScoreRank public String help() { return "The number of top alliances to provide roles for, defaults to `0`"; } - }.setupRequirements(f -> f.requires(AUTOROLE)); + }.setupRequirements(f -> f.requires(AUTOROLE_ALLIANCES)); public static GuildSetting DO_NOT_RAID_TOP_X = new GuildIntegerSetting(GuildSettingCategory.FOREIGN_AFFAIRS) { @NoFormat @Command(descMethod = "help") @@ -1067,7 +1067,7 @@ public String AUTOROLE_ALLY_GOV(@Me GuildDB db, @Me User user, boolean enabled) public String help() { return "Whether to give gov/member roles to allies (this is intended for coalition servers), `true` or `false`"; } - }.setupRequirements(f -> f.requires(AUTOROLE).requiresCoalition(Coalition.ALLIES).requiresNot(ALLIANCE_ID)); + }.setupRequirements(f -> f.requires(AUTOROLE_ALLIANCES).requiresCoalition(Coalition.ALLIES).requiresNot(ALLIANCE_ID)); public static GuildSetting> AUTOROLE_ALLY_ROLES = new GuildEnumSetSetting(GuildSettingCategory.ROLE, Roles.class) { @NoFormat @Command(descMethod = "help") diff --git a/src/main/java/link/locutus/discord/util/task/roles/AutoRoleInfo.java b/src/main/java/link/locutus/discord/util/task/roles/AutoRoleInfo.java new file mode 100644 index 00000000..072dfb2b --- /dev/null +++ b/src/main/java/link/locutus/discord/util/task/roles/AutoRoleInfo.java @@ -0,0 +1,400 @@ +package link.locutus.discord.util.task.roles; + +import link.locutus.discord.db.GuildDB; +import link.locutus.discord.util.RateLimitUtil; +import link.locutus.discord.util.math.CIEDE2000; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.exceptions.PermissionException; +import net.dv8tion.jda.api.requests.restaction.RoleAction; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class AutoRoleInfo { + + private final Map createMap; + private final Map> addRoles; + private final Map> removeRoles; + private final Map nickSet; + private final GuildDB db; + private final Map> errors; + private final Map> success; + private final String syncDbResult; + + public AutoRoleInfo(GuildDB db, String syncDbResult) { + this.db = db; + this.syncDbResult = syncDbResult; + this.createMap = new LinkedHashMap<>(); + this.addRoles = new LinkedHashMap<>(); + this.removeRoles = new LinkedHashMap<>(); + this.nickSet = new LinkedHashMap<>(); + this.errors = new LinkedHashMap<>(); + this.success = new LinkedHashMap<>(); + } + + public String getSyncDbResult() { + return syncDbResult; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + if (!createMap.isEmpty()) { + result.append("Create Roles:\n"); + for (RoleOrCreate create : createMap.values()) { + result.append("- " + create.name).append("\n"); + } + } + if (!addRoles.isEmpty()) { + result.append("Add Roles:\n"); + for (Map.Entry> entry : addRoles.entrySet()) { + String roleNames = entry.getValue().stream().map(roleAdd -> roleAdd.role.name).collect(Collectors.joining(", ")); + result.append("- ").append(entry.getKey().getEffectiveName()).append(" -> ").append(roleNames).append("\n"); + } + } + if (!removeRoles.isEmpty()) { + result.append("Remove Roles:\n"); + for (Map.Entry> entry : removeRoles.entrySet()) { + String roleNames = entry.getValue().stream().map(Role::getName).collect(Collectors.joining(", ")); + result.append("- ").append(entry.getKey().getEffectiveName()).append(" -> ").append(roleNames).append("\n"); + } + } + if (!nickSet.isEmpty()) { + result.append("Set Nicknames:\n"); + for (Map.Entry entry : nickSet.entrySet()) { + result.append("- ").append(entry.getKey().getEffectiveName()).append(" -> ").append(entry.getValue()).append("\n"); + } + } + return result.toString(); + } + + public String getChangesAndErrorMessage() { + StringBuilder response = new StringBuilder(); + if (!errors.isEmpty()) { + response.append("Errors:\n"); + for (Map.Entry> entry : errors.entrySet()) { + response.append("- ").append(entry.getKey().getEffectiveName()).append(" -> ").append(String.join(", ", entry.getValue())).append("\n"); + } + } + if (!success.isEmpty()) { + response.append("Success:\n"); + for (Map.Entry> entry : success.entrySet()) { + response.append("- ").append(entry.getKey().getEffectiveName()).append(" -> ").append(String.join(", ", entry.getValue())).append("\n"); + } + } + return success.toString(); + } + + public RoleOrCreate createRole(Role role, String roleName, int position, Supplier color) { + RoleOrCreate create; + if (role != null) { + create = new RoleOrCreate(role, roleName, position, color); + } else { + create = createMap.computeIfAbsent(roleName, k -> new RoleOrCreate(role, roleName, position, color)); + } + return create; + } + + public Map> getErrors() { + return errors; + } + + public Map> getSuccess() { + return success; + } + + /** + * Execute the changes + * @return a list of error messages grouped by member + */ + public void execute() { + errors.clear(); + success.clear(); + + for (Map.Entry> entry : addRoles.entrySet()) { + for (RoleAdd roleAdd : entry.getValue()) { + roleAdd.submit(db.getGuild()); + } + } + + List tasks = new ArrayList<>(); + // removeRoles + for (Map.Entry> entry : removeRoles.entrySet()) { + Member member = entry.getKey(); + for (Role role : entry.getValue()) { + if (!member.getRoles().contains(role)) { + errors.computeIfAbsent(member, k -> new ArrayList<>()).add("Failed to remove role `" + role.getName() + "`: Member does not have role"); + continue; + } + try { + tasks.add(RateLimitUtil.queue(db.getGuild().removeRoleFromMember(member, role)).thenAccept(v -> { + success.computeIfAbsent(member, k -> new ArrayList<>()).add("Removed role `" + role.getName() + "` from " + member.getEffectiveName()); + })); + } catch (PermissionException e) { + errors.computeIfAbsent(member, k -> new ArrayList<>()).add("Failed to remove role `" + role.getName() + "`: " + e.getMessage()); + } + } + } + + for (Map.Entry entry : nickSet.entrySet()) { + Member member = entry.getKey(); + String nick = entry.getValue(); + if (nick == null) { + // remove nick + if (member.getNickname() == null) { + errors.computeIfAbsent(member, k -> new ArrayList<>()).add("Failed to remove nickname: Member does not have nickname"); + continue; + } + try { + tasks.add(RateLimitUtil.queue(db.getGuild().modifyNickname(member, null)).thenAccept(v -> { + success.computeIfAbsent(member, k -> new ArrayList<>()).add("Removed nickname from " + member.getEffectiveName()); + })); + } catch (PermissionException e) { + errors.computeIfAbsent(member, k -> new ArrayList<>()).add("Failed to remove nickname: " + e.getMessage()); + } + } else { + // set nick + if (member.getNickname() != null && member.getNickname().equals(nick)) { + errors.computeIfAbsent(member, k -> new ArrayList<>()).add("Failed to set nickname: Member already has nickname"); + continue; + } + try { + tasks.add(RateLimitUtil.queue(db.getGuild().modifyNickname(member, nick)).thenAccept(v -> { + success.computeIfAbsent(member, k -> new ArrayList<>()).add("Set nickname of " + member.getEffectiveName() + " to `" + nick + "`"); + })); + } catch (PermissionException e) { + errors.computeIfAbsent(member, k -> new ArrayList<>()).add("Failed to set nickname: " + e.getMessage()); + } + } + } + + // addRoles + for (Map.Entry> entry : addRoles.entrySet()) { + Member member = entry.getKey(); + for (RoleAdd roleAdd : entry.getValue()) { + if (!roleAdd.get()) { + errors.computeIfAbsent(member, k -> new ArrayList<>()).add(roleAdd.failedMessage); + } else { + success.computeIfAbsent(member, k -> new ArrayList<>()).add("Added role `" + roleAdd.role.name + "` to " + member.getEffectiveName()); + } + } + } + + // poll tasks + for (CompletableFuture task : tasks) { + try { + task.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + } + + public void logError(Member member, String error) { + errors.computeIfAbsent(member, k -> new ArrayList<>()).add(error); + } + + public void addRoleToMember(Member member, RoleOrCreate create) { + RoleAdd roleAdd = new RoleAdd(member, create); + addRoles.computeIfAbsent(member, k -> new HashSet<>()).add(roleAdd); + } + + public void addRoleToMember(Member member, Role role) { + RoleOrCreate create = createRole(role, role.getName(), -1, () -> role.getColor()); + addRoleToMember(member, create); + } + + public void removeRoleFromMember(Member member, Role role) { + removeRoles.computeIfAbsent(member, k -> new HashSet<>()).add(role); + } + + public void modifyNickname(Member member, String name) { + nickSet.put(member, name); + } + + public static class RoleAdd { + private final Member member; + private final RoleOrCreate role; + + private boolean success; + private String failedMessage; + private CompletableFuture future; + + public RoleAdd(Member member, RoleOrCreate role) { + this.member = member; + this.role = role; + } + + public CompletableFuture submit(Guild guild) { + if (future != null) return future; + this.future = this.role.submit(guild).thenApply(new Function() { + @Override + public Boolean apply(Role role) { + if (role == null) { + success = false; + failedMessage = "Failed to create role `" + RoleAdd.this.role.name + "`: " + RoleAdd.this.role.failedCreateMessage; + return false; + } + if (member.getRoles().contains(role)) { + success = false; + failedMessage = "Member already has role `" + role.getName() + "`"; + return false; + } + RateLimitUtil.queue(guild.addRoleToMember(member, role)).thenAccept(new Consumer() { + @Override + public void accept(Void aVoid) { + success = true; + } + }).exceptionally(new Function() { + @Override + public Void apply(Throwable throwable) { + success = false; + failedMessage = "Failed to add role `" + role.getName() + "`: " + throwable.getMessage(); + return null; + } + }).join(); + return success; + } + }); + return future; + } + + public boolean get() { + if (future != null) { + try { + future.join(); + } catch (RuntimeException e) { + failedMessage = e.getMessage(); + } + future = null; + } + return success; + } + } + + public static class RoleOrCreate { + private Role role; + private final String name; + private final Supplier hasColor; + private final int position; + private boolean fetched = false; + + private CompletableFuture future; + + private String failedCreateMessage = null; + + public RoleOrCreate(Role roleOrNull, String name, int position, Supplier hasColor) { + this.role = roleOrNull; + this.name = name; + this.hasColor = hasColor; + this.position = position; + } + + public CompletableFuture submit(Guild guild) { + if (future != null) return future; + if (role == null && !fetched) { + Color color = hasColor.get(); + RoleAction create = guild.createRole().setName(name); + create = create.setMentionable(false).setHoisted(true); + if (color != null) { + create = create.setColor(color); + } + try { + fetched = true; + future = RateLimitUtil.queue(create).thenApply(r -> { + this.role = r; + if (position >= 0) { + RateLimitUtil.queue(guild.modifyRolePositions().selectPosition(role).moveTo(position)); + } + return r; + }).exceptionally(e -> { + this.failedCreateMessage = "Failed to create role `" + name + "`: " + e.getMessage(); + return role; + }); + } catch (PermissionException e) { + failedCreateMessage = "Failed to create role `" + name + "`: " + e.getMessage(); + future = CompletableFuture.failedFuture(e); + } + } + if (future != null) { + return future; + } else { + return CompletableFuture.completedFuture(role); + } + } + + public Role get() { + if (role == null && future != null) { + try { + role = future.join(); + } catch (RuntimeException e) { + failedCreateMessage = "Failed to create role `" + name + "`: " + e.getMessage(); + } + future = null; + } + return role; + } + } + + private static Color BG = Color.decode("#36393E"); + + private Set existingColors = new HashSet<>(); + + public Supplier supplyColor(int allianceId, Collection allianceRoles) { + return new Supplier() { + private Color color; + @Override + public Color get() { + if (color != null) return color; + + if (existingColors == null) { + existingColors = new HashSet<>(); + allianceRoles.forEach(r -> { + if (r.getColor() != null) existingColors.add(r.getColor()); + }); + } + + Random random = new Random(allianceId); + double maxDiff = 0; + for (int i = 0; i < 100; i++) { + int nextInt = random.nextInt(0xffffff + 1); + String colorCode = String.format("#%06x", nextInt); + Color nextColor = Color.decode(colorCode); + + if (CIEDE2000.calculateDeltaE(BG, nextColor) < 12) continue; + + double minDiff = Double.MAX_VALUE; + for (Color otherColor : existingColors) { + if (otherColor != null) { + minDiff = Math.min(minDiff, CIEDE2000.calculateDeltaE(nextColor, otherColor)); + } + } + if (minDiff > maxDiff) { + maxDiff = minDiff; + color = nextColor; + } + if (minDiff > 12) break; + } + if (color != null) { + existingColors.add(color); + } + return color; + } + }; + } +} diff --git a/src/main/java/link/locutus/discord/util/task/roles/AutoRoleTask.java b/src/main/java/link/locutus/discord/util/task/roles/AutoRoleTask.java index 750c8c29..0d2e7a2e 100644 --- a/src/main/java/link/locutus/discord/util/task/roles/AutoRoleTask.java +++ b/src/main/java/link/locutus/discord/util/task/roles/AutoRoleTask.java @@ -12,6 +12,7 @@ import link.locutus.discord.pnw.PNWUser; import link.locutus.discord.user.Roles; import link.locutus.discord.util.MathMan; +import link.locutus.discord.util.PnwUtil; import link.locutus.discord.util.RateLimitUtil; import link.locutus.discord.util.discord.DiscordUtil; import link.locutus.discord.util.math.ArrayUtil; @@ -31,6 +32,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; @@ -38,6 +40,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -46,9 +49,7 @@ public class AutoRoleTask implements IAutoRoleTask { private final Guild guild; private int position; - private Map allianceRoles; - private Role registeredRole; private final GuildDB db; private GuildDB.AutoNickOption setNickname; @@ -69,6 +70,14 @@ public AutoRoleTask(Guild guild, GuildDB db) { syncDB(); } + public boolean isMember(Member member, DBNation nation) { + Set aaIds = GuildKey.ALLIANCE_ID.getOrNull(db); + if (aaIds == null || aaIds.isEmpty()) { + return db.getAllies(false).contains(nation.getAlliance_id()) && nation.getPositionEnum().id >= Rank.MEMBER.id; + } + return (nation != null && aaIds.contains(nation.getAlliance_id()) && nation.getPositionEnum().id >= Rank.MEMBER.id); + } + public void setAllianceMask(GuildDB.AutoRoleOption value) { this.setAllianceMask = value == null ? GuildDB.AutoRoleOption.FALSE : value; } @@ -77,32 +86,26 @@ public void setNickname(GuildDB.AutoNickOption value) { this.setNickname = value == null ? GuildDB.AutoNickOption.FALSE : value; } - public synchronized void syncDB() { + public synchronized String syncDB() { + Map info = new LinkedHashMap<>(); + GuildDB.AutoNickOption nickOpt = db.getOrNull(GuildKey.AUTONICK); if (nickOpt != null) { setNickname(nickOpt); } - GuildDB.AutoRoleOption roleOpt = db.getOrNull(GuildKey.AUTOROLE); + GuildDB.AutoRoleOption roleOpt = db.getOrNull(GuildKey.AUTOROLE_ALLIANCES); if (roleOpt != null) { - try { - setAllianceMask(roleOpt); - } catch (IllegalArgumentException e) {} + setAllianceMask(roleOpt); } + initRegisteredRole = false; - List roles = guild.getRoles(); + List roles = db.getGuild().getRoles(); this.allianceRoles = new ConcurrentHashMap<>(DiscordUtil.getAARoles(roles)); this.cityRoleMap = new ConcurrentHashMap<>(DiscordUtil.getCityRoles(roles)); this.cityRoles = new HashSet<>(); for (Set value : cityRoleMap.values()) cityRoles.addAll(value); - if (!cityRoles.isEmpty()) { - System.out.println("City roles: " + guild.getIdLong()); - for (Role cityRole : cityRoles) { - System.out.println("- " + cityRole.getName()); - } - } - fetchTaxRoles(true); this.autoRoleAllyGov = Boolean.TRUE == db.getOrNull(GuildKey.AUTOROLE_ALLY_GOV); @@ -137,10 +140,57 @@ public synchronized void syncDB() { Function previousAllowed = allowedAAs; allowedAAs = f -> previousAllowed.apply(f) || masked.contains(f); } + registeredRole = Roles.REGISTERED.toRole(guild); + + info.put(GuildKey.AUTONICK.name(), setNickname + ""); + info.put(GuildKey.AUTOROLE_ALLIANCES.name(), setAllianceMask + ""); + info.put(GuildKey.AUTOROLE_ALLIANCE_RANK.name(), autoRoleRank == null ? "Member" : autoRoleRank.name()); + info.put(GuildKey.AUTOROLE_TOP_X.name(), allowedAAs == null ? "All" : "Top " + topX); + if (!masked.isEmpty()) { + info.put("Masked Alliances", masked.stream().map(f -> PnwUtil.getName(f, true)).collect(Collectors.joining("\n"))); + } + info.put(GuildKey.AUTOROLE_ALLY_GOV.name() + " (for coalition servers)", autoRoleAllyGov + ""); + if (allianceRoles.isEmpty()) { + info.put("Found Alliance Roles", "None (Roles are generated based on settings)"); + } else { + // join by markdown list + List aaNamesList = allianceRoles.entrySet().stream().map(f -> f.getKey() + " -> " + f.getValue().getName()).toList(); + String listStr = "- " + String.join("\n- ", aaNamesList); + info.put("Found Alliance Roles", listStr); + } + info.put(Roles.REGISTERED.name(), registeredRole == null ? "None" : registeredRole.getName()); + if (cityRoles.isEmpty()) { + info.put("Found City Roles", "None (Make one e.g. `c10-20` or `c21+`)"); + } else { + // join by markdown list + List cityNamesList = cityRoles.stream().map(Role::getName).toList(); + info.put("Found City Roles", String.join("\n", cityNamesList)); + } + if (taxRoles.isEmpty()) { + info.put("Found Tax Roles", "None (Make one e.g. `25/25`)"); + } else { + // join by markdown list + List taxNamesList = taxRoles.entrySet().stream().map(f -> f.getKey().getKey() + "/" + f.getKey().getValue() + " -> " + f.getValue().getName()).toList(); + info.put("Found Tax Roles", String.join("\n", taxNamesList)); + } + + StringBuilder result = new StringBuilder(); + for (Map.Entry entry : info.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (value.contains("\n")) { + result.append(key).append(":\n").append(value).append("\n"); + } else { + result.append(key).append(": ").append(value).append("\n"); + } + } + result.append("\n"); + result.append("\nSetting Info: " + CM.settings.info.cmd.toSlashMention()); + result.append("\nRole Info: " + CM.role.setAlias.cmd.toSlashMention()); + return result.toString(); } - public void autoRoleAllies(Consumer output, Set members) { - if (output == null) output = f -> {}; + public void autoRoleAllies(AutoRoleInfo info, Set members) { if (!members.isEmpty()) { DiscordDB discordDb = Locutus.imp().getDiscordDB(); @@ -220,21 +270,21 @@ public void autoRoleAllies(Consumer output, Set members) { if (memberRole != null) { memberRoles.computeIfAbsent(member, f -> new ArrayList<>()).add(memberRole); if (!roles.contains(memberRole)) { - tasks.add(RateLimitUtil.queue(guild.addRoleToMember(member, memberRole))); + info.addRoleToMember(member, memberRole); } } for (int i = 0; i < allyDiscordRoles.length; i++) { Role allyRole = allyDiscordRoles[i]; Role thisRole = thisDiscordRoles[i]; if (allyRole == null || thisRole == null) { - if (thisRole != null) output.accept("Role not registered " + thisRole.getName()); +// if (thisRole != null) output.accept("Role not registered " + thisRole.getName()); continue; } if (allyRoles.contains(allyRole)) { memberRoles.computeIfAbsent(member, f -> new ArrayList<>()).add(thisRole); if (!roles.contains(thisRole)) { - tasks.add(RateLimitUtil.queue(guild.addRoleToMember(member, thisRole))); + info.addRoleToMember(member, thisRole); } } } @@ -253,20 +303,20 @@ public void autoRoleAllies(Consumer output, Set members) { if (allies.contains(nation.getAlliance_id()) && nation.getPosition() > 1) { isMember = true; if (!roles.contains(memberRole)) { - tasks.add(RateLimitUtil.queue(guild.addRoleToMember(member, memberRole))); + info.addRoleToMember(member, memberRole); } } } } if (!isMember && roles.contains(memberRole)) { - tasks.add(RateLimitUtil.queue(guild.removeRoleFromMember(member, memberRole))); + info.removeRoleFromMember(member, memberRole); } List allowed = memberRoles.getOrDefault(member, new ArrayList<>()); for (Role role : thisDiscordRoles) { if (roles.contains(role) && !allowed.contains(role)) { - tasks.add(RateLimitUtil.queue(guild.removeRoleFromMember(member, role))); + info.removeRoleFromMember(member, role); } } } @@ -284,20 +334,24 @@ public void autoRoleAllies(Consumer output, Set members) { } @Override - public synchronized void autoRoleAll(Consumer output) { - syncDB(); - if (setNickname == GuildDB.AutoNickOption.FALSE && setAllianceMask == GuildDB.AutoRoleOption.FALSE) return; - - ArrayDeque tasks = new ArrayDeque<>(); + public synchronized AutoRoleInfo autoRoleAll(boolean confirm) { + String syncDbResult = syncDB(); + AutoRoleInfo info = new AutoRoleInfo(db, syncDbResult); List members = guild.getMembers(); Map existantAllianceRoles = new HashMap<>(allianceRoles); + Set memberAllianceIds = new HashSet<>(); + int requiredRank = autoRoleRank == null ? Rank.MEMBER.id : autoRoleRank.id; for (int i = 0; i < members.size(); i++) { Member member = members.get(i); + DBNation nation = DiscordUtil.getNation(member.getIdLong()); + if (nation != null && nation.getPositionEnum().id >= requiredRank) { + memberAllianceIds.add(nation.getAlliance_id()); + } try { - autoRole(member, true, output, f -> tasks.add(f)); + autoRole(info, member, nation, true); } catch (Throwable e) { e.printStackTrace(); throw e; @@ -305,53 +359,32 @@ public synchronized void autoRoleAll(Consumer output) { } if (autoRoleAllyGov) { HashSet memberSet = new HashSet<>(members); - autoRoleAllies(output, memberSet); - } - while (!tasks.isEmpty()) { - try { - tasks.poll().get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } catch (Throwable e) { - e.printStackTrace(); - throw e; - } + autoRoleAllies(info, memberSet); } for (Map.Entry entry : existantAllianceRoles.entrySet()) { Role role = entry.getValue(); List withRole = guild.getMembersWithRoles(role); - if (withRole.isEmpty()) { + if (!memberAllianceIds.contains(entry.getKey()) && withRole.isEmpty()) { allianceRoles.remove(entry.getKey()); - tasks.add(RateLimitUtil.queue(role.delete())); - } - } - while (!tasks.isEmpty()) { - try { - tasks.poll().get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); + RateLimitUtil.queue(role.delete()); } } + return info; } private boolean initRegisteredRole = false; private Map nationAACache = new HashMap<>(); @Override - public synchronized void autoRole(Member member, Consumer output) { - autoRole(member, false, output, f -> {}); + public synchronized AutoRoleInfo autoRole(Member member, DBNation nation, boolean confirm) { + AutoRoleInfo info = new AutoRoleInfo(db, ""); + autoRole(info, member, nation, false); + return info; } - public synchronized void autoNick(boolean autoAll, Member member, Consumer output, Consumer tasks, Supplier pnwUserSup, Supplier nationSup) { - PNWUser pnwUser = pnwUserSup.get(); - if (pnwUser == null) { - output.accept(member.getEffectiveName() + " has the registered role, but no DB entry has been found."); - return; - } - DBNation nation = nationSup.get(); + public synchronized void autoNick(AutoRoleInfo info, boolean autoAll, Member member, DBNation nation) { if (nation == null) { - output.accept(member.getEffectiveName() + " is registered, but no nation entry has been found"); return; } String leaderOrNation; @@ -384,76 +417,54 @@ public synchronized void autoNick(boolean autoAll, Member member, Consumer output, Consumer tasks) { - if (setNickname == GuildDB.AutoNickOption.FALSE && setAllianceMask == GuildDB.AutoRoleOption.FALSE) { - return; - } - if (allianceRoles.isEmpty()) syncDB(); - { - initRegisteredRole = true; - this.registeredRole = Roles.REGISTERED.toRole(guild); - } + public synchronized void autoRole(AutoRoleInfo info, Member member, DBNation nation, boolean autoAll) { + initRegisteredRole = true; + this.registeredRole = Roles.REGISTERED.toRole(guild); try { User user = member.getUser(); List roles = member.getRoles(); - Supplier pnwUserSup = ArrayUtil.memorize(() -> Locutus.imp().getDiscordDB().getUser(user)); - Supplier nationSup = ArrayUtil.memorize(() -> { - PNWUser pnwUser = pnwUserSup.get(); - if (pnwUser == null) return null; - return Locutus.imp().getNationDB().getNation(pnwUser.getNationId()); - }); - - boolean isRegistered = registeredRole != null && roles.contains(registeredRole); - - if (!isRegistered && registeredRole != null) { - if (nationSup.get() != null) { - RateLimitUtil.complete(guild.addRoleToMember(user, registeredRole)); - isRegistered = true; + + boolean hasRegisteredRole = registeredRole != null && roles.contains(registeredRole); + + if (!hasRegisteredRole && registeredRole != null) { + if (nation != null) { + info.addRoleToMember(member, registeredRole); } } - if (!isRegistered && !autoAll) { + if (nation != null) { if (registeredRole == null) { - output.accept("No registered role exists. Please create one on discord, then use " + CM.role.setAlias.cmd.create(Roles.REGISTERED.name(), null, null, null) + ""); + info.logError(member, "No registered role exists. Please create one on discord, then use " + CM.role.setAlias.cmd.create(Roles.REGISTERED.name(), null, null, null) + ""); } else { - output.accept(member.getEffectiveName() + " is NOT registered"); + info.logError(member, "Not registered. See: " + CM.register.cmd.toSlashMention()); } } if (!autoAll && this.autoRoleAllyGov) { - autoRoleAllies(output, Collections.singleton(member)); + autoRoleAllies(info, Collections.singleton(member)); } if (setAllianceMask != null && setAllianceMask != GuildDB.AutoRoleOption.FALSE) { - autoRoleAlliance(member, isRegistered, pnwUserSup, nationSup, autoAll, output, tasks); + autoRoleAlliance(info, member, nation, autoAll); } - if (isRegistered) { - autoRoleCities(member, nationSup, output, tasks); + if (nation != null) { + autoRoleCities(info, member, nation); } - if (setNickname != null && setNickname != GuildDB.AutoNickOption.FALSE && member.getNickname() == null && isRegistered) { - autoNick(autoAll, member, output, tasks, pnwUserSup, nationSup); - PNWUser pnwUser = pnwUserSup.get(); + if (setNickname != null && setNickname != GuildDB.AutoNickOption.FALSE && member.getNickname() == null && nation != null) { + autoNick(info, autoAll, member, nation); } else if (!autoAll && (setNickname == null || setNickname == GuildDB.AutoNickOption.FALSE)) { - output.accept("Auto nickname is disabled"); + info.logError(member, "Auto nickname is disabled"); } } catch (Throwable e) { e.printStackTrace(); - output.accept("Failed for " + member.getEffectiveName() + ": " + e.getClass().getSimpleName() + " | " + e.getMessage()); -// e.printStackTrace(); + info.logError(member, "Failed: " + e.getClass().getSimpleName() + " | " + e.getMessage()); } } @@ -475,16 +486,16 @@ private Map, Role> fetchTaxRoles(boolean update) { return tmp; } - public void updateTaxRoles(Map brackets) { + public void updateTaxRoles(AutoRoleInfo info, Map brackets) { fetchTaxRoles(true); for (Member member : guild.getMembers()) { DBNation nation = DiscordUtil.getNation(member.getIdLong()); TaxBracket bracket = nation != null ? brackets.get(nation) : null; - updateTaxRole(member, bracket); + updateTaxRole(info, member, bracket); } } - public void updateTaxRole(Member member, TaxBracket bracket) { + public void updateTaxRole(AutoRoleInfo info, Member member, TaxBracket bracket) { Map, Role> tmpTaxRoles = fetchTaxRoles(false); Role expectedRole = null; if (bracket != null) { @@ -497,168 +508,128 @@ public void updateTaxRole(Member member, TaxBracket bracket) { for (Map.Entry, Role> entry : tmpTaxRoles.entrySet()) { Role taxRole = entry.getValue(); if (!taxRole.equals(expectedRole) && roles.contains(taxRole)) { - RateLimitUtil.queue(guild.removeRoleFromMember(member, taxRole)); + info.removeRoleFromMember(member, taxRole); } } if (expectedRole != null && !roles.contains(expectedRole)) { - RateLimitUtil.queue(guild.addRoleToMember(member, expectedRole)); + info.addRoleToMember(member, expectedRole); } } - public void autoRoleAlliance(Member member, boolean isRegistered, Supplier pnwUserSup, Supplier nationSup, boolean autoAll, Consumer output, Consumer tasks) { - if (isRegistered) { - PNWUser pnwUser = pnwUserSup.get(); - if (pnwUser != null) { - DBNation nation = nationSup.get(); - if (nation != null) { - if (!allianceRoles.isEmpty() && position == -1) { - position = allianceRoles.values().iterator().next().getPosition(); - } + public void autoRoleAlliance(AutoRoleInfo info, Member member, DBNation nation, boolean autoAll) { + if (nation != null) { + if (!allianceRoles.isEmpty() && position == -1) { + position = allianceRoles.values().iterator().next().getPosition(); + } - int alliance_id = nation.getAlliance_id(); - if (nation.getPosition() < autoRoleRank.id || !allowedAAs.apply(alliance_id)) { - alliance_id = 0; - } + int alliance_id = nation.getAlliance_id(); + if (nation.getPosition() < autoRoleRank.id || !allowedAAs.apply(alliance_id)) { + alliance_id = 0; + } - Integer currentAARole = nationAACache.get(nation.getNation_id()); - if (currentAARole != null && currentAARole.equals(alliance_id) && autoAll) { - return; - } else { - nationAACache.put(nation.getNation_id(), alliance_id); - } + Integer currentAARole = nationAACache.get(nation.getNation_id()); + if (currentAARole != null && currentAARole.equals(alliance_id) && autoAll) { + return; + } else { + nationAACache.put(nation.getNation_id(), alliance_id); + } - Role role = allianceRoles.get(alliance_id); - if (role == null && alliance_id != 0) { - role = createRole(position, guild, alliance_id, nation.getAllianceName()); - if (role != null) { - allianceRoles.put(alliance_id, role); - } - } + Map myRoles = DiscordUtil.getAARoles(member.getRoles()); - Map myRoles = DiscordUtil.getAARoles(member.getRoles()); + if (alliance_id == 0) { + for (Map.Entry entry : myRoles.entrySet()) { + info.removeRoleFromMember(member, entry.getValue()); + } + return; + } - if (myRoles.size() == 1 && role != null && myRoles.get(alliance_id) == role) { - return; - } - if (alliance_id == 0) { - for (Map.Entry entry : myRoles.entrySet()) { - tasks.accept(RateLimitUtil.queue(guild.removeRoleFromMember(member, entry.getValue()))); - } - return; - } - for (Map.Entry entry : myRoles.entrySet()) { - if (entry.getValue().getIdLong() != role.getIdLong()) { - if (!autoAll) - output.accept("Remove " + entry.getValue().getName() + " to " + member.getEffectiveName()); - tasks.accept(RateLimitUtil.queue(guild.removeRoleFromMember(member, entry.getValue()))); - } - } - if (!myRoles.containsKey(alliance_id)) { - if (!autoAll) - output.accept("Add " + role.getName() + " to " + member.getEffectiveName()); - tasks.accept(RateLimitUtil.queue(guild.addRoleToMember(member, role))); + Role role = allianceRoles.get(alliance_id); + + for (Map.Entry entry : myRoles.entrySet()) { + if (entry.getKey() != alliance_id) { + info.removeRoleFromMember(member, entry.getValue()); + } + } + if (!myRoles.containsKey(alliance_id)) { + if (role == null) { + String roleName = "AA " + alliance_id + " " + nation.getAllianceName(); + List roles = guild.getRolesByName(roleName, false); + if (roles.size() == 1) role = roles.get(0); + if (role == null) { + AutoRoleInfo.RoleOrCreate roleAdd = info.createRole(null, roleName, position, info.supplyColor(alliance_id, allianceRoles.values())); + info.addRoleToMember(member, roleAdd); } + } + if (role != null) { + info.addRoleToMember(member, role); } else { - if (!autoAll) output.accept("No nation found for " + pnwUser.getNationId()); + // (position, guild, alliance_id, nation.getAllianceName()) + + } - } else isRegistered = false; - } - if (!isRegistered && !autoAll) { - PNWUser pnwUser = pnwUserSup.get(); - if (pnwUser == null) { - Map memberAARoles = DiscordUtil.getAARoles(member.getRoles()); - if (!memberAARoles.isEmpty()) { - for (Map.Entry entry : memberAARoles.entrySet()) { - output.accept("Remove " + entry.getValue().getName() + " from " + member.getEffectiveName()); - tasks.accept(RateLimitUtil.queue(guild.removeRoleFromMember(member, entry.getValue()))); - } + } + } + if (nation == null) { + Map memberAARoles = DiscordUtil.getAARoles(member.getRoles()); + if (!memberAARoles.isEmpty()) { + for (Map.Entry entry : memberAARoles.entrySet()) { + info.removeRoleFromMember(member, entry.getValue()); } } } } - public void autoRoleCities(Member member, Supplier nationSup, Consumer output, Consumer tasks) { + @Override + public AutoRoleInfo autoRoleCities(Member member, DBNation nation) { + AutoRoleInfo info = new AutoRoleInfo(db, ""); + autoRoleCities(info, member, nation); + info.execute(); + return info; + } + + @Override + public AutoRoleInfo updateTaxRoles(Map brackets) { + AutoRoleInfo info = new AutoRoleInfo(db, ""); + updateTaxRoles(info, brackets); + info.execute(); + return info; + } + + @Override + public AutoRoleInfo updateTaxRole(Member member, TaxBracket bracket) { + AutoRoleInfo info = new AutoRoleInfo(db, ""); + updateTaxRole(info, member, bracket); + info.execute(); + return info; + } + + public void autoRoleCities(AutoRoleInfo info, Member member, DBNation nation) { + if (nation == null) { + return; + } if (cityRoles.isEmpty()) return; - Role memberRole = Roles.MEMBER.toRole(member.getGuild()); - Set allianceIds = db.getAllianceIds(); - if (!allianceIds.isEmpty() || memberRole != null) { - DBNation nation = nationSup.get(); - if (nation == null) { - return; - } - Set allowed; - if (((!allianceIds.contains(nation.getAlliance_id()) || nation.getPosition() <= 1)) || - (allianceIds.isEmpty() && (memberRole == null || !member.getRoles().contains(memberRole)))) { - allowed = new HashSet<>(); - } else { - allowed = new HashSet<>(cityRoleMap.getOrDefault(nation.getCities(), new HashSet<>())); - } - List roles = new ArrayList<>(member.getRoles()); - for (Role role : roles) { + if (isMember(member, nation)) { + Set allowed = new HashSet<>(cityRoleMap.getOrDefault(nation.getCities(), new HashSet<>())); + for (Role role : cityRoles) { if (allowed.contains(role)) { allowed.remove(role); continue; } Map.Entry cityRole = DiscordUtil.getCityRange(role.getName()); if (cityRole == null) continue; - - output.accept("Remove " + role.getName() + " from " + member.getEffectiveName()); - tasks.accept(RateLimitUtil.queue(guild.removeRoleFromMember(member, role))); + info.removeRoleFromMember(member, role); } for (Role role : allowed) { - output.accept("Add " + role.getName() + " to " + member.getEffectiveName()); - tasks.accept(RateLimitUtil.queue(guild.addRoleToMember(member, role))); + info.addRoleToMember(member, role); } - } - } - - public Role createRole(int position, Guild guild, int allianceId, String allianceName) { - Random random = new Random(allianceId); - Color color = null; - double maxDiff = 0; - for (int i = 0; i < 100; i++) { - int nextInt = random.nextInt(0xffffff + 1); - String colorCode = String.format("#%06x", nextInt); - Color nextColor = Color.decode(colorCode); - - if (CIEDE2000.calculateDeltaE(BG, nextColor) < 12) continue; - - double minDiff = Double.MAX_VALUE; - for (Role role : allianceRoles.values()) { - Color otherColor = role.getColor(); - if (otherColor != null) { - minDiff = Math.min(minDiff, CIEDE2000.calculateDeltaE(nextColor, otherColor)); + } else { + List memberRoles = member.getRoles(); + for (Role role : cityRoles) { + if (memberRoles.contains(role)) { + info.removeRoleFromMember(member, role); } } - if (minDiff > maxDiff) { - maxDiff = minDiff; - color = nextColor; - } - if (minDiff > 12) break; - } - - String roleName = "AA " + allianceId + " " + allianceName; - Role role = RateLimitUtil.complete(guild.createRole() - .setName(roleName) - .setColor(color) - .setMentionable(false) - .setHoisted(true) - ); - - if (role == null) { - List roles = guild.getRolesByName(roleName, false); - if (roles.size() == 1) role = roles.get(0); - else { - throw new IllegalStateException("Could not create role: " + roleName); - } } - - if (position != -1) { - RateLimitUtil.complete(guild.modifyRolePositions().selectPosition(role).moveTo(position)); - } - return role; } - - private static Color BG = Color.decode("#36393E"); } diff --git a/src/main/java/link/locutus/discord/util/task/roles/DelegateAutoRoleTask.java b/src/main/java/link/locutus/discord/util/task/roles/DelegateAutoRoleTask.java index 0141de6d..57ae6bd2 100644 --- a/src/main/java/link/locutus/discord/util/task/roles/DelegateAutoRoleTask.java +++ b/src/main/java/link/locutus/discord/util/task/roles/DelegateAutoRoleTask.java @@ -10,38 +10,38 @@ import java.util.function.Supplier; public class DelegateAutoRoleTask implements IAutoRoleTask { - private final IAutoRoleTask task; - @Override - public void autoRoleCities(Member member, Supplier nationSup, Consumer output, Consumer tasks) { - task.autoRoleCities(member, nationSup, output, tasks); + public AutoRoleInfo autoRoleCities(Member member, DBNation nation) { + return task.autoRoleCities(member, nation); } @Override - public void updateTaxRoles(Map brackets) { - task.updateTaxRoles(brackets); + public AutoRoleInfo updateTaxRoles(Map brackets) { + return task.updateTaxRoles(brackets); } @Override - public void updateTaxRole(Member member, TaxBracket bracket) { - task.updateTaxRole(member, bracket); + public AutoRoleInfo updateTaxRole(Member member, TaxBracket bracket) { + return task.updateTaxRole(member, bracket); } @Override - public void autoRoleAll(Consumer output) { - task.autoRoleAll(output); + public AutoRoleInfo autoRoleAll(boolean confirm) { + return task.autoRoleAll(confirm); } @Override - public void autoRole(Member member, Consumer output) { - task.autoRole(member, output); + public AutoRoleInfo autoRole(Member member, DBNation nation, boolean confirm) { + return task.autoRole(member, nation, confirm); } @Override - public void syncDB() { - task.syncDB(); + public String syncDB() { + return task.syncDB(); } + private final IAutoRoleTask task; + public DelegateAutoRoleTask(IAutoRoleTask task) { this.task = task; } diff --git a/src/main/java/link/locutus/discord/util/task/roles/IAutoRoleTask.java b/src/main/java/link/locutus/discord/util/task/roles/IAutoRoleTask.java index 8cab1297..d1e61f0a 100644 --- a/src/main/java/link/locutus/discord/util/task/roles/IAutoRoleTask.java +++ b/src/main/java/link/locutus/discord/util/task/roles/IAutoRoleTask.java @@ -10,15 +10,15 @@ import java.util.function.Supplier; public interface IAutoRoleTask { - void autoRoleCities(Member member, Supplier nationSup, Consumer output, Consumer tasks); + AutoRoleInfo autoRoleCities(Member member, DBNation nation); - void updateTaxRoles(Map brackets); + AutoRoleInfo updateTaxRoles(Map brackets); - void updateTaxRole(Member member, TaxBracket bracket); + AutoRoleInfo updateTaxRole(Member member, TaxBracket bracket); - void autoRoleAll(Consumer output); + AutoRoleInfo autoRoleAll(boolean confirm); - void autoRole(Member member, Consumer output); + AutoRoleInfo autoRole(Member member, DBNation nation, boolean confirm); - void syncDB(); + String syncDB(); } diff --git a/src/main/java/link/locutus/discord/util/update/NationUpdateProcessor.java b/src/main/java/link/locutus/discord/util/update/NationUpdateProcessor.java index 638a6221..7803317c 100644 --- a/src/main/java/link/locutus/discord/util/update/NationUpdateProcessor.java +++ b/src/main/java/link/locutus/discord/util/update/NationUpdateProcessor.java @@ -685,7 +685,7 @@ public void onNationChangeName(NationChangeNameEvent event) { if (db.getOrNull(GuildKey.AUTONICK) == GuildDB.AutoNickOption.NATION) { try { - db.getAutoRoleTask().autoRole(member, System.out::println); + db.getAutoRoleTask().autoRole(member, event.getCurrent(), true); } catch (Throwable e) { e.printStackTrace(); } @@ -706,7 +706,7 @@ public void onNationChangeLeader(NationChangeLeaderEvent event) { if (db.getOrNull(GuildKey.AUTONICK) == GuildDB.AutoNickOption.LEADER) { try { - db.getAutoRoleTask().autoRole(member, System.out::println); + db.getAutoRoleTask().autoRole(member, event.getCurrent(), true); } catch (Throwable e) { e.printStackTrace(); }