Compare commits

...

8 Commits

Author SHA1 Message Date
Andrea Cavalli ebc30f2718 Fix loglevel, fix settings corruption, fix banned bots. 2021-01-10 03:14:49 +01:00
Andrea Cavalli c286176031 Update .gitignore and pom.xml 2021-01-10 01:41:29 +01:00
Andrea Cavalli f1642713d6 Update 2021-01-10 00:08:30 +01:00
Andrea Cavalli a258991e83 Add proxy 2020-12-06 15:38:49 +01:00
Andrea Cavalli 35ef9912f4 Singleton application. Phone number login. 2020-12-06 12:56:14 +01:00
Andrea Cavalli 8c38d60c72 Better flood handling 2020-11-26 21:37:34 +01:00
Andrea Cavalli 3ba6ef1d2d Package branding 2020-11-04 00:50:10 +01:00
Andrea Cavalli 213e5a88f1 Branding 2020-11-04 00:48:39 +01:00
17 changed files with 411 additions and 78 deletions

3
.gitignore vendored
View File

@ -198,4 +198,5 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/java,maven,windows,linux,macos,intellij+all
# End of https://www.toptal.com/developers/gitignore/api/java,maven,windows,linux,macos,intellij+all
/debug/

14
pom.xml
View File

@ -2,7 +2,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it.cavallium</groupId>
<artifactId>TransferBot</artifactId>
<artifactId>ParallelAdder</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
@ -52,7 +52,17 @@
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlib-session-container</artifactId>
<version>4.0.16</version>
<version>[4.2.7,)</version>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-natives-linux-amd64</artifactId>
<version>[1.0.0,)</version>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-natives-windows-amd64</artifactId>
<version>[1.0.0,)</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>

View File

@ -30,7 +30,13 @@ public class App extends Application {
@Override
public void start(Stage stage) throws IOException, CantLoadLibrary {
if (!SystemUtils.lockInstance(".app-lock")) {
System.exit(1);
}
// Initialize TDLib library natives
Init.start();
clusterManager = new TdClusterManager(null, null, Vertx.vertx(), null);
settingsService = new SettingsServiceImpl();

View File

@ -2,15 +2,18 @@ package it.cavallium;
import static it.cavallium.StaticSettings.REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP;
import static it.cavallium.StaticSettings.requiresAdminPrivilegesOnSourceSupergroup;
import static it.cavallium.TransferUtils.getLongPhoneNumber;
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.sun.scenario.Settings;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
import javafx.application.Platform;
import javafx.event.ActionEvent;
@ -110,6 +113,16 @@ public class PrimaryController {
log.scrollTo(logString);
return Mono.empty();
})).subscribe();
this.onAfterInitialize();
}
private void onAfterInitialize() {
for (Long number : App.getTransferService().getInitialPhoneNumbers()) {
this.addUserbot("+" + number, phoneNumber -> {
});
}
}
private void disableClicks() {
@ -132,8 +145,13 @@ public class PrimaryController {
@FXML
public void addUserbot(ActionEvent actionEvent) {
var eventSource = (Node) actionEvent.getSource();
var phoneNumberText = phoneNumber.getText();
this.addUserbot(phoneNumberText, phoneNumber -> {
App.getSettingsService().addPhoneNumber(getLongPhoneNumber(phoneNumber));
});
}
private void addUserbot(String phoneNumberText, Consumer<PhoneNumber> doneSuccessfully) {
try {
var phoneNumber = getUserbotPhoneNumber(phoneNumberText);
var phoneNumberPrettyText = getUserbotPrettyPrintPhoneNumber(phoneNumber);
@ -198,40 +216,47 @@ public class PrimaryController {
}
}).flatMap((_v) -> MonoFxUtils.runLater(() -> {
var alert = new Alert(AlertType.INFORMATION,
"Added userbot " + PhoneNumberUtil
.getInstance()
.format(phoneNumber, PhoneNumberFormat.INTERNATIONAL),
ButtonType.CLOSE
"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)
);
.getItems()
.add(0,
new Text(phoneNumberPrettyText)
);
this.phoneNumber.setText("");
doneSuccessfully.accept(phoneNumber);
});
})
.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();
});
});
.doOnTerminate(this::enableClicks)
.subscribe(_v -> {}, error -> {
Platform.runLater(() -> {
var skipAndDeleteUserbotBtn = new ButtonType("Skip and delete userbot");
var skipBtn = new ButtonType("Skip");
var alert = new Alert(AlertType.ERROR,
error.getLocalizedMessage(),
skipBtn,
skipAndDeleteUserbotBtn
);
alert.setHeaderText("Error while adding the userbot " + PhoneNumberUtil
.getInstance()
.format(phoneNumber, PhoneNumberFormat.INTERNATIONAL));
MonoFxUtils.showAndWait(alert).subscribe(result -> {
if (skipAndDeleteUserbotBtn.getText().equals(result.getText())) {
App.getSettingsService().removePhoneNumber(getLongPhoneNumber(phoneNumber));
}
}, System.err::println, this::enableClicks);
});
});
} catch (NumberFormatException | NumberParseException ex) {
var alert = new Alert(AlertType.ERROR, "Can't parse phone number format", ButtonType.CLOSE);
alert.showAndWait();
enableClicks();
}
}
@ -359,7 +384,7 @@ public class PrimaryController {
})
)
.onErrorResume(error -> MonoFxUtils.runLater(() -> {
var alert = new Alert(AlertType.ERROR, "Error during transfer", ButtonType.CLOSE);
var alert = new Alert(AlertType.ERROR, "Error during users addition", ButtonType.CLOSE);
alert.setContentText(error.getLocalizedMessage());
return MonoFxUtils.showAndWait(alert);
}).then())

View File

@ -1,9 +1,30 @@
package it.cavallium;
import java.time.Duration;
import java.util.Set;
public interface SettingsService {
void setDelayBetweenAdds(Duration duration);
Duration getDelayBetweenAdds();
void addPhoneNumber(Long phoneNumber);
void removePhoneNumber(Long phoneNumber);
void clearPhoneNumbers();
Set<Long> getPhoneNumbers();
String getProxyIp();
void setProxyIp(String value);
Integer getProxyPort();
void setProxyPort(int value);
String getProxySecret();
void setProxySecret(String value);
}

View File

@ -1,22 +1,31 @@
package it.cavallium;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.vertx.core.impl.ConcurrentHashSet;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.event.Level;
public class SettingsServiceImpl implements SettingsService {
private static final Gson gson = new Gson();
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final AtomicReference<Duration> delayBetweenAdds = new AtomicReference<>(Duration.ZERO);
private final AtomicReference<String> proxyIp = new AtomicReference<>("proxy.digitalresistance.dog");
private final AtomicReference<Integer> proxyPort = new AtomicReference<>(443);
private final AtomicReference<String> proxySecret = new AtomicReference<>("d41d8cd98f00b204e9800998ecf8427e");
private final ConcurrentHashSet<Long> phoneNumbers = new ConcurrentHashSet<>();
public SettingsServiceImpl() {
synchronized (gson) {
@ -27,6 +36,21 @@ public class SettingsServiceImpl implements SettingsService {
Optional.ofNullable(settings.getAsJsonPrimitive("delayBetweenAdds")).ifPresent(seconds -> {
this.delayBetweenAdds.set(Duration.ofSeconds(seconds.getAsInt()));
});
Optional.ofNullable(settings.getAsJsonArray("phoneNumbers")).ifPresent(phoneNumbers -> {
this.phoneNumbers.clear();
for (JsonElement phoneNumber : phoneNumbers) {
this.phoneNumbers.add(Long.parseLong(phoneNumber.getAsString()));
}
});
Optional.ofNullable(settings.getAsJsonPrimitive("proxyIp")).ifPresent(value -> {
this.proxyIp.set(value.getAsString());
});
Optional.ofNullable(settings.getAsJsonPrimitive("proxyPort")).ifPresent(value -> {
this.proxyPort.set(value.getAsInt());
});
Optional.ofNullable(settings.getAsJsonPrimitive("proxySecret")).ifPresent(value -> {
this.proxySecret.set(value.getAsString());
});
}
} catch (Exception e) {
e.printStackTrace();
@ -39,8 +63,16 @@ public class SettingsServiceImpl implements SettingsService {
synchronized (gson) {
var settings = new JsonObject();
settings.addProperty("delayBetweenAdds", delayBetweenAdds.get().toSeconds());
var phoneNumbers = new JsonArray();
for (Long phoneNumber : this.phoneNumbers) {
phoneNumbers.add("" + phoneNumber);
}
settings.add("phoneNumbers", phoneNumbers);
settings.addProperty("proxyIp", proxyIp.get());
settings.addProperty("proxyPort", proxyPort.get());
settings.addProperty("proxySecret", proxySecret.get());
try {
Files.writeString(Paths.get("settings.json"), gson.toJson(settings), StandardCharsets.UTF_8, StandardOpenOption.CREATE);
Files.writeString(Paths.get("settings.json"), gson.toJson(settings), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
App.getLogService().append(Level.ERROR, e.getLocalizedMessage());
}
@ -57,4 +89,60 @@ public class SettingsServiceImpl implements SettingsService {
public Duration getDelayBetweenAdds() {
return delayBetweenAdds.get();
}
@Override
public void addPhoneNumber(Long phoneNumber) {
phoneNumbers.add(phoneNumber);
saveSettings();
}
@Override
public void removePhoneNumber(Long phoneNumber) {
phoneNumbers.remove(phoneNumber);
saveSettings();
}
@Override
public void clearPhoneNumbers() {
phoneNumbers.clear();
saveSettings();
}
@Override
public Set<Long> getPhoneNumbers() {
return new HashSet<>(phoneNumbers);
}
@Override
public String getProxyIp() {
return proxyIp.get();
}
@Override
public void setProxyIp(String value) {
this.proxyIp.set(value);
saveSettings();
}
@Override
public Integer getProxyPort() {
return proxyPort.get();
}
@Override
public void setProxyPort(int value) {
this.proxyPort.set(value);
saveSettings();
}
@Override
public String getProxySecret() {
return proxySecret.get();
}
@Override
public void setProxySecret(String value) {
this.proxySecret.set(value);
saveSettings();
}
}

View File

@ -2,8 +2,8 @@ package it.cavallium;
public class StaticSettings {
public static final String BRANDING_NAME = "TransferBot";
public static final boolean REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP = true;
public static final String BRANDING_NAME = "P4RALLEL ADDER";
public static final boolean REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP = false;
public static boolean requiresAdminPrivilegesOnSourceSupergroup() {
return REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP;

View File

@ -0,0 +1,39 @@
package it.cavallium;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SystemUtils {
private static final Logger log = LoggerFactory.getLogger(SystemUtils.class);
public static boolean lockInstance(final String lockFile) {
try {
final File file = new File(lockFile);
final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
final FileLock fileLock = randomAccessFile.getChannel().tryLock();
if (fileLock != null) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
fileLock.release();
randomAccessFile.close();
if (!file.delete()) {
log.warn("Unable to delete lock file: " + lockFile);
}
} catch (Exception e) {
System.err.println();
log.error("Unable to remove lock file: " + lockFile, e);
}
}));
return true;
}
} catch (Exception e) {
log.error("Unable to create and/or lock file: " + lockFile, e);
}
return false;
}
}

View File

@ -1,6 +1,7 @@
package it.cavallium;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AddProxy;
import it.tdlight.jni.TdApi.AuthorizationState;
import it.tdlight.jni.TdApi.AuthorizationStateReady;
import it.tdlight.jni.TdApi.Chat;
@ -9,6 +10,7 @@ import it.tdlight.jni.TdApi.ChatMemberStatusCreator;
import it.tdlight.jni.TdApi.ChatMemberStatusMember;
import it.tdlight.jni.TdApi.GetSupergroupFullInfo;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.jni.TdApi.ProxyTypeMtproto;
import it.tdlight.jni.TdApi.Supergroup;
import it.tdlight.jni.TdApi.SupergroupFullInfo;
import it.tdlight.jni.TdApi.Update;
@ -56,6 +58,18 @@ public class TransferClient {
.publishOn(scheduler)
.flatMap(this::onUpdate)
.subscribe();
this.setupProxy();
}
private void setupProxy() {
var ip = App.getSettingsService().getProxyIp();
var port = App.getSettingsService().getProxyPort();
var secret = App.getSettingsService().getProxySecret();
this.client.send(new AddProxy(ip, port, true, new ProxyTypeMtproto(secret))).doOnError(e -> {
e.printStackTrace();
}).subscribe();
}
public User getClientUser() {

View File

@ -35,4 +35,6 @@ public interface TransferService {
Function<UserStatus, Mono<Void>> userStatusConsumer,
Function<Integer, Mono<Void>> percentageConsumer,
Function<String, Mono<Void>> phaseDescriptionConsumer);
Iterable<? extends Long> getInitialPhoneNumbers();
}

View File

@ -8,9 +8,7 @@ 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 io.vertx.core.impl.ConcurrentHashSet;
import com.hazelcast.cp.internal.datastructures.atomicref.AtomicRef;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.AuthorizationStateClosing;
@ -20,15 +18,13 @@ import it.tdlight.jni.TdApi.ChatMemberStatusAdministrator;
import it.tdlight.jni.TdApi.ChatMemberStatusCreator;
import it.tdlight.jni.TdApi.ChatMemberStatusLeft;
import it.tdlight.jni.TdApi.ChatMemberStatusMember;
import it.tdlight.jni.TdApi.GetMe;
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.FatalErrorType;
import it.tdlight.tdlibsession.td.TdError;
import it.tdlight.tdlibsession.td.easy.AsyncTdEasy;
import it.tdlight.tdlibsession.td.easy.ParameterInfoPasswordHint;
@ -47,6 +43,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
@ -57,6 +54,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.util.function.Tuples;
public class TransferServiceImpl implements TransferService {
@ -67,6 +65,7 @@ public class TransferServiceImpl implements TransferService {
private final Scheduler updatesScheduler;
private int apiId;
private String apiHash;
private boolean waitingInitialPhoneNumbers = true;
public TransferServiceImpl(TdClusterManager clusterManager) {
this.clusterManager = clusterManager;
@ -89,19 +88,23 @@ public class TransferServiceImpl implements TransferService {
BiFunction<AsyncTdEasy, String, Mono<String>> otpSupplier,
Supplier<Mono<Void>> onUserbotClosed) {
long phoneNumberLong = getLongPhoneNumber(phoneNumber);
long phoneNumberLong = TransferUtils.getLongPhoneNumber(phoneNumber);
if (clients.containsKey(phoneNumberLong)) {
return Mono.just(AddUserBotResult.newFailed("Userbot already added!"));
}
String alias = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberFormat.INTERNATIONAL);
final AtomicReference<FatalErrorType> fatalError = new AtomicReference<>(null);
return Mono
.fromCallable(() -> {
return AsyncTdMiddleDirect.getAndDeployInstance(clusterManager, alias, "" + phoneNumberLong);
synchronized (clusterManager) {
return AsyncTdMiddleDirect.getAndDeployInstance(clusterManager, alias, "" + phoneNumberLong)
.block(Duration.ofSeconds(60));
}
})
.subscribeOn(Schedulers.boundedElastic())
.flatMap(v -> v)
.map(middle -> new AsyncTdEasy(middle, alias)).flatMap(client -> {
return client
.execute(new TdApi.SetLogVerbosityLevel(0))
@ -121,6 +124,7 @@ public class TransferServiceImpl implements TransferService {
.setSystemLanguageCode("en")
.setDeviceModel(System.getProperty("os.name"))
.setSystemVersion(System.getProperty("os.version"))
.setFatalErrorHandler((FatalErrorType error) -> Mono.fromRunnable(() -> fatalError.set(error)))
.setParameterRequestHandler((parameter, parameterInfo) -> {
switch (parameter) {
case ASK_FIRST_NAME:
@ -189,7 +193,7 @@ public class TransferServiceImpl implements TransferService {
if (state.getConstructor() == AuthorizationStateReady.CONSTRUCTOR) {
sink.complete();
} else {
sink.error(new Exception(state.getClass().getSimpleName()));
sink.error(new Exception("Userbot closed with state: " + state.getClass().getSimpleName()));
}
})
.then();
@ -220,23 +224,24 @@ public class TransferServiceImpl implements TransferService {
newClients.onNext(new ItemUpdate<>(false, newClient));
}));
})
.map(_v -> AddUserBotResult.newSuccess());
.map(_v -> AddUserBotResult.newSuccess())
.onErrorMap(error -> {
var fatalErrorValue = fatalError.get();
if (fatalErrorValue != null) {
return new TdError(500, fatalErrorValue.toString());
} else {
return error;
}
});
}
private Mono<Void> onClientUpdate(Update update) {
return Mono.empty();
}
private static long getLongPhoneNumber(PhoneNumber phoneNumber) {
return Long.parseLong(PhoneNumberUtil
.getInstance()
.format(phoneNumber, PhoneNumberFormat.E164)
.replace("+", ""));
}
@Override
public Mono<Void> closeUserbot(PhoneNumber phoneNumber) {
var client = clients.remove(getLongPhoneNumber(phoneNumber));
var client = clients.remove(TransferUtils.getLongPhoneNumber(phoneNumber));
newClients.onNext(new ItemUpdate<>(true, client));
if (client == null) {
return Mono.error(new Exception("Userbot " + phoneNumber + " was not found!"));
@ -309,7 +314,7 @@ public class TransferServiceImpl implements TransferService {
return percentageConsumer
.apply(0)
.then(phaseDescriptionConsumer.apply("Transfer from " + sourceGroup.getTitle() + " to " + destGroup.getTitle()))
.then(phaseDescriptionConsumer.apply("Add users 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 " + (REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP ? "removing" : "managing") + " users in the source group"))
@ -389,11 +394,11 @@ public class TransferServiceImpl implements TransferService {
return Mono.empty();
}))
.collect(Collectors.toSet())
.map(transferDestClients -> Tuple2.of(transferSourceClients, transferDestClients));
.map(transferDestClients -> Tuples.of(transferSourceClients, transferDestClients));
})
.map(clientsTuple -> {
var sourceClients = clientsTuple.element1;
var destClients = clientsTuple.element2;
var sourceClients = clientsTuple.getT1();
var destClients = clientsTuple.getT2();
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<TransferClient>(sourceClients);
@ -432,7 +437,7 @@ public class TransferServiceImpl implements TransferService {
return new Exception("Error while obtaining source group members: " + error.getLocalizedMessage(), error);
})
.subscribeOn(Schedulers.boundedElastic())
.map(members -> Tuple2.of(clients, members));
.map(members -> Tuples.of(clients, members));
})
// Finished getting the list of members of the source group
@ -442,8 +447,8 @@ public class TransferServiceImpl implements TransferService {
.then(percentageConsumer.apply(10)).thenReturn(context);
})
.flatMap(context -> {
var clients = context.element1;
var unresolvedUsers = context.element2;
var clients = context.getT1();
var unresolvedUsers = context.getT2();
return Flux
.fromIterable(clients)
.flatMap(client -> Flux
@ -458,7 +463,7 @@ public class TransferServiceImpl implements TransferService {
.collect(Collectors.toSet())
)
.last()
.map(resolvedUsers -> Tuple2.of(clients, resolvedUsers));
.map(resolvedUsers -> Tuples.of(clients, resolvedUsers));
})
// Finished resolving users
@ -468,8 +473,8 @@ public class TransferServiceImpl implements TransferService {
.then(percentageConsumer.apply(15)).thenReturn(context);
})
.flatMap(context -> {
var clients = context.element1;
var unfilteredUsers = context.element2;
var clients = context.getT1();
var unfilteredUsers = context.getT2();
return Flux
.fromIterable(unfilteredUsers)
.<User>flatMap(user -> {
@ -508,23 +513,26 @@ public class TransferServiceImpl implements TransferService {
.thenReturn(user);
})
.collect(Collectors.toSet())
.map(resolvedUsers -> Tuple3.of(clients, resolvedUsers, unfilteredUsers.size()));
.map(resolvedUsers -> Tuples.of(clients, resolvedUsers, unfilteredUsers.size()));
})
// Finished filtering unsuitable users
// Transfer users
.flatMap(context -> {
return phaseDescriptionConsumer.apply("Transferring users")
return phaseDescriptionConsumer.apply("Adding users")
.then(percentageConsumer.apply(20)).thenReturn(context);
})
.flatMap(context -> {
var clients = context.element1;
var users = context.element2;
var totalUsersCount = context.element3;
var clients = context.getT1();
var users = context.getT2();
var totalUsersCount = context.getT3();
var client = clients.stream().skip(ThreadLocalRandom.current().nextInt(clients.size())).findFirst().orElseThrow();
AtomicInteger processedUsersStats = new AtomicInteger(0);
return Flux
.fromIterable(users)
// Apply delay between user
.delayElements(App.getSettingsService().getDelayBetweenAdds())
.flatMap(user -> {
return percentageConsumer
.apply(20 + processedUsersStats.getAndIncrement() / users.size() * (100 - 20))
@ -538,18 +546,36 @@ public class TransferServiceImpl implements TransferService {
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());
} else if (TdLightUtils.errorEquals(new TdError(result.cause().code, result.cause().message), 400, "PEER_FLOOD")) {
return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.ADD_FLOOD, "")).then(Mono.empty());
} else if (TdLightUtils.errorEquals(new TdError(result.cause().code, result.cause().message), 429, "Too Many Requests")) {
return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.ADD_TOO_MANY_REQUESTS, "")).then(Mono.empty());
} else if (TdLightUtils.errorEquals(new TdError(result.cause().code, result.cause().message), 403, "USER_CHANNELS_TOO_MUCH")) {
return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.USER_CHANNELS_TOO_MUCH, "")).then(Mono.empty());
} else {
return Mono.just(result);
}
} else {
return Mono.just(result);
}
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() + "\"");
App.getLogService().append(Level.WARN, "Can't add user \"" + getName(user) + "\" to supergroup \"" + destGroup.getSupergroupIdInt() + " " + destGroup.getTitle() + "\" (" + error.getLocalizedMessage() + ")");
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);
UserStatusType userStatusType;
if (REMOVE_MEMBERS_FROM_SOURCE_SUPERGROUP) {
userStatusType = UserStatusType.ADDED_AND_WAITING_TO_BE_REMOVED;
} else {
userStatusType = UserStatusType.DONE;
}
return userStatusConsumer
.apply(new UserStatus(getName(user), user.id, userStatusType, ""))
.then(Mono.empty())
.thenReturn(user);
});
})
.<User>flatMap(user -> {
@ -576,18 +602,18 @@ public class TransferServiceImpl implements TransferService {
});
} else {
// Don't remove the user from the source supergroup
return Mono.just(user);
transferredSuccessfullyUsersStats.incrementAndGet();
return userStatusConsumer.apply(new UserStatus(getName(user), user.id, UserStatusType.DONE, "")).then(Mono.empty()).thenReturn(user);
}
})
.delayElements(App.getSettingsService().getDelayBetweenAdds())
.collect(Collectors.toSet())
.map(resolvedUsers -> Tuple3.of(clients, resolvedUsers, totalUsersCount));
.map(resolvedUsers -> Tuples.of(clients, resolvedUsers, totalUsersCount));
})
// Finished transferring users
.doOnNext(context -> {
App.getLogService().append(Level.INFO, "Transfer done. Transferred " + transferredSuccessfullyUsersStats.get() + "/" + context.element3 + " users");
App.getLogService().append(Level.INFO, "Users added. Added " + transferredSuccessfullyUsersStats.get() + "/" + context.getT3() + " users");
})
.then(percentageConsumer.apply(100))
@ -596,6 +622,16 @@ public class TransferServiceImpl implements TransferService {
.then();
}
@Override
public Iterable<? extends Long> getInitialPhoneNumbers() {
if (waitingInitialPhoneNumbers) {
waitingInitialPhoneNumbers = false;
return new HashSet<>(App.getSettingsService().getPhoneNumbers());
} else {
return Set.of();
}
}
private static String getName(User user) {
return String.join(" ", List.of("" + user.id, user.firstName, user.lastName));
}

View File

@ -1,5 +1,8 @@
package it.cavallium;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Chat;
import it.tdlight.jni.TdApi.ChatListMain;
@ -52,6 +55,13 @@ public class TransferUtils {
}
}
public static long getLongPhoneNumber(PhoneNumber phoneNumber) {
return Long.parseLong(PhoneNumberUtil
.getInstance()
.format(phoneNumber, PhoneNumberFormat.E164)
.replace("+", ""));
}
private static class ChatIdAndOrderOffsets {

View File

@ -53,6 +53,12 @@ public class UserStatus {
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 ADD_FLOOD:
return "✔️ Can't add to destination group: an userbot has been blocked by telegram for flooding";
case USER_CHANNELS_TOO_MUCH:
return "✔️ Can't add to destination group: subscribed to too many channels";
case ADD_TOO_MANY_REQUESTS:
return "✔️ Can't add to destination group: an userbot has been blocked by telegram for making too many requests";
case CANT_ADD:
return "❌ Can't add to destination group";
case UNKNOWN:

View File

@ -1,5 +1,43 @@
package it.cavallium;
public enum UserStatusType {
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
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,
USER_CHANNELS_TOO_MUCH,
ADD_FLOOD,
ADD_TOO_MANY_REQUESTS,
UNKNOWN
}
/*
Possible errors
Code Type Description
400 BOTS_TOO_MUCH There are too many bots in this chat/channel
400 BOT_GROUPS_BLOCKED This bot can't be added to groups
400 CHANNEL_INVALID The provided channel is invalid
400 CHANNEL_PRIVATE You haven't joined this channel/supergroup
400 CHAT_ADMIN_REQUIRED You must be an admin in this chat to do this
400 CHAT_INVALID Invalid chat
403 CHAT_WRITE_FORBIDDEN You can't write in this chat
400 INPUT_USER_DEACTIVATED The specified user was deleted
400 MSG_ID_INVALID Invalid message ID provided
400 USERS_TOO_MUCH The maximum number of users has been exceeded (to create a chat, for example)
400 USER_BANNED_IN_CHANNEL You're banned from sending messages in supergroups/channels
400 USER_BLOCKED User blocked
400 USER_BOT Bots can only be admins in channels.
403 USER_CHANNELS_TOO_MUCH One of the users you tried to add is already in too many channels/supergroups
400 USER_ID_INVALID The provided user ID is invalid
400 USER_KICKED This user was kicked from this supergroup/channel
400 USER_NOT_MUTUAL_CONTACT The provided user is not a mutual contact
403 USER_PRIVACY_RESTRICTED The user's privacy settings do not allow you to do this
*/

View File

@ -73,7 +73,7 @@
</BorderPane>
<BorderPane>
<top>
<Label alignment="CENTER" prefWidth="-1.0" style="&#10;" text="Transfer" textAlignment="CENTER" wrapText="false" BorderPane.alignment="CENTER">
<Label alignment="CENTER" prefWidth="-1.0" style="&#10;" text="Current operation management" textAlignment="CENTER" wrapText="false" BorderPane.alignment="CENTER">
<font>
<Font size="18.0" fx:id="x11" />
</font>
@ -97,7 +97,7 @@
<children>
<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" />
<Button fx:id="transferBtn" mnemonicParsing="false" onAction="#onDoTransfer" text="Add users" />
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- log4j2.xml - Example log4j configuration file
Place this file in the same directory as your server.jar, edit
to taste, and add -Dlog4j.configurationFile=log4j2.xml to your
server startup flags.
More log4j example configs can be found at
<http://logging.apache.org/log4j/2.x/manual/appenders.html>.
-->
<Configuration status="WARN" packages="net.minecraft,com.mojang">
<Appenders>
<!-- DEFAULT APPENDERS -->
<!-- console logging - logs to stdout -->
<Console name="ConsoleAppender" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} %style{%processId}{magenta} [%15.15t] %style{%-20.20c{1}}{cyan} : %m%n%ex"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<!-- DEFAULT LOGGERS -->
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY"
onMismatch="NEUTRAL" />
</filters>
<AppenderRef ref="ConsoleAppender"/>
<!-- EXAMPLE LOGGERS -->
<!-- <AppenderRef ref="LegacyServerLog"/> -->
</Root>
</Loggers>
</Configuration>

@ -1 +0,0 @@
Subproject commit b1ab4777479c10d48d2fec8392c52a6b66d66233