Implement the majority of the API

This commit is contained in:
Oliver-Akins 2023-08-31 19:12:34 -06:00
parent e322f24457
commit 4b61e73573
15 changed files with 475 additions and 6 deletions

179
api/package-lock.json generated
View file

@ -13,6 +13,8 @@
"glob": "^10.3.3", "glob": "^10.3.3",
"joi": "^17.9.2", "joi": "^17.9.2",
"module-alias": "^2.2.3", "module-alias": "^2.2.3",
"mysql2": "^3.6.0",
"tmi.js": "^1.8.5",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"tslog": "^4.8.2" "tslog": "^4.8.2"
}, },
@ -20,6 +22,7 @@
"@types/chai": "^4.3.5", "@types/chai": "^4.3.5",
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.1",
"@types/node": "^20.4.7", "@types/node": "^20.4.7",
"@types/tmi.js": "^1.8.3",
"chai": "^4.3.7", "chai": "^4.3.7",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"ts-mocha": "^10.0.0", "ts-mocha": "^10.0.0",
@ -521,6 +524,12 @@
"integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==", "integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==",
"dev": true "dev": true
}, },
"node_modules/@types/tmi.js": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@types/tmi.js/-/tmi.js-1.8.3.tgz",
"integrity": "sha512-piKPU1DF+Lxnh9BV0gVpbJIMnKOQT6zT0o6NLE0DZgHNW7fGxReRRSp987+Ph1aYiyNWXXbxAeQAovTlEYC1hw==",
"dev": true
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.10.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
@ -855,6 +864,14 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/diff": { "node_modules/diff": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
@ -967,6 +984,14 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/get-caller-file": { "node_modules/get-caller-file": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -1050,6 +1075,17 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -1125,6 +1161,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
},
"node_modules/is-unicode-supported": { "node_modules/is-unicode-supported": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@ -1238,6 +1279,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
"node_modules/loupe": { "node_modules/loupe": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
@ -1402,6 +1448,51 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true "dev": true
}, },
"node_modules/mysql2": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz",
"integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/mysql2/node_modules/lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
"engines": {
"node": ">=16.14"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
"dependencies": {
"lru-cache": "^7.14.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/named-placeholders/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"engines": {
"node": ">=12"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
@ -1414,6 +1505,25 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -1574,6 +1684,16 @@
} }
] ]
}, },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serialize-javascript": { "node_modules/serialize-javascript": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
@ -1632,6 +1752,14 @@
"source-map": "^0.6.0" "source-map": "^0.6.0"
} }
}, },
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -1717,6 +1845,18 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/tmi.js": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/tmi.js/-/tmi.js-1.8.5.tgz",
"integrity": "sha512-A9qrydfe1e0VWM9MViVhhxVgvLpnk7pFShVUWePsSTtoi+A1X+Zjdoa7OJd7/YsgHXGj3GkNEvnWop/1WwZuew==",
"dependencies": {
"node-fetch": "^2.6.1",
"ws": "^8.2.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -1729,6 +1869,11 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-mocha": { "node_modules/ts-mocha": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-10.0.0.tgz", "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-10.0.0.tgz",
@ -1920,6 +2065,20 @@
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true "dev": true
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -1980,6 +2139,26 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true "dev": true
}, },
"node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -13,6 +13,8 @@
"glob": "^10.3.3", "glob": "^10.3.3",
"joi": "^17.9.2", "joi": "^17.9.2",
"module-alias": "^2.2.3", "module-alias": "^2.2.3",
"mysql2": "^3.6.0",
"tmi.js": "^1.8.5",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"tslog": "^4.8.2" "tslog": "^4.8.2"
}, },
@ -20,6 +22,7 @@
"@types/chai": "^4.3.5", "@types/chai": "^4.3.5",
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.1",
"@types/node": "^20.4.7", "@types/node": "^20.4.7",
"@types/tmi.js": "^1.8.3",
"chai": "^4.3.7", "chai": "^4.3.7",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"ts-mocha": "^10.0.0", "ts-mocha": "^10.0.0",

View file

@ -0,0 +1,37 @@
import { ServerRoute } from "@hapi/hapi";
import boom from "@hapi/boom";
const route: ServerRoute = {
method: `POST`, path: `/{channel}`,
async handler(request, h) {
const { channel } = request.params;
const { bot, db } = request.server.app;
let [ channels ] = await db.query<Array<any>>(
`select * from tqna.twitchs where channel = ?`,
[ channel ]
);
if (channels.length > 0) {
return h.response().code(202);
};
try {
await bot.join(channel);
} catch {
throw boom.badRequest(`Could not connect to that channel`);
};
try {
await db.query(
`insert into tqna.twitchs ( channel ) values ( ? )`,
[ channel ]
);
} catch {
await bot.part(channel);
throw boom.badImplementation(`Failed to save the channel`);
};
return h.response().code(204);
},
};
export default route;

View file

@ -0,0 +1,56 @@
import { Query } from "~/types/utilities";
import { ServerRoute } from "@hapi/hapi";
import Joi from "joi";
const route: ServerRoute = {
method: `POST`, path: `/{channel}/questions`,
options: {
validate: {
params: Joi.object({
channel: Joi.string().required(),
}),
payload: Joi.object({
question: Joi.string().required(),
asker: Joi.string().required(),
}),
},
},
async handler(request, h) {
const { channel } = request.params;
const { db, log } = request.server.app;
const payload = request.payload as any;
log.debug(`Attempting to save a new question in channel: ${channel}`);
let errored = false;
let question = null;
let conn = await db.getConnection();
await conn.beginTransaction();
try {
await db.query(
`insert into tqna.questions ( channel, asker, question ) values ( ?, ?, ?)`,
[ channel, payload.asker, payload.question ]
);
let [ questions ] = await db.query<Query<Question, false>>(
`select * from tqna.questions where id = last_insert_id() limit 1`
);
question = questions[0];
await conn.commit();
} catch {
log.error(`Failed to add the question`);
await conn.rollback();
} finally {
conn.release();
};
if (errored) {
throw new Error();
};
log.debug(`Created new question in channel: ${channel}`);
return question;
},
};
export default route;

View file

@ -0,0 +1,25 @@
import { ServerRoute } from "@hapi/hapi";
import Joi from "joi";
const route: ServerRoute = {
method: `GET`, path: `/{channel}/questions`,
options: {
validate: {
params: Joi.object({
channel: Joi.string().required(),
}),
},
},
async handler(request) {
const { channel } = request.params;
const { db, log } = request.server.app;
log.debug(`Listing questions for channel: ${channel}`);
let [ questions ] = await db.query(
`select * from tqna.questions as q where q.channel = ?`,
[ channel ]
);
return questions;
},
};
export default route;

View file

@ -0,0 +1,77 @@
import { Query } from "~/types/utilities";
import { ServerRoute } from "@hapi/hapi";
import Joi from "joi";
const route: ServerRoute = {
method: `POST`, path: `/{channel}/questions/{question_id}`,
options: {
validate: {
params: Joi.object({
channel: Joi
.string()
.required(),
question_id: Joi
.number()
.positive()
.integer()
.not(0)
.required(),
}),
payload: Joi.object({
question: Joi.string().optional(),
asker: Joi.string().optional(),
answered: Joi.boolean().optional(),
id: Joi.forbidden(),
})
.min(1),
},
},
async handler(request, h) {
const { channel, question_id } = request.params;
const { db, log } = request.server.app;
const payload = request.payload as any;
let question = null;
log.debug(`Updating question ${question_id} for channel ${channel}`);
let conn = await db.getConnection();
await conn.beginTransaction();
try {
let setters = [];
let values = [];
for (const key in payload) {
let v = payload[key];
if (v.startsWith(`__`)) {
setters.push(`${key} = ${v}`);
} else {
setters.push(`${key} = ?`);
values.push(v);
};
};
await db.query(
`update tqna.questions
( ${setters.join(`, `)} )
where channel = ? and id = ?
limit 1`,
[...values, channel, question_id ]
);
let [ questions ] = await db.query<Query<Question, false>>(
`select * from tqna.questions where id = ? limit 1`,
[ question_id ]
);
question = questions[0];
await conn.commit();
} catch {
log.error(`Failed to add the question`);
await conn.rollback();
} finally {
conn.release();
};
return question;
},
};
export default route;

View file

@ -1,11 +1,15 @@
import { Server } from "@hapi/hapi"; import { Server } from "@hapi/hapi";
import { globSync } from "glob"; import { globSync } from "glob";
import path from "path"; import path from "path";
import { log } from "./main";
const server = new Server({ const server = new Server({
port: 6969, port: 6969,
host: `0.0.0.0`, host: `0.0.0.0`,
debug: {
request: [ `*` ],
},
}); });
@ -17,6 +21,7 @@ async function registerRoutes() {
for (const file of files) { for (const file of files) {
let route = (await import(path.join(__dirname, file))).default; let route = (await import(path.join(__dirname, file))).default;
server.route(route); server.route(route);
log.debug(`Registered route: ${route.method} ${route.path}`);
}; };
}; };

View file

@ -2,10 +2,12 @@
// at the top of this file as the first statement // at the top of this file as the first statement
import "module-alias/register"; import "module-alias/register";
import { start as startBot } from "~/twitch-bot";
import { start as startApi } from "~/hapi";
import mysql from "mysql2/promise";
import { Logger } from "tslog"; import { Logger } from "tslog";
import { start } from "~/hapi";
export const isDev = process.env.NODE_ENV?.startsWith(`dev`); export const isDev = !!process.env.NODE_ENV?.startsWith(`dev`);
let logLevel = 4; let logLevel = 4;
if (process.env.LOG_LEVEL) { if (process.env.LOG_LEVEL) {
@ -22,8 +24,26 @@ export const log = new Logger({
async function main() { async function main() {
let server = await start();
log.info(`Server listening`) const db = mysql.createPool({
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: `tqna`,
namedPlaceholders: true,
});
let server = await startApi();
server.app.log = log;
server.app.isDev = isDev;
server.app.db = db;
log.info(`Server listening`);
let tmi = await startBot(db, server);
server.app.bot = tmi;
log.info(`Bot listening`);
}; };
if (require.main === module) { if (require.main === module) {

34
api/src/twitch-bot.ts Normal file
View file

@ -0,0 +1,34 @@
import { Pool, RowDataPacket } from "mysql2/promise";
import { Server } from "@hapi/hapi";
import { log } from "./main";
import tmi from "tmi.js";
export async function start(db: Pool, server: Server) {
let [ channels ] = await db.query<Array<any>>({
sql: `select channel from tqna.twitchs`,
rowsAsArray: true
});
const client = new tmi.Client({
channels: [...new Set(channels.flat())] as string[],
});
client.on(`message`, async (channel, user, message, self) => {
log.silly(`Twitch message: [c:${channel}] [u:${user}] ${message}`);
if (self) { return };
if (message.match(/^[Q]:/i)) {
await server.inject({
method: `POST`, url: `/${channel.slice(1)}/questions`,
payload: {
question: message.slice(2).trim(),
asker: user["display-name"] ?? user.username,
},
});
};
});
await client.connect();
return client;
};

7
api/src/types/database.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
interface Question {
readonly __id: number;
channel: number;
asker: string;
question: string;
answered: boolean;
}

12
api/src/types/hapi.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
import mysql from "mysql2/promise";
import { Client } from "tmi.js";
import { Logger } from "tslog";
declare module "@hapi/hapi" {
interface ServerApplicationState {
db: mysql.Pool;
log: Logger<unknown>;
isDev: boolean;
bot: Client;
}
}

View file

@ -0,0 +1,6 @@
import type { RowDataPacket } from "mysql2/promise";
export type Query<T, P = true> =
P extends true
? (Partial<T> & RowDataPacket)[]
: (T & RowDataPacket)[]

View file

View file

@ -1,9 +1,14 @@
create database if not exists tqna; create database if not exists tqna;
create table if not exists tqna.twitchs (
`id` integer not null auto_increment primary key,
`channel` text
);
create table if not exists tqna.questions ( create table if not exists tqna.questions (
`__id` integer not null auto_increment primary key, `id` integer not null auto_increment primary key,
`channel` text, `channel` text,
`asker` text, `asker` text,
`question` text, `question` text,
`answered` boolean `answered` boolean not null default false
); );

View file

@ -11,6 +11,9 @@ services:
- "6969:6969" - "6969:6969"
environment: environment:
NODE_ENV: "dev" NODE_ENV: "dev"
MYSQL_HOST: "db"
MYSQL_USER: "root"
MYSQL_PASSWORD: "root"
healthcheck: healthcheck:
disable: true disable: true
volumes: volumes: