mirror of
https://github.com/qwerinope/qweribot.git
synced 2025-12-18 21:51:38 +01:00
first commit, basic command handling and auth managing
This commit is contained in:
24
.example.env
Normal file
24
.example.env
Normal file
@@ -0,0 +1,24 @@
|
||||
# Settings that have been commented out are optional
|
||||
|
||||
# Application config
|
||||
CLIENT_ID= # Client_id gotten from the twitch dev console
|
||||
CLIENT_SECRET= # Client_secret gotten from the twitch dev console
|
||||
# REDIRECT_URL= # Redirect URL that has been set in the twitch dev console. Defaults to: http://localhost:3456
|
||||
# REDIRECT_PORT= # Redirect port if the REDIRECT_URL has not been set. Defaults to 3456. This is also the port the bot will listen on to authenticate
|
||||
# COMMAND_PREFIX= # The prefix which will be used to activate commands. Defaults to '!'. When requiring a space between prefix and command, escape the space with a backslash
|
||||
|
||||
# The Twitch IDs required below can be gotten from this website: https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
|
||||
|
||||
# Streamer config
|
||||
STREAMER_ID= # Twitch ID of the streaming user
|
||||
|
||||
# Chatter config
|
||||
CHATTER_ID= # Twitch ID of the chatting user
|
||||
CHATTER_IS_STREAMER= # If the bot that activates on commands is on the same account as the streamer, set this to true. Make sure the STREAMER_ID and CHATTER_ID match in that case.
|
||||
|
||||
# SurrealDB config
|
||||
SURREAL_URL= # SurrealDB URL, can either be remotely hosted or selfhosted
|
||||
SURREAL_NAMESPACE= # SurrealDB Namespace. You need to create this manually
|
||||
SURREAL_DATABASE= # SurrealDB Database. You need to create this manually
|
||||
SURREAL_USERNAME= # SurrealDB username for authenticating
|
||||
SURREAL_PASSWORD= # SurrealDB password for authenticating. Remember to escape characters like $ with a backslash
|
||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
README.md
Normal file
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# qweribot
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.13. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
84
bot/auth.ts
Normal file
84
bot/auth.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { RefreshingAuthProvider, exchangeCode, type AccessToken } from "@twurple/auth";
|
||||
import { createAuthRecord, deleteAuthRecord, getAuthRecord, updateAuthRecord } from "./db/dbAuth";
|
||||
|
||||
async function initAuth(userId: string, clientId: string, clientSecret: string, requestedIntents: string[], streamer: boolean): Promise<AccessToken> {
|
||||
const port = process.env.REDIRECT_PORT ?? 3456
|
||||
const redirectURL = process.env.REDIRECT_URL ?? `http://localhost:${port}`;
|
||||
// Set the default url and port to http://localhost:3456
|
||||
|
||||
const state = Bun.randomUUIDv7().replace(/-/g, "").slice(0, 32);
|
||||
// Generate random state variable to prevent cross-site-scripting attacks
|
||||
|
||||
const instruction = `Visit this URL as ${streamer ? 'the streamer' : 'the chatter'} to authenticate the bot.`
|
||||
console.info(instruction);
|
||||
console.info(`https://id.twitch.tv/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectURL}&response_type=code&scope=${requestedIntents.join('+')}&state=${state}`);
|
||||
|
||||
const createCodePromise = () => {
|
||||
let resolver: (code: string) => void;
|
||||
const promise = new Promise<string>((resolve) => { resolver = resolve; });
|
||||
return { promise, resolver: resolver! };
|
||||
}
|
||||
|
||||
const { promise: codepromise, resolver } = createCodePromise();
|
||||
|
||||
const server = Bun.serve({
|
||||
port,
|
||||
fetch(request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
if (searchParams.has('code') && searchParams.has('state') && searchParams.get('state') === state) {
|
||||
// Check if the required fields exist on the params and validate the state
|
||||
resolver(searchParams.get('code')!);
|
||||
return new Response("Successfully authenticated!");
|
||||
} else {
|
||||
return new Response(`Authentication attempt unsuccessful, please make sure the redirect url in the twitch developer console is set to ${redirectURL} and that the bot is listening to that url & port.`, { status: 400 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await deleteAuthRecord(userId);
|
||||
|
||||
const code = await codepromise;
|
||||
await server.stop(false);
|
||||
console.info(`Authentication code received.`);
|
||||
const tokenData = await exchangeCode(clientId, clientSecret, code, redirectURL);
|
||||
console.info(`Successfully authenticated code.`);
|
||||
await createAuthRecord(tokenData, userId);
|
||||
console.info(`Successfully saved auth data in the database.`)
|
||||
return tokenData;
|
||||
};
|
||||
|
||||
export async function createAuthProvider(user: string, intents: string[], streamer = false): Promise<RefreshingAuthProvider> {
|
||||
const clientId = process.env.CLIENT_ID;
|
||||
if (!clientId) { console.error("Please provide a CLIENT_ID in .env."); process.exit(1); };
|
||||
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
if (!clientSecret) { console.error("Please provide a CLIENT_SECRET in .env."); process.exit(1); };
|
||||
|
||||
const authRecord = await getAuthRecord(user, intents);
|
||||
|
||||
const token = authRecord ? authRecord.accesstoken : await initAuth(user, clientId, clientSecret, intents, streamer);
|
||||
|
||||
const authData = new RefreshingAuthProvider({
|
||||
clientId,
|
||||
clientSecret
|
||||
});
|
||||
await authData.addUserForToken(token, intents);
|
||||
|
||||
authData.onRefresh(async (user, token) => {
|
||||
console.info(`Successfully refreshed auth for user ${user}`);
|
||||
await updateAuthRecord(user, token.scope, token);
|
||||
});
|
||||
|
||||
authData.onRefreshFailure((user, err) => {
|
||||
console.error(`ERROR: Failed to refresh auth for user ${user}: ${err.name} ${err.message}`);
|
||||
});
|
||||
|
||||
try {
|
||||
await authData.refreshAccessTokenForUser(user);
|
||||
} catch (err) {
|
||||
console.error(`Failed to refresh user ${user}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`);
|
||||
await deleteAuthRecord(user);
|
||||
};
|
||||
|
||||
return authData;
|
||||
};
|
||||
42
bot/commands/index.ts
Normal file
42
bot/commands/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
|
||||
import { type HelixSendChatMessageParams } from "@twurple/api";
|
||||
import { User } from "../user";
|
||||
|
||||
/** The Command class represents a command */
|
||||
export class Command {
|
||||
public readonly name: string;
|
||||
public readonly aliases: string[];
|
||||
public readonly requiredIntents: string[];
|
||||
public readonly execute: (message: EventSubChannelChatMessageEvent, sender: User) => Promise<void>;
|
||||
constructor(name: string, aliases: string[], requiredIntents: string[], execution: (message: EventSubChannelChatMessageEvent, sender: User) => Promise<void>) {
|
||||
this.name = name;
|
||||
this.aliases = aliases;
|
||||
this.requiredIntents = requiredIntents;
|
||||
this.execute = execution;
|
||||
};
|
||||
};
|
||||
|
||||
import { readdir } from 'node:fs/promises';
|
||||
const commands = new Map<string, Command>;
|
||||
const intents: string[] = [];
|
||||
|
||||
const files = await readdir(import.meta.dir);
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.ts')) continue;
|
||||
if (file === import.meta.file) continue;
|
||||
const command: Command = await import(import.meta.dir + '/' + file.slice(0, -3)).then(a => a.default);
|
||||
intents.push(...command.requiredIntents);
|
||||
for (const alias of command.aliases) {
|
||||
commands.set(alias, command); // Since it's not a primitive type the map is filled with references to the command, not the actual object
|
||||
};
|
||||
};
|
||||
|
||||
export default commands;
|
||||
export { intents };
|
||||
|
||||
import { singleUserMode, chatterApi, chatterId, streamerId } from "..";
|
||||
|
||||
/** Helper function to send a message to the stream */
|
||||
export const sendMessage = async (message: string, options?: HelixSendChatMessageParams) => {
|
||||
singleUserMode ? await chatterApi.chat.sendChatMessage(streamerId, message, options) : chatterApi.asUser(chatterId, async newapi => newapi.chat.sendChatMessage(streamerId, message, options));
|
||||
};
|
||||
10
bot/commands/ping.ts
Normal file
10
bot/commands/ping.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Command, sendMessage } from ".";
|
||||
|
||||
// This command is purely for testing
|
||||
export default new Command('ping',
|
||||
['ping'],
|
||||
[],
|
||||
async msg => {
|
||||
await sendMessage('pong!', { replyParentMessageId: msg.messageId });
|
||||
}
|
||||
);
|
||||
17
bot/commands/yabai.ts
Normal file
17
bot/commands/yabai.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Command, sendMessage } from ".";
|
||||
import { type HelixSendChatMessageParams } from "@twurple/api";
|
||||
|
||||
// Remake of the !yabai command in ttv/kiara_tv
|
||||
export default new Command('yabai',
|
||||
['yabai', 'goon'],
|
||||
[],
|
||||
async msg => {
|
||||
const replyobj: HelixSendChatMessageParams = { replyParentMessageId: msg.messageId }
|
||||
const rand = Math.floor(Math.random() * 100) + 1;
|
||||
if (rand < 25) await sendMessage(`${rand}% yabai! chumpi4Bewwy`, replyobj);
|
||||
else if (rand < 50) await sendMessage(`${rand}% yabai chumpi4Hustle`, replyobj);
|
||||
else if (rand === 50) await sendMessage(`${rand}% yabai kiawaBlank`, replyobj);
|
||||
else if (rand < 80) await sendMessage(`${rand}% yabai chumpi4Shock`, replyobj);
|
||||
else await sendMessage(`${rand}% yabai chumpi4Jail`, replyobj);
|
||||
}
|
||||
);
|
||||
31
bot/db/connection.ts
Normal file
31
bot/db/connection.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import Surreal from "surrealdb";
|
||||
|
||||
const surrealurl = process.env.SURREAL_URL ?? "";
|
||||
if (surrealurl === "") { console.error("Please provide a SURREAL_URL in .env."); process.exit(1); };
|
||||
const namespace = process.env.SURREAL_NAMESPACE ?? "";
|
||||
if (namespace === "") { console.error("Please provide a SURREAL_NAMESPACE in .env."); process.exit(1); };
|
||||
const database = process.env.SURREAL_DATABASE ?? "";
|
||||
if (database === "") { console.error("Please provide a SURREAL_DATABASE in .env."); process.exit(1); };
|
||||
const username = process.env.SURREAL_USERNAME ?? "";
|
||||
if (username === "") { console.error("Please provide a SURREAL_USERNAME in .env."); process.exit(1); };
|
||||
const password = process.env.SURREAL_PASSWORD ?? "";
|
||||
if (password === "") { console.error("Please provide a SURREAL_PASSWORD in .env."); process.exit(1); };
|
||||
|
||||
export default async function DB(): Promise<Surreal> {
|
||||
const db = new Surreal();
|
||||
try {
|
||||
await db.connect(surrealurl, {
|
||||
auth: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
});
|
||||
await db.use({ namespace, database });
|
||||
return db;
|
||||
}
|
||||
catch (err) {
|
||||
console.error("Failed to connect to SurrealDB:", err instanceof Error ? err.message : String(err));
|
||||
await db.close();
|
||||
throw err;
|
||||
};
|
||||
};
|
||||
78
bot/db/dbAuth.ts
Normal file
78
bot/db/dbAuth.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { AccessToken } from "@twurple/auth";
|
||||
import DB from "./connection";
|
||||
import type { RecordId } from "surrealdb";
|
||||
|
||||
type authRecord = {
|
||||
accesstoken: AccessToken,
|
||||
user: string,
|
||||
};
|
||||
|
||||
export async function createAuthRecord(token: AccessToken, userId: string): Promise<void> {
|
||||
const db = await DB();
|
||||
if (!db) return;
|
||||
|
||||
const data: authRecord = {
|
||||
accesstoken: token,
|
||||
user: userId
|
||||
};
|
||||
|
||||
try {
|
||||
await db.create("auth", data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
await db.close();
|
||||
};
|
||||
};
|
||||
|
||||
type getAuthRecordQuery = authRecord & { id: RecordId };
|
||||
type getAuthRecordResult = { accesstoken: AccessToken, id: RecordId };
|
||||
|
||||
export async function getAuthRecord(userId: string, requiredIntents: string[]): Promise<getAuthRecordResult | undefined> {
|
||||
const db = await DB();
|
||||
if (!db) return undefined;
|
||||
try {
|
||||
const data = await db.query<getAuthRecordQuery[][]>("SELECT * from auth WHERE user=$userId AND accesstoken.scope CONTAINSALL $intents;", { userId, intents: requiredIntents });
|
||||
if (!data[0] || !data[0][0]) return undefined;
|
||||
return { accesstoken: data[0][0].accesstoken, id: data[0][0].id };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return undefined;
|
||||
} finally {
|
||||
await db.close();
|
||||
};
|
||||
};
|
||||
|
||||
export async function updateAuthRecord(userId: string, intents: string[], newtoken: AccessToken): Promise<boolean> {
|
||||
const db = await DB();
|
||||
if (!db) return false;
|
||||
try {
|
||||
const data = await getAuthRecord(userId, intents);
|
||||
const newrecord: authRecord = {
|
||||
accesstoken: newtoken,
|
||||
user: userId
|
||||
};
|
||||
await db.update(data?.id!, newrecord);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return false;
|
||||
} finally {
|
||||
await db.close();
|
||||
};
|
||||
};
|
||||
|
||||
export async function deleteAuthRecord(userId: string): Promise<void> {
|
||||
const db = await DB();
|
||||
if (!db) return;
|
||||
try {
|
||||
const data = await db.query<getAuthRecordQuery[][]>("SELECT * FROM auth WHERE user=$userId;", { userId });
|
||||
if (!data[0] || !data[0][0]) return undefined;
|
||||
console.log(data)
|
||||
for (const obj of data[0]) {
|
||||
db.delete(obj.id);
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
};
|
||||
};
|
||||
27
bot/events/index.ts
Normal file
27
bot/events/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { eventSub } from "..";
|
||||
|
||||
eventSub.onSubscriptionCreateSuccess(event => {
|
||||
console.info(`Successfully created EventSub subscription: ${event.id}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionCreateFailure(event => {
|
||||
eventSub.stop()
|
||||
console.error(`Failed to create EventSub subscription: ${event.id}`);
|
||||
eventSub.start()
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionDeleteSuccess(event => {
|
||||
console.info(`Successfully deleted EventSub subscription: ${event.id}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionDeleteFailure(event => {
|
||||
console.error(`Failed to delete EventSub subscription: ${event.id}`);
|
||||
});
|
||||
|
||||
import { readdir } from 'node:fs/promises';
|
||||
const files = await readdir(import.meta.dir);
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.ts')) continue;
|
||||
if (file === import.meta.file) continue;
|
||||
await import(import.meta.dir + '/' + file.slice(0, -3));
|
||||
};
|
||||
27
bot/events/message.ts
Normal file
27
bot/events/message.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { redis } from "bun";
|
||||
import { chatterId, streamerId, eventSub, commandPrefix } from "..";
|
||||
import { User } from "../user";
|
||||
import commands, { sendMessage } from "../commands";
|
||||
|
||||
console.info(`Loaded the ${commands.keys().toArray().join(', ')} commands`);
|
||||
|
||||
eventSub.onChannelChatMessage(streamerId, streamerId, async msg => {
|
||||
// Get user from cache or place user in cache
|
||||
const user = await User.init(msg.chatterName);
|
||||
|
||||
// Manage vulnerable chatters
|
||||
if (![chatterId, streamerId].includes(msg.chatterId)) {// Don't add the chatter or streamer to the vulnerable chatters
|
||||
if (!await redis.sismember("vulnchatters", msg.chatterId)) {
|
||||
await redis.sadd('vulnchatters', msg.chatterId);
|
||||
console.debug(`${msg.chatterDisplayName} is now vulnerable to explosives.`);
|
||||
};
|
||||
};
|
||||
|
||||
// Parse commands:
|
||||
if (msg.messageText.startsWith(commandPrefix)) {
|
||||
const commandSelection = msg.messageText.slice(commandPrefix.length).split(' ')[0]!;
|
||||
const selected = commands.get(commandSelection.toLowerCase());
|
||||
if (!selected) { await sendMessage(`${commandSelection} command does not exist`, { replyParentMessageId: msg.messageId }); return; };
|
||||
await selected.execute(msg, user!);
|
||||
};
|
||||
});
|
||||
34
bot/index.ts
Normal file
34
bot/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createAuthProvider } from "./auth";
|
||||
import { ApiClient } from "@twurple/api";
|
||||
import { EventSubWsListener } from "@twurple/eventsub-ws";
|
||||
import { intents } from "./commands";
|
||||
|
||||
const CHATTERBASEINTENTS = ["user:read:chat", "user:write:chat", "user:bot"];
|
||||
const STREAMERBASEINTENTS = ["user:read:chat"];
|
||||
|
||||
export const singleUserMode = process.env.CHATTER_IS_STREAMER === 'true';
|
||||
export const chatterId = process.env.CHATTER_ID ?? "";
|
||||
if (chatterId === "") { console.error('Please set a CHATTER_ID in the .env'); process.exit(1); };
|
||||
export const streamerId = process.env.STREAMER_ID ?? "";
|
||||
if (streamerId === "") { console.log('Please set a STREAMER_ID in the .env'); process.exit(1); };
|
||||
|
||||
const chatterIntents = singleUserMode ? CHATTERBASEINTENTS.concat(STREAMERBASEINTENTS) : CHATTERBASEINTENTS;
|
||||
const streamerIntents = STREAMERBASEINTENTS.concat(intents);
|
||||
|
||||
export const chatterAuthProvider = await createAuthProvider(chatterId, chatterIntents);
|
||||
export const streamerAuthProvider = singleUserMode ? undefined : await createAuthProvider(streamerId, streamerIntents, true);
|
||||
|
||||
/** chatterApi should be used for sending messages, retrieving user data, etc */
|
||||
export const chatterApi = new ApiClient({ authProvider: chatterAuthProvider });
|
||||
|
||||
/** streamerApi should be used for: adding/removing mods, managing timeouts, etc. */
|
||||
export const streamerApi = streamerAuthProvider ? new ApiClient({ authProvider: streamerAuthProvider }) : chatterApi; // if there is no streamer user, use the chatter user
|
||||
|
||||
/** As the streamerApi has either the streamer or the chatter if the chatter IS the streamer this has streamer permissions */
|
||||
export const eventSub = new EventSubWsListener({ apiClient: streamerApi });
|
||||
|
||||
export const commandPrefix = process.env.COMMAND_PREFIX ?? "!";
|
||||
|
||||
await import("./events");
|
||||
|
||||
eventSub.start();
|
||||
45
bot/user.ts
Normal file
45
bot/user.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { redis } from "bun";
|
||||
import { chatterApi } from ".";
|
||||
|
||||
export class User {
|
||||
constructor(username: string) {
|
||||
this.username = username;
|
||||
};
|
||||
|
||||
public username: string;
|
||||
public id: string | undefined | null; // The null here and below serves the function to make typescript shut the fuck up
|
||||
public displayName: string | undefined | null;
|
||||
|
||||
static async init(username: string): Promise<User | null> {
|
||||
const userObj = new User(username);
|
||||
const cachedata = await redis.exists(`user:${username}`);
|
||||
if (!cachedata) {
|
||||
const twitchdata = await chatterApi.users.getUserByName(username!);
|
||||
if (!twitchdata) return null; // User does not exist in redis and twitch api can't get them: return null
|
||||
await redis.set(`user:${username}:id`, twitchdata.id);
|
||||
await redis.set(`user:${username}:displayName`, twitchdata.displayName);
|
||||
await redis.set(`user:${username}:itemlock`, '0');
|
||||
};
|
||||
await userObj._setOptions();
|
||||
return userObj;
|
||||
};
|
||||
|
||||
private async _setOptions(): Promise<void> {
|
||||
this.id = await redis.get(`user:${this.username}:id`);
|
||||
this.displayName = await redis.get(`user:${this.username}:displayName`);
|
||||
};
|
||||
|
||||
public async itemLock(): Promise<boolean> {
|
||||
const lock = await redis.get(`user:${this.username}:itemlock`);
|
||||
if (lock === '0') return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
public async setLock(): Promise<void> {
|
||||
await redis.set(`user:${this.username}:itemlock`, '1');
|
||||
};
|
||||
|
||||
public async clearLock(): Promise<void> {
|
||||
await redis.set(`user:${this.username}:itemlock`, '0');
|
||||
};
|
||||
};
|
||||
89
bun.lock
Normal file
89
bun.lock
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "qweribot",
|
||||
"dependencies": {
|
||||
"@twurple/auth": "^7.3.0",
|
||||
"@twurple/eventsub-ws": "^7.3.0",
|
||||
"pocketbase": "^0.26.0",
|
||||
"surrealdb": "^1.3.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@d-fischer/cache-decorators": ["@d-fischer/cache-decorators@4.0.1", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.3", "tslib": "^2.6.2" } }, "sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA=="],
|
||||
|
||||
"@d-fischer/connection": ["@d-fischer/connection@9.0.0", "", { "dependencies": { "@d-fischer/isomorphic-ws": "^7.0.0", "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.5.0", "@d-fischer/typed-event-emitter": "^3.3.0", "@types/ws": "^8.5.4", "tslib": "^2.4.1", "ws": "^8.11.0" } }, "sha512-Mljp/EbaE+eYWfsFXUOk+RfpbHgrWGL/60JkAvjYixw6KREfi5r17XdUiXe54ByAQox6jwgdN2vebdmW1BT+nQ=="],
|
||||
|
||||
"@d-fischer/cross-fetch": ["@d-fischer/cross-fetch@5.0.5", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-symjDUPInTrkfIsZc2n2mo9hiAJLcTJsZkNICjZajEWnWpJ3s3zn50/FY8xpNUAf5w3eFuQii2wxztTGpvG1Xg=="],
|
||||
|
||||
"@d-fischer/detect-node": ["@d-fischer/detect-node@3.0.1", "", {}, "sha512-0Rf3XwTzuTh8+oPZW9SfxTIiL+26RRJ0BRPwj5oVjZFyFKmsj9RGfN2zuTRjOuA3FCK/jYm06HOhwNK+8Pfv8w=="],
|
||||
|
||||
"@d-fischer/isomorphic-ws": ["@d-fischer/isomorphic-ws@7.0.2", "", { "peerDependencies": { "ws": "^8.2.0" } }, "sha512-xK+qIJUF0ne3dsjq5Y3BviQ4M+gx9dzkN+dPP7abBMje4YRfow+X9jBgeEoTe5e+Q6+8hI9R0b37Okkk8Vf0hQ=="],
|
||||
|
||||
"@d-fischer/logger": ["@d-fischer/logger@4.2.3", "", { "dependencies": { "@d-fischer/detect-node": "^3.0.1", "@d-fischer/shared-utils": "^3.2.0", "tslib": "^2.0.3" } }, "sha512-mJUx9OgjrNVLQa4od/+bqnmD164VTCKnK5B4WOW8TX5y/3w2i58p+PMRE45gUuFjk2BVtOZUg55JQM3d619fdw=="],
|
||||
|
||||
"@d-fischer/qs": ["@d-fischer/qs@7.0.2", "", {}, "sha512-yAu3xDooiL+ef84Jo8nLjDjWBRk7RXk163Y6aTvRB7FauYd3spQD/dWvgT7R4CrN54Juhrrc3dMY7mc+jZGurQ=="],
|
||||
|
||||
"@d-fischer/rate-limiter": ["@d-fischer/rate-limiter@1.1.0", "", { "dependencies": { "@d-fischer/logger": "^4.2.3", "@d-fischer/shared-utils": "^3.6.3", "tslib": "^2.6.2" } }, "sha512-O5HgACwApyCZhp4JTEBEtbv/W3eAwEkrARFvgWnEsDmXgCMWjIHwohWoHre5BW6IYXFSHBGsuZB/EvNL3942kQ=="],
|
||||
|
||||
"@d-fischer/shared-utils": ["@d-fischer/shared-utils@3.6.4", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-BPkVLHfn2Lbyo/ENDBwtEB8JVQ+9OzkjJhUunLaxkw4k59YFlQxUUwlDBejVSFcpQT0t+D3CQlX+ySZnQj0wxw=="],
|
||||
|
||||
"@d-fischer/typed-event-emitter": ["@d-fischer/typed-event-emitter@3.3.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-OvSEOa8icfdWDqcRtjSEZtgJTFOFNgTjje7zaL0+nAtu2/kZtRCSK5wUMrI/aXtCH8o0Qz2vA8UqkhWUTARFQQ=="],
|
||||
|
||||
"@twurple/api": ["@twurple/api@7.3.0", "", { "dependencies": { "@d-fischer/cache-decorators": "^4.0.0", "@d-fischer/cross-fetch": "^5.0.1", "@d-fischer/detect-node": "^3.0.1", "@d-fischer/logger": "^4.2.1", "@d-fischer/rate-limiter": "^1.1.0", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.1", "@twurple/api-call": "7.3.0", "@twurple/common": "7.3.0", "retry": "^0.13.1", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/auth": "7.3.0" } }, "sha512-QtaVgYi50E3AB/Nxivjou/u6w1cuQ6g4R8lzQawYDaQNtlP2Ue8vvYuSp2PfxSpe8vNiKhgV8hZAs+j4V29sxQ=="],
|
||||
|
||||
"@twurple/api-call": ["@twurple/api-call@7.3.0", "", { "dependencies": { "@d-fischer/cross-fetch": "^5.0.1", "@d-fischer/qs": "^7.0.2", "@d-fischer/shared-utils": "^3.6.1", "@twurple/common": "7.3.0", "tslib": "^2.0.3" } }, "sha512-nx389kXjVphAeR3RfnzkRRf7Qa45wqHla067/mr3YxnUICCg4YOFv0Jb5UohQGHkj5h18mDZ3iUu/x2J49c1lA=="],
|
||||
|
||||
"@twurple/auth": ["@twurple/auth@7.3.0", "", { "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.1", "@twurple/api-call": "7.3.0", "@twurple/common": "7.3.0", "tslib": "^2.0.3" } }, "sha512-K68nFbQswfaEVCWP2MEPcxhHRR/N8kIHBP6AnRXzgSpmvWxhjOitz9oyP04di5DI1rJE+2NRauv1qFDyYia/qg=="],
|
||||
|
||||
"@twurple/common": ["@twurple/common@7.3.0", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", "tslib": "^2.0.3" } }, "sha512-BGNniY7PBIohxfpRQ1bsOxUaktZcXZOExq8ojCtnsNBVDlchNEX2fYsere03ZwTLd48XBtxsdaUaeQXbx1aXLw=="],
|
||||
|
||||
"@twurple/eventsub-base": ["@twurple/eventsub-base@7.3.0", "", { "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", "@twurple/api": "7.3.0", "@twurple/auth": "7.3.0", "@twurple/common": "7.3.0", "tslib": "^2.0.3" } }, "sha512-Wc/3qpyFfyvjabk/tQJVjAke+ixp5QWUT7LsuU+kMcCf46jsRQMB3InoXsZMRgX5sD1frBZzxUEJ7ujhxb8Ngw=="],
|
||||
|
||||
"@twurple/eventsub-ws": ["@twurple/eventsub-ws@7.3.0", "", { "dependencies": { "@d-fischer/connection": "^9.0.0", "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", "@twurple/auth": "7.3.0", "@twurple/common": "7.3.0", "@twurple/eventsub-base": "7.3.0", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/api": "7.3.0" } }, "sha512-jtIMdW/atTrn5Eo3XGS8Lw0EIsK3GQsZGJDLYRwqw2bCV8ZnQoZ8YaXUJb5Wd+gebUfeBr4j7mvZlGc+Wkp17w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
|
||||
|
||||
"isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="],
|
||||
|
||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.26.0", "", {}, "sha512-WBBeOgz4Jnrd7a1KEzSBUJqpTortKKCcp16j5KoF+4tNIyQHsmynj+qRSvS56/RVacVMbAqO8Qkfj3N84fpzEw=="],
|
||||
|
||||
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||
|
||||
"surrealdb": ["surrealdb@1.3.2", "", { "dependencies": { "isows": "^1.0.6", "uuidv7": "^1.0.1" }, "peerDependencies": { "tslib": "^2.6.3", "typescript": "^5.0.0" } }, "sha512-mL7nij33iuon3IQP72F46fgX3p2LAxFCWCBDbZB7IohZ13RTEwJVNq7nZeP1eMSceQUpKzS6OHIWOuF9LYAkNw=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"uuidv7": ["uuidv7@1.0.2", "", { "bin": { "uuidv7": "cli.js" } }, "sha512-8JQkH4ooXnm1JCIhqTMbtmdnYEn6oKukBxHn1Ic9878jMkL7daTI7anTExfY18VRCX7tcdn5quzvCb6EWrR8PA=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
|
||||
}
|
||||
}
|
||||
7
compose.yml
Normal file
7
compose.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
valkey:
|
||||
image: valkey/valkey:alpine
|
||||
container_name: valkey
|
||||
ports:
|
||||
- 6379:6379
|
||||
restart: no
|
||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "qweribot",
|
||||
"module": "bot/index.ts",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@twurple/auth": "^7.3.0",
|
||||
"@twurple/eventsub-ws": "^7.3.0",
|
||||
"pocketbase": "^0.26.0",
|
||||
"surrealdb": "^1.3.2"
|
||||
}
|
||||
}
|
||||
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user