public async void EnsureLongPollingEndpointWithPoliciesAsync(TeamsDataContext ctx, Action <List <IChatMessage> > onChatChanged, CancellationToken cancellationToken)
        {
            var endpoint = GetEndpoint(ctx);

            if (endpoint?.IsValid() ?? false)
            {
                // keep valid endpoint running
                return;
            }

            logger.Debug("[{TenantName}] EnsureLongPollingEndpointWithPoliciesAsync called, entering wait loop", ctx.Tenant.TenantName);
            while (!cancellationToken.IsCancellationRequested)
            {
                var retryPolicy = Policy.Handle <Exception>().WaitAndRetryForeverAsync((retryAttempt) =>
                {
                    var waitTimeSec = Math.Min(Math.Pow(2, retryAttempt - 1), 30 * 60);
                    logger.Debug("[{TenantName}] Calculated retry time for retry attempt #{RetryAttempty}: {Seconds} seconds", ctx.Tenant.TenantName, retryAttempt, waitTimeSec);
                    return(TimeSpan.FromSeconds(waitTimeSec));
                });

                await retryPolicy.ExecuteAsync(async (cancellationToken) =>
                {
                    // this will never return except there is an exception
                    await EnsureLongPollingEndpointAndReplaceExistingAsync(ctx, onChatChanged, cancellationToken);
                }, cancellationToken);
            }
        }
        private void EnsureLongPollingEndpointsAreUpAndRunning(TeamsDataContext ctx)
        {
            if (config.TenantIdsToNotSubscribeForNotifications != null && config.TenantIdsToNotSubscribeForNotifications.Contains(ctx.Tenant.TenantId, StringComparer.InvariantCultureIgnoreCase))
            {
                logger.Debug("[{TenantName}] Skipping subscription for events for tenant since it is included in the ignore list.", ctx.Tenant.TenantName);
                return;
            }

            longPollingRegistry.EnsureLongPollingEndpointWithPoliciesAsync(ctx,
                                                                           chatMessages =>
            {
                _ = Task.Run(async() =>
                {
                    logger.Debug("[{TenantName}] Retrieved {0} chat messages as notification from {@From}", ctx.Tenant.TenantName, chatMessages.Count, chatMessages.Select(m => m.From.Select(u => u.DisplayName)));
                    var someFailed = false;
                    foreach (var message in chatMessages)
                    {
                        var result = await chatRegistry.StoreSingleChatMessageAsync(ctx, message);
                        if (!result)
                        {
                            someFailed = true;
                        }
                    }
                    if (someFailed)
                    {
                        logger.Debug("[{TenantName}] Received notifications for chats yet unknown. Queueing chat retrieval.", ctx.Tenant.TenantName);
                        // this will retrieve new chats and create the corresponding folders
                        internalChatRetrievalRequests.OnNext(ctx);
                    }
                });
            }, default);
        }
Exemple #3
0
 public void Initialize()
 {
     fakeContext = new TeamsDataContext((TeamsParticipant)"00000000-0000-beef-0000-000000000000", new ProcessedTenant(new Tenant()
     {
         tenantId = "00000000-0000-feeb-0000-000000000000", userId = "00000000-0000-beef-0000-000000000000", tenantName = "Fake Tenant"
     }, DateTime.UtcNow));
 }
Exemple #4
0
 public void Initialize()
 {
     fakeContext = new TeamsDataContext((TeamsParticipant)"test_fakemainuser", new ProcessedTenant(new Tenant()
     {
         tenantId = "Tenant ID", userId = "User ID", tenantName = "Fake User"
     }, DateTime.UtcNow));
 }
        public void TestQueueOperations()
        {
            var chatsToRetrieve = new SimplePriorityQueue <ChatQueueItem, HigherVersionWinsComparerChat>(new LowerVersionWinsChatComparer());

            var dummyContext = new TeamsDataContext((TeamsParticipant)"00000000-0000-beef-0000-000000000000", new ProcessedTenant(new Tenant(), DateTime.UtcNow));

            var chatLowVersion = new HigherVersionWinsComparerChat(new Chat()
            {
                version = 5, id = "1"
            });
            var chatMediumVersion = new HigherVersionWinsComparerChat(new Chat()
            {
                version = 50, id = "2"
            });
            var chatHighVersion = new HigherVersionWinsComparerChat(new Chat()
            {
                version = 500, id = "3"
            });

            chatsToRetrieve.Enqueue(new ChatQueueItem(dummyContext, chatLowVersion), chatLowVersion);
            chatsToRetrieve.Enqueue(new ChatQueueItem(dummyContext, chatMediumVersion), chatMediumVersion);
            chatsToRetrieve.Enqueue(new ChatQueueItem(dummyContext, chatHighVersion), chatHighVersion);

            var isRemoved = chatsToRetrieve.TryRemove(new ChatQueueItem(dummyContext, chatMediumVersion));

            Assert.IsTrue(isRemoved);
            isRemoved = chatsToRetrieve.TryRemove(new ChatQueueItem(dummyContext, chatMediumVersion));
            Assert.IsFalse(isRemoved);
        }
 private (ChatQueueItem?, int) GetNextChatToProcess(TeamsDataContext ctx)
 {
     if (chatsToRetrieve.TryGetValue(ctx, out var queue))
     {
         queue.TryDequeue(out ChatQueueItem? result);
         return(result, queue.Count);
     }
     return(null, 0);
 }
