@steve02081504/fount-p2p 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,256 +0,0 @@
1
- import { parseEntityHash } from '../core/entity_id.mjs'
2
- import { getEntityStore } from '../node/instance.mjs'
3
-
4
- import { profileAvatarFileUrl } from './files/url.mjs'
5
- import {
6
- applyAvatarToAllLocales,
7
- normalizeLocalizedMap,
8
- resolveProfilePresentation,
9
- } from './localized_core.mjs'
10
- import { resolveInfoDefaultsForEntity } from './presentation_registry.mjs'
11
- import { isWritableLocalEntity } from './replica.mjs'
12
-
13
- /** 超过该毫秒未心跳则视为离线 */
14
- const HEARTBEAT_STALE_MS = 120_000
15
-
16
- const MANUAL_STATUSES = new Set(['online', 'idle', 'dnd', 'invisible', 'away', 'busy', 'offline'])
17
- const PROFILE_JSON = 'profile.json'
18
-
19
- /**
20
- * @param {string} entityHash 128 位 entityHash
21
- * @param {{ nodeHash: string, subjectHash: string }} parsed parseEntityHash 结果
22
- * @returns {object} 默认资料
23
- */
24
- function getDefaultProfile(entityHash, parsed) {
25
- return {
26
- entityHash,
27
- nodeHash: parsed.nodeHash,
28
- subjectHash: parsed.subjectHash,
29
- localized: {},
30
- status: 'online',
31
- customStatus: '',
32
- lastSeenAt: 0,
33
- stats: {
34
- joinedAt: Date.now(),
35
- messageCount: 0,
36
- groupCount: 0,
37
- channelCount: 0,
38
- },
39
- }
40
- }
41
-
42
- /**
43
- * @param {object} profileData 原始对象
44
- * @returns {object} 可写入磁盘的资料
45
- */
46
- function toStoredProfile(profileData) {
47
- return {
48
- entityHash: profileData.entityHash,
49
- nodeHash: profileData.nodeHash,
50
- subjectHash: profileData.subjectHash,
51
- localized: normalizeLocalizedMap(profileData.localized),
52
- status: profileData.status || 'online',
53
- customStatus: String(profileData.customStatus || '').trim(),
54
- lastSeenAt: profileData.lastSeenAt || 0,
55
- stats: {
56
- joinedAt: profileData.stats?.joinedAt || Date.now(),
57
- messageCount: profileData.stats?.messageCount || 0,
58
- groupCount: profileData.stats?.groupCount || 0,
59
- channelCount: profileData.stats?.channelCount || 0,
60
- },
61
- }
62
- }
63
-
64
- /**
65
- * @param {object} profile 用户资料
66
- * @param {string} [viewerEntityHash] 查看者 entityHash
67
- * @param {{ isSelf?: boolean }} [options] isSelf 为 true 时隐身对本人可见
68
- * @returns {string} 有效状态
69
- */
70
- export function computeEffectiveStatus(profile, viewerEntityHash, options = {}) {
71
- const stored = String(profile?.status || 'online')
72
- const isSelf = options.isSelf
73
- ?? (viewerEntityHash && profile?.entityHash === viewerEntityHash)
74
- const lastSeen = profile?.lastSeenAt || 0
75
- const recentlySeen = lastSeen > 0 && Date.now() - lastSeen < HEARTBEAT_STALE_MS
76
-
77
- if (stored === 'invisible')
78
- return isSelf ? 'invisible' : 'offline'
79
-
80
- if (!recentlySeen)
81
- return 'offline'
82
-
83
- return stored
84
- }
85
-
86
- /**
87
- * @param {string} entityHash 128 位 entityHash
88
- * @param {string | null} [replicaUsername] 展示默认字段用
89
- * @param {{ groupId?: string, skipPresentation?: boolean, locales?: string[], infoDefaults?: object }} [options] 选项
90
- * @returns {Promise<object>} 资料对象
91
- */
92
- export async function getProfile(entityHash, replicaUsername = null, options = {}) {
93
- const parsed = parseEntityHash(entityHash)
94
- if (!parsed) throw new Error('invalid entityHash')
95
-
96
- const store = getEntityStore()
97
- const defaultProfile = getDefaultProfile(parsed.entityHash, parsed)
98
- let stored = defaultProfile
99
-
100
- const onDisk = await store.readEntityJson(parsed.entityHash, PROFILE_JSON)
101
- if (onDisk)
102
- stored = toStoredProfile({ ...defaultProfile, ...onDisk })
103
- else if (isWritableLocalEntity(parsed.entityHash))
104
- await store.writeEntityJson(parsed.entityHash, PROFILE_JSON, stored)
105
-
106
- const locales = options.locales || ['zh-CN', 'en-UK']
107
- const merged = {
108
- ...stored,
109
- entityHash: parsed.entityHash,
110
- nodeHash: parsed.nodeHash,
111
- subjectHash: parsed.subjectHash,
112
- }
113
-
114
- if (options.skipPresentation) return merged
115
-
116
- let { infoDefaults } = options
117
- if (!infoDefaults && replicaUsername)
118
- infoDefaults = await resolveInfoDefaultsForEntity(replicaUsername, parsed.entityHash, locales)
119
- if (!infoDefaults)
120
- infoDefaults = { name: `${parsed.subjectHash.slice(0, 8)}…${parsed.subjectHash.slice(-4)}`, avatar: '', description: '', description_markdown: '', version: '', author: '', home_page: '', issue_page: '', tags: [], links: [] }
121
-
122
- const resolved = resolveProfilePresentation(merged, locales, infoDefaults)
123
- return {
124
- ...merged,
125
- ...resolved,
126
- infoDefaults,
127
- localeKeys: Object.keys(merged.localized),
128
- }
129
- }
130
-
131
- /**
132
- * @param {string} replicaUsername 副本用户名 所有者
133
- * @param {string} entityHash 128 位 entityHash
134
- * @returns {Promise<void>}
135
- */
136
- export async function recordHeartbeat(replicaUsername, entityHash) {
137
- void replicaUsername
138
- const profile = await getProfile(entityHash, null, { skipPresentation: true })
139
- profile.lastSeenAt = Date.now()
140
- await getEntityStore().writeEntityJson(entityHash, PROFILE_JSON, toStoredProfile(profile))
141
- return { lastSeenAt: profile.lastSeenAt }
142
- }
143
-
144
- /**
145
- * @param {string} replicaUsername 副本用户名 所有者
146
- * @param {string} entityHash 128 位 entityHash
147
- * @param {object} updates 更新内容
148
- * @param {{ groupId?: string, skipPresentation?: boolean, locales?: string[] }} [options] 选项
149
- * @returns {Promise<object>} 更新后的资料
150
- */
151
- export async function updateProfile(replicaUsername, entityHash, updates, options = {}) {
152
- if (!isWritableLocalEntity(entityHash))
153
- throw new Error('entity not writable on this replica')
154
-
155
- const profile = await getProfile(entityHash, replicaUsername, {
156
- groupId: options.groupId,
157
- skipPresentation: true,
158
- })
159
- const parsed = parseEntityHash(entityHash)
160
-
161
- const localized = updates.localized != null
162
- ? normalizeLocalizedMap(updates.localized)
163
- : profile.localized
164
-
165
- const updatedProfile = toStoredProfile({
166
- ...profile,
167
- entityHash: parsed.entityHash,
168
- nodeHash: parsed.nodeHash,
169
- subjectHash: parsed.subjectHash,
170
- localized,
171
- status: updates.status != null ? updates.status : profile.status,
172
- customStatus: updates.customStatus != null ? updates.customStatus : profile.customStatus,
173
- lastSeenAt: updates.lastSeenAt != null ? updates.lastSeenAt : profile.lastSeenAt,
174
- stats: updates.stats ? { ...profile.stats, ...updates.stats } : profile.stats,
175
- })
176
-
177
- await getEntityStore().writeEntityJson(entityHash, PROFILE_JSON, updatedProfile)
178
- if (options.skipPresentation) return updatedProfile
179
- const locales = options.locales || ['zh-CN', 'en-UK']
180
- const infoDefaults = await resolveInfoDefaultsForEntity(replicaUsername, entityHash, locales)
181
- const resolved = resolveProfilePresentation(updatedProfile, locales, infoDefaults)
182
- return { ...updatedProfile, ...resolved, infoDefaults, localeKeys: Object.keys(updatedProfile.localized) }
183
- }
184
-
185
- /**
186
- * @param {string} replicaUsername 副本用户名 所有者
187
- * @param {string} entityHash 128 位 entityHash
188
- * @param {Buffer} fileBuffer 文件缓冲区
189
- * @param {string} filename 文件名
190
- * @returns {Promise<string>} 头像 URL
191
- */
192
- export async function uploadAvatar(replicaUsername, entityHash, fileBuffer, filename) {
193
- if (!isWritableLocalEntity(entityHash))
194
- throw new Error('entity not writable on this replica')
195
-
196
- const { putFileManifest } = await import('./files/evfs.mjs')
197
- await putFileManifest({
198
- ownerEntityHash: entityHash,
199
- logicalPath: 'profile/avatar',
200
- plaintext: fileBuffer,
201
- name: filename || 'avatar',
202
- mimeType: 'image/png',
203
- ceMode: 'convergent',
204
- })
205
-
206
- const avatarUrl = profileAvatarFileUrl(entityHash)
207
- const profile = await getProfile(entityHash, replicaUsername, { skipPresentation: true })
208
- await updateProfile(replicaUsername, entityHash, {
209
- localized: applyAvatarToAllLocales(profile.localized, avatarUrl),
210
- }, { skipPresentation: true })
211
- return avatarUrl
212
- }
213
-
214
- /**
215
- * @param {string} entityHash 128 位 entityHash
216
- * @returns {Promise<object>} 统计字段
217
- */
218
- export async function getStats(entityHash) {
219
- const profile = await getProfile(entityHash)
220
- return profile.stats
221
- }
222
-
223
- /**
224
- * @param {string} replicaUsername 副本用户名 所有者
225
- * @param {string} entityHash 128 位 entityHash
226
- * @param {string} status 状态
227
- * @param {string} [customStatus] 自定义状态
228
- * @returns {Promise<{ status: string, customStatus: string, lastSeenAt: number }>} 更新后的状态字段
229
- */
230
- export async function updateStatus(replicaUsername, entityHash, status, customStatus = '') {
231
- if (!MANUAL_STATUSES.has(status))
232
- throw new Error('invalid status')
233
- const updated = await updateProfile(replicaUsername, entityHash, {
234
- status,
235
- customStatus,
236
- lastSeenAt: Date.now(),
237
- }, { skipPresentation: true })
238
- return {
239
- status: updated.status,
240
- customStatus: updated.customStatus,
241
- lastSeenAt: updated.lastSeenAt,
242
- }
243
- }
244
-
245
- /**
246
- * 确保本节点操作者实体目录存在。
247
- * @param {string} replicaUsername 副本用户名 所有者
248
- * @param {string} entityHash 128 位 entityHash
249
- * @returns {Promise<object>} 本节点实体资料
250
- */
251
- export async function ensureLocalEntityProfile(replicaUsername, entityHash) {
252
- void replicaUsername
253
- if (!isWritableLocalEntity(entityHash))
254
- throw new Error('entity not on local node')
255
- return getProfile(entityHash, replicaUsername, { skipPresentation: true })
256
- }
@@ -1,20 +0,0 @@
1
- import { parseEntityHash } from '../core/entity_id.mjs'
2
- import { entityHashFromKeys, getNodeHash } from '../node/identity.mjs'
3
-
4
- /**
5
- * @param {string} recoveryPubKeyHex 64 位十六进制 recovery 公钥
6
- * @returns {string | null} 本节点 entityHash
7
- */
8
- export function resolveLocalEntityHashFromRecoveryPubKeyHex(recoveryPubKeyHex) {
9
- return entityHashFromKeys(getNodeHash(), recoveryPubKeyHex)
10
- }
11
-
12
- /**
13
- * @param {string} entityHash 目标 entityHash
14
- * @returns {boolean} 是否为本节点可写实体
15
- */
16
- export function isWritableLocalEntity(entityHash) {
17
- const parsed = parseEntityHash(entityHash)
18
- if (!parsed) return false
19
- return parsed.nodeHash === getNodeHash()
20
- }
@@ -1,38 +0,0 @@
1
- /** @type {Map<string, (replicaUsername: string, groupId: string) => Promise<object | null>>} */
2
- const providersByOwner = new Map()
3
-
4
- /**
5
- * @param {string} ownerId 注册方
6
- * @param {(replicaUsername: string, groupId: string) => Promise<object | null>} provider 物化会话
7
- * @returns {void}
8
- */
9
- export function registerMaterializedSessionProvider(ownerId, provider) {
10
- providersByOwner.set(String(ownerId), provider)
11
- }
12
-
13
- /**
14
- * @param {string} ownerId 注册方
15
- * @returns {void}
16
- */
17
- export function unregisterMaterializedSessionProvider(ownerId) {
18
- providersByOwner.delete(String(ownerId))
19
- }
20
-
21
- /**
22
- * @param {string} replicaUsername 副本用户名
23
- * @param {string} groupId 群 ID
24
- * @param {string} [ownerId] 注册方 id;省略时遍历所有 provider
25
- * @returns {Promise<object | null>} 物化会话
26
- */
27
- export async function getMaterializedSession(replicaUsername, groupId, ownerId) {
28
- if (ownerId != null) {
29
- const provider = providersByOwner.get(String(ownerId))
30
- if (!provider) return null
31
- return provider(replicaUsername, groupId)
32
- }
33
- for (const provider of providersByOwner.values()) {
34
- const session = await provider(replicaUsername, groupId)
35
- if (session) return session
36
- }
37
- return null
38
- }
@@ -1,35 +0,0 @@
1
- /** @type {Map<string, (replicaUsername: string, groupId: string) => Promise<string>>} */
2
- const groupMemberEntityByOwner = new Map()
3
-
4
- /**
5
- * @param {string} ownerId 注册方
6
- * @param {(replicaUsername: string, groupId: string) => Promise<string>} resolver 群成员 entityHash
7
- * @returns {void}
8
- */
9
- export function registerGroupMemberEntityResolver(ownerId, resolver) {
10
- groupMemberEntityByOwner.set(String(ownerId), resolver)
11
- }
12
-
13
- /**
14
- * @param {string} ownerId 注册方
15
- * @returns {void}
16
- */
17
- export function unregisterGroupMemberEntityResolver(ownerId) {
18
- groupMemberEntityByOwner.delete(String(ownerId))
19
- }
20
-
21
- /**
22
- * @param {string} replicaUsername 副本用户名
23
- * @param {string} groupId 群 ID
24
- * @returns {Promise<string | null>} entityHash;无注册方时 null
25
- */
26
- export async function resolveGroupMemberEntityHash(replicaUsername, groupId) {
27
- for (const resolver of groupMemberEntityByOwner.values())
28
- try {
29
- const eh = await resolver(replicaUsername, groupId)
30
- if (eh) return eh
31
- }
32
- catch { /* next */ }
33
-
34
- return null
35
- }