mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 07:58:55 -07:00
[link] store share links
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* The `link` table (migrations/0001_link.sql): one row per share code minted by
|
||||
* `POST /actionlink`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The code alphabet: upper-case letters and digits without the look-alikes (0/O, 1/I/L),
|
||||
* since a player may read a code aloud or type it from a screenshot. 31 symbols, so six of
|
||||
* them give ~887 million codes.
|
||||
*/
|
||||
const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'
|
||||
export const CODE_LENGTH = 6
|
||||
|
||||
/** How many fresh codes to try before giving up on a run of collisions. */
|
||||
const MAX_ATTEMPTS = 8
|
||||
|
||||
/** A random code. Rejection-sampled so every symbol is equally likely. */
|
||||
export function randomCode(): string {
|
||||
// The largest multiple of the alphabet size that fits in a byte.
|
||||
const limit = 256 - (256 % CODE_ALPHABET.length)
|
||||
let code = ''
|
||||
while (code.length < CODE_LENGTH) {
|
||||
for (const byte of crypto.getRandomValues(new Uint8Array(CODE_LENGTH * 2))) {
|
||||
if (byte >= limit) continue
|
||||
code += CODE_ALPHABET[byte % CODE_ALPHABET.length]
|
||||
if (code.length === CODE_LENGTH) break
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
export interface NewLink {
|
||||
creatorAccountId: number
|
||||
codeType: number
|
||||
extraDataId: number | null
|
||||
data: string | null
|
||||
validHours: number
|
||||
}
|
||||
|
||||
export interface LinkRow {
|
||||
code: string
|
||||
creator_account_id: number
|
||||
code_type: number
|
||||
extra_data_id: number | null
|
||||
data: string | null
|
||||
valid_hours: number
|
||||
created_at: string
|
||||
expires_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a link under a fresh code and return the code. A code already taken is skipped
|
||||
* (`ON CONFLICT DO NOTHING` returns no row) and another drawn.
|
||||
*/
|
||||
export async function createLink(db: D1Database, link: NewLink, now = new Date()): Promise<string> {
|
||||
const createdAt = now.toISOString()
|
||||
const expiresAt = new Date(now.getTime() + link.validHours * 3_600_000).toISOString()
|
||||
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
||||
const code = randomCode()
|
||||
const row = await db
|
||||
.prepare(
|
||||
`INSERT INTO link (code, creator_account_id, code_type, extra_data_id, data, valid_hours, created_at, expires_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||
ON CONFLICT (code) DO NOTHING
|
||||
RETURNING code`
|
||||
)
|
||||
.bind(
|
||||
code,
|
||||
link.creatorAccountId,
|
||||
link.codeType,
|
||||
link.extraDataId,
|
||||
link.data,
|
||||
link.validHours,
|
||||
createdAt,
|
||||
expiresAt
|
||||
)
|
||||
.first<{ code: string }>()
|
||||
if (row) return row.code
|
||||
}
|
||||
throw new Error(`no free link code after ${MAX_ATTEMPTS} attempts`)
|
||||
}
|
||||
|
||||
/** One link by code, or null. */
|
||||
export async function getLink(db: D1Database, code: string): Promise<LinkRow | null> {
|
||||
return db.prepare('SELECT * FROM link WHERE code = ?1').bind(code).first<LinkRow>()
|
||||
}
|
||||
Reference in New Issue
Block a user