2222log = get_logger (__name__ )
2323
2424
25+ def dest_host_filter (host : str ) -> dict :
26+ """A host verdict reaches links that hide the host in a geo rule or a
27+ variant, not only links whose long_url points there. The legacy urls and
28+ emojis collections carry no rules, so their host-only queries stay
29+ complete without this."""
30+ return {"$or" : [{"dest.host" : host }, {"dest.secondary_hosts" : host }]}
31+
32+
33+ # Every host a link can route to, for grouping: main plus secondary.
34+ _ALL_HOSTS = {"$setUnion" : [["$dest.host" ], {"$ifNull" : ["$dest.secondary_hosts" , []]}]}
35+
36+
2537# Mongo caps a single query document at 16MB; a host-wide block can name
2638# tens of thousands of (alias, domain) pairs, so the $or is chunked.
2739_ALIAS_CHUNK = 1_000
@@ -478,7 +490,7 @@ async def list_by_dest_host_with_urls(
478490 deliberately status-blind: a re-delivered block must still evict
479491 entries the first attempt flipped but failed to evict."""
480492 cursor = self ._col .find (
481- { "dest. host" : host } ,
493+ dest_host_filter ( host ) ,
482494 {"alias" : 1 , "domain" : 1 , "long_url" : 1 },
483495 ).limit (limit )
484496 docs = await cursor .to_list (length = limit )
@@ -492,13 +504,24 @@ async def list_by_dest_host_with_urls(
492504 return [(d ["alias" ], d .get ("domain" , "" ), d .get ("long_url" , "" )) for d in docs ]
493505
494506 async def unblock_by_dest_host (self , host : str ) -> int :
495- """Flip BLOCKED links pointing at *host* back to ACTIVE, scoped to
496- docs carrying ``blocked_reason`` so a manual operator ban is never
497- undone. Stamps stay; ``unblocked_at`` records the reversal."""
507+ """Remove *host* as a block cause and reactivate the links that have
508+ no cause left. A link also blocked for another of its destinations
509+ stays BLOCKED; a per-link block (``blocked_hosts`` null) is never
510+ touched by a host unblock. Blocks stamped before ``blocked_hosts``
511+ existed were host-of-long_url blocks, so they match on ``dest.host``.
512+ Scoped to docs carrying ``blocked_reason`` so a manual operator ban is
513+ never undone. Stamps stay; ``unblocked_at`` records the reversal."""
498514 now = datetime .now (timezone .utc )
515+ await self ._col .update_many (
516+ {"blocked_hosts" : host , "status" : UrlStatus .BLOCKED .value },
517+ {"$pull" : {"blocked_hosts" : host }},
518+ )
499519 result = await self ._col .update_many (
500520 {
501- "dest.host" : host ,
521+ "$or" : [
522+ {** dest_host_filter (host ), "blocked_hosts" : []},
523+ {"blocked_hosts" : {"$exists" : False }, "dest.host" : host },
524+ ],
502525 "status" : UrlStatus .BLOCKED .value ,
503526 "blocked_reason" : {"$exists" : True },
504527 },
@@ -519,7 +542,7 @@ async def list_active_owned_by_dest_host(
519542 event set (anonymous links have no possible webhook subscriber)."""
520543 cursor = self ._col .find (
521544 {
522- "dest. host" : host ,
545+ ** dest_host_filter ( host ) ,
523546 "status" : UrlStatus .ACTIVE .value ,
524547 "owner_id" : {"$ne" : ANONYMOUS_OWNER_ID },
525548 }
@@ -543,6 +566,8 @@ async def list_active_hosts_by_registrable(
543566 "status" : UrlStatus .ACTIVE .value ,
544567 }
545568 },
569+ # Main host only: a secondary host's own registrable domain is not
570+ # the matched one, so fanning it out here would tag it wrongly.
546571 {
547572 "$group" : {
548573 "_id" : "$dest.host" ,
@@ -570,9 +595,11 @@ async def list_recent_destination_hosts(
570595 since_id = ObjectId .from_datetime (since )
571596 pipeline = [
572597 {"$match" : {"_id" : {"$gte" : since_id }, "dest.host" : {"$exists" : True }}},
598+ {"$project" : {"hosts" : _ALL_HOSTS , "long_url" : 1 }},
599+ {"$unwind" : "$hosts" },
573600 {
574601 "$group" : {
575- "_id" : "$dest.host " ,
602+ "_id" : "$hosts " ,
576603 "sample_url" : {"$first" : "$long_url" },
577604 }
578605 },
@@ -623,6 +650,8 @@ async def block_active_by_aliases(
623650 "updated_at" : now ,
624651 "blocked_at" : now ,
625652 "blocked_reason" : reason ,
653+ # Per-link cause: a host unblock never reactivates it.
654+ "blocked_hosts" : None ,
626655 }
627656 },
628657 )
@@ -635,7 +664,7 @@ async def destination_history(self, host: str) -> dict:
635664 how many links point here, the anon/owned split, total clicks, and
636665 the earliest sighting. All facts we already own; no network."""
637666 pipeline = [
638- {"$match" : { "dest. host" : host } },
667+ {"$match" : dest_host_filter ( host ) },
639668 {
640669 "$group" : {
641670 "_id" : None ,
@@ -698,7 +727,7 @@ async def host_breadth(self, host: str, *, sample: int = 8) -> dict:
698727 benefit of the doubt.
699728 """
700729 pipeline = [
701- {"$match" : { "dest. host" : host } },
730+ {"$match" : dest_host_filter ( host ) },
702731 {
703732 "$group" : {
704733 "_id" : None ,
@@ -748,16 +777,58 @@ async def block_active_by_dest_host(self, host: str, *, reason: str) -> int:
748777 audit trail — ``updated_at`` is lossy, these survive later edits."""
749778 now = datetime .now (timezone .utc )
750779 result = await self ._col .update_many (
751- {"dest. host" : host , "status" : UrlStatus .ACTIVE .value },
780+ {** dest_host_filter ( host ) , "status" : UrlStatus .ACTIVE .value },
752781 {
753782 "$set" : {
754783 "status" : UrlStatus .BLOCKED .value ,
755784 "updated_at" : now ,
756785 "blocked_at" : now ,
757786 "blocked_reason" : reason ,
787+ "blocked_hosts" : [host ],
758788 }
759789 },
760790 )
791+ # Already-blocked links gain this host as a second cause; a pre-field
792+ # block's first cause is dest.host. Per-link blocks (null) are skipped.
793+ await self ._col .update_many (
794+ {
795+ ** dest_host_filter (host ),
796+ "status" : UrlStatus .BLOCKED .value ,
797+ "blocked_reason" : {"$exists" : True },
798+ "$or" : [
799+ {"blocked_hosts" : {"$type" : "array" }},
800+ {"blocked_hosts" : {"$exists" : False }},
801+ ],
802+ },
803+ [
804+ {
805+ "$set" : {
806+ "blocked_hosts" : {
807+ "$setUnion" : [
808+ {
809+ "$ifNull" : [
810+ "$blocked_hosts" ,
811+ {
812+ "$cond" : [
813+ {
814+ "$eq" : [
815+ {"$type" : "$dest.host" },
816+ "string" ,
817+ ]
818+ },
819+ ["$dest.host" ],
820+ [],
821+ ]
822+ },
823+ ]
824+ },
825+ [host ],
826+ ]
827+ }
828+ }
829+ }
830+ ],
831+ )
761832 return int (result .modified_count )
762833
763834 async def increment_clicks (
0 commit comments