Consistent groups list

This commit is contained in:
Andrea Cavalli 2020-10-19 19:13:55 +02:00
parent bd4132e6a7
commit 316604d3d5
11 changed files with 363 additions and 34 deletions

View File

@ -15,6 +15,10 @@ public class BaseChatInfo {
return supergroupId;
}
public int getSupergroupIdInt() {
return TransferUtils.chatIdToChatEntityId(supergroupId);
}
public String getTitle() {
return title;
}

View File

@ -0,0 +1,54 @@
package it.cavallium;
import java.util.Objects;
import java.util.StringJoiner;
public class ItemUpdate<T> {
private final boolean removed;
private final T item;
public ItemUpdate(boolean removed, T item) {
this.removed = removed;
this.item = item;
}
public boolean isRemoved() {
return removed;
}
public T getItem() {
return item;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ItemUpdate<?> that = (ItemUpdate<?>) o;
if (removed != that.removed) {
return false;
}
return Objects.equals(item, that.item);
}
@Override
public int hashCode() {
int result = (removed ? 1 : 0);
result = 31 * result + (item != null ? item.hashCode() : 0);
return result;
}
@Override
public String toString() {
return new StringJoiner(", ", ItemUpdate.class.getSimpleName() + "[", "]")
.add("removed=" + removed)
.add("item=" + item)
.toString();
}
}

View File

@ -7,6 +7,7 @@ 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;
@ -16,13 +17,17 @@ 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.ListView;
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;
@ -31,8 +36,8 @@ public class PrimaryController {
@FXML private Pane main;
@FXML private TextField phoneNumber;
@FXML private ListView<Text> userbotsList;
@FXML private ComboBox<BaseChatInfo> sourceGroupCombo;
@FXML private ComboBox<BaseChatInfo> destGroupCombo;
@FXML private ChoiceBox<BaseChatInfo> sourceGroupCombo;
@FXML private ChoiceBox<BaseChatInfo> destGroupCombo;
@FXML private ListView<LogEntry> log;
@FXML
@ -57,6 +62,36 @@ public class PrimaryController {
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.getLogService().listenUpdates().subscribeOn(Schedulers.boundedElastic()).flatMap(logString -> MonoFxUtils.runLater(() -> {
if (log.getItems().size() >= App.getLogService().getMaxSize()) {
log.getItems().remove(0);
@ -139,6 +174,10 @@ public class PrimaryController {
return false;
}
});
if (userbotsList.getItems().isEmpty()) {
this.sourceGroupCombo.getItems().clear();
this.destGroupCombo.getItems().clear();
}
return Mono.empty();
});
}).handle((result, sink) -> {
@ -156,12 +195,14 @@ public class PrimaryController {
);
return MonoFxUtils.showAndWait(alert);
})).doOnSuccess((_v) -> {
userbotsList
.getItems()
.add(0,
new Text(phoneNumberPrettyText)
);
this.phoneNumber.setText("");
Platform.runLater(() -> {
userbotsList
.getItems()
.add(0,
new Text(phoneNumberPrettyText)
);
this.phoneNumber.setText("");
});
})
.doOnTerminate(this::enableClicks)
.subscribe(_v -> {}, error -> {
@ -268,5 +309,36 @@ public class PrimaryController {
@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 -> {
}, () -> {
enableClicks();
});
}
}

View File

@ -0,0 +1,66 @@
package it.cavallium;
import java.util.Objects;
import java.util.StringJoiner;
public class SupergroupInfo {
private final BaseChatInfo baseChatInfo;
private final boolean canRestrictMembers;
private final boolean canInviteUsers;
public SupergroupInfo(BaseChatInfo baseChatInfo, boolean canRestrictMembers, boolean canInviteUsers) {
this.baseChatInfo = baseChatInfo;
this.canRestrictMembers = canRestrictMembers;
this.canInviteUsers = canInviteUsers;
}
public BaseChatInfo getBaseChatInfo() {
return baseChatInfo;
}
public boolean canInviteUsers() {
return canInviteUsers;
}
public boolean canRestrictMembers() {
return canRestrictMembers;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SupergroupInfo that = (SupergroupInfo) o;
if (canRestrictMembers != that.canRestrictMembers) {
return false;
}
if (canInviteUsers != that.canInviteUsers) {
return false;
}
return Objects.equals(baseChatInfo, that.baseChatInfo);
}
@Override
public int hashCode() {
int result = baseChatInfo != null ? baseChatInfo.hashCode() : 0;
result = 31 * result + (canRestrictMembers ? 1 : 0);
result = 31 * result + (canInviteUsers ? 1 : 0);
return result;
}
@Override
public String toString() {
return new StringJoiner(", ", SupergroupInfo.class.getSimpleName() + "[", "]")
.add("baseChatInfo=" + baseChatInfo)
.add("canRestrictMembers=" + canRestrictMembers)
.add("canInviteUsers=" + canInviteUsers)
.toString();
}
}

