Enable the bot to access database stuff!

So much database stuff. An absurd amount of database stuff.
main
Mari 2 years ago
parent d09c503add
commit 88018a0f16
  1. 8
      .env.example
  2. 0
      liquibase-changelog.yml
  3. 5
      liquibase.properties
  4. 110
      migrations/0001-characters.sql
  5. 223
      package-lock.json
  6. 13
      package.json
  7. 0
      src/bin/bot.spec.ts
  8. 60
      src/bin/bot.ts
  9. 79
      src/bin/migrate.ts
  10. 36
      src/commands/bot/index.ts
  11. 21
      src/commands/bot/rebuild.ts
  12. 21
      src/commands/bot/restart.ts
  13. 24
      src/commands/bot/shutdown.ts
  14. 63
      src/commands/character/create.ts
  15. 26
      src/commands/character/index.ts
  16. 6
      src/commands/index.spec.ts
  17. 64
      src/commands/index.ts
  18. 42
      src/commands/types.ts
  19. 16
      src/database/database.ts
  20. 10
      src/database/inmemory/database.ts
  21. 46
      src/database/inmemory/users.ts
  22. 110
      src/database/users.ts
  23. 6
      src/ipc/restart.ts
  24. 6
      src/types/interactions.ts

@ -1 +1,9 @@
DISCORD_TOKEN=
BOT_OWNER_ID=
PGHOST=free-tier11.gcp-us-east1.cockroachlabs.cloud
PGPORT=26257
PGDATABASE=nomrpg-chesting
PGUSER=
PGPASSWORD=
PGSSLMODE=verify-full
PGOPTIONS=--cluster=deliciousreya-nom-rpg-2220

@ -1,7 +1,4 @@
# PostgreSQL
classpath: lib/postgresql-42.5.0.jar
driver: org.postgresql.Driver
url: jdbc:postgresql://free-tier11.gcp-us-east1.cockroachlabs.cloud:26257/nomrpg-chesting?options=--cluster%3Ddeliciousreya-nom-rpg-2220&sslmode=verify-full
username: reya
changeLogFile: configuration.yml
liquibase.hub.mode=off
liquibase.databaseClass=liquibase.database.core.CockroachDatabase

