const clean = value => String(value ?? '—').replace(/[\r\n\u0000-\u001f\u007f]/g, ' ').slice(0, 160); export class AccessService { constructor(config, store, providers) { this.config = config; this.store = store; this.providers = providers; this.pending = new Map(); } request(identity) { const key = `${identity.platform}:${identity.user_id}`; if (this.pending.has(key)) return this.pending.get(key); const task = this.createRequest(identity).finally(() => this.pending.delete(key)); this.pending.set(key, task); return task; } async createRequest(identity) { const provider = this.providers[identity.platform]; if (!provider) return 'This access provider is unavailable.'; const { request, fresh } = this.store.request(identity); if (request.status === 'approved') return `Your existing number is reusable until ${new Date(request.codeExpiresAt).toISOString()}. Check your DMs. If you lost it, contact an administrator.`; if (request.status === 'delivering') return 'An administrator is sending your number. Check your DMs shortly.'; // Confirm DMs work before asking admins to approve the request. await provider.sendUser(identity.user_id, fresh ? 'Your Dungeons & Ground access request has been recorded. An administrator will review it. Your number will arrive here after approval.' : 'Your request is still pending. We are checking that an administrator has received it.'); const lines = ['Dungeons & Ground · Access request', `platform: ${request.platform}`, `user_id: ${request.user_id}`, `request_id: ${request.id}`]; for (const field of this.config.adminFields) { lines.push(`${field}: ${clean(field === 'created_at' ? new Date(request.createdAt).toISOString() : request[field])}`); } const buttons = [{ label: 'Approve', id: `approve:${request.id}` }, { label: 'Deny', id: `deny:${request.id}` }]; for (const adminId of this.config[identity.platform].admins) { if (this.store.getRequest(request.id).notifiedAdmins.includes(adminId)) continue; try { await provider.sendUser(adminId, lines.join('\n'), buttons); this.store.updateRequest(request.id, r => r.notifiedAdmins.push(adminId)); } catch { /* Keep undelivered admins retryable when the user presses Get access again. */ } } if (!this.store.getRequest(request.id).notifiedAdmins.length) return 'Your request is saved, but we could not contact an administrator. Please try Get access again later.'; return 'Request sent. You will receive your access number in a DM after an administrator approves it.'; } async decide(platform, adminId, requestId, decision) { if (!this.config[platform]?.admins.includes(adminId)) return 'Only configured administrators can review access requests.'; const request = this.store.getRequest(requestId); if (!request || request.platform !== platform) return 'Request not found.'; if (request.status !== 'pending') return `This request is already ${request.status}.`; if (request.expiresAt <= this.store.now()) return 'This request has expired. The user needs to request access again.'; const provider = this.providers[platform]; if (decision === 'deny') { this.store.updateRequest(requestId, r => { r.status = 'denied'; r.adminId = adminId; }); try { await provider.sendUser(request.user_id, 'Your Dungeons & Ground access request was declined. Contact an administrator if you have questions.'); } catch { return 'Request denied. The user could not be reached by DM.'; } return 'Request denied. The user has been notified.'; } if (decision !== 'approve') return 'Unknown action.'; const code = this.store.reserveCode(requestId, adminId); if (!code) return 'This request is already being reviewed.'; const expiresAt = this.store.getRequest(requestId).codeExpiresAt; try { await provider.sendUser(request.user_id, `Access approved!\n\nYour number: ${code}\n\nEnter it at ${this.config.publicUrl.origin}\nIt is reusable until ${new Date(expiresAt).toISOString()} (UTC). Keep it private. After it expires, use Get access to request a new number.`); } catch { this.store.updateRequest(requestId, r => { r.status = 'pending'; delete r.codeHash; delete r.codeExpiresAt; delete r.adminId; }); return 'Could not deliver the number. Ask the user to enable DMs or /start the bot, then press Approve again. No number was activated.'; } this.store.updateRequest(requestId, r => { r.status = 'approved'; r.approvedAt = this.store.now(); }); return 'Approved. The access number was sent privately to the user.'; } }