View File

@ -16,11 +16,12 @@ import it.tdlight.jni.TdApi.UpdateSupergroup;
import it.tdlight.jni.TdApi.UpdateSupergroupFullInfo;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.easy.AsyncTdEasy;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.reactivestreams.Publisher;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
@ -31,6 +32,7 @@ public class TransferClient {
private final ConcurrentHashMap<Integer, Supergroup> supergroupInfos = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, SupergroupFullInfo> supergroupFullInfos = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Chat> chats = new ConcurrentHashMap<>();
private final EmitterProcessor<ItemUpdate<SupergroupInfo>> usefulSupergroups = EmitterProcessor.create();
private final Scheduler scheduler;
public TransferClient(AsyncTdEasy client) {
@ -91,6 +93,8 @@ public class TransferClient {
private Mono<Void> onUpdateSupergroupFullInfo(int id, SupergroupFullInfo supergroupFullInfo) {
if (supergroupFullInfos.put(id, supergroupFullInfo) == null) {
var sgInfo = getSupergroupBaseChatInfo(id);
sgInfo.ifPresent(t -> this.usefulSupergroups.onNext(new ItemUpdate<>(false, t)));
// ok
}
return Mono.empty();
@ -104,28 +108,38 @@ public class TransferClient {
return client.<T>send(request).publishOn(scheduler);
}
public Set<BaseChatInfo> getAdminSupergroups(boolean canRestrictMembers, boolean canInviteUsers) {
public Set<BaseChatInfo> getAdminSupergroups(boolean requireCanRestrictMembers, boolean requireCanInviteUsers) {
return supergroupFullInfos.entrySet().stream().flatMap(entry -> {
int id = entry.getKey();
var fullInfo = entry.getValue();
var chatInfo = chats.get(TransferUtils.chatEntityIdToChatId(id, TChatType.SUPERGROUP));
var baseInfo = supergroupInfos.get(id);
if (chatInfo == null || baseInfo == null) {
return Stream.<BaseChatInfo>empty();
} else {
if (baseInfo.status.getConstructor() != ChatMemberStatusAdministrator.CONSTRUCTOR) {
return Stream.empty();
return getSupergroupBaseChatInfo(id).filter(sgInfo -> {
if (!sgInfo.canRestrictMembers() && requireCanRestrictMembers) {
return false;
}
var adminStatus = (ChatMemberStatusAdministrator) baseInfo.status;
if (!adminStatus.canRestrictMembers && canRestrictMembers) {
return Stream.empty();
if (!sgInfo.canInviteUsers() && requireCanInviteUsers) {
return false;
}
if (!adminStatus.canInviteUsers && canInviteUsers) {
return Stream.empty();
}
var baseChatInfo = new BaseChatInfo(id, chatInfo.title);
return Stream.<BaseChatInfo>of(baseChatInfo);
}
return true;
}).map(SupergroupInfo::getBaseChatInfo).stream();
}).collect(Collectors.toSet());
}
private Optional<SupergroupInfo> getSupergroupBaseChatInfo(int id) {
var chatInfo = chats.get(TransferUtils.chatEntityIdToChatId(id, TChatType.SUPERGROUP));
var baseInfo = supergroupInfos.get(id);
if (chatInfo == null || baseInfo == null) {
return Optional.empty();
} else {
if (baseInfo.status.getConstructor() != ChatMemberStatusAdministrator.CONSTRUCTOR) {
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<ItemUpdate<SupergroupInfo>> subscribeAdminSupergroups() {
return usefulSupergroups.hide();
}
}

View File

@ -6,6 +6,7 @@ import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface TransferService {
@ -26,4 +27,8 @@ public interface TransferService {
Set<PhoneNumber> getPhoneNumbers();
Set<BaseChatInfo> getAdminSupergroups(boolean canRestrictMembers, boolean canInviteUsers);
Flux<ItemUpdate<SupergroupInfo>> subscribeAdminSupergroups();
Mono<Void> transferMembers(BaseChatInfo sourceGroup, BaseChatInfo destGroup, Function<UserStatus, Mono<Void>> userStatusConsumer);
}

View File

@ -2,6 +2,7 @@ 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;
@ -35,6 +36,7 @@ import org.slf4j.LoggerFactory;
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;
@ -42,6 +44,8 @@ public class TransferServiceImpl implements TransferService {
private final TdClusterManager clusterManager;
private final ConcurrentHashMap<Long, TransferClient> clients = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Set<TransferClient>> supergroupClients = new ConcurrentHashMap<>();
private final EmitterProcessor<ItemUpdate<TransferClient>> newClients = EmitterProcessor.create();
private final Scheduler updatesScheduler;
private int apiId;
private String apiHash;
@ -172,7 +176,11 @@ public class TransferServiceImpl implements TransferService {
})
.then();
}))
.doOnSuccess((_v) -> clients.put(phoneNumberLong, new TransferClient(client))));
.doOnSuccess((_v) -> {
var newClient = new TransferClient(client);
clients.put(phoneNumberLong, newClient);
newClients.onNext(new ItemUpdate<>(false, newClient));
}));
})
.map(_v -> AddUserBotResult.newSuccess());
}
@ -191,6 +199,7 @@ public class TransferServiceImpl implements TransferService {
@Override
public Mono<Void> closeUserbot(PhoneNumber phoneNumber) {
var client = clients.remove(getLongPhoneNumber(phoneNumber));
newClients.onNext(new ItemUpdate<>(true, client));
if (client == null) {
return Mono.error(new Exception("Userbot " + phoneNumber + " was not found!"));
} else {
@ -224,6 +233,31 @@ public class TransferServiceImpl implements TransferService {
return adminSupergroups;
}
@Override
public Flux<ItemUpdate<SupergroupInfo>> subscribeAdminSupergroups() {
return Flux.merge(this.newClients, Flux
.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;
});
}))
.flatMap(f -> f);
}
@Override
public Mono<Void> transferMembers(BaseChatInfo sourceGroup,
BaseChatInfo destGroup,
Function<UserStatus, Mono<Void>> userStatusConsumer) {
return Mono.empty();
}
@Override
public Mono<Void> quit() {
return Flux

View File

@ -0,0 +1,75 @@
package it.cavallium;
import java.util.StringJoiner;
public class UserStatus {
private final String name;
private final int id;
private final UserStatusType statusType;
private final String errorDescription;
public UserStatus(String name, int id, UserStatusType statusType, String errorDescription) {
this.name = name;
this.id = id;
this.statusType = statusType;
this.errorDescription = errorDescription;
}
public int getId() {
return id;
}
public String getErrorDescription() {
return errorDescription;
}
public String getName() {
return name;
}
public UserStatusType getStatusType() {
return statusType;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserStatus that = (UserStatus) o;
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;
}
@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;
}
@Override
public String toString() {
return new StringJoiner(", ", UserStatus.class.getSimpleName() + "[", "]")
.add("name='" + name + "'")
.add("id=" + id)
.add("errorType=" + statusType)
.add("errorDescription='" + errorDescription + "'")
.toString();
}
}

View File

@ -0,0 +1,5 @@
package it.cavallium;
public enum UserStatusType {
UNKNOWN
}

View File

@ -2,7 +2,7 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Menu?>
@ -95,8 +95,8 @@
<children>
<VBox maxHeight="-Infinity" minHeight="-Infinity" spacing="10.0" GridPane.columnSpan="2">
<children>
<ComboBox fx:id="sourceGroupCombo" editable="true" onAction="#onSourceGroupAction" onHiding="#onSourceGroupHiding" onInputMethodTextChanged="#onSourceGroupTextChanged" onShowing="#onSourceGroupShowing" prefHeight="28.0" promptText="Source group" visibleRowCount="40" />
<ComboBox fx:id="destGroupCombo" editable="true" onAction="#onDestGroupAction" onHiding="#onDestGroupHiding" onInputMethodTextChanged="#onDestGroupTextChanged" onShowing="#onDestGroupShowing" prefHeight="28.0" promptText="Destination group" visibleRowCount="40" VBox.vgrow="ALWAYS" />
<ChoiceBox fx:id="sourceGroupCombo" minWidth="200.0" prefHeight="28.0" />
<ChoiceBox fx:id="destGroupCombo" minWidth="200.0" prefHeight="28.0" />
<Button fx:id="transferBtn" mnemonicParsing="false" onAction="#onDoTransfer" text="Transfer" />
</children>
<padding>
@ -127,7 +127,7 @@
</Text>
</top>
<center>
<ListView fx:id="log" BorderPane.alignment="CENTER" style="-fx-font-size: 8"/>
<ListView fx:id="log" style="-fx-font-size: 8" BorderPane.alignment="CENTER" />
</center>
</BorderPane>
</items>

@ -1 +1 @@
Subproject commit 4fe39ae7d145b4164c8c4fb4536feac2b2694915
Subproject commit 85bac8670d201922ac695f3f086eb0c6cd526e8f