Exemple #7
0
 public async Task <ProcessedChat?> GetStoredProcessedChatAsync(TeamsDataContext ctx, bool forceChatIndexUpdate, string chatId)
 {
     logger.Debug("[{TenantName}] GetChatMetadataAsync for chat {ID} | {Context}", ctx.Tenant.TenantName, chatId.Truncate(Constants.ChatIdLogLength, true), ctx);
     return(await BlobCache.UserAccount
            .GetOrFetchObject(
                GetChatMetadataCacheKey(chatId),
                () => chatStore.GetChatMetadataAsync(ctx, forceChatIndexUpdate, chatId),
                DateTimeOffset.Now + TimeSpan.FromMinutes(SingleChatCacheLifetimeMins)
                ));
 }
        public async Task <ProcessedChat> ProcessChatMessagesAsync(TeamsDataContext ctx, Chat chat, IEnumerable <Message> messages)
        {
            var result = new ProcessedChat(chat);
            // like 00000000-0000-beef-0000-000000000000
            var userId = ctx.Tenant.UserId;
            // from oldest to newest
            var orderedMessages = messages.OrderBy(m => m.originalarrivaltime);
            // TODO: combine this with the conversion logic of processedMessageFactory.CreateProcessedMessage().InitFromMessageAsync -> extract users first to have names ready, then generate processed messages
            HashSet <TeamsUserWithSource> usersFromChat;

            usersFromChat = CollectUsersFromChatAndMessages(chat, orderedMessages.ToList());
            result.UserIds.AddRange(usersFromChat.Select(value => value.User).Distinct()); // TODO: check if this is still necessary of if we use TeamsUserStore instead
            await UpdateUserObjectsAsync(ctx, result.UserIds);

            // note: it is important to process them from oldest to newest to catch all user names floating around in messages (e.g. chat messages contain user display names needed to put names in call end messages)
            // note2: this does not always work which is why there is the job to resolve unknown user ids
            result.OrderedMessages = (await Task.WhenAll(orderedMessages.Select(async m => await processedMessageFactory.CreateProcessedMessage().InitFromMessageAsync(ctx, chat.id, m)))).OrderBy(m => m.OriginalArrivalTime);

            // case 1: there is a custom title
            var title = chat.title?.Trim() ?? "";

            // this handles a title containing user mris (instead of custom title set by the user)
            var matches = Regex.Matches(title, TeamsParticipant.MriPatternOpen);

            // case 2: Teams provides a title consisting of user ids - collect them
            if (matches.Count > 0)
            {
                usersFromChat.AddRange(matches.Select(g => new TeamsUserWithSource((TeamsParticipant)g.Value, TeamsUserSource.FoundInChatTitle)));
            }

            // remove self and remove any "chat" that participated
            usersFromChat.RemoveWhere(value => value.User.Equals(ctx.Tenant.UserId) || value.User.Kind == ParticipantKind.TeamsChat);
            if (usersFromChat.Count == 0)
            {
                // no users left? add self...
                usersFromChat.Add(new TeamsUserWithSource(ctx.Tenant.UserId, TeamsUserSource.Self));
            }

            if (string.IsNullOrWhiteSpace(title))
            {
                title = string.Join(", ", usersFromChat
                                    .Where(value => value.UserSource == TeamsUserSource.ChatCreator || value.UserSource == TeamsUserSource.FoundInChatTitle || value.UserSource == TeamsUserSource.OfficialChatMember || value.UserSource == TeamsUserSource.SenderOfMessageInChat || value.UserSource == TeamsUserSource.Self)
                                    .Select(value => value.User)
                                    .Distinct());
            }

            title = await teamsUserRegistry.ReplaceUserIdsWithDisplayNamesAsync(ctx, title?.Trim());

            if (string.IsNullOrEmpty(title))
            {
                title = chat.id;
            }
            result.ChatTitle = title;
            return(result);
        }
        public TeamsLongPollingEndpoint?GetEndpoint(TeamsDataContext ctx)
        {
            lock (knownEndpoints)
            {
                if (knownEndpoints.TryGetValue(ctx, out var result))
                {
                    return(result);
                }
            }

            return(null);
        }
        public void TestChatPriorityQueue()
        {
            var chatsToRetrieve = new SimplePriorityQueue <(TeamsDataContext, HigherVersionWinsComparerChat), HigherVersionWinsComparerChat>(new LowerVersionWinsChatComparer());

            var dummyContext   = new TeamsDataContext();
            var chatLowVersion = new HigherVersionWinsComparerChat(new Chat()
            {
                version = 5
            });
            var chatMediumVersion = new HigherVersionWinsComparerChat(new Chat()
            {
                version = 50
            });
            var chatHighVersion = new HigherVersionWinsComparerChat(new Chat()
            {
                version = 500
            });

            // note: last message version _always_ comes before any "normal" thread version
            var chatWithLowestLastMessageVersion = new HigherVersionWinsComparerChat(new Chat()
            {
                version = 5000, LastMessage = new Message()
                {
                    version = "1"
                }
            });
            var chatWithHighestLastMessageVersion = new HigherVersionWinsComparerChat(new Chat()
            {
                version = 1, LastMessage = new Message()
                {
                    version = "50000"
                }
            });

            chatsToRetrieve.Enqueue((dummyContext, chatLowVersion), chatLowVersion);
            chatsToRetrieve.Enqueue((dummyContext, chatHighVersion), chatHighVersion);
            chatsToRetrieve.Enqueue((dummyContext, chatMediumVersion), chatMediumVersion);
            chatsToRetrieve.Enqueue((dummyContext, chatWithLowestLastMessageVersion), chatWithLowestLastMessageVersion);
            chatsToRetrieve.Enqueue((dummyContext, chatWithHighestLastMessageVersion), chatWithHighestLastMessageVersion);

            (TeamsDataContext, HigherVersionWinsComparerChat)chat;

            chat = chatsToRetrieve.Dequeue();
            Assert.AreEqual(1, chat.Item2.Chat.version);
            chat = chatsToRetrieve.Dequeue();
            Assert.AreEqual(5000, chat.Item2.Chat.version);
            chat = chatsToRetrieve.Dequeue();
            Assert.AreEqual(500, chat.Item2.Chat.version);
            chat = chatsToRetrieve.Dequeue();
            Assert.AreEqual(50, chat.Item2.Chat.version);
            chat = chatsToRetrieve.Dequeue();
            Assert.AreEqual(5, chat.Item2.Chat.version);
        }
 public void RemoveEndpointIfExisting(TeamsDataContext ctx)
 {
     lock (knownEndpoints)
     {
         var endpoint = GetEndpoint(ctx);
         if (endpoint != null)
         {
             logger.Debug("[{TenantName}] Removing long poll endpoint {EndpointId}", ctx.Tenant.TenantName, endpoint.Id.Truncate(Constants.UserIdLogLength, true));
             knownEndpoints.Remove(ctx);
             endpoint.DeleteAsync(ctx).Wait();
         }
     }
 }
