diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 8fecc03..abf34d0 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,11 +1,6 @@ # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - name: Java CI with Maven on: @@ -16,20 +11,22 @@ on: jobs: build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up JDK 8 - uses: actions/setup-java@v3 - with: - java-version: '8' - distribution: 'temurin' - cache: maven - - name: Build with Maven - run: mvn -B package --file pom.xml - - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up JDK 18 + uses: actions/setup-java@v3 + with: + java-version: '18' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + - name: Upload built package + uses: actions/upload-artifact@v3 + with: + name: discord-transfer-jar + path: target/*.jar + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@v3 diff --git a/README.md b/README.md index cf909b4..163da17 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ _A discord bot for copying messages between guilds_ [![build](https://github.com/BilliAlpha/discord-transfer/actions/workflows/maven.yml/badge.svg)](https://github.com/BilliAlpha/discord-transfer/actions/workflows/maven.yml) -**Current version: [v2.2.2](https://github.com/BilliAlpha/discord-transfer/releases/latest)** +**Current version: [v2.3.0](https://github.com/BilliAlpha/discord-transfer/releases/latest)** ## How to use ? ## diff --git a/pom.xml b/pom.xml index 344064f..5632b7c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,11 +6,12 @@ com.billialpha.discord discord-transfer - 2.2.2 + 2.3.0 - 1.8 - 1.8 + UTF-8 + 17 + 17 @@ -18,14 +19,16 @@ org.apache.maven.plugins maven-compiler-plugin + 3.11.0 - 8 - 8 + 17 + 17 org.apache.maven.plugins maven-jar-plugin + 3.3.0 @@ -37,6 +40,7 @@ org.apache.maven.plugins maven-shade-plugin + 3.3.0 package @@ -53,7 +57,7 @@ ch.qos.logback logback-classic - 1.4.5 + 1.4.7 diff --git a/src/main/java/com/billialpha/discord/transfer/DiscordTransfer.java b/src/main/java/com/billialpha/discord/transfer/DiscordTransfer.java index 956f47d..19e6d56 100644 --- a/src/main/java/com/billialpha/discord/transfer/DiscordTransfer.java +++ b/src/main/java/com/billialpha/discord/transfer/DiscordTransfer.java @@ -1,9 +1,10 @@ package com.billialpha.discord.transfer; +import ch.qos.logback.classic.Level; import discord4j.common.util.Snowflake; import discord4j.core.DiscordClient; import discord4j.core.GatewayDiscordClient; -import discord4j.core.event.domain.message.MessageCreateEvent; +import discord4j.core.object.Embed; import discord4j.core.object.entity.Attachment; import discord4j.core.object.entity.Guild; import discord4j.core.object.entity.Message; @@ -13,11 +14,10 @@ import discord4j.core.object.entity.channel.VoiceChannel; import discord4j.core.object.reaction.Reaction; import discord4j.core.object.reaction.ReactionEmoji; -import discord4j.core.spec.EmbedCreateSpec; -import discord4j.core.spec.MessageCreateSpec; -import discord4j.core.spec.TextChannelCreateSpec; -import discord4j.core.spec.VoiceChannelCreateSpec; +import discord4j.core.spec.*; import discord4j.discordjson.possible.Possible; +import discord4j.gateway.intent.Intent; +import discord4j.gateway.intent.IntentSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -35,10 +35,7 @@ import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeParseException; -import java.util.HashSet; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -48,49 +45,80 @@ * @author BilliAlpha */ public class DiscordTransfer { - public static final String VERSION = "2.2.2"; + public static final String VERSION = "2.3.0"; private static final Logger LOGGER = LoggerFactory.getLogger(DiscordTransfer.class); - private final GatewayDiscordClient client; - private final Guild srcGuild; - private final Guild destGuild; + private GatewayDiscordClient client; + private Guild srcGuild; + private Guild destGuild; private final Set skipChannels; private Set categories; private Instant afterDate; private int delay; private Thread thread; + private Runnable action; private final Scheduler scheduler; + private int verbosity = 0; + private boolean reUploadFiles = true; - public DiscordTransfer(String token, long srcGuild, long destGuild) { - DiscordClient discord = DiscordClient.create(token); - LOGGER.info("Logging in ..."); - client = Objects.requireNonNull(discord.login().block(), "Invalid bot token"); - User self = Objects.requireNonNull(client.getSelf().block()); - LOGGER.info("Logged in, user: "+self.getUsername()+"#"+self.getDiscriminator()); - client.on(MessageCreateEvent.class) - .filter(evt -> evt.getMessage().getContent().equals("migrate stop")) - .subscribe(e -> { - LOGGER.info("Logging out ..."); - stop(); - }); - Snowflake sourceGuild = Snowflake.of(srcGuild); - this.srcGuild = Objects.requireNonNull(client.getGuildById(sourceGuild).block(), "Invalid id for source guild"); - LOGGER.info("Loaded source guild: "+this.srcGuild.getName()+" ("+srcGuild+")"); - Snowflake destinationGuild = Snowflake.of(destGuild); - this.destGuild = Objects.requireNonNull(client.getGuildById(destinationGuild).block(), "Invalid id for dest guild"); - LOGGER.info("Loaded dest guild: "+this.destGuild.getName()+" ("+destGuild+")"); + public DiscordTransfer() { this.categories = null; this.skipChannels = new HashSet<>(0); this.scheduler = Schedulers.parallel(); } + public void init(String token) { + if (client != null) { + throw new IllegalStateException("Client already initialized"); + } + + DiscordClient discord = DiscordClient.create(token); + + LOGGER.debug("Logging in ..."); + client = Objects.requireNonNull(discord.gateway() + .setEnabledIntents(IntentSet.of(Intent.GUILD_MESSAGES)) + .login().block(), "Invalid bot token"); + User self = Objects.requireNonNull(client.getSelf().block()); + LOGGER.info("Logged in, user: "+self.getUsername()+"#"+self.getDiscriminator()); + } + public void start() { - if (thread != null) throw new IllegalStateException("Migration already started"); - thread = new Thread(this::migrate); + if (client == null) throw new IllegalStateException("Client not initialized"); + if (thread != null) throw new IllegalStateException("Action already started"); + if (action == null) throw new IllegalStateException("No action to perform"); + thread = new Thread(() -> { + try { + this.action.run(); + } catch (Throwable ex) { + System.err.println("ERROR: "+ex.getMessage()); + if (verbosity > 0) { + ex.printStackTrace(); + } else { + System.out.println("Run with verbose flag to get stacktrace."); + } + } + }); thread.start(); } + public void run() { + start(); + try { + thread.join(); + } catch (InterruptedException ex) { + return; + } + stop(); + } + public void stop() { - if (thread != null) thread.interrupt(); + if (thread != null) { + thread.interrupt(); + try { + thread.join(); + } catch (InterruptedException ex) { + return; + } + } client.logout().block(); } @@ -150,28 +178,25 @@ public Flux cleanMigratedEmotes() { public void migrate() { LOGGER.info("Starting migration ..."); - try { - Long migrated = getSelectedCategories() - .parallel() - .runOn(scheduler) - .flatMap(srcCat -> - destGuild.getChannels().ofType(Category.class) - .filter(c -> c.getName().equals(srcCat.getName())) - .singleOrEmpty() - .switchIfEmpty( - destGuild.createCategory(srcCat.getName())) - .flatMapMany(dstCat -> migrateCategory(srcCat, dstCat)) - ) - .reduce(Long::sum) - .block(); - LOGGER.info("Migration finished successfully ("+migrated+" messages)"); - client.logout().block(); - LOGGER.info("Logged out"); - } catch (Throwable t) { - LOGGER.error("Error .. Shutting down !"); - t.printStackTrace(); - client.logout().block(); - } + Optional migrated = getSelectedCategories() + .parallel() + .runOn(scheduler) + .flatMap(srcCat -> + destGuild.getChannels().ofType(Category.class) + .filter(c -> c.getName().equals(srcCat.getName())) + .singleOrEmpty() + .switchIfEmpty( + destGuild.createCategory(srcCat.getName())) + .flatMapMany(dstCat -> migrateCategory(srcCat, dstCat)) + ) + .reduce(Long::sum) + .blockOptional(); + if (migrated.isPresent()) + LOGGER.info("Migration finished successfully ("+migrated.get()+" messages)"); + else + LOGGER.info("Migration finished without migrating any message"); + client.logout().block(); + LOGGER.debug("Logged out"); } private Mono migrateCategory(@NonNull Category srcCat, @NonNull Category dstCat) { @@ -253,32 +278,9 @@ private Mono migrateMessage(@NonNull Message msg, @NonNull TextChannel LOGGER.info("Migrating message ("+dstChan.getName()+"/#"+msg.getId().asString()+"): "+ author.getUsername()+" at "+msg.getTimestamp()); MessageCreateSpec.Builder m = MessageCreateSpec.builder(); - Attachment image = null; - for (Attachment att : msg.getAttachments()) { - if (image == null && att.getWidth().isPresent()) { - image = att; - continue; - } - try { - HttpURLConnection conn = (HttpURLConnection) new URL(att.getUrl()).openConnection(); - conn.setRequestProperty("User-Agent", "DiscordTransfer (v"+VERSION+")"); - if (conn.getResponseCode()/100 != 2 && conn.getContentLength() > 0) { - InputStream stream = conn.getErrorStream(); - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int nRead; - byte[] data = new byte[1024]; - while ((nRead = stream.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, nRead); - } - stream.close(); - buffer.flush(); - LOGGER.warn("Attachment HTTP error:\n"+new String(buffer.toByteArray(), StandardCharsets.UTF_8)); - } else m.addFile(att.getFilename(), conn.getInputStream()); - } catch (IOException e) { - LOGGER.warn("Unable to forward attachment", e); - } - } - LOGGER.debug("Raw message:\n\t"+msg.getContent().replaceAll("\n", "\n\t")); + + // Add message info to embed + LOGGER.debug("Raw message:\n\t" + msg.getContent().replaceAll("\n", "\n\t")); String message = msg.getContent().replaceAll("<@&\\d+>", ""); // Remove role mentions EmbedCreateSpec.Builder embed = EmbedCreateSpec.builder() .author( @@ -287,8 +289,67 @@ private Mono migrateMessage(@NonNull Message msg, @NonNull TextChannel author.getAvatarUrl()) .timestamp(msg.getEditedTimestamp().orElse(msg.getTimestamp())) .description(message); - if (image != null) embed.image(image.getUrl()); - m.addEmbed(embed.build()); + + if (reUploadFiles) { + // Download and re-upload files + m.addEmbed(embed.build()); // Send message embed now because we won't need it later + for (Attachment att : msg.getAttachments()) { + try { + HttpURLConnection conn = (HttpURLConnection) new URL(att.getUrl()).openConnection(); + conn.setRequestProperty("User-Agent", "DiscordTransfer (v"+VERSION+")"); + if (conn.getResponseCode()/100 != 2 && conn.getContentLength() > 0) { + // Decode error message + InputStream stream = conn.getErrorStream(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[1024]; + while ((nRead = stream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + stream.close(); + buffer.flush(); + LOGGER.warn("Attachment HTTP error:\n\t"+ + new String(buffer.toByteArray(), StandardCharsets.UTF_8) + .replaceAll("\n", "\n\t")); + } else { + // Upload downloaded data + m.addFile(att.getFilename(), conn.getInputStream()); + } + } catch (IOException e) { + LOGGER.warn("Unable to forward attachment", e); + } + } + } else { + // Just link to the original files + boolean firstImage = true; + List otherEmbeds = new ArrayList<>(); + for (Attachment att : msg.getAttachments()) { + if (att.getWidth().isPresent()) { + // This is an image + if (firstImage) { + // Include first image in embed + embed.image(att.getUrl()); + firstImage = false; + } else { + // Create new embeds with following images + otherEmbeds.add(EmbedCreateSpec.builder().image(att.getUrl()).build()); + } + } else { + // This is a file + otherEmbeds.add(EmbedCreateSpec.builder().title(att.getFilename()).url(att.getUrl()).build()); + } + } + + m.addEmbed(embed.build()); + m.addAllEmbeds(otherEmbeds); + } + + // Clone embeds from source message + for (Embed sourceEmbed : msg.getEmbeds()) { + m.addEmbed(cloneEmbed(sourceEmbed)); + } + + // Perform creation dstChan.createMessage(m.build()) /* .onErrorResume(err -> { @@ -306,9 +367,39 @@ private Mono migrateMessage(@NonNull Message msg, @NonNull TextChannel }) .subscribe(), fut::completeExceptionally); + + // Return future return Mono.fromFuture(fut); } + public EmbedCreateSpec cloneEmbed(Embed sourceEmbed) { + EmbedCreateSpec.Builder newEmbed = EmbedCreateSpec.builder(); + sourceEmbed.getAuthor().ifPresent(embedAuthor -> newEmbed.author( + embedAuthor.getName().orElseThrow(() -> new IllegalStateException("Embed author has no name")), + embedAuthor.getUrl().orElse(null), + embedAuthor.getIconUrl().orElse(null))); + sourceEmbed.getTitle().ifPresent(newEmbed::title); + sourceEmbed.getUrl().ifPresent(newEmbed::url); + sourceEmbed.getColor().ifPresent(newEmbed::color); + sourceEmbed.getThumbnail().ifPresent(embedThumbnail -> newEmbed.thumbnail(embedThumbnail.getUrl())); + sourceEmbed.getDescription().ifPresent(newEmbed::description); + sourceEmbed.getImage().ifPresent(embedImage -> newEmbed.image(embedImage.getUrl())); + sourceEmbed.getFields() + .stream() + .map(f -> EmbedCreateFields.Field.of(f.getName(), f.getValue(), f.isInline())) + .forEach(newEmbed::addField); + sourceEmbed.getTimestamp().ifPresent(newEmbed::timestamp); + sourceEmbed.getFooter().ifPresent(embedFooter -> newEmbed.footer( + embedFooter.getText(), + embedFooter.getIconUrl().orElse(null))); + return newEmbed.build(); + + } + + // ====== Internal classes + + public static class HelpRequestedException extends RuntimeException {} + public static class TextChannelMigrationResult { public final TextChannel sourceChan; public final TextChannel destChan; @@ -333,135 +424,258 @@ public long getMessageCount() { } } - // ================================================================================================================= + // ====== Command line interface - public static void main(String[] args) { - if (args.length == 1 && ("help".equals(args[0]) || "?".equals(args[0]) || "--help".equals(args[0]))) { - System.out.println("discord-transfer"); - System.out.print("by BilliAlpha (billi.pamege.300@gmail.com) "); - System.out.println("-- https://github.com/BilliAlpha/discord-transfer"); - System.out.println(); - System.out.println("A discord bot for copying messages between guilds"); - System.out.println(); - System.out.println("Usage: [options...]"); - System.out.println(" Arguments:"); - System.out.println(" : The action to perform"); - System.out.println(" help - Show this message and exit"); - System.out.println(" migrate - Migrate messages from source guild to dest guild"); - System.out.println(" clean - Delete migration reactions"); - System.out.println(" : The Discord ID of the source guild"); - System.out.println(" : The Discord ID of the destination guild"); - System.out.println(); - System.out.println(" Options:"); - System.out.println(" --category , -c "); - System.out.println(" Limit the migration to specific categories, this option expects a Discord ID."); - System.out.println(" You can use this option multiple times to migrate multiple categories."); - System.out.println(" By default, if this option is not present all categories are migrated."); - System.out.println(); - System.out.println(" --skip , -s "); - System.out.println(" Do not migrate a channel, this option expects a Discord ID."); - System.out.println(" You can use this option multiple times to skip multiple channels."); - System.out.println(); - System.out.println(" --after , -a "); - System.out.println(" Only migrate messages sent after the given date."); - System.out.println(" Date is expected to follow ISO-8601 format (ex: `1997−07−16T19:20:30,451Z`)"); - System.out.println(); - System.out.println(" --delay , -d "); - System.out.println(" Add a delay (in milliseconds) between each message migration."); - System.out.println(" By default there is no delay."); - System.out.println(); - System.out.println(" Environment variables:"); - System.out.println(" DISCORD_TOKEN: Required, the Discord bot token"); - return; + private void parseMigrateArguments(String... args) { + if (args.length != 2) { + throw new IllegalArgumentException("Wrong arguments (expected: srcGuild, destGuild)"); } - if (args.length < 3) { - System.err.println("Missing arguments (action, srcGuild, destGuild)"); - System.out.println("See 'help' action for help"); - return; + long srcGuild; + long destGuild; + try { + srcGuild = Long.parseLong(args[0]); + destGuild = Long.parseLong(args[1]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid guild IDs"); } - String token = System.getenv("DISCORD_TOKEN"); - if (token == null || token.isEmpty()) { - System.err.println("Missing DISCORD_TOKEN"); - System.out.println("See 'help' action for help"); - return; + Snowflake sourceGuild = Snowflake.of(srcGuild); + this.srcGuild = client.getGuildById(sourceGuild) + .onErrorResume((err) -> Mono.empty()) + .blockOptional() + .orElseThrow(() -> new IllegalArgumentException("Invalid id for source guild: "+srcGuild)); + LOGGER.info("Loaded source guild: "+this.srcGuild.getName()+" ("+srcGuild+")"); + + Snowflake destinationGuild = Snowflake.of(destGuild); + this.destGuild = client.getGuildById(destinationGuild) + .onErrorResume((err) -> Mono.empty()) + .blockOptional() + .orElseThrow(() -> new IllegalArgumentException("Invalid id for dest guild: "+destGuild)); + LOGGER.info("Loaded dest guild: "+this.destGuild.getName()+" ("+destGuild+")"); + + this.action = this::migrate; + } + + private void parseCleanArguments(String... args) { + if (args.length != 1) { + throw new IllegalArgumentException("Wrong arguments (expected: srcGuild)"); } long srcGuild; - long destGuild; try { - srcGuild = Long.parseLong(args[1]); - destGuild = Long.parseLong(args[2]); + srcGuild = Long.parseLong(args[0]); } catch (NumberFormatException e) { - System.err.println("Invalid guild IDs"); - System.out.println("See 'help' action for help"); - return; + throw new IllegalArgumentException("Invalid guild IDs"); } - DiscordTransfer app = new DiscordTransfer(token, srcGuild, destGuild); - - if (args.length > 3) { - for (int i = 3; i < args.length; i++) { - String arg = args[i]; - if ("-c".equals(arg) || "--category".equals(arg)) { - try { - app.addCategory(Snowflake.of(Long.parseLong(args[++i]))); - } catch (ArrayIndexOutOfBoundsException ex) { - System.err.println("Missing category ID"); - return; - } catch (NumberFormatException ex) { - System.err.println("Invalid category ID: "+args[i]); - return; - } - } else if ("-s".equals(arg) || "--skip".equals(arg)) { - try { - app.skipChannel(Snowflake.of(Long.parseLong(args[++i]))); - } catch (ArrayIndexOutOfBoundsException ex) { - System.err.println("Missing skip channel ID"); - return; - } catch (NumberFormatException ex) { - System.err.println("Invalid skip channel ID: "+args[i]); - return; - } - } else if ("-a".equals(arg) || "--after".equals(arg)) { - try { - app.afterDate(Instant.parse(args[++i])); - } catch (ArrayIndexOutOfBoundsException ex) { - System.err.println("Missing DATE value"); - return; - } catch (DateTimeParseException ex) { - System.err.println("Invalid DATE format: "+args[i]); - return; - } - } else if ("-d".equals(arg) || "--delay".equals(arg)) { - try { - app.widthDelay(Integer.parseUnsignedInt(args[++i])); - } catch (ArrayIndexOutOfBoundsException ex) { - System.err.println("Missing DELAY value"); - return; - } catch (NumberFormatException ex) { - System.err.println("Invalid DELAY: "+args[i]); - return; + Snowflake sourceGuild = Snowflake.of(srcGuild); + this.srcGuild = client.getGuildById(sourceGuild) + .onErrorResume((err) -> Mono.empty()) + .blockOptional() + .orElseThrow(() -> new IllegalArgumentException("Invalid id for source guild: "+srcGuild)); + LOGGER.info("Loaded source guild: "+this.srcGuild.getName()+" ("+srcGuild+")"); + + this.action = () -> cleanMigratedEmotes().blockLast(); + } + + public void parseArguments(String action, String... args) { + if (action.equals("migrate")) this.parseMigrateArguments(args); + else if (action.equals("clean")) this.parseCleanArguments(args); + else throw new IllegalArgumentException("Unknown action: " + action); + } + + private void parseOption(String opt, Iterator params) { + LOGGER.debug("Got option: "+opt); + if ("h".equals(opt) || "?".equals(opt) || "--help".equals(opt)) { + throw new HelpRequestedException(); + } + + if ("c".equals(opt) || "--category".equals(opt)) { + String value; + try { + value = params.next(); + } catch (NoSuchElementException ex) { + throw new IllegalArgumentException("Missing category ID"); + } + try { + this.addCategory(Snowflake.of(Long.parseLong(value))); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Invalid category ID: "+value, ex); + } + } else if ("s".equals(opt) || "--skip".equals(opt)) { + String value; + try { + value = params.next(); + } catch (NoSuchElementException ex) { + throw new IllegalArgumentException("Missing skip channel ID"); + } + try { + this.skipChannel(Snowflake.of(Long.parseLong(value))); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Invalid skip channel ID: "+value, ex); + } + } else if ("a".equals(opt) || "--after".equals(opt)) { + String value; + try { + value = params.next(); + } catch (NoSuchElementException ex) { + throw new IllegalArgumentException("Missing DATE value for option "+opt); + } + try { + this.afterDate(Instant.parse(value)); + } catch (DateTimeParseException ex) { + throw new IllegalArgumentException("Invalid DATE format: "+value, ex); + } + } else if ("d".equals(opt) || "--delay".equals(opt)) { + String value; + try { + value = params.next(); + } catch (NoSuchElementException ex) { + throw new IllegalArgumentException("Missing DELAY value"); + } + try { + this.widthDelay(Integer.parseUnsignedInt(value)); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Invalid DELAY: "+value, ex); + } + } else if ("--no-reupload".equals(opt)) { + reUploadFiles = false; + } else if ("v".equals(opt) || "--verbose".equals(opt)) { + if (this.verbosity == 0) { + ((ch.qos.logback.classic.Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).setLevel(Level.DEBUG); + LOGGER.debug("Debug logging enabled!"); + } + this.verbosity++; + } else if ("q".equals(opt) || "--quiet".equals(opt)) { + ((ch.qos.logback.classic.Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).setLevel(Level.ERROR); + } else { + throw new IllegalArgumentException("Unsupported option: "+opt); + } + } + + public String handleCliCall(String... rawArgs) { + List positionalArgs = new ArrayList<>(); + Iterator iterArgs = Arrays.asList(rawArgs).iterator(); + try { + // If we have nothing to do + if (rawArgs.length == 0) { + throw new HelpRequestedException(); + } + + // Parse options + while (iterArgs.hasNext()) { + String arg = iterArgs.next(); + if (arg.startsWith("--")) { + parseOption(arg, iterArgs); + } else if (arg.startsWith("-")) { + String[] shortArgs = arg.substring(1).split(""); + for (int i = 0; i < shortArgs.length; i++) { + boolean last = (i + 1 == shortArgs.length); + parseOption(shortArgs[i], last ? iterArgs : Collections.emptyIterator()); } } else { - System.err.println("Unsupported option: "+arg); - System.out.println("See 'help' action for help"); - return; + positionalArgs.add(arg); } } + + // Get selected action + if (positionalArgs.size() == 0) { + throw new IllegalArgumentException("Missing action"); + } + String action = positionalArgs.remove(0); + if ("help".equals(action) || "?".equals(action)) { + throw new HelpRequestedException(); + } + + // Get token from env and init client + String token = System.getenv("DISCORD_TOKEN"); + if (token == null || token.isEmpty()) { + throw new IllegalStateException("Missing DISCORD_TOKEN"); + } + this.init(token); + + // Parse action args + this.parseArguments(action, positionalArgs.toArray(new String[0])); + return action; + } catch (HelpRequestedException ignored) { + DiscordTransfer.printHelp(); + return "help"; } + } - LOGGER.info("Start action '"+args[0]+"'"); - if (args[0].equals("migrate")) { - app.start(); - app.await(); - } else if (args[0].equals("clean")) { - app.cleanMigratedEmotes().blockLast(); - app.stop(); - } else { - app.stop(); - throw new IllegalArgumentException("Unknown action"); + public static void printHelp() { + System.out.print("discord-transfer"); + System.out.println(" v"+DiscordTransfer.VERSION); + System.out.print(" by BilliAlpha (billi.pamege.300@gmail.com) "); + System.out.println("-- https://github.com/BilliAlpha/discord-transfer"); + System.out.println(); + System.out.println("A discord bot for copying messages between guilds"); + System.out.println(); + System.out.println("Usage: [arguments..] [options...]"); + System.out.println(); + System.out.println("Actions:"); + System.out.println(" help - Show this message and exit"); + System.out.println(); + System.out.println(" migrate - Migrate messages from source guild to dest guild"); + System.out.println(" Arguments:"); + System.out.println(" : The Discord ID of the source guild"); + System.out.println(" : The Discord ID of the destination guild"); + System.out.println(); + System.out.println(" clean - Delete migration reactions"); + System.out.println(" Arguments:"); + System.out.println(" : The Discord ID of the guild to clean"); + System.out.println(); + System.out.println(" Options:"); + System.out.println(" --category , -c "); + System.out.println(" Limit the migration to specific categories, this option expects a Discord ID."); + System.out.println(" You can use this option multiple times to migrate multiple categories."); + System.out.println(" By default, if this option is not present all categories are migrated."); + System.out.println(); + System.out.println(" --skip , -s "); + System.out.println(" Do not migrate a channel, this option expects a Discord ID."); + System.out.println(" You can use this option multiple times to skip multiple channels."); + System.out.println(); + System.out.println(" --after , -a "); + System.out.println(" Only migrate messages sent after the given date."); + System.out.println(" Date is expected to follow ISO-8601 format (ex: `1997−07−16T19:20:30,451Z`)"); + System.out.println(); + System.out.println(" --delay , -d "); + System.out.println(" Add a delay (in milliseconds) between each message migration."); + System.out.println(" By default there is no delay."); + System.out.println(); + System.out.println(" --no-reupload"); + System.out.println(" By default all attachments will be downloaded and re-uploaded to prevent"); + System.out.println(" losing them if the original message is deleted. With this option you can"); + System.out.println(" disable re-upload and only link to the files from the original message."); + System.out.println(); + System.out.println(" -v, --verbose"); + System.out.println(" Can be specified multiple times, increases verbosity."); + System.out.println(); + System.out.println(" -q, --quiet"); + System.out.println(" Be as silent as possible."); + System.out.println(); + System.out.println(" Environment variables:"); + System.out.println(" DISCORD_TOKEN: Required, the Discord bot token"); + } + + // ================================================================================================================= + + public static void main(String[] args) { + String action; + DiscordTransfer app = new DiscordTransfer(); + try { + action = app.handleCliCall(args); + } catch (Exception ex) { + System.err.println(ex.getMessage()); + if (ex.getCause() != null) System.out.println("Caused by: "+ex.getCause().getMessage()); + System.out.println("See 'help' action for help"); + return; } + + LOGGER.debug("Action: " + action); + if ("help".equals(action)) return; + + app.run(); } }