41
41
import java .util .ArrayList ;
42
42
import java .util .HashSet ;
43
43
import java .util .List ;
44
- import java .util .Optional ;
45
44
import java .util .Set ;
46
45
import java .util .concurrent .CompletableFuture ;
47
- import java .util .function .Function ;
48
46
49
47
public class MigrateCommand extends Command {
50
48
private static final Logger LOGGER = LoggerFactory .getLogger (MigrateCommand .class );
@@ -63,10 +61,13 @@ public class MigrateCommand extends Command {
63
61
"Limit the migration to specific categories" , Snowflake ::of )
64
62
.withArrayOption ("skip-channel" , "s" ,
65
63
"Ignore this channel during migration" , Snowflake ::of )
64
+ .withArrayOption ("include-channel" , "i" ,
65
+ "Include this channel during migration" , Snowflake ::of )
66
66
.withOption ("after" , "a" ,
67
67
"Only migrate messages after the given date" , Instant ::parse )
68
68
.withOption ("delay" , "d" ,
69
69
"Pause between each message posted" , Integer ::parseUnsignedInt , 0 )
70
+ .withFlag ("text-only" , null ,"Only migrate text channels" )
70
71
.withFlag ("no-bot" , null ,"Do not copy bot messages" )
71
72
.withFlag ("no-reupload" , null , "Do not re-upload attachments" )
72
73
.build (),
@@ -77,24 +78,28 @@ public class MigrateCommand extends Command {
77
78
private final Guild srcGuild ;
78
79
private final Guild destGuild ;
79
80
private final Set <Snowflake > skipChannels ;
81
+ private final Set <Snowflake > includeChannels ;
80
82
private final Set <Snowflake > categories ;
81
83
private final Instant afterDate ;
82
84
private final int delay ;
83
85
private final boolean reUploadFiles ;
84
86
private final boolean noBotMessages ;
87
+ private final boolean textOnly ;
85
88
private final int verbosity ;
86
89
private final Scheduler scheduler ;
87
90
88
91
public MigrateCommand (Invocation params ) {
89
92
this .client = params .client ;
90
93
this .skipChannels = new HashSet <>(params .getList ("skip-channel" ));
94
+ this .includeChannels = new HashSet <>(params .getList ("include-channel" ));
91
95
this .categories = new HashSet <>(params .getList ("category" ));
92
96
this .afterDate = params .get ("after" );
93
97
this .delay = params .get ("delay" );
94
98
this .reUploadFiles = !params .hasFlag ("no-reupload" );
95
99
this .verbosity = params .get ("verbose" );
96
100
this .scheduler = Schedulers .parallel ();
97
101
this .noBotMessages = params .hasFlag ("no-bot" );
102
+ this .textOnly = params .hasFlag ("text-only" );
98
103
99
104
Snowflake srcGuildId = params .get ("source" );
100
105
try {
@@ -116,43 +121,54 @@ public MigrateCommand(Invocation params) {
116
121
@ Override
117
122
public void execute () {
118
123
LOGGER .info ("Starting migration ..." );
119
- Optional <Long > migrated = getSelectedCategories ()
124
+
125
+ if (!textOnly ) {
126
+ LOGGER .info ("Creating categories and voice channels in destination guild" );
127
+ long migratedVoiceChans = getSelectedCategories ()
128
+ .parallel ()
129
+ .runOn (scheduler )
130
+ .flatMap (this ::migrateCategory )
131
+ .reduce (Long ::sum )
132
+ .blockOptional ()
133
+ .orElse (0L );
134
+ if (migratedVoiceChans > 0 ) {
135
+ LOGGER .info ("Successfully created " +migratedVoiceChans +" voice channels" );
136
+ } else {
137
+ LOGGER .info ("No voice channel created" );
138
+ }
139
+ }
140
+
141
+ LOGGER .info ("Migrating text channels" );
142
+ long migratedMessages = getSelectedTextChannels ()
120
143
.parallel ()
121
144
.runOn (scheduler )
122
- .flatMap (srcCat ->
123
- destGuild .getChannels ().ofType (Category .class )
124
- .filter (c -> c .getName ().equals (srcCat .getName ()))
125
- .singleOrEmpty ()
126
- .switchIfEmpty (
127
- destGuild .createCategory (srcCat .getName ()))
128
- .flatMapMany (dstCat -> migrateCategory (srcCat , dstCat ))
129
- )
145
+ .flatMap (c -> this .migrateTextChannel (c ).onErrorContinue ((err , x ) ->
146
+ LOGGER .warn ("Error in text channel migration (" + c .getName () + "):" , err )))
147
+ .map (TextChannelMigrationResult ::messageCount )
130
148
.reduce (Long ::sum )
131
- .blockOptional ();
132
-
133
- if (migrated . isPresent () && migrated . get () > 0 ) {
134
- LOGGER .info ("Migration finished successfully (" + migrated . get () +" messages) " );
149
+ .blockOptional ()
150
+ . orElse ( 0L );
151
+ if (migratedMessages > 0 ) {
152
+ LOGGER .info ("Successfully migrated " + migratedMessages +" messages" );
135
153
} else {
136
- LOGGER .info ("Migration finished without migrating any message" );
154
+ LOGGER .info ("No message migrated " );
137
155
}
138
156
139
157
client .logout ().block ();
140
158
LOGGER .debug ("Logged out" );
141
159
}
142
160
143
- private Mono <Long > migrateCategory (@ NonNull Category srcCat , @ NonNull Category dstCat ) {
161
+ private Mono <Long > migrateCategory (@ NonNull Category srcCat ) {
144
162
LOGGER .info ("Migrating category: " +srcCat .getName ()+" (" +srcCat .getId ().asString ()+")" );
145
- return migrateCategoryVoiceChannels (srcCat , dstCat )
146
- .then (migrateCategoryTextChannels (srcCat , dstCat )
147
- .map (TextChannelMigrationResult ::messageCount )
148
- .reduce (Long ::sum ))
149
- .onErrorResume (err -> {
150
- LOGGER .warn ("Error in category migration" , err );
151
- return Mono .empty ();
152
- });
163
+ return destGuild .getChannels ().ofType (Category .class )
164
+ .filter (c -> c .getName ().equals (srcCat .getName ()))
165
+ .singleOrEmpty ()
166
+ .switchIfEmpty (destGuild .createCategory (srcCat .getName ()))
167
+ .flatMapMany (dstCat -> migrateCategoryVoiceChannels (srcCat , dstCat ))
168
+ .reduce (Long ::sum );
153
169
}
154
170
155
- private Mono <Void > migrateCategoryVoiceChannels (@ NonNull Category srcCat , @ NonNull Category dstCat ) {
171
+ private Mono <Long > migrateCategoryVoiceChannels (@ NonNull Category srcCat , @ NonNull Category dstCat ) {
156
172
return srcCat .getChannels ().ofType (VoiceChannel .class )
157
173
// Filter on non-migrated channels
158
174
.filterWhen (srcChan -> dstCat .getChannels ()
@@ -166,34 +182,42 @@ private Mono<Void> migrateCategoryVoiceChannels(@NonNull Category srcCat, @NonNu
166
182
.name (srcChan .getName ())
167
183
.parentId (dstCat .getId ())
168
184
.position (srcChan .getRawPosition ()).build ()))
169
- .then ();
185
+ .count ();
170
186
}
171
187
172
- private Flux <TextChannelMigrationResult > migrateCategoryTextChannels (@ NonNull Category srcCat , @ NonNull Category dstCat ) {
173
- return srcCat .getChannels ().ofType (TextChannel .class )
174
- .parallel ().runOn (scheduler )
175
- // Filter on non-skipped channels
176
- .filter (c -> !skipChannels .contains (c .getId ()))
177
- .flatMap (srcChan -> dstCat .getChannels ()
178
- .ofType (TextChannel .class )
179
- .filter (c -> c .getName ().equals (srcChan .getName ()))
180
- .singleOrEmpty ()
181
- .switchIfEmpty (destGuild .createTextChannel (TextChannelCreateSpec .builder ()
182
- .name (srcChan .getName ())
183
- .topic (srcChan .getTopic ().map (Possible ::of ).orElse (Possible .absent ()))
184
- .nsfw (srcChan .isNsfw ())
185
- .parentId (dstCat .getId ())
186
- .position (srcChan .getRawPosition ())
187
- .build ()))
188
- .zipWith (Mono .just (srcChan )))
189
- // T1 = dest chan, T2 = src chan
190
- .flatMap (tuple -> migrateTextChannel (tuple .getT2 (), tuple .getT1 ())
191
- .count ()
192
- .map (count -> new TextChannelMigrationResult (tuple .getT2 (), tuple .getT1 (), count )))
193
- .groups ().flatMap (Flux ::collectList ).flatMapIterable (Function .identity ())
194
- .onErrorContinue ((err , x ) -> LOGGER .warn ("Error in channel migration" , err ));
188
+ private Flux <TextChannelMigrationResult > migrateTextChannel (@ NonNull TextChannel srcChan ) {
189
+ return srcChan .getCategory ()
190
+ .flatMapMany (srcCat ->
191
+ // Find destination category
192
+ destGuild .getChannels ()
193
+ .ofType (Category .class )
194
+ // Filter on name
195
+ .filter (cat -> srcCat .getName ().equals (cat .getName ()))
196
+ // Create category if it doesn't exist
197
+ .switchIfEmpty (destGuild .createCategory (srcCat .getName ()))
198
+ .flatMap (dstCat ->
199
+ // Find destination channel
200
+ dstCat .getChannels ()
201
+ .ofType (TextChannel .class )
202
+ // Filter on name
203
+ .filter (c -> c .getName ().equals (srcChan .getName ()))
204
+ .singleOrEmpty ()
205
+ // Create channel if it doesn't exist
206
+ .switchIfEmpty (destGuild .createTextChannel (TextChannelCreateSpec .builder ()
207
+ .name (srcChan .getName ())
208
+ .topic (srcChan .getTopic ().map (Possible ::of ).orElse (Possible .absent ()))
209
+ .nsfw (srcChan .isNsfw ())
210
+ .parentId (dstCat .getId ())
211
+ .position (srcChan .getRawPosition ())
212
+ .build ()))
213
+ )
214
+ )
215
+ // Migrate channel messages
216
+ .flatMap (dstChan -> migrateTextChannelMessages (srcChan , dstChan ));
195
217
}
196
- private Flux <Message > migrateTextChannel (@ NonNull TextChannel srcChan , @ NonNull TextChannel dstChan ) {
218
+ private Mono <TextChannelMigrationResult > migrateTextChannelMessages (
219
+ @ NonNull TextChannel srcChan , @ NonNull TextChannel dstChan
220
+ ) {
197
221
LOGGER .info ("Migrating channel: " +srcChan .getName ()+" (" +srcChan .getId ().asString ()+")" );
198
222
Snowflake startDate = getChannelStartDate (srcChan .getId ());
199
223
LOGGER .debug ("Channel date: " +startDate .getTimestamp ());
@@ -202,7 +226,9 @@ private Flux<Message> migrateTextChannel(@NonNull TextChannel srcChan, @NonNull
202
226
return flux .filter (m -> m .getReactions ().stream () // Filter on non migrated messages
203
227
.filter (Reaction ::selfReacted )
204
228
.noneMatch (r -> r .getEmoji ().equals (MIGRATED_EMOJI )))
205
- .flatMap (m -> migrateMessage (m , dstChan )); // Perform migration
229
+ .flatMap (m -> migrateMessage (m , dstChan )) // Perform migration
230
+ .count ()
231
+ .map (count -> new TextChannelMigrationResult (srcChan , dstChan , count ));
206
232
}
207
233
208
234
private Mono <Message > migrateMessage (@ NonNull Message msg , @ NonNull TextChannel dstChan ) {
@@ -350,13 +376,47 @@ private Snowflake getChannelStartDate(Snowflake chanId) {
350
376
: chanId ;
351
377
}
352
378
379
+ /**
380
+ * The list of text channels to migrate.
381
+ * <p>
382
+ * This list includes all text channels from the selected categories
383
+ * (see {@link MigrateCommand#getSelectedCategories()} excluding skipped channels
384
+ * to which explicitly included channels are added.
385
+ * </p>
386
+ * @return A flux of selected text channels in the source guild.
387
+ */
388
+ private Flux <TextChannel > getSelectedTextChannels () {
389
+ return Flux .concat (
390
+ getSelectedCategories ()
391
+ .flatMap (Category ::getChannels )
392
+ .ofType (TextChannel .class ),
393
+ Mono .justOrEmpty (includeChannels )
394
+ .flatMapMany (Flux ::fromIterable )
395
+ .flatMap (srcGuild ::getChannelById )
396
+ .ofType (TextChannel .class )
397
+ ).filter (c -> !skipChannels .contains (c .getId ()));
398
+ }
399
+
400
+ /**
401
+ * The list of categories to migrate.
402
+ * <p>
403
+ * If at least one category was specified in parameter then only return
404
+ * explicitly selected categories, otherwise, and if no explicit channel are included,
405
+ * return all categories present in the source guild.
406
+ * </p>
407
+ * @return A flux of selected categories in the source guild.
408
+ */
353
409
private Flux <Category > getSelectedCategories () {
354
- if (categories != null ) {
355
- return Flux .fromIterable (categories )
356
- .flatMap (srcGuild ::getChannelById )
357
- .ofType (Category .class );
358
- }
359
- return srcGuild .getChannels ().ofType (Category .class );
410
+ return Mono .justOrEmpty (categories )
411
+ .flatMapMany (Flux ::fromIterable )
412
+ .flatMap (srcGuild ::getChannelById )
413
+ .ofType (Category .class )
414
+ // If no catagory was selected
415
+ .switchIfEmpty (includeChannels != null && includeChannels .size () > 0
416
+ // But there are included channels, no categories
417
+ ? Flux .empty ()
418
+ // Otherwise, return all existing categories
419
+ : srcGuild .getChannels ().ofType (Category .class ));
360
420
}
361
421
362
422
public record TextChannelMigrationResult (TextChannel sourceChan , TextChannel destChan , long messageCount ) {}
0 commit comments