@ -4,8 +4,12 @@
CREATE TABLE IF NOT EXISTS users
(
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
discord_snowflake INT NOT NULL,
INDEX discord_users (discord_snowflake)
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
active_at TIMESTAMPTZ NULL DEFAULT NULL,
FAMILY essentials (id, is_admin),
FAMILY timestamps (created_at, updated_at, active_at)
);
--rollback DROP TABLE IF EXISTS users;
@ -41,14 +45,15 @@ VALUES (0, 'Basic', '', 0),
(14, 'Bully', '', 14),
(15, 'Mythic', '', 15),
(16, 'Toy', '', 16),
(17, 'Cute', '', 17);
(17, 'Cute', '', 17)
ON CONFLICT DO NOTHING;
--rollback DELETE FROM types WHERE id in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
--changeset reya:difficulties_table runInTransaction:false
CREATE TABLE IF NOT EXISTS difficulties
(
id INT NOT NULL PRIMARY KEY,
name STRING NOT NULL,
name STRING NOT NULL UNIQUE,
description STRING NOT NULL,
display_order INT NOT NULL,
endo_only BOOLEAN NOT NULL DEFAULT false,
@ -83,17 +88,15 @@ VALUES (0, 'Endo Only', 'You can never be digested. You always come out sooner o
false, true, 90, 50, 90, true),
(6, 'Extremely Dangerous',
'If you get devoured and you don''t get out, you''ll lose just about everything. Be very, very careful...', 6,
false, true, 100, 100, 100, true),
(7, 'Impossible',
'There is nothing to look forward to after digestion but an eternity on someone else''s thighs. When you''re digested, your life is over. There''s no coming back. Ever.',
7, false, true, 100, 100, 100, false);
false, true, 100, 100, 100, true)
ON CONFLICT DO NOTHING;
--rollback DELETE FROM difficulties WHERE id IN (0, 1, 2, 3, 4, 5, 6, 7);
--changeset reya:preferences_table runInTransaction:false
CREATE TABLE IF NOT EXISTS preferences
(
id INT NOT NULL PRIMARY KEY,
name STRING NOT NULL,
name STRING NOT NULL UNIQUE,
description STRING NOT NULL,
display_order INT NOT NULL,
can_use_vore BOOLEAN NOT NULL,
@ -106,7 +109,8 @@ INSERT INTO preferences (id, name, description, display_order, can_use_vore, can
VALUES (0, 'Observer', 'You can neither eat nor be eaten.', 0, false, false),
(1, 'Prey Only', 'You can only be eaten, not eat.', 1, false, true),
(2, 'Pred Only', 'You can only eat, not be eaten.', 2, true, false),
(3, 'Switch', 'You can both eat and be eaten.', 3, true, true);
(3, 'Switch', 'You can both eat and be eaten.', 3, true, true)
ON CONFLICT DO NOTHING;
--rollback DELETE FROM preferences WHERE id IN (0, 1, 2, 3);
--changeset reya:genders_table runInTransaction:false
@ -114,6 +118,7 @@ CREATE TABLE IF NOT EXISTS genders
(
id INT NOT NULL PRIMARY KEY,
name STRING NOT NULL,
pronouns STRING NOT NULL UNIQUE,
display_order INT NOT NULL,
use_plural BOOLEAN NOT NULL,
subjective STRING NOT NULL,
@ -125,33 +130,36 @@ CREATE TABLE IF NOT EXISTS genders
--rollback DROP TABLE IF EXISTS genders;
--changeset reya:genders_values runInTransaction:true
INSERT INTO genders (id, name, display_order, use_plural, subjective, adjective, possessive, reflexive, objective)
VALUES (0, 'Non-binary (name only)', 0, false, '@@', '@@''s', '@@''s', '@@''s self', '@@'),
(1, 'Female (she/her)', 1, false, 'she', 'her', 'hers', 'herself', 'her'),
(2, 'Non-binary (they/them)', 2, true, 'they', 'their', 'theirs', 'themself', 'them'),
(3, 'Male (he/him)', 3, false, 'he', 'his', 'his', 'himself', 'him'),
(4, 'Object (it/its)', 4, false, 'it', 'its', 'its', 'itself', 'it'),
(5, 'Herm (shi/hir)', 5, false, 'shi', 'hir', 'hirs', 'hirself', 'hir'),
(6, 'Non-binary (ae/aer)', 6, false, 'ae', 'aer', 'aers', 'aerself', 'aer'),
(7, 'Non-binary (fae/faer)', 7, false, 'fae', 'faer', 'faers', 'faerself', 'faer'),
(8, 'Non-binary (e/em)', 8, false, 'e', 'eir', 'eirs', 'emself', 'em'),
(9, 'Non-binary (ey/em)', 9, false, 'ey', 'eir', 'eirs', 'emself', 'em'),
(10, 'Non-binary (per/per)', 10, false, 'per', 'pers', 'pers', 'perself', 'per'),
(11, 'Non-binary (ve/ver)', 11, false, 've', 'vis', 'vis', 'verself', 'ver'),
(12, 'Non-binary (xe/xem)', 12, false, 'xe', 'xyr', 'xyrs', 'xemself', 'xem'),
(13, 'Non-binary (ze/hir)', 13, false, 'ze', 'hir', 'hirs', 'hirself', 'hir'),
(14, 'Non-binary (zie/hir)', 14, false, 'zie', 'hir', 'hirs', 'hirself', 'hir'),
(15, 'Non-binary (zie/zim)', 15, false, 'zie', 'zir', 'zis', 'zieself', 'zim'),
(16, 'Non-binary (sie/sie)', 16, false, 'sie', 'hir', 'hirs', 'hirself', 'sie'),
(17, 'Non-binary (te/ter)', 17, false, 'te', 'tem', 'ters', 'terself', 'ter');
INSERT INTO genders (id, name, pronouns, display_order, use_plural, subjective, adjective, possessive, reflexive,
objective)
VALUES (0, 'Non-binary', 'name only', 0, false, '@@', '@@''s', '@@''s', '@@''s self', '@@'),
(1, 'Female', 'she/her', 1, false, 'she', 'her', 'hers', 'herself', 'her'),
(2, 'Non-binary', 'they/them', 2, true, 'they', 'their', 'theirs', 'themself', 'them'),
(3, 'Male', 'he/him', 3, false, 'he', 'his', 'his', 'himself', 'him'),
(4, 'Object', 'it/its', 4, false, 'it', 'its', 'its', 'itself', 'it'),
(5, 'Herm', 'shi/hir', 5, false, 'shi', 'hir', 'hirs', 'hirself', 'hir'),
(6, 'Non-binary', 'ae/aer', 6, false, 'ae', 'aer', 'aers', 'aerself', 'aer'),
(7, 'Non-binary', 'fae/faer', 7, false, 'fae', 'faer', 'faers', 'faerself', 'faer'),
(8, 'Non-binary', 'e/em', 8, false, 'e', 'eir', 'eirs', 'emself', 'em'),
(9, 'Non-binary', 'ey/em', 9, false, 'ey', 'eir', 'eirs', 'emself', 'em'),
(10, 'Non-binary', 'per/per', 10, false, 'per', 'pers', 'pers', 'perself', 'per'),
(11, 'Non-binary', 've/ver', 11, false, 've', 'vis', 'vis', 'verself', 'ver'),
(12, 'Non-binary', 'xe/xem', 12, false, 'xe', 'xyr', 'xyrs', 'xemself', 'xem'),
(13, 'Non-binary', 'ze/hir', 13, false, 'ze', 'hir', 'hirs', 'hirself', 'hir'),
(14, 'Non-binary', 'zie/hir', 14, false, 'zie', 'hir', 'hirs', 'hirself', 'hir'),
(15, 'Non-binary', 'zie/zim', 15, false, 'zie', 'zir', 'zis', 'zieself', 'zim'),
(16, 'Non-binary', 'sie/sie', 16, false, 'sie', 'hir', 'hirs', 'hirself', 'sie'),
(17, 'Non-binary', 'te/ter', 17, false, 'te', 'tem', 'ters', 'terself', 'ter')
ON CONFLICT DO NOTHING;
--rollback DELETE FROM genders WHERE id IN (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17);
--changeset reya:characters_table runInTransaction:false
CREATE TABLE IF NOT EXISTS characters
(
id UUID NOT NULL PRIMARY KEY,
id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
name STRING NOT NULL,
discriminator INT NULL DEFAULT NULL,
title STRING NOT NULL,
profile STRING NOT NULL,
gender_id INT NOT NULL REFERENCES genders (id) ON DELETE RESTRICT,
@ -199,11 +207,11 @@ CREATE TABLE IF NOT EXISTS characters
resilience_proficiency INT NOT NULL DEFAULT 0,
speed_proficiency INT NOT NULL DEFAULT 0,
UNIQUE (user_id, name, discriminator),
FAMILY character_base (id, user_id, name, title, profile, gender_id, type1_id, type2_id, base_confidence,
base_health,
base_stamina, base_brawn, base_durability, base_intensity, base_resilience, base_speed),
FAMILY character_reformation_stats (
min_confidence_talent, min_health_talent, min_stamina_talent, min_brawn_talent,
FAMILY character_reformation_stats (min_confidence_talent, min_health_talent, min_stamina_talent, min_brawn_talent,
min_durability_talent, min_intensity_talent, min_resilience_talent,
min_speed_talent,
confidence_talent, health_talent, stamina_talent, brawn_talent,
@ -212,4 +220,40 @@ CREATE TABLE IF NOT EXISTS characters
health_proficiency, stamina_proficiency, brawn_proficiency, durability_proficiency,
intensity_proficiency, resilience_proficiency, speed_proficiency)
);
--rollback DROP TABLE IF EXISTS characters_table;
--rollback DROP TABLE IF EXISTS characters;
--changeset reya:character_creation_table runInTransaction:false
CREATE TABLE IF NOT EXISTS character_creation
(
user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
character_id UUID NULL REFERENCES characters (id) ON DELETE CASCADE,
name STRING NULL,
title STRING NULL,
profile STRING NULL,
gender_id INT NULL REFERENCES genders (id) ON DELETE RESTRICT,
difficulty_id INT NULL REFERENCES difficulties (id) ON DELETE RESTRICT,
preference_id INT NULL REFERENCES preferences (id) ON DELETE RESTRICT,
type1_id INT NULL REFERENCES types (id) ON DELETE RESTRICT,
type2_id INT NULL REFERENCES types (id) ON DELETE RESTRICT,
base_confidence INT NULL DEFAULT 70,
base_health INT NULL DEFAULT 70,
base_stamina INT NULL DEFAULT 70,
base_brawn INT NULL DEFAULT 70,
base_durability INT NULL DEFAULT 70,
base_intensity INT NULL DEFAULT 70,
base_resilience INT NULL DEFAULT 70,
base_speed INT NULL DEFAULT 70,
PRIMARY KEY (user_id, character_id)
);
--rollback DROP TABLE IF EXISTS character_creation;
--changeset reya:userDefaultDifficultyPreferenceGender
ALTER TABLE users
ADD COLUMN default_gender_id INT NULL REFERENCES genders (id) ON DELETE RESTRICT DEFAULT NULL
CREATE IF NOT EXISTS FAMILY character_defaults,
ADD COLUMN default_difficulty_id INT NULL REFERENCES difficulties (id) ON DELETE RESTRICT DEFAULT NULL
FAMILY character_defaults,
ADD COLUMN default_preference_id INT NULL REFERENCES preferences (id) ON DELETE RESTRICT DEFAULT NULL
FAMILY character_defaults;
--rollback ALTER

223
package-lock.json generated

@ -10,11 +10,15 @@
"license": "ISC",
"dependencies": {
"@reduxjs/toolkit": "^1.8.5",
"@types/fnv-plus": "^1.3.0",
"@types/uuid": "^8.3.4",
"discord-api-types": "^0.37.12",
"discord.js": "^14.5.0",
"dotenv": "^16.0.3",
"fnv-plus": "^1.3.1",
"liquibase": "^4.4.0",
"ts-postgres": "^1.3.0"
"pg": "^8.8.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@babel/cli": "^7.19.3",
@ -2429,6 +2433,11 @@
"@babel/types": "^7.3.0"
}
},
"node_modules/@types/fnv-plus": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@types/fnv-plus/-/fnv-plus-1.3.0.tgz",
"integrity": "sha512-ijls8MsO6Q9JUSd5w1v4y2ijM6S4D/nmOyI/FwcepvrZfym0wZhLdYGFD5TJID7tga0O3I7SmtK69RzpSJ1Fcw=="
},
"node_modules/@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@ -2490,6 +2499,11 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -2833,6 +2847,14 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"engines": {
"node": ">=4"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@ -3352,6 +3374,11 @@
"node": ">=8"
}
},
"node_modules/fnv-plus": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/fnv-plus/-/fnv-plus-1.3.1.tgz",
"integrity": "sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw=="
},
"node_modules/fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@ -4701,6 +4728,11 @@
"node": ">=6"
}
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@ -4764,26 +4796,61 @@
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/pg": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"dev": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==",
"dev": true
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
@ -4795,6 +4862,14 @@
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -4847,7 +4922,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"engines": {
"node": ">=4"
}
@ -4856,7 +4930,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -4865,7 +4938,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -4874,7 +4946,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"dependencies": {
"xtend": "^4.0.0"
},
@ -5197,6 +5268,14 @@
"source-map": "^0.6.0"
}
},
"node_modules/split2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
"integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -5551,22 +5630,6 @@
}
}
},
"node_modules/ts-postgres": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-postgres/-/ts-postgres-1.3.0.tgz",
"integrity": "sha512-YQY6omZM9RiMeJpzyVn36ankicZnTbSTkHaq+NTkqrLHSRYigsNW9JsPwrZXxLt1es3mV+V6/VUj0eOVcYXq1g==",
"dependencies": {
"ts-typed-events": "^3.0.0"
},
"engines": {
"node": ">=10.7.0"
}
},
"node_modules/ts-typed-events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ts-typed-events/-/ts-typed-events-3.0.0.tgz",
"integrity": "sha512-+2FZ0XPX+UPR7PO8ZQjuvnuDMYRhzrDaCRaNHaBG1xSL//0oPa3XMU5yxgDTzW67VzkE33fQpx1YxWBdkaF7Zw=="
},
"node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
@ -5688,6 +5751,14 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@ -5792,7 +5863,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"engines": {
"node": ">=0.4"
}
@ -7602,6 +7672,11 @@
"@babel/types": "^7.3.0"
}
},
"@types/fnv-plus": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@types/fnv-plus/-/fnv-plus-1.3.0.tgz",
"integrity": "sha512-ijls8MsO6Q9JUSd5w1v4y2ijM6S4D/nmOyI/FwcepvrZfym0wZhLdYGFD5TJID7tga0O3I7SmtK69RzpSJ1Fcw=="
},
"@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@ -7663,6 +7738,11 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@ -7924,6 +8004,11 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
},
"busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@ -8303,6 +8388,11 @@
"path-exists": "^4.0.0"
}
},
"fnv-plus": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/fnv-plus/-/fnv-plus-1.3.1.tgz",
"integrity": "sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw=="
},
"fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@ -9307,6 +9397,11 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@ -9348,23 +9443,45 @@
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
"integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A=="
},
"pg": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
}
},
"pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
},
"pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"dev": true
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
},
"pg-pool": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"requires": {}
},
"pg-protocol": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==",
"dev": true
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
},
"pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"requires": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
@ -9373,6 +9490,14 @@
"postgres-interval": "^1.1.0"
}
},
"pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"requires": {
"split2": "^4.1.0"
}
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -9409,26 +9534,22 @@
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
},
"postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="
},
"postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="
},
"postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"requires": {
"xtend": "^4.0.0"
}
@ -9680,6 +9801,11 @@
"source-map": "^0.6.0"
}
},
"split2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
"integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ=="
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -9901,19 +10027,6 @@
"yn": "3.1.1"
}
},
"ts-postgres": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-postgres/-/ts-postgres-1.3.0.tgz",
"integrity": "sha512-YQY6omZM9RiMeJpzyVn36ankicZnTbSTkHaq+NTkqrLHSRYigsNW9JsPwrZXxLt1es3mV+V6/VUj0eOVcYXq1g==",
"requires": {
"ts-typed-events": "^3.0.0"
}
},
"ts-typed-events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ts-typed-events/-/ts-typed-events-3.0.0.tgz",
"integrity": "sha512-+2FZ0XPX+UPR7PO8ZQjuvnuDMYRhzrDaCRaNHaBG1xSL//0oPa3XMU5yxgDTzW67VzkE33fQpx1YxWBdkaF7Zw=="
},
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
@ -9988,6 +10101,11 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@ -10059,8 +10177,7 @@
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
"version": "5.0.8",

