most-box 0.2.1 → 0.2.2
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 +66 -6
- package/electron/deepLink.js +29 -0
- package/electron/main.js +87 -20
- package/out/admin/index.html +0 -0
- package/out/app/index.html +0 -0
- package/out/assets/AppShell-BOtfY12t.js +1 -0
- package/out/assets/FilePreviewOverlay-C5qK9HAE.js +1 -0
- package/out/assets/LanguageToggle-Di5b88mK.js +1 -0
- package/out/assets/MarketingHeader-BOytKcCc.js +1 -0
- package/out/assets/MarketingLayout-B8m1Q7Pa.js +1 -0
- package/out/assets/{MarketingThemeToggle-qlwCZU1o.js → MarketingThemeToggle-C6fggkl7.js} +1 -1
- package/out/assets/{MilkdownEditor-_TGlDyA_.js → MilkdownEditor-Cfze75zl.js} +1 -1
- package/out/assets/OpenSidebarButton-DDuALgJ2.js +1 -0
- package/out/assets/SidebarAccount-bo1ypzrJ.js +1 -0
- package/out/assets/arrow-right-RldN7Rwi.js +1 -0
- package/out/assets/circle-alert-BQoSDxUe.js +1 -0
- package/out/assets/cloud-DHoTMeZY.js +1 -0
- package/out/assets/copy-BtJHJnqH.js +1 -0
- package/out/assets/download-Bg-OdoxM.js +1 -0
- package/out/assets/downloadValidation-CUvbvj9f.js +1 -0
- package/out/assets/external-link-m8ZIQe4p.js +1 -0
- package/out/assets/file-text-DG1orIkZ.js +1 -0
- package/out/assets/game-s6irY8hS.js +1 -0
- package/out/assets/hard-drive-BB-sllXA.js +1 -0
- package/out/assets/index-3OD3Chi9.css +1 -0
- package/out/assets/index-BSOvFG3o.css +1 -0
- package/out/assets/index-C0xqKeu-.css +1 -0
- package/out/assets/index-CrAXrmfP.js +31 -0
- package/out/assets/index.lazy-0Njp0U6I.js +1 -0
- package/out/assets/index.lazy-BTndBeBF.js +1 -0
- package/out/assets/index.lazy-Bq39jYTl.js +1 -0
- package/out/assets/index.lazy-Bu93oGzJ.js +2 -0
- package/out/assets/index.lazy-BvdBwgHx.js +1 -0
- package/out/assets/index.lazy-CIx8ist6.js +3 -0
- package/out/assets/index.lazy-DK7R297q.js +1 -0
- package/out/assets/index.lazy-DVOcrcEB.js +2 -0
- package/out/assets/index.lazy-DejOniAm.js +1 -0
- package/out/assets/index.lazy-DfsgyiMN.js +1 -0
- package/out/assets/index.lazy-DpNIiSXF.js +1 -0
- package/out/assets/index.lazy-Vnvz-t7T.js +1 -0
- package/out/assets/index.lazy-v7nBJ-sF.js +1 -0
- package/out/assets/key-round-DIQ3Xt5F.js +1 -0
- package/out/assets/lock-tf1t2yuy.js +1 -0
- package/out/assets/message-square-DaQH7q-P.js +1 -0
- package/out/assets/mp-DvFTsIL9.js +1 -0
- package/out/assets/music-DaFvU2DL.js +1 -0
- package/out/assets/notebook-pen-CqEFOHKx.js +1 -0
- package/out/assets/play-B7q6F75-.js +1 -0
- package/out/assets/plus-H2i2mspM.js +1 -0
- package/out/assets/refresh-cw-roxAhABl.js +1 -0
- package/out/assets/save-DR0O9ReR.js +1 -0
- package/out/assets/search-ncblG-zw.js +1 -0
- package/out/assets/send-m1XCcuPn.js +1 -0
- package/out/assets/shield-check-BwcvTU4U.js +1 -0
- package/out/assets/trash-2-CNpsqYc1.js +1 -0
- package/out/assets/triangle-alert-DP9EP7IM.js +1 -0
- package/out/assets/upload-V--8p13l.js +1 -0
- package/out/assets/useChannelMessages-46C52EyL.js +3 -0
- package/out/assets/useGameRoom-BtxPpfck.js +1 -0
- package/out/assets/useNavigate-DWlBD_-b.js +1 -0
- package/out/assets/userStore-C4vdYsQp.js +4 -0
- package/out/assets/wallet-CozFU6yK.js +1 -0
- package/out/assets/wifi-DIR3g_8A.js +1 -0
- package/out/chat/index.html +0 -0
- package/out/chat/join/index.html +0 -0
- package/out/download/index.html +2 -2
- package/out/game/gandengyan/index.html +0 -0
- package/out/game/index.html +0 -0
- package/out/game/zhajinhua/index.html +0 -0
- package/out/index.html +2 -2
- package/out/note/index.html +0 -0
- package/out/ping/index.html +2 -2
- package/out/profile/index.html +0 -0
- package/out/web3/index.html +0 -0
- package/package.json +9 -1
- package/server/index.js +36 -7
- package/server/src/core/channelIdentity.js +6 -0
- package/server/src/core/gameRoom.js +15 -5
- package/server/src/core/mostLink.js +8 -7
- package/server/src/core/zhajinhua.js +6 -0
- package/server/src/games/gandengyan.js +10 -0
- package/server/src/http/access.js +63 -12
- package/server/src/http/app.js +34 -838
- package/server/src/http/nodeStatus.js +101 -9
- package/server/src/http/routePolicy.js +2 -0
- package/server/src/http/routes/channelRoutes.js +163 -0
- package/server/src/http/routes/fileRoutes.js +345 -0
- package/server/src/http/routes/nodeRoutes.js +323 -0
- package/server/src/http/routes/seedRoutes.js +58 -0
- package/server/src/index.js +197 -16
- package/server/src/node/config.js +2 -6
- package/server/src/utils/avatar.js +30 -15
- package/server/src/utils/downloadMessages.js +0 -2
- package/out/assets/AppShell-OiOEqXPr.js +0 -1
- package/out/assets/ChatUi-Cif5LRF3.js +0 -1
- package/out/assets/CopyButton-Dm7krgbq.js +0 -1
- package/out/assets/LanguageToggle-B4ZNuBCV.js +0 -1
- package/out/assets/MarketingHeader-yIZuQP7m.js +0 -1
- package/out/assets/MarketingLayout-DVH0Nx7S.js +0 -1
- package/out/assets/MoveModal-BVr4Q7-b.js +0 -1
- package/out/assets/Nav-5xeettNJ.js +0 -1
- package/out/assets/NoteSidebar-DpniUKmy.js +0 -1
- package/out/assets/OpenSidebarButton-BfgG2HIT.js +0 -1
- package/out/assets/PemBlock-CxwIepth.js +0 -1
- package/out/assets/SidebarAccount-Zg5DZblE.js +0 -1
- package/out/assets/arrow-right-CL9YSDVS.js +0 -1
- package/out/assets/channelApi-DNdJfsJ-.js +0 -1
- package/out/assets/chevron-down-CnLh_-aO.js +0 -1
- package/out/assets/circle-alert-oiiRDvhx.js +0 -1
- package/out/assets/cloud-BEe2N89j.js +0 -1
- package/out/assets/code-9LB8QqxL.js +0 -1
- package/out/assets/copy-giX4rmFJ.js +0 -1
- package/out/assets/download-D0oMEYQZ.js +0 -1
- package/out/assets/downloadValidation-Bk1VsBBo.js +0 -1
- package/out/assets/external-link-Cm2WCUxv.js +0 -1
- package/out/assets/filePreview-BZ50vZZf.js +0 -1
- package/out/assets/game-Bvz4dspe.js +0 -1
- package/out/assets/hard-drive-B3CQbcp2.js +0 -1
- package/out/assets/index-8eWJAjpY.css +0 -1
- package/out/assets/index-BZc4blbW.css +0 -1
- package/out/assets/index-BkZvz4WA.css +0 -1
- package/out/assets/index-WCK14Vja.js +0 -34
- package/out/assets/index.lazy-5Q6GuMNT.js +0 -1
- package/out/assets/index.lazy-5jq6EFXa.js +0 -3
- package/out/assets/index.lazy-7n1Q-NrA.js +0 -3
- package/out/assets/index.lazy-BFnOyQFj.js +0 -1
- package/out/assets/index.lazy-B_oPp6qK.js +0 -1
- package/out/assets/index.lazy-BvY50KVz.js +0 -1
- package/out/assets/index.lazy-C0Kn_amZ.js +0 -1
- package/out/assets/index.lazy-C3cek3Gn.js +0 -1
- package/out/assets/index.lazy-CLpPkdy1.js +0 -1
- package/out/assets/index.lazy-Cpr1kApf.js +0 -2
- package/out/assets/index.lazy-CuwLZiUK.js +0 -1
- package/out/assets/index.lazy-DDc3Ylgf.js +0 -2
- package/out/assets/index.lazy-Dg3aqOss.js +0 -1
- package/out/assets/key-round-CzuljhND.js +0 -1
- package/out/assets/lock-D2NhNoJW.js +0 -1
- package/out/assets/message-square-DwBq_Go5.js +0 -1
- package/out/assets/mp-Bln2MB9G.js +0 -1
- package/out/assets/music-CB73K5Gz.js +0 -1
- package/out/assets/notebook-pen-Up7r5zoI.js +0 -1
- package/out/assets/play-OszVgROb.js +0 -1
- package/out/assets/plus-BbxQG_Ai.js +0 -1
- package/out/assets/save-CiqyiifY.js +0 -1
- package/out/assets/search-gqAPOsgS.js +0 -1
- package/out/assets/send-vwCWsZGP.js +0 -1
- package/out/assets/shield-check-CxWxsNLc.js +0 -1
- package/out/assets/trash-2-DNGr8IgF.js +0 -1
- package/out/assets/triangle-alert-B_1BlX1b.js +0 -1
- package/out/assets/upload-Dxl7GUzb.js +0 -1
- package/out/assets/useChannelMessages-7bYKXU_R.js +0 -3
- package/out/assets/useGameRoom-DqA1mkfk.js +0 -1
- package/out/assets/wallet-DlkawdPJ.js +0 -1
- package/out/assets/wifi-sBOKcPFM.js +0 -1
- package/out/demo/index.html +0 -0
- /package/out/avatars/default/{ocean.svg → dolphin.svg} +0 -0
- /package/out/avatars/default/{violet.svg → owl.svg} +0 -0
- /package/out/avatars/default/{mint.svg → panda.svg} +0 -0
- /package/out/avatars/default/{dusk.svg → snow-mountain.svg} +0 -0
- /package/out/avatars/default/{ember.svg → tiger.svg} +0 -0
- /package/out/avatars/default/{sage.svg → turtle.svg} +0 -0
- /package/public/avatars/default/{ocean.svg → dolphin.svg} +0 -0
- /package/public/avatars/default/{violet.svg → owl.svg} +0 -0
- /package/public/avatars/default/{mint.svg → panda.svg} +0 -0
- /package/public/avatars/default/{dusk.svg → snow-mountain.svg} +0 -0
- /package/public/avatars/default/{ember.svg → tiger.svg} +0 -0
- /package/public/avatars/default/{sage.svg → turtle.svg} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { fileURLToPath } from 'node:url'
|
|
4
5
|
import { DEFAULT_NODE_HOST } from '../node/config.js'
|
|
@@ -19,16 +20,49 @@ function readPackageJson() {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
function isWildcardHost(host) {
|
|
24
|
+
return host === '0.0.0.0' || host === '::'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isLoopbackHost(host) {
|
|
28
|
+
return ['localhost', '127.0.0.1', '::1', '[::1]'].includes(host)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getNetworkAddresses(appPort, appHost = DEFAULT_NODE_HOST) {
|
|
32
|
+
const addresses = [
|
|
33
|
+
{ type: 'local', ip: 'localhost', label: '本机', iface: 'loopback' },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
if (isWildcardHost(appHost)) {
|
|
37
|
+
for (const [iface, entries = []] of Object.entries(os.networkInterfaces())) {
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.internal) continue
|
|
40
|
+
addresses.push({
|
|
41
|
+
type: entry.family === 'IPv6' ? 'ipv6' : 'lan',
|
|
42
|
+
ip: entry.address,
|
|
43
|
+
label: iface,
|
|
44
|
+
iface,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else if (!isLoopbackHost(appHost)) {
|
|
49
|
+
addresses.push({
|
|
50
|
+
type: 'listen',
|
|
51
|
+
ip: appHost,
|
|
52
|
+
label: '监听地址',
|
|
53
|
+
iface: 'configured',
|
|
54
|
+
})
|
|
28
55
|
}
|
|
56
|
+
|
|
57
|
+
return { port: appPort, addresses }
|
|
29
58
|
}
|
|
30
59
|
|
|
31
|
-
export async function buildNodeStatus(
|
|
60
|
+
export async function buildNodeStatus(
|
|
61
|
+
engine,
|
|
62
|
+
configStore,
|
|
63
|
+
appPort,
|
|
64
|
+
appHost = DEFAULT_NODE_HOST
|
|
65
|
+
) {
|
|
32
66
|
const config = configStore.getNodeConfig()
|
|
33
67
|
const { remoteInvites, ...publicConfig } = config
|
|
34
68
|
const remoteInviteCount = remoteInvites.length
|
|
@@ -41,9 +75,9 @@ export async function buildNodeStatus(engine, configStore, appPort) {
|
|
|
41
75
|
version: PACKAGE_JSON.version,
|
|
42
76
|
uptimeSeconds: Math.floor(process.uptime()),
|
|
43
77
|
nodeId: engine.getNodeId(),
|
|
44
|
-
host:
|
|
78
|
+
host: appHost,
|
|
45
79
|
port: appPort,
|
|
46
|
-
listen: getNetworkAddresses(appPort),
|
|
80
|
+
listen: getNetworkAddresses(appPort, appHost),
|
|
47
81
|
dataPath: configStore.getDataPath(),
|
|
48
82
|
config: {
|
|
49
83
|
...publicConfig,
|
|
@@ -149,6 +183,16 @@ export function buildOpenApiSpec(appPort) {
|
|
|
149
183
|
responses: { 200: { description: 'User sync status' } },
|
|
150
184
|
},
|
|
151
185
|
},
|
|
186
|
+
'/api/user/profile': {
|
|
187
|
+
get: {
|
|
188
|
+
summary: 'Read authenticated synced profile metadata',
|
|
189
|
+
responses: { 200: { description: 'Synced profile' } },
|
|
190
|
+
},
|
|
191
|
+
put: {
|
|
192
|
+
summary: 'Update authenticated synced profile metadata',
|
|
193
|
+
responses: { 200: { description: 'Synced profile update result' } },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
152
196
|
'/api/files': {
|
|
153
197
|
get: {
|
|
154
198
|
summary: 'List published files for the authenticated local user',
|
|
@@ -192,6 +236,54 @@ export function buildOpenApiSpec(appPort) {
|
|
|
192
236
|
responses: { 200: { description: 'File bytes' } },
|
|
193
237
|
},
|
|
194
238
|
},
|
|
239
|
+
'/api/channels': {
|
|
240
|
+
get: {
|
|
241
|
+
summary: 'List authenticated user channels',
|
|
242
|
+
responses: { 200: { description: 'Channel list' } },
|
|
243
|
+
},
|
|
244
|
+
post: {
|
|
245
|
+
summary: 'Create or join a P2P channel',
|
|
246
|
+
responses: { 200: { description: 'Channel metadata' } },
|
|
247
|
+
},
|
|
248
|
+
delete: {
|
|
249
|
+
summary: 'Leave a P2P channel',
|
|
250
|
+
responses: { 200: { description: 'Updated channel list' } },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
'/api/channels/{name}/messages': {
|
|
254
|
+
get: {
|
|
255
|
+
summary: 'Read P2P channel messages',
|
|
256
|
+
responses: { 200: { description: 'Channel messages' } },
|
|
257
|
+
},
|
|
258
|
+
post: {
|
|
259
|
+
summary: 'Send a P2P channel message',
|
|
260
|
+
responses: { 200: { description: 'Created channel message' } },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
'/api/channels/{name}/members': {
|
|
264
|
+
get: {
|
|
265
|
+
summary: 'List P2P channel members',
|
|
266
|
+
responses: { 200: { description: 'Channel members' } },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
'/api/channels/{name}/peers': {
|
|
270
|
+
get: {
|
|
271
|
+
summary: 'List currently connected channel peers',
|
|
272
|
+
responses: { 200: { description: 'Channel peers' } },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
'/api/channels/{name}/remark': {
|
|
276
|
+
put: {
|
|
277
|
+
summary: 'Set an authenticated user channel remark',
|
|
278
|
+
responses: { 200: { description: 'Updated channel remark' } },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
'/api/channels/{name}/pin': {
|
|
282
|
+
put: {
|
|
283
|
+
summary: 'Pin or unpin a channel for the authenticated user',
|
|
284
|
+
responses: { 200: { description: 'Updated pin state' } },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
195
287
|
},
|
|
196
288
|
}
|
|
197
289
|
}
|
|
@@ -21,6 +21,8 @@ export function requiresUserAuth(path) {
|
|
|
21
21
|
path === '/api/download/check' ||
|
|
22
22
|
path === '/api/download' ||
|
|
23
23
|
path === '/api/download/cancel' ||
|
|
24
|
+
path === '/api/p2p/pull' ||
|
|
25
|
+
path === '/api/user/profile' ||
|
|
24
26
|
path === '/api/user/sync/start' ||
|
|
25
27
|
path === '/api/user/sync/status' ||
|
|
26
28
|
path === '/api/trash' ||
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { normalizeAddress } from '../../utils/auth.js'
|
|
2
|
+
import { badRequestOrAppError } from '../errors.js'
|
|
3
|
+
|
|
4
|
+
export function registerChannelRoutes(app, { engine }) {
|
|
5
|
+
app.post('/api/channels', async c => {
|
|
6
|
+
const body = await c.req.json()
|
|
7
|
+
if (!body.name || !body.name.trim()) {
|
|
8
|
+
return c.json({ error: 'name is required' }, 400)
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const channelOptions = {
|
|
12
|
+
ownerAddress: c.get('userAddress'),
|
|
13
|
+
displayName: body.displayName,
|
|
14
|
+
discover: true,
|
|
15
|
+
}
|
|
16
|
+
if (Object.prototype.hasOwnProperty.call(body, 'avatar')) {
|
|
17
|
+
channelOptions.avatar = body.avatar
|
|
18
|
+
}
|
|
19
|
+
const result = await engine.createChannel(
|
|
20
|
+
body.name.trim(),
|
|
21
|
+
body.type || 'personal',
|
|
22
|
+
channelOptions
|
|
23
|
+
)
|
|
24
|
+
return c.json({ success: true, ...result })
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return c.json({ error: err.message }, 400)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
app.get('/api/channels', c => {
|
|
31
|
+
return c.json(
|
|
32
|
+
engine.listChannels({
|
|
33
|
+
ownerAddress: c.get('userAddress'),
|
|
34
|
+
type: c.req.query('type'),
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const leaveChannelForRequest = async (c, channelIdentifier) => {
|
|
40
|
+
const name = String(channelIdentifier || '').trim()
|
|
41
|
+
if (!name) {
|
|
42
|
+
return c.json({ error: '频道标识不能为空' }, 400)
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const result = await engine.leaveChannel(name, {
|
|
46
|
+
ownerAddress: c.get('userAddress'),
|
|
47
|
+
})
|
|
48
|
+
return c.json({ success: true, channels: result })
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return c.json({ error: err.message }, 400)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
app.delete('/api/channels', async c => {
|
|
55
|
+
const body = await c.req.json().catch(() => ({}))
|
|
56
|
+
return leaveChannelForRequest(c, body.channelKey || body.name)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
app.get('/api/channels/:name/messages', async c => {
|
|
60
|
+
const name = c.req.param('name')
|
|
61
|
+
const limit = parseInt(c.req.query('limit') || '100', 10)
|
|
62
|
+
const offset = parseInt(c.req.query('offset') || '0', 10)
|
|
63
|
+
try {
|
|
64
|
+
const messages = await engine.getChannelMessages(name, {
|
|
65
|
+
limit,
|
|
66
|
+
offset,
|
|
67
|
+
ownerAddress: c.get('userAddress'),
|
|
68
|
+
})
|
|
69
|
+
return c.json(messages)
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return badRequestOrAppError(c, err)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
app.get('/api/channels/:name/members', c => {
|
|
76
|
+
try {
|
|
77
|
+
return c.json(
|
|
78
|
+
engine.getChannelMembers(c.req.param('name'), {
|
|
79
|
+
ownerAddress: c.get('userAddress'),
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return badRequestOrAppError(c, err)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
app.post('/api/channels/:name/messages', async c => {
|
|
88
|
+
const name = c.req.param('name')
|
|
89
|
+
const body = await c.req.json()
|
|
90
|
+
if (!body.content || !body.content.trim()) {
|
|
91
|
+
return c.json({ error: 'content is required' }, 400)
|
|
92
|
+
}
|
|
93
|
+
if (!body.author || !body.authorName) {
|
|
94
|
+
return c.json({ error: 'author and authorName are required' }, 400)
|
|
95
|
+
}
|
|
96
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(body.author)) {
|
|
97
|
+
return c.json({ error: 'Invalid author format' }, 400)
|
|
98
|
+
}
|
|
99
|
+
if (normalizeAddress(body.author) !== c.get('userAddress')) {
|
|
100
|
+
return c.json({ error: 'message author must match logged-in user' }, 403)
|
|
101
|
+
}
|
|
102
|
+
if (body.authorName.length > 50) {
|
|
103
|
+
return c.json({ error: 'authorName too long' }, 400)
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const messageOptions = {
|
|
107
|
+
ownerAddress: c.get('userAddress'),
|
|
108
|
+
attachment: body.attachment,
|
|
109
|
+
}
|
|
110
|
+
if (Object.prototype.hasOwnProperty.call(body, 'avatar')) {
|
|
111
|
+
messageOptions.avatar = body.avatar
|
|
112
|
+
}
|
|
113
|
+
const message = await engine.sendMessage(
|
|
114
|
+
name,
|
|
115
|
+
body.content,
|
|
116
|
+
body.author,
|
|
117
|
+
body.authorName,
|
|
118
|
+
messageOptions
|
|
119
|
+
)
|
|
120
|
+
return c.json({ success: true, message })
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return badRequestOrAppError(c, err)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
app.get('/api/channels/:name/peers', c => {
|
|
127
|
+
try {
|
|
128
|
+
return c.json(
|
|
129
|
+
engine.getChannelPeers(c.req.param('name'), {
|
|
130
|
+
ownerAddress: c.get('userAddress'),
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return badRequestOrAppError(c, err)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
app.put('/api/channels/:name/remark', async c => {
|
|
139
|
+
const name = c.req.param('name')
|
|
140
|
+
const body = await c.req.json()
|
|
141
|
+
try {
|
|
142
|
+
const remark = engine.setChannelRemark(name, body.remark, {
|
|
143
|
+
ownerAddress: c.get('userAddress'),
|
|
144
|
+
})
|
|
145
|
+
return c.json({ success: true, remark })
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return c.json({ error: err.message }, 400)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
app.put('/api/channels/:name/pin', async c => {
|
|
152
|
+
const name = c.req.param('name')
|
|
153
|
+
const body = await c.req.json()
|
|
154
|
+
try {
|
|
155
|
+
const pinned = engine.setChannelPinned(name, Boolean(body.pinned), {
|
|
156
|
+
ownerAddress: c.get('userAddress'),
|
|
157
|
+
})
|
|
158
|
+
return c.json({ success: true, pinned })
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return c.json({ error: err.message }, 400)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { parseMostLink, validateCidString } from '../../core/cid.js'
|
|
3
|
+
import { sanitizeFilename } from '../../utils/security.js'
|
|
4
|
+
import { badRequestOrAppError, errorJson } from '../errors.js'
|
|
5
|
+
import { validationErrorPayload } from '../routePolicy.js'
|
|
6
|
+
import { parseMultipartBusboy } from '../uploads.js'
|
|
7
|
+
import { getMimeType } from '../staticFiles.js'
|
|
8
|
+
|
|
9
|
+
export function registerFileRoutes(app, { engine, configStore, wsBroadcast }) {
|
|
10
|
+
app.get('/api/files', c => {
|
|
11
|
+
return c.json(
|
|
12
|
+
engine.listPublishedFiles({ ownerAddress: c.get('userAddress') })
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
app.post('/api/publish', async c => {
|
|
17
|
+
const req = c.env.incoming
|
|
18
|
+
const result = await parseMultipartBusboy(
|
|
19
|
+
req,
|
|
20
|
+
configStore.getNodeConfig().maxFileSizeBytes
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if (!result || !result.filename) {
|
|
24
|
+
return c.json({ error: 'No file provided' }, 400)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const publishResult = await engine.publishFile(
|
|
29
|
+
result.filePath,
|
|
30
|
+
result.filename,
|
|
31
|
+
{ ownerAddress: c.get('userAddress') }
|
|
32
|
+
)
|
|
33
|
+
return c.json({ success: true, ...publishResult })
|
|
34
|
+
} finally {
|
|
35
|
+
fs.unlink(result.filePath, () => {})
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
app.post('/api/download/check', async c => {
|
|
40
|
+
const body = await c.req.json()
|
|
41
|
+
if (!body.link) {
|
|
42
|
+
return c.json({ error: 'link is required' }, 400)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed = parseMostLink(body.link)
|
|
46
|
+
if (parsed.errorCode) {
|
|
47
|
+
return c.json(
|
|
48
|
+
validationErrorPayload(parsed.errorCode, parsed.details),
|
|
49
|
+
400
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const localAvailability = await engine.getLocalCidAvailability(body.link, {
|
|
54
|
+
ownerAddress: c.get('userAddress'),
|
|
55
|
+
})
|
|
56
|
+
if (localAvailability) {
|
|
57
|
+
return c.json({
|
|
58
|
+
success: true,
|
|
59
|
+
available: true,
|
|
60
|
+
cid: parsed.cid,
|
|
61
|
+
fileName: localAvailability.fileName,
|
|
62
|
+
size: Number(localAvailability.size) || null,
|
|
63
|
+
alreadyExists: true,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (engine.hasDownloadNameConflict(parsed.fileName)) {
|
|
68
|
+
return c.json(
|
|
69
|
+
{
|
|
70
|
+
error: `已有同名文件: ${parsed.fileName}`,
|
|
71
|
+
code: 'CONFLICT',
|
|
72
|
+
},
|
|
73
|
+
409
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const timeout =
|
|
79
|
+
body.timeout === undefined ? undefined : Number(body.timeout)
|
|
80
|
+
const result = await engine.checkDownloadAvailability(body.link, {
|
|
81
|
+
ownerAddress: c.get('userAddress'),
|
|
82
|
+
timeout: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined,
|
|
83
|
+
})
|
|
84
|
+
return c.json({ success: true, ...result })
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return errorJson(c, err)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
app.post('/api/download', async c => {
|
|
91
|
+
const body = await c.req.json()
|
|
92
|
+
if (!body.link) {
|
|
93
|
+
return c.json({ error: 'link is required' }, 400)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const taskId = `dl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
97
|
+
|
|
98
|
+
const parsed = parseMostLink(body.link)
|
|
99
|
+
if (parsed.errorCode) {
|
|
100
|
+
return c.json(
|
|
101
|
+
validationErrorPayload(parsed.errorCode, parsed.details),
|
|
102
|
+
400
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const localAvailability = await engine.getLocalCidAvailability(body.link, {
|
|
107
|
+
ownerAddress: c.get('userAddress'),
|
|
108
|
+
})
|
|
109
|
+
if (localAvailability) {
|
|
110
|
+
console.log(`[MostBox] CID content already exists locally: ${parsed.cid}`)
|
|
111
|
+
try {
|
|
112
|
+
const result = await engine.downloadFile(body.link, taskId, {
|
|
113
|
+
ownerAddress: c.get('userAddress'),
|
|
114
|
+
})
|
|
115
|
+
return c.json({ success: true, ...result })
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return errorJson(c, err)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (engine.hasDownloadNameConflict(parsed.fileName)) {
|
|
122
|
+
return c.json(
|
|
123
|
+
{
|
|
124
|
+
error: `已有同名文件: ${parsed.fileName}`,
|
|
125
|
+
code: 'CONFLICT',
|
|
126
|
+
},
|
|
127
|
+
409
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
engine
|
|
132
|
+
.downloadFile(body.link, taskId, { ownerAddress: c.get('userAddress') })
|
|
133
|
+
.catch(err => {
|
|
134
|
+
if (err.message === 'Download cancelled') {
|
|
135
|
+
wsBroadcast('download:cancelled', { taskId })
|
|
136
|
+
} else {
|
|
137
|
+
wsBroadcast('download:error', { taskId, error: err.message })
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return c.json({ success: true, taskId })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
app.post('/api/download/cancel', async c => {
|
|
145
|
+
const body = await c.req.json()
|
|
146
|
+
if (!body.taskId) {
|
|
147
|
+
return c.json({ error: 'taskId is required' }, 400)
|
|
148
|
+
}
|
|
149
|
+
engine.cancelDownload(body.taskId)
|
|
150
|
+
return c.json({ success: true })
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
app.delete('/api/files/:cid', async c => {
|
|
154
|
+
const cid = c.req.param('cid')
|
|
155
|
+
const cidValidation = validateCidString(cid)
|
|
156
|
+
if (!cidValidation.valid) {
|
|
157
|
+
return c.json(validationErrorPayload(cidValidation.errorCode), 400)
|
|
158
|
+
}
|
|
159
|
+
const result = await engine.deletePublishedFile(cid, {
|
|
160
|
+
ownerAddress: c.get('userAddress'),
|
|
161
|
+
})
|
|
162
|
+
return c.json(result)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
app.post('/api/files/:cid/cache', async c => {
|
|
166
|
+
const cid = c.req.param('cid')
|
|
167
|
+
const cidValidation = validateCidString(cid)
|
|
168
|
+
if (!cidValidation.valid) {
|
|
169
|
+
return c.json(validationErrorPayload(cidValidation.errorCode), 400)
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const body = await c.req.json().catch(() => ({}))
|
|
173
|
+
const timeout =
|
|
174
|
+
body.timeout === undefined ? undefined : Number(body.timeout)
|
|
175
|
+
const result = await engine.cacheFile(cid, {
|
|
176
|
+
ownerAddress: c.get('userAddress'),
|
|
177
|
+
timeout: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined,
|
|
178
|
+
taskId: body.taskId,
|
|
179
|
+
})
|
|
180
|
+
return c.json({ success: true, ...result })
|
|
181
|
+
} catch (err) {
|
|
182
|
+
return badRequestOrAppError(c, err)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
app.post('/api/move', async c => {
|
|
187
|
+
const body = await c.req.json()
|
|
188
|
+
if (!body.cid || !body.newFileName) {
|
|
189
|
+
return c.json({ error: 'cid and newFileName are required' }, 400)
|
|
190
|
+
}
|
|
191
|
+
const cidValidation = validateCidString(body.cid)
|
|
192
|
+
if (!cidValidation.valid) {
|
|
193
|
+
return c.json(validationErrorPayload(cidValidation.errorCode), 400)
|
|
194
|
+
}
|
|
195
|
+
const cleanFileName = sanitizeFilename(body.newFileName)
|
|
196
|
+
if (
|
|
197
|
+
!cleanFileName ||
|
|
198
|
+
cleanFileName === 'unnamed' ||
|
|
199
|
+
body.newFileName.length > 255
|
|
200
|
+
) {
|
|
201
|
+
return c.json({ error: 'Invalid filename' }, 400)
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const result = engine.moveFile(body.cid, cleanFileName, {
|
|
205
|
+
ownerAddress: c.get('userAddress'),
|
|
206
|
+
})
|
|
207
|
+
return c.json({ success: true, ...result })
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return badRequestOrAppError(c, err)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
app.get('/api/files/:cid/download', async c => {
|
|
214
|
+
const cid = c.req.param('cid')
|
|
215
|
+
const cidValidation = validateCidString(cid)
|
|
216
|
+
if (!cidValidation.valid) {
|
|
217
|
+
return c.json(validationErrorPayload(cidValidation.errorCode), 400)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const rangeHeader = c.req.header('range')
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
if (rangeHeader) {
|
|
224
|
+
const rangeMatch = rangeHeader.match(/bytes=(\d+)-(\d*)/)
|
|
225
|
+
if (rangeMatch) {
|
|
226
|
+
const start = parseInt(rangeMatch[1], 10)
|
|
227
|
+
const end = rangeMatch[2] ? parseInt(rangeMatch[2], 10) : undefined
|
|
228
|
+
const offset = start
|
|
229
|
+
const limit = end !== undefined ? end - start + 1 : undefined
|
|
230
|
+
|
|
231
|
+
const result = await engine.readFileRaw(cid, {
|
|
232
|
+
offset,
|
|
233
|
+
limit,
|
|
234
|
+
public: true,
|
|
235
|
+
})
|
|
236
|
+
const contentType = getMimeType(result.fileName)
|
|
237
|
+
|
|
238
|
+
c.header('Content-Type', contentType)
|
|
239
|
+
c.header('Content-Length', String(result.buffer.length))
|
|
240
|
+
c.header(
|
|
241
|
+
'Content-Range',
|
|
242
|
+
`bytes ${offset}-${offset + result.buffer.length - 1}/${result.totalSize}`
|
|
243
|
+
)
|
|
244
|
+
c.header('Accept-Ranges', 'bytes')
|
|
245
|
+
c.status(206)
|
|
246
|
+
return c.body(result.buffer)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const result = await engine.readFileRaw(cid, {
|
|
251
|
+
public: true,
|
|
252
|
+
})
|
|
253
|
+
const contentType = getMimeType(result.fileName)
|
|
254
|
+
c.header('Content-Type', contentType)
|
|
255
|
+
c.header('Content-Length', String(result.totalSize))
|
|
256
|
+
c.header('Accept-Ranges', 'bytes')
|
|
257
|
+
c.header(
|
|
258
|
+
'Content-Disposition',
|
|
259
|
+
`inline; filename="${encodeURIComponent(result.fileName)}"`
|
|
260
|
+
)
|
|
261
|
+
return c.body(result.buffer)
|
|
262
|
+
} catch (err) {
|
|
263
|
+
if (err.message === 'File not found') {
|
|
264
|
+
return c.json({ error: err.message }, 404)
|
|
265
|
+
}
|
|
266
|
+
return c.json({ error: err.message }, 400)
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
app.get('/api/trash', c => {
|
|
271
|
+
return c.json(engine.listTrashFiles({ ownerAddress: c.get('userAddress') }))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
app.post('/api/trash/:cid/restore', async c => {
|
|
275
|
+
const cid = c.req.param('cid')
|
|
276
|
+
const cidValidation = validateCidString(cid)
|
|
277
|
+
if (!cidValidation.valid) {
|
|
278
|
+
return c.json(validationErrorPayload(cidValidation.errorCode), 400)
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const result = await engine.restoreTrashFile(cid, {
|
|
282
|
+
ownerAddress: c.get('userAddress'),
|
|
283
|
+
})
|
|
284
|
+
return c.json({ success: true, files: result })
|
|
285
|
+
} catch (err) {
|
|
286
|
+
return c.json({ error: err.message }, 400)
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
app.delete('/api/trash/:cid', async c => {
|
|
291
|
+
const cid = c.req.param('cid')
|
|
292
|
+
const cidValidation = validateCidString(cid)
|
|
293
|
+
if (!cidValidation.valid) {
|
|
294
|
+
return c.json(validationErrorPayload(cidValidation.errorCode), 400)
|
|
295
|
+
}
|
|
296
|
+
const result = await engine.permanentDeleteTrashFile(cid, {
|
|
297
|
+
ownerAddress: c.get('userAddress'),
|
|
298
|
+
})
|
|
299
|
+
return c.json({ success: true, trashFiles: result })
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
app.delete('/api/trash', async c => {
|
|
303
|
+
const result = await engine.emptyTrash({
|
|
304
|
+
ownerAddress: c.get('userAddress'),
|
|
305
|
+
})
|
|
306
|
+
return c.json({ success: true, trashFiles: result })
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
app.post('/api/files/:cid/star', async c => {
|
|
310
|
+
const cid = c.req.param('cid')
|
|
311
|
+
const cidValidation = validateCidString(cid)
|
|
312
|
+
if (!cidValidation.valid) {
|
|
313
|
+
return c.json(validationErrorPayload(cidValidation.errorCode), 400)
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const result = engine.toggleStarred(cid, {
|
|
317
|
+
ownerAddress: c.get('userAddress'),
|
|
318
|
+
})
|
|
319
|
+
return c.json({ success: true, ...result })
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return c.json({ error: err.message }, 400)
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
app.post('/api/folder/rename', async c => {
|
|
326
|
+
const body = await c.req.json()
|
|
327
|
+
if (!body.oldPath || !body.newPath) {
|
|
328
|
+
return c.json({ error: 'oldPath and newPath are required' }, 400)
|
|
329
|
+
}
|
|
330
|
+
if (body.oldPath.length > 500 || body.newPath.length > 500) {
|
|
331
|
+
return c.json({ error: 'Path too long' }, 400)
|
|
332
|
+
}
|
|
333
|
+
if (body.oldPath.includes('..') || body.newPath.includes('..')) {
|
|
334
|
+
return c.json({ error: 'Path traversal not allowed' }, 400)
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const result = engine.renameFolder(body.oldPath, body.newPath, {
|
|
338
|
+
ownerAddress: c.get('userAddress'),
|
|
339
|
+
})
|
|
340
|
+
return c.json({ success: true, ...result })
|
|
341
|
+
} catch (err) {
|
|
342
|
+
return badRequestOrAppError(c, err)
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
}
|