private static Dictionary <int, PostModel> GetPostModelsById(
            ChattyThread thread, ChattyLolCounts lolCounts)
        {
            var threadLolCounts = lolCounts.GetThreadLolCounts(thread.ThreadId);

            return(PostModel.CreateList(thread, threadLolCounts).ToDictionary(x => x.Id));
        }
 public async Task PrePopulate(Chatty newChatty, ChattyLolCounts newChattyLolCounts, List <EventModel> events)
 {
     await _lock.WithWriteLock(nameof(PrePopulate),
                               action : () =>
     {
         _chatty          = newChatty;
         _chattyLolCounts = newChattyLolCounts;
         _events.Clear();
         _events.AddRange(events);
     });
 }
        public async Task Start(Chatty newChatty, ChattyLolCounts newChattyLolCounts)
        {
            if (_chatty != null || _chattyLolCounts != null)
            {
                throw new InvalidOperationException($"{nameof(EventProvider)} already started.");
            }

            await _lock.WithWriteLock(nameof(Start),
                                      action : () =>
            {
                _chatty          = newChatty;
                _chattyLolCounts = newChattyLolCounts;
            });
        }
        public async Task <(string Json, ChattyLolCounts LolCounts)> DownloadChattyLolCounts(
            string previousJson = null, ChattyLolCounts previousChattyLolCounts = null)
        {
            var json = await _downloadService.DownloadWithSharedLogin(
                "https://www.shacknews.com/api2/api-index.php?action2=ext_get_counts",
                verifyLoginStatus : false);

            if (json == previousJson)
            {
                return(json, previousChattyLolCounts);
            }

            var response = JsonSerializer.Deserialize <Dictionary <string, Dictionary <string, Dictionary <string, string> > > >(
                json);

            return(json,
                   new ChattyLolCounts
            {
                CountsByThreadId = (
                    from threadPair in response
                    let threadId = int.Parse(threadPair.Key)
                                   let threadTagsDict = (
                        from postPair in threadPair.Value
                        let postId = int.Parse(postPair.Key)
                                     let postLols = (
                            from x in postPair.Value
                            select new LolModel {
                    Tag = x.Key, Count = int.Parse(x.Value)
                }
                            ).ToList()
                                                    select(PostId: postId, PostTags: postLols)
                        ).ToDictionary(x => x.PostId, x => x.PostTags)
                                                        let threadLolCounts = new ThreadLolCounts {
                    CountsByPostId = threadTagsDict
                }
                    select(ThreadId: threadId, ThreadLolCounts: threadLolCounts)
                    ).ToDictionary(x => x.ThreadId, x => x.ThreadLolCounts)
            });
        }
        public async Task Update(Chatty newChatty, ChattyLolCounts newChattyLolCounts)
        {
            var oldChatty = await _lock.WithReadLock(nameof(Update), func : () => _chatty);

            if (oldChatty != null)
            {
                // safety first, this should never throw if the scraper has done its job correctly
                foreach (var oldThread in oldChatty.Threads)
                {
                    if (!newChatty.ThreadsByRootId.ContainsKey(oldThread.ThreadId) &&
                        !newChatty.NukedThreadIds.Contains(oldThread.ThreadId) &&
                        !newChatty.ExpiredThreadIds.Contains(oldThread.ThreadId))
                    {
                        throw new Exception($"Thread ID {oldThread.ThreadId} disappeared but is not listed as expired or nuked!");
                    }
                }
            }

            await _lock.WithWriteLock(nameof(Update),
                                      action : () =>
            {
                if (_chatty == null || _chattyLolCounts == null)
                {
                    _chatty          = newChatty;
                    _chattyLolCounts = newChattyLolCounts;
                    return;
                }

                var nextEventId = _events.Count == 0 ? 1 : _events.Last().EventId + 1;
                var newEvents   = new List <EventModel>();

                foreach (var oldThreadId in
                         from thread in _chatty.Threads
                         where !newChatty.ThreadsByRootId.ContainsKey(thread.ThreadId)
                         select thread.ThreadId)
                {
                    // was this thread nuked or did it expire?
                    if (newChatty.NukedThreadIds.Contains(oldThreadId))
                    {
                        // nuked thread
                        newEvents.Add(
                            new EventModel
                        {
                            EventId   = nextEventId++,
                            EventDate = DateTimeOffset.Now,
                            EventType = EventType.CategoryChange,
                            EventData = new CategoryChangeEventDataModel
                            {
                                PostId   = oldThreadId,
                                Category = ModerationFlag.Nuked
                            }
                        });
                    }
                }

                foreach (var newThread in
                         from thread in newChatty.Threads
                         where !_chatty.ThreadsByRootId.ContainsKey(thread.ThreadId)
                         select thread)
                {
                    // new thread
                    var newPostModels = GetPostModelsById(newThread, newChattyLolCounts);
                    foreach (var newPost in newPostModels.Values.OrderBy(x => x.Id))
                    {
                        newEvents.Add(
                            new EventModel
                        {
                            EventId   = nextEventId++,
                            EventDate = DateTimeOffset.Now,
                            EventType = EventType.NewPost,
                            EventData = new NewPostEventDataModel
                            {
                                PostId       = newPost.Id,
                                Post         = newPost,
                                ParentAuthor = newPost.ParentId == 0 ? "" : newPostModels[newPost.ParentId].Author
                            }
                        });
                    }
                }

                foreach (var newThread in newChatty.Threads)
                {
                    if (!_chatty.ThreadsByRootId.TryGetValue(newThread.ThreadId, out var oldThread))
                    {
                        continue;
                    }

                    var oldPostModels = GetPostModelsById(oldThread, _chattyLolCounts);
                    var newPostModels = GetPostModelsById(newThread, newChattyLolCounts);

                    foreach (var oldPost in
                             from postPair in oldPostModels
                             where !newPostModels.ContainsKey(postPair.Key)
                             orderby postPair.Key
                             select postPair.Value)
                    {
                        // nuked post
                        newEvents.Add(
                            new EventModel
                        {
                            EventId   = nextEventId++,
                            EventDate = DateTimeOffset.Now,
                            EventType = EventType.CategoryChange,
                            EventData = new CategoryChangeEventDataModel
                            {
                                PostId   = oldPost.Id,
                                Category = ModerationFlag.Nuked
                            }
                        });
                    }

                    foreach (var newPost in
                             from postPair in newPostModels
                             where !oldPostModels.ContainsKey(postPair.Key)
                             orderby postPair.Key
                             select postPair.Value)
                    {
                        // new post
                        newEvents.Add(
                            new EventModel
                        {
                            EventId   = nextEventId++,
                            EventDate = DateTimeOffset.Now,
                            EventType = EventType.NewPost,
                            EventData = new NewPostEventDataModel
                            {
                                PostId       = newPost.Id,
                                Post         = newPost,
                                ParentAuthor = newPost.ParentId == 0 ? "" : newPostModels[newPost.ParentId].Author
                            }
                        });
                    }

                    foreach (var newPost in newPostModels.Values)
                    {
                        if (!oldPostModels.TryGetValue(newPost.Id, out var oldPost))
                        {
                            continue;
                        }

                        if (oldPost.Category != newPost.Category)
                        {
                            newEvents.Add(
                                new EventModel
                            {
                                EventId   = nextEventId++,
                                EventDate = DateTimeOffset.Now,
                                EventType = EventType.CategoryChange,
                                EventData = new CategoryChangeEventDataModel
                                {
                                    PostId   = newPost.Id,
                                    Category = newPost.Category
                                }
                            });
                        }

                        if (!Enumerable.SequenceEqual(
                                oldPost.Lols.OrderBy(x => x.Tag),
                                newPost.Lols.OrderBy(x => x.Tag)))
                        {
                            newEvents.Add(
                                new EventModel
                            {
                                EventId   = nextEventId++,
                                EventDate = DateTimeOffset.Now,
                                EventType = EventType.LolCountsUpdate,
                                EventData = new LolCountsUpdateEventDataModel
                                {
                                    Updates = (
                                        from x in newPost.Lols
                                        select new LolCountUpdateModel
                                    {
                                        PostId = newPost.Id,
                                        Tag = x.Tag,
                                        Count = x.Count
                                    }).ToList()
                                }
                            });
                        }

                        if (oldPost.Author != newPost.Author)     // detect deleted users
                        {
                            _logger.LogInformation($"postChange: {oldPost.Author} -> {newPost.Author}.");
                            newEvents.Add(
                                new EventModel
                            {
                                EventId   = nextEventId++,
                                EventDate = DateTimeOffset.Now,
                                EventType = EventType.PostChange,
                                EventData = new PostChangeEventDataModel
                                {
                                    PostId = newPost.Id
                                }
                            });
                        }

                        if (oldPost.IsFrozen != newPost.IsFrozen)
                        {
                            _logger.LogInformation($"Post {newPost.Id} freeze change: {oldPost.IsFrozen} -> {newPost.IsFrozen}.");
                            newEvents.Add(
                                new EventModel
                            {
                                EventId   = nextEventId++,
                                EventDate = DateTimeOffset.Now,
                                EventType = EventType.PostFreezeChange,
                                EventData = new PostFrozenChangeEventDataModel
                                {
                                    PostId = newPost.Id,
                                    Frozen = newPost.IsFrozen
                                }
                            }
                                );
                        }
                    }
                }

                _chatty          = newChatty;
                _chattyLolCounts = newChattyLolCounts;
                _events.AddRange(newEvents);

                while (_events.Count > MAX_EVENTS)
                {
                    _events.RemoveAt(0);
                }
            });
        }
 public void Update(Chatty chatty, ChattyLolCounts lolCounts)
 {
     Chatty    = chatty;
     LolCounts = lolCounts;
 }