Exemple #12
0
        public async Task <MyChatsAndTeams?> GetUpdatedChatsAndTeamsAsync(TeamsDataContext ctx)
        {
            logger.Debug("[{TenantName}] {Method}: Starting to update chats", ctx.Tenant.TenantName, nameof(GetUpdatedChatsAndTeamsAsync));
            var toBeUpdatedChatsAndTeams = await GetAllChatsAndTeamsAsync(ctx);

            if (toBeUpdatedChatsAndTeams == null)
            {
                logger.Debug("[{TenantName}] {Method}: Initial list of chats is null; leaving", ctx.Tenant.TenantName, nameof(GetUpdatedChatsAndTeamsAsync));
                // error while retrieving all data? cannot retrieve update either
                return(null);
            }

            if (string.IsNullOrWhiteSpace(toBeUpdatedChatsAndTeams.metadata?.syncToken))
            {
                logger.Debug("[{TenantName}] {Method}: Initial list of chats contains no sync token but we need one to update; leaving", ctx.Tenant.TenantName, nameof(GetUpdatedChatsAndTeamsAsync));
                // cannot get update if there is no sync token
                return(toBeUpdatedChatsAndTeams);
            }

            var deltaChatsAndTeams = await teamsTenantApiAccessor.GetMyChatsAndTeamsAsync(ctx, toBeUpdatedChatsAndTeams.metadata.syncToken);

            if (deltaChatsAndTeams == null)
            {
                logger.Debug("[{TenantName}] {Method}: Got null as delta result; leaving", ctx.Tenant.TenantName, nameof(GetUpdatedChatsAndTeamsAsync));
                return(toBeUpdatedChatsAndTeams);
            }
            logger.Debug("[{TenantName}] {Method}: {ChatCount} chats are new or updated with old sync token {SyncToken}", ctx.Tenant.TenantName, nameof(GetUpdatedChatsAndTeamsAsync), toBeUpdatedChatsAndTeams.chats.Count, toBeUpdatedChatsAndTeams.metadata.syncToken.FromBase64String());

            Experimental.CheckForActiveMeeting(logger, ctx, deltaChatsAndTeams);

            var oldChatsToReplace = toBeUpdatedChatsAndTeams.chats.Where(oldValue => deltaChatsAndTeams.chats.FirstOrDefault(updatedValue => updatedValue?.id == oldValue?.id) != default);

            logger.Debug("[{TenantName}] {Method}: {ChatCount} old chats need an update", ctx.Tenant.TenantName, nameof(GetUpdatedChatsAndTeamsAsync), oldChatsToReplace.Count());
            foreach (var oldChat in oldChatsToReplace.ToList())
            {
                toBeUpdatedChatsAndTeams.chats.Remove(oldChat);
            }
            toBeUpdatedChatsAndTeams.chats.AddRange(deltaChatsAndTeams.chats);
            teamsTenantApiAccessor.SortChats(toBeUpdatedChatsAndTeams);

            logger.Debug("[{TenantName}] {Method}: Updating sync token to {SyncToken}", ctx.Tenant.TenantName, nameof(GetUpdatedChatsAndTeamsAsync), deltaChatsAndTeams.metadata.syncToken.FromBase64String());
            toBeUpdatedChatsAndTeams.metadata.syncToken = deltaChatsAndTeams.metadata.syncToken;
            await BlobCache.UserAccount.InsertObject($"ts.{ctx.Tenant.UserId}.chatsAndTeams", toBeUpdatedChatsAndTeams, DateTimeOffset.Now + TimeSpan.FromDays(ChatsAndTeamsCacheLifetimeDays));

            logger.Debug("[{TenantName}] {Method}: Done", ctx.Tenant.TenantName, nameof(GetUpdatedChatsAndTeamsAsync));
            return(toBeUpdatedChatsAndTeams);
        }
        public async Task <TeamsLongPollingEndpoint?> GetExistingEndpoint(TeamsDataContext ctx, string endpointId)
        {
            logger.Debug("[{TenantName}] Entering GetExistingEndpoint to get long poll endpoint status", ctx.Tenant.TenantName);
            var userId       = ctx.Tenant.UserId;
            var tokenContext = tokenRetriever.GetOrCreateUserTokenContext(userId);
            var tokenInfo    = tokenContext[TeamsTokenType.MyChatsAuthHeader];

            if (tokenInfo == null || !tokenInfo.IsValid())
            {
                logger.Debug("[{TenantName}] Exiting GetExistingEndpoint because no token found or already expired", ctx.Tenant.TenantName);
                return(null);
            }

            var client = Utils.CreateHttpClient();

            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Add("x-ms-client-type", "web");
            client.DefaultRequestHeaders.Add("Authentication", tokenInfo.AuthHeader);
            client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.57");
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.DefaultRequestHeaders.Add("Referer", "https://teams.microsoft.com/");
            client.DefaultRequestHeaders.Add("Origin", "https://teams.microsoft.com");
            client.DefaultRequestHeaders.Add("ClientInfo", "os=windows; osVer=10; proc=x86; lcid=de-de; deviceType=1; country=de; clientName=skypeteams; utcOffset=+01:00; timezone=Europe/Berlin");
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));
            client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("de"));

            var url = $"{tokenContext?.ChatServiceUrl}/v2/users/ME/endpoints/{endpointId}";
            var messagesHttpResult = await client.GetAsync(url);

            if (messagesHttpResult.IsSuccessStatusCode)
            {
                var buffer = await messagesHttpResult.Content.ReadAsByteArrayAsync();

                var data   = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
                var result = JsonUtils.DeserializeObject <RegisterEndpoint_ResponseBody>(logger, data);
                logger.Debug("[{TenantName}] Successfully got long poll endpoint info: {EndpointId}", ctx.Tenant.TenantName, result.id.Truncate(Constants.UserIdLogLength, true));
                return(new TeamsLongPollingEndpoint(logger, tokenRetriever, processedNotificationMessageFactory, this, result));
            }
            else
            {
                logger.Debug("[{TenantName}] Got non-success status code for long poll endpoint status retrieval; complete result: \r\n{@messagesHttpResult}", ctx.Tenant.TenantName, messagesHttpResult);
            }

            return(null);
        }
        public override async Task <IChatMessage> InitFromMessageAsync <T>(TeamsDataContext ctx, string chatId, T message)
        {
            if (message is not Resource m)
            {
                throw new ArgumentException($"Cannot init {nameof(ProcessedMessage)} from type {message.GetType()}", nameof(message));
            }

            notificationResource = m;
            this.ctx             = ctx;
            Messagetype          = m.messagetype;
            Id     = m.id; // note: this is the same ID as the one of the "real" message retrieved via the messages endpoint
            ChatId = m.to;
            OriginalArrivalTime = m.originalarrivaltime ?? Utils.JavaScriptUtcMsToDateTime(long.Parse(m.version ?? "0"));

            await ExtractSendersReceiversAndSubject(chatId);
            await GenerateTextContentExtractUsersAndUpdateSubject();

            ReplaceImageUrlsByContentIds();
            return(this);
        }
