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

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);
};