feat: Archipelago public channel (Tor), FileBrowser auto-login
Public Channel: - "Archipelago" channel in Mesh — broadcasts to all federation peers over Tor - Shows received messages from all peers with pubkey label - Auto-polls every 15s for new messages - Orange-branded channel icon with unread badge - Send handler routes to Tor broadcast when arch channel is active FileBrowser Auto-Login: - All filebrowser-client methods now call ensureAuth() before requests - Auto-authenticates with default credentials if not logged in - Fixes "files don't work when FileBrowser hasn't been logged into" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c5417640a2
commit
c4853fe746
@ -73,7 +73,15 @@ class FileBrowserClient {
|
||||
return h
|
||||
}
|
||||
|
||||
/** Ensure we're authenticated before making a request. Auto-logins if needed. */
|
||||
private async ensureAuth(): Promise<void> {
|
||||
if (this.token) return
|
||||
const ok = await this.login()
|
||||
if (!ok) throw new Error('FileBrowser authentication failed — please open Cloud to log in')
|
||||
}
|
||||
|
||||
async listDirectory(path: string): Promise<FileBrowserItem[]> {
|
||||
await this.ensureAuth()
|
||||
const safePath = sanitizePath(path)
|
||||
const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, {
|
||||
headers: this.headers(),
|
||||
@ -100,6 +108,7 @@ class FileBrowserClient {
|
||||
* Use this for img/video/audio src attributes and download links.
|
||||
*/
|
||||
async fetchBlobUrl(path: string): Promise<string> {
|
||||
await this.ensureAuth()
|
||||
const safePath = sanitizePath(path)
|
||||
const res = await fetch(`${this.baseUrl}/api/raw${safePath}`, {
|
||||
headers: this.headers(),
|
||||
@ -125,6 +134,7 @@ class FileBrowserClient {
|
||||
}
|
||||
|
||||
async upload(dirPath: string, file: File): Promise<void> {
|
||||
await this.ensureAuth()
|
||||
const sanitized = sanitizePath(dirPath)
|
||||
const safePath = sanitized.endsWith('/') ? sanitized : `${sanitized}/`
|
||||
const encodedName = encodeURIComponent(file.name)
|
||||
@ -143,6 +153,7 @@ class FileBrowserClient {
|
||||
}
|
||||
|
||||
async createFolder(parentPath: string, name: string): Promise<void> {
|
||||
await this.ensureAuth()
|
||||
const sanitized = sanitizePath(parentPath)
|
||||
const safePath = sanitized.endsWith('/') ? sanitized : `${sanitized}/`
|
||||
const sanitizedName = name.replace(/\.\./g, '').replace(/\//g, '')
|
||||
@ -154,6 +165,7 @@ class FileBrowserClient {
|
||||
}
|
||||
|
||||
async deleteItem(path: string): Promise<void> {
|
||||
await this.ensureAuth()
|
||||
const safePath = sanitizePath(path)
|
||||
const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, {
|
||||
method: 'DELETE',
|
||||
|
||||
@ -35,6 +35,54 @@ let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
// The Public channel (always available on Meshcore)
|
||||
const publicChannel = { index: 0, name: 'Public' }
|
||||
|
||||
// Archipelago Channel — Tor-based messaging to all federated/peered nodes
|
||||
const archChannelActive = ref(false)
|
||||
const archMessages = ref<Array<{ from_pubkey: string; message: string; timestamp: string }>>([])
|
||||
const archUnread = ref(0)
|
||||
let archPollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function openArchChannel() {
|
||||
activeChatPeer.value = null
|
||||
activeChatChannel.value = null
|
||||
archChannelActive.value = true
|
||||
archUnread.value = 0
|
||||
mobileShowChat.value = true
|
||||
loadArchMessages()
|
||||
if (!archPollInterval) {
|
||||
archPollInterval = setInterval(loadArchMessages, 15000)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadArchMessages() {
|
||||
try {
|
||||
const res = await rpcClient.getReceivedMessages()
|
||||
archMessages.value = res.messages || []
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
async function sendArchMessage() {
|
||||
if (!messageText.value.trim()) return
|
||||
sendError.value = ''
|
||||
try {
|
||||
// Broadcast to all federated peers over Tor
|
||||
const nodes = await rpcClient.federationListNodes()
|
||||
const msg = messageText.value.trim()
|
||||
let sent = 0
|
||||
for (const node of nodes.nodes) {
|
||||
try {
|
||||
await rpcClient.sendMessageToPeer(node.onion || node.did, msg)
|
||||
sent++
|
||||
} catch { /* some peers may be offline */ }
|
||||
}
|
||||
messageText.value = ''
|
||||
if (sent === 0) sendError.value = 'No peers reachable'
|
||||
// Reload to see the message
|
||||
setTimeout(loadArchMessages, 2000)
|
||||
} catch (e) {
|
||||
sendError.value = e instanceof Error ? e.message : 'Send failed'
|
||||
}
|
||||
}
|
||||
|
||||
const togglingOffGrid = ref(false)
|
||||
const peerSessionInfo = ref<SessionStatus | null>(null)
|
||||
|
||||
@ -244,21 +292,38 @@ onUnmounted(() => {
|
||||
|
||||
// Active chat name for the header
|
||||
const activeChatName = computed(() => {
|
||||
if (archChannelActive.value) return 'Archipelago'
|
||||
if (activeChatChannel.value) return activeChatChannel.value.name
|
||||
if (activeChatPeer.value) return activeChatPeer.value.advert_name
|
||||
return ''
|
||||
})
|
||||
|
||||
const activeChatSub = computed(() => {
|
||||
if (activeChatChannel.value) return 'Mesh channel'
|
||||
if (archChannelActive.value) return 'All nodes over Tor'
|
||||
if (activeChatChannel.value) return 'Mesh radio'
|
||||
if (activeChatPeer.value) return truncatePubkey(activeChatPeer.value.pubkey_hex)
|
||||
return ''
|
||||
})
|
||||
|
||||
const hasActiveChat = computed(() => !!activeChatPeer.value || !!activeChatChannel.value)
|
||||
const hasActiveChat = computed(() => !!activeChatPeer.value || !!activeChatChannel.value || archChannelActive.value)
|
||||
|
||||
// Messages filtered to the active chat
|
||||
const chatMessages = computed(() => {
|
||||
if (archChannelActive.value) {
|
||||
// Map Tor messages to mesh message format for rendering
|
||||
return archMessages.value.map((m, i) => ({
|
||||
id: i,
|
||||
peer_contact_id: -99,
|
||||
peer_name: m.from_pubkey.slice(0, 12) + '...',
|
||||
direction: 'received' as const,
|
||||
plaintext: m.message,
|
||||
timestamp: m.timestamp,
|
||||
delivered: true,
|
||||
encrypted: false,
|
||||
message_type: undefined,
|
||||
typed_payload: undefined,
|
||||
}))
|
||||
}
|
||||
if (activeChatChannel.value) {
|
||||
// Channel messages have negative contact_id = -(channel_index + 1)
|
||||
const chanId = -(activeChatChannel.value.index + 1)
|
||||
@ -289,6 +354,7 @@ const sortedPeers = computed(() => {
|
||||
function openChat(peer: MeshPeer) {
|
||||
activeChatPeer.value = peer
|
||||
activeChatChannel.value = null
|
||||
archChannelActive.value = false
|
||||
sendError.value = ''
|
||||
messageText.value = ''
|
||||
activeTab.value = 'chat'
|
||||
@ -300,6 +366,7 @@ function openChat(peer: MeshPeer) {
|
||||
function openChannelChat(channel: { index: number; name: string }) {
|
||||
activeChatChannel.value = channel
|
||||
activeChatPeer.value = null
|
||||
archChannelActive.value = false
|
||||
sendError.value = ''
|
||||
messageText.value = ''
|
||||
activeTab.value = 'chat'
|
||||
@ -310,11 +377,18 @@ function openChannelChat(channel: { index: number; name: string }) {
|
||||
function closeChat() {
|
||||
activeChatPeer.value = null
|
||||
activeChatChannel.value = null
|
||||
archChannelActive.value = false
|
||||
if (archPollInterval) { clearInterval(archPollInterval); archPollInterval = null }
|
||||
mobileShowChat.value = false
|
||||
mesh.clearViewingChat()
|
||||
}
|
||||
|
||||
async function handleSendMessage() {
|
||||
if (archChannelActive.value) {
|
||||
await sendArchMessage()
|
||||
nextTick(() => scrollChatToBottom())
|
||||
return
|
||||
}
|
||||
if (!activeChatPeer.value || !messageText.value.trim()) return
|
||||
sendError.value = ''
|
||||
try {
|
||||
@ -498,7 +572,20 @@ function truncatePubkey(hex: string | null): string {
|
||||
</div>
|
||||
|
||||
<div v-else class="mesh-peer-list">
|
||||
<!-- Public channel -->
|
||||
<!-- Archipelago Channel (Tor — all nodes) -->
|
||||
<div
|
||||
class="mesh-peer-row is-channel"
|
||||
:class="{ active: archChannelActive }"
|
||||
@click="openArchChannel"
|
||||
>
|
||||
<div class="mesh-peer-avatar channel" style="background: rgba(251,146,60,0.2); color: #fb923c;">A</div>
|
||||
<div class="mesh-peer-info">
|
||||
<div class="mesh-peer-name">Archipelago</div>
|
||||
<div class="mesh-peer-sub">All nodes over Tor</div>
|
||||
</div>
|
||||
<span v-if="archUnread > 0" class="ml-auto text-[10px] px-1.5 py-0.5 rounded-full bg-orange-500/30 text-orange-300">{{ archUnread }}</span>
|
||||
</div>
|
||||
<!-- Public channel (Mesh radio) -->
|
||||
<div
|
||||
class="mesh-peer-row is-channel"
|
||||
:class="{ active: activeChatChannel?.index === 0 }"
|
||||
@ -507,7 +594,7 @@ function truncatePubkey(hex: string | null): string {
|
||||
<div class="mesh-peer-avatar channel">#</div>
|
||||
<div class="mesh-peer-info">
|
||||
<div class="mesh-peer-name">Public</div>
|
||||
<div class="mesh-peer-sub">Mesh channel</div>
|
||||
<div class="mesh-peer-sub">Mesh radio</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user