Skip to content

Commit

Permalink
Social proof blocks (bluesky-social#2603)
Browse files Browse the repository at this point in the history
* Add bidirectional blocks state

* Filter out edge blocks from knownFollowers

* Add tests

* Destructure map

Co-authored-by: devin ivy <[email protected]>

* Cleanup

* Consolidate known followers tests

* Clean up seed, nice naming, update tests

* Add mixed test

* Add mergeNestedMaps, add tests

* Appease linting gods

* Clarify naming

* minor tidy

---------

Co-authored-by: devin ivy <[email protected]>
  • Loading branch information
estrattonbailey and devinivy authored Jul 2, 2024
1 parent f05539d commit e54518f
Show file tree
Hide file tree
Showing 7 changed files with 441 additions and 66 deletions.
62 changes: 48 additions & 14 deletions packages/bsky/src/hydration/hydrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ import {
} from './label'
import {
HydrationMap,
Merges,
RecordInfo,
ItemRef,
didFromUri,
urisByCollection,
mergeMaps,
mergeNestedMaps,
mergeManyMaps,
} from './util'
import {
FeedGenAggs,
Expand Down Expand Up @@ -102,6 +104,7 @@ export type HydrationState = {
labelerViewers?: LabelerViewerStates
labelerAggs?: LabelerAggs
knownFollowers?: KnownFollowers
bidirectionalBlocks?: BidirectionalBlocks
}

export type PostBlock = { embed: boolean; reply: boolean }
Expand All @@ -111,6 +114,8 @@ type PostBlockPairs = { embed?: RelationshipPair; reply?: RelationshipPair }
export type FollowBlock = boolean
export type FollowBlocks = HydrationMap<FollowBlock>

export type BidirectionalBlocks = HydrationMap<HydrationMap<boolean>>

export class Hydrator {
actor: ActorHydrator
feed: FeedHydrator
Expand Down Expand Up @@ -215,13 +220,23 @@ export class Hydrator {
)
}

const knownFollowersDids = Array.from(knownFollowers.values())
const subjectsToKnownFollowersMap = Array.from(
knownFollowers.keys(),
).reduce((acc, did) => {
const known = knownFollowers.get(did)
if (known) {
acc.set(did, known.followers)
}
return acc
}, new Map<string, string[]>())
const allKnownFollowerDids = Array.from(knownFollowers.values())
.filter(Boolean)
.flatMap((f) => f!.followers)
const allDids = Array.from(new Set(dids.concat(knownFollowersDids)))
const [state, profileAggs] = await Promise.all([
const allDids = Array.from(new Set(dids.concat(allKnownFollowerDids)))
const [state, profileAggs, bidirectionalBlocks] = await Promise.all([
this.hydrateProfiles(allDids, ctx),
this.actor.getProfileAggregates(dids),
this.hydrateBidirectionalBlocks(subjectsToKnownFollowersMap),
])
const starterPackUriSet = new Set<string>()
state.actors?.forEach((actor) => {
Expand All @@ -237,6 +252,7 @@ export class Hydrator {
profileAggs,
knownFollowers,
ctx,
bidirectionalBlocks,
})
}

Expand Down Expand Up @@ -737,6 +753,30 @@ export class Hydrator {
return { follows, followBlocks }
}

async hydrateBidirectionalBlocks(
didMap: Map<string, string[]>, // DID -> DID[]
): Promise<BidirectionalBlocks> {
const pairs: RelationshipPair[] = []
for (const [source, targets] of didMap) {
for (const target of targets) {
pairs.push([source, target])
}
}

const result = new HydrationMap<HydrationMap<boolean>>()
const blocks = await this.graph.getBidirectionalBlocks(pairs)

for (const [source, targets] of didMap) {
const didBlocks = new HydrationMap<boolean>()
for (const target of targets) {
didBlocks.set(target, blocks.isBlocked(source, target))
}
result.set(source, didBlocks)
}

return result
}

// app.bsky.labeler.def#labelerViewDetailed
// - labeler
// - profile
Expand Down Expand Up @@ -1022,23 +1062,17 @@ export const mergeStates = (
labelerAggs: mergeMaps(stateA.labelerAggs, stateB.labelerAggs),
labelerViewers: mergeMaps(stateA.labelerViewers, stateB.labelerViewers),
knownFollowers: mergeMaps(stateA.knownFollowers, stateB.knownFollowers),
bidirectionalBlocks: mergeNestedMaps(
stateA.bidirectionalBlocks,
stateB.bidirectionalBlocks,
),
}
}

const mergeMaps = <M extends Merges>(mapA?: M, mapB?: M): M | undefined => {
if (!mapA) return mapB
if (!mapB) return mapA
return mapA.merge(mapB)
}

const mergeManyStates = (...states: HydrationState[]) => {
return states.reduce(mergeStates, {} as HydrationState)
}

const mergeManyMaps = <T>(...maps: HydrationMap<T>[]) => {
return maps.reduce(mergeMaps, undefined as HydrationMap<T> | undefined)
}

const actionTakedownLabels = <T>(
keys: string[],
hydrationMap: HydrationMap<T>,
Expand Down
28 changes: 28 additions & 0 deletions packages/bsky/src/hydration/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ export type RecordInfo<T> = {
takedownRef: string | undefined
}

export const mergeMaps = <V, M extends HydrationMap<V>>(
mapA?: M,
mapB?: M,
): M | undefined => {
if (!mapA) return mapB
if (!mapB) return mapA
return mapA.merge(mapB)
}

export const mergeNestedMaps = <V, M extends HydrationMap<HydrationMap<V>>>(
mapA?: M,
mapB?: M,
): M | undefined => {
if (!mapA) return mapB
if (!mapB) return mapA

for (const [key, map] of mapB) {
const merged = mergeMaps(mapA.get(key) ?? undefined, map ?? undefined)
mapA.set(key, merged ?? null)
}

return mapA
}

export const mergeManyMaps = <T>(...maps: HydrationMap<T>[]) => {
return maps.reduce(mergeMaps, undefined as HydrationMap<T> | undefined)
}

export type ItemRef = { uri: string; cid?: string }

export const parseRecord = <T>(
Expand Down
30 changes: 13 additions & 17 deletions packages/bsky/src/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'
import { mapDefined } from '@atproto/common'
import { ImageUriBuilder } from '../image/uri'
import { HydrationState } from '../hydration/hydrator'
import { ProfileViewerState as HydratorProfileViewerState } from '../hydration/actor'
import { ids } from '../lexicon/lexicons'
import {
ProfileViewDetailed,
Expand Down Expand Up @@ -113,10 +112,7 @@ export class Views {
if (!actor) return
const baseView = this.profile(did, state)
if (!baseView) return
const knownFollowersSkeleton = state.knownFollowers?.get(did)
const knownFollowers = knownFollowersSkeleton
? this.knownFollowers(knownFollowersSkeleton, state)
: undefined
const knownFollowers = this.knownFollowers(did, state)
const profileAggs = state.profileAggs?.get(did)
return {
...baseView,
Expand Down Expand Up @@ -221,10 +217,7 @@ export class Views {
if (!actor) return
const baseView = this.profile(did, state)
if (!baseView) return
const knownFollowersSkeleton = state.knownFollowers?.get(did)
const knownFollowers = knownFollowersSkeleton
? this.knownFollowers(knownFollowersSkeleton, state)
: undefined
const knownFollowers = this.knownFollowers(did, state)
return {
...baseView,
viewer: baseView.viewer
Expand Down Expand Up @@ -260,19 +253,22 @@ export class Views {
}
}

knownFollowers(
knownFollowers: Required<HydratorProfileViewerState>['knownFollowers'],
state: HydrationState,
) {
const followers = mapDefined(knownFollowers.followers, (did) => {
if (this.viewerBlockExists(did, state)) {
knownFollowers(did: string, state: HydrationState) {
const knownFollowers = state.knownFollowers?.get(did)
if (!knownFollowers) return
const blocks = state.bidirectionalBlocks?.get(did)
const followers = mapDefined(knownFollowers.followers, (followerDid) => {
if (this.viewerBlockExists(followerDid, state)) {
return undefined
}
if (blocks?.get(followerDid) === true) {
return undefined
}
if (this.actorIsNoHosted(did, state)) {
if (this.actorIsNoHosted(followerDid, state)) {
// @TODO only needed right now to work around getProfile's { includeTakedowns: true }
return undefined
}
return this.profileBasic(did, state)
return this.profileBasic(followerDid, state)
})
return { count: knownFollowers.count, followers }
}
Expand Down
82 changes: 82 additions & 0 deletions packages/bsky/tests/hydration/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
HydrationMap,
mergeMaps,
mergeManyMaps,
mergeNestedMaps,
} from '../../src/hydration/util'

const mapToObj = (map: HydrationMap<any>) => {
const obj: Record<string, any> = {}
for (const [key, value] of map) {
obj[key] = value
}
return obj
}

describe('hydration util', () => {
it(`mergeMaps: merges two maps`, () => {
const compare = new HydrationMap<string>()
compare.set('a', 'a')
compare.set('b', 'b')

const a = new HydrationMap<string>().set('a', 'a')
const b = new HydrationMap<string>().set('b', 'b')
const merged = mergeMaps(a, b)

expect(mapToObj(merged!)).toEqual(mapToObj(compare))
})

it(`mergeManyMaps: merges three maps`, () => {
const compare = new HydrationMap<string>()
compare.set('a', 'a')
compare.set('b', 'b')
compare.set('c', 'c')

const a = new HydrationMap<string>().set('a', 'a')
const b = new HydrationMap<string>().set('b', 'b')
const c = new HydrationMap<string>().set('c', 'c')
const merged = mergeManyMaps(a, b, c)

expect(mapToObj(merged!)).toEqual(mapToObj(compare))
})

it(`mergeNestedMaps: merges two nested maps`, () => {
const compare = new HydrationMap<HydrationMap<string>>()
const compareA = new HydrationMap<string>().set('a', 'a')
const compareB = new HydrationMap<string>().set('b', 'b')
compare.set('a', compareA)
compare.set('b', compareB)

const a = new HydrationMap<HydrationMap<string>>().set(
'a',
new HydrationMap<string>().set('a', 'a'),
)
const b = new HydrationMap<HydrationMap<string>>().set(
'b',
new HydrationMap<string>().set('b', 'b'),
)
const merged = mergeNestedMaps(a, b)

expect(mapToObj(merged!)).toEqual(mapToObj(compare))
})

it(`mergeNestedMaps: merges two nested maps with common keys`, () => {
const compare = new HydrationMap<HydrationMap<boolean>>()
const compareA = new HydrationMap<boolean>()
compareA.set('b', true)
compareA.set('c', true)
compare.set('a', compareA)

const a = new HydrationMap<HydrationMap<boolean>>().set(
'a',
new HydrationMap<boolean>().set('b', true),
)
const b = new HydrationMap<HydrationMap<boolean>>().set(
'a',
new HydrationMap<boolean>().set('c', true),
)
const merged = mergeNestedMaps(a, b)

expect(mapToObj(merged!)).toEqual(mapToObj(compare))
})
})
Loading

0 comments on commit e54518f

Please sign in to comment.