You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
113 lines
4.6 KiB
113 lines
4.6 KiB
import {Snowflake} from "discord-api-types/globals.js"
|
|
import {fast1a52 as fnvFast1a52} from "fnv-plus"
|
|
import {parse as uuidParse, v1 as uuidV1, validate as uuidValidate, version as uuidVersion} from "uuid"
|
|
import {SnowflakeUtil} from "discord.js"
|
|
import {QueryType} from "./database.js"
|
|
|
|
export interface UsersTable {
|
|
getUuidForActiveSnowflake(snowflake: Snowflake): Promise<string>
|
|
|
|
getActiveSnowflakeIsAdmin(snowflake: Snowflake): Promise<boolean>
|
|
|
|
createBotOwnerAsAdmin(snowflake: Snowflake): Promise<void>
|
|
}
|
|
|
|
export class UsersTableImpl implements UsersTable {
|
|
private readonly _query: QueryType
|
|
|
|
constructor({query}: { query: QueryType }) {
|
|
this._query = query
|
|
}
|
|
|
|
async getUuidForActiveSnowflake(snowflake: Snowflake): Promise<string> {
|
|
const result = await this._query<{ id: string }, [string]>(
|
|
`INSERT INTO users (id, is_admin, created_at, updated_at, active_at)
|
|
VALUES ($1, FALSE, now(), now(), now())
|
|
ON CONFLICT (id) DO UPDATE SET active_at = now()
|
|
RETURNING id;`, [userSnowflakeToUuid(snowflake)])
|
|
return result.rows[0].id
|
|
}
|
|
|
|
async getActiveSnowflakeIsAdmin(snowflake: Snowflake): Promise<boolean> {
|
|
const result = await this._query<{ is_admin: boolean }, [string]>(
|
|
`UPDATE users
|
|
SET active_at = NOW()
|
|
WHERE id = $1
|
|
RETURNING is_admin;`, [userSnowflakeToUuid(snowflake)])
|
|
if (result.rowCount === 0) {
|
|
return false
|
|
}
|
|
return result.rows[0].is_admin
|
|
}
|
|
|
|
async createBotOwnerAsAdmin(snowflake: Snowflake): Promise<void> {
|
|
await this._query<{}, [string]>(
|
|
`INSERT INTO users (id, is_admin, created_at, updated_at)
|
|
VALUES ($1, TRUE, now(), now())
|
|
ON CONFLICT (id) DO UPDATE SET is_admin = TRUE,
|
|
updated_at = NOW()
|
|
WHERE users.is_admin = FALSE;`, [userSnowflakeToUuid(snowflake)])
|
|
}
|
|
}
|
|
|
|
export function userUuidToSnowflake(uuid: string): Snowflake | null {
|
|
if (!uuidValidate(uuid) || uuidVersion(uuid) !== 1) {
|
|
return null
|
|
}
|
|
const bytes = uuidParse(uuid)
|
|
const time_low = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]
|
|
const time_mid = bytes[4] << 8 | bytes[5]
|
|
const time_high = (bytes[6] & 0x0F) << 8 | bytes[7]
|
|
const timestamp = ((BigInt(time_high) << 48n) | (BigInt(time_mid) << 32n) | BigInt(time_low)) / 10000n
|
|
const increment = BigInt((bytes[8] & 0x0F) << 8 | bytes[9])
|
|
const workerId = BigInt((bytes[14] & 0x03) << 3 | (bytes[15] & 0x07) >> 5)
|
|
const processId = BigInt(bytes[15] & 0x1F)
|
|
|
|
return SnowflakeUtil.generate({
|
|
timestamp,
|
|
increment,
|
|
workerId,
|
|
processId,
|
|
}).toString(10)
|
|
}
|
|
|
|
export function userSnowflakeToUuid(snowflake: Snowflake): string {
|
|
const {
|
|
timestamp,
|
|
increment,
|
|
workerId,
|
|
processId,
|
|
} = SnowflakeUtil.deconstruct(snowflake)
|
|
|
|
// Grab 52 hash bits for filling the empty spaces in the UUID.
|
|
const hashCode = fnvFast1a52(snowflake)
|
|
// We get 10000 possibilities for number of 100-nanosecond periods.
|
|
const nanoseconds = (hashCode % 10000)
|
|
// The node ID is 48 bits, and we have 10 from the process and worker IDs.
|
|
// The first bit must be 1 to indicate a made up node name, but the remaining 37 are open.
|
|
// Grab 37 bits after removing the 10000 variants of the 100-nanosecond periods.
|
|
const nodeIdPadding = Math.floor(hashCode / 10000) & 0x1FFFFFFFFF
|
|
const nodeId = new Uint8Array(6)
|
|
// Set the highest bit, indicating a multicast address...
|
|
// ... indicating an invalid address, indicating a made up node name.
|
|
// Then insert the hash padding and worker/process IDs.
|
|
// It doesn't matter what order they're in, as long as it's consistent.
|
|
// In this case, we choose the padding to be most significant, then worker, then process.
|
|
nodeId[0] = 0x80 | ((nodeIdPadding >> 30) & 0x7F)
|
|
nodeId[1] = (nodeIdPadding >> 22) & 0xFF
|
|
nodeId[2] = (nodeIdPadding >> 14) & 0xFF
|
|
nodeId[3] = (nodeIdPadding >> 6) & 0xFF
|
|
nodeId[4] = ((nodeIdPadding & 0x3F) << 2) | Number((workerId >> 3n) & 0x03n)
|
|
nodeId[5] = Number(((workerId & 0x07n) << 5n) | (processId & 0x1Fn))
|
|
return uuidV1({
|
|
rng: () => {
|
|
throw Error("rng is not supposed to be invoked")
|
|
},
|
|
// The snowflake increment is 12 bits.
|
|
// UUIDs use a 12 bit clock sequence number. Perfect fit!
|
|
clockseq: Number(increment & 0xFFFn),
|
|
nsecs: nanoseconds,
|
|
msecs: Number(timestamp & 0x3FFFFFFFFFFn),
|
|
node: nodeId,
|
|
})
|
|
} |