@steve02081504/fount-p2p 0.0.1 → 0.0.3

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.infoDefaults
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,16 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- const dir = path.resolve(process.cwd(), 'debug_logs')
5
-
6
- /**
7
- * P2P 观测落盘:写入进程 cwd 下 `debug_logs/`(与 monorepo `debugLog` 行为对齐)。
8
- * @param {string} name 日志文件名(不含扩展名)
9
- * @param {unknown} data 调试数据
10
- * @returns {Promise<void>}
11
- */
12
- export async function debugLog(name, data) {
13
- const text = Object(data) instanceof String ? data : JSON.stringify(data)
14
- await fs.promises.mkdir(dir, { recursive: true })
15
- await fs.promises.appendFile(path.join(dir, `${name}.log`), text)
16
- }