743 lines
22 KiB
TypeScript
743 lines
22 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import * as crypto from 'crypto';
|
|
|
|
import * as pg from 'pg';
|
|
|
|
import * as uuid from 'uuid';
|
|
|
|
import ConcurrentQueue from '../concurrent-queue/concurrent-queue';
|
|
import Logger from '../logger/logger';
|
|
|
|
const LOG = Logger.create('db');
|
|
|
|
const db = new pg.Client({
|
|
host: 'localhost',
|
|
user: 'cordis',
|
|
password: 'cordis_pass',
|
|
database: 'cordis',
|
|
});
|
|
|
|
export default class DB {
|
|
static async connect(): Promise<void> {
|
|
await db.connect();
|
|
}
|
|
|
|
static async end(): Promise<void> {
|
|
await db.end();
|
|
}
|
|
|
|
private static TRANSACTION_QUEUE = new ConcurrentQueue<void>(1);
|
|
|
|
static async beginTransaction(): Promise<void> {
|
|
await db.query('BEGIN');
|
|
}
|
|
|
|
static async rollbackTransaction(): Promise<void> {
|
|
await db.query('ROLLBACK');
|
|
}
|
|
|
|
static async commitTransaction(): Promise<void> {
|
|
await db.query('COMMIT');
|
|
}
|
|
|
|
static async queueTransaction(func: () => Promise<void>): Promise<void> {
|
|
await DB.TRANSACTION_QUEUE.push(async () => {
|
|
try {
|
|
await this.beginTransaction();
|
|
await func();
|
|
await this.commitTransaction();
|
|
} catch (e) {
|
|
await this.rollbackTransaction();
|
|
throw e;
|
|
}
|
|
});
|
|
}
|
|
|
|
static async getAllGuilds(): Promise<any[]> {
|
|
const result = await db.query(
|
|
'SELECT * FROM "guilds" LEFT JOIN "guilds_meta" ON "guilds"."id"="guilds_meta"."id"',
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
// returns the member_id and guild_id for the member with specified public key
|
|
static async getMemberInfo(publicKey: crypto.KeyObject): Promise<any> {
|
|
const der = publicKey.export({ type: 'spki', format: 'der' });
|
|
|
|
// eslint-disable-next-line newline-per-chained-call
|
|
LOG.silly(
|
|
`searching for public key (der hash: ${LOG.inspect(
|
|
crypto.createHash('sha256').update(der).digest().toString('hex').slice(0, 8),
|
|
)})`,
|
|
);
|
|
|
|
const result = await db.query('SELECT "id" AS member_id, "guild_id" FROM "members" WHERE "public_key"=$1', [
|
|
der,
|
|
]);
|
|
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to find member with specified public key');
|
|
}
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async getGuild(guildId: string): Promise<any> {
|
|
const result = await db.query('SELECT "name", "icon_resource_id" FROM "guilds_meta" WHERE "id"=$1', [guildId]);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to find guild with specified id');
|
|
}
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async setName(guildId: string, name: string): Promise<any> {
|
|
const result = await db.query(
|
|
`
|
|
UPDATE "guilds_meta" SET "name"=$1 WHERE "id"=$2
|
|
RETURNING "name", "icon_resource_id"
|
|
`,
|
|
[name, guildId],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to update guild name');
|
|
}
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async setIcon(guildId: string, iconResourceId: string): Promise<any> {
|
|
const result = await db.query(
|
|
`
|
|
UPDATE "guilds_meta" SET "icon_resource_id"=$1 WHERE "id"=$2
|
|
RETURNING "name", "icon_resource_id"
|
|
`,
|
|
[iconResourceId, guildId],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to update guild icon');
|
|
}
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async getMembers(guildId: string): Promise<any[]> {
|
|
const result = await db.query('SELECT * FROM "members_with_roles" WHERE "guild_id"=$1', [guildId]);
|
|
return result.rows;
|
|
}
|
|
|
|
static async getMember(guildId: string, memberId: string): Promise<any> {
|
|
const result = await db.query('SELECT * FROM "members_with_roles" WHERE "guild_id"=$1 AND "id"=$2', [
|
|
guildId,
|
|
memberId,
|
|
]);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to get member');
|
|
}
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async getChannels(guildId: string): Promise<any[]> {
|
|
const result = await db.query(
|
|
'SELECT "id", "index", "name", "flavor_text" FROM "channels" WHERE "guild_id"=$1 ORDER BY "index"',
|
|
[guildId],
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
static async createChannel(guildId: string, name: string, flavorText: string): Promise<any> {
|
|
let channel = null;
|
|
await DB.queueTransaction(async () => {
|
|
const indexResult = await db.query('SELECT MAX("index") AS max_index FROM "channels" WHERE "guild_id"=$1', [
|
|
guildId,
|
|
]);
|
|
if (indexResult.rows.length !== 1) {
|
|
throw new Error('invalid index result');
|
|
}
|
|
const newIndex = (indexResult.rows[0].max_index || 0) + 1;
|
|
const insertResult = await db.query(
|
|
`
|
|
INSERT INTO "channels" ("guild_id", "index", "name", "flavor_text")
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING "id", "index", "name", "flavor_text"
|
|
`,
|
|
[guildId, newIndex, name, flavorText],
|
|
);
|
|
if (insertResult.rows.length !== 1) {
|
|
throw new Error('unable to insert channel');
|
|
}
|
|
channel = insertResult.rows[0];
|
|
});
|
|
return channel;
|
|
}
|
|
|
|
static async updateChannel(guildId: string, channelId: string, name: string, flavorText: string): Promise<any> {
|
|
const result = await db.query(
|
|
`
|
|
UPDATE "channels" SET "name"=$1, "flavor_text"=$2 WHERE "guild_id"=$3 AND "id"=$4
|
|
RETURNING *
|
|
`,
|
|
[name, flavorText, guildId, channelId],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to update channel');
|
|
}
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async getMessagesRecent(guildId: string, channelId: string, number: string): Promise<any> {
|
|
const result = await db.query(
|
|
`
|
|
SELECT * FROM (
|
|
SELECT
|
|
"id", "channel_id", "member_id"
|
|
,"sent_dtg", "text"
|
|
, "resource_id"
|
|
, "resource_name"
|
|
, "resource_width"
|
|
, "resource_height"
|
|
, "resource_preview_id"
|
|
, "order"
|
|
FROM "messages"
|
|
WHERE "guild_id"=$1 AND "channel_id"=$2
|
|
ORDER BY "order" DESC
|
|
LIMIT $3
|
|
) AS "r" ORDER BY "r"."order" ASC`,
|
|
[guildId, channelId, number],
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
static async getMessagesBefore(
|
|
guildId: string,
|
|
channelId: string,
|
|
messageOrderId: string,
|
|
number: number,
|
|
): Promise<any[]> {
|
|
const result = await db.query(
|
|
`
|
|
SELECT * FROM (
|
|
SELECT
|
|
"id", "channel_id", "member_id"
|
|
, "sent_dtg", "text"
|
|
, "resource_id"
|
|
, "resource_name"
|
|
, "resource_width"
|
|
, "resource_height"
|
|
, "resource_preview_id"
|
|
, "order"
|
|
FROM "messages"
|
|
WHERE
|
|
"guild_id"=$1
|
|
AND "channel_id"=$2
|
|
AND "order" < $3
|
|
ORDER BY "order" DESC
|
|
LIMIT $4
|
|
) AS "r" ORDER BY "r"."order" ASC`,
|
|
[guildId, channelId, messageOrderId, number],
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
static async getMessagesAfter(
|
|
guildId: string,
|
|
channelId: string,
|
|
messageOrderId: string,
|
|
number: number,
|
|
): Promise<any[]> {
|
|
const result = await db.query(
|
|
`
|
|
SELECT
|
|
"id", "channel_id", "member_id"
|
|
, "sent_dtg", "text"
|
|
, "resource_id"
|
|
, "resource_name"
|
|
, "resource_width"
|
|
, "resource_height"
|
|
, "resource_preview_id"
|
|
, "order"
|
|
FROM "messages"
|
|
WHERE
|
|
"guild_id"=$1
|
|
AND "channel_id"=$2
|
|
AND "order" > $3
|
|
ORDER BY "order" ASC, "id" ASC
|
|
LIMIT $4`,
|
|
[guildId, channelId, messageOrderId, number],
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
static async getResource(guildId: string, resourceId: string): Promise<any> {
|
|
const result = await db.query(
|
|
`
|
|
SELECT
|
|
"id", "guild_id", "hash", "data"
|
|
FROM
|
|
"resources"
|
|
WHERE
|
|
"guild_id"=$1
|
|
AND "id"=$2
|
|
|
|
`,
|
|
[guildId, resourceId],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error(`unable to find specified resource g#${guildId}, r#${resourceId}`);
|
|
}
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async insertMessage(guildId: string, channelId: string, memberId: string, text: string): Promise<any> {
|
|
const id = uuid.v4();
|
|
const result = await db.query(
|
|
`INSERT INTO "messages" (
|
|
"id", "guild_id", "channel_id", "member_id", "text", "sent_dtg", "order"
|
|
) VALUES ($1::UUID, $2, $3, $4, $5, NOW(), LPAD(FLOOR(EXTRACT(epoch FROM NOW())::decimal * 100000)::text, 20, '0') || $1::text)
|
|
RETURNING
|
|
"id", "channel_id", "member_id"
|
|
, "text", "sent_dtg"
|
|
, "resource_id"
|
|
, "resource_name"
|
|
, "resource_width"
|
|
, "resource_height"
|
|
, "resource_preview_id"
|
|
, "order"
|
|
`,
|
|
[id, guildId, channelId, memberId, text],
|
|
);
|
|
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to properly insert message');
|
|
}
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async insertMessageWithResource(
|
|
guildId: string,
|
|
channelId: string,
|
|
memberId: string,
|
|
text: string | null,
|
|
resourceId: string,
|
|
resourceName: string,
|
|
resourceWidth: number | null,
|
|
resourceHeight: number | null,
|
|
resourcePreviewId: string | null,
|
|
): Promise<any> {
|
|
const id = uuid.v4();
|
|
const result = await db.query(
|
|
`INSERT INTO "messages" (
|
|
"id", "guild_id", "channel_id", "member_id", "text",
|
|
"resource_id", "resource_name", "resource_width", "resource_height",
|
|
"resource_preview_id", "sent_dtg", "order"
|
|
) VALUES ($1::UUID, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), LPAD(FLOOR(EXTRACT(epoch FROM NOW())::decimal * 100000)::text, 20, '0') || $1::text)
|
|
RETURNING
|
|
"id", "channel_id", "member_id"
|
|
, "text", "sent_dtg"
|
|
, "resource_id"
|
|
, "resource_name"
|
|
, "resource_width"
|
|
, "resource_height"
|
|
, "resource_preview_id"
|
|
, "order"
|
|
`,
|
|
[
|
|
id,
|
|
guildId,
|
|
channelId,
|
|
memberId,
|
|
text,
|
|
resourceId,
|
|
resourceName,
|
|
resourceWidth,
|
|
resourceHeight,
|
|
resourcePreviewId,
|
|
],
|
|
);
|
|
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to properly insert message (with resource)');
|
|
}
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
// returns the resource id. Resources are deduped based on their hash
|
|
static async insertResource(guildId: string, resourceBuff: Buffer): Promise<string> {
|
|
// hACK: using on conflict set guild_id=guild_id to ensure that RETURNING gives the proper id and we don't need another select
|
|
const resourceResult = await db.query(
|
|
`INSERT INTO "resources" ("guild_id", "hash", "data") VALUES ($1, digest($2::bytea, 'sha256'), $2::bytea)
|
|
ON CONFLICT ("guild_id", "hash") DO UPDATE
|
|
SET "guild_id"=EXCLUDED."guild_id"
|
|
RETURNING "id"`,
|
|
[guildId, resourceBuff],
|
|
);
|
|
|
|
if (resourceResult.rows.length !== 1) {
|
|
throw new Error('unable to insert resource');
|
|
}
|
|
return resourceResult.rows[0].id;
|
|
}
|
|
|
|
// static async updateMessage(
|
|
// guildId: string,
|
|
// messageId: string,
|
|
// channelId: string,
|
|
// memberId: string,
|
|
// text: string | null,
|
|
// resourceId: string | null,
|
|
// resourceName: string | null,
|
|
// resourceWidth: number | null,
|
|
// resourceHeight: number | null,
|
|
// resourcePreviewId: string | null
|
|
// ) {
|
|
// let updateResult = await db.query(
|
|
// `INSERT INTO "messages" (
|
|
// "guild_id", "channel_id", "member_id", "text",
|
|
// "resource_id", "resource_name", "resource_width", "resource_height",
|
|
// "resource_preview_id", "sent_dtg"
|
|
// ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
|
|
// wHERE
|
|
// "guild_id"=$1
|
|
// aND "message_id"=$2
|
|
// `
|
|
// )
|
|
// }
|
|
|
|
// resets all members status to Offline for specified guildId
|
|
static async clearAllMemberStatus(guildId: string): Promise<void> {
|
|
await db.query(`UPDATE "members" SET "status"='offline' WHERE "guild_id"=$1`, [guildId]);
|
|
}
|
|
|
|
// set the status of a specified member
|
|
static async setMemberStatus(guildId: string, memberId: string, status: string): Promise<void> {
|
|
const result = await db.query(
|
|
`
|
|
UPDATE "members" SET "status"=$1 WHERE "guild_id"=$2 AND "id"=$3
|
|
`,
|
|
[status, guildId, memberId],
|
|
);
|
|
if (result.rowCount !== 1) {
|
|
throw new Error('unable to update status');
|
|
}
|
|
}
|
|
|
|
// set the display name of a specified member
|
|
static async setMemberDisplayName(guildId: string, memberId: string, displayName: string): Promise<void> {
|
|
const result = await db.query(
|
|
`
|
|
UPDATE "members" SET "display_name"=$1 WHERE "guild_id"=$2 AND "id"=$3
|
|
`,
|
|
[displayName, guildId, memberId],
|
|
);
|
|
if (result.rowCount !== 1) {
|
|
throw new Error('unable to update status');
|
|
}
|
|
}
|
|
|
|
// creates a role in the guild (returns the id)
|
|
static async createRole(guildId: string, name: string, color: string, priority: number): Promise<string> {
|
|
const result = await db.query(
|
|
`
|
|
INSERT INTO "roles"
|
|
("guild_id", "name", "color", "priority")
|
|
VALUES
|
|
($1, $2, $3, $4)
|
|
RETURNING
|
|
"id"
|
|
`,
|
|
[guildId, name, color, priority],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to create role');
|
|
}
|
|
return result.rows[0].id;
|
|
}
|
|
|
|
static async removeRole(guildId: string, roleId: string): Promise<void> {
|
|
const result = await db.query(`DELETE FROM "roles" WHERE "id"=$1 AND "guild_id"=$2`, [roleId, guildId]);
|
|
if (result.rowCount !== 1) {
|
|
throw new Error('unable to remove role');
|
|
}
|
|
}
|
|
|
|
static async assignRoleToMember(guildId: string, roleId: string, memberId: string): Promise<void> {
|
|
const existsResult = await db.query(
|
|
`
|
|
SELECT COUNT(*)::integer AS c FROM "member_roles" WHERE
|
|
"role_id" = $1
|
|
AND "guild_id" = $2
|
|
AND "member_id" = $3
|
|
`,
|
|
[roleId, guildId, memberId],
|
|
);
|
|
if (existsResult.rows.length !== 1) {
|
|
throw new Error('unable to check for existing privilege');
|
|
}
|
|
if (existsResult.rows[0].c !== 0) {
|
|
LOG.warn(`r#${roleId} already assigned to u#${memberId} in g#${guildId}`);
|
|
return;
|
|
}
|
|
const result = await db.query(
|
|
`
|
|
INSERT INTO "member_roles"
|
|
("role_id", "guild_id", "member_id")
|
|
VALUES
|
|
($1, $2, $3)
|
|
RETURNING
|
|
"role_id"
|
|
`,
|
|
[roleId, guildId, memberId],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to assign user to role');
|
|
}
|
|
}
|
|
|
|
static async revokeRoleFromMember(guildId: string, roleId: string, memberId: string): Promise<void> {
|
|
const existsResult = await db.query(
|
|
`
|
|
SELECT COUNT(*)::integer AS c FROM "member_roles" WHERE
|
|
"role_id" = $1
|
|
AND "guild_id" = $2
|
|
AND "member_id" = $3
|
|
`,
|
|
[roleId, guildId, memberId],
|
|
);
|
|
if (existsResult.rows.length !== 1) {
|
|
throw new Error('unable to check for existing privilege');
|
|
}
|
|
if (existsResult.rows[0].c === 0) {
|
|
LOG.warn(`r#${roleId} was never assigned to u#${memberId} in g#${guildId}`);
|
|
return;
|
|
}
|
|
const result = await db.query(
|
|
`
|
|
DELETE FROM "member_roles"
|
|
WHERE
|
|
"role_id"=$1
|
|
AND "guild_id"=$2
|
|
AND "member_id"=$3
|
|
`,
|
|
[roleId, guildId, memberId],
|
|
);
|
|
if (result.rowCount !== 1) {
|
|
throw new Error('unable to revoke role from user');
|
|
}
|
|
}
|
|
|
|
static async assignRolePrivilege(guildId: string, roleId: string, privilege: string): Promise<void> {
|
|
const existsResult = await db.query(
|
|
`
|
|
SELECT COUNT(*)::integer AS c FROM "role_privileges" WHERE
|
|
"role_id" = $1
|
|
AND "guild_id" = $2
|
|
AND "privilege" = $3
|
|
`,
|
|
[roleId, guildId, privilege],
|
|
);
|
|
if (existsResult.rows.length !== 1) {
|
|
throw new Error('unable to check for existing privilege');
|
|
}
|
|
if (existsResult.rows[0].c !== 0) {
|
|
LOG.warn(`privilege ${privilege} already exists for r#${roleId} on g#${guildId}`);
|
|
return;
|
|
}
|
|
const result = await db.query(
|
|
`
|
|
INSERT INTO "role_privileges"
|
|
("role_id", "guild_id", "privilege")
|
|
VALUES
|
|
($1, $2, $3)
|
|
RETURNING
|
|
"role_id"
|
|
`,
|
|
[roleId, guildId, privilege],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to add privilege to role privileges');
|
|
}
|
|
}
|
|
|
|
static async revokeRolePrivilege(roleId: string, guildId: string, privilege: string): Promise<void> {
|
|
const existsResult = await db.query(
|
|
`
|
|
SELECT COUNT(*)::integer AS c FROM "role_privileges" WHERE
|
|
"role_id" = $1
|
|
AND "guild_id" = $2
|
|
AND "privilege" = $3
|
|
`,
|
|
[roleId, guildId, privilege],
|
|
);
|
|
if (existsResult.rows.length !== 1) {
|
|
throw new Error('unable to check for existing privilege');
|
|
}
|
|
if (existsResult.rows[0].c === 0) {
|
|
LOG.warn(`privilege ${privilege} did not exist for r#${roleId} on g#${guildId}`);
|
|
return;
|
|
}
|
|
const result = await db.query(
|
|
`
|
|
DELETE FROM "role_privileges"
|
|
WHERE
|
|
"role_id"=$1
|
|
, "guild_id"=$2
|
|
, "privilege"=$3
|
|
`,
|
|
[roleId, guildId, privilege],
|
|
);
|
|
if (result.rowCount !== 1) {
|
|
throw new Error('unable to revoke privilege to role privileges');
|
|
}
|
|
}
|
|
|
|
static async hasPrivilege(guildId: string, memberId: string, privilege: string): Promise<boolean> {
|
|
const result = await db.query(
|
|
`
|
|
SELECT COUNT(*)::integer AS c FROM
|
|
member_roles
|
|
, role_privileges
|
|
WHERE
|
|
member_roles.role_id=role_privileges.role_id
|
|
AND member_roles.guild_id=$1
|
|
AND member_roles.member_id=$2
|
|
AND role_privileges.privilege=$3
|
|
`,
|
|
[guildId, memberId, privilege],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to check for privilege');
|
|
}
|
|
return result.rows[0].c > 0;
|
|
}
|
|
|
|
static async setMemberAvatarResourceId(guildId: string, memberId: string, avatarResourceId: string): Promise<void> {
|
|
const result = await db.query(
|
|
`
|
|
UPDATE "members" SET "avatar_resource_id"=$1 WHERE "guild_id"=$2 AND "id"=$3
|
|
`,
|
|
[avatarResourceId, guildId, memberId],
|
|
);
|
|
if (result.rowCount !== 1) {
|
|
throw new Error('unable to update status');
|
|
}
|
|
}
|
|
|
|
static async getTokens(guildId: string): Promise<any[]> {
|
|
const result = await db.query(
|
|
`
|
|
SELECT "id", "token", "member_id", "created", "expires" FROM tokens
|
|
WHERE "guild_id"=$1
|
|
`,
|
|
[guildId],
|
|
);
|
|
return result.rows;
|
|
}
|
|
|
|
//insert into tokens (guild_id, expires) VALUES ('226b3e9e-5220-4205-bf5b-6738b9b3bb39', NOW() + '7 days'::interval) RETURNING "token", "expires";
|
|
static async createToken(guildId: string, expiresAfter: string | null): Promise<any> {
|
|
const result = await db.query(
|
|
`
|
|
INSERT INTO "tokens" ("guild_id", "expires")
|
|
VALUES ($1, CASE WHEN $2::text IS NULL THEN NULL ELSE NOW() + $2::interval END)
|
|
RETURNING "id", "token", "member_id", "created", "expires"
|
|
`,
|
|
[guildId, expiresAfter],
|
|
);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to insert a token');
|
|
}
|
|
return result.rows[0];
|
|
}
|
|
|
|
static async isTokenReal(token: string): Promise<boolean> {
|
|
const result = await db.query(`SELECT COUNT(*)::integer AS c FROM "tokens" WHERE "token"=$1`, [token]);
|
|
return result.rows[0].c === 1;
|
|
}
|
|
|
|
static async isTokenForGuild(token: string, guildId: string): Promise<boolean> {
|
|
const result = await db.query(
|
|
`SELECT COUNT(*)::integer AS c FROM "tokens" WHERE "token"=$1 AND "guild_id"=$2`,
|
|
[token, guildId],
|
|
);
|
|
return result.rows[0].c === 1;
|
|
}
|
|
|
|
static async isTokenTaken(token: string): Promise<boolean> {
|
|
const result = await db.query(
|
|
`SELECT COUNT(*)::integer AS c FROM "tokens" WHERE "token"=$1 AND "member_id" IS NOT NULL`,
|
|
[token],
|
|
);
|
|
return result.rows[0].c === 1;
|
|
}
|
|
|
|
static async isTokenActive(token: string): Promise<boolean> {
|
|
const result = await db.query(
|
|
`SELECT COUNT(*)::integer AS c FROM "tokens" WHERE "token"=$1 AND "member_id" IS NULL AND "expires">NOW()`,
|
|
[token],
|
|
);
|
|
return result.rows[0].c === 1;
|
|
}
|
|
|
|
// registers a user for with a token
|
|
// NOTE: Tokens are unique across guilds so they can be used to get the guildId
|
|
static async registerWithToken(
|
|
token: string,
|
|
derBuff: Buffer,
|
|
displayName: string,
|
|
avatarBuff: Buffer,
|
|
): Promise<{ guildId: string; memberId: string }> {
|
|
// insert avatar as resource
|
|
let result: { memberId: string; guildId: string } | null = null;
|
|
await DB.queueTransaction(async () => {
|
|
const resultguildId = await db.query(
|
|
`
|
|
SELECT "guild_id" FROM "tokens" WHERE "token"=$1
|
|
`,
|
|
[token],
|
|
);
|
|
if (resultguildId.rows.length !== 1) {
|
|
throw new Error('unable to get token guild id');
|
|
}
|
|
const guildId = resultguildId.rows[0].guild_id as string;
|
|
const avatarResourceId = await DB.insertResource(guildId, avatarBuff);
|
|
const resultInsertMember = await db.query(
|
|
`
|
|
INSERT INTO "members" (
|
|
"guild_id", "public_key", "display_name", "avatar_resource_id"
|
|
) VALUES ($1, $2, $3, $4)
|
|
RETURNING "id"
|
|
`,
|
|
[guildId, derBuff, displayName, avatarResourceId],
|
|
);
|
|
if (resultInsertMember.rows.length !== 1) {
|
|
throw new Error('unable to insert member');
|
|
}
|
|
const memberId = resultInsertMember.rows[0].id;
|
|
const resultUpdate = await db.query(
|
|
`
|
|
UPDATE "tokens" SET "member_id"=$1 WHERE "token"=$2
|
|
`,
|
|
[memberId, token],
|
|
);
|
|
if (resultUpdate.rowCount !== 1) {
|
|
throw new Error('unable to update token with new member');
|
|
}
|
|
result = { guildId, memberId };
|
|
});
|
|
if (!result) {
|
|
throw new Error('result was not set');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static async revokeToken(guildId: string, token: string): Promise<any> {
|
|
const result = await db.query('DELETE FROM "tokens" WHERE "guild_id"=$1 AND "token"=$2 RETURNING *', [
|
|
guildId,
|
|
token,
|
|
]);
|
|
if (result.rows.length !== 1) {
|
|
throw new Error('unable to remove token');
|
|
}
|
|
return result.rows[0];
|
|
}
|
|
}
|