From 1c13945b8a1f4a48b33a56d8aa2606987cec4c14 Mon Sep 17 00:00:00 2001 From: Andrea Cavalli Date: Tue, 20 Oct 2020 00:31:11 +0200 Subject: [PATCH] Partial implementation of transfer --- src/main/java/it/cavallium/App.java | 80 +-- src/main/java/it/cavallium/MonoFxUtils.java | 11 + .../java/it/cavallium/PrimaryController.java | 594 +++++++++--------- .../it/cavallium/SecondaryController.java | 8 +- .../java/it/cavallium/TransferClient.java | 43 +- .../java/it/cavallium/TransferService.java | 6 +- .../it/cavallium/TransferServiceImpl.java | 267 +++++++- src/main/java/it/cavallium/TransferUtils.java | 95 ++- src/main/java/it/cavallium/UserStatus.java | 42 +- .../java/it/cavallium/UserStatusType.java | 2 +- src/main/resources/it/cavallium/primary.fxml | 18 +- tdlib-session-container | 2 +- 12 files changed, 791 insertions(+), 377 deletions(-) diff --git a/src/main/java/it/cavallium/App.java b/src/main/java/it/cavallium/App.java index 232b180..8bd7844 100644 --- a/src/main/java/it/cavallium/App.java +++ b/src/main/java/it/cavallium/App.java @@ -19,57 +19,57 @@ import org.slf4j.event.Level; */ public class App extends Application { - public static final String VERSION = "1.0.0"; - private static TdClusterManager clusterManager; + public static final String VERSION = "1.0.0"; + private static TdClusterManager clusterManager; - private static TransferService transferService; - private static LogService logService; + private static TransferService transferService; + private static LogService logService; - private static Scene scene; + private static Scene scene; - @Override - public void start(Stage stage) throws IOException, CantLoadLibrary { - Init.start(); - clusterManager = new TdClusterManager(null, null, Vertx.vertx(), null); + @Override + public void start(Stage stage) throws IOException, CantLoadLibrary { + Init.start(); + clusterManager = new TdClusterManager(null, null, Vertx.vertx(), null); - logService = new LogServiceImpl(); - logService.append(Level.INFO, "Initializing"); + logService = new LogServiceImpl(); + logService.append(Level.INFO, "Initializing"); - transferService = new TransferServiceImpl(clusterManager); + transferService = new TransferServiceImpl(clusterManager); - transferService.setApiId(94575); - transferService.setApiHash("a3406de8d171bb422bb6ddf3bbd800e2"); + transferService.setApiId(94575); + transferService.setApiHash("a3406de8d171bb422bb6ddf3bbd800e2"); - scene = new Scene(loadFXML("primary"), 800, 600); - stage.setScene(scene); - stage.show(); - logService.append(Level.INFO, "Initialized"); - } + scene = new Scene(loadFXML("primary"), 800, 600); + stage.setScene(scene); + stage.show(); + logService.append(Level.INFO, "Initialized"); + } - @Override - public void stop() throws Exception { - App.getTransferService().quit().block(Duration.ofSeconds(15)); - System.exit(0); - } + @Override + public void stop() throws Exception { + App.getTransferService().quit().block(Duration.ofSeconds(15)); + System.exit(0); + } - static void setRoot(String fxml) throws IOException { - scene.setRoot(loadFXML(fxml)); - } + static void setRoot(String fxml) throws IOException { + scene.setRoot(loadFXML(fxml)); + } - private static Parent loadFXML(String fxml) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource(fxml + ".fxml")); - return fxmlLoader.load(); - } + private static Parent loadFXML(String fxml) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource(fxml + ".fxml")); + return fxmlLoader.load(); + } - public static void main(String[] args) { - launch(); - } + public static void main(String[] args) { + launch(); + } - public static TransferService getTransferService() { - return transferService; - } + public static TransferService getTransferService() { + return transferService; + } - public static LogService getLogService() { - return logService; - } + public static LogService getLogService() { + return logService; + } } \ No newline at end of file diff --git a/src/main/java/it/cavallium/MonoFxUtils.java b/src/main/java/it/cavallium/MonoFxUtils.java index 09a6fd2..6a3c4ba 100644 --- a/src/main/java/it/cavallium/MonoFxUtils.java +++ b/src/main/java/it/cavallium/MonoFxUtils.java @@ -1,9 +1,12 @@ package it.cavallium; +import it.tdlight.jni.TdApi; +import it.tdlight.tdlibsession.td.TdResult; import java.util.Optional; import java.util.function.Supplier; import javafx.application.Platform; import javafx.scene.control.Dialog; +import org.slf4j.event.Level; import reactor.core.publisher.Mono; public class MonoFxUtils { @@ -46,4 +49,12 @@ public class MonoFxUtils { }); }); } + + public static Mono orElseLogSkipError(TdResult optional) { + if (optional.failed()) { + App.getLogService().append(Level.ERROR, "Received TDLib error: " + optional.cause()); + return Mono.empty(); + } + return Mono.just(optional.result()); + } } diff --git a/src/main/java/it/cavallium/PrimaryController.java b/src/main/java/it/cavallium/PrimaryController.java index 80f28f7..b293396 100644 --- a/src/main/java/it/cavallium/PrimaryController.java +++ b/src/main/java/it/cavallium/PrimaryController.java @@ -20,7 +20,10 @@ import javafx.scene.control.ButtonType; import javafx.scene.control.ChoiceBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Dialog; +import javafx.scene.control.Label; import javafx.scene.control.ListView; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.control.TextInputDialog; import javafx.scene.input.InputMethodEvent; @@ -33,312 +36,341 @@ import reactor.core.scheduler.Schedulers; public class PrimaryController { - @FXML private Pane main; - @FXML private TextField phoneNumber; - @FXML private ListView userbotsList; - @FXML private ChoiceBox sourceGroupCombo; - @FXML private ChoiceBox destGroupCombo; - @FXML private ListView log; + public Label statusTxt; + public ProgressBar statusBar; + public Label statusPercentage; + public TableView statusTable; + @FXML private Pane main; + @FXML private TextField phoneNumber; + @FXML private ListView userbotsList; + @FXML private ChoiceBox sourceGroupCombo; + @FXML private ChoiceBox destGroupCombo; + @FXML private ListView log; - @FXML - protected void initialize() { - for (PhoneNumber number : App.getTransferService().getPhoneNumbers()) { - userbotsList - .getItems() - .add(0, - new Text(getUserbotPrettyPrintPhoneNumber(number)) - ); - } + @FXML + protected void initialize() { + for (PhoneNumber number : App.getTransferService().getPhoneNumbers()) { + userbotsList + .getItems() + .add(0, + new Text(getUserbotPrettyPrintPhoneNumber(number)) + ); + } - var srcItems = sourceGroupCombo.getItems(); - srcItems.clear(); - for (BaseChatInfo originSupergroups : App.getTransferService().getAdminSupergroups(true, false)) { - srcItems.add(originSupergroups); - } + var srcItems = sourceGroupCombo.getItems(); + srcItems.clear(); + for (BaseChatInfo originSupergroups : App.getTransferService().getAdminSupergroups(true, false)) { + srcItems.add(originSupergroups); + } - var destItems = destGroupCombo.getItems(); - destItems.clear(); - for (BaseChatInfo destSupergroups : App.getTransferService().getAdminSupergroups(false, true)) { - destItems.add(destSupergroups); - } + var destItems = destGroupCombo.getItems(); + destItems.clear(); + for (BaseChatInfo destSupergroups : App.getTransferService().getAdminSupergroups(false, true)) { + destItems.add(destSupergroups); + } - App - .getTransferService() - .subscribeAdminSupergroups() - .subscribeOn(Schedulers.single()) - .subscribe((supergroupInfo) -> { - Platform.runLater(() -> { - var srcItems2 = sourceGroupCombo.getItems(); - var destItems2 = destGroupCombo.getItems(); - if (userbotsList.getItems().isEmpty()) { - srcItems2.clear(); - destItems2.clear(); - } else { - boolean added = false; - if (supergroupInfo.getItem().canRestrictMembers()) { - if (!srcItems2.contains(supergroupInfo.getItem().getBaseChatInfo())) { - added |= srcItems2.add(supergroupInfo.getItem().getBaseChatInfo()); - } - } - if (supergroupInfo.getItem().canInviteUsers()) { - if (!destItems2.contains(supergroupInfo.getItem().getBaseChatInfo())) { - added |= destItems2.add(supergroupInfo.getItem().getBaseChatInfo()); - } - } - if (added) { - App.getLogService().append(Level.INFO, "Found group \"" + supergroupInfo.getItem().getBaseChatInfo().getTitle() + "\""); - } - } - }); - }); + App + .getTransferService() + .subscribeAdminSupergroups() + .subscribeOn(Schedulers.single()) + .subscribe((supergroupInfo) -> { + Platform.runLater(() -> { + var srcItems2 = sourceGroupCombo.getItems(); + var destItems2 = destGroupCombo.getItems(); + if (userbotsList.getItems().isEmpty()) { + srcItems2.clear(); + destItems2.clear(); + } else { + boolean added = false; + if (supergroupInfo.getItem().canRestrictMembers()) { + if (!srcItems2.contains(supergroupInfo.getItem().getBaseChatInfo())) { + added |= srcItems2.add(supergroupInfo.getItem().getBaseChatInfo()); + } + } + if (supergroupInfo.getItem().canInviteUsers()) { + if (!destItems2.contains(supergroupInfo.getItem().getBaseChatInfo())) { + added |= destItems2.add(supergroupInfo.getItem().getBaseChatInfo()); + } + } + if (added) { + App.getLogService().append(Level.INFO, "Found group \"" + supergroupInfo.getItem().getBaseChatInfo().getTitle() + "\""); + } + } + }); + }); - App.getLogService().listenUpdates().subscribeOn(Schedulers.boundedElastic()).flatMap(logString -> MonoFxUtils.runLater(() -> { - if (log.getItems().size() >= App.getLogService().getMaxSize()) { - log.getItems().remove(0); - } - log.getItems().add(logString); - log.scrollTo(logString); - return Mono.empty(); - })).subscribe(); - } + App.getLogService().listenUpdates().subscribeOn(Schedulers.boundedElastic()).flatMap(logString -> MonoFxUtils.runLater(() -> { + if (log.getItems().size() >= App.getLogService().getMaxSize()) { + log.getItems().remove(0); + } + log.getItems().add(logString); + log.scrollTo(logString); + return Mono.empty(); + })).subscribe(); + } - private void disableClicks() { - main.setDisable(true); - } + private void disableClicks() { + main.setDisable(true); + } - private void enableClicks() { - main.setDisable(false); - } + private void enableClicks() { + main.setDisable(false); + } - @FXML - public void openSettings(ActionEvent actionEvent) throws IOException { - App.setRoot("settings"); - } + @FXML + public void openSettings(ActionEvent actionEvent) throws IOException { + App.setRoot("settings"); + } - @FXML - public void quit(ActionEvent actionEvent) { - Platform.exit(); - } + @FXML + public void quit(ActionEvent actionEvent) { + Platform.exit(); + } - @FXML - public void addUserbot(ActionEvent actionEvent) { - var eventSource = (Node) actionEvent.getSource(); - var phoneNumberText = phoneNumber.getText(); - try { - var phoneNumber = getUserbotPhoneNumber(phoneNumberText); - var phoneNumberPrettyText = getUserbotPrettyPrintPhoneNumber(phoneNumber); + @FXML + public void addUserbot(ActionEvent actionEvent) { + var eventSource = (Node) actionEvent.getSource(); + var phoneNumberText = phoneNumber.getText(); + try { + var phoneNumber = getUserbotPhoneNumber(phoneNumberText); + var phoneNumberPrettyText = getUserbotPrettyPrintPhoneNumber(phoneNumber); - disableClicks(); - App.getTransferService().addUserbot(phoneNumber, (client) -> MonoFxUtils.runLater(() -> { - var authCodeAlert = new TextInputDialog(); - authCodeAlert.setHeaderText("Insert authentication code that you received"); - return PrimaryController.loopAlert(authCodeAlert, (authCodeText) -> { - if (authCodeText.matches("^[0-9]{3,8}$")) { - try { - int authCode = Integer.parseUnsignedInt(authCodeText); - return Mono.just(authCode); - } catch (NumberFormatException ex) { - var alert = new Alert(AlertType.ERROR, "Can't parse authentication code number", ButtonType.CLOSE); - return MonoFxUtils.showAndWait(alert).then(Mono.empty()); - } - } else { - var alert = new Alert(AlertType.ERROR, "Wrong authentication code number format", ButtonType.CLOSE); - return MonoFxUtils.showAndWait(alert).then(Mono.empty()); - } - }); - }), (client, hint) -> MonoFxUtils.runLater(() -> { - String hintText = ""; - if (hint != null && !hint.isBlank()) { - hintText = " (hint: " + hint + ")"; - } - var otpAlert = new TextInputDialog(); - otpAlert.setHeaderText("Insert user password" + hintText + ":"); - return loopAlert(otpAlert, (otpText) -> { - if (otpText.length() > 0) { - return Mono.just(otpText); - } else { - var alert = new Alert(AlertType.ERROR, "Wrong user password format", ButtonType.CLOSE); - alert.showAndWait(); - return Mono.empty(); - } - }); - }), () -> { - // closed, remove from list - return MonoFxUtils.runLater(() -> { - userbotsList.getItems().removeIf(textBox -> { - try { - return getUserbotPhoneNumber(textBox.getText()).equals(phoneNumber); - } catch (NumberParseException e) { - // Can't happen - e.printStackTrace(); - return false; - } - }); - if (userbotsList.getItems().isEmpty()) { - this.sourceGroupCombo.getItems().clear(); - this.destGroupCombo.getItems().clear(); - } - return Mono.empty(); - }); - }).handle((result, sink) -> { - if (result.failed()) { - sink.error(new Exception(result.getErrorMessage())); - } else { - sink.next(result); - } - }).flatMap((_v) -> MonoFxUtils.runLater(() -> { - var alert = new Alert(AlertType.INFORMATION, - "Added userbot " + PhoneNumberUtil - .getInstance() - .format(phoneNumber, PhoneNumberFormat.INTERNATIONAL), - ButtonType.CLOSE - ); - return MonoFxUtils.showAndWait(alert); - })).doOnSuccess((_v) -> { - Platform.runLater(() -> { - userbotsList - .getItems() - .add(0, - new Text(phoneNumberPrettyText) - ); - this.phoneNumber.setText(""); - }); - }) - .doOnTerminate(this::enableClicks) - .subscribe(_v -> {}, error -> { - Platform.runLater(() -> { - var alert = new Alert(AlertType.ERROR, - "Error while adding the userbot " + PhoneNumberUtil - .getInstance() - .format(phoneNumber, PhoneNumberFormat.INTERNATIONAL), - ButtonType.CLOSE - ); - alert.setContentText(error.getLocalizedMessage()); - MonoFxUtils.showAndWait(alert).subscribe(); + disableClicks(); + App.getTransferService().addUserbot(phoneNumber, (client) -> MonoFxUtils.runLater(() -> { + var authCodeAlert = new TextInputDialog(); + authCodeAlert.setHeaderText("Insert authentication code that you received"); + return PrimaryController.loopAlert(authCodeAlert, (authCodeText) -> { + if (authCodeText.matches("^[0-9]{3,8}$")) { + try { + int authCode = Integer.parseUnsignedInt(authCodeText); + return Mono.just(authCode); + } catch (NumberFormatException ex) { + var alert = new Alert(AlertType.ERROR, "Can't parse authentication code number", ButtonType.CLOSE); + return MonoFxUtils.showAndWait(alert).then(Mono.empty()); + } + } else { + var alert = new Alert(AlertType.ERROR, "Wrong authentication code number format", ButtonType.CLOSE); + return MonoFxUtils.showAndWait(alert).then(Mono.empty()); + } + }); + }), (client, hint) -> MonoFxUtils.runLater(() -> { + String hintText = ""; + if (hint != null && !hint.isBlank()) { + hintText = " (hint: " + hint + ")"; + } + var otpAlert = new TextInputDialog(); + otpAlert.setHeaderText("Insert user password" + hintText + ":"); + return loopAlert(otpAlert, (otpText) -> { + if (otpText.length() > 0) { + return Mono.just(otpText); + } else { + var alert = new Alert(AlertType.ERROR, "Wrong user password format", ButtonType.CLOSE); + alert.showAndWait(); + return Mono.empty(); + } + }); + }), () -> { + // closed, remove from list + return MonoFxUtils.runLater(() -> { + userbotsList.getItems().removeIf(textBox -> { + try { + return getUserbotPhoneNumber(textBox.getText()).equals(phoneNumber); + } catch (NumberParseException e) { + // Can't happen + e.printStackTrace(); + return false; + } + }); + if (userbotsList.getItems().isEmpty()) { + this.sourceGroupCombo.getItems().clear(); + this.destGroupCombo.getItems().clear(); + } + return Mono.empty(); + }); + }).handle((result, sink) -> { + if (result.failed()) { + sink.error(new Exception(result.getErrorMessage())); + } else { + sink.next(result); + } + }).flatMap((_v) -> MonoFxUtils.runLater(() -> { + var alert = new Alert(AlertType.INFORMATION, + "Added userbot " + PhoneNumberUtil + .getInstance() + .format(phoneNumber, PhoneNumberFormat.INTERNATIONAL), + ButtonType.CLOSE + ); + return MonoFxUtils.showAndWait(alert); + })).doOnSuccess((_v) -> { + Platform.runLater(() -> { + userbotsList + .getItems() + .add(0, + new Text(phoneNumberPrettyText) + ); + this.phoneNumber.setText(""); + }); + }) + .doOnTerminate(this::enableClicks) + .subscribe(_v -> {}, error -> { + Platform.runLater(() -> { + var alert = new Alert(AlertType.ERROR, + "Error while adding the userbot " + PhoneNumberUtil + .getInstance() + .format(phoneNumber, PhoneNumberFormat.INTERNATIONAL), + ButtonType.CLOSE + ); + alert.setContentText(error.getLocalizedMessage()); + MonoFxUtils.showAndWait(alert).subscribe(); - enableClicks(); - }); - }); - } catch (NumberFormatException | NumberParseException ex) { - var alert = new Alert(AlertType.ERROR, "Can't parse phone number format", ButtonType.CLOSE); - alert.showAndWait(); - } - } + enableClicks(); + }); + }); + } catch (NumberFormatException | NumberParseException ex) { + var alert = new Alert(AlertType.ERROR, "Can't parse phone number format", ButtonType.CLOSE); + alert.showAndWait(); + } + } - private String getUserbotPrettyPrintPhoneNumber(PhoneNumber phoneNumber) { - return PhoneNumberUtil - .getInstance() - .format(phoneNumber, PhoneNumberFormat.INTERNATIONAL); - } + private String getUserbotPrettyPrintPhoneNumber(PhoneNumber phoneNumber) { + return PhoneNumberUtil + .getInstance() + .format(phoneNumber, PhoneNumberFormat.INTERNATIONAL); + } - public static PhoneNumber getUserbotPhoneNumber(String phoneNumberText) throws NumberParseException { - return PhoneNumberUtil - .getInstance() - .parse(phoneNumberText, PhoneNumberUtil.getCountryMobileToken(1)); - } + public static PhoneNumber getUserbotPhoneNumber(String phoneNumberText) throws NumberParseException { + return PhoneNumberUtil + .getInstance() + .parse(phoneNumberText, PhoneNumberUtil.getCountryMobileToken(1)); + } - private static Mono loopAlert(TextInputDialog alert, Function> resultParser) { - return MonoFxUtils.showAndWaitOpt(alert) - .flatMap(resultOpt -> { - if (resultOpt.isEmpty()) { - return Mono.just(Optional.empty()); - } else { - return resultParser.apply(resultOpt.get()).filter(Objects::nonNull).map(Optional::of); - } - }) - .repeatWhen(s -> s.takeWhile(n -> n == 0)) - .last(Optional.empty()) - .flatMap(opt -> opt.map(Mono::just).orElseGet(Mono::empty)); - } + private static Mono loopAlert(TextInputDialog alert, Function> resultParser) { + return MonoFxUtils.showAndWaitOpt(alert) + .flatMap(resultOpt -> { + if (resultOpt.isEmpty()) { + return Mono.just(Optional.empty()); + } else { + return resultParser.apply(resultOpt.get()).filter(Objects::nonNull).map(Optional::of); + } + }) + .repeatWhen(s -> s.takeWhile(n -> n == 0)) + .last(Optional.empty()) + .flatMap(opt -> opt.map(Mono::just).orElseGet(Mono::empty)); + } - @FXML - public void removeUserbot(ActionEvent actionEvent) { - var selectedItem = userbotsList.getSelectionModel().getSelectedItem(); - if (selectedItem != null) { - disableClicks(); - try { - var phoneNumber = getUserbotPhoneNumber(selectedItem.getText()); + @FXML + public void removeUserbot(ActionEvent actionEvent) { + var selectedItem = userbotsList.getSelectionModel().getSelectedItem(); + if (selectedItem != null) { + disableClicks(); + try { + var phoneNumber = getUserbotPhoneNumber(selectedItem.getText()); - App.getTransferService() - .closeUserbot(phoneNumber) - .subscribeOn(Schedulers.boundedElastic()) - .then(MonoFxUtils.runLater(() -> { - // closed, remove from list - userbotsList.getItems().remove(selectedItem); - return Mono.empty(); - })) - .doOnTerminate(this::enableClicks) - .subscribe(); - } catch (NumberParseException e) { - e.printStackTrace(); - } - } - } + App.getTransferService() + .closeUserbot(phoneNumber) + .subscribeOn(Schedulers.boundedElastic()) + .then(MonoFxUtils.runLater(() -> { + // closed, remove from list + userbotsList.getItems().remove(selectedItem); + return Mono.empty(); + })) + .doOnTerminate(this::enableClicks) + .subscribe(); + } catch (NumberParseException e) { + e.printStackTrace(); + } + } + } - @FXML - public void onSourceGroupAction(ActionEvent actionEvent) { - } + @FXML + public void onSourceGroupAction(ActionEvent actionEvent) { + } - @FXML - public void onSourceGroupHiding(Event event) { - } + @FXML + public void onSourceGroupHiding(Event event) { + } - @FXML - public void onSourceGroupTextChanged(InputMethodEvent inputMethodEvent) { - } + @FXML + public void onSourceGroupTextChanged(InputMethodEvent inputMethodEvent) { + } - @FXML - public void onSourceGroupShowing(Event event) { - } + @FXML + public void onSourceGroupShowing(Event event) { + } - @FXML - public void onDestGroupAction(ActionEvent actionEvent) { - } + @FXML + public void onDestGroupAction(ActionEvent actionEvent) { + } - @FXML - public void onDestGroupHiding(Event event) { - } + @FXML + public void onDestGroupHiding(Event event) { + } - @FXML - public void onDestGroupTextChanged(InputMethodEvent inputMethodEvent) { - } + @FXML + public void onDestGroupTextChanged(InputMethodEvent inputMethodEvent) { + } - @FXML - public void onDestGroupShowing(Event event) { - } + @FXML + public void onDestGroupShowing(Event event) { + } - @FXML - public void onDoTransfer(ActionEvent actionEvent) { - if (sourceGroupCombo.getSelectionModel().getSelectedItem() == null || !(sourceGroupCombo.getSelectionModel().getSelectedItem() instanceof BaseChatInfo)) { - MonoFxUtils - .showAndWait(new Alert(AlertType.ERROR, "You must select a source group", ButtonType.CLOSE)) - .subscribe(); - return; - } - if (destGroupCombo.getSelectionModel().getSelectedItem() == null || !(destGroupCombo.getSelectionModel().getSelectedItem() instanceof BaseChatInfo)) { - MonoFxUtils - .showAndWait(new Alert(AlertType.ERROR, "You must select a destination group", ButtonType.CLOSE)) - .subscribe(); - return; - } - disableClicks(); - App - .getTransferService() - .transferMembers(sourceGroupCombo.getSelectionModel().getSelectedItem(), - destGroupCombo.getSelectionModel().getSelectedItem(), - userStatus -> { - return Mono.empty(); - } - ) - .onErrorResume(error -> MonoFxUtils.runLater(() -> { - var alert = new Alert(AlertType.ERROR, "Error during transfer", ButtonType.CLOSE); - alert.setContentText(error.getLocalizedMessage()); - return MonoFxUtils.showAndWait(alert); - }).then()) - .subscribe(_v -> {}, _v -> { + @SuppressWarnings("ConstantConditions") + @FXML + public void onDoTransfer(ActionEvent actionEvent) { + if (sourceGroupCombo.getSelectionModel().getSelectedItem() == null + || !(sourceGroupCombo.getSelectionModel().getSelectedItem() instanceof BaseChatInfo)) { + MonoFxUtils + .showAndWait(new Alert(AlertType.ERROR, "You must select a source group", ButtonType.CLOSE)) + .subscribe(); + return; + } + if (destGroupCombo.getSelectionModel().getSelectedItem() == null + || !(destGroupCombo.getSelectionModel().getSelectedItem() instanceof BaseChatInfo)) { + MonoFxUtils + .showAndWait(new Alert(AlertType.ERROR, "You must select a destination group", ButtonType.CLOSE)) + .subscribe(); + return; + } + disableClicks(); + statusTable.getItems().clear(); + App + .getTransferService() + .transferMembers(sourceGroupCombo.getSelectionModel().getSelectedItem(), + destGroupCombo.getSelectionModel().getSelectedItem(), + userStatus -> MonoFxUtils.runLater(() -> { + if (statusTable.getItems().contains(userStatus)) { + statusTable.getItems().remove(userStatus); + } + statusTable.getItems().add(userStatus); + return Mono.empty(); + }), + percentage -> MonoFxUtils.runLater(() -> { + statusPercentage.setText(percentage + "%"); + statusBar.setProgress(((double) percentage / 100d)); + return Mono.empty(); + }), + phaseDescription -> MonoFxUtils.runLater(() -> { + App.getLogService().append(Level.INFO, phaseDescription); + statusTxt.setText(phaseDescription); + return Mono.empty(); + }) + ) + .onErrorResume(error -> MonoFxUtils.runLater(() -> { + var alert = new Alert(AlertType.ERROR, "Error during transfer", ButtonType.CLOSE); + alert.setContentText(error.getLocalizedMessage()); + return MonoFxUtils.showAndWait(alert); + }).then()) + .doOnTerminate(() -> { + Platform.runLater(() -> { + statusTxt.setText(""); + statusPercentage.setText("0%"); + statusBar.setProgress(0d); + enableClicks(); + }); + }) + .subscribe(_v -> { + }, _v -> { - }, () -> { - enableClicks(); - }); - } + }, () -> {}); + } } diff --git a/src/main/java/it/cavallium/SecondaryController.java b/src/main/java/it/cavallium/SecondaryController.java index a9ed1a0..bd667e9 100644 --- a/src/main/java/it/cavallium/SecondaryController.java +++ b/src/main/java/it/cavallium/SecondaryController.java @@ -5,8 +5,8 @@ import javafx.fxml.FXML; public class SecondaryController { - @FXML - private void switchToPrimary() throws IOException { - App.setRoot("primary"); - } + @FXML + private void switchToPrimary() throws IOException { + App.setRoot("primary"); + } } \ No newline at end of file diff --git a/src/main/java/it/cavallium/TransferClient.java b/src/main/java/it/cavallium/TransferClient.java index 7bfce93..70ea826 100644 --- a/src/main/java/it/cavallium/TransferClient.java +++ b/src/main/java/it/cavallium/TransferClient.java @@ -5,6 +5,7 @@ import it.tdlight.jni.TdApi.AuthorizationState; import it.tdlight.jni.TdApi.AuthorizationStateReady; import it.tdlight.jni.TdApi.Chat; import it.tdlight.jni.TdApi.ChatMemberStatusAdministrator; +import it.tdlight.jni.TdApi.ChatMemberStatusCreator; import it.tdlight.jni.TdApi.GetSupergroupFullInfo; import it.tdlight.jni.TdApi.Object; import it.tdlight.jni.TdApi.Supergroup; @@ -18,6 +19,7 @@ import it.tdlight.tdlibsession.td.TdResult; import it.tdlight.tdlibsession.td.easy.AsyncTdEasy; import java.util.Optional; import java.util.Set; +import java.util.StringJoiner; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import reactor.core.publisher.EmitterProcessor; @@ -28,6 +30,7 @@ import reactor.core.scheduler.Schedulers; public class TransferClient { + private final String alias; private final AsyncTdEasy client; private final ConcurrentHashMap supergroupInfos = new ConcurrentHashMap<>(); private final ConcurrentHashMap supergroupFullInfos = new ConcurrentHashMap<>(); @@ -35,7 +38,8 @@ public class TransferClient { private final EmitterProcessor> usefulSupergroups = EmitterProcessor.create(); private final Scheduler scheduler; - public TransferClient(AsyncTdEasy client) { + public TransferClient(String alias, AsyncTdEasy client) { + this.alias = alias; this.client = client; this.scheduler = Schedulers.boundedElastic(); @@ -81,12 +85,18 @@ public class TransferClient { } private Mono onUpdateSupergroup(Supergroup supergroup) { - if (supergroupInfos.put(supergroup.id, supergroup) == null) { - return this - .send(new GetSupergroupFullInfo(supergroup.id)) - .filter(TdResult::succeeded) - .map(TdResult::result) - .flatMap(supergroupFullInfo -> onUpdateSupergroupFullInfo(supergroup.id, supergroupFullInfo)); + // Fast checks to ignore most unwanted infos + if (!supergroup.isChannel + && + (supergroup.status.getConstructor() == ChatMemberStatusAdministrator.CONSTRUCTOR + || supergroup.status.getConstructor() == ChatMemberStatusCreator.CONSTRUCTOR)) { + if (supergroupInfos.put(supergroup.id, supergroup) == null) { + return this + .send(new GetSupergroupFullInfo(supergroup.id)) + .filter(TdResult::succeeded) + .map(TdResult::result) + .flatMap(supergroupFullInfo -> onUpdateSupergroupFullInfo(supergroup.id, supergroupFullInfo)); + } } return Mono.empty(); } @@ -129,17 +139,26 @@ public class TransferClient { if (chatInfo == null || baseInfo == null) { return Optional.empty(); } else { - if (baseInfo.status.getConstructor() != ChatMemberStatusAdministrator.CONSTRUCTOR) { + var baseChatInfo = new BaseChatInfo(id, chatInfo.title); + if (baseInfo.status.getConstructor() == ChatMemberStatusCreator.CONSTRUCTOR) { + var sgInfo = new SupergroupInfo(baseChatInfo, true, true); + return Optional.of(sgInfo); + } else if (baseInfo.status.getConstructor() == ChatMemberStatusAdministrator.CONSTRUCTOR) { + var adminStatus = (ChatMemberStatusAdministrator) baseInfo.status; + var sgInfo = new SupergroupInfo(baseChatInfo, adminStatus.canRestrictMembers, adminStatus.canInviteUsers); + return Optional.of(sgInfo); + } else { return Optional.empty(); } - var adminStatus = (ChatMemberStatusAdministrator) baseInfo.status; - var baseChatInfo = new BaseChatInfo(id, chatInfo.title); - var sgInfo = new SupergroupInfo(baseChatInfo, adminStatus.canRestrictMembers, adminStatus.canInviteUsers); - return Optional.of(sgInfo); } } public Flux> subscribeAdminSupergroups() { return usefulSupergroups.hide(); } + + @Override + public String toString() { + return alias; + } } diff --git a/src/main/java/it/cavallium/TransferService.java b/src/main/java/it/cavallium/TransferService.java index 2122644..3a817f4 100644 --- a/src/main/java/it/cavallium/TransferService.java +++ b/src/main/java/it/cavallium/TransferService.java @@ -30,5 +30,9 @@ public interface TransferService { Flux> subscribeAdminSupergroups(); - Mono transferMembers(BaseChatInfo sourceGroup, BaseChatInfo destGroup, Function> userStatusConsumer); + Mono transferMembers(BaseChatInfo sourceGroup, + BaseChatInfo destGroup, + Function> userStatusConsumer, + Function> percentageConsumer, + Function> phaseDescriptionConsumer); } diff --git a/src/main/java/it/cavallium/TransferServiceImpl.java b/src/main/java/it/cavallium/TransferServiceImpl.java index f5c98f9..0f92320 100644 --- a/src/main/java/it/cavallium/TransferServiceImpl.java +++ b/src/main/java/it/cavallium/TransferServiceImpl.java @@ -2,41 +2,48 @@ package it.cavallium; import static it.cavallium.PrimaryController.getUserbotPhoneNumber; -import com.google.common.collect.ConcurrentHashMultiset; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; +import com.hazelcast.cp.internal.util.Tuple2; +import com.hazelcast.cp.internal.util.Tuple3; import it.tdlight.jni.TdApi; import it.tdlight.jni.TdApi.AuthorizationStateClosed; import it.tdlight.jni.TdApi.AuthorizationStateClosing; 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.GetUserPrivacySettingRules; +import it.tdlight.jni.TdApi.Supergroup; +import it.tdlight.jni.TdApi.SupergroupFullInfo; import it.tdlight.jni.TdApi.Update; -import it.tdlight.jni.TdApi.UpdateSupergroup; +import it.tdlight.jni.TdApi.User; +import it.tdlight.jni.TdApi.UserFullInfo; +import it.tdlight.jni.TdApi.UserTypeRegular; 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.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectSets; import java.io.File; +import java.time.Duration; import java.util.HashSet; -import java.util.Optional; +import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import org.apache.logging.log4j.LogManager; -import org.reactivestreams.Publisher; -import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; import reactor.core.publisher.EmitterProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.ReplayProcessor; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; @@ -177,8 +184,28 @@ public class TransferServiceImpl implements TransferService { .then(); })) .doOnSuccess((_v) -> { - var newClient = new TransferClient(client); + var newClient = new TransferClient(alias, client); clients.put(phoneNumberLong, newClient); + + newClient.subscribeAdminSupergroups().doOnNext(supergroupInfoItemUpdate -> { + if (supergroupInfoItemUpdate.isRemoved()) { + supergroupClients.compute(supergroupInfoItemUpdate.getItem().getBaseChatInfo().getSupergroupIdInt(), (sgId, clients) -> { + if (clients != null) { + clients.remove(newClient); + } + return clients; + }); + } else { + supergroupClients.compute(supergroupInfoItemUpdate.getItem().getBaseChatInfo().getSupergroupIdInt(), (sgId, clients) -> { + if (clients == null) { + clients = ObjectSets.synchronize(new ObjectOpenHashSet<>()); + } + clients.add(newClient); + return clients; + }); + } + }).subscribe(); + newClients.onNext(new ItemUpdate<>(false, newClient)); })); }) @@ -239,23 +266,222 @@ public class TransferServiceImpl implements TransferService { .fromStream(clients.values().stream().map(client -> new ItemUpdate<>(false, client)))) .filter(itemClient -> !itemClient.isRemoved()) .map(ItemUpdate::getItem) - .map(client1 -> client1.subscribeAdminSupergroups().doOnNext(supergroupInfoItemUpdate -> { - supergroupClients.compute(supergroupInfoItemUpdate.getItem().getBaseChatInfo().getSupergroupIdInt(), (sgId, clients) -> { - if (clients == null) { - clients = new HashSet<>(); - } - clients.add(client1); - return clients; - }); - })) + .map(TransferClient::subscribeAdminSupergroups) .flatMap(f -> f); } @Override public Mono transferMembers(BaseChatInfo sourceGroup, BaseChatInfo destGroup, - Function> userStatusConsumer) { - return Mono.empty(); + Function> userStatusConsumer, + Function> percentageConsumer, + Function> phaseDescriptionConsumer) { + var sourceSupergroupClients = this.supergroupClients + .getOrDefault(sourceGroup.getSupergroupIdInt(), Set.of()) + .stream() + .filter(clients::containsValue) + .collect(Collectors.toSet()); + if (sourceSupergroupClients.isEmpty()) { + return Mono.error(new Exception("No userbot can remove members from the source group")); + } + + var destSupergroupClients = this.supergroupClients + .getOrDefault(destGroup.getSupergroupIdInt(), Set.of()) + .stream() + .filter(clients::containsValue) + .collect(Collectors.toSet()); + if (destSupergroupClients.isEmpty()) { + return Mono.error(new Exception("No userbot can add members to the destination group")); + } + + return percentageConsumer + .apply(0) + .then(phaseDescriptionConsumer.apply("Transfer from " + sourceGroup.getTitle() + " to " + destGroup.getTitle())) + + // Check and get the set of userbots that can transfer users from group X to group Y + .then(phaseDescriptionConsumer.apply("Checking available userbots for removing users in the source group")) + .thenMany(Flux.fromIterable(sourceSupergroupClients)) + .flatMap(client -> client + .send(new TdApi.GetMe()) + .timeout(Duration.ofSeconds(5)) + .then(client.send(new TdApi.GetSupergroupFullInfo(sourceGroup.getSupergroupIdInt()))) + .timeout(Duration.ofSeconds(5)) + .then(client.send(new TdApi.GetSupergroup(sourceGroup.getSupergroupIdInt()))) + .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) { + return true; + } else { + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't administer group " + sourceGroup.getTitle()); + } + } else { + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: " + sourceGroupFullInfo.cause()); + } + return false; + }) + .map(_v -> client) + .onErrorResume(e -> { + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: " + e.getLocalizedMessage()); + return Mono.empty(); + })) + .collect(Collectors.toSet()) + .flatMap(transferSourceClients -> { + return phaseDescriptionConsumer.apply("Checking available userbots for adding users in the destination group") + .thenMany(Flux.fromIterable(destSupergroupClients)) + .flatMap(client -> client + .send(new TdApi.GetMe()) + .timeout(Duration.ofSeconds(5)) + .then(client.send(new TdApi.GetSupergroup(destGroup.getSupergroupIdInt()))) + .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) { + return true; + } else { + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: Can't administer group " + destGroup.getTitle()); + } + } else { + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: " + destGroupFullInfo.cause()); + } + return false; + }) + .map(_v -> client) + .onErrorResume(e -> { + App.getLogService().append(Level.WARN, "Userbot " + client + " failed: " + e.getLocalizedMessage()); + return Mono.empty(); + })) + .collect(Collectors.toSet()) + .map(transferDestClients -> Tuple2.of(transferSourceClients, transferDestClients)); + }) + .map(clientsTuple -> { + var sourceClients = clientsTuple.element1; + var destClients = clientsTuple.element2; + App.getLogService().append(Level.INFO, "Found source userbots: " + sourceClients.stream().map(TransferClient::toString).collect(Collectors.joining(", "))); + App.getLogService().append(Level.INFO, "Found destination userbots: " + destClients.stream().map(TransferClient::toString).collect(Collectors.joining(", "))); + var chosenClients = new HashSet(sourceClients); + chosenClients.retainAll(destClients); + return chosenClients; + }) + .filter(chosenClients -> !chosenClients.isEmpty()) + .doOnNext(chosenClients -> { + App.getLogService().append(Level.INFO, "Chosen userbots: " + chosenClients.stream().map(TransferClient::toString).collect(Collectors.joining(", "))); + }) + .switchIfEmpty(Mono.defer(() -> { + App.getLogService().append(Level.ERROR, "No userbots are admin in both groups!"); + return Mono.error(new Exception("No userbots are admin in both groups!")); + })) + // Now we have a set of userbots that can transfer the users + + // Get the list of members of the first group from a bot + .flatMap(clients -> { + return phaseDescriptionConsumer.apply("Obtaining group members") + .then(percentageConsumer.apply(5)).thenReturn(clients); + }).flatMap(clients -> { + // Get the first userbot + var client = clients.stream().findAny().orElseThrow(() -> new NullPointerException("No userbots found")); + + // Get the members of the source group + return Mono.fromCallable(() -> { + var members = TransferUtils.getSupergroupMembers(client, sourceGroup.getSupergroupIdInt()); + App.getLogService().append(Level.INFO, "Source group has " + members.size() + " members."); + return members; + }).subscribeOn(Schedulers.boundedElastic()).map(members -> Tuple3.of(client, clients, members)); + }) + // Finished getting the list of members of the source group + + // Resolve users + .flatMap(context -> { + return phaseDescriptionConsumer.apply("Resolving users") + .then(percentageConsumer.apply(10)).thenReturn(context); + }) + .flatMap(context -> { + var client = context.element1; + var clients = context.element2; + var unresolvedUsers = context.element3; + return Flux + .fromIterable(unresolvedUsers) + .flatMap(userId -> client.send(new TdApi.GetUser(userId))) + .timeout(Duration.ofMinutes(2)) + .flatMap(MonoFxUtils::orElseLogSkipError) + .collect(Collectors.toSet()) + .map(resolvedUsers -> Tuple3.of(client, clients, resolvedUsers)); + }) + // Finished resolving users + + // Filter out unsuitable users + .flatMap(context -> { + return phaseDescriptionConsumer.apply("Filtering users") + .then(percentageConsumer.apply(15)).thenReturn(context); + }) + .flatMap(context -> { + var client = context.element1; + var clients = context.element2; + var unfilteredUsers = context.element3; + return Flux + .fromIterable(unfilteredUsers) + .filter(user -> { + if (user.haveAccess) { + return true; + } + userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.NO_ACCESS_HASH, "")); + return false; + }) + .filter(user -> { + if (user.type.getConstructor() == UserTypeRegular.CONSTRUCTOR) { + return true; + } + userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.NOT_REGULAR_USER, "")); + return false; + }) + .filter(user -> { + if (!user.isScam) { + return true; + } + userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.SCAM_USER, "")); + return false; + }) + .filter(user -> { + if (user.restrictionReason == null || user.restrictionReason.isEmpty()) { + return true; + } + userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.RESTRICTED_USER, "Restricted user: " + user.restrictionReason)); + return false; + }) + .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)); + }) + // Finished filtering unsuitable users + + + .then(percentageConsumer.apply(100)) + .then(phaseDescriptionConsumer.apply("Done")) + .then(Mono.delay(Duration.ofMillis(500))) + .then(); + } + + private static String getName(User user) { + return String.join(" ", List.of("" + user.id, user.firstName, user.lastName)); } @Override @@ -263,7 +489,6 @@ public class TransferServiceImpl implements TransferService { return Flux .fromIterable(clients.values()) .flatMap(client -> client.send(new TdApi.Close())) - .log() .collectList() .then(); } diff --git a/src/main/java/it/cavallium/TransferUtils.java b/src/main/java/it/cavallium/TransferUtils.java index 486c809..afb7187 100644 --- a/src/main/java/it/cavallium/TransferUtils.java +++ b/src/main/java/it/cavallium/TransferUtils.java @@ -1,20 +1,26 @@ package it.cavallium; +import it.tdlight.jni.TdApi; import it.tdlight.jni.TdApi.Chat; import it.tdlight.jni.TdApi.ChatListMain; +import it.tdlight.jni.TdApi.ChatMember; +import it.tdlight.jni.TdApi.ChatMemberStatus; +import it.tdlight.jni.TdApi.ChatMembers; import it.tdlight.jni.TdApi.ChatPosition; import it.tdlight.jni.TdApi.Chats; import it.tdlight.jni.TdApi.GetChat; import it.tdlight.jni.TdApi.GetChats; +import it.tdlight.jni.TdApi.GetSupergroupMembers; +import it.tdlight.jni.TdApi.SupergroupFullInfo; import it.tdlight.utils.MonoUtils; +import java.time.Duration; import java.util.Arrays; +import java.util.HashMap; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.slf4j.event.Level; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -142,6 +148,9 @@ public class TransferUtils { .subscriberContext(context); }) .repeatWhen(nFlux -> nFlux.takeWhile(n -> n > 0)) + .doOnNext(chats -> { + App.getLogService().append(Level.DEBUG, "Received " + chats.size() + " home chats"); + }) .flatMap(Flux::fromIterable) .subscriberContext(ctx -> ctx.put("offsets", new AtomicReference<>(new ChatIdAndOrderOffsets(0L, 9223372036854775807L)) @@ -157,4 +166,86 @@ public class TransferUtils { } return Optional.empty(); } + + + public static final String[] searchTable = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "l", "m", "o", "p", "p", "q", + "r", "r", "s", "t", "u", "v", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "bot", "#"}; + + /** + * Reimport supergroup or channel members fully + */ + public static Set getSupergroupMembers(TransferClient client, int supergroupId) { + var supergroupFullInfo = Objects.requireNonNull( + client.send(new TdApi.GetSupergroupFullInfo(supergroupId)) + .flatMap(MonoUtils::orElseThrow) + .block()); + + var fullChatMemberList = new HashMap(supergroupFullInfo.memberCount + 20); + + final var timeout = 24 * 60 * 60 * 1000; /* 1 day */ + final var limit = 200; + + { + var offset = 0; + final var filter = new TdApi.SupergroupMembersFilterSearch(""); + + while (true) { + TdApi.ChatMembers members = client.send(new TdApi.GetSupergroupMembers(supergroupId, + filter, + offset, + limit + )).timeout(Duration.ofMinutes(2)).flatMap(MonoUtils::orElseThrow).blockOptional().orElseThrow(); + for (ChatMember member : members.members) { + fullChatMemberList.put(member.userId, member.status); + } + offset += members.members.length; + if (members.members.length == 0) { + break; + } + } + } + + // if upgradedFromBasicGroupId != 0 is a group + // if upgradedFromMaxMessageId != 0 is a group + if (supergroupFullInfo.upgradedFromBasicGroupId == 0 && supergroupFullInfo.upgradedFromMaxMessageId == 0 + && supergroupFullInfo.memberCount > 500) { + TdApi.Chat info = client + .send(new TdApi.GetChat(supergroupId)) + .flatMap(MonoUtils::orElseThrow) + .timeout(Duration.ofMinutes(2)) + .blockOptional() + .orElseThrow(); + if (info.type.getConstructor() == TdApi.ChatTypeSupergroup.CONSTRUCTOR) { + var type = (TdApi.ChatTypeSupergroup) info.type; + if (type.isChannel) { + final var filterChannel = new TdApi.ChatMembersFilterMembers(); + for (String q : searchTable) { + int offset = 0; + while (true) { + final var filter = new TdApi.SupergroupMembersFilterSearch(q); + var members = client.send(new GetSupergroupMembers(supergroupId, filter, offset, limit)) + .flatMap(MonoUtils::orElseThrow) + .timeout(Duration.ofMinutes(2)) + .blockOptional() + .orElseThrow(); + for (int i = 0; i < members.members.length; i++) { + var member = members.members[i]; + var userId = member.userId; + if (!fullChatMemberList.containsKey(userId)) { + fullChatMemberList.put(userId, member.status); + } + } + offset += members.members.length; + + if (members.members.length == 0) { + break; + } + } + } + } + } + } + + return fullChatMemberList.keySet(); + } } diff --git a/src/main/java/it/cavallium/UserStatus.java b/src/main/java/it/cavallium/UserStatus.java index f2c7446..51acb26 100644 --- a/src/main/java/it/cavallium/UserStatus.java +++ b/src/main/java/it/cavallium/UserStatus.java @@ -1,18 +1,42 @@ package it.cavallium; import java.util.StringJoiner; +import javafx.beans.property.SimpleStringProperty; public class UserStatus { private final String name; private final int id; private final UserStatusType statusType; private final String errorDescription; + private final SimpleStringProperty user = new SimpleStringProperty(""); + private final SimpleStringProperty status = new SimpleStringProperty(""); public UserStatus(String name, int id, UserStatusType statusType, String errorDescription) { this.name = name; this.id = id; this.statusType = statusType; this.errorDescription = errorDescription; + this.user.set(name); + this.status.set(errorDescription.isBlank() ? translateStatusType(statusType) : errorDescription); + } + + 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 NOT_REGULAR_USER: + return "Not a regular user, skipped"; + default: + return statusType.toString(); + } } public int getId() { @@ -31,6 +55,14 @@ public class UserStatus { return statusType; } + public String getUser() { + return user.get(); + } + + public String getStatus() { + return status.get(); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -45,21 +77,13 @@ public class UserStatus { if (id != that.id) { return false; } - if (name != null ? !name.equals(that.name) : that.name != null) { - return false; - } - if (statusType != that.statusType) { - return false; - } - return errorDescription != null ? errorDescription.equals(that.errorDescription) : that.errorDescription == null; + return true; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + id; - result = 31 * result + (statusType != null ? statusType.hashCode() : 0); - result = 31 * result + (errorDescription != null ? errorDescription.hashCode() : 0); return result; } diff --git a/src/main/java/it/cavallium/UserStatusType.java b/src/main/java/it/cavallium/UserStatusType.java index 7cb9f9e..7511ce7 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 { - UNKNOWN + NO_ACCESS_HASH, NOT_REGULAR_USER, SCAM_USER, RESTRICTED_USER, JUST_FOUND, UNKNOWN } diff --git a/src/main/resources/it/cavallium/primary.fxml b/src/main/resources/it/cavallium/primary.fxml index 3c480e7..5285edb 100644 --- a/src/main/resources/it/cavallium/primary.fxml +++ b/src/main/resources/it/cavallium/primary.fxml @@ -15,6 +15,7 @@ + @@ -38,7 +39,7 @@ - + @@ -90,7 +91,6 @@ - @@ -105,8 +105,16 @@ - - + + + + + + + + + + @@ -127,7 +135,7 @@
- +
diff --git a/tdlib-session-container b/tdlib-session-container index 85bac86..6fd5b09 160000 --- a/tdlib-session-container +++ b/tdlib-session-container @@ -1 +1 @@ -Subproject commit 85bac8670d201922ac695f3f086eb0c6cd526e8f +Subproject commit 6fd5b099c3f90e1dbca28d37c9003541e47b4d51