Exemple #15
0
        public async Task TestResolveParticipantPlaceholders()
        {
            using var kernel = new FakeItEasyMockingKernel();
            kernel.Rebind <ILogger>().ToConstant(Log.Logger);
            kernel.Rebind <ITeamsUserStore>().ToConstant(A.Fake <ITeamsUserStore>()).InSingletonScope();
            kernel.Rebind <ITeamsUserRegistry>().To <TeamsUserRegistry>().InSingletonScope();
            var userRegistry = kernel.Get <ITeamsUserRegistry>();
            var logger       = kernel.Get <ILogger>();

            var fakeContext = new TeamsDataContext((TeamsParticipant)"8:orgid:00000000-0000-beef-0000-000000000000", new ProcessedTenant(new Tenant()
            {
                tenantId = "Tenant ID", userId = "8:orgid:00000000-0000-beef-0000-000000000000", tenantName = "Fake User"
            }, DateTime.UtcNow));
            await userRegistry.RegisterDisplayNameForUserIdAsync(fakeContext, (TeamsParticipant)"12300000-0000-beef-0000-000000000000", "Test Name", DateTime.UtcNow);

            var message = A.Fake <IMutableChatMessage>();

            message.MessageSubject = "User {{12300000-0000-beef-0000-000000000000}} added: User {{12300000-0000-beef-0000-000000000000}}, Heinrich Ulbricht";
            await TeamsChatRetriever.ResolveParticipantPlaceholders(logger, userRegistry, fakeContext, message);

            Assert.AreEqual("Test Name added: Test Name, Heinrich Ulbricht", message.MessageSubject);
        }
Exemple #16
0
        public async Task TestBotAndChatReplacement()
        {
            using var kernel = new FakeItEasyMockingKernel();
            kernel.Rebind <ILogger>().ToConstant(Log.Logger);
            kernel.Rebind <ITeamsUserStore>().ToConstant(A.Fake <ITeamsUserStore>()).InSingletonScope();
            kernel.Rebind <ITeamsUserRegistry>().To <TeamsUserRegistry>().InSingletonScope();
            var userRegistry = kernel.Get <ITeamsUserRegistry>();

            var fakeContext = new TeamsDataContext((TeamsParticipant)"8:orgid:00000000-0000-beef-0000-000000000000", new ProcessedTenant(new Tenant()
            {
                tenantId = "Tenant ID", userId = "8:orgid:00000000-0000-beef-0000-000000000000", tenantName = "Fake User"
            }, DateTime.UtcNow));

            await userRegistry.RecognizeUserIdAsync(fakeContext, (TeamsParticipant)"817c2506-de4a-4795-971e-371ea75a03ed");                                                        // polly

            await userRegistry.RecognizeUserIdAsync(fakeContext, (TeamsParticipant)"19:00000000-0000-beef-0000-000000000000_00000000-0000-beef-0000-000000000000@unq.gbl.spaces"); // a chat

            string s;

            s = await userRegistry.ReplaceUserIdsWithDisplayNamesAsync(fakeContext, "817c2506-de4a-4795-971e-371ea75a03ed, 19:00000000-0000-beef-0000-000000000000_00000000-0000-beef-0000-000000000000@unq.gbl.spaces");

            Assert.AreEqual("Polly, Microsoft Teams Chat", s);
        }
        private async Task EnsureLongPollingEndpointAndReplaceExistingAsync(TeamsDataContext ctx, Action <List <IChatMessage> > onChatChanged, CancellationToken cancellationToken)
        {
            logger.Debug("[{TenantName}] EnsureLongPollingEndpointAndReplaceExistingAsync called", ctx.Tenant.TenantName);
            TeamsLongPollingEndpoint?endpoint;

            lock (knownEndpoints)
            {
                RemoveEndpointIfExisting(ctx);
                endpoint = longPollingApi.CreateEndpoint(ctx).Result;
                if (endpoint == null)
                {
                    logger.Debug("[{TenantName}] Couldn't create long polling endpoint", ctx.Tenant.TenantName);

                    // maybe we got no token
                    throw new TeamsLongPollException($"Got null result for long polling endpoint creation for tenant {ctx.Tenant.TenantName}");
                }
                logger.Debug("[{TenantName}] Storing long poll endpoint {EndpointId}", ctx.Tenant.TenantName, endpoint.Id.Truncate(Constants.UserIdLogLength, true));
                knownEndpoints.Add(ctx, endpoint);
            }

            logger.Debug("[{TenantName}] Starting polling for endpoint {EndpointId}", ctx.Tenant.TenantName, endpoint.Id.Truncate(Constants.UserIdLogLength, true));
            await endpoint.PollUntilErrorAsync(ctx, onChatChanged);
        }
Exemple #18
0
        public async Task <MyChatsAndTeams?> GetAllChatsAndTeamsAsync(TeamsDataContext ctx)
        {
            var cacheKey    = $"ts.{ctx.Tenant.UserId}.chatsAndTeams";
            var cachedChats = await BlobCache.UserAccount
                              .GetObject <MyChatsAndTeams?>(cacheKey)
                              .Catch(Observable.Return <MyChatsAndTeams?>(null));

            // only return cached values if there are any
            if (cachedChats != null && cachedChats.chats.Count > 0)
            {
                logger.Debug("[{TenantName}] Got list of chats and teams from cache", ctx.Tenant.TenantName);
                return(cachedChats);
            }
            // otherwise: retrieve
            var chatsAndTeams = await teamsTenantApiAccessor.GetMyChatsAndTeamsAsync(ctx);

            if (chatsAndTeams != null)
            {
                await BlobCache.UserAccount.InsertObject(cacheKey, chatsAndTeams, DateTimeOffset.Now + TimeSpan.FromDays(ChatsAndTeamsCacheLifetimeDays));
            }
            Experimental.CheckForActiveMeeting(logger, ctx, chatsAndTeams);
            return(chatsAndTeams);
        }
