435 lines
No EOL
13 KiB
Markdown
435 lines
No EOL
13 KiB
Markdown
# CommunityPool Component
|
|
|
|
The CommunityPool component manages the donation and distribution of license keys within the VoiceUwu community.
|
|
|
|
## Pseudocode
|
|
|
|
```swift
|
|
class CommunityPoolManager {
|
|
// CloudKit container for community features
|
|
private let container = CKContainer(identifier: "iCloud.com.voiceuwu.community")
|
|
private let publicDB: CKDatabase
|
|
|
|
// Local cache
|
|
@Published private(set) var poolStatus: PoolStatus?
|
|
@Published private(set) var userRequests: [KeyRequest] = []
|
|
@Published private(set) var isLoading = false
|
|
|
|
init() {
|
|
self.publicDB = container.publicCloudDatabase
|
|
loadPoolStatus()
|
|
}
|
|
|
|
// MARK: - Pool Status
|
|
|
|
struct PoolStatus {
|
|
let totalKeys: Int
|
|
let level1Keys: Int
|
|
let level2Keys: Int
|
|
let recentDonations: [DonationInfo]
|
|
let lastUpdated: Date
|
|
}
|
|
|
|
func loadPoolStatus() async {
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
// Query pool statistics
|
|
let query = CKQuery(
|
|
recordType: "PoolStatus",
|
|
predicate: NSPredicate(value: true)
|
|
)
|
|
|
|
let results = try await publicDB.records(matching: query)
|
|
|
|
if let record = results.matchResults.first?.1.get() {
|
|
poolStatus = PoolStatus(
|
|
totalKeys: record["totalKeys"] as? Int ?? 0,
|
|
level1Keys: record["level1Keys"] as? Int ?? 0,
|
|
level2Keys: record["level2Keys"] as? Int ?? 0,
|
|
recentDonations: parseRecentDonations(record),
|
|
lastUpdated: record.modificationDate ?? Date()
|
|
)
|
|
}
|
|
} catch {
|
|
print("Failed to load pool status: \(error)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Donation System
|
|
|
|
struct KeyDonation {
|
|
let keys: [String]
|
|
let donorName: String?
|
|
let message: String?
|
|
let isAnonymous: Bool
|
|
}
|
|
|
|
func donateKeys(_ donation: KeyDonation) async throws {
|
|
// Validate keys before donation
|
|
for key in donation.keys {
|
|
guard KeyValidator().validate(key).isValid else {
|
|
throw DonationError.invalidKey(key)
|
|
}
|
|
}
|
|
|
|
// Create donation records
|
|
for key in donation.keys {
|
|
let record = CKRecord(recordType: "KeyDonation")
|
|
|
|
// Store encrypted key
|
|
record["encryptedKey"] = encrypt(key)
|
|
record["keyLevel"] = extractLevel(from: key).rawValue
|
|
record["donationDate"] = Date()
|
|
|
|
// Donor info (anonymous if requested)
|
|
if !donation.isAnonymous {
|
|
record["donorName"] = donation.donorName ?? "Unknown"
|
|
}
|
|
record["donorHash"] = generateDonorHash()
|
|
|
|
// Optional message
|
|
if let message = donation.message {
|
|
record["message"] = sanitizeMessage(message)
|
|
}
|
|
|
|
try await publicDB.save(record)
|
|
}
|
|
|
|
// Remove keys from local inventory
|
|
KeyManager.shared.removeFromInventory(donation.keys)
|
|
|
|
// Update local cache
|
|
await loadPoolStatus()
|
|
}
|
|
|
|
// MARK: - Request System
|
|
|
|
struct KeyRequest {
|
|
let id: String
|
|
let category: RequestCategory
|
|
let reason: String
|
|
let requestDate: Date
|
|
let status: RequestStatus
|
|
let level: LicenseLevel
|
|
}
|
|
|
|
enum RequestCategory: String, CaseIterable {
|
|
case student = "student"
|
|
case educator = "educator"
|
|
case financial = "financial"
|
|
case contributor = "contributor"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .student: return "Student"
|
|
case .educator: return "Educator"
|
|
case .financial: return "Financial Need"
|
|
case .contributor: return "Open Source Contributor"
|
|
}
|
|
}
|
|
|
|
var verificationRequired: Bool {
|
|
switch self {
|
|
case .student, .educator: return true
|
|
case .financial, .contributor: return false
|
|
}
|
|
}
|
|
}
|
|
|
|
enum RequestStatus: String {
|
|
case pending = "pending"
|
|
case approved = "approved"
|
|
case denied = "denied"
|
|
case fulfilled = "fulfilled"
|
|
}
|
|
|
|
func submitRequest(
|
|
category: RequestCategory,
|
|
reason: String,
|
|
level: LicenseLevel,
|
|
verification: VerificationData?
|
|
) async throws {
|
|
// Check request limits
|
|
let existingRequests = try await loadUserRequests()
|
|
|
|
// One request per 30 days
|
|
if let lastRequest = existingRequests.last {
|
|
let daysSinceLastRequest = Date().timeIntervalSince(lastRequest.requestDate) / 86400
|
|
if daysSinceLastRequest < 30 {
|
|
throw RequestError.tooSoon(daysRemaining: Int(30 - daysSinceLastRequest))
|
|
}
|
|
}
|
|
|
|
// Create request record
|
|
let record = CKRecord(recordType: "KeyRequest")
|
|
record["deviceID"] = getAnonymousDeviceID()
|
|
record["category"] = category.rawValue
|
|
record["reason"] = sanitizeReason(reason)
|
|
record["requestDate"] = Date()
|
|
record["status"] = RequestStatus.pending.rawValue
|
|
record["level"] = level.rawValue
|
|
|
|
// Add verification if required
|
|
if category.verificationRequired {
|
|
guard let verification = verification else {
|
|
throw RequestError.verificationRequired
|
|
}
|
|
record["verification"] = try encodeVerification(verification)
|
|
}
|
|
|
|
try await publicDB.save(record)
|
|
|
|
// Reload requests
|
|
userRequests = try await loadUserRequests()
|
|
}
|
|
|
|
// MARK: - Distribution Algorithm
|
|
|
|
func checkForApprovedKeys() async throws -> String? {
|
|
let deviceID = getAnonymousDeviceID()
|
|
|
|
// Query for approved requests
|
|
let predicate = NSPredicate(
|
|
format: "deviceID == %@ AND status == %@",
|
|
deviceID,
|
|
RequestStatus.approved.rawValue
|
|
)
|
|
|
|
let query = CKQuery(recordType: "KeyRequest", predicate: predicate)
|
|
let results = try await publicDB.records(matching: query)
|
|
|
|
for (_, result) in results.matchResults {
|
|
if case .success(let record) = result,
|
|
let keyID = record["assignedKeyID"] as? String {
|
|
|
|
// Fetch the actual key
|
|
let key = try await fetchAssignedKey(keyID)
|
|
|
|
// Mark as fulfilled
|
|
record["status"] = RequestStatus.fulfilled.rawValue
|
|
try await publicDB.save(record)
|
|
|
|
return key
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func fetchAssignedKey(_ keyID: String) async throws -> String {
|
|
// Fetch encrypted key from secure storage
|
|
let recordID = CKRecord.ID(recordName: keyID)
|
|
let record = try await publicDB.record(for: recordID)
|
|
|
|
guard let encryptedKey = record["encryptedKey"] as? String else {
|
|
throw DistributionError.keyNotFound
|
|
}
|
|
|
|
return decrypt(encryptedKey)
|
|
}
|
|
|
|
// MARK: - Privacy & Security
|
|
|
|
private func getAnonymousDeviceID() -> String {
|
|
// Generate consistent anonymous ID
|
|
if let existing = UserDefaults.standard.string(forKey: "anonymousDeviceID") {
|
|
return existing
|
|
}
|
|
|
|
let newID = UUID().uuidString
|
|
UserDefaults.standard.set(newID, forKey: "anonymousDeviceID")
|
|
return newID
|
|
}
|
|
|
|
private func generateDonorHash() -> String {
|
|
// One-way hash for donor tracking without identification
|
|
let deviceID = getAnonymousDeviceID()
|
|
let timestamp = Date().timeIntervalSince1970
|
|
let combined = "\(deviceID)\(timestamp)"
|
|
|
|
return SHA256.hash(data: Data(combined.utf8))
|
|
.compactMap { String(format: "%02x", $0) }
|
|
.joined()
|
|
.prefix(8)
|
|
.uppercased()
|
|
}
|
|
|
|
private func encrypt(_ key: String) -> String {
|
|
// Simplified encryption for demonstration
|
|
// In production, use proper encryption
|
|
return Data(key.utf8).base64EncodedString()
|
|
}
|
|
|
|
private func decrypt(_ encrypted: String) -> String {
|
|
// Simplified decryption for demonstration
|
|
guard let data = Data(base64Encoded: encrypted),
|
|
let key = String(data: data, encoding: .utf8) else {
|
|
return ""
|
|
}
|
|
return key
|
|
}
|
|
|
|
private func sanitizeMessage(_ message: String) -> String {
|
|
// Remove any potentially harmful content
|
|
let cleaned = message
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.replacingOccurrences(of: "[^a-zA-Z0-9 .,!?'-]", with: "", options: .regularExpression)
|
|
|
|
return String(cleaned.prefix(200)) // Limit length
|
|
}
|
|
}
|
|
|
|
// MARK: - UI Components
|
|
|
|
struct CommunityPoolView: View {
|
|
@StateObject private var poolManager = CommunityPoolManager()
|
|
@State private var selectedTab = 0
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Pool status header
|
|
if let status = poolManager.poolStatus {
|
|
PoolStatusHeader(status: status)
|
|
}
|
|
|
|
// Tab selection
|
|
Picker("View", selection: $selectedTab) {
|
|
Text("Donate").tag(0)
|
|
Text("Request").tag(1)
|
|
Text("Activity").tag(2)
|
|
}
|
|
.pickerStyle(SegmentedPickerStyle())
|
|
.padding()
|
|
|
|
// Content
|
|
ScrollView {
|
|
switch selectedTab {
|
|
case 0:
|
|
DonateKeysView()
|
|
case 1:
|
|
RequestKeyView()
|
|
case 2:
|
|
CommunityActivityView()
|
|
default:
|
|
EmptyView()
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Community Pool")
|
|
.task {
|
|
await poolManager.loadPoolStatus()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PoolStatusHeader: View {
|
|
let status: CommunityPoolManager.PoolStatus
|
|
|
|
var body: some View {
|
|
VStack(spacing: 12) {
|
|
Text("Community Pool")
|
|
.font(.headline)
|
|
|
|
HStack(spacing: 20) {
|
|
VStack {
|
|
Text("\(status.totalKeys)")
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
Text("Total Keys")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Divider()
|
|
.frame(height: 40)
|
|
|
|
VStack {
|
|
Text("\(status.level1Keys)")
|
|
.font(.title3)
|
|
.foregroundColor(.blue)
|
|
Text("Level 1")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
VStack {
|
|
Text("\(status.level2Keys)")
|
|
.font(.title3)
|
|
.foregroundColor(.purple)
|
|
Text("Level 2")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
if !status.recentDonations.isEmpty {
|
|
Text("Recent: \(status.recentDonations.first!.message)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.secondarySystemBackground))
|
|
.cornerRadius(12)
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Types
|
|
|
|
enum DonationError: LocalizedError {
|
|
case invalidKey(String)
|
|
case donationFailed
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .invalidKey(let key):
|
|
return "Invalid key: \(key)"
|
|
case .donationFailed:
|
|
return "Failed to donate keys"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum RequestError: LocalizedError {
|
|
case tooSoon(daysRemaining: Int)
|
|
case verificationRequired
|
|
case requestFailed
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .tooSoon(let days):
|
|
return "Please wait \(days) more days before requesting again"
|
|
case .verificationRequired:
|
|
return "Verification required for this category"
|
|
case .requestFailed:
|
|
return "Failed to submit request"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Community Pool Features
|
|
|
|
1. **Anonymous Donations**
|
|
- No user tracking
|
|
- Optional messages
|
|
- Donor recognition (optional)
|
|
|
|
2. **Fair Distribution**
|
|
- Category-based priority
|
|
- Request limits (30-day cooldown)
|
|
- Verification for some categories
|
|
|
|
3. **Privacy First**
|
|
- Anonymous device IDs
|
|
- Encrypted key storage
|
|
- No personal data collection
|
|
|
|
4. **Community Building**
|
|
- Recent donation feed
|
|
- Pool statistics
|
|
- Success stories |