BasBBS macOS App 完整设计方案
BasBBS 是一个纯客户端的分布式 P2P 论坛系统 App,针对 macOS 平台开发,用户体验模仿 Discord,包括分栏布局、实时聊天、频道导航等。数据完全通过 P2P 方式分布和同步,无需中央服务器。采用 P2SP(Peer-to-Super-Peer)架构,其中 Super-Peer 使用 NATS 服务器(nats-server)作为发现和通讯中继,同时保留纯 P2P 功能通过 NATS 的 leaf nodes 实现直接 peer 连接。身份认证基于自签名证书,数据存储使用 SQLite,开发语言优先 Swift 6 以利用其增强的并发性和严格模式。UI 使用 SwiftUI 模仿 Discord 的三栏设计。
这个方案覆盖架构、功能细节、代码示例、UI 布局、开发流程、安全性考虑、测试和部署,以及参考链接。设计强调去中心化:所有论坛数据(如帖子)在本地 SQLite 存储,消息通过 NATS JetStream 广播和拉取,确保持久化和实时性。
1. 整体架构
BasBBS 的架构分为以下层,确保模块化和可维护性:
- UI 层:SwiftUI 构建,支持 macOS 窗口管理和暗黑模式。使用 NavigationSplitView 实现 Discord-like 分栏。
- 网络层:使用 nats.swift 库(v0.4.0,支持 JetStream)处理通讯。P2SP 模式下,连接 nats-server 作为 Super-Peer 用于发现和中继;P2P 模式下,使用 leaf nodes 直接连接 peers。
- P2SP:Super-Peer(nats-server)处理节点发现(通过 "discovery" 主题订阅/发布 peer 信息,如 IP、公钥)和消息路由。JetStream 启用于服务器,用于持久流(streams)存储历史消息。
- P2P 保留:fallback 到直接连接,使用 NATS 的 auto-discovery 和 WebSocket。Leaf nodes 允许 peers 扩展集群,形成 mesh 网络。
- 数据层:SQLite 数据库(~/.basbbs/database.sqlite),使用 sqlite.swift (v0.15.4) 操作。存储帖子、消息、用户身份和频道。
- 身份与安全层:自签名证书(RSA/ED25519),消息用私钥签名。使用 Swift 的 SecKey API 或 CryptoKit 处理签名/验证。
- 初始化流程:
- 检查/创建 ~/.basbbs/ 目录。
- 生成证书:调用 ssh-keygen 生成私钥/公钥(~/.basbbs/id_ed25519 和 .pub)。
- 配置网络:用户选择 P2SP(输入 Super-Peer URL)或 P2P(输入种子列表)。
- 连接 NATS,发布发现消息,订阅频道。
- 离线支持:本地 SQLite 缓存历史;重连时,通过 JetStream pull consumer 同步缺失消息。
架构支持扩展:社区可托管公共 Super-Peer 以简化发现。
2. 身份认证与 Email 绑定
证书生成:
- 使用 Swift 的 Process API 调用系统 ssh-keygen,无需额外依赖。
- 示例代码(Swift 6 actor 确保并发安全):
import Foundation
actor KeyManager {
func generateKeys() async throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh-keygen")
process.arguments = ["-t", "ed25519", "-f", "\(NSHomeDirectory())/.basbbs/id_ed25519", "-N", "", "-q"]
try process.run()
process.waitUntilExit()
if process.terminationStatus != 0 {
throw KeyGenError(status: process.terminationStatus)
}
}
}
enum KeyGenError: Error {
case status(Int32)
}
- 公钥哈希作为用户 ID,用于 NATS 主题和验证。
Email 绑定:
- 用户发布特殊帖子(subject: "email-binding.<user_id>"),内容 JSON: {"email": "user@example.com", "timestamp": Date()},用私钥签名(CryptoKit: Signer.sign(data))。
- 其他 peers 收到后,验证签名(Verifier.verify(signature, data, pubKey)),若有效则更新本地 SQLite 的 users 表。
- 数据库 schema 示例:
import SQLite
let db = try Connection("\(NSHomeDirectory())/.basbbs/database.sqlite")
let users = Table("users")
let id = Expression<String>("id") // 公钥哈希
let email = Expression<String?>("email")
let pubkey = Expression<String>("pubkey")
let verified = Expression<Bool>("verified")
try db.run(users.create(ifNotExists: true) { t in
t.column(id, primaryKey: true)
t.column(email)
t.column(pubkey)
t.column(verified, defaultValue: false)
})
- 签名验证失败时,丢弃消息并日志。
3. 数据存储与同步
- SQLite 操作:使用 sqlite.swift (v0.15.4),支持事务、索引和 FTS(全文搜索)用于论坛搜索。
- 表设计:
- posts: id (UUID PRIMARY KEY), content (text), sender_id (string), signature (blob), timestamp (datetime), channel_id (string)
- channels: id (string PRIMARY KEY), name (string), description (text), members_json (text)
- messages: id (UUID PRIMARY KEY), post_id (foreign key), content (text), etc.
- 示例插入/查询(使用 Swift 6 async):
import SQLite
import Combine
actor DataStore {
private let db: Connection
init() throws {
let path = "\(NSHomeDirectory())/.basbbs/database.sqlite"
db = try Connection(path)
try createTables()
}
private func createTables() throws {
// Create tables as above
}
func insertPost(post: PostModel) async throws {
let posts = Table("posts")
let insert = posts.insert(
id <- post.id.uuidString,
content <- post.content,
sender_id <- post.senderId,
signature <- post.signature,
timestamp <- post.timestamp,
channel_id <- post.channelId
)
try db.run(insert)
}
func fetchPosts(for channel: String) async throws -> [PostModel] {
let posts = Table("posts")
let query = posts.filter(channel_id == channel).order(timestamp.desc)
return try db.prepare(query).map { row in
// Map to PostModel
}
}
}
- 同步机制:
- P2SP:JetStream 流("forum-history",subjects: "forum.>")持久化消息。发布帖子到流;订阅时,创建 pull consumer,fetch 未消费消息,验证后插入 SQLite。
- P2P:自定义历史请求:发布 "history.request.<channel_id>.<seq>",peers 回复序列化数据片段。
- 冲突解决:基于时间戳和签名,使用最新有效签名覆盖。
4. 通讯协议与 NATS 集成
- NATS 客户端:nats.swift v0.4.0,支持 JetStream 管理(streams/consumers)和基本 pub-sub。
- 连接与模式切换:
import Nats
enum NetworkMode {
case p2sp, p2p
}
actor NATSManager {
private var connection: NatsConnection?
private var jetStream: JetStream?
func connect(mode: NetworkMode, superPeerURL: URL?, p2pSeeds: [URL]) async throws {
let options = NatsClientOptions()
.enableEcho(false)
.enableVerbose(true)
.enableReconnect(true)
if mode == .p2sp, let url = superPeerURL {
options.url(url)
} else {
options.urls(p2pSeeds)
}
connection = try await NatsConnection.connect(with: options)
jetStream = connection?.jetStream()
// JetStream 配置
let streamConfig = StreamConfig(name: "forum-history", subjects: ["forum.>"], storage: .file)
try await jetStream?.addStream(config: streamConfig)
// 发现:发布 peer info
let payload = try JSONEncoder().encode(PeerInfo(id: userID, pubKey: pubKeyString))
try await connection?.publish(payload: payload, subject: "discovery.\(userID)")
}
func publishPost(channel: String, post: PostModel) async throws {
let signedPayload = try post.signedData() // JSON + signature
try await connection?.publish(payload: signedPayload, subject: "forum.\(channel).post")
}
func subscribeToChannel(channel: String) async throws -> AsyncThrowingStream<PostModel, Error> {
let sub = try await connection?.subscribe(subject: "forum.\(channel).>")
return AsyncThrowingStream { continuation in
Task {
for try await msg in sub {
// 验证签名,解析到 PostModel
if let verifiedPost = try? PostModel(from: msg, verify: true) {
continuation.yield(verifiedPost)
}
}
}
}
}
func syncHistory(channel: String) async throws {
let consumerConfig = ConsumerConfig(durableName: "basbbs-consumer-\(channel)", deliverPolicy: .all)
let consumer = try await jetStream?.addConsumer(stream: "forum-history", config: consumerConfig)
let messages = try await consumer?.fetch(max: 500) for msg in messages {
// 插入 DataStore
}
}
}
- nats-server 作为 Super-Peer:
5. 界面设计(模仿 Discord)
使用 SwiftUI 构建,优先 Swift 6 的严格并发(启用 Strict Concurrency 检查以确保数据 race-free)。
- 分栏布局:NavigationSplitView 三列。
- 左侧栏:List 显示服务器(P2P 群组)和频道。从 DataStore 动态加载,支持搜索。
- 中间栏:ScrollView 显示帖子/消息,支持无限滚动(@State 更新从 NATS stream)。聊天输入:TextField + Button,发送签名消息。
- 右侧栏:VStack 显示用户详情、在线 peers(从 NATS presence)和附件。
- 关键视图示例:
import SwiftUI
struct MainView: View {
@State private var selectedChannel: String?
@StateObject private var natsManager = NATSManager.shared
@State private var messages: [PostModel] = []
var body: some View {
NavigationSplitView {
// 左侧: Channels
List(selection: $selectedChannel) {
ForEach(channels) { channel in
Text(channel.name)
}
}
.navigationTitle("Channels")
} content: {
// 中间: Chat
if let channel = selectedChannel {
VStack {
ScrollView {
LazyVStack {
ForEach(messages) { message in
Text(message.body)
}
}
}
HStack {
TextField("Type message", text: .constant(""))
Button("Send") {
// Publish via NATS
}
}
}
.onAppear {
Task {
messages = try await DataStore.fetchPosts(for: channel)
for await newMsg in try await natsManager.subscribeToChannel(channel) {
messages.append(newMsg)
}
}
}
}
} detail: {
// 右侧: Details
VStack {
Text("User Info")
List(peers) { peer in
Text(peer.id)
}
}
}
.toolbar {
ToolbarItem {
Menu("Network") {
Picker("Mode", selection: $networkMode) {
Text("P2SP").tag(NetworkMode.p2sp)
Text("P2P").tag(NetworkMode.p2p)
}
}
}
}
}
}
- 其他 UI 元素:
- 顶部搜索栏:搜索帖子/用户,使用 SQLite FTS。
- 通知:UserNotifications for 新消息。
- 主题:支持系统暗黑模式,颜色方案模仿 Discord(#36393f 背景)。
- 设置视图:配置网络模式、输入 URL、生成/导入密钥。
6. 开发、测试、安全与部署
- 开发注意:
- 依赖:Swift Package Manager 添加 nats.swift (from: "0.4.0")、sqlite.swift (from: "0.15.4")。
- Swift 6 最佳实践:启用 Strict Concurrency,use actors for shared state,async/await for network/DB。 项目从 Apple 教程创建 macOS App target。
- App 生命周期:集成 suspend/resume in AppDelegate for NATS 连接。
- 安全性:
- 所有消息签名验证,防止 MITM。
- TLS 启用于 NATS(客户端 options.enableTLS(true))。
- 私钥存储在 Keychain(SecItemAdd)。
- 审计:nats-server 经 2025 Trail of Bits 审计,无已知漏洞。
- 测试:
- 单元:XCTest for KeyManager、DataStore。
- 集成:模拟 NATS 服务器(本地运行 nats-server),测试 P2SP/P2P 同步。
- E2E:多实例测试消息广播。
- 部署:
- macOS App Bundle,支持 sandbox(~/.basbbs 在用户目录)。
- 分发:通过 Mac App Store 或独立 DMG。
- 用户指导:安装 nats-server for Super-Peer(可选)。
参考链接 URL