Exemple #19
0
        private async Task UpdateChatIndexCacheEntry(TeamsDataContext ctx, ProcessedChat?chat)
        {
            if (chat == null)
            {
                return;
            }
            var cacheKey  = $"ts.{ctx.Tenant.TenantId}.chatIndex";
            var chatIndex = await GetChatIndexAsync(ctx);

            chatIndex.AddOrUpdate(chat.Id, chat, (_, existingValue) =>
            {
                if (chat.Version > existingValue.Version)
                {
                    logger.Debug("[{TenantName}] Updating chat index entry for chat {ChatId}", ctx.Tenant.TenantName, chat.Id.Truncate(Constants.ChatIdLogLength, true));
                    return(chat);
                }
                else
                {
                    return(existingValue);
                }
            });
            await BlobCache.UserAccount.InsertObject(cacheKey, chatIndex, DateTimeOffset.Now + TimeSpan.FromMinutes(ChatIndexCacheLifetimeMins));
        }
Exemple #20
0
        public async Task TestMriReplacement()
        {
            using var kernel = new FakeItEasyMockingKernel();
            kernel.Rebind <ILogger>().ToConstant(Log.Logger);
            kernel.Rebind <ITeamsUserStore>().ToConstant(A.Fake <ITeamsUserStore>()).InSingletonScope();
            kernel.Rebind <ITeamsUserRegistry>().To <TeamsUserRegistry>().InSingletonScope();
            var userRegistry = kernel.Get <ITeamsUserRegistry>();

            var fakeContext = new TeamsDataContext((TeamsParticipant)"8:orgid:00000000-0000-beef-0000-000000000000", new ProcessedTenant(new Tenant()
            {
                tenantId = "Tenant ID", userId = "8:orgid:00000000-0000-beef-0000-000000000000", tenantName = "Fake User"
            }, DateTime.UtcNow));

            var userId = (TeamsParticipant)"8:orgid:00000000-0000-beef-0000-000000000000";
            await userRegistry.RecognizeUserIdAsync(fakeContext, userId);

            await userRegistry.RegisterDisplayNameForUserIdAsync(fakeContext, userId, "User Name", DateTime.UtcNow);

            var user = await userRegistry.GetUserByIdAsync(fakeContext, userId, false);

            Assert.AreEqual("User Name", user.DisplayName);

            string s;

            s = await userRegistry.ReplaceUserIdsWithDisplayNamesAsync(fakeContext, "8:orgid:00000000-0000-beef-0000-000000000000");

            Assert.AreEqual("User Name", s);
            s = await userRegistry.ReplaceUserIdsWithDisplayNamesAsync(fakeContext, "00000000-0000-beef-0000-000000000000");

            Assert.AreEqual("User Name", s);
            s = await userRegistry.ReplaceUserIdsWithDisplayNamesAsync(fakeContext, "00000000-0000-beef-0000-000000000000, 00000000-0000-beef-0000-000000000000");

            Assert.AreEqual("User Name, User Name", s);
            s = await userRegistry.ReplaceUserIdsWithDisplayNamesAsync(fakeContext, "8:00000000-0000-beef-0000-000000000000");

            Assert.AreEqual("User Name", s);
        }
Exemple #21
0
        private async Task UpdateTenantsForTokenAsync(TeamsTokenInfo tokenInfo)
        {
            Debug.Assert(tokenInfo.TokenType == TeamsTokenType.MyTenantsAuthHeader);

            var contexts = dataContexts.Where(ctx => ctx.Tenant?.UserId == tokenInfo.UserId);

            if (!contexts.Any())
            {
                var tenants = await teamsGlobalApiAccessor.GetTenantsAsync(tokenInfo);

                if (tenants == null)
                {
                    return;
                }

                var mainTenant = tenants.Where(t => t.UserType == "member");
                if (!mainTenant.Any())
                {
                    logger.Warning("Cannot find main tenant for user {UserId}", tokenInfo.UserId);
                    return;
                }
                if (mainTenant.Count() > 1)
                {
                    logger.Warning("Found multiple main tenants for user {UserId}; this needs to be handled in code", tokenInfo.UserId);
                    return;
                }
                var mainUserId = mainTenant.Single().UserId;

                foreach (var tenant in tenants)
                {
                    var dataContext = new TeamsDataContext(mainUserId, tenant);
                    dataContexts.Add(dataContext);
                    TenantSource.OnNext(dataContext);
                }
            }
        }
        private async Task RetrieveChatsAsync(TeamsDataContext ctx)
        {
            logger.Debug("[{TenantName}] Entering {Method} for tenant | {Context}", ctx.Tenant.TenantName, nameof(RetrieveChatsAsync), ctx);

            var debugWhiteList = new List <string>();

            if (debugWhiteList.Count == 0)
            {
                EnsureLongPollingEndpointsAreUpAndRunning(ctx);
            }

            MyChatsAndTeams?myChatsAndTeams = await chatRegistry.GetUpdatedChatsAndTeamsAsync(ctx);

            if (myChatsAndTeams == null)
            {
                logger.Debug("[{TenantName}] Got no chats in {Method}, exiting", ctx.Tenant.TenantName, nameof(RetrieveChatsAsync));
                return;
            }

            logger.Debug("[{TenantName}] Retrieved {ChatCount} chats, queuing all for retrieval/update check | {Context}", ctx.Tenant.TenantName, myChatsAndTeams.chats.Count, ctx);
            var queue = GetQueueForDataContext(ctx);

            foreach (var chat in myChatsAndTeams.chats)
            {
                var comparableChat = new HigherVersionWinsComparerChat(chat);
                if (queue.TryRemove(new ChatQueueItem(ctx, comparableChat)))
                {
                    logger.Verbose("[{TenantName}] Updating already queued chat {ChatId} retrieval entry | {Context}", ctx.Tenant.TenantName, chat.id.Truncate(Constants.ChatIdLogLength, true), ctx);
                }

                if (debugWhiteList.Count == 0 || debugWhiteList.Contains(chat.id))
                {
                    queue.Enqueue(new ChatQueueItem(ctx, comparableChat), comparableChat);
                }
            }
        }