@ -3,21 +3,26 @@
"version": "0.0.0",
"description": "Vore RPG core",
"private": true,
"main": "build/index.js",
"main": "build/bin/bot.js",
"scripts": {
"build": "tsc",
"build": "tsc && babel src --out-dir=build --extensions=.ts",
"migrate": "node build/bin/migrate.js",
"start": "node build/bin/bot.js",
"test": "jest"
},
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@reduxjs/toolkit": "^1.8.5",
"@types/fnv-plus": "^1.3.0",
"@types/uuid": "^8.3.4",
"discord-api-types": "^0.37.12",
"discord.js": "^14.5.0",
"dotenv": "^16.0.3",
"fnv-plus": "^1.3.1",
"liquibase": "^4.4.0",
"ts-postgres": "^1.3.0"
"pg": "^8.8.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@babel/cli": "^7.19.3",

@ -1,17 +1,37 @@
import {BaseInteraction, Client} from "discord.js"
import {config} from "dotenv"
import {isChatInputCommand} from "./types/interactions"
import {commandDefinitions, executeCommand, storeCachedCommands} from "./commands/index"
import {checkIsRestart, reportFailed, reportReady, reportStarted} from "./ipc/restart"
import {defaultPresence} from "./defaultPresence"
import {isAutocomplete, isChatInputCommand} from "../types/interactions.js"
import {Commands} from "../commands/index.js"
import {checkIsRestart, reportFailed, reportReady, reportStarted} from "../ipc/restart.js"
import {defaultPresence} from "../defaultPresence.js"
import {Pool} from "pg"
import {Database, DatabaseImpl} from "../database/database.js"
async function main() {
async function bot() {
await checkIsRestart()
config()
const c = new Client({
intents: [],
})
const p = new Pool({
application_name: "VoreRPG Bot",
})
async function cleanUp() {
await p.end()
c.destroy()
}
const db: Database = new DatabaseImpl(p.query.bind(p))
const cmd = new Commands({users: db.users, cleanUp})
c.on("ready", async () => {
try {
await db.users.createBotOwnerAsAdmin(process.env.BOT_OWNER_ID || "")
} catch (ex) {
await reportFailed(c, ex)
return
}
const app = c.application
if (!app) {
c.destroy()
@ -19,10 +39,10 @@ async function main() {
return
} else {
try {
storeCachedCommands(await app.commands.set(commandDefinitions))
cmd.setCache(await app.commands.set(cmd.definitions))
const g = await c.guilds.fetch()
for (const guild of g.values()) {
storeCachedCommands(await app.commands.set(commandDefinitions, guild.id))
cmd.setCache(await app.commands.set(cmd.definitions, guild.id))
}
} catch (ex) {
c.destroy()
@ -64,28 +84,44 @@ async function main() {
}
if (isChatInputCommand(ev)) {
try {
await executeCommand(ev)
await cmd.execute(ev)
} catch (ex) {
console.log("failed executing command", ev, ex)
}
if (!ev.replied) {
console.log("never replied to command", ev)
try {
await ev.reply({
ephemeral: true,
content: "Uuguuu... I can't think straight... try again later, 'kay?",
})
} catch (innerEx) {
console.log("failed sending error response", innerEx)
} catch (ex) {
console.log("failed sending error reply", ex)
}
}
} else if (isAutocomplete(ev)) {
try {
await cmd.autocomplete(ev)
} catch (ex) {
console.log("failed autocompleting for command", ev, ex)
}
if (!ev.responded) {
console.log("never autocompleted for command", ev)
try {
await ev.respond([])
} catch (ex) {
console.log("failed sending error response", ex)
}
}
} else if (ev.isRepliable()) {
try {
console.log("unknown command", ev)
await ev.reply({
ephemeral: true,
content: "Huuuuuuuh? ... I don't know what to do with that yet.",
})
} catch (ex) {
console.log("failed sending unknown command response", ex)
console.log("failed sending unknown command reply", ex)
}
} else {
console.log("got an interaction but can't reply to it")
@ -95,6 +131,6 @@ async function main() {
await c.login(process.env.DISCORD_TOKEN || "")
}
main().catch((ex) => {
bot().catch((ex) => {
console.log("main thread failed", ex)
})

@ -0,0 +1,79 @@
import {config} from "dotenv"
import {Liquibase, LiquibaseLogLevels} from "liquibase"
import {resolve} from "path"
interface PostgresEnvVars {
host?: string
port?: string
db?: string
sslmode?: string
options?: string
user?: string
pass?: string
}
async function migrate() {
config()
const vars: PostgresEnvVars = {}
for (const variable of Object.keys(process.env)) {
const value = process.env[variable] || ""
if (variable.startsWith("PG")) {
switch (variable) {
case "PGHOST":
vars.host = value
break
case "PGPORT":
vars.port = value
break
case "PGDATABASE":
vars.db = value
break
case "PGUSER":
vars.user = value
break
case "PGPASSWORD":
vars.pass = value
break
case "PGOPTIONS":
vars.options = value
break
case "PGSSLMODE":
vars.sslmode = value
break
default:
console.log("Unknown PG* environment variable: " + variable)
}
}
}
const params = []
params.push(`ApplicationName=${encodeURIComponent("VoreRPG Migration")}`)
if (vars.options) {
params.push(`options=${encodeURIComponent(vars.options)}`)
}
if (vars.sslmode) {
params.push(`sslmode=${encodeURIComponent(vars.sslmode)}`)
}
const liquibase = new Liquibase({
changeLogFile: "liquibase-changelog.yml",
classpath: resolve("lib/postgresql-42.5.0.jar"),
liquibase: resolve("node_modules/liquibase/dist/liquibase/liquibase"),
liquibasePropertiesFile: resolve("liquibase.properties"),
username: vars.user || process.env.USER || process.env.USERNAME || "postgres",
password: vars.pass || "",
logLevel: LiquibaseLogLevels.Debug,
url: `jdbc:postgresql://`
+ `${vars.host ? encodeURIComponent(vars.host) : ""}${vars.port ? `:${encodeURIComponent(vars.port)}` : ""}`
+ `${vars.db ? `/${encodeURIComponent(vars.db)}` : ""}${params.length > 0 ? `?${params.join("&")}` : ""}`,
})
try {
await liquibase.update({})
} catch (err) {
console.log("Liquibase failed")
console.log(err)
}
}
migrate().catch((err) => {
console.log("Main thread failed", err)
})

@ -1,21 +1,31 @@
import {BaseChatInputCommandData, CommandWithSubcommandsData} from "../types"
import {BaseChatInputCommandData, CommandWithSubcommandsData, SubcommandData} from "../types"
import {ApplicationCommandType} from "discord.js"
import {commandBotRestart} from "./restart"
import {commandBotShutdown} from "./shutdown"
import {commandBotRebuild} from "./rebuild"
import {BotRestartCommand} from "./restart"
import {BotShutdownCommand} from "./shutdown"
import {BotRebuildCommand} from "./rebuild"
import {UsersTable} from "../../database/users.js"
export class BotCommand extends CommandWithSubcommandsData {
readonly rebuild: BotRebuildCommand
readonly restart: BotRestartCommand
readonly shutdown: BotShutdownCommand
readonly subcommands: SubcommandData[]
constructor({users, cleanUp}: { users: UsersTable, cleanUp: () => Promise<void> }) {
super()
this.rebuild = new BotRebuildCommand({users, cleanUp})
this.restart = new BotRestartCommand({users, cleanUp})
this.shutdown = new BotShutdownCommand({users, cleanUp})
this.subcommands = [
this.rebuild,
this.restart,
this.shutdown,
]
}
class BotCommandData extends CommandWithSubcommandsData {
readonly baseDefinition: BaseChatInputCommandData = {
name: "bot",
type: ApplicationCommandType.ChatInput,
description: "Commands to manage the bot's status.",
}
readonly subcommands = [
commandBotRebuild,
commandBotRestart,
commandBotShutdown,
]
}
export const commandBot = new BotCommandData()

@ -1,4 +1,4 @@
import {adminId, SubcommandData} from "../types"
import {SubcommandData} from "../types"
import {
ActivityType,
ApplicationCommandOptionType,
@ -9,8 +9,18 @@ import {wrappedRestart} from "../../ipc/restart"
import {fork} from "child_process"
import {defaultPresence} from "../../defaultPresence"
import {resolve as resolvePath} from "path"
import {UsersTable} from "../../database/users.js"
export class BotRebuildCommand extends SubcommandData {
private readonly _users: UsersTable
private readonly _cleanUp: () => Promise<void>
constructor({users, cleanUp}: { users: UsersTable, cleanUp(): Promise<void> }) {
super()
this._users = users
this._cleanUp = cleanUp
}
class RebuildCommand extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = {
name: "rebuild",
description: "Rebuilds and restarts the bot.",
@ -18,8 +28,7 @@ class RebuildCommand extends SubcommandData {
}
async execute(b: ChatInputCommandInteraction): Promise<void> {
const user = b.user || b.member
if (!!user && user.id === adminId) {
if (await this._users.getActiveSnowflakeIsAdmin(b.user.id)) {
await b.reply({
content: "I dunno... Let's check if this will work...",
ephemeral: true,
@ -159,7 +168,7 @@ class RebuildCommand extends SubcommandData {
ephemeral: true,
content: "Phewwww... now I'll just... take a quick nap after all that hard work...",
})
await wrappedRestart(b)
await wrappedRestart(b, this._cleanUp)
} else {
await b.reply({
ephemeral: true,
@ -168,5 +177,3 @@ class RebuildCommand extends SubcommandData {
}
}
}
export const commandBotRebuild = new RebuildCommand()

@ -1,4 +1,4 @@
import {adminId, SubcommandData} from "../types"
import {SubcommandData} from "../types"
import {
ActivityType,
ApplicationCommandOptionType,
@ -6,8 +6,18 @@ import {
ChatInputCommandInteraction,
} from "discord.js"
import {wrappedRestart} from "../../ipc/restart"
import {UsersTable} from "../../database/users.js"
export class BotRestartCommand extends SubcommandData {
private readonly _users: UsersTable
private readonly _cleanUp: () => Promise<void>
constructor({users, cleanUp}: { users: UsersTable, cleanUp(): Promise<void> }) {
super()
this._users = users
this._cleanUp = cleanUp
}
class RestartCommand extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = {
name: "restart",
type: ApplicationCommandOptionType.Subcommand,
@ -15,8 +25,7 @@ class RestartCommand extends SubcommandData {
}
async execute(b: ChatInputCommandInteraction): Promise<void> {
const user = b.user || b.member
if (!!user && user.id === adminId) {
if (await this._users.getActiveSnowflakeIsAdmin(b.user.id)) {
await b.reply({
ephemeral: true,
content: "Yaaaawwn... Okay... Just a quick nap then...",
@ -32,7 +41,7 @@ class RestartCommand extends SubcommandData {
},
],
})
await wrappedRestart(b)
await wrappedRestart(b, this._cleanUp)
} else {
await b.reply({
ephemeral: true,
@ -41,5 +50,3 @@ class RestartCommand extends SubcommandData {
}
}
}
export const commandBotRestart = new RestartCommand()

@ -1,7 +1,17 @@
import {adminId, SubcommandData} from "../types"
import {SubcommandData} from "../types"
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ChatInputCommandInteraction} from "discord.js"
import {UsersTable} from "../../database/users.js"
export class BotShutdownCommand extends SubcommandData {
private readonly _users: UsersTable
private readonly _cleanUp: () => Promise<void>
constructor({users, cleanUp}: { users: UsersTable, cleanUp(): Promise<void> }) {
super()
this._users = users
this._cleanUp = cleanUp
}
class ShutdownCommand extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = {
name: "shutdown",
type: ApplicationCommandOptionType.Subcommand,
@ -9,18 +19,16 @@ class ShutdownCommand extends SubcommandData {
}
async execute(b: ChatInputCommandInteraction): Promise<void> {
const user = b.user || b.member
if (!!user && user.id === adminId) {
if (await this._users.getActiveSnowflakeIsAdmin(b.user.id)) {
await b.reply({
ephemeral: true,
content: "Good night =w=",
})
const self = b.client.user
self.presence.set({
b.client.user.presence.set({
status: "invisible",
activities: [],
})
b.client.destroy()
await this._cleanUp()
} else {
await b.reply({
ephemeral: true,
@ -29,5 +37,3 @@ class ShutdownCommand extends SubcommandData {
}
}
}
export const commandBotShutdown = new ShutdownCommand()

@ -1,31 +1,80 @@
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ChatInputCommandInteraction} from "discord.js"
import {SubcommandData} from "../types"
import {UsersTable} from "../../database/users.js"
class CharacterCreateCommandData extends SubcommandData {
export class CharacterCreateCommand extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = {
name: "create",
type: ApplicationCommandOptionType.Subcommand,
description: "Begins the process of creating a new character.",
description: "Begins the process of creating a new character. Omit parameters for interactive process.",
options: [
{
name: "name",
type: ApplicationCommandOptionType.String,
description: "The character's name.",
description: "The character's name, 1-20 characters. You can change this later.",
required: false,
maxLength: 20,
minLength: 1,
},
{
name: "title",
type: ApplicationCommandOptionType.String,
description: "A title for your character, optionally starting or ending with an ellipsis (...).",
description: "The character's title, with @@ where your character's name goes. @@ alone means no title.",
required: false,
maxLength: 30,
minLength: 2,
},
{
name: "pronouns",
type: ApplicationCommandOptionType.String,
description: "Your character's pronouns. Don't worry, you can change this later.",
required: false,
autocomplete: true,
},
{
name: "difficulty",
type: ApplicationCommandOptionType.String,
description: "The difficulty of reformation. Don't worry, you can change this later.",
required: false,
autocomplete: true,
},
{
name: "preference",
type: ApplicationCommandOptionType.String,
description: "What role(s) your character is able to play in vore. Don't worry, you can change this later.",
required: false,
autocomplete: true,
},
{
name: "types",
type: ApplicationCommandOptionType.String,
description: "The type or types that describe your character. Don't worry, you can change this later.",
required: false,
autocomplete: true,
},
{
name: "stats",
type: ApplicationCommandOptionType.String,
description: "Slash(/)-delimited base stats: Confidence/Health/Stamina/Brawn/Durability/Intensity/Resilience/Speed",
required: false,
},
],
}
private readonly _users: UsersTable
constructor({users}: { users: UsersTable }) {
super()
this._users = users
}
async execute(b: ChatInputCommandInteraction) {
await b.deferReply({ephemeral: true})
await b.reply("Okaaaay, I'll make you a character ❤\n\nRight after this nap...")
// create database interfacing code, using this format:
// INSERT INTO character_creation () VALUES () ON CONFLICT DO UPDATE SET (excluded.field IS NULL ? field : excluded.field)
// then prompt the user to fill in the first null field, using a modal to input the text fields
await b.followUp({
ephemeral: true,
content: "Zz... mn?",
})
}
}
export const commandCharacterCreate = new CharacterCreateCommandData()

@ -1,21 +1,29 @@
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ApplicationCommandType} from "discord.js"
import {commandCharacterCreate} from "./create"
import {BaseChatInputCommandData, CommandWithSubcommandsData} from "../types"
import {BaseChatInputCommandData, CommandWithSubcommandsData, SubcommandData} from "../types"
import {UsersTable} from "../../database/users.js"
import {CharacterCreateCommand} from "./create.js"
export class CharacterCommand extends CommandWithSubcommandsData {
readonly create: CharacterCreateCommand
readonly subcommands: SubcommandData[]
private readonly _users: UsersTable
constructor({users}: { users: UsersTable }) {
super()
this._users = users
this.create = new CharacterCreateCommand({users})
this.subcommands = [
this.create,
]
}
class CharacterCommandData extends CommandWithSubcommandsData {
readonly baseDefinition: BaseChatInputCommandData = {
name: "character",
type: ApplicationCommandType.ChatInput,
description: "Commands to manage your characters.",
}
readonly subcommands = [
commandCharacterCreate,
]
}
export const commandCharacter = new CharacterCommandData()
const otherSubcommands: ApplicationCommandSubCommandData[] = [
{
name: "select",

@ -1,9 +1,11 @@
import {commandDefinitions} from "./index"
import {Commands} from "./index"
import {describe, expect, test} from "@jest/globals"
import {InMemoryDatabase} from "../database/inmemory/database.js"
describe("command definitions", () => {
test("has no descriptions over 100 characters", () => {
expect(commandDefinitions)
const db = new InMemoryDatabase()
expect(new Commands({users: db.users, cleanUp: async () => undefined}).definitions)
.not
.toContain(expect.objectContaining({"description": expect.stringMatching(/.{101,}/)}))
})

@ -3,42 +3,66 @@ import {
ApplicationCommandDataResolvable,
ApplicationCommandOptionType,
ApplicationCommandType,
AutocompleteInteraction,
ChatInputCommandInteraction,
Collection,
} from "discord.js"
import {commandCharacter} from "./character/index"
import {CommandData} from "./types"
import {commandBot} from "./bot/index"
import {CharacterCommand} from "./character/index"
import {autocompleteNotImplementedError, CommandData} from "./types"
import {BotCommand} from "./bot/index"
import {UsersTable} from "../database/users.js"
const commands: CommandData[] = [
commandCharacter,
commandBot,
]
export const commandDefinitions: ApplicationCommandDataResolvable[] = commands.map((c) => c.definition)
export const invalidCommandError = "No command by that name exists."
const _commandNameCache: Record<string, CommandData> = Object.fromEntries(
commands.map((c) => [c.definition.name, c]),
)
export class Commands {
readonly all: CommandData[]
readonly character: CharacterCommand
readonly bot: BotCommand
readonly definitions: ApplicationCommandDataResolvable[]
private readonly _nameCache: Record<string, CommandData>
export const invalidCommandError = "No command by that name exists."
constructor({users, cleanUp}: { users: UsersTable, cleanUp: () => Promise<void> }) {
this.character = new CharacterCommand({users})
this.bot = new BotCommand({users, cleanUp})
this.all = [
this.character,
this.bot,
]
this.definitions = this.all.map((c) => c.definition)
this._nameCache = Object.fromEntries(
this.all.map((c) => [c.definition.name, c]),
)
}
export function storeCachedCommands(data: Collection<string, ApplicationCommand>) {
setCache(data: Collection<string, ApplicationCommand>) {
for (const command of data.values()) {
if (_commandNameCache.hasOwnProperty(command.name)) {
_commandNameCache[command.name].setCached(command)
if (this._nameCache.hasOwnProperty(command.name)) {
this._nameCache[command.name].setCached(command)
} else {
console.log("No such command when caching commands: " + command.name)
}
}
}
}
export async function executeCommand(ev: ChatInputCommandInteraction) {
async execute(ev: ChatInputCommandInteraction) {
const name = ev.commandName
if (!_commandNameCache.hasOwnProperty(name)) {
if (!this._nameCache.hasOwnProperty(name)) {
throw invalidCommandError
}
await _commandNameCache[name].execute(ev)
await this._nameCache[name].execute(ev)
}
async autocomplete(ev: AutocompleteInteraction) {
const name = ev.commandName
if (this._nameCache.hasOwnProperty(name)) {
const command = this._nameCache[name]
if (command.autocomplete) {
await command.autocomplete(ev)
return
}
}
throw autocompleteNotImplementedError
}
}
const otherCommands: ApplicationCommandDataResolvable[] = [

@ -2,6 +2,7 @@ import {
ApplicationCommand,
ApplicationCommandSubCommandData,
ApplicationCommandSubGroupData,
AutocompleteInteraction,
ChatInputApplicationCommandData,
ChatInputCommandInteraction,
} from "discord.js"
@ -9,8 +10,7 @@ import {
export const noSubcommandError = "No subcommand was provided, but one is required."
export const invalidSubcommandGroupError = "The subcommand group provided does not exist."
export const invalidSubcommandError = "The subcommand provided does not exist."
export const adminId = "126936953789743104"
export const autocompleteNotImplementedError = "An autocomplete implementation was not provided."
export abstract class CommandData {
private _cachedGlobal: ApplicationCommand | null = null
@ -40,6 +40,8 @@ export abstract class CommandData {
}
abstract execute(b: ChatInputCommandInteraction): Promise<void>
autocomplete?(b: AutocompleteInteraction): Promise<void>
}
export type BaseChatInputCommandData = Omit<ChatInputApplicationCommandData, "options">
@ -59,7 +61,7 @@ export abstract class CommandWithSubcommandsData extends CommandData {
protected abstract get subcommands(): (SubcommandData | SubcommandGroupData)[]
async execute(b: ChatInputCommandInteraction) {
async execute(b: ChatInputCommandInteraction): Promise<void> {
const group = b.options.getSubcommandGroup()
const subcommand = b.options.getSubcommand()
if (group !== null) {
@ -71,6 +73,26 @@ export abstract class CommandWithSubcommandsData extends CommandData {
}
}
async autocomplete?(b: AutocompleteInteraction): Promise<void> {
const group = b.options.getSubcommandGroup()
const subcommand = b.options.getSubcommand()
if (group !== null) {
const groupObject = this.resolveSubcommandGroup(group)
if (groupObject.autocomplete) {
await groupObject.autocomplete(b)
return
}
}
if (subcommand !== null) {
const subcommandObject = this.resolveSubcommand(subcommand)
if (subcommandObject.autocomplete) {
await subcommandObject.autocomplete(b)
return
}
}
throw autocompleteNotImplementedError
}
protected resolveSubcommandGroup(name: string): SubcommandGroupData {
let cache = this._subcommandGroupsCache
if (cache === null) {
@ -131,6 +153,18 @@ export abstract class SubcommandGroupData {
}
}
async autocomplete?(b: AutocompleteInteraction): Promise<void> {
const subcommand = b.options.getSubcommand()
if (subcommand !== null) {
const subcommandObject = this.resolveSubcommand(subcommand)
if (subcommandObject.autocomplete) {
await subcommandObject.autocomplete(b)
return
}
}
throw autocompleteNotImplementedError
}
protected resolveSubcommand(name: string): SubcommandData {
let cache = this._subcommandsCache
if (cache === null) {
@ -151,4 +185,6 @@ export abstract class SubcommandData {
abstract get definition(): ApplicationCommandSubCommandData
abstract execute(b: ChatInputCommandInteraction): Promise<void>
autocomplete?(b: AutocompleteInteraction): Promise<void>
}

@ -0,0 +1,16 @@
import {Client} from "pg"
import {UsersTable, UsersTableImpl} from "./users.js"
export interface Database {
readonly users: UsersTable
}
export class DatabaseImpl implements Database {
readonly users: UsersTableImpl
private readonly _query: Client["query"]
constructor(query: Client["query"]) {
this._query = query
this.users = new UsersTableImpl(this._query)
}
}

@ -0,0 +1,10 @@
import {Database} from "../database.js"
import {InMemoryUsersTable} from "./users.js"
export class InMemoryDatabase implements Database {
readonly users: InMemoryUsersTable
constructor() {
this.users = new InMemoryUsersTable()
}
}

@ -0,0 +1,46 @@
import {userSnowflakeToUuid, UsersTable} from "../users.js"
import {Snowflake} from "discord-api-types/globals.js"
interface InMemoryUserData {
id: string,
is_admin: boolean
active_at: Date | null
}
export class InMemoryUsersTable implements UsersTable {
private readonly _users: Record<string, InMemoryUserData> = {}
async createBotOwnerAsAdmin(snowflake: Snowflake): Promise<void> {
const uuid = userSnowflakeToUuid(snowflake)
this._users[uuid] = {
id: uuid,
is_admin: true,
active_at: null,
}
}
async getActiveSnowflakeIsAdmin(snowflake: Snowflake): Promise<boolean> {
const uuid = userSnowflakeToUuid(snowflake)
if (!this._users.hasOwnProperty(uuid)) {
this._users[uuid] = {
id: uuid,
is_admin: false,
active_at: new Date(),
}
}
const user = this._users[uuid]
return user.is_admin
}
async getUuidForActiveSnowflake(snowflake: Snowflake): Promise<string> {
const uuid = userSnowflakeToUuid(snowflake)
if (!this._users.hasOwnProperty(uuid)) {
this._users[uuid] = {
id: uuid,
is_admin: false,
active_at: new Date(),
}
}
return uuid
}
}

@ -0,0 +1,110 @@
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 {Client} from "pg"
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: Client["query"]
constructor(query: Client["query"]) {
this._query = query
}
async getUuidForActiveSnowflake(snowflake: Snowflake): Promise<string> {
const result = await this._query(`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(`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(`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,
})
}

@ -83,7 +83,7 @@ export async function reportReady(c: Client<true>): Promise<boolean> {
return true
}
export async function reportFailed(c: Client, error: unknown): Promise<void> {
export async function reportFailed(c: Client<true>, error: unknown): Promise<void> {
console.log("failed to start", error)
if (restartState) {
try {
@ -105,7 +105,7 @@ export async function reportFailed(c: Client, error: unknown): Promise<void> {
process.send(startData)
}
export async function wrappedRestart(b: ChatInputCommandInteraction) {
export async function wrappedRestart(b: ChatInputCommandInteraction, cleanup: () => Promise<void>) {
try {
await doRestart(b.client, b.webhook.id, b.webhook.token, RestartReason.RestartCommand)
} catch (ex) {
@ -126,7 +126,7 @@ export async function wrappedRestart(b: ChatInputCommandInteraction) {
}
}
} finally {
b.client.destroy()
await cleanup()
}
}

@ -1,5 +1,9 @@
import {BaseInteraction, ChatInputCommandInteraction} from "discord.js"
import {AutocompleteInteraction, BaseInteraction, ChatInputCommandInteraction} from "discord.js"
export function isChatInputCommand(x: BaseInteraction): x is ChatInputCommandInteraction {
return x.isChatInputCommand()
}
export function isAutocomplete(x: BaseInteraction): x is AutocompleteInteraction {
return x.isAutocomplete()
}
Loading…
Cancel
Save