diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10578122..271c11c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,8 @@ repos: - id: next-lint-website name: Lint website files: ^website/ + exclude: ^website/node_modules/ types_or: [javascript, jsx, ts, tsx] - language: system + language: node pass_filenames: false - entry: bash -c "cd website && npm install && npm run lint" + entry: website/next-lint.js diff --git a/discord-bot/bot/bot.py b/discord-bot/bot/bot.py index a305946f..b2a2eb25 100644 --- a/discord-bot/bot/bot.py +++ b/discord-bot/bot/bot.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- """Bot logic.""" +from datetime import datetime + import aiosqlite import hikari import lightbulb import miru from bot.api_client import OasstApiClient from bot.settings import Settings +from bot.utils import EMPTY, mention settings = Settings() @@ -38,3 +41,77 @@ async def on_stopping(event: hikari.StoppingEvent): """Cleanup.""" await bot.d.db.close() await bot.d.oasst_api.close() + + +async def _send_error_embed( + content: str, exception: lightbulb.errors.LightbulbError | BaseException, ctx: lightbulb.Context +) -> None: + ctx.command + embed = hikari.Embed( + title=f"`{exception.__class__.__name__}` Error{f' in `{ctx.command.name}`' if ctx.command else '' }", + description=content, + color=0xFF0000, + timestamp=datetime.now().astimezone(), + ).set_author(name=ctx.author.username, url=str(ctx.author.avatar_url)) + + await ctx.respond(EMPTY, embed=embed) + + +@bot.listen(lightbulb.CommandErrorEvent) +async def on_error(event: lightbulb.CommandErrorEvent) -> None: + """Error handler for the bot.""" + # Unwrap the exception to get the original cause + exc = event.exception.__cause__ or event.exception + ctx = event.context + + if isinstance(event.exception, lightbulb.CommandInvocationError): + if not event.context.command: + await _send_error_embed("Something went wrong", exc, ctx) + else: + await _send_error_embed( + f"Something went wrong during invocation of command `{event.context.command.name}`.", exc, ctx + ) + + raise event.exception + + # Not an owner + if isinstance(exc, lightbulb.NotOwner): + await _send_error_embed("You are not the owner of this bot.", exc, ctx) + # Command is on cooldown + elif isinstance(exc, lightbulb.CommandIsOnCooldown): + await _send_error_embed(f"This command is on cooldown. Retry in `{exc.retry_after:.2f}` seconds.", exc, ctx) + # Missing permissions + elif isinstance(exc, lightbulb.errors.MissingRequiredPermission): + await _send_error_embed( + f"You do not have permission to use this command. Missing permissions: {exc.missing_perms}", exc, ctx + ) + # Missing roles + elif isinstance(exc, lightbulb.errors.MissingRequiredRole): + assert event.context.guild_id is not None # Roles only exist in guilds + await _send_error_embed( + f"You do not have the correct role to use this command. Missing role(s): {[mention(r, 'role') for r in exc.missing_roles]}", + exc, + ctx, + ) + # Only a guild command + elif isinstance(exc, lightbulb.errors.OnlyInGuild): + await _send_error_embed("This command can only be run in servers.", exc, ctx) + # Only a DM command + elif isinstance(exc, lightbulb.errors.OnlyInDM): + await _send_error_embed("This command can only be run in DMs.", exc, ctx) + # Not enough arguments + elif isinstance(exc, lightbulb.errors.NotEnoughArguments): + await _send_error_embed( + f"Not enough arguments were supplied to the command. {[opt.name for opt in exc.missing_options]}", exc, ctx + ) + # Bot missing permission + elif isinstance(exc, lightbulb.errors.BotMissingRequiredPermission): + await _send_error_embed( + f"The bot does not have the correct permission(s) to execute this command. Missing permissions: {exc.missing_perms}", + exc, + ctx, + ) + elif isinstance(exc, lightbulb.errors.MissingRequiredAttachment): + await _send_error_embed("Not enough attachemnts were supplied to this command.", exc, ctx) + else: + raise exc diff --git a/discord-bot/bot/extensions/guild_settings.py b/discord-bot/bot/extensions/guild_settings.py index 1aba9f47..d09407da 100644 --- a/discord-bot/bot/extensions/guild_settings.py +++ b/discord-bot/bot/extensions/guild_settings.py @@ -5,7 +5,7 @@ import lightbulb from aiosqlite import Connection from bot.db.schemas import GuildSettings from bot.utils import mention -from lightbulb.utils.permissions import permissions_in +from lightbulb.utils import permissions_in from loguru import logger plugin = lightbulb.Plugin("GuildSettings") @@ -56,32 +56,42 @@ if guild_settings.log_channel_id else 'not set'} @settings.child @lightbulb.option("channel", "The channel to use.", hikari.TextableGuildChannel) -@lightbulb.command("log_channel", "Set the channel that the bot logs task and label completions in.") +@lightbulb.command("log_channel", "Set the channel that the bot logs task and label completions in.", ephemeral=True) @lightbulb.implements(lightbulb.SlashSubCommand) async def log_channel(ctx: lightbulb.SlashContext) -> None: """Set the channel that the bot logs task and label completions in.""" channel: hikari.TextableGuildChannel = ctx.options.channel conn: Connection = ctx.bot.d.db assert ctx.guild_id is not None # `guild_only` check - assert isinstance(channel, hikari.PermissibleGuildChannel) # Check if the bot can send messages in that channel - assert (me := ctx.bot.get_me()) is not None # non-None after `StartedEvent` - if (own_member := ctx.bot.cache.get_member(ctx.guild_id, me.id)) is None: - own_member = await ctx.bot.rest.fetch_member(ctx.guild_id, me.id) - perms = permissions_in(channel, own_member) - if perms & ~hikari.Permissions.SEND_MESSAGES: - await ctx.respond("I don't have permission to send messages in that channel.") + assert isinstance(channel, hikari.InteractionChannel) # Slash commands are interactions + me = ctx.bot.cache.get_me() or await ctx.bot.rest.fetch_my_user() + own_member = ctx.bot.cache.get_member(ctx.guild_id, me.id) or await ctx.bot.rest.fetch_member(ctx.guild_id, me.id) + + # Get the channel from the cache if it is there, otherwise fetch it + if (ch := ctx.bot.cache.get_guild_channel(channel.id)) is None: + ch = {ch.id: ch for ch in await ctx.bot.rest.fetch_guild_channels(channel.id)}[channel.id] + + if not isinstance(ch, hikari.GuildTextChannel): + await ctx.respond(f"{ch.mention} is not a text channel.") + return + + # if the bot's permissions for this channel don't contain SEND_MESSAGE + # This will also filter out categories and voice channels + print(permissions_in(ch, own_member) & hikari.Permissions.SEND_MESSAGES) + if not permissions_in(ch, own_member) & hikari.Permissions.SEND_MESSAGES: + await ctx.respond(f"I don't have permission to send messages in {ch.mention}.") return await ctx.respond(f"Setting `log_channel` to {channel.mention}.") + # update the database async with conn.cursor() as cursor: await cursor.execute( "INSERT OR REPLACE INTO guild_settings (guild_id, log_channel_id) VALUES (?, ?)", (ctx.guild_id, channel.id), ) - await conn.commit() logger.info(f"Updated `log_channel` for {ctx.guild_id} to {channel.id}.") diff --git a/website/.eslintrc.json b/website/.eslintrc.json index 95127c06..04b5d542 100644 --- a/website/.eslintrc.json +++ b/website/.eslintrc.json @@ -6,6 +6,9 @@ "next/core-web-vitals" ], "rules": { - "sort-imports": "warn" - } + "unused-imports/no-unused-imports": "warn", + "simple-import-sort/imports": "warn", + "simple-import-sort/exports": "warn" + }, + "plugins": ["simple-import-sort", "unused-imports"] } diff --git a/website/next-lint.js b/website/next-lint.js new file mode 100755 index 00000000..0b3a5c90 --- /dev/null +++ b/website/next-lint.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node +const { spawnSync } = require("child_process"); +async function npmLint() { + const spawnOption = { + shell: true, + env: process.env, + stdio: "inherit", + cwd: "./website", + }; + let npmInstall; + let npmRunLint; + try { + npmInstall = await spawnSync("npm", ["install"], spawnOption); + if (npmInstall.status !== 0) { + process.exit(npmInstall.status); + } + npmRunLint = await spawnSync("npm", ["run lint"], spawnOption); + process.exit(npmRunLint.status); + } catch (error) { + console.error(error); + process.exit(1); + } +} +npmLint(); diff --git a/website/package-lock.json b/website/package-lock.json index 6edb9a1a..1587c01e 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@chakra-ui/react": "^2.4.4", "@dnd-kit/core": "^6.0.6", + "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.1", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", @@ -25,6 +26,7 @@ "clsx": "^1.2.1", "eslint": "8.29.0", "eslint-config-next": "13.0.6", + "eslint-plugin-simple-import-sort": "^8.0.0", "focus-visible": "^5.2.0", "framer-motion": "^6.5.1", "next": "13.0.6", @@ -58,6 +60,7 @@ "cypress": "^12.2.0", "cypress-image-diff-js": "^1.23.0", "eslint-plugin-storybook": "^0.6.8", + "eslint-plugin-unused-imports": "^2.0.0", "prettier": "2.8.1", "prisma": "^4.7.1", "typescript": "4.9.4" @@ -3354,6 +3357,19 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@dnd-kit/modifiers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz", + "integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.6", + "react": ">=16.8.0" + } + }, "node_modules/@dnd-kit/sortable": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.1.tgz", @@ -16343,6 +16359,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-8.0.0.tgz", + "integrity": "sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, "node_modules/eslint-plugin-storybook": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.6.8.tgz", @@ -16370,6 +16394,36 @@ "lodash": "^4.17.15" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -30865,6 +30919,15 @@ "tslib": "^2.0.0" } }, + "@dnd-kit/modifiers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz", + "integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==", + "requires": { + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + } + }, "@dnd-kit/sortable": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.1.tgz", @@ -40973,6 +41036,12 @@ "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "requires": {} }, + "eslint-plugin-simple-import-sort": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-8.0.0.tgz", + "integrity": "sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==", + "requires": {} + }, "eslint-plugin-storybook": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.6.8.tgz", @@ -40996,6 +41065,21 @@ } } }, + "eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/website/package.json b/website/package.json index e999d000..bc1f3b69 100644 --- a/website/package.json +++ b/website/package.json @@ -12,11 +12,15 @@ "build-storybook": "build-storybook", "cypress": "cypress open", "cypress:run": "cypress run", - "cypress:image-baseline": "cypress-image-diff -u" + "cypress:image-baseline": "cypress-image-diff -u", + "fix:lint": "eslint --fix src/ --ext .js,.jsx,.ts,.tsx", + "fix:format": "prettier --write ./src", + "fix": "npm run fix:format && npm run fix:lint" }, "dependencies": { "@chakra-ui/react": "^2.4.4", "@dnd-kit/core": "^6.0.6", + "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.1", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", @@ -32,6 +36,7 @@ "clsx": "^1.2.1", "eslint": "8.29.0", "eslint-config-next": "13.0.6", + "eslint-plugin-simple-import-sort": "^8.0.0", "focus-visible": "^5.2.0", "framer-motion": "^6.5.1", "next": "13.0.6", @@ -65,6 +70,7 @@ "cypress": "^12.2.0", "cypress-image-diff-js": "^1.23.0", "eslint-plugin-storybook": "^0.6.8", + "eslint-plugin-unused-imports": "^2.0.0", "prettier": "2.8.1", "prisma": "^4.7.1", "typescript": "4.9.4" diff --git a/website/src/components/Container.cy.tsx b/website/src/components/Container.cy.tsx index 5ffa204f..3e458307 100644 --- a/website/src/components/Container.cy.tsx +++ b/website/src/components/Container.cy.tsx @@ -1,4 +1,5 @@ import React from "react"; + import { Container } from "./Container"; describe("", () => { diff --git a/website/src/components/Faq.tsx b/website/src/components/Faq.tsx index d7c2eae9..8f42b920 100644 --- a/website/src/components/Faq.tsx +++ b/website/src/components/Faq.tsx @@ -1,5 +1,3 @@ -import Link from "next/link"; - import { Container } from "./Container"; const faqs = [ diff --git a/website/src/components/Footer.tsx b/website/src/components/Footer.tsx index 28765664..5c774398 100644 --- a/website/src/components/Footer.tsx +++ b/website/src/components/Footer.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; import Link from "next/link"; + import { Container } from "./Container"; export function Footer() { @@ -19,24 +20,21 @@ export function Footer() {