Exemple #23
0
 private async Task ClearChatIndexCache(TeamsDataContext ctx)
 {
     logger.Debug("[{TenantName}] Clearing chat index cache", ctx.Tenant.TenantName);
     _ = await BlobCache.UserAccount.InvalidateObject <Dictionary <string, TeamsChatIndexEntry> >($"ts.{ctx.Tenant.TenantId}.chatIndex");
 }
        public async Task DownloadImagesAsync(TeamsDataContext ctx, IOrderedEnumerable <IChatMessage> messages)
        {
            var userId       = ctx.Tenant.UserId;
            var tokenContext = tokenRetriever.GetUserTokenContext(userId, true);
            var tokenInfo    = tokenContext?[TeamsTokenType.MyChatsAuthHeader];

            if (tokenInfo == null || !tokenInfo.IsValid())
            {
                logger.Debug("[{TenantName}] Cannot get conversations for user {userId}, no token present", ctx.Tenant.TenantName, userId.Truncate(Constants.UserIdLogLength, true));
                return;
            }
            var clientForTeamsImages = Utils.CreateHttpClient();

            clientForTeamsImages.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
            clientForTeamsImages.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
            clientForTeamsImages.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
            clientForTeamsImages.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));
            clientForTeamsImages.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("de"));
            clientForTeamsImages.DefaultRequestHeaders.Add("x-ms-client-type", "web");
            clientForTeamsImages.DefaultRequestHeaders.Add("Authorization", tokenInfo.AuthHeader.Replace("skypetoken=", "skype_token "));
            clientForTeamsImages.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.57");
            clientForTeamsImages.DefaultRequestHeaders.Add("Referer", "https://teams.microsoft.com/");
            clientForTeamsImages.DefaultRequestHeaders.Add("Origin", "https://teams.microsoft.com");

            var clientForPublicImages = Utils.CreateHttpClient();

            clientForPublicImages.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("image/webp", 0.8));
            clientForPublicImages.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("image/apng", 0.8));
            clientForPublicImages.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("image/*", 0.8));
            clientForPublicImages.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*", 0.8));
            clientForPublicImages.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
            clientForPublicImages.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
            clientForPublicImages.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));
            clientForPublicImages.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("de"));
            clientForPublicImages.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.57");

            foreach (var m in messages)
            {
                foreach (var cid in m.ContentIds.Keys)
                {
                    var imageInfo = m.ContentIds[cid];

                    byte[]? buffer;
                    try
                    {
                        buffer = await Akavache.BlobCache.UserAccount.Get(imageInfo.CacheKey);
                    }
                    catch
                    {
                        buffer = null;
                    }
                    if (buffer == null)
                    {
                        HttpResponseMessage result;

                        if (imageInfo.ImageType == ImageType.TeamsWithAuthentication)
                        {
                            result = await clientForTeamsImages.GetAsync(imageInfo.Url);
                        }
                        else if (imageInfo.ImageType == ImageType.Public)
                        {
                            result = await clientForPublicImages.GetAsync(imageInfo.Url);
                        }
                        else
                        {
                            throw new TeasmCompanionException("Unknown image type");
                        }
                        if (result.IsSuccessStatusCode)
                        {
                            buffer = await result.Content.ReadAsByteArrayAsync();

                            await Akavache.BlobCache.UserAccount.Insert(imageInfo.CacheKey, buffer);

                            await Akavache.BlobCache.UserAccount.Flush();
                        }
                    }
                }
            }
            await Akavache.BlobCache.UserAccount.Flush();
        }
        private async Task UpdateUserObjectsAsync(TeamsDataContext ctx, IEnumerable <TeamsParticipant> userIds)
        {
            logger.Information("[{TenantName}] Processing request to update users: '{UserIds}'", ctx.Tenant.TenantName, userIds);
            foreach (var id in userIds)
            {
                await teamsUserRegistry.RecognizeUserIdAsync(ctx, id);
            }

            var userId    = ctx.Tenant.UserId;
            var tokenInfo = tokenRetriever.GetTokenForIdentity(userId, "https://teams.microsoft.com/api/mt/emea/beta/users/fetch");

            if (tokenInfo == null)
            {
                logger.Debug("[{TenantName}] Cannot get user objects in user context {userId}, no token present; exiting", ctx.Tenant.TenantName, userId.Truncate(Constants.UserIdLogLength, true));
                return;
            }

            var tenantUsersWithUndefinedState      = (await teamsUserRegistry.GetTenantUsersAsync(ctx)).Where(user => user.State == TeamsUserState.Undefined);
            var tenantUsersWithUndefinedStateCount = tenantUsersWithUndefinedState.Count();

            logger.Debug("[{TenantName}] Have to handle {Count} users with undefined state", ctx.Tenant.TenantName, tenantUsersWithUndefinedStateCount);
            var count = 0;

            foreach (var user in tenantUsersWithUndefinedState)
            {
                if (count > 0)
                {
                    var waitSecs = 1;
                    logger.Debug("[{TenantName}] Waiting {Secs} seconds before retrieving the user info for '{UserName}'", ctx.Tenant.TenantName, waitSecs, user.DisplayName);
                    await Task.Delay(TimeSpan.FromSeconds(waitSecs)); // there is some evidence that we need this if there are many users in undefined state
                }
                count++;
                try
                {
                    logger.Debug("[{TenantName}] Trying to get user info for '{UserName}' ({Count} of {AllCount})", ctx.Tenant.TenantName, user.DisplayName, count, tenantUsersWithUndefinedStateCount);
                    var client = Utils.CreateHttpClient();
                    client.DefaultRequestHeaders.Accept.Clear();
                    client.DefaultRequestHeaders.Add("authorization", tokenInfo.AuthHeader);
                    client.DefaultRequestHeaders.Add("authority", "teams.microsoft.com");
                    client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36");
                    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")
                    {
                        CharSet = Encoding.UTF8.WebName
                    });
                    client.DefaultRequestHeaders.Add("Origin", "https://teams.microsoft.com");
                    client.DefaultRequestHeaders.Add("Connection", "keep-alive");
                    client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
                    client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
                    client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));

                    var result = await client.PostAsync("https://teams.microsoft.com/api/mt/emea/beta/users/fetch?isMailAddress=false&canBeSmtpAddress=false&enableGuest=true&includeIBBarredUsers=true&skypeTeamsInfo=true",
                                                        new StringContent($"[\"{user.UserId}\"]", Encoding.UTF8, "application/json"));

                    if (result.IsSuccessStatusCode)
                    {
                        var buffer = await result.Content.ReadAsByteArrayAsync();

                        var data = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
                        logger.Information("[{TenantName}] User info retrieval request returned successful for '{UserId}': {Data}", ctx.Tenant.TenantName, user.UserId, data);
                        var dataObject = JsonUtils.DeserializeObject <FetchUserResponse>(logger, data);
                        if (dataObject.value?.Count == 1)
                        {
                            logger.Information("[{TenantName}] Got info, registering '{UserName}' as found in tenant", ctx.Tenant.TenantName, user.DisplayName);
                            user.RegisterOriginalUser(dataObject.value[0]).State = TeamsUserState.FoundInTenant;
                            await teamsUserRegistry.MarkUserAsChanged(ctx, user);
                        }
                        else
                        if (dataObject.value?.Count == 0)
                        {
                            logger.Information("[{TenantName}] Got empty info, registering '{UserName}' as missing from tenant", ctx.Tenant.TenantName, user.DisplayName);
                            user.State = TeamsUserState.MissingFromTenant;
                            await teamsUserRegistry.MarkUserAsChanged(ctx, user);
                        }
                        else
                        {
                            throw new TeasmCompanionException($"[{ctx.Tenant.TenantName}] Found multiple users for '{user.UserId.Mri}' - this is never expected to happen");
                        }
                    }
                    else
                    {
                        if (result.StatusCode != System.Net.HttpStatusCode.Unauthorized)
                        {
                            logger.Information("[{TenantName}] Got non-success status code for user '{UserId}'; marking this user as missing from tenant", ctx.Tenant.TenantName, user.UserId);
                            user.State = TeamsUserState.MissingFromTenant; // TODO: need to check for auth errors here
                            await teamsUserRegistry.MarkUserAsChanged(ctx, user);
                        }
                        else
                        {
                            logger.Debug("[{TenantName}] Got auth error while fetching info for user '{UserId}'; will be tried again later", ctx.Tenant.TenantName, user.UserId);
                        }
                    }
                }
                catch (Exception e)
                {
                    logger.Warning(e, "[{TenantName}] Exception while fetching info for user '{UserId}'; this will probably happen again, investigate the cause! Eating the exception for now.", ctx.Tenant.TenantName, user.UserId);
                }
            }
        }
        public async Task <TeamsLongPollingEndpoint?> CreateEndpoint(TeamsDataContext ctx)
        {
            logger.Debug("[{TenantName}] Entering CreateEndpoint to create long poll endpoint", ctx.Tenant.TenantName);
            var userId       = ctx.Tenant.UserId;
            var tokenContext = tokenRetriever.GetOrCreateUserTokenContext(userId);
            var tokenInfo    = tokenContext[TeamsTokenType.MyChatsAuthHeader];

            if (tokenInfo == null || !tokenInfo.IsValid())
            {
                logger.Debug("[{TenantName}] Exiting CreateEndpoint because no token found or already expired", ctx.Tenant.TenantName);
                return(null);
            }

            var client = Utils.CreateHttpClient();

            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Add("x-ms-client-type", "web");
            client.DefaultRequestHeaders.Add("Authentication", tokenInfo.AuthHeader);
            client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.57");
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.DefaultRequestHeaders.Add("Referer", "https://teams.microsoft.com/");
            client.DefaultRequestHeaders.Add("Origin", "https://teams.microsoft.com");
            client.DefaultRequestHeaders.Add("ClientInfo", "os=windows; osVer=10; proc=x86; lcid=de-de; deviceType=1; country=de; clientName=skypeteams; utcOffset=+01:00; timezone=Europe/Berlin");
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));
            client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("de"));

            var endpointId = Guid.NewGuid();
            var url        = $"{tokenContext?.ChatServiceUrl}/v2/users/ME/endpoints/{endpointId}";

            var requestBody = new PUT_RegisterEndpoint_RequestBody()
            {
                startingTimeSpan = 0,
                endpointFeatures = "Agent,Presence2015,MessageProperties,CustomUserProperties,NotificationStream,SupportsSkipRosterFromThreads",
                subscriptions    = new List <RequestSubscription>()
                {
                    new RequestSubscription()
                    {
                        channelType         = "HttpLongPoll",
                        interestedResources = new List <string>()
                        {
                            "/v1/users/ME/conversations/ALL/properties",
                            "/v1/users/ME/conversations/ALL/messages",
                            "/v1/threads/ALL"
                        }
                    }
                }
            };

            var messagesHttpResult = await client.PutAsync(url,
                                                           new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json"));

            if (messagesHttpResult.IsSuccessStatusCode)
            {
                var buffer = await messagesHttpResult.Content.ReadAsByteArrayAsync();

                var data   = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
                var result = JsonUtils.DeserializeObject <RegisterEndpoint_ResponseBody>(logger, data);
                logger.Debug("[{TenantName}] Successfully created long poll endpoint: {EndpointId}", ctx.Tenant.TenantName, result.id.Truncate(Constants.UserIdLogLength, true));
                return(new TeamsLongPollingEndpoint(logger, tokenRetriever, processedNotificationMessageFactory, this, result));
            }
            else
            {
                logger.Debug("[{TenantName}] Got non-success status code for long poll endpoint creation; complete result: \r\n{@messagesHttpResult}", ctx.Tenant.TenantName, messagesHttpResult);
            }

            return(null);
        }
        private async Task <IEnumerable <Message> > RetrieveMessagesForChatAsync(TeamsDataContext ctx, string chatId, long startTime)
        {
            logger.Debug("[{TenantName}] Entering {Method} to retrieve messages for chat {ChatId} (startTime: {StartTime} == {StartTimeReadable})", ctx.Tenant.TenantName, nameof(RetrieveMessagesForChatAsync), chatId.Truncate(Constants.ChatIdLogLength, true), startTime, startTime > 0 ? Utils.JavaScriptUtcMsToDateTime(startTime) : "all");
            var userId       = ctx.Tenant.UserId;
            var tokenContext = tokenRetriever.GetOrCreateUserTokenContext(userId);
            var tokenInfo    = tokenContext[TeamsTokenType.MyChatsAuthHeader];

            if (config.ChatIdIgnoreList.Contains(chatId, StringComparer.InvariantCultureIgnoreCase))
            {
                logger.Debug("[{TenantName}] Chat {ChatId} is on the ignore list, exiting with fake empty message list", ctx.Tenant.TenantName, chatId.Truncate(Constants.ChatIdLogLength, true));
                return(new List <Message>());
            }

            if (tokenInfo == null || !tokenInfo.IsValid())
            {
                logger.Debug("[{TenantName}] Exiting {MethodName} because no token found or already expired.", ctx.Tenant.TenantName, nameof(RetrieveMessagesForChatAsync));
                return(new List <Message>());
            }

            var urlLeftPart = $"{tokenContext?.ChatServiceUrl}/v1/users/ME/conversations";

            var client = Utils.CreateHttpClient();

            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Add("x-ms-client-type", "web");
            client.DefaultRequestHeaders.Add("Authentication", tokenInfo.AuthHeader);
            client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.57");
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.DefaultRequestHeaders.Add("Referer", "https://teams.microsoft.com/");
            client.DefaultRequestHeaders.Add("Origin", "https://teams.microsoft.com");
            client.DefaultRequestHeaders.Add("ClientInfo", "ClientInfo: os=windows; osVer=10; proc=x86; lcid=de-de; deviceType=1; country=de; clientName=skypeteams; utcOffset=+01:00; timezone=Europe/Berlin");
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));
            client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("de"));

            var result       = new List <Message>();
            var url          = $"{urlLeftPart}/{HttpUtility.UrlEncode(chatId)}/messages?view=msnp24Equivalent|supportsMessageProperties&pageSize=200&startTime={startTime}";
            var waitSomeTime = false;

            do
            {
                if (waitSomeTime)
                {
                    var waitSecs = 30;
                    logger.Debug("[{TenantName}] Waiting {Secs} seconds before retrieving the next batch for chat {ChatId}", ctx.Tenant.TenantName, waitSecs, chatId.Truncate(Constants.ChatIdLogLength, true));
                    await Task.Delay(TimeSpan.FromSeconds(waitSecs)); // there is no evidence that we need this but it seems like a cautious approach
                }

                logger.Debug("[{TenantName}] Retrieving messages for chat {ChatId} from URL {Url}", ctx.Tenant.TenantName, chatId.Truncate(Constants.ChatIdLogLength, true), url);
                var messagesHttpResult = await client.GetAsync(url);

                if (messagesHttpResult.IsSuccessStatusCode)
                {
                    var buffer = await messagesHttpResult.Content.ReadAsByteArrayAsync();

                    var data     = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
                    var messages = JsonUtils.DeserializeObject <ThreadMessages>(logger, data);
                    startTime = messages._metadata.lastCompleteSegmentStartTime; // this will be 1 if there is only one page, otherwise this is the startTime for the next query
                    logger.Debug("[{TenantName}] Got {Count} chat messages for chat {ChatId}", ctx.Tenant.TenantName, messages.messages.Count, chatId.Truncate(Constants.ChatIdLogLength, true));
                    result.AddRange(messages.messages);
                    waitSomeTime = messages.messages.Count > 150;

                    if (url == messages._metadata.syncState)
                    {
                        break;
                    }
                    url = messages._metadata.syncState;
                }
                else
                {
                    break;
                }
            } while (startTime > 1);

            return(result);
        }
 public async Task <IEnumerable <Message> > RetrieveMessagesForChatSinceAsync(TeamsDataContext ctx, string chatId, long startTime)
 {
     return(await RetrieveMessagesForChatAsync(ctx, chatId, startTime));
 }
 public async Task <IEnumerable <Message> > RetrieveAllMessagesForChatAsync(TeamsDataContext ctx, Chat chat)
 {
     return(await RetrieveMessagesForChatAsync(ctx, chat.Id, 1));
 }
        public async Task <MyChatsAndTeams?> GetMyChatsAndTeamsAsync(TeamsDataContext ctx, string?base64SyncToken = null)
        {
            var userId = ctx.Tenant.UserId;

            logger.Debug("[{TenantName}] Entering {Method} for user {UserId}", ctx.Tenant.TenantName, nameof(GetMyChatsAndTeamsAsync), userId.Truncate(Constants.UserIdLogLength, true));

            var tokenContext = tokenRetriever.GetOrCreateUserTokenContext(userId);
            var tokenInfo    = tokenContext[TeamsTokenType.MyTeamsAuthHeader];

            if (tokenInfo == null || !tokenInfo.IsValid())
            {
                logger.Debug("[{TenantName}] Exiting {Method} because no token found or already expired.", ctx.Tenant.TenantName, nameof(GetMyChatsAndTeamsAsync));
                return(null);
            }

            var client = Utils.CreateHttpClient();

            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Add("x-ms-client-type", "web");
            client.DefaultRequestHeaders.Add("Authorization", tokenInfo.AuthHeader);
            client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.57");
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.DefaultRequestHeaders.Add("Referer", "https://teams.microsoft.com/_");
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
            client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));
            client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("de"));

            if (base64SyncToken != null)
            {
                var decodedSyncToken = base64SyncToken.FromBase64String();
                logger.Debug("[{TenantName}] Retrieving chats and teams update for sync token {SyncToken}", ctx.Tenant.TenantName, decodedSyncToken);
                client.DefaultRequestHeaders.Add("x-ms-synctoken", base64SyncToken);
            }

            string url;

            if (base64SyncToken == null)
            {
                url = "https://teams.microsoft.com/api/csa/api/v1/teams/users/me?isPrefetch=false&enableMembershipSummary=true";
            }
            else
            {
                url = "https://teams.microsoft.com/api/csa/api/v1/teams/users/me/updates?isPrefetch=false&enableMembershipSummary=true";
            }
            var result = await client.GetAsync(url);

            if (result.IsSuccessStatusCode)
            {
                var buffer = await result.Content.ReadAsByteArrayAsync();

                var data          = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
                var chatsAndTeams = JsonUtils.DeserializeObject <MyChatsAndTeams>(logger, data);
                logger.Debug("[{TenantName}] Successfully retrieved chats and teams data for user {UserId}", ctx.Tenant.TenantName, userId.Truncate(Constants.UserIdLogLength, true));

                return(SortChats(chatsAndTeams));
            }
            else
            {
                logger.Information("[{TenantName}] Got error status code {StatusCode} when retrieving chats for user {UserId}", ctx.Tenant.TenantName, result.StatusCode, userId.Truncate(Constants.UserIdLogLength, true));
                return(null);
            }
        }