0
0
Fork 0
This commit is contained in:
Oliver Akins 2022-07-22 00:22:49 -06:00
parent 6a5f642fb4
commit 48dffc112a
No known key found for this signature in database
GPG key ID: 3C2014AF9457AF99
21 changed files with 1327 additions and 0 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
data/
config.toml
# Logs
logs
*.log

126
docs/index.html Normal file
View file

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lurk Message Manager</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
<link rel="stylesheet" href="./style.css">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./script.js" defer></script>
</head>
<body>
<div id="app">
<div v-if="!authenticated" id="login">
<h2>Login</h2>
<label for="username">Username</label>
<input id="username" type="text" v-model="login.username">
<label for="password">Password</label>
<input id="password" type="password" v-model="login.password">
<button
@click.stop="tryLogin"
>
Login
</button>
</div>
<div v-cloak v-else>
<div id="header">
<label for="channel-selector">
Select Channel
</label>
<select
name="channel selector"
id="channel-selector"
v-model="channel"
@change="getMessages"
>
<option value="" selected disabled>Select a Channel</option>
<option
v-for="channel in channels"
:key="channel"
:value="channel"
>
{{channel}}
</option>
</select>
<button
:disabled="new_group"
@click.stop="addGroup"
>
Add Message Group
</button>
</div>
<hr>
<div class="body">
<div
v-for="(group, i) in messages"
:key="group.id"
class="lurk-group"
>
<div class="flex-container">
<h3 style="flex-grow: 1;">Message Group {{i + 1}}</h3>
<button
v-if="group.id != 'new-group'"
@click.stop="deleteGroup(group)"
>
Delete Group
</button>
<div v-else>
<button
@click.stop="removeNewGroup"
>
Cancel
</button>
<button
:disabled="group.lurk.length == 0 || group.unlurk.length == 0"
@click.stop="saveGroup(group)"
>
Save Group
</button>
</div>
</div>
<i>Group ID: {{group.id}}</i>
<hr>
<div class="lurk-messages group-messages">
<h4>Lurk Messages</h4>
<div v-for="msg in group.lurk" class="flex-container message">
<div style="flex-grow: 1">{{msg}}</div>
<button @click.stop="deleteLurk(group, msg)">Delete Message</button>
</div>
<br>
<div class="flex-container">
<input type="text" class="message-input" v-model="group.new.lurk">
<button
:disabled="group.new.lurk.length == 0"
@click.stop="addLurk(group)"
>
Add Lurk Message
</button>
</div>
</div>
<hr>
<div class="unlurk-messages group-messages">
<h4>Unlurk Messages</h4>
<div v-for="msg in group.unlurk" class="flex-container message">
<div style="flex-grow: 1">{{msg}}</div>
<button @click.stop="deleteUnlurk(group, msg)">Delete Message</button>
</div>
<br>
<div class="flex-container">
<input type="text" class="message-input" v-model="group.new.unlurk">
<button
:disabled="group.new.unlurk.length == 0"
@click.stop="addUnlurk(group)"
>
Add Unlurk Message
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

182
docs/script.js Normal file
View file

@ -0,0 +1,182 @@
const app = new Vue({
el: `#app`,
data() {return {
url: `http://localhost:4000`,
authenticated: false,
channels: [],
channel: ``,
login: {
username: `alkali`,
password: `metal`,
},
api: null,
messages: [],
new_group: false,
}},
methods: {
async tryLogin() {
try {
let r = await axios.post(
`${this.url}/login`,
undefined,
{ auth: this.login }
);
this.api = axios.create({
baseURL: this.url,
validateStatus: null,
auth: this.login,
});
this.channels.push(...r.data.sort());
this.authenticated = true;
} catch (_) {};
},
async getMessages() {
if (!this.api) { return };
let r = await this.api.get(`/manage/${this.channel}`);
if (r.status === 200) {
for (const m of r.data) {
m.new = {
lurk: ``,
unlurk: ``,
};
};
this.messages = r.data;
} else {
this.messages = [];
};
},
async addLurk(group) {
if (group.id === `new-group`) {
group.lurk.push(group.new.lurk);
} else {
if (!this.api) { return };
let payload = {
lurk: [
...group.lurk,
group.new.lurk
],
unlurk: group.unlurk,
};
let r = await this.api.patch(
`/manage/${this.channel}/message/${group.id}`,
payload
);
if (r.status != 200) {
alert(`Failed to update the group. Status: ${r.status}`);
} else {
group.lurk.push(group.new.lurk);
alert(`Added lurk message successfully.`);
};
};
group.new.lurk = ``;
},
async addUnlurk(group) {
if (group.id === `new-group`) {
group.unlurk.push(group.new.unlurk);
} else {
if (!this.api) { return };
let payload = {
lurk: group.lurk,
unlurk: [
...group.unlurk,
group.new.unlurk
],
};
let r = await this.api.patch(
`/manage/${this.channel}/message/${group.id}`,
payload
);
if (r.status != 200) {
alert(`Failed to update the group. Status: ${r.status}`);
} else {
group.unlurk.push(group.new.unlurk);
alert(`Added unlurk message successfully.`);
};
};
group.new.unlurk = ``;
},
addGroup() {
this.new_group = true;
this.messages.unshift({
lurk: [],
unlurk: [],
id: "new-group",
new: {
lurk: ``,
unlurk: ``,
},
});
},
async saveGroup(group) {
if (!this.api) { return };
let r = await this.api.post(`/manage/${this.channel}/message`, {
lurk: group.lurk,
unlurk: group.unlurk,
});
if (r.status === 200) {
this.messages.shift()
this.messages.push({
...r.data,
new: {
lurk: ``,
unlurk: ``,
},
});
alert(`Group has been saved`);
};
this.new_group = false;
},
async deleteGroup(group) {
if (!this.api) { return };
let r = await this.api.delete(`/manage/${this.channel}/message/${group.id}`);
if (r.status === 200) {
this.messages.splice(this.messages.indexOf(group, 1));
alert(`Group has been deleted`);
};
},
async deleteLurk(group, message) {
if (group.id == "new-group") {
group.lurk.splice(group.lurk.indexOf(message), 1);
} else {
if (!this.api) { return };
let r = await this.api.patch(
`/manage/${this.channel}/message/${group.id}`,
{
lurk: group.lurk.filter(m => m != message),
unlurk: group.unlurk,
}
);
if (r.status == 200) {
group.lurk.splice(group.lurk.indexOf(message), 1);
alert(`Deleted message successfully`);
} else {
alert(`Failed to delete the message. Status: ${r.status}`);
};
};
},
async deleteUnlurk(group, message) {
if (group.id === `new-group`) {
group.unlurk.splice(group.unlurk.indexOf(message), 1);
} else {
if (!this.api) { return };
let r = await this.api.patch(
`/manage/${this.channel}/message/${group.id}`,
{
lurk: group.lurk,
unlurk: group.unlurk.filter(m => m != message),
}
);
if (r.status == 200) {
group.unlurk.splice(group.unlurk.indexOf(message), 1);
alert(`Deleted message successfully`);
} else {
alert(`Failed to delete the message. Status: ${r.status}`);
};
};
},
removeNewGroup() {
this.messages.shift();
this.new_group = false;
},
},
});

35
docs/style.css Normal file
View file

@ -0,0 +1,35 @@
[v-cloak] {
display: none;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 5px;
}
#login {
text-align: center;
}
#login > input {
margin: 5px auto;
}
.lurk-group {
background: #1a242f;
border-radius: 10px;
margin: 7px;
padding: 10px;
}
.message {
display: flex;
align-items: center;
}
.flex-container {
display: flex;
}
.message-input {
flex-grow: 1;
}

8
makefile Normal file
View file

@ -0,0 +1,8 @@
.PHONY: dist dev prod
dist:
@rm -rf dist
tsc
dev: dist
NODE_ENV=development node dist/main.js

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "twitch-lurk-message-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Oliver Akins",
"license": "UNLICENSED",
"dependencies": {
"@hapi/basic": "^6.0.0",
"@hapi/boom": "^10.0.0",
"@hapi/hapi": "^20.2.2",
"glob": "^8.0.3",
"joi": "^17.6.0",
"module-alias": "^2.2.2",
"toml": "^3.0.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/glob": "^7.2.0",
"@types/hapi__basic": "^5.1.2",
"@types/hapi__hapi": "^20.0.12",
"@types/node": "^18.0.6",
"@types/uuid": "^8.3.4"
},
"_moduleAliases": {
"@": "./dist"
}
}

417
pnpm-lock.yaml generated Normal file
View file

@ -0,0 +1,417 @@
lockfileVersion: 5.4
specifiers:
'@hapi/basic': ^6.0.0
'@hapi/boom': ^10.0.0
'@hapi/hapi': ^20.2.2
'@types/glob': ^7.2.0
'@types/hapi__basic': ^5.1.2
'@types/hapi__hapi': ^20.0.12
'@types/node': ^18.0.6
'@types/uuid': ^8.3.4
glob: ^8.0.3
joi: ^17.6.0
module-alias: ^2.2.2
toml: ^3.0.0
uuid: ^8.3.2
dependencies:
'@hapi/basic': 6.0.0
'@hapi/boom': 10.0.0
'@hapi/hapi': 20.2.2
glob: 8.0.3
joi: 17.6.0
module-alias: 2.2.2
toml: 3.0.0
uuid: 8.3.2
devDependencies:
'@types/glob': 7.2.0
'@types/hapi__basic': 5.1.2
'@types/hapi__hapi': 20.0.12
'@types/node': 18.0.6
'@types/uuid': 8.3.4
packages:
/@hapi/accept/5.0.2:
resolution: {integrity: sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/hoek': 9.3.0
dev: false
/@hapi/ammo/5.0.1:
resolution: {integrity: sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA==}
dependencies:
'@hapi/hoek': 9.3.0
dev: false
/@hapi/b64/5.0.0:
resolution: {integrity: sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw==}
dependencies:
'@hapi/hoek': 9.3.0
/@hapi/basic/6.0.0:
resolution: {integrity: sha512-nWWSXNCq3WptnP3To2c8kfQiRFDUnd9FQOcMS0B85y1x/m12c0hhp+VdmK60BMe44k6WIog1n6g8f9gZOagqBg==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/hoek': 9.3.0
dev: false
/@hapi/boom/10.0.0:
resolution: {integrity: sha512-1YVs9tLHhypBqqinKQRqh7FUERIolarQApO37OWkzD+z6y6USi871Sv746zBPKcIOBuI6g6y4FrwX87mmJ90Gg==}
dependencies:
'@hapi/hoek': 10.0.0
dev: false
/@hapi/boom/9.1.4:
resolution: {integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==}
dependencies:
'@hapi/hoek': 9.3.0
/@hapi/bounce/2.0.0:
resolution: {integrity: sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/hoek': 9.3.0
dev: false
/@hapi/bourne/2.1.0:
resolution: {integrity: sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q==}
/@hapi/call/8.0.1:
resolution: {integrity: sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/hoek': 9.3.0
dev: false
/@hapi/catbox-memory/5.0.1:
resolution: {integrity: sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/hoek': 9.3.0
dev: false
/@hapi/catbox/11.1.1:
resolution: {integrity: sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/hoek': 9.3.0
'@hapi/podium': 4.1.3
'@hapi/validate': 1.1.3
dev: false
/@hapi/content/5.0.2:
resolution: {integrity: sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw==}
dependencies:
'@hapi/boom': 9.1.4
dev: false
/@hapi/cryptiles/5.1.0:
resolution: {integrity: sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==}
engines: {node: '>=12.0.0'}
dependencies:
'@hapi/boom': 9.1.4
/@hapi/file/2.0.0:
resolution: {integrity: sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==}
dev: false
/@hapi/hapi/20.2.2:
resolution: {integrity: sha512-crhU6TIKt7QsksWLYctDBAXogk9PYAm7UzdpETyuBHC2pCa6/+B5NykiOVLG/3FCIgHo/raPVtan8bYtByHORQ==}
engines: {node: '>=12.0.0'}
dependencies:
'@hapi/accept': 5.0.2
'@hapi/ammo': 5.0.1
'@hapi/boom': 9.1.4
'@hapi/bounce': 2.0.0
'@hapi/call': 8.0.1
'@hapi/catbox': 11.1.1
'@hapi/catbox-memory': 5.0.1
'@hapi/heavy': 7.0.1
'@hapi/hoek': 9.3.0
'@hapi/mimos': 6.0.0
'@hapi/podium': 4.1.3
'@hapi/shot': 5.0.5
'@hapi/somever': 3.0.1
'@hapi/statehood': 7.0.4
'@hapi/subtext': 7.0.4
'@hapi/teamwork': 5.1.1
'@hapi/topo': 5.1.0
'@hapi/validate': 1.1.3
dev: false
/@hapi/heavy/7.0.1:
resolution: {integrity: sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/hoek': 9.3.0
'@hapi/validate': 1.1.3
dev: false
/@hapi/hoek/10.0.0:
resolution: {integrity: sha512-CeNFz1JcLZ5xE8Vc9aau37cgHw9bxXqSDK/D55GF2GAOv0n0XjyyjSodHtKahB7A1tV3FlgCpijp3zkSITmBdA==}
dev: false
/@hapi/hoek/9.3.0:
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
/@hapi/iron/6.0.0:
resolution: {integrity: sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw==}
dependencies:
'@hapi/b64': 5.0.0
'@hapi/boom': 9.1.4
'@hapi/bourne': 2.1.0
'@hapi/cryptiles': 5.1.0
'@hapi/hoek': 9.3.0
/@hapi/mimos/6.0.0:
resolution: {integrity: sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg==}
dependencies:
'@hapi/hoek': 9.3.0
mime-db: 1.52.0
dev: false
/@hapi/nigel/4.0.2:
resolution: {integrity: sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw==}
engines: {node: '>=12.0.0'}
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/vise': 4.0.0
dev: false
/@hapi/pez/5.0.3:
resolution: {integrity: sha512-mpikYRJjtrbJgdDHG/H9ySqYqwJ+QU/D7FXsYciS9P7NYBXE2ayKDAy3H0ou6CohOCaxPuTV4SZ0D936+VomHA==}
dependencies:
'@hapi/b64': 5.0.0
'@hapi/boom': 9.1.4
'@hapi/content': 5.0.2
'@hapi/hoek': 9.3.0
'@hapi/nigel': 4.0.2
dev: false
/@hapi/podium/4.1.3:
resolution: {integrity: sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g==}
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/teamwork': 5.1.1
'@hapi/validate': 1.1.3
/@hapi/shot/5.0.5:
resolution: {integrity: sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A==}
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/validate': 1.1.3
dev: false
/@hapi/somever/3.0.1:
resolution: {integrity: sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w==}
dependencies:
'@hapi/bounce': 2.0.0
'@hapi/hoek': 9.3.0
dev: false
/@hapi/statehood/7.0.4:
resolution: {integrity: sha512-Fia6atroOVmc5+2bNOxF6Zv9vpbNAjEXNcUbWXavDqhnJDlchwUUwKS5LCi5mGtCTxRhUKKHwuxuBZJkmLZ7fw==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/bounce': 2.0.0
'@hapi/bourne': 2.1.0
'@hapi/cryptiles': 5.1.0
'@hapi/hoek': 9.3.0
'@hapi/iron': 6.0.0
'@hapi/validate': 1.1.3
dev: false
/@hapi/subtext/7.0.4:
resolution: {integrity: sha512-Y72moHhbRuO8kwBHFEnCRw7oOnhNh4Pl+aonxAze18jkyMpE4Gwz4lNID7ei8vd3lpXC2rKdkxXJgtfY+WttRw==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/bourne': 2.1.0
'@hapi/content': 5.0.2
'@hapi/file': 2.0.0
'@hapi/hoek': 9.3.0
'@hapi/pez': 5.0.3
'@hapi/wreck': 17.2.0
dev: false
/@hapi/teamwork/5.1.1:
resolution: {integrity: sha512-1oPx9AE5TIv+V6Ih54RP9lTZBso3rP8j4Xhb6iSVwPXtAM+sDopl5TFMv5Paw73UnpZJ9gjcrTE1BXrWt9eQrg==}
engines: {node: '>=12.0.0'}
/@hapi/topo/5.1.0:
resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==}
dependencies:
'@hapi/hoek': 9.3.0
/@hapi/validate/1.1.3:
resolution: {integrity: sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA==}
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/topo': 5.1.0
/@hapi/vise/4.0.0:
resolution: {integrity: sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg==}
dependencies:
'@hapi/hoek': 9.3.0
dev: false
/@hapi/wreck/17.2.0:
resolution: {integrity: sha512-pJ5kjYoRPYDv+eIuiLQqhGon341fr2bNIYZjuotuPJG/3Ilzr/XtI+JAp0A86E2bYfsS3zBPABuS2ICkaXFT8g==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/bourne': 2.1.0
'@hapi/hoek': 9.3.0
dev: false
/@sideway/address/4.1.4:
resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
dependencies:
'@hapi/hoek': 9.3.0
/@sideway/formula/3.0.0:
resolution: {integrity: sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==}
/@sideway/pinpoint/2.0.0:
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
/@types/glob/7.2.0:
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
dependencies:
'@types/minimatch': 3.0.5
'@types/node': 18.0.6
dev: true
/@types/hapi__basic/5.1.2:
resolution: {integrity: sha512-sqoQ34nwmRlNgQ5fdHHnghyVzs23aC2d30l9G1sKvdrh4PDV22bnQ+8fBCjoItT+0gGSuXIoG4qdoVZGBjH5EQ==}
dependencies:
'@types/hapi__hapi': 20.0.12
joi: 17.6.0
dev: true
/@types/hapi__catbox/10.2.4:
resolution: {integrity: sha512-A6ivRrXD5glmnJna1UAGw87QNZRp/vdFO9U4GS+WhOMWzHnw+oTGkMvg0g6y1930CbeheGOCm7A1qHsqH7AXqg==}
dev: true
/@types/hapi__hapi/20.0.12:
resolution: {integrity: sha512-B+0fceCzFvbIOVv5YWOZzbHtEff8BLlGH3etrkcOedyj7F0unC5FjzFfaaO5gwlhJDdX0cmmMeRg2pwRdMa2CQ==}
dependencies:
'@hapi/boom': 9.1.4
'@hapi/iron': 6.0.0
'@hapi/podium': 4.1.3
'@types/hapi__catbox': 10.2.4
'@types/hapi__mimos': 4.1.4
'@types/hapi__shot': 4.1.2
'@types/node': 18.0.6
joi: 17.6.0
dev: true
/@types/hapi__mimos/4.1.4:
resolution: {integrity: sha512-i9hvJpFYTT/qzB5xKWvDYaSXrIiNqi4ephi+5Lo6+DoQdwqPXQgmVVOZR+s3MBiHoFqsCZCX9TmVWG3HczmTEQ==}
dependencies:
'@types/mime-db': 1.43.1
dev: true
/@types/hapi__shot/4.1.2:
resolution: {integrity: sha512-8wWgLVP1TeGqgzZtCdt+F+k15DWQvLG1Yv6ZzPfb3D5WIo5/S+GGKtJBVo2uNEcqabP5Ifc71QnJTDnTmw1axA==}
dependencies:
'@types/node': 18.0.6
dev: true
/@types/mime-db/1.43.1:
resolution: {integrity: sha512-kGZJY+R+WnR5Rk+RPHUMERtb2qBRViIHCBdtUrY+NmwuGb8pQdfTqQiCKPrxpdoycl8KWm2DLdkpoSdt479XoQ==}
dev: true
/@types/minimatch/3.0.5:
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
dev: true
/@types/node/18.0.6:
resolution: {integrity: sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==}
dev: true
/@types/uuid/8.3.4:
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
dev: true
/balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: false
/brace-expansion/2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
dev: false
/fs.realpath/1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: false
/glob/8.0.3:
resolution: {integrity: sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==}
engines: {node: '>=12'}
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 5.1.0
once: 1.4.0
dev: false
/inflight/1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
once: 1.4.0
wrappy: 1.0.2
dev: false
/inherits/2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/joi/17.6.0:
resolution: {integrity: sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==}
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/topo': 5.1.0
'@sideway/address': 4.1.4
'@sideway/formula': 3.0.0
'@sideway/pinpoint': 2.0.0
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/minimatch/5.1.0:
resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==}
engines: {node: '>=10'}
dependencies:
brace-expansion: 2.0.1
dev: false
/module-alias/2.2.2:
resolution: {integrity: sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==}
dev: false
/once/1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
dev: false
/toml/3.0.0:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
dev: false
/uuid/8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
dev: false
/wrappy/1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: false

17
src/endpoints/login.ts Normal file
View file

@ -0,0 +1,17 @@
import { db } from "@/main";
import { ServerRoute } from "@hapi/hapi";
const data: ServerRoute = {
method: `POST`, path: `/login`,
async handler(request, h) {
const { access } = request.auth.credentials as { username: string, access: string[] };
let channels = access.filter(x => x != "*");
if (access.includes(`*`)) {
channels.push(...Object.keys(db).filter(c => !channels.includes(c)));
};
return h.response(channels).code(200);
},
};
export default data;

View file

@ -0,0 +1,27 @@
import { ServerRoute } from "@hapi/hapi";
import boom from "@hapi/boom";
import { db } from "@/main";
import Joi from "joi";
const data: ServerRoute = {
method: `POST`, path: `/manage`,
options: {
validate: {
payload: Joi.object({
channel: Joi.string().alphanum(),
}),
},
},
async handler(request, h) {
const { channel } = request.params;
if (!db[channel]) {
throw boom.notFound(`Invalid channel`);
};
db[channel].lurkers = {};
return h.response().code(200);
},
};
export default data;

View file

@ -0,0 +1,38 @@
import { ServerRoute } from "@hapi/hapi";
import boom from "@hapi/boom";
import { db } from "@/main";
import { v4 } from "uuid";
import Joi from "joi";
const data: ServerRoute = {
method: [`POST`, `PUT`], path: `/manage/{channel}/message`,
options: {
validate: {
payload: Joi.object({
lurk: Joi.array().items(Joi.string().min(1)).min(1),
unlurk: Joi.array().items(Joi.string().min(1)).min(1),
}),
params: Joi.object({
channel: Joi.string().alphanum(),
}),
},
},
async handler(request, h) {
const { channel } = request.params;
const data = request.payload as lurk_message;
const id = v4();
if (!db[channel]) {
throw boom.notFound(`Invalid channel`);
};
db[channel].messages[id] = data;
return h.response({
lurk: data.lurk,
unlurk: data.unlurk,
id,
}).code(200);
},
};
export default data;

View file

@ -0,0 +1,33 @@
import { ServerRoute } from "@hapi/hapi";
import boom from "@hapi/boom";
import { db } from "@/main";
import Joi from "joi";
const data: ServerRoute = {
method: `DELETE`, path: `/manage/{channel}/message/{id}`,
options: {
validate: {
params: Joi.object({
channel: Joi.string().alphanum(),
id: Joi.string().uuid(),
}),
},
},
async handler(request, h) {
const { channel, id } = request.params;
if (!db[channel]) {
throw boom.notFound(`Invalid channel`);
};
if (!db[channel].messages[id]) {
throw boom.notFound(`Invalid ID`);
};
let message = db[channel].messages[id];
delete db[channel].messages[id];
return h.response(message).code(200);
},
};
export default data;

View file

@ -0,0 +1,27 @@
import { ServerRoute } from "@hapi/hapi";
import boom from "@hapi/boom";
import { db } from "@/main";
const data: ServerRoute = {
method: `GET`, path: `/manage/{channel}`,
async handler(request, h) {
const { channel } = request.params;
if (!db[channel]) {
throw boom.notFound(`Invalid channel`);
};
let messages = [];
for (const messageId in db[channel].messages) {
let message = db[channel].messages[messageId];
messages.push({
id: messageId,
lurk: message.lurk,
unlurk: message.unlurk,
});
};
return h.response(messages).code(200);
},
};
export default data;

View file

@ -0,0 +1,27 @@
import { ServerRoute } from "@hapi/hapi";
import boom from "@hapi/boom";
import { db } from "@/main";
import Joi from "joi";
const data: ServerRoute = {
method: `DELETE`, path: `/manage/{channel}/lurkers`,
options: {
validate: {
params: Joi.object({
channel: Joi.string().alphanum(),
}),
},
},
async handler(request, h) {
const { channel } = request.params;
if (!db[channel]) {
throw boom.notFound(`Invalid channel`);
};
db[channel].lurkers = {};
return h.response().code(200);
},
};
export default data;

View file

@ -0,0 +1,34 @@
import { ServerRoute } from "@hapi/hapi";
import boom from "@hapi/boom";
import { db } from "@/main";
import { v4 } from "uuid";
import Joi from "joi";
const data: ServerRoute = {
method: `PATCH`, path: `/manage/{channel}/message/{id}`,
options: {
validate: {
payload: Joi.object({
lurk: Joi.array().items(Joi.string().min(1)).min(1),
unlurk: Joi.array().items(Joi.string().min(1)).min(1),
}),
params: Joi.object({
channel: Joi.string().alphanum(),
id: Joi.string().uuid(),
}),
},
},
async handler(request, h) {
const { channel, id } = request.params;
const data = request.payload as lurk_message;
if (!db[channel]) {
throw boom.notFound(`Invalid channel`);
};
db[channel].messages[id] = data;
return h.response(`Updated message set with ID: ${id}`).code(200);
},
};
export default data;

View file

@ -0,0 +1,46 @@
import { ServerRoute } from "@hapi/hapi";
import { formatMessage } from "@/utils";
import boom from "@hapi/boom";
import { db } from "@/main";
import Joi from "joi";
const data: ServerRoute = {
method: `GET`, path: `/{channel}/lurk`,
options: {
auth: false,
validate: {
params: Joi.object({
channel: Joi.string().alphanum(),
}),
query: Joi.object({
user: Joi.string(),
}),
},
},
async handler(request, h) {
const { channel } = request.params;
const { user } = request.query;
if (!db[channel]) {
throw boom.notFound(`Invalid channel`);
};
const messages = db[channel].messages;
const messageIds = Object.keys(messages);
const messageId = messageIds[Math.floor(Math.random() * messageIds.length)];
const message = messages[messageId];
let lurkMessage = message.lurk[Math.floor(Math.random() * message.lurk.length)];
db[channel].lurkers[user] = messageId;
let twitchMessage = formatMessage(
lurkMessage,
{
user,
}
);
return h.response(twitchMessage).code(200);
},
};
export default data;

View file

@ -0,0 +1,46 @@
import { ServerRoute } from "@hapi/hapi";
import { formatMessage } from "@/utils";
import boom from "@hapi/boom";
import { db } from "@/main";
import Joi from "joi";
const data: ServerRoute = {
method: `GET`, path: `/{channel}/unlurk`,
options: {
auth: false,
validate: {
params: Joi.object({
channel: Joi.string().alphanum(),
}),
query: Joi.object({
user: Joi.string(),
}),
},
},
async handler(request, h) {
const { channel } = request.params;
const { user } = request.query;
if (!db[channel]) {
throw boom.notFound(`Invalid channel`);
};
const messages = db[channel].messages;
const messageId = db[channel].lurkers[user];
const message = messages[messageId];
let lurkMessage = message.unlurk[Math.floor(Math.random() * message.unlurk.length)];
delete db[channel].lurkers[user];
let twitchMessage = formatMessage(
lurkMessage,
{
user,
channel,
}
);
return h.response(twitchMessage).code(200);
},
};
export default data;

95
src/main.ts Normal file
View file

@ -0,0 +1,95 @@
// Filepath alias resolution
import "module-alias/register";
// Begin personal code
import { Server, Request } from "@hapi/hapi";
import basic from "@hapi/basic";
import path from "path";
import glob from "glob";
import toml from "toml";
import fs from "fs";
const isDev = process.env.NODE_ENV?.startsWith(`dev`);
// load the config
if (!fs.existsSync(`config.toml`)) {
console.log(`Please fill out the config and then try starting the server again.`);
process.exit(1);
};
export const config: config = toml.parse(fs.readFileSync(`config.toml`, `utf-8`));
// Load the database
if (!fs.existsSync(`data/db.json`)) {
console.log(`Can't find database file, creating default`);
fs.writeFileSync(`data/db.json`, `{}`);
};
export var db: database = JSON.parse(fs.readFileSync(`data/db.json`, `utf-8`));
function saveDB() {
console.log(`Saving database`);
fs.writeFileSync(`data/db.json`, JSON.stringify(db, null, `\t`));
process.exit(0);
};
process.on(`SIGINT`, saveDB);
process.on(`SIGTERM`, saveDB);
process.on(`uncaughtException`, saveDB);
async function init() {
const server = new Server({
port: config.server.port,
routes: {
cors: {
origin: [
isDev ? `*` : `oliver.akins.me/Twitch-Lurk-Message-API/`,
],
credentials: true,
},
},
});
await server.register(basic);
server.auth.strategy(`basic`, `basic`, {
async validate(request: Request, username: string, password: string) {
let isValid = false;
let user: account|undefined;
for (const account of config.accounts) {
if (username == account.username) {
user = account;
isValid = (
password == account.password
&& (
request.params?.channel == null
|| account.access.includes(`*`)
|| account.access.includes(request.params.channel)
)
);
break;
};
};
return { isValid, credentials: { username, access: user?.access } };
},
});
server.auth.default(`basic`);
// Register all the routes
let files = glob.sync(
`endpoints/**/!(*.map)`,
{ cwd: __dirname, nodir: true}
);
for (var file of files) {
let route = (await import(path.join(__dirname, file))).default;
console.log(`Registering route: ${route.method} ${route.path}`);
server.route(route);
};
server.start().then(() => {
console.log(`Server listening on ${config.server.host}:${config.server.port}`);
});
};
init();

14
src/types/config.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
interface account {
username: string;
password: string;
access: string[];
}
interface config {
server: {
host: string;
port: number;
};
accounts: account[];
}

11
src/types/database.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
interface lurk_message {
lurk: string[];
unlurk: string[];
}
interface database {
[index: string]: {
lurkers: {[index: string]: string}
messages: {[index: string]: lurk_message}
};
}

4
src/utils.ts Normal file
View file

@ -0,0 +1,4 @@
export function formatMessage(message: string, meta: any): string {
return message
.replace(/\$\(user\)/, meta.user);
};

105
tsconfig.json Normal file
View file

@ -0,0 +1,105 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": {
"@/*": [ "./src/*" ]
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}