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
+
+ 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();
}
}