diff --git a/src/main/java/it/auties/whatsapp/api/ConnectionBuilder.java b/src/main/java/it/auties/whatsapp/api/ConnectionBuilder.java index d9aa13d2..00e641c6 100644 --- a/src/main/java/it/auties/whatsapp/api/ConnectionBuilder.java +++ b/src/main/java/it/auties/whatsapp/api/ConnectionBuilder.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.api; import it.auties.whatsapp.controller.ControllerSerializer; -import it.auties.whatsapp.util.ControllerHelper; +import it.auties.whatsapp.controller.StoreKeysPair; import java.util.List; import java.util.Objects; @@ -53,8 +53,8 @@ public T newConnection() { */ public T newConnection(UUID uuid) { var sessionUuid = Objects.requireNonNullElseGet(uuid, UUID::randomUUID); - var sessionStoreAndKeys = ControllerHelper.deserialize(sessionUuid, null, null, clientType, serializer) - .orElseGet(() -> ControllerHelper.create(sessionUuid, null, null, clientType, serializer)); + var sessionStoreAndKeys = serializer.deserializeStoreKeysPair(sessionUuid, null, null, clientType) + .orElseGet(() -> serializer.newStoreKeysPair(sessionUuid, null, null, clientType)); return createConnection(sessionStoreAndKeys); } @@ -67,8 +67,8 @@ public T newConnection(UUID uuid) { * @return a non-null options selector */ public T newConnection(long phoneNumber) { - var sessionStoreAndKeys = ControllerHelper.deserialize(null, phoneNumber, null, clientType, serializer) - .orElseGet(() -> ControllerHelper.create(UUID.randomUUID(), phoneNumber, null, clientType, serializer)); + var sessionStoreAndKeys = serializer.deserializeStoreKeysPair(null, phoneNumber, null, clientType) + .orElseGet(() -> serializer.newStoreKeysPair(UUID.randomUUID(), phoneNumber, null, clientType)); return createConnection(sessionStoreAndKeys); } @@ -81,8 +81,8 @@ public T newConnection(long phoneNumber) { * @return a non-null options selector */ public T newConnection(String alias) { - var sessionStoreAndKeys = ControllerHelper.deserialize(null, null, alias, clientType, serializer) - .orElseGet(() -> ControllerHelper.create(UUID.randomUUID(), null, alias != null ? List.of(alias) : null, clientType, serializer)); + var sessionStoreAndKeys = serializer.deserializeStoreKeysPair(null, null, alias, clientType) + .orElseGet(() -> serializer.newStoreKeysPair(UUID.randomUUID(), null, alias != null ? List.of(alias) : null, clientType)); return createConnection(sessionStoreAndKeys); } @@ -114,8 +114,8 @@ public T lastConnection() { */ public Optional newOptionalConnection(UUID uuid) { var sessionUuid = Objects.requireNonNullElseGet(uuid, UUID::randomUUID); - var sessionStoreAndKeys = ControllerHelper.deserialize(sessionUuid, null, null, clientType, serializer); - return sessionStoreAndKeys.map(this::createConnection); + return serializer.deserializeStoreKeysPair(sessionUuid, null, null, clientType) + .map(this::createConnection); } /** @@ -125,8 +125,8 @@ public Optional newOptionalConnection(UUID uuid) { * @return a non-null options selector */ public Optional newOptionalConnection(Long phoneNumber) { - var sessionStoreAndKeys = ControllerHelper.deserialize(null, phoneNumber, null, clientType, serializer); - return sessionStoreAndKeys.map(this::createConnection); + return serializer.deserializeStoreKeysPair(null, phoneNumber, null, clientType) + .map(this::createConnection); } /** @@ -137,8 +137,8 @@ public Optional newOptionalConnection(Long phoneNumber) { * @return a non-null options selector */ public Optional newOptionalConnection(String alias) { - var sessionStoreAndKeys = ControllerHelper.deserialize(null, null, alias, clientType, serializer); - return sessionStoreAndKeys.map(this::createConnection); + return serializer.deserializeStoreKeysPair(null, null, alias, clientType) + .map(this::createConnection); } /** @@ -160,7 +160,7 @@ public Optional lastOptionalConnection() { } @SuppressWarnings("unchecked") - private T createConnection(ControllerHelper.StoreAndKeysPair sessionStoreAndKeys) { + private T createConnection(StoreKeysPair sessionStoreAndKeys) { return (T) switch (clientType) { case WEB -> new WebOptionsBuilder(sessionStoreAndKeys.store(), sessionStoreAndKeys.keys()); case MOBILE -> new MobileOptionsBuilder(sessionStoreAndKeys.store(), sessionStoreAndKeys.keys()); diff --git a/src/main/java/it/auties/whatsapp/api/Whatsapp.java b/src/main/java/it/auties/whatsapp/api/Whatsapp.java index 9d804cf0..9d739b36 100644 --- a/src/main/java/it/auties/whatsapp/api/Whatsapp.java +++ b/src/main/java/it/auties/whatsapp/api/Whatsapp.java @@ -145,7 +145,7 @@ protected Whatsapp(Store store, Keys keys, ErrorHandler errorHandler, WebVerific return; } - store.addListeners(ListenerScanner.scan(this)); + store.addListeners(ListenerScanner.scan(this, store.autodetectListeners())); } /** diff --git a/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java b/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java index 6b804996..5da7af07 100644 --- a/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java +++ b/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java @@ -5,6 +5,7 @@ import it.auties.whatsapp.util.DefaultControllerSerializer; import java.nio.file.Path; +import java.util.Collection; import java.util.LinkedList; import java.util.Optional; import java.util.UUID; @@ -52,6 +53,28 @@ static ControllerSerializer toSmile(Path baseDirectory) { */ LinkedList listPhoneNumbers(ClientType type); + /** + * Creates a fresh pair of store and keys + * + * @param uuid the non-null uuid + * @param phoneNumber the nullable phone number + * @param alias the nullable alias + * @param clientType the non-null client type + * @return a non-null store-keys pair + */ + StoreKeysPair newStoreKeysPair(UUID uuid, Long phoneNumber, Collection alias, ClientType clientType); + + /** + * Deserializes a store-keys pair from a list of possible identifiers + * + * @param uuid the nullable identifying unique id + * @param phoneNumber the nullable identifying phone number + * @param alias the nullable identifying alias + * @param clientType the non-null client type + * @return an optional store-keys pair + */ + Optional deserializeStoreKeysPair(UUID uuid, Long phoneNumber, String alias, ClientType clientType); + /** * Serializes the keys * diff --git a/src/main/java/it/auties/whatsapp/controller/Store.java b/src/main/java/it/auties/whatsapp/controller/Store.java index 1e024a0f..2a4ff371 100644 --- a/src/main/java/it/auties/whatsapp/controller/Store.java +++ b/src/main/java/it/auties/whatsapp/controller/Store.java @@ -262,10 +262,15 @@ public final class Store extends Controller { */ private boolean autodetectListeners; + /** + * Whether the listeners that were automatically scanned should be cached + */ + private boolean cacheDetectedListeners; /** * Whether updates about the presence of the session should be sent automatically to Whatsapp * For example, when the bot is started, the status of the companion is changed to available if this option is enabled + * If this option is enabled, the companion will not receive notifications because the bot will instantly read them */ private boolean automaticPresenceUpdates; @@ -289,7 +294,7 @@ public final class Store extends Controller { * All args constructor */ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public Store(UUID uuid, PhoneNumber phoneNumber, ControllerSerializer serializer, ClientType clientType, Collection alias, URI proxy, FutureReference version, boolean online, CountryLocale locale, String name, String businessAddress, Double businessLongitude, Double businessLatitude, String businessDescription, String businessWebsite, String businessEmail, BusinessCategory businessCategory, String deviceHash, LinkedHashMap linkedDevicesKeys, URI profilePicture, String about, Jid jid, Jid lid, ConcurrentHashMap properties, ConcurrentHashMap contacts, ConcurrentHashMap> status, ConcurrentHashMap newsletters, ConcurrentHashMap privacySettings, ConcurrentHashMap calls, boolean unarchiveChats, boolean twentyFourHourFormat, long initializationTimeStamp, ChatEphemeralTimer newChatsEphemeralTimer, TextPreviewSetting textPreviewSetting, WebHistoryLength historyLength, boolean autodetectListeners, boolean automaticPresenceUpdates, ReleaseChannel releaseChannel, CompanionDevice device, boolean checkPatchMacs) { + public Store(UUID uuid, PhoneNumber phoneNumber, ControllerSerializer serializer, ClientType clientType, Collection alias, URI proxy, FutureReference version, boolean online, CountryLocale locale, String name, String businessAddress, Double businessLongitude, Double businessLatitude, String businessDescription, String businessWebsite, String businessEmail, BusinessCategory businessCategory, String deviceHash, LinkedHashMap linkedDevicesKeys, URI profilePicture, String about, Jid jid, Jid lid, ConcurrentHashMap properties, ConcurrentHashMap contacts, ConcurrentHashMap> status, ConcurrentHashMap newsletters, ConcurrentHashMap privacySettings, ConcurrentHashMap calls, boolean unarchiveChats, boolean twentyFourHourFormat, long initializationTimeStamp, ChatEphemeralTimer newChatsEphemeralTimer, TextPreviewSetting textPreviewSetting, WebHistoryLength historyLength, boolean autodetectListeners, boolean cacheDetectedListeners, boolean automaticPresenceUpdates, ReleaseChannel releaseChannel, CompanionDevice device, boolean checkPatchMacs) { super(uuid, phoneNumber, serializer, clientType, alias); if (proxy != null) { ProxyAuthenticator.register(proxy); @@ -332,6 +337,7 @@ public Store(UUID uuid, PhoneNumber phoneNumber, ControllerSerializer serializer this.textPreviewSetting = textPreviewSetting; this.historyLength = historyLength; this.autodetectListeners = autodetectListeners; + this.cacheDetectedListeners = cacheDetectedListeners; this.automaticPresenceUpdates = automaticPresenceUpdates; this.releaseChannel = releaseChannel; this.device = device; @@ -1286,6 +1292,10 @@ public boolean autodetectListeners() { return this.autodetectListeners; } + public boolean cacheDetectedListeners() { + return cacheDetectedListeners; + } + public boolean automaticPresenceUpdates() { return this.automaticPresenceUpdates; } @@ -1412,6 +1422,10 @@ public Store setAutodetectListeners(boolean autodetectListeners) { return this; } + public void setCacheDetectedListeners(boolean cacheDetectedListeners) { + this.cacheDetectedListeners = cacheDetectedListeners; + } + public Store setAutomaticPresenceUpdates(boolean automaticPresenceUpdates) { this.automaticPresenceUpdates = automaticPresenceUpdates; return this; diff --git a/src/main/java/it/auties/whatsapp/controller/StoreKeysPair.java b/src/main/java/it/auties/whatsapp/controller/StoreKeysPair.java new file mode 100644 index 00000000..dfbee138 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/controller/StoreKeysPair.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.controller; + +import it.auties.whatsapp.util.Validate; + +import java.util.Objects; + +/** + * A pair of Store and Keys with the same uuid + * @param store + * @param keys + */ +public record StoreKeysPair(Store store, Keys keys) { + public StoreKeysPair { + Validate.isTrue(Objects.equals(store.uuid(), keys.uuid()), "UUID mismatch between store and keys"); + } +} diff --git a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java index eaa8682d..d3bdf115 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java @@ -38,7 +38,6 @@ import it.auties.whatsapp.model.sync.PatchType; import it.auties.whatsapp.model.sync.PrimaryFeature; import it.auties.whatsapp.util.Clock; -import it.auties.whatsapp.util.ControllerHelper; import java.net.SocketException; import java.net.URI; @@ -286,7 +285,8 @@ public CompletableFuture disconnect(DisconnectReason reason) { var number = store.phoneNumber() .map(PhoneNumber::number) .orElse(null); - var result = ControllerHelper.create(uuid, number, store.alias(), store.clientType(), store.serializer()); + var result = store.serializer() + .newStoreKeysPair(uuid, number, store.alias(), store.clientType()); this.keys = result.keys(); this.store = result.store(); store.addListeners(oldListeners); @@ -597,7 +597,7 @@ public CompletableFuture sendReceipt(Jid jid, Jid participant, List Objects.equals(type, "read") || Objects.equals(type, "read-self")) .put("to", jid) .put("type", type, Objects::nonNull); diff --git a/src/main/java/it/auties/whatsapp/util/ControllerHelper.java b/src/main/java/it/auties/whatsapp/util/ControllerHelper.java deleted file mode 100644 index ea0aaac0..00000000 --- a/src/main/java/it/auties/whatsapp/util/ControllerHelper.java +++ /dev/null @@ -1,158 +0,0 @@ -package it.auties.whatsapp.util; - -import it.auties.whatsapp.api.ClientType; -import it.auties.whatsapp.api.TextPreviewSetting; -import it.auties.whatsapp.api.WebHistoryLength; -import it.auties.whatsapp.controller.ControllerSerializer; -import it.auties.whatsapp.controller.Keys; -import it.auties.whatsapp.controller.Store; -import it.auties.whatsapp.model.chat.ChatEphemeralTimer; -import it.auties.whatsapp.model.companion.CompanionDevice; -import it.auties.whatsapp.model.jid.Jid; -import it.auties.whatsapp.model.mobile.PhoneNumber; -import it.auties.whatsapp.model.signal.auth.UserAgent; -import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; -import it.auties.whatsapp.model.signal.keypair.SignalSignedKeyPair; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -public class ControllerHelper { - public static Optional deserialize(UUID uuid, Long phoneNumber, String alias, ClientType clientType, ControllerSerializer serializer) { - if (uuid != null) { - var store = serializer.deserializeStore(clientType, uuid); - if(store.isEmpty()) { - return Optional.empty(); - } - - store.get().setSerializer(serializer); - serializer.attributeStore(store.get()); - var keys = serializer.deserializeKeys(clientType, uuid); - if(keys.isEmpty()) { - return Optional.empty(); - } - - keys.get().setSerializer(serializer); - return Optional.of(new StoreAndKeysPair(store.get(), keys.get())); - } - - if (phoneNumber != null) { - var store = serializer.deserializeStore(clientType, phoneNumber); - if(store.isEmpty()) { - return Optional.empty(); - } - - store.get().setSerializer(serializer); - serializer.attributeStore(store.get()); - var keys = serializer.deserializeKeys(clientType, phoneNumber); - if(keys.isEmpty()) { - return Optional.empty(); - } - - keys.get().setSerializer(serializer); - return Optional.of(new StoreAndKeysPair(store.get(), keys.get())); - } - - if (alias != null) { - var store = serializer.deserializeStore(clientType, alias); - if(store.isEmpty()) { - return Optional.empty(); - } - - store.get().setSerializer(serializer); - serializer.attributeStore(store.get()); - var keys = serializer.deserializeKeys(clientType, alias); - if(keys.isEmpty()) { - return Optional.empty(); - } - - keys.get().setSerializer(serializer); - return Optional.of(new StoreAndKeysPair(store.get(), keys.get())); - } - - return Optional.empty(); - } - - public static StoreAndKeysPair create(UUID uuid, Long phoneNumber, Collection alias, ClientType clientType, ControllerSerializer serializer) { - var parsedPhoneNumber = PhoneNumber.ofNullable(phoneNumber); - var store = new Store( - uuid, - parsedPhoneNumber.orElse(null), - serializer, - clientType, - alias, - null, - null, - false, - null, - Specification.Whatsapp.DEFAULT_NAME, - null, - null, - null, - null, - null, - null, - null, - null, - new LinkedHashMap<>(), - null, - null, - phoneNumber != null ? Jid.of(phoneNumber) : null, - null, - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - false, - false, - Clock.nowSeconds(), - ChatEphemeralTimer.OFF, - TextPreviewSetting.ENABLED_WITH_INFERENCE, - WebHistoryLength.standard(), - true, - true, - UserAgent.ReleaseChannel.RELEASE, - clientType == ClientType.WEB ? CompanionDevice.web() : CompanionDevice.ios(false), - false - ); - serializer.linkMetadata(store); - var registrationId = KeyHelper.registrationId(); - var identityKeyPair = SignalKeyPair.random(); - var keys = new Keys( - uuid, - parsedPhoneNumber.orElse(null), - serializer, - clientType, - alias, - registrationId, - SignalKeyPair.random(), - SignalKeyPair.random(), - identityKeyPair, - SignalKeyPair.random(), - SignalSignedKeyPair.of(registrationId, identityKeyPair), - null, - null, - new ArrayList<>(), - KeyHelper.phoneId(), - KeyHelper.deviceId(), - KeyHelper.identityId(), - null, - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - false, - false, - false - ); - serializer.serializeKeys(keys, true); - return new StoreAndKeysPair(store, keys); - } - - public record StoreAndKeysPair(Store store, Keys keys) { - - } -} diff --git a/src/main/java/it/auties/whatsapp/util/DefaultControllerSerializer.java b/src/main/java/it/auties/whatsapp/util/DefaultControllerSerializer.java index e4dc85b4..640316da 100644 --- a/src/main/java/it/auties/whatsapp/util/DefaultControllerSerializer.java +++ b/src/main/java/it/auties/whatsapp/util/DefaultControllerSerializer.java @@ -1,17 +1,21 @@ package it.auties.whatsapp.util; import it.auties.whatsapp.api.ClientType; -import it.auties.whatsapp.controller.Controller; -import it.auties.whatsapp.controller.ControllerSerializer; -import it.auties.whatsapp.controller.Keys; -import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.api.TextPreviewSetting; +import it.auties.whatsapp.api.WebHistoryLength; +import it.auties.whatsapp.controller.*; import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.chat.ChatBuilder; +import it.auties.whatsapp.model.chat.ChatEphemeralTimer; +import it.auties.whatsapp.model.companion.CompanionDevice; import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.mobile.PhoneNumber; import it.auties.whatsapp.model.newsletter.Newsletter; +import it.auties.whatsapp.model.signal.auth.UserAgent; +import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; +import it.auties.whatsapp.model.signal.keypair.SignalSignedKeyPair; import it.auties.whatsapp.model.sync.HistorySyncMessage; import java.io.IOException; @@ -69,6 +73,143 @@ private DefaultControllerSerializer(Path baseDirectory) { this.attributeStoreSerializers = new ConcurrentHashMap<>(); } + @Override + public StoreKeysPair newStoreKeysPair(UUID uuid, Long phoneNumber, Collection alias, ClientType clientType) { + var parsedPhoneNumber = PhoneNumber.ofNullable(phoneNumber); + var store = new Store( + uuid, + parsedPhoneNumber.orElse(null), + this, + clientType, + alias, + null, + null, + false, + null, + Specification.Whatsapp.DEFAULT_NAME, + null, + null, + null, + null, + null, + null, + null, + null, + new LinkedHashMap<>(), + null, + null, + phoneNumber != null ? Jid.of(phoneNumber) : null, + null, + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + false, + false, + Clock.nowSeconds(), + ChatEphemeralTimer.OFF, + TextPreviewSetting.ENABLED_WITH_INFERENCE, + WebHistoryLength.standard(), + true, + true, + true, + UserAgent.ReleaseChannel.RELEASE, + clientType == ClientType.WEB ? CompanionDevice.web() : CompanionDevice.ios(false), + false + ); + linkMetadata(store); + var registrationId = KeyHelper.registrationId(); + var identityKeyPair = SignalKeyPair.random(); + var keys = new Keys( + uuid, + parsedPhoneNumber.orElse(null), + this, + clientType, + alias, + registrationId, + SignalKeyPair.random(), + SignalKeyPair.random(), + identityKeyPair, + SignalKeyPair.random(), + SignalSignedKeyPair.of(registrationId, identityKeyPair), + null, + null, + new ArrayList<>(), + KeyHelper.phoneId(), + KeyHelper.deviceId(), + KeyHelper.identityId(), + null, + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + new ConcurrentHashMap<>(), + false, + false, + false + ); + serializeKeys(keys, true); + return new StoreKeysPair(store, keys); + } + + @Override + public Optional deserializeStoreKeysPair(UUID uuid, Long phoneNumber, String alias, ClientType clientType) { + if (uuid != null) { + var store = deserializeStore(clientType, uuid); + if(store.isEmpty()) { + return Optional.empty(); + } + + store.get().setSerializer(this); + attributeStore(store.get()); + var keys = deserializeKeys(clientType, uuid); + if(keys.isEmpty()) { + return Optional.empty(); + } + + keys.get().setSerializer(this); + return Optional.of(new StoreKeysPair(store.get(), keys.get())); + } + + if (phoneNumber != null) { + var store = deserializeStore(clientType, phoneNumber); + if(store.isEmpty()) { + return Optional.empty(); + } + + store.get().setSerializer(this); + attributeStore(store.get()); + var keys = deserializeKeys(clientType, phoneNumber); + if(keys.isEmpty()) { + return Optional.empty(); + } + + keys.get().setSerializer(this); + return Optional.of(new StoreKeysPair(store.get(), keys.get())); + } + + if (alias != null) { + var store = deserializeStore(clientType, alias); + if(store.isEmpty()) { + return Optional.empty(); + } + + store.get().setSerializer(this); + attributeStore(store.get()); + var keys = deserializeKeys(clientType, alias); + if(keys.isEmpty()) { + return Optional.empty(); + } + + keys.get().setSerializer(this); + return Optional.of(new StoreKeysPair(store.get(), keys.get())); + } + + return Optional.empty(); + } + @Override public LinkedList listIds(ClientType type) { if (cachedUuids != null) { diff --git a/src/main/java/it/auties/whatsapp/util/ListenerScanner.java b/src/main/java/it/auties/whatsapp/util/ListenerScanner.java index a35c7d4b..4b5d48b4 100644 --- a/src/main/java/it/auties/whatsapp/util/ListenerScanner.java +++ b/src/main/java/it/auties/whatsapp/util/ListenerScanner.java @@ -11,22 +11,30 @@ import java.util.NoSuchElementException; public final class ListenerScanner { - private static final List> listeners; + private static final List> cache; static { + cache = loadListeners(); + } + + private static List> loadListeners() { try (var scanner = createScanner()) { - listeners = scanner.getClassesWithAnnotation(RegisterListener.class).loadClasses(); + return scanner.getClassesWithAnnotation(RegisterListener.class).loadClasses(); } } - public static List scan(Whatsapp whatsapp) { - return listeners.stream() + public static List scan(Whatsapp whatsapp, boolean useCache) { + var listeners = useCache ? cache : loadListeners(); + return cache.stream() .map(listener -> initialize(listener, whatsapp)) .toList(); } private static ScanResult createScanner() { - return new ClassGraph().enableClassInfo().enableAnnotationInfo().scan(); + return new ClassGraph() + .enableClassInfo() + .enableAnnotationInfo() + .scan(); } private static Listener initialize(Class listener, Whatsapp whatsapp) {