mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[api] add influencer badges
This commit is contained in:
@@ -15,13 +15,14 @@ import { bindPlaceholders, chunkForBinds } from './d1-binds'
|
||||
|
||||
/**
|
||||
* Schema DDL — the head schema, i.e. what the table looks like after every migration
|
||||
* (0001_accounts + 0002_avatar + 0009_account_has_plus, sans seed INSERTs; 0004 added a
|
||||
* `platform_id` generated column and 0008 dropped it again, so it appears here in neither
|
||||
* form).
|
||||
* (0001_accounts + 0002_avatar + 0009_account_has_plus + 0012_account_is_influencer, sans
|
||||
* seed INSERTs; 0004 added a `platform_id` generated column and 0008 dropped it again, so it
|
||||
* appears here in neither form).
|
||||
*
|
||||
* `has_plus` mirrors the blob's `hasPlus` so the operator's Plus token reload can find every
|
||||
* subscriber through its partial index instead of scanning every account's JSON. It is 1 for
|
||||
* JSON true, 0 for false and NULL when the key is absent; the index holds only the 1s.
|
||||
* `is_influencer` does the same for `isInfluencer`, for econ's influencer list.
|
||||
*/
|
||||
export const SCHEMA_DDL: string[] = [
|
||||
`CREATE TABLE IF NOT EXISTS account (
|
||||
@@ -29,11 +30,13 @@ export const SCHEMA_DDL: string[] = [
|
||||
avatar TEXT,
|
||||
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL,
|
||||
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.username'))) VIRTUAL,
|
||||
has_plus INTEGER GENERATED ALWAYS AS (json_extract(data, '$.hasPlus')) VIRTUAL
|
||||
has_plus INTEGER GENERATED ALWAYS AS (json_extract(data, '$.hasPlus')) VIRTUAL,
|
||||
is_influencer INTEGER GENERATED ALWAYS AS (json_extract(data, '$.isInfluencer')) VIRTUAL
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_account_id ON account (account_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_accounts_username_lower ON account (username_lower)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_account_has_plus ON account (has_plus) WHERE has_plus = 1`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_account_is_influencer ON account (is_influencer) WHERE is_influencer = 1`,
|
||||
]
|
||||
|
||||
/** Client-facing account shape (camelCase, exactly as the client's AccountDTO). */
|
||||
@@ -127,6 +130,14 @@ export interface Account {
|
||||
* `runx admin grant-moderator`. Absent/false means no role.
|
||||
*/
|
||||
isModerator?: boolean
|
||||
/**
|
||||
* Whether this account holds the influencer role. It rides in the token's `role` claim
|
||||
* (as `influencer`) and puts the account in econ's influencer partner program list
|
||||
* (`GET /api/influencerpartnerprogram/influencers`, via {@link listInfluencerIds}) —
|
||||
* there is no `/role/influencer/:id` lookup. Operator-granted only, via
|
||||
* `runx admin grant-influencer`. Absent/false means no role.
|
||||
*/
|
||||
isInfluencer?: boolean
|
||||
/**
|
||||
* Whether this account is a SANDBOX account — served out of the sandbox Tachyon pool
|
||||
* rather than the live one (`match` `GET /player/connection-info`). Operator-set only;
|
||||
@@ -425,6 +436,20 @@ export async function getAccountsByIds(db: D1Database, ids: number[]): Promise<A
|
||||
return parseAll(pages.flatMap((page) => page.results))
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of every account flagged `isInfluencer`, lowest id first — the whole influencer
|
||||
* partner program, which econ serves to every client on launch. Through the `is_influencer`
|
||||
* generated column and its partial index (auth migration 0012): the predicate must stay
|
||||
* exactly `is_influencer = 1` for the index to apply, and the index holds only those rows,
|
||||
* so this never scans the account table. Only the ids are read; the blob is not parsed.
|
||||
*/
|
||||
export async function listInfluencerIds(db: D1Database): Promise<number[]> {
|
||||
const { results } = await db
|
||||
.prepare(`SELECT account_id FROM account WHERE is_influencer = 1 ORDER BY account_id`)
|
||||
.all<{ account_id: number }>()
|
||||
return results.map((r) => r.account_id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Every account flagged `hasStudio`, lowest id first — the website's staff list. A JSON
|
||||
* scan rather than a generated column: the list is short and only staff read it.
|
||||
|
||||
@@ -28,6 +28,7 @@ import type { D1ExecResult } from '../d1'
|
||||
* runx admin clear-password --username alice [--remote]
|
||||
* runx admin lookup --username alice [--remote]
|
||||
* runx admin grant-developer --account 1 [--revoke] [--remote]
|
||||
* runx admin grant-influencer --account 1 [--revoke] [--remote]
|
||||
* runx admin grant-plus --username alice [--revoke] [--remote]
|
||||
* runx admin reload-plus <amount> [--dry-run] [--remote]
|
||||
* runx admin cai-load [--file <export.json>] [--dry-run] [--remote]
|
||||
@@ -169,6 +170,11 @@ function grantRoleCommand(name: string, jsonKey: string, roleLabel: string, noun
|
||||
|
||||
const grantDeveloper = grantRoleCommand('grant-developer', 'isDeveloper', 'developer')
|
||||
const grantModerator = grantRoleCommand('grant-moderator', 'isModerator', 'moderator')
|
||||
/**
|
||||
* The influencer role, the account's `isInfluencer` flag. `auth` stamps it into the token's
|
||||
* `role` claim as `influencer` on the next login; nothing else reads the flag.
|
||||
*/
|
||||
const grantInfluencer = grantRoleCommand('grant-influencer', 'isInfluencer', 'influencer')
|
||||
|
||||
/**
|
||||
* Rec Room Plus, the account's `hasPlus` flag. Players normally get it themselves by
|
||||
@@ -394,6 +400,7 @@ const lookup = new Command('lookup')
|
||||
(json_extract(data, '$.passwordHash') IS NOT NULL) AS hasPassword,
|
||||
(json_extract(data, '$.isDeveloper') = 1) AS isDeveloper,
|
||||
(json_extract(data, '$.isModerator') = 1) AS isModerator,
|
||||
(json_extract(data, '$.isInfluencer') = 1) AS isInfluencer,
|
||||
(json_extract(data, '$.hasStudio') = 1) AS hasStudio
|
||||
FROM account WHERE ${where}`
|
||||
const res = await execSql(sql, remote)
|
||||
@@ -408,7 +415,13 @@ const lookup = new Command('lookup')
|
||||
: typeof v === 'object'
|
||||
? JSON.stringify(v)
|
||||
: String(v as number | string | boolean)
|
||||
const boolKeys = new Set(['hasPassword', 'isDeveloper', 'isModerator', 'hasStudio'])
|
||||
const boolKeys = new Set([
|
||||
'hasPassword',
|
||||
'isDeveloper',
|
||||
'isModerator',
|
||||
'isInfluencer',
|
||||
'hasStudio',
|
||||
])
|
||||
const table = new Table()
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
const shown = boolKeys.has(key) ? (value === 1 ? 'yes' : 'no') : asText(value)
|
||||
@@ -426,6 +439,7 @@ export const adminCmd = new Command('admin')
|
||||
.addCommand(clearPassword)
|
||||
.addCommand(grantDeveloper)
|
||||
.addCommand(grantModerator)
|
||||
.addCommand(grantInfluencer)
|
||||
.addCommand(grantPlus)
|
||||
.addCommand(grantStudio)
|
||||
.addCommand(reloadPlus)
|
||||
|
||||
Reference in New Issue
Block a user