mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(relationships): enforce friends-only galaxy scope
This commit is contained in:
1 parent
42dec0b75e
commit
c6676e68a5
8 files changed
+112
-11
No files matched your search
@@ -75,7 +75,7 @@ function emptyNeighborhoodResponse(
|
||||
class FakePeopleRelationshipsService implements PeopleRelationshipsService {
|
||||
graphCalls: Array<{ acceptStale?: boolean; timeRangePreset?: string; query?: string; graphScope?: string }> = []
|
||||
recomputeCalls: Array<{ timeRangePreset?: string; query?: string; graphScope?: string }> = []
|
||||
neighborhoodCalls: Array<{ key: string; acceptStale?: boolean; timeRangePreset?: string }> = []
|
||||
neighborhoodCalls: Array<{ key: string; acceptStale?: boolean; timeRangePreset?: string; graphScope?: string }> = []
|
||||
closeCalls = 0
|
||||
|
||||
getGraph(options?: {
|
||||
@@ -95,9 +95,14 @@ class FakePeopleRelationshipsService implements PeopleRelationshipsService {
|
||||
|
||||
getNeighborhood(
|
||||
key: string,
|
||||
options?: { acceptStale?: boolean; timeRangePreset?: string }
|
||||
options?: { acceptStale?: boolean; timeRangePreset?: string; graphScope?: string }
|
||||
): PeopleRelationshipsNeighborhoodResponse {
|
||||
this.neighborhoodCalls.push({ key, acceptStale: options?.acceptStale, timeRangePreset: options?.timeRangePreset })
|
||||
this.neighborhoodCalls.push({
|
||||
key,
|
||||
acceptStale: options?.acceptStale,
|
||||
timeRangePreset: options?.timeRangePreset,
|
||||
graphScope: options?.graphScope,
|
||||
})
|
||||
return emptyNeighborhoodResponse('missing')
|
||||
}
|
||||
|
||||
@@ -199,7 +204,7 @@ test('POST /_web/people/relationships/recompute forwards time range, search quer
|
||||
assert.deepEqual(service.recomputeCalls, [{ timeRangePreset: '3y', query: 'Bob', graphScope: 'close' }])
|
||||
})
|
||||
|
||||
test('GET /_web/people/relationships/:key/neighborhood forwards decoded contact key', async (t) => {
|
||||
test('GET /_web/people/relationships/:key/neighborhood forwards contact key and graph scope', async (t) => {
|
||||
const service = new FakePeopleRelationshipsService()
|
||||
const app = Fastify()
|
||||
t.after(async () => app.close())
|
||||
@@ -208,11 +213,15 @@ test('GET /_web/people/relationships/:key/neighborhood forwards decoded contact
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/_web/people/relationships/${encodeURIComponent('weixin:alice')}/neighborhood?acceptStale=1&timeRange=5y`,
|
||||
url: `/_web/people/relationships/${encodeURIComponent(
|
||||
'weixin:alice'
|
||||
)}/neighborhood?acceptStale=1&timeRange=5y&scope=friends`,
|
||||
})
|
||||
|
||||
assert.equal(response.statusCode, 200)
|
||||
assert.deepEqual(service.neighborhoodCalls, [{ key: 'weixin:alice', acceptStale: true, timeRangePreset: '5y' }])
|
||||
assert.deepEqual(service.neighborhoodCalls, [
|
||||
{ key: 'weixin:alice', acceptStale: true, timeRangePreset: '5y', graphScope: 'friends' },
|
||||
])
|
||||
})
|
||||
|
||||
test('closes people relationships service when Fastify app closes', async () => {
|
||||
|
||||
@@ -57,6 +57,7 @@ export function registerPeopleRelationshipsRoutes(server: FastifyInstance, ctx:
|
||||
return service.getNeighborhood(request.params.key, {
|
||||
acceptStale: isTruthy(request.query.acceptStale),
|
||||
timeRangePreset: parseContactsTimeRangePreset(request.query.timeRange),
|
||||
graphScope: parsePeopleRelationshipsGraphScope(request.query.scope),
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -407,6 +407,56 @@ test('returns a friends relationships graph without groupmate nodes while keepin
|
||||
}
|
||||
})
|
||||
|
||||
test('keeps friend neighborhoods limited to friends and their relationships', () => {
|
||||
const dir = makeTempDir()
|
||||
try {
|
||||
const service = createPeopleRelationshipsService({
|
||||
adapter: makeAdapter(),
|
||||
systemDir: dir,
|
||||
runner: async () => {
|
||||
throw new Error('runner should not be called for fresh injected snapshot')
|
||||
},
|
||||
})
|
||||
const snapshot = makeSnapshot(makeFreshSignature())
|
||||
const friend = makeNode({
|
||||
key: 'weixin:friend',
|
||||
displayName: 'Friend',
|
||||
pool: 'friend',
|
||||
friendSource: 'private',
|
||||
rank: 5,
|
||||
score: 60,
|
||||
})
|
||||
snapshot.nodes = [...snapshot.nodes, friend]
|
||||
snapshot.edges = [...snapshot.edges, makeEdge('weixin:alice', friend.key, 10)]
|
||||
service.replaceSnapshotForTests?.(snapshot)
|
||||
|
||||
const response = service.getNeighborhood('weixin:alice', {
|
||||
acceptStale: true,
|
||||
graphScope: 'friends',
|
||||
})
|
||||
const keys = response.graph.nodes.map((node) => node.key)
|
||||
|
||||
assert.deepEqual(keys, ['weixin:alice', friend.key])
|
||||
assert.equal(
|
||||
response.graph.nodes.every((node) => node.kind === 'owner' || node.pool === 'friend'),
|
||||
true
|
||||
)
|
||||
assert.deepEqual(
|
||||
response.graph.edges.map((edge) => [edge.sourceKey, edge.targetKey]),
|
||||
[['weixin:alice', friend.key]]
|
||||
)
|
||||
|
||||
const groupmateResponse = service.getNeighborhood('weixin:bob', {
|
||||
acceptStale: true,
|
||||
graphScope: 'friends',
|
||||
})
|
||||
assert.equal(groupmateResponse.contact, null)
|
||||
assert.deepEqual(groupmateResponse.graph, { nodes: [], edges: [], communities: [] })
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('applies manual contact friend overrides to people relationships graph scopes', () => {
|
||||
const dir = makeTempDir()
|
||||
try {
|
||||
|
||||
@@ -126,8 +126,11 @@ class DefaultPeopleRelationshipsService implements PeopleRelationshipsService {
|
||||
const snapshot = this.getSnapshotForResponse(timeRangePreset)
|
||||
const status = this.getCacheStatus(signature, timeRangePreset)
|
||||
const includeSnapshot = shouldIncludeSnapshot(status, options.acceptStale)
|
||||
const graph = includeSnapshot && snapshot ? buildPeopleRelationshipsNeighborhoodGraph(snapshot, key) : emptyGraph()
|
||||
const contact = includeSnapshot ? (snapshot?.nodes.find((node) => node.key === key) ?? null) : null
|
||||
const graphScope = normalizePeopleRelationshipsGraphScope(options.graphScope)
|
||||
const scopedSnapshot = snapshot ? buildNeighborhoodSnapshotForScope(snapshot, graphScope) : null
|
||||
const graph =
|
||||
includeSnapshot && scopedSnapshot ? buildPeopleRelationshipsNeighborhoodGraph(scopedSnapshot, key) : emptyGraph()
|
||||
const contact = includeSnapshot ? (scopedSnapshot?.nodes.find((node) => node.key === key) ?? null) : null
|
||||
return {
|
||||
contact,
|
||||
graph,
|
||||
@@ -369,6 +372,21 @@ function buildFriendsRelationshipsGraph(snapshot: PeopleRelationshipsSnapshot):
|
||||
}
|
||||
}
|
||||
|
||||
function buildNeighborhoodSnapshotForScope(
|
||||
snapshot: PeopleRelationshipsSnapshot,
|
||||
scope: PeopleRelationshipsGraphScope
|
||||
): PeopleRelationshipsSnapshot {
|
||||
if (scope !== 'friends') return snapshot
|
||||
|
||||
const graph = buildFriendsRelationshipsGraph(snapshot)
|
||||
return {
|
||||
...snapshot,
|
||||
nodes: graph.nodes,
|
||||
edges: graph.edges,
|
||||
communities: graph.communities,
|
||||
}
|
||||
}
|
||||
|
||||
function buildCloseRelationshipsGraph(snapshot: PeopleRelationshipsSnapshot): PeopleRelationshipsGraphData {
|
||||
const selectedKeys = new Set<string>()
|
||||
for (const node of snapshot.nodes) {
|
||||
|
||||
@@ -61,6 +61,7 @@ const props = withDefaults(
|
||||
selectedKey?: string | null
|
||||
privacyMode?: boolean
|
||||
safeInsetRight?: number
|
||||
emphasizeEdges?: boolean
|
||||
label: string
|
||||
ownerLabel: string
|
||||
}>(),
|
||||
@@ -68,6 +69,7 @@ const props = withDefaults(
|
||||
selectedKey: null,
|
||||
privacyMode: false,
|
||||
safeInsetRight: 0,
|
||||
emphasizeEdges: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -344,7 +346,7 @@ function renderGraph(shouldFit = false) {
|
||||
const targetX = target.x + offsetX
|
||||
const targetY = target.y + offsetY
|
||||
|
||||
const alpha = edge.visibility === 2 ? 0.22 : 0.07
|
||||
const alpha = edge.visibility === 2 ? (props.emphasizeEdges ? 0.26 : 0.22) : props.emphasizeEdges ? 0.1 : 0.07
|
||||
const width = Math.min(1.4, Math.max(0.4, Math.log10(edge.weight + 1) * 0.75))
|
||||
const edgeColor = getNodeColor(source)
|
||||
edgeLayer.moveTo(sourceX, sourceY).lineTo(targetX, targetY).stroke({ color: edgeColor, width, alpha })
|
||||
@@ -773,7 +775,14 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.graph.nodes, props.graph.edges, props.selectedKey, props.privacyMode, props.safeInsetRight],
|
||||
() => [
|
||||
props.graph.nodes,
|
||||
props.graph.edges,
|
||||
props.selectedKey,
|
||||
props.privacyMode,
|
||||
props.safeInsetRight,
|
||||
props.emphasizeEdges,
|
||||
],
|
||||
() => {
|
||||
renderGraph(false)
|
||||
},
|
||||
|
||||
@@ -63,6 +63,7 @@ const props = withDefaults(
|
||||
selectedKey?: string | null
|
||||
privacyMode?: boolean
|
||||
safeInsetRight?: number
|
||||
emphasizeEdges?: boolean
|
||||
label: string
|
||||
ownerLabel: string
|
||||
}>(),
|
||||
@@ -70,6 +71,7 @@ const props = withDefaults(
|
||||
selectedKey: null,
|
||||
privacyMode: false,
|
||||
safeInsetRight: 0,
|
||||
emphasizeEdges: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -194,7 +196,7 @@ function addThinEdgePaths(
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: bucket === 'highlight' ? 0.62 : bucket === 'normal' ? 0.21 : 0.05,
|
||||
opacity: bucket === 'highlight' ? 0.62 : bucket === 'normal' ? (props.emphasizeEdges ? 0.27 : 0.21) : 0.05,
|
||||
linewidth,
|
||||
blending: THREE.NormalBlending,
|
||||
depthTest: true,
|
||||
@@ -847,6 +849,14 @@ watch(
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.emphasizeEdges,
|
||||
() => {
|
||||
renderGraph(false)
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.privacyMode,
|
||||
() => {
|
||||
|
||||
@@ -485,6 +485,7 @@ async function loadNeighborhood(key: string) {
|
||||
const next = await dataService.getPeopleRelationshipNeighborhood(key, {
|
||||
acceptStale: true,
|
||||
timeRangePreset: timeRangePreset.value,
|
||||
graphScope: graphScope.value,
|
||||
})
|
||||
if (requestId !== neighborhoodRequestId.value) return
|
||||
|
||||
@@ -661,6 +662,7 @@ onBeforeUnmount(() => {
|
||||
:selected-key="canvasSelectedKey"
|
||||
:privacy-mode="privacyMode"
|
||||
:safe-inset-right="detailPanelSafeInsetRight"
|
||||
:emphasize-edges="graphScope === 'friends'"
|
||||
:label="t('relationships.canvas.label3d')"
|
||||
:owner-label="t('relationships.owner.me')"
|
||||
@fallback="handleThreeCanvasFallback"
|
||||
@@ -673,6 +675,7 @@ onBeforeUnmount(() => {
|
||||
:selected-key="canvasSelectedKey"
|
||||
:privacy-mode="privacyMode"
|
||||
:safe-inset-right="detailPanelSafeInsetRight"
|
||||
:emphasize-edges="graphScope === 'friends'"
|
||||
:label="t('relationships.canvas.label')"
|
||||
:owner-label="t('relationships.owner.me')"
|
||||
@select-node="selectNode"
|
||||
|
||||
@@ -207,6 +207,7 @@ export class FetchDataAdapter implements DataAdapter {
|
||||
const params = new URLSearchParams()
|
||||
if (options?.acceptStale) params.set('acceptStale', '1')
|
||||
if (options?.timeRangePreset) params.set('timeRange', options.timeRangePreset)
|
||||
if (options?.graphScope) params.set('scope', options.graphScope)
|
||||
const qs = params.toString()
|
||||
return get(`/people/relationships/${encodeURIComponent(key)}/neighborhood${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user