public static void FeedTimer(
            [TimerTrigger("0 */30 * * * *")] TimerInfo timer,
            [CosmosDB("FeedStateDatabase", "FeedItemsCollection", ConnectionStringSetting = "slackstackfeed_CosmosDB", Id = "state")] SlackStackState state,
            [Queue("slackfeed-items", Connection = "slackstackfeed_QueueStorage")] ICollector <QueuedJob> triggers,
            ILogger log)
        {
            if (state == null)
            {
                log.LogDebug("no state object.");
            }

            if (state.Feeds == null)
            {
                log.LogDebug("no state feeds.");
                return;
            }

            if (!state.Feeds.Any())
            {
                log.LogDebug("state feeds empty.");
            }

            // NOTE: KeyValuePair has no built-in deconstructor, but tuple does. We could either write an extension method, or convert the Dictionary items.
            foreach ((var key, var feed) in state.Feeds.Select(x => (x.Key, x.Value)))
            {
                if (key == null || feed == null || feed.Teams == null || !feed.Teams.Any())
                {
                    log.LogDebug("no feed data.");
                    continue;
                }

                if (TimerFunctions.IsFeedUpdated(feed, log))
                {
                    foreach (var team in feed.Teams)
                    {
                        triggers.Add(new QueuedJob
                        {
                            Feed = key,
                            Team = team,
                        });
                    }
                }

                feed.Processed = DateTime.Now;
            }

            state.Processed = DateTime.Now;
        }
        public static void StackFeeder(
            [QueueTrigger("slackfeed-items", Connection = "slackstackfeed_QueueStorage")] QueuedJob job,
            [CosmosDB("FeedStateDatabase", "FeedItemsCollection", ConnectionStringSetting = "slackstackfeed_CosmosDB", Id = "state")] SlackStackState state,
            [CosmosDB("FeedStateDatabase", "FeedItemsCollection", ConnectionStringSetting = "slackstackfeed_CosmosDB", Id = "{team}")] SlackTeam team,
            DateTime processed,
            ILogger log)
        {
            if (!team.Active || !team.Subscriptions.ContainsKey(job.Feed))
            {
                return;
            }

            var feed = state.Feeds[job.Feed];

            try
            {
                var        atom = XDocument.Load(feed.SourceUri.AbsoluteUri).Root;
                XNamespace bs   = "http://www.w3.org/2005/Atom";

                DateTime.TryParse(atom.Element(bs + "updated").Value, out var updated);
                if (updated > processed)
                {
                    log.LogDebug("Feed recently updated, scanning for entries...");

                    using (var client = new HttpClient(new HttpClientHandler {
                        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
                    }))
                    {
                        foreach (var entry in atom.Elements(bs + "entry"))
                        {
                            var id = entry.Element(bs + "id")?.Value ?? string.Empty;

                            if (string.IsNullOrEmpty(id) || team.Posted.Contains(id))
                            {
                                continue;
                            }

                            if (!DateTime.TryParse(entry.Element(bs + "published").Value, out var published))
                            {
                                log.LogDebug("Invalid entry data -- missing published date.");
                                continue;
                            }

                            var raw  = entry.Element(bs + "summary").Value;
                            var post = StackFunctions.Slackify(raw);

                            var userLink = entry.Element(bs + "author").Element(bs + "uri").Value ?? string.Empty;
                            var match    = Regex.Match(userLink, $"https://{feed.Site}.stackexchange.com/users/([0-9]+)");
                            var user     = default(StackUser);

                            if (match.Success)
                            {
                                var userData  = client.GetStringAsync($"https://api.stackexchange.com/2.2/users/{match.Groups[1].Value}?site={feed.Site}&key=MYe90O9jVj1YJI12XqK0BA((&filter=!)RwdAtHo34gjVfkkY.BZV4L(").Result;
                                var stackData = JsonConvert.DeserializeObject <StackApiResponse <StackUser> >(userData);
                                if (string.IsNullOrEmpty(stackData.ErrorMessage) && stackData.Items.Any())
                                {
                                    user = stackData.Items[0];
                                }
                            }

                            var attachments = new[]
                            {
                                new
                                {
                                    mrkdwn_in   = new string[] { "text", "fields" },
                                    title       = entry.Element(bs + "title")?.Value ?? string.Empty,
                                    title_link  = id,
                                    text        = post,
                                    thumb_url   = feed.LogoUri.AbsoluteUri,
                                    author_name = user?.DisplayName ?? string.Empty,
                                    author_link = user?.Link?.AbsoluteUri ?? string.Empty,
                                    author_icon = user?.ProfileImage?.AbsoluteUri ?? string.Empty,
                                    fields      = entry.Elements(bs + "category").Select(tag => new { value = $"`{tag.Attribute("term").Value}`", @short = true }),
                                    ts          = new DateTimeOffset(published).ToUnixTimeSeconds()
                                }
                            };

                            foreach (var channel in team.Subscriptions[job.Feed])
                            {
                                var data = new FormUrlEncodedContent(new Dictionary <string, string>
                                {
                                    { "as_user", "false" },
                                    { "username", "Slack Stack Feed" },
                                    { "token", team.BotToken },
                                    { "channel", channel },
                                    { "text", $"New Question Posted to: <{feed.Uri}| *{feed.Name}*>." },
                                    { "unfurl_links", "false" },
                                    { "unfurl_media", "false" },
                                    { "attachments", JsonConvert.SerializeObject(attachments, new JsonSerializerSettings {
                                            Formatting = Formatting.Indented
                                        }) }
                                });

                                var response = client.PostAsync("https://slack.com/api/chat.postMessage", data).Result;
                                response.EnsureSuccessStatusCode();
                            }

                            team.Posted.Add(id);
                        }
                    }
                }

                // Only keep the last 30 posts we sent, to avoid filling up DocumentDB storage.
                team.Posted = team.Posted.Skip(team.Posted.Count - 30).ToList();
            }
            catch (Exception ex)
            {
                log.LogDebug(ex.ToString());
            }
        }