protected override Yield FilterRecipients(DispatcherEvent ev, PubSubSubscription subscription, Result <DispatcherEvent> result)
        {
            var    recipients2 = new List <DispatcherRecipient>();
            uint?  pageid      = null;
            string wikiId      = null;

            if (ev.HasDocument)
            {
                var changeDoc = ev.AsDocument();
                pageid = changeDoc["pageid"].AsUInt;
                wikiId = changeDoc["@wikiid"].AsText;
            }
            var userIds = new Dictionary <int, DispatcherRecipient>();

            foreach (var recipient in subscription.Recipients)
            {
                var authtoken = recipient.Doc["@authtoken"].AsText;
                if (string.IsNullOrEmpty(authtoken))
                {
                    // if the recipient has no authtoken, but has a userid, collect the Id so we can authorize it against the page
                    int?userId = recipient.Doc["@userid"].AsInt;
                    if (userId.HasValue)
                    {
                        userIds.Add(userId.Value, recipient);
                    }
                }
                else if (authtoken == _authtoken)
                {
                    // master authtoken means the recipient doesn't need page level authorization (such as lucene)
                    recipients2.Add(recipient);
                }
                else if (!string.IsNullOrEmpty(wikiId))
                {
                    var key = authtoken + ":" + wikiId;
                    if (!_validatedKeys.Contains(key))
                    {
                        // no valid key found, need to check with API to validate
                        XDoc settings = null;
                        yield return(_deki.At("site", "settings")
                                     .With("apikey", _authtoken)
                                     .WithHeader("X-Deki-Site", "id=" + wikiId)
                                     .Get(new Result <DreamMessage>())
                                     .Set(x => settings = x.IsSuccessful ? x.ToDocument() : null));

                        if (settings == null || !authtoken.EqualsInvariant(settings["security/api-key"].AsText))
                        {
                            continue;
                        }
                        _validatedKeys.Add(key);
                    }

                    // instance authtoken means the recipient doesn't need page level authorization (such as lucene)
                    recipients2.Add(recipient);
                }
            }
            if (userIds.Count > 0 && (ev.Channel.Segments.Length <= 2 || !ev.Channel.Segments[2].EqualsInvariantIgnoreCase("delete")))
            {
                // check all userId's against the page to prune set to authorized users
                var users = new XDoc("users");
                foreach (int userid in userIds.Keys)
                {
                    users.Start("user").Attr("id", userid).End();
                }
                if (pageid.HasValue)
                {
                    Result <DreamMessage> userAuthResult;
                    yield return(userAuthResult = _deki.At("pages", pageid.Value.ToString(), "allowed")
                                                  .With("permissions", "read,subscribe")
                                                  .With("filterdisabled", true)
                                                  .WithHeader("X-Deki-Site", "id=" + wikiId)
                                                  .PostAsync(users));

                    DreamMessage userAuth = userAuthResult.Value;
                    if (userAuth.IsSuccessful)
                    {
                        int authorized = 0;
                        foreach (XDoc userid in userAuth.ToDocument()["user/@id"])
                        {
                            DispatcherRecipient recipient;
                            if (!userIds.TryGetValue(userid.AsInt.GetValueOrDefault(), out recipient))
                            {
                                continue;
                            }
                            authorized++;
                            recipients2.Add(recipient);
                        }
                        if (authorized != userIds.Count)
                        {
                            _log.DebugFormat("requested auth on {0} users, received auth on {1} for page {2}", userIds.Count, authorized, pageid.Value);
                        }
                    }
                    else
                    {
                        _log.WarnFormat("unable to retrieve user auth for page '{0}': {1}", pageid, userAuth.Status);
                    }
                }
            }
            result.Return(recipients2.Count == 0 ? null : ev.WithRecipient(true, recipients2.ToArray()));
            yield break;
        }
        protected override Yield GetListenersByChannelResourceMatch(DispatcherEvent ev, Result <Dictionary <XUri, List <PubSubSubscription> > > result)
        {
            if (!ev.Channel.Segments[1].EqualsInvariantIgnoreCase("pages"))
            {
                // not a page DispatcherEvent or a page delete DispatcherEvent, use default matcher
                Result <Dictionary <XUri, List <PubSubSubscription> > > baseResult;
                yield return(baseResult = Coroutine.Invoke(base.GetListenersByChannelResourceMatch, ev, new Result <Dictionary <XUri, List <PubSubSubscription> > >()));

                result.Return(baseResult);
                yield break;
            }
            var matches = new List <PubSubSubscription>();

            if (ev.Channel.Segments.Length <= 2 || !ev.Channel.Segments[2].EqualsInvariantIgnoreCase("delete"))
            {
                // dispatch to all PubSubSubscriptions that listen for this DispatcherEvent and its contents
                XDoc   evDoc  = ev.AsDocument();
                uint?  pageid = evDoc["pageid"].AsUInt;
                string wikiId = evDoc["@wikiid"].AsText;
                bool   first  = true;
                _log.DebugFormat("trying dispatch based on channel & page PubSubSubscriptions for page '{0}' from wiki '{1}'", pageid, wikiId);

                // fetch parent page id's for this page so that we can resolve infinite depth PubSubSubscriptions
                Result <DreamMessage> pageHierarchyResult;
                yield return(pageHierarchyResult = _deki.At("pages", pageid.ToString()).WithHeader("X-Deki-Site", "id=" + wikiId).GetAsync());

                DreamMessage pageHierarchy = pageHierarchyResult.Value;
                if (pageHierarchy.IsSuccessful)
                {
                    XDoc pageDoc = pageHierarchy.ToDocument();
                    while (pageid.HasValue)
                    {
                        List <Tuplet <PubSubSubscription, bool> > subs;
                        _subscriptionsByPage.TryGetValue(pageid.Value, out subs);
                        if (subs != null)
                        {
                            // only the first pageId (the one from the event) triggers on non-infinite depth subs
                            foreach (var sub in subs)
                            {
                                if ((sub.Item2 || first) && !matches.Contains(sub.Item1))
                                {
                                    matches.Add(sub.Item1);
                                }
                            }
                        }

                        // get parent id and then set pageDoc to the parent's subdoc, so we can descend the ancesstor tree further
                        pageid  = pageDoc["page.parent/@id"].AsUInt;
                        pageDoc = pageDoc["page.parent"];
                        first   = false;
                    }
                }
                else
                {
                    _log.WarnFormat("unable to retrieve page doc for page '{0}': {1}", pageid, pageHierarchy.Status);
                }
            }
            ICollection <PubSubSubscription> listeningSubs;

            lock (_channelMap) {
                // get all the PubSubSubscriptions that are wild card matches (which is basically those that didn't
                // have any resources in their PubSubSubscription) and add them to the above matches
                foreach (var sub in _resourceMap.GetMatches(new XUri("http://dummy/dummy")))
                {
                    if (!matches.Contains(sub))
                    {
                        matches.Add(sub);
                    }
                }
                listeningSubs = _channelMap.GetMatches(ev.Channel, matches);
            }
            var listeners = new Dictionary <XUri, List <PubSubSubscription> >();

            foreach (var sub in listeningSubs)
            {
                List <PubSubSubscription> subs;
                if (!listeners.TryGetValue(sub.Destination, out subs))
                {
                    subs = new List <PubSubSubscription>();
                    listeners.Add(sub.Destination, subs);
                    subs.Add(sub);
                }
                else if (!subs.Contains(sub))
                {
                    subs.Add(sub);
                }
            }
            result.Return(listeners);
            yield break;
        }