From a5b06b8d47242978ab6236ee0a832aadc3270087 Mon Sep 17 00:00:00 2001 From: Andrea Cavalli Date: Tue, 20 Oct 2020 02:14:04 +0200 Subject: [PATCH] Done --- .../java/it/cavallium/PrimaryController.java | 1 + .../java/it/cavallium/TransferClient.java | 2 +- .../it/cavallium/TransferServiceImpl.java | 159 +++++++++++++----- src/main/java/it/cavallium/UserStatus.java | 47 ++++-- .../java/it/cavallium/UserStatusType.java | 2 +- src/main/resources/it/cavallium/primary.fxml | 10 +- tdlib-session-container | 2 +- 7 files changed, 162 insertions(+), 61 deletions(-) diff --git a/src/main/java/it/cavallium/PrimaryController.java b/src/main/java/it/cavallium/PrimaryController.java index b293396..3d6dab8 100644 --- a/src/main/java/it/cavallium/PrimaryController.java +++ b/src/main/java/it/cavallium/PrimaryController.java @@ -362,6 +362,7 @@ public class PrimaryController { }).then()) .doOnTerminate(() -> { Platform.runLater(() -> { + statusTable.refresh(); statusTxt.setText(""); statusPercentage.setText("0%"); statusBar.setProgress(0d); diff --git a/src/main/java/it/cavallium/TransferClient.java b/src/main/java/it/cavallium/TransferClient.java index 70ea826..6374961 100644 --- a/src/main/java/it/cavallium/TransferClient.java +++ b/src/main/java/it/cavallium/TransferClient.java @@ -139,7 +139,7 @@ public class TransferClient { if (chatInfo == null || baseInfo == null) { return Optional.empty(); } else { - var baseChatInfo = new BaseChatInfo(id, chatInfo.title); + var baseChatInfo = new BaseChatInfo(TransferUtils.chatEntityIdToChatId(id, TChatType.SUPERGROUP), chatInfo.title); if (baseInfo.status.getConstructor() == ChatMemberStatusCreator.CONSTRUCTOR) { var sgInfo = new SupergroupInfo(baseChatInfo, true, true); return Optional.of(sgInfo); diff --git a/src/main/java/it/cavallium/TransferServiceImpl.java b/src/main/java/it/cavallium/TransferServiceImpl.java index 0f92320..f3e46d5 100644 --- a/src/main/java/it/cavallium/TransferServiceImpl.java +++ b/src/main/java/it/cavallium/TransferServiceImpl.java @@ -15,19 +15,23 @@ import it.tdlight.jni.TdApi.AuthorizationStateLoggingOut; import it.tdlight.jni.TdApi.AuthorizationStateReady; import it.tdlight.jni.TdApi.ChatMemberStatusAdministrator; import it.tdlight.jni.TdApi.ChatMemberStatusCreator; +import it.tdlight.jni.TdApi.ChatMemberStatusLeft; import it.tdlight.jni.TdApi.GetUserPrivacySettingRules; +import it.tdlight.jni.TdApi.Ok; import it.tdlight.jni.TdApi.Supergroup; import it.tdlight.jni.TdApi.SupergroupFullInfo; import it.tdlight.jni.TdApi.Update; import it.tdlight.jni.TdApi.User; import it.tdlight.jni.TdApi.UserFullInfo; import it.tdlight.jni.TdApi.UserTypeRegular; +import it.tdlight.tdlibsession.td.TdError; import it.tdlight.tdlibsession.td.easy.AsyncTdEasy; import it.tdlight.tdlibsession.td.easy.ParameterInfoPasswordHint; import it.tdlight.tdlibsession.td.easy.TdEasySettings; import it.tdlight.tdlibsession.td.middle.TdClusterManager; import it.tdlight.tdlibsession.td.middle.direct.AsyncTdMiddleDirect; import it.tdlight.utils.MonoUtils; +import it.tdlight.utils.TdLightUtils; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectSets; import java.io.File; @@ -36,6 +40,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -294,6 +300,8 @@ public class TransferServiceImpl implements TransferService { return Mono.error(new Exception("No userbot can add members to the destination group")); } + AtomicInteger transferredSuccessfullyUsersStats = new AtomicInteger(0); + return percentageConsumer .apply(0) .then(phaseDescriptionConsumer.apply("Transfer from " + sourceGroup.getTitle() + " to " + destGroup.getTitle())) @@ -304,26 +312,25 @@ public class TransferServiceImpl implements TransferService { .flatMap(client -> client .send(new TdApi.GetMe()) .timeout(Duration.ofSeconds(5)) + .flatMap(MonoUtils::orElseThrow) .then(client.send(new TdApi.GetSupergroupFullInfo(sourceGroup.getSupergroupIdInt()))) + .flatMap(MonoUtils::orElseThrow) .timeout(Duration.ofSeconds(5)) .then(client.send(new TdApi.GetSupergroup(sourceGroup.getSupergroupIdInt()))) + .flatMap(MonoUtils::orElseThrow) .timeout(Duration.ofSeconds(5)) .filter(sourceGroupFullInfo -> { - if (sourceGroupFullInfo.succeeded()) { - if (sourceGroupFullInfo.result().status.getConstructor() == ChatMemberStatusAdministrator.CONSTRUCTOR) { - var statusAdmin = (ChatMemberStatusAdministrator) sourceGroupFullInfo.result().status; - if (statusAdmin.canRestrictMembers) { - return true; - } else { - App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't restrict members of group " + sourceGroup.getTitle()); - } - } else if (sourceGroupFullInfo.result().status.getConstructor() == ChatMemberStatusCreator.CONSTRUCTOR) { + if (sourceGroupFullInfo.status.getConstructor() == ChatMemberStatusAdministrator.CONSTRUCTOR) { + var statusAdmin = (ChatMemberStatusAdministrator) sourceGroupFullInfo.status; + if (statusAdmin.canRestrictMembers) { return true; } else { - App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't administer group " + sourceGroup.getTitle()); + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't restrict members of group " + sourceGroup.getTitle()); } + } else if (sourceGroupFullInfo.status.getConstructor() == ChatMemberStatusCreator.CONSTRUCTOR) { + return true; } else { - App.getLogService().append(Level.WARN, "Userbot " + client + " failed: " + sourceGroupFullInfo.cause()); + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't administer group " + sourceGroup.getTitle()); } return false; }) @@ -338,25 +345,23 @@ public class TransferServiceImpl implements TransferService { .thenMany(Flux.fromIterable(destSupergroupClients)) .flatMap(client -> client .send(new TdApi.GetMe()) + .flatMap(MonoUtils::orElseThrow) .timeout(Duration.ofSeconds(5)) .then(client.send(new TdApi.GetSupergroup(destGroup.getSupergroupIdInt()))) + .flatMap(MonoUtils::orElseThrow) .timeout(Duration.ofSeconds(5)) .filter(destGroupFullInfo -> { - if (destGroupFullInfo.succeeded()) { - if (destGroupFullInfo.result().status.getConstructor() == ChatMemberStatusAdministrator.CONSTRUCTOR) { - var statusAdmin = (ChatMemberStatusAdministrator) destGroupFullInfo.result().status; - if (statusAdmin.canInviteUsers) { - return true; - } else { - App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't invite members to group " + destGroup.getTitle()); - } - } else if (destGroupFullInfo.result().status.getConstructor() == ChatMemberStatusCreator.CONSTRUCTOR) { + if (destGroupFullInfo.status.getConstructor() == ChatMemberStatusAdministrator.CONSTRUCTOR) { + var statusAdmin = (ChatMemberStatusAdministrator) destGroupFullInfo.status; + if (statusAdmin.canInviteUsers) { return true; } else { - App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't administer group " + destGroup.getTitle()); + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't invite members to group " + destGroup.getTitle()); } + } else if (destGroupFullInfo.status.getConstructor() == ChatMemberStatusCreator.CONSTRUCTOR) { + return true; } else { - App.getLogService().append(Level.WARN, "Userbot " + client + " failed: " + destGroupFullInfo.cause()); + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't administer group " + destGroup.getTitle()); } return false; }) @@ -416,8 +421,12 @@ public class TransferServiceImpl implements TransferService { return Flux .fromIterable(unresolvedUsers) .flatMap(userId -> client.send(new TdApi.GetUser(userId))) - .timeout(Duration.ofMinutes(2)) .flatMap(MonoFxUtils::orElseLogSkipError) + .timeout(Duration.ofMinutes(2)) + .onErrorResume(error -> { + App.getLogService().append(Level.WARN, "Error while resolving an user: " + error); + return Mono.empty(); + }) .collect(Collectors.toSet()) .map(resolvedUsers -> Tuple3.of(client, clients, resolvedUsers)); }) @@ -434,45 +443,109 @@ public class TransferServiceImpl implements TransferService { var unfilteredUsers = context.element3; return Flux .fromIterable(unfilteredUsers) - .filter(user -> { + .flatMap(user -> { if (user.haveAccess) { - return true; + return Mono.just(user); } - userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.NO_ACCESS_HASH, "")); - return false; + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.NO_ACCESS_HASH, "")).then(Mono.empty()); }) - .filter(user -> { + .flatMap(user -> { if (user.type.getConstructor() == UserTypeRegular.CONSTRUCTOR) { - return true; + return Mono.just(user); } - userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.NOT_REGULAR_USER, "")); - return false; + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.NOT_REGULAR_USER, "")).then(Mono.empty()); }) - .filter(user -> { + .flatMap(user -> { if (!user.isScam) { - return true; + return Mono.just(user); } - userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.SCAM_USER, "")); - return false; + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.SCAM_USER, "")).then(Mono.empty()); }) - .filter(user -> { + .flatMap(user -> { if (user.restrictionReason == null || user.restrictionReason.isEmpty()) { - return true; + return Mono.just(user); } - userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.RESTRICTED_USER, "Restricted user: " + user.restrictionReason)); - return false; + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.RESTRICTED_USER, "Restricted user: " + user.restrictionReason)).then(Mono.empty()); }) .flatMap(user -> { return userStatusConsumer .apply(new UserStatus(getName(user), user.id, UserStatusType.JUST_FOUND, "")) .thenReturn(user); }) - .timeout(Duration.ofMinutes(2)) .collect(Collectors.toSet()) - .map(resolvedUsers -> Tuple3.of(client, clients, resolvedUsers)); + .map(resolvedUsers -> Tuple2.of(clients, resolvedUsers)); }) // Finished filtering unsuitable users + // Transfer users + .flatMap(context -> { + return phaseDescriptionConsumer.apply("Transferring users") + .then(percentageConsumer.apply(20)).thenReturn(context); + }) + .flatMap(context -> { + var clients = context.element1; + var users = context.element2; + var client = clients.stream().skip(ThreadLocalRandom.current().nextInt(clients.size())).findFirst().orElseThrow(); + AtomicInteger processedUsersStats = new AtomicInteger(0); + return Flux + .fromIterable(users) + .flatMap(user -> { + return percentageConsumer + .apply(20 + processedUsersStats.getAndIncrement() / users.size() * (100 - 20)) + .thenReturn(user); + }) + .flatMap(user -> { + return client.send(new TdApi.AddChatMember(destGroup.getSupergroupId(), user.id, 0)) + .flatMap(result -> { + if (result.failed()) { + if (TdLightUtils.errorEquals(new TdError(result.cause().code, result.cause().message), 403, "USER_PRIVACY_RESTRICTED")) { + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.USER_PRIVACY_RESTRICTED, "")).then(Mono.empty()); + } else if (TdLightUtils.errorEquals(new TdError(result.cause().code, result.cause().message), 400, "USER_NOT_MUTUAL_CONTACT")) { + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.USER_NOT_MUTUAL_CONTACT, "")).then(Mono.empty()); + } + } + return Mono.just(result); + }) + .flatMap(MonoUtils::orElseThrow) + .timeout(Duration.ofMinutes(2)) + .onErrorResume((error) -> { + App.getLogService().append(Level.WARN, "Can't add user \"" + getName(user) + "\" to supergroup \"" + destGroup.getSupergroupIdInt() + " " + destGroup.getTitle() + "\""); + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.CANT_ADD, "Can't add to destination supergroup: " + error.getLocalizedMessage())).then(Mono.empty()); + }) + .flatMap(_v -> { + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.ADDED_AND_WAITING_TO_BE_REMOVED, "")).then(Mono.empty()).thenReturn(user); + }); + }) + .flatMap(user -> { + return client.send(new TdApi.SetChatMemberStatus(sourceGroup.getSupergroupId(), user.id, new ChatMemberStatusLeft())) + .flatMap(result -> { + if (result.failed()) { + if (TdLightUtils.errorEquals(new TdError(result.cause().code, result.cause().message), 3, "Can't remove chat owner")) { + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.CANT_REMOVE_CHAT_OWNER, "")).then(Mono.empty()); + } + } + return Mono.just(result); + }) + .flatMap(MonoUtils::orElseThrow) + .timeout(Duration.ofMinutes(2)) + .onErrorResume((error) -> { + App.getLogService().append(Level.WARN, "Can't remove user \"" + getName(user) + "\" from supergroup \"" + sourceGroup.getSupergroupIdInt() + " " + sourceGroup.getTitle() + "\""); + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.CANT_REMOVE, "Can't remove from source supergroup: " + error.getLocalizedMessage())).then(Mono.empty()); + }) + .flatMap(_v -> { + transferredSuccessfullyUsersStats.incrementAndGet(); + return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.DONE, "")).then(Mono.empty()).thenReturn(user); + }); + }) + .collect(Collectors.toSet()) + .map(resolvedUsers -> Tuple2.of(clients, resolvedUsers)); + }) + // Finished transferring users + + + .doOnNext(context -> { + App.getLogService().append(Level.INFO, "Transfer done. Transferred " + transferredSuccessfullyUsersStats.get() + "/" + context.element2.size() + " users"); + }) .then(percentageConsumer.apply(100)) .then(phaseDescriptionConsumer.apply("Done")) @@ -489,6 +562,10 @@ public class TransferServiceImpl implements TransferService { return Flux .fromIterable(clients.values()) .flatMap(client -> client.send(new TdApi.Close())) + .onErrorResume(error -> { + App.getLogService().append(Level.ERROR, "Can't close a tdlib instance: " + error.getLocalizedMessage()); + return Mono.empty(); + }) .collectList() .then(); } diff --git a/src/main/java/it/cavallium/UserStatus.java b/src/main/java/it/cavallium/UserStatus.java index 51acb26..228f68a 100644 --- a/src/main/java/it/cavallium/UserStatus.java +++ b/src/main/java/it/cavallium/UserStatus.java @@ -17,23 +17,46 @@ public class UserStatus { this.statusType = statusType; this.errorDescription = errorDescription; this.user.set(name); - this.status.set(errorDescription.isBlank() ? translateStatusType(statusType) : errorDescription); + this.status.set(errorDescription.isBlank() ? escapeeStatusType(translateStatusType(statusType)) : errorDescription); + } + + private String escapeeStatusType(String statusText) { + return statusText + .replace("✔", "[ OK ]") + .replace("⏳", "[ ** ]") + .replace("⚠", "[WARN]") + .replace("❌", "[ERR!]") + .replace("❓", "[ ?? ]"); } private String translateStatusType(UserStatusType statusType) { switch (statusType) { - case SCAM_USER: - return "Scam user, skipped"; - case NO_ACCESS_HASH: - return "No access hash, skipped"; - case RESTRICTED_USER: - return "User restricted, skipped"; - case JUST_FOUND: - return ""; - case UNKNOWN: - return "Unknown"; + case DONE: + return "✔️ Done".substring(2); case NOT_REGULAR_USER: - return "Not a regular user, skipped"; + return "✔️ Not a regular user, skipped"; + case SCAM_USER: + return "✔️ Scam user, skipped"; + case CANT_REMOVE_CHAT_OWNER: + return "✔️ Chat owner, skipped"; + case JUST_FOUND: + return "⏳ Found"; + case ADDED_AND_WAITING_TO_BE_REMOVED: + return "⏳ Added to destination group, still not removed from source group"; + case NO_ACCESS_HASH: + return "⚠️ No access hash, skipped"; + case RESTRICTED_USER: + return "⚠️ User restricted, skipped"; + case CANT_REMOVE: + return "⚠️ Can't remove from source group"; + case USER_PRIVACY_RESTRICTED: + return "❌ Can't add to destination group: this user privacy settings don't allow invitations to groups"; + case USER_NOT_MUTUAL_CONTACT: + return "❌ Can't add to destination group: this user only allows mutual contacts to invite it in the destination group"; + case CANT_ADD: + return "❌ Can't add to destination group"; + case UNKNOWN: + return "❓ Unknown"; default: return statusType.toString(); } diff --git a/src/main/java/it/cavallium/UserStatusType.java b/src/main/java/it/cavallium/UserStatusType.java index 7511ce7..8d47f9d 100644 --- a/src/main/java/it/cavallium/UserStatusType.java +++ b/src/main/java/it/cavallium/UserStatusType.java @@ -1,5 +1,5 @@ package it.cavallium; public enum UserStatusType { - NO_ACCESS_HASH, NOT_REGULAR_USER, SCAM_USER, RESTRICTED_USER, JUST_FOUND, UNKNOWN + NO_ACCESS_HASH, NOT_REGULAR_USER, SCAM_USER, RESTRICTED_USER, JUST_FOUND, CANT_ADD, ADDED_AND_WAITING_TO_BE_REMOVED, DONE, CANT_REMOVE, USER_PRIVACY_RESTRICTED, USER_NOT_MUTUAL_CONTACT, CANT_REMOVE_CHAT_OWNER, UNKNOWN } diff --git a/src/main/resources/it/cavallium/primary.fxml b/src/main/resources/it/cavallium/primary.fxml index 30f1311..e234d2e 100644 --- a/src/main/resources/it/cavallium/primary.fxml +++ b/src/main/resources/it/cavallium/primary.fxml @@ -41,9 +41,9 @@ - + - +