package im.dlg.botsdk.internal; import com.google.common.collect.Sets; import com.google.common.util.concurrent.ListenableFuture; import com.google.protobuf.Empty; import com.google.protobuf.GeneratedMessageV3; import com.google.protobuf.StringValue; import dialog.*; import dialog.MessagingOuterClass.Dialog; import dialog.MessagingOuterClass.HistoryMessage; import dialog.MessagingOuterClass.ListLoadMode; import dialog.MessagingOuterClass.RequestLoadDialogs; import im.dlg.botsdk.BotConfig; import im.dlg.botsdk.BotCredentials; import im.dlg.botsdk.model.Message; import im.dlg.botsdk.model.content.Content; import im.dlg.botsdk.listeners.UpdateListener; import im.dlg.botsdk.retry.TaskManager; import im.dlg.botsdk.utils.*; import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.stub.AbstractStub; import io.grpc.stub.MetadataUtils; import io.grpc.stub.StreamObserver; import net.javacrumbs.futureconverter.java8guava.FutureConverter; import org.apache.commons.lang3.tuple.ImmutablePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Field; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import static dialog.AuthenticationOuterClass.*; import static dialog.Peers.*; import static dialog.RegistrationOuterClass.*; import static dialog.SequenceAndUpdatesOuterClass.*; public class InternalBot { public static final long RECONNECT_DELAY = 1000; private static final Integer APP_ID = 11011; private final Logger log = LoggerFactory.getLogger(InternalBot.class); private final Map<String, OutPeer> outPeerMap = new ConcurrentHashMap<>(); private final Map<Class, List<UpdateListener>> subscribers = new ConcurrentHashMap<>(); private final AtomicInteger currentSequence = new AtomicInteger(); private final InternalBotStream stream = new InternalBotStream(this, currentSequence); private final ManagedChannel channel; private final BotConfig config; private final boolean anonymousAuth; private volatile Metadata metadata; public InternalBot(ManagedChannel channel, BotConfig config, boolean anonymousAuth) { this.channel = channel; this.config = config; this.anonymousAuth = anonymousAuth; } public CompletableFuture<Void> start() { CompletableFuture<Metadata> meta = new CompletableFuture<>(); RequestRegisterDevice request = RequestRegisterDevice.newBuilder() .setAppId(APP_ID) .setAppTitle(config.getName()) .setDeviceTitle(config.getName()) .build(); return FutureConverter.toCompletableFuture( RegistrationGrpc.newFutureStub(channel).registerDevice(request) ).whenComplete((res, t) -> { if (res == null) { meta.completeExceptionally(t); return; } Metadata header = new Metadata(); Metadata.Key<String> key = Metadata.Key.of("x-auth-ticket", Metadata.ASCII_STRING_MARSHALLER); header.put(key, res.getToken()); meta.complete(header); metadata = header; log.info("Bot registered with token = {}", res.getToken()); }).thenCompose(res -> meta).thenCompose(m -> { if (anonymousAuth) { return FutureConverter.toCompletableFuture(withToken(m, AuthenticationGrpc.newFutureStub(channel), stub -> stub.startAnonymousAuth( RequestStartAnonymousAuth.newBuilder() .setApiKey("BotSdk") .setAppId(APP_ID) .setDeviceTitle(config.getName()) .addPreferredLanguages("RU") .setTimeZone(StringValue.newBuilder().setValue("+3").build()) .build() ) )).thenApply(res -> new ImmutablePair<>(res.getUser(), m)); } else if (config.getCredentials() != null) { BotCredentials credentials = config.getCredentials(); switch (credentials.getMethod()) { case TOKEN: return FutureConverter.toCompletableFuture(withToken(m, AuthenticationGrpc.newFutureStub(channel), stub -> stub.startTokenAuth( RequestStartTokenAuth.newBuilder() .setApiKey("BotSdk") .setAppId(APP_ID) .setDeviceTitle(config.getName()) .addPreferredLanguages("RU") .setTimeZone(StringValue.newBuilder().setValue("+3").build()) .setToken(credentials.getValue()) .build() ) )).thenApply(res -> new ImmutablePair<>(res.getUser(), m)); case PASSWORD: return FutureConverter.toCompletableFuture(withToken(m, AuthenticationGrpc.newFutureStub(channel), stub -> stub.startUsernameAuth( RequestStartUsernameAuth.newBuilder() .setUsername(config.getName()) .setApiKey("BotSdk") .setAppId(APP_ID) .setDeviceTitle(config.getName()) .addPreferredLanguages("RU") .setTimeZone(StringValue.newBuilder().setValue("+3").build()) .build() ))) .thenCompose(res -> FutureConverter.toCompletableFuture(withToken(m, AuthenticationGrpc.newFutureStub(channel), stub -> stub.validatePassword( RequestValidatePassword.newBuilder() .setTransactionHash(res.getTransactionHash()) .setPassword(credentials.getValue()) .build() )))) .thenApply(res -> new ImmutablePair<>(res.getUser(), m)); default: return null; } } else { return null; } }).thenApply(p -> { withToken(p.getRight(), SequenceAndUpdatesGrpc.newStub(channel), stub -> { stub.seqUpdates(Empty.newBuilder().build(), stream); return null; } ); log.info("Bot authorized with id = {}", p.getLeft().getId()); return null; }); } public void reconnect() { withToken(metadata, SequenceAndUpdatesGrpc.newStub(channel), stub -> { stub.seqUpdates(Empty.newBuilder().build(), stream); return new Object(); }); } public <T extends AbstractStub<T>, R> CompletableFuture<R> withToken(T stub, Function<T, ListenableFuture<R>> f) { T newStub = MetadataUtils.attachHeaders(stub, metadata); TaskManager<R> task = new TaskManager<>(FutureConverter.toCompletableFuture(f.apply(newStub)), config.getRetryOptions()); return task.scheduleTask(0); } public <T extends AbstractStub<T>, R> R withToken(Metadata meta, T stub, Function<T, R> f) { T newStub = MetadataUtils.attachHeaders(stub, meta); return f.apply(newStub); } public <T extends AbstractStub<T>, R> StreamObserver<R> withObserverToken(T stub, Function<T, StreamObserver<R>> f) { T newStub = MetadataUtils.attachHeaders(stub, metadata); return f.apply(newStub); } public CompletableFuture<Optional<OutPeer>> findOutPeer(Peer peer) { String peerHash = PeerUtils.peerHasher(peer); OutPeer outPeer = outPeerMap.get(peerHash); if (outPeer != null) { return CompletableFuture.completedFuture(Optional.of(outPeer)); } // Implicitly load outPeers of loaded dialogs return loadDialogs(Sets.newHashSet(peer)) .thenApply(x -> outPeerMap.containsKey(peerHash) ? Optional.of(outPeerMap.get(peerHash)) : Optional.empty()); } public CompletableFuture<Optional<OutPeer>> loadSenderOutPeer(Integer senderId, OutPeer peer, long date) { Peer senderPeer = PeerUtils.toUserPeer(senderId); String peerHash = PeerUtils.peerHasher(senderPeer); return outPeerMap.containsKey(peerHash) ? CompletableFuture.completedFuture(Optional.of(outPeerMap.get(peerHash))) : // implicitly load outPeers of loaded dialogs load(peer, date, 2).thenApply(x -> outPeerMap.containsKey(peerHash) ? Optional.of(outPeerMap.get(peerHash)) : Optional.empty() ); } public CompletableFuture<Optional<OutPeer>> findUserOutPeer(int userId) { return findOutPeer(PeerUtils.toUserPeer(userId)); } public CompletableFuture<ResponseGetReferencedEntitites> getRefEntities(Collection<UserOutPeer> userOutPeers, Collection<GroupOutPeer> groupOutPeers) { RequestGetReferencedEntitites.Builder requestBuilder = RequestGetReferencedEntitites.newBuilder(); if (userOutPeers != null) { requestBuilder.addAllUsers(userOutPeers); } if (groupOutPeers != null) { requestBuilder.addAllGroups(groupOutPeers); } return withToken(SequenceAndUpdatesGrpc.newFutureStub(channel), stub -> stub.getReferencedEntitites(requestBuilder.build())); } private void putOutPeer(OutPeer outPeer) { outPeerMap.put(PeerUtils.peerHasher(PeerUtils.toPeer(outPeer)), outPeer); } private void putOutPeer(UserOutPeer outPeer) { outPeerMap.put(PeerUtils.peerHasher(PeerUtils.toPeer(outPeer)), PeerUtils.toOutPeer(outPeer)); } private void putOutPeer(GroupOutPeer outPeer) { outPeerMap.put(PeerUtils.peerHasher(PeerUtils.toPeer(outPeer)), PeerUtils.toOutPeer(outPeer)); } public CompletableFuture<List<Message>> load(OutPeer peer, long from, Integer limit) { return withToken( MessagingGrpc.newFutureStub(channel), stub -> stub.loadHistory(MessagingOuterClass.RequestLoadHistory.newBuilder() .setDate(from) .setLimit(limit) .setLoadMode(ListLoadMode.LISTLOADMODE_FORWARD) .addAllOptimizations(Constants.OPTIMISATIONS) .setPeer(peer) .build()) ).thenApply(res -> { res.getGroupPeersList().forEach(this::putOutPeer); res.getUserPeersList().forEach(this::putOutPeer); List<HistoryMessage> historyList = res.getHistoryList(); List<Message> messages = new ArrayList<>(); for (HistoryMessage hm : historyList) { putOutPeer(hm.getSenderPeer()); messages.add(new Message( PeerUtils.toDomainPeer(peer), PeerUtils.toDomainPeer(hm.getSenderPeer()), UUIDUtils.convert(hm.getMid()), hm.getMessage().getTextMessage().getText(), hm.getDate(), Content.fromMessage(hm.getMessage()) )); } return messages; }); } CompletableFuture<List<Dialog>> loadDialogs(Set<Peer> peers) { RequestLoadDialogs request = RequestLoadDialogs.newBuilder() .setLimit(1) .addAllPeersToLoad(peers) .addAllOptimizations(Constants.OPTIMISATIONS) .build(); return withToken( MessagingGrpc.newFutureStub(channel), stub -> stub.loadDialogs(request) ).thenApply(res -> { res.getGroupPeersList().forEach(this::putOutPeer); res.getUserPeersList().forEach(this::putOutPeer); return res.getDialogsList(); }); } public synchronized <T extends GeneratedMessageV3> void subscribeOn(Class<T> clazz, UpdateListener<T> listener) { subscribers.computeIfAbsent(clazz, s -> new CopyOnWriteArrayList<>()); subscribers.get(clazz).add(listener); } public int getCurrentSequence() { return currentSequence.get(); } public CompletableFuture<Integer> getDifference(int seq) { RequestGetDifference request = RequestGetDifference.newBuilder().setSeq(seq).build(); return withToken(SequenceAndUpdatesGrpc.newFutureStub(channel) .withDeadlineAfter(2, TimeUnit.MINUTES), stub -> stub.getDifference(request)) .thenCompose(res -> { for (UpdateSeqUpdate update : res.getUpdatesList()) { callListeners(update); } int lastSeq = res.getSeq(); this.currentSequence.set(lastSeq); if (res.getNeedMore()) { return getDifference(lastSeq); } else { return CompletableFuture.completedFuture(lastSeq); } }); } void callListeners(UpdateSeqUpdate upd) { Object updateRaw = null; try { Field f = upd.getClass().getDeclaredField("update_"); f.setAccessible(true); updateRaw = f.get(upd); } catch (Exception e) { log.error("Failed to get sequence update field", e); } if (!(updateRaw instanceof GeneratedMessageV3)) { return; } GeneratedMessageV3 up = (GeneratedMessageV3) updateRaw; for (Map.Entry<Class, List<UpdateListener>> entry : subscribers.entrySet()) { if (updateRaw.getClass().isAssignableFrom(entry.getKey())) { entry.getValue().forEach(listener -> listener.onUpdate(up)); } } } }