@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.
- package/README.md +2 -1
- package/core/entity_id.mjs +4 -4
- package/deno.json +4 -0
- package/discovery/bt.mjs +1 -1
- package/entity/files/evfs.mjs +19 -0
- package/entity/replica.mjs +4 -4
- package/federation/{operator_key_chain.mjs → entity_key_chain.mjs} +32 -33
- package/federation/manifest_fetch_pending.mjs +90 -0
- package/files/chunk_fetch.mjs +3 -34
- package/files/chunk_responder.mjs +60 -2
- package/files/fetch_fanout.mjs +38 -0
- package/files/manifest_fetch.mjs +97 -0
- package/files/public_manifest.mjs +153 -0
- package/files/transfer_key.mjs +2 -4
- package/governance/join_pow.mjs +1 -1
- package/link/channel_mux.mjs +3 -1
- package/link/link.mjs +1 -1
- package/link/sdp_fingerprint.mjs +1 -1
- package/mailbox/store.mjs +1 -1
- package/node/denylist.mjs +2 -2
- package/node/identity.mjs +5 -5
- package/node/personal_block.mjs +1 -1
- package/overlay/index.mjs +1 -1
- package/package.json +2 -1
- package/transport/rtc_connection_budget.mjs +2 -0
- package/transport/stale_peer_log.mjs +0 -3
- package/wire/part_invoke.mjs +1 -1
- package/wire/rate_bucket.mjs +1 -1
- package/entity/files/url.mjs +0 -19
- package/entity/localized_core.mjs +0 -141
- package/entity/presentation_registry.mjs +0 -44
- package/entity/profile.mjs +0 -256
- package/utils/debug_log.mjs +0 -16
package/entity/profile.mjs
DELETED
|
@@ -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
|
-
}
|
package/utils/debug_log.mjs
DELETED
|
@@ -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
|
-
}
|