package it.cavallium; import static it.cavallium.StaticSettings.REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP; import static it.cavallium.StaticSettings.requiresAdminPrivilegesOnSourceSupergroup; 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 java.io.IOException; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Executor; import java.util.function.Function; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.event.Event; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; 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; import javafx.scene.layout.Pane; import javafx.scene.text.Text; import org.jetbrains.annotations.NotNull; import org.slf4j.event.Level; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; public class PrimaryController { 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)) ); } var srcItems = sourceGroupCombo.getItems(); srcItems.clear(); for (BaseChatInfo originSupergroups : App.getTransferService().getAdminSupergroups(REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP, false)) { srcItems.add(originSupergroups); } 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() || !REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP) { 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(); } private void disableClicks() { main.setDisable(true); } private void enableClicks() { main.setDisable(false); } @FXML public void openSettings(ActionEvent actionEvent) throws IOException { App.setRoot("settings"); } @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); 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(); } } 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)); } 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()); 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 onSourceGroupHiding(Event event) { } @FXML public void onSourceGroupTextChanged(InputMethodEvent inputMethodEvent) { } @FXML public void onSourceGroupShowing(Event event) { } @FXML public void onDestGroupAction(ActionEvent actionEvent) { } @FXML public void onDestGroupHiding(Event event) { } @FXML public void onDestGroupTextChanged(InputMethodEvent inputMethodEvent) { } @FXML public void onDestGroupShowing(Event event) { } @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(() -> { statusTable.refresh(); statusTxt.setText(""); statusPercentage.setText("0%"); statusBar.setProgress(0d); enableClicks(); }); }) .subscribe(_v -> { }, _v -> { }, () -> {}); } }