openvibers/openvibe.live Commits
Site tips account: a typed PowerChat username, not a user-id pointer
Changes
@@@ -1057,11 +1057,12 @@ function initDb() {// registration has the scopes.['powerchat_scopes', 'profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger chat:write viewcount:write subscriptions:write follows:write currency:write tips:write', 'OAuth scopes requested from each streamer (space-delimited)', 'string'],['powerchat_sandbox_username', 'alex', 'Sandbox streamer username the app can act on until approved (the app owner’s PowerChat username)', 'string'],-- // Site-wide fallback PowerChat account: the OpenVibe user id whose connected-- // PowerChat receives tips when a streamer has no PowerChat of their own, and-- // hosts all Vibes purchases. That user connects PowerChat from their normal-- // dashboard card; this setting just points at them (default: the owner).-- ['powerchat_site_user_id', '1', 'OpenVibe user id whose PowerChat connection is the site-wide tip/purchase account (fallback when a streamer has no PowerChat)', 'number'],++ // Site-wide tips account: the PowerChat USERNAME whose tip page hosts all++ // Vibes purchases and the donation fallback for streamers without their own++ // PowerChat. Just a typed username (the checkout link is a canonical URL) —++ // the app credentials above handle attribution, and that PowerChat account++ // must have the app connected on PowerChat's side so webhooks fire for it.++ ['powerchat_site_tip_username', '', 'PowerChat username whose tip page receives site purchases + fallback donations (that account must have the app connected on PowerChat)', 'string'],];const seedPc = database.prepare("INSERT OR IGNORE INTO site_settings (key, value, description, type) VALUES (?, ?, ?, ?)");for (const [k, v, d, t] of powerchatSeeds) seedPc.run(k, v, d, t);@@@ -1074,6 +1075,21 @@ function initDb() {'profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger','profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger chat:write viewcount:write subscriptions:write follows:write currency:write');database.prepare(`UPDATE site_settings SET value = 'alex' WHERE key = 'powerchat_sandbox_username' AND value = 'n8admin'`).run();++ // powerchat_site_user_id (pointed at an OpenVibe user's connection) is replaced++ // by powerchat_site_tip_username (a directly-typed PowerChat username). Carry++ // the old pointer's resolved PowerChat username over once, then drop it.++ const oldSite = database.prepare(`SELECT value FROM site_settings WHERE key = 'powerchat_site_user_id'`).get();++ if (oldSite) {++ try {++ const uid = parseInt(oldSite.value, 10);++ const conn = uid ? database.prepare('SELECT powerchat_username FROM powerchat_connections WHERE user_id = ?').get(uid) : null;++ if (conn && conn.powerchat_username) {++ database.prepare(`UPDATE site_settings SET value = ? WHERE key = 'powerchat_site_tip_username' AND (value IS NULL OR value = '')`)++ .run(conn.powerchat_username);++ }++ } catch { /* best-effort carry-over */ }++ database.prepare(`DELETE FROM site_settings WHERE key = 'powerchat_site_user_id'`).run();++ }} catch (e) { console.warn('[DB] Settings seed:', e.message); }// Internal job/cache state (JSON blobs the AI jobs persist across restarts).@@@ -28,12 +28,24 @@ const db = require('../db/database');const oauth = require('./powerchat-oauth');// ── Site account resolution ──────────────────────────────────--function siteAccountUserId() {-- const v = parseInt(String(db.getSetting('powerchat_site_user_id') || ''), 10);-- return Number.isFinite(v) && v > 0 ? v : null;++// The site tips account is just a TYPED PowerChat username (admin setting) — a++// dedicated account made for receiving site money. Checkout links are canonical++// URLs built from the username + our client id; confirmation arrives via the++// signed webhook, so no tokens are needed on our side. The only external++// requirement: that PowerChat account must have the app connected on PowerChat++// (Connect card / OAuth consent) so webhooks fire and refs echo back for it.++function getSiteAccount() {++ try {++ const c = oauth.getConfig();++ if (!c.enabled || !c.clientId) return null;++ const username = String(db.getSetting('powerchat_site_tip_username') || '').trim();++ return username ? { username } : null;++ } catch { return null; }}--// A usable checkout host = connected via app OAuth AND granted checkout:attribute--// (attribution refs only echo back for grants that carry the scope).++function isAvailable() { return !!getSiteAccount(); }++++// A streamer-direct checkout host = connected via app OAuth AND granted++// checkout:attribute (attribution refs only echo back for grants carrying the scope).function _checkoutConn(userId) {if (!userId) return null;try {@@@ -44,12 +56,6 @@ function _checkoutConn(userId) {return conn;} catch { return null; }}--function getSiteAccount() {-- const userId = siteAccountUserId();-- const conn = _checkoutConn(userId);-- return conn ? { userId, username: conn.powerchat_username } : null;--}--function isAvailable() { return !!getSiteAccount(); }// Canonical attribution link (documented stable URL shape — no API round-trip needed).function tipLinkFor(pcUsername, ref) {@@@ -169,8 +169,23 @@ function _handleNotice(userId, message, kind) {function processEvent(envelope) {if (!envelope || !envelope.type) return;const userId = _resolveStreamerUserId(envelope.streamer);-- if (!userId) { console.warn(`[PowerChat] webhook ${envelope.type} for unknown streamer`, envelope.streamer); return; }const data = envelope.data || {};++ if (!userId) {++ // The site tips account is a bare PowerChat username (no OpenVibe connection++ // row) — its checkout-attributed donations must still be fulfilled. Anything++ // else arriving for it (e.g. someone tipping that account directly, outside a++ // checkout link) has no on-site home and is deliberately ignored.++ const siteName = String(db.getSetting('powerchat_site_tip_username') || '').trim().toLowerCase();++ const from = String((envelope.streamer && envelope.streamer.username) || '').toLowerCase();++ if (siteName && from === siteName) {++ if (envelope.type === 'donation.completed' && data.appExternalRef && !data.isTest && data.source !== 'developer_app') {++ try { require('./powerchat-checkout').handleAttributedDonation(null, data); } catch (e) { console.warn('[PowerChat] site-account fulfillment:', e.message); }++ }++ return;++ }++ console.warn(`[PowerChat] webhook ${envelope.type} for unknown streamer`, envelope.streamer);++ return;++ }// Dashboard "Send test webhook" deliveries carry data.isTest — they exist to verify// the receiver (signature/dedupe/wiring), and must NEVER credit goals or fire the// real celebration pipeline.
@@ -1057,11 +1057,12 @@ function initDb() {
@@ -1057,11 +1057,12 @@ function initDb() {
// registration has the scopes.
// registration has the scopes.
['powerchat_scopes', 'profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger chat:write viewcount:write subscriptions:write follows:write currency:write tips:write', 'OAuth scopes requested from each streamer (space-delimited)', 'string'],
['powerchat_scopes', 'profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger chat:write viewcount:write subscriptions:write follows:write currency:write tips:write', 'OAuth scopes requested from each streamer (space-delimited)', 'string'],
['powerchat_sandbox_username', 'alex', 'Sandbox streamer username the app can act on until approved (the app owner’s PowerChat username)', 'string'],
['powerchat_sandbox_username', 'alex', 'Sandbox streamer username the app can act on until approved (the app owner’s PowerChat username)', 'string'],
- // Site-wide fallback PowerChat account: the OpenVibe user id whose connected
- // PowerChat receives tips when a streamer has no PowerChat of their own, and
- // hosts all Vibes purchases. That user connects PowerChat from their normal
- // dashboard card; this setting just points at them (default: the owner).
- ['powerchat_site_user_id', '1', 'OpenVibe user id whose PowerChat connection is the site-wide tip/purchase account (fallback when a streamer has no PowerChat)', 'number'],
+ // Site-wide tips account: the PowerChat USERNAME whose tip page hosts all
+ // Vibes purchases and the donation fallback for streamers without their own
+ // PowerChat. Just a typed username (the checkout link is a canonical URL) —
+ // the app credentials above handle attribution, and that PowerChat account
+ // must have the app connected on PowerChat's side so webhooks fire for it.
+ ['powerchat_site_tip_username', '', 'PowerChat username whose tip page receives site purchases + fallback donations (that account must have the app connected on PowerChat)', 'string'],
];
];
const seedPc = database.prepare("INSERT OR IGNORE INTO site_settings (key, value, description, type) VALUES (?, ?, ?, ?)");
const seedPc = database.prepare("INSERT OR IGNORE INTO site_settings (key, value, description, type) VALUES (?, ?, ?, ?)");
for (const [k, v, d, t] of powerchatSeeds) seedPc.run(k, v, d, t);
for (const [k, v, d, t] of powerchatSeeds) seedPc.run(k, v, d, t);
@@ -1074,6 +1075,21 @@ function initDb() {
@@ -1074,6 +1075,21 @@ function initDb() {
'profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger',
'profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger',
'profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger chat:write viewcount:write subscriptions:write follows:write currency:write');
'profile:read webhooks:events checkout:attribute paid_messages:read alerts:trigger chat:write viewcount:write subscriptions:write follows:write currency:write');
database.prepare(`UPDATE site_settings SET value = 'alex' WHERE key = 'powerchat_sandbox_username' AND value = 'n8admin'`).run();
database.prepare(`UPDATE site_settings SET value = 'alex' WHERE key = 'powerchat_sandbox_username' AND value = 'n8admin'`).run();
+ // powerchat_site_user_id (pointed at an OpenVibe user's connection) is replaced
+ // by powerchat_site_tip_username (a directly-typed PowerChat username). Carry
+ // the old pointer's resolved PowerChat username over once, then drop it.
+ const oldSite = database.prepare(`SELECT value FROM site_settings WHERE key = 'powerchat_site_user_id'`).get();
+ if (oldSite) {
+ try {
+ const uid = parseInt(oldSite.value, 10);
+ const conn = uid ? database.prepare('SELECT powerchat_username FROM powerchat_connections WHERE user_id = ?').get(uid) : null;
+ if (conn && conn.powerchat_username) {
+ database.prepare(`UPDATE site_settings SET value = ? WHERE key = 'powerchat_site_tip_username' AND (value IS NULL OR value = '')`)
+ .run(conn.powerchat_username);
+ }
+ } catch { /* best-effort carry-over */ }
+ database.prepare(`DELETE FROM site_settings WHERE key = 'powerchat_site_user_id'`).run();
+ }
} catch (e) { console.warn('[DB] Settings seed:', e.message); }
} catch (e) { console.warn('[DB] Settings seed:', e.message); }
// Internal job/cache state (JSON blobs the AI jobs persist across restarts).
// Internal job/cache state (JSON blobs the AI jobs persist across restarts).
@@ -28,12 +28,24 @@ const db = require('../db/database');
@@ -28,12 +28,24 @@ const db = require('../db/database');
const oauth = require('./powerchat-oauth');
const oauth = require('./powerchat-oauth');
// ── Site account resolution ──────────────────────────────────
// ── Site account resolution ──────────────────────────────────
-function siteAccountUserId() {
- const v = parseInt(String(db.getSetting('powerchat_site_user_id') || ''), 10);
- return Number.isFinite(v) && v > 0 ? v : null;
+// The site tips account is just a TYPED PowerChat username (admin setting) — a
+// dedicated account made for receiving site money. Checkout links are canonical
+// URLs built from the username + our client id; confirmation arrives via the
+// signed webhook, so no tokens are needed on our side. The only external
+// requirement: that PowerChat account must have the app connected on PowerChat
+// (Connect card / OAuth consent) so webhooks fire and refs echo back for it.
+function getSiteAccount() {
+ try {
+ const c = oauth.getConfig();
+ if (!c.enabled || !c.clientId) return null;
+ const username = String(db.getSetting('powerchat_site_tip_username') || '').trim();
+ return username ? { username } : null;
+ } catch { return null; }
}
}
-// A usable checkout host = connected via app OAuth AND granted checkout:attribute
-// (attribution refs only echo back for grants that carry the scope).
+function isAvailable() { return !!getSiteAccount(); }
+
+// A streamer-direct checkout host = connected via app OAuth AND granted
+// checkout:attribute (attribution refs only echo back for grants carrying the scope).
function _checkoutConn(userId) {
function _checkoutConn(userId) {
if (!userId) return null;
if (!userId) return null;
try {
try {
@@ -44,12 +56,6 @@ function _checkoutConn(userId) {
@@ -44,12 +56,6 @@ function _checkoutConn(userId) {
return conn;
return conn;
} catch { return null; }
} catch { return null; }
}
}
-function getSiteAccount() {
- const userId = siteAccountUserId();
- const conn = _checkoutConn(userId);
- return conn ? { userId, username: conn.powerchat_username } : null;
-}
-function isAvailable() { return !!getSiteAccount(); }
// Canonical attribution link (documented stable URL shape — no API round-trip needed).
// Canonical attribution link (documented stable URL shape — no API round-trip needed).
function tipLinkFor(pcUsername, ref) {
function tipLinkFor(pcUsername, ref) {
@@ -169,8 +169,23 @@ function _handleNotice(userId, message, kind) {
@@ -169,8 +169,23 @@ function _handleNotice(userId, message, kind) {
function processEvent(envelope) {
function processEvent(envelope) {
if (!envelope || !envelope.type) return;
if (!envelope || !envelope.type) return;
const userId = _resolveStreamerUserId(envelope.streamer);
const userId = _resolveStreamerUserId(envelope.streamer);
- if (!userId) { console.warn(`[PowerChat] webhook ${envelope.type} for unknown streamer`, envelope.streamer); return; }
const data = envelope.data || {};
const data = envelope.data || {};
+ if (!userId) {
+ // The site tips account is a bare PowerChat username (no OpenVibe connection
+ // row) — its checkout-attributed donations must still be fulfilled. Anything
+ // else arriving for it (e.g. someone tipping that account directly, outside a
+ // checkout link) has no on-site home and is deliberately ignored.
+ const siteName = String(db.getSetting('powerchat_site_tip_username') || '').trim().toLowerCase();
+ const from = String((envelope.streamer && envelope.streamer.username) || '').toLowerCase();
+ if (siteName && from === siteName) {
+ if (envelope.type === 'donation.completed' && data.appExternalRef && !data.isTest && data.source !== 'developer_app') {
+ try { require('./powerchat-checkout').handleAttributedDonation(null, data); } catch (e) { console.warn('[PowerChat] site-account fulfillment:', e.message); }
+ }
+ return;
+ }
+ console.warn(`[PowerChat] webhook ${envelope.type} for unknown streamer`, envelope.streamer);
+ return;
+ }
// Dashboard "Send test webhook" deliveries carry data.isTest — they exist to verify
// Dashboard "Send test webhook" deliveries carry data.isTest — they exist to verify
// the receiver (signature/dedupe/wiring), and must NEVER credit goals or fire the
// the receiver (signature/dedupe/wiring), and must NEVER credit goals or fire the
// real celebration pipeline.
// real celebration pipeline.