diff --git a/.clang-format b/.clang-format index 371fedc..3f98c96 100644 --- a/.clang-format +++ b/.clang-format @@ -1,113 +1,14 @@ ---- -AccessModifierOffset: -4 -AlignAfterOpenBracket: Align -AlignConsecutiveAssignments: false -AlignConsecutiveDeclarations: false -AlignEscapedNewlines: Left -AlignOperands: true -AlignTrailingComments: false -AllowAllParametersOfDeclarationOnNextLine: false -AllowShortBlocksOnASingleLine: false -AllowShortCaseLabelsOnASingleLine: false -AllowShortFunctionsOnASingleLine: None -AllowShortIfStatementsOnASingleLine: false -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterDefinitionReturnType: None -AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: false -AlwaysBreakTemplateDeclarations: false -BinPackArguments: true -BinPackParameters: true -BraceWrapping: - AfterClass: false - AfterControlStatement: false - AfterEnum: false - AfterFunction: true - AfterNamespace: true - AfterObjCDeclaration: false - AfterStruct: false - AfterUnion: false - AfterExternBlock: false - BeforeCatch: false - BeforeElse: false - IndentBraces: false - SplitEmptyFunction: true - SplitEmptyRecord: true - SplitEmptyNamespace: true -BreakBeforeBinaryOperators: None -BreakBeforeBraces: Custom -BreakBeforeInheritanceComma: false -BreakBeforeTernaryOperators: false -BreakConstructorInitializersBeforeComma: false -BreakConstructorInitializers: BeforeComma -BreakAfterJavaFieldAnnotations: false -BreakStringLiterals: false +BasedOnStyle: Google +IndentWidth: 4 ColumnLimit: 80 -CommentPragmas: "^ IWYU pragma:" -CompactNamespaces: false -ConstructorInitializerAllOnOneLineOrOnePerLine: false -ConstructorInitializerIndentWidth: 8 -ContinuationIndentWidth: 8 -Cpp11BracedListStyle: false -DerivePointerAlignment: false -DisableFormat: false -ExperimentalAutoDetectBinPacking: false -FixNamespaceComments: false - -ForEachMacros: - - foreach - - Q_FOREACH - - BOOST_FOREACH - -IncludeBlocks: Preserve -IncludeCategories: - - Regex: ".*" - Priority: 1 -IncludeIsMainRegex: "(Test)?$" -IndentCaseLabels: false -IndentGotoLabels: false -IndentPPDirectives: None -IndentWidth: 8 -IndentWrappedFunctionNames: false -JavaScriptQuotes: Leave -JavaScriptWrapImports: true -KeepEmptyLinesAtTheStartOfBlocks: false -MacroBlockBegin: "" -MacroBlockEnd: "" -MaxEmptyLinesToKeep: 1 -NamespaceIndentation: None -ObjCBinPackProtocolList: Auto -ObjCBlockIndentWidth: 8 -ObjCSpaceAfterProperty: true -ObjCSpaceBeforeProtocolList: true - -# Taken from git's rules -PenaltyBreakAssignment: 10 -PenaltyBreakBeforeFirstCallParameter: 30 -PenaltyBreakComment: 10 -PenaltyBreakFirstLessLess: 0 -PenaltyBreakString: 10 -PenaltyExcessCharacter: 100 -PenaltyReturnTypeOnItsOwnLine: 60 - -PointerAlignment: Right -ReflowComments: false +TabWidth: 4 +UseTab: Always +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AllowShortBlocksOnASingleLine: true +AllowShortFunctionsOnASingleLine: true +AllowShortLambdasOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true SortIncludes: true SortUsingDeclarations: true -SpaceAfterCStyleCast: false -SpaceAfterTemplateKeyword: true -SpaceBeforeAssignmentOperators: true -SpaceBeforeCtorInitializerColon: true -SpaceBeforeInheritanceColon: true -SpaceBeforeParens: ControlStatementsExceptForEachMacros -SpaceBeforeRangeBasedForLoopColon: true -SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 1 -SpacesInAngles: false -SpacesInContainerLiterals: false -SpacesInCStyleCastParentheses: false -SpacesInParentheses: false -SpacesInSquareBrackets: false -Standard: Cpp03 -TabWidth: 8 -UseTab: Always +PenaltyIndentedWhitespace: 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1032ab6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,211 @@ +name: CI + +on: + push: + branches: + - main + - dev + paths: + - "**ci.yml" + - "**.hpp" + - "**.cpp" + - "**CMakeLists.txt" + pull_request: + branches: + - main + - dev + paths: + - "**ci.yml" + - "**.hpp" + - "**.cpp" + - "**CMakeLists.txt" + +env: + BRANCH_NAME: ${{ github.ref == 'refs/heads/dev' && 'dev' || 'main' }} + PRERELEASE: ${{ github.ref == 'refs/heads/dev' && 'true' || 'false' }} + TAG_SUFFIX: ${{ github.ref == 'refs/heads/dev' && '-dev' || '' }} + +jobs: + changes: + runs-on: ubuntu-22.04 + outputs: + build: ${{ steps.filter.outputs.src }} + ci: ${{ steps.filter.outputs.ci }} + + steps: + - name: Checkout + if: github.event_name == 'push' + uses: actions/checkout@v3 + + - uses: dorny/paths-filter@v2 + id: filter + with: + base: ${{ env.BRANCH_NAME }} + filters: | + src: + - '**/*.cpp' + - '**/*.hpp' + - '**/CMakeLists.txt' + ci: + - '.github/workflows/ci.yml' + build: + needs: changes + strategy: + fail-fast: false + matrix: + config: + - { + os: ubuntu-22.04, + arch: x64, + binary_path: saber, + output_name: saber-linux-x64, + } + - { + os: windows-2022, + arch: x64, + binary_path: Release/saber.exe, + output_name: saber-windows-x64.exe, + } + - { + os: windows-2022, + arch: x86, + binary_path: Release/saber.exe, + output_name: saber-windows-x86.exe, + } + + name: build-${{ matrix.config.os }}-${{ matrix.config.arch }} + runs-on: ${{ matrix.config.os }} + if: needs.changes.outputs.build == 'true' + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y cmake g++-12 libssl-dev ninja-build rpm zlib1g-dev + + - name: Add MSBuild to PATH (Windows) + if: runner.os == 'Windows' + uses: microsoft/setup-msbuild@v1.1 + + - name: Configure CMake (Linux) + if: runner.os == 'Linux' + run: cmake -S . -B build -G Ninja + env: + CXX: g++-12 + + - name: Configure CMake (Windows x64) + if: runner.os == 'Windows' && matrix.config.arch == 'x64' + run: cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -T host=x64 + + - name: Configure CMake (Windows x86) + if: runner.os == 'Windows' && matrix.config.arch == 'x86' + run: cmake -S . -B build -D net_FORCE_BUILD_OPENSSL=ON -G "Visual Studio 17 2022" -A Win32 -T host=x86 + + - name: Build + run: cmake --build build --config Release + + - name: Move binary (Linux) + if: runner.os == 'Linux' + run: mv build/${{ matrix.config.binary_path }} build/${{ matrix.config.output_name }} + + - name: Move binary (Windows) + if: runner.os == 'Windows' + run: Move-Item build/${{ matrix.config.binary_path }} build/${{ matrix.config.output_name }} + + - name: Upload Binary + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.config.output_name }} + path: ./build/${{ matrix.config.output_name }} + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-22.04 + if: github.event_name == 'push' + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Get Latest Tag + id: latest-tag + run: | + if [[ "${{ github.ref }}" == 'refs/heads/dev' ]]; then + latest_tag=$(git tag -l | grep "\-dev" | sort -V | tail -n 1 || true) + else + latest_tag=$(git tag -l | grep -v "\-dev" | sort -V | tail -n 1 || true) + fi + + if [[ -z $latest_tag ]]; then + latest_tag="" + fi + + echo "::set-output name=tag::$latest_tag" + shell: bash + + - name: Get Next Version + id: semver + uses: ietf-tools/semver-action@v1 + with: + token: ${{ github.token }} + branch: ${{ env.BRANCH_NAME }} + fromTag: ${{ steps.latest-tag.outputs.tag }} + + - name: Create Draft Release + uses: ncipollo/release-action@v1.12.0 + with: + prerelease: ${{ env.PRERELEASE }} + draft: false + commit: ${{ github.sha }} + tag: ${{ steps.semver.outputs.next }}${{ env.TAG_SUFFIX }} + name: ${{ steps.semver.outputs.next }}${{ env.TAG_SUFFIX }} + body: "*pending*" + token: ${{ github.token }} + + - name: Update CHANGELOG + id: changelog + uses: requarks/changelog-action@v1 + with: + token: ${{ github.token }} + tag: ${{ steps.semver.outputs.next }}${{ env.TAG_SUFFIX }} + writeToFile: false + + - name: Create Release + uses: ncipollo/release-action@v1.12.0 + with: + prerelease: ${{ env.PRERELEASE }} + allowUpdates: true + draft: false + makeLatest: true + commit: ${{ github.sha }} + tag: ${{ steps.semver.outputs.next }}${{ env.TAG_SUFFIX }} + name: ${{ steps.semver.outputs.next }}${{ env.TAG_SUFFIX }} + body: ${{ steps.changelog.outputs.changes }} + token: ${{ github.token }} + + outputs: + next: ${{ steps.semver.outputs.next }} + + upload: + needs: release + runs-on: ubuntu-22.04 + if: github.event_name == 'push' + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v3 + with: + path: . + + - name: Upload artifacts to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ github.token }} + file_glob: true + file: "**/*" + tag: ${{ needs.release.outputs.next }}${{ env.TAG_SUFFIX }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 6872158..67e5bbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,16 +46,24 @@ cpmfindpackage( ) file(GLOB_RECURSE sources CONFIGURE_DEPENDS "include/*.hpp" "src/*.cpp") +list(FILTER sources EXCLUDE REGEX ".*main\\.cpp$") -add_executable(${PROJECT_NAME} ${sources}) +add_library(${PROJECT_NAME}_lib ${sources}) +add_executable(${PROJECT_NAME} src/main.cpp) + +target_compile_features(${PROJECT_NAME}_lib PUBLIC cxx_std_17) + +if(WIN32) + target_compile_definitions(${PROJECT_NAME} PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() -target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) target_include_directories( - ${PROJECT_NAME} PUBLIC $ - $ + ${PROJECT_NAME}_lib PUBLIC $ + $ ) -target_link_libraries(${PROJECT_NAME} PRIVATE ekizu spdlog::spdlog) +target_link_libraries(${PROJECT_NAME}_lib PUBLIC ekizu spdlog::spdlog) +target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_lib) file(GLOB_RECURSE commands CONFIGURE_DEPENDS commands/*.cpp) @@ -63,15 +71,9 @@ foreach(fullcmdname ${commands}) get_filename_component(cmdname ${fullcmdname} NAME_WE) message(STATUS "Found command:'cmd_${cmdname}'") add_library(cmd_${cmdname} SHARED ${fullcmdname}) - target_include_directories( - cmd_${cmdname} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include - ) - target_link_libraries(cmd_${cmdname} PRIVATE ekizu) + target_link_libraries(cmd_${cmdname} PRIVATE ${PROJECT_NAME}_lib) endforeach(fullcmdname) if(${PROJECT_NAME}_INSTALL) install(TARGETS ${PROJECT_NAME} DESTINATION bin) -endif() - -target_compile_options(${PROJECT_NAME} PUBLIC -fsanitize=thread) -target_link_options(${PROJECT_NAME} PUBLIC -fsanitize=thread) +endif() \ No newline at end of file diff --git a/commands/Fun/8ball.cpp b/commands/Fun/8ball.cpp index e38e4fd..eb1749c 100644 --- a/commands/Fun/8ball.cpp +++ b/commands/Fun/8ball.cpp @@ -1,4 +1,3 @@ -#include #include #include @@ -6,39 +5,31 @@ using namespace saber; struct Eightball : Command { explicit Eightball(Saber *creator) - : Command(creator, + : Command( + creator, CommandOptions{ "8ball", DIRNAME, true, {}, - false, {}, {}, - { "eight-ball", "eightball" }, + {}, + {"eight-ball", "eightball"}, "8ball ", "Asks the Magic 8-Ball for some psychic wisdom.", - { ekizu::Permissions::SendMessages, - ekizu::Permissions::EmbedLinks }, - {}, - false, - false, - 3000, - {}, + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, {}, {}, {}, - {} }) - { - } + 3000, + }) {} - void setup() override - { - } + void setup() override {} void execute(const ekizu::Message &message, - const std::vector &args) override - { + const std::vector &args) override { if (args.empty()) { (void)bot->http.create_message(message.channel_id) .content("You need to ask a question!") @@ -54,7 +45,7 @@ struct Eightball : Command { .send(); } - private: + private: std::vector responses{ "It is certain.", "It is decidedly so.", diff --git a/commands/Fun/countdown.cpp b/commands/Fun/countdown.cpp new file mode 100644 index 0000000..404c581 --- /dev/null +++ b/commands/Fun/countdown.cpp @@ -0,0 +1,52 @@ +#include +#include + +using namespace saber; + +struct Countdown : Command { + explicit Countdown(Saber *creator) + : Command( + creator, + CommandOptions{ + "countdown", + DIRNAME, + true, + {}, + {}, + {}, + {}, + {}, + "countdown", + "Start counting down from 5.", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 0, + }) {} + + void setup() override {} + + void execute(const ekizu::Message &message, + const std::vector &args) override { + if (!args.empty()) { return; } + + const std::vector countdown{ + "five", "four", "three", "two", "one"}; + + for (const std::string &num : countdown) { + (void)bot->http.create_message(message.channel_id) + .content("**:" + num + ":**") + .send(); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + (void)bot->http.create_message(message.channel_id) + .content("**:ok:** DING DING DING") + .send(); + } +}; + +COMMAND_ALLOC(Countdown) +COMMAND_FREE(Countdown) diff --git a/commands/Fun/css.cpp b/commands/Fun/css.cpp new file mode 100644 index 0000000..19b2ad7 --- /dev/null +++ b/commands/Fun/css.cpp @@ -0,0 +1,46 @@ +#include +#include + +using namespace saber; + +struct CSS : Command { + explicit CSS(Saber *creator) + : Command( + creator, + CommandOptions{ + "css", + DIRNAME, + true, + {}, + {}, + {}, + {}, + {}, + "css", + "", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 0, + }) {} + + void setup() override {} + + void execute(const ekizu::Message &message, + const std::vector &args) override { + if (!args.empty()) { return; } + + (void)bot->http.create_message(message.channel_id) + .content( + "https://media2.giphy.com/media/yYSSBtDgbbRzq/" + "giphy.gif?cid=" + "ecf05e47ckbtzm84p629vw655dbua1qzaiw8tl46ejp4f0xj&ep=v1_gifs_" + "search&rid=giphy.gif&ct=g") + .send(); + } +}; + +COMMAND_ALLOC(CSS) +COMMAND_FREE(CSS) diff --git a/commands/Fun/hype.cpp b/commands/Fun/hype.cpp new file mode 100644 index 0000000..d7c2cff --- /dev/null +++ b/commands/Fun/hype.cpp @@ -0,0 +1,57 @@ +#include +#include + +using namespace saber; + +struct Hype : Command { + explicit Hype(Saber *creator) + : Command( + creator, + CommandOptions{ + "hype", + DIRNAME, + true, + {}, + {}, + {}, + {}, + {"hypu", "train"}, + "hype", + "", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 0, + }) {} + + void setup() override {} + + void execute(const ekizu::Message &message, + const std::vector &args) override { + if (!args.empty()) { return; } + + const std::string &selectedHype = + hypu[util::get_random_number(0, hypu.size() - 1)]; + std::string msg = ":train2: CHOO CHOO " + selectedHype; + + (void)bot->http.create_message(message.channel_id).content(msg).send(); + } + + private: + std::vector hypu{ + "https://cdn.discordapp.com/attachments/102817255661772800/" + "219514281136357376/tumblr_nr6ndeEpus1u21ng6o1_540.gif", + "https://cdn.discordapp.com/attachments/102817255661772800/" + "219518372839161859/tumblr_n1h2afSbCu1ttmhgqo1_500.gif", + "https://gfycat.com/HairyFloweryBarebirdbat", + "https://i.imgur.com/PFAQSLA.gif", + "https://abload.de/img/ezgif-32008219442iq0i.gif", + "https://i.imgur.com/vOVwq5o.jpg", + "https://i.imgur.com/Ki12X4j.jpg", + "https://media.giphy.com/media/b1o4elYH8Tqjm/giphy.gif"}; +}; + +COMMAND_ALLOC(Hype) +COMMAND_FREE(Hype) diff --git a/commands/Fun/meme.cpp b/commands/Fun/meme.cpp new file mode 100644 index 0000000..3095b00 --- /dev/null +++ b/commands/Fun/meme.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include + +using namespace saber; + +struct Meme : Command { + explicit Meme(Saber *creator) + : Command( + creator, + CommandOptions{ + "meme", + DIRNAME, + true, + {}, + {}, + {}, + {}, + {}, + "meme", + "Displays a random meme from the `memes`, `dankmemes`, or " + "`me_irl` subreddits.", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 0, + }) {} + + void setup() override {} + + void execute( + const ekizu::Message &message, + [[maybe_unused]] const std::vector &args) override { + fetch_meme(message); + } + + void fetch_meme(const ekizu::Message &message) { + auto res = net::http::get("https://meme-api.com/gimme"); + + if (!res) { return; } + + const auto json = nlohmann::json::parse(res->body, nullptr, false); + + if (json.is_discarded() || !json.is_object()) { return; } + + auto embed = + ekizu::EmbedBuilder{} + .set_title(json["title"]) + .set_description(fmt::format( + "By: **{}** | {}", json["author"].get(), + json["postLink"].get())) + .set_image({ + json["url"].get(), + }) + .build(); + + (void)bot->http.create_message(message.channel_id) + .embeds({std::move(embed)}) + .send(); + } +}; + +COMMAND_ALLOC(Meme) +COMMAND_FREE(Meme) diff --git a/commands/Fun/ping.cpp b/commands/Fun/ping.cpp new file mode 100644 index 0000000..183768c --- /dev/null +++ b/commands/Fun/ping.cpp @@ -0,0 +1,41 @@ +#include +#include + +using namespace saber; + +struct Ping : Command { + explicit Ping(Saber *creator) + : Command( + creator, + CommandOptions{ + "ping", + DIRNAME, + true, + {}, + {}, + {}, + {}, + {}, + "ping", + "Replies with pong.", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 3000, + }) {} + + void setup() override {} + + void execute( + const ekizu::Message &message, + [[maybe_unused]] const std::vector &args) override { + (void)bot->http.create_message(message.channel_id) + .content("Pong!") + .send(); + } +}; + +COMMAND_ALLOC(Ping) +COMMAND_FREE(Ping) diff --git a/commands/Fun/praise.cpp b/commands/Fun/praise.cpp new file mode 100644 index 0000000..955651b --- /dev/null +++ b/commands/Fun/praise.cpp @@ -0,0 +1,42 @@ +#include +#include + +using namespace saber; + +struct Praise : Command { + explicit Praise(Saber *creator) + : Command( + creator, + CommandOptions{ + "praise", + DIRNAME, + true, + {}, + {}, + {}, + {}, + {}, + "praise", + "", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 0, + }) {} + + void setup() override {} + + void execute(const ekizu::Message &message, + const std::vector &args) override { + if (!args.empty()) { return; } + + (void)bot->http.create_message(message.channel_id) + .content("https://i.imgur.com/K8ySn3e.gif") + .send(); + } +}; + +COMMAND_ALLOC(Praise) +COMMAND_FREE(Praise) diff --git a/commands/Fun/xkcd.cpp b/commands/Fun/xkcd.cpp new file mode 100644 index 0000000..78c3d43 --- /dev/null +++ b/commands/Fun/xkcd.cpp @@ -0,0 +1,75 @@ +#include +#include +#include + +using namespace saber; + +struct XKCD : Command { + explicit XKCD(Saber *creator) + : Command( + creator, + CommandOptions{ + "xkcd", + DIRNAME, + true, + {}, + {}, + {}, + {}, + {}, + "xkcd", + "", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 0, + }) {} + + void setup() override {} + + void execute(const ekizu::Message &message, + const std::vector &args) override { + fetchXKCD(message, args); + } + + private: + std::string api_url{"https://xkcd.com{}info.0.json"}; + + void fetchXKCD(const ekizu::Message &message, + [[maybe_unused]] const std::vector &args) { + const auto res = + net::http::get(api_url.replace(api_url.find("{}"), 2, "/")); + + if (!res || res->status_code != net::HttpStatus::Ok) { + bot->logger->error("Error while fetching XKCD data"); + (void)bot->http.create_message(message.channel_id) + .content("There was an error. Please try again.") + .send(); + return; + } + + const auto json = nlohmann::json::parse(res->body, nullptr, false); + + if (json.is_discarded() || !json.is_object()) { + bot->logger->error("Error parsing XKCD data"); + (void)bot->http.create_message(message.channel_id) + .content("There was an error. Please try again.") + .send(); + return; + } + + const auto comic_url = fmt::format("https://xkcd.com/{}", json["num"]); + const auto msg = fmt::format( + "**{}**\n{}\nAlt Text:```{}```XKCD Link: <{}>", + json["safe_title"].get(), + json["img"].get(), json["alt"].get(), + comic_url); + + (void)bot->http.create_message(message.channel_id).content(msg).send(); + } +}; + +COMMAND_ALLOC(XKCD) +COMMAND_FREE(XKCD) diff --git a/commands/Misc/about.cpp b/commands/Misc/about.cpp new file mode 100644 index 0000000..c88231c --- /dev/null +++ b/commands/Misc/about.cpp @@ -0,0 +1,93 @@ +#include +#include +#include + +using namespace saber; + +struct About : Command { + explicit About(Saber *creator) + : Command( + creator, + CommandOptions{ + "about", + DIRNAME, + true, + true, + false, + {}, + {}, + {}, + "about", + "Info about me.", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 2000, + }) {} + + void setup() override { + bot->logger->info("Setting up About command"); + + bot->http.get_user(bot->owner_id) + .send() + .map([this](const ekizu::User &owner) { + bot->logger->info( + "Fetched owner: {} from the API", owner.username); + bot->users_cache.put(owner.id, owner); + bot->http.get_current_user().send().map( + [this, &owner](const ekizu::User &user) { + bot->logger->info( + "Fetched our own user: {} from the API", + user.username); + bot->users_cache.put(user.id, user); + + about_embed = + ekizu::EmbedBuilder{} + .set_title(fmt::format( + "🔥About :: Yamato | ID :: {}", user.id)) + .set_description( + "Yamato is a simple bot that was built for " + "my " + "personal Discord server. From providing " + "7DS " + "game " + "updates to moderating your own server, " + "this " + "is " + "the bot for you! I am glad you enjoy my " + "bot " + "and " + "hope you enjoy your stay 💖.") + .set_thumbnail({user.display_avatar_url()}) + .add_fields({ + {"Info\nOwner", + fmt::format("{}", bot->owner_id), true}, + }) + .set_footer( + {fmt::format("Made by {}", owner.username), + owner.display_avatar_url()}) + .build(); + + bot->logger->info("About command setup complete"); + }); + }); + } + + void execute( + const ekizu::Message &message, + [[maybe_unused]] const std::vector &args) override { + if (!about_embed) { return; } + + (void)bot->http.create_message(message.channel_id) + .embeds({*about_embed}) + .send(); + } + + private: + std::optional about_embed; +}; + +COMMAND_ALLOC(About) +COMMAND_FREE(About) diff --git a/commands/Misc/help.cpp b/commands/Misc/help.cpp new file mode 100644 index 0000000..a7b38ea --- /dev/null +++ b/commands/Misc/help.cpp @@ -0,0 +1,104 @@ +#include +#include +#include +#include + +using namespace saber; + +struct Help : Command { + explicit Help(Saber *creator) + : Command( + creator, + CommandOptions{ + "help", + DIRNAME, + true, + {}, + {}, + {}, + {}, + {}, + "help", + "", + {ekizu::Permissions::SendMessages, + ekizu::Permissions::EmbedLinks}, + {}, + {}, + {}, + 3000}) {} + + void setup() override {} + + void execute(const ekizu::Message &message, + const std::vector &args) override { + // TODO: Implement help menu when no args are given. + + return get_command_help(message, args[0]); + } + + void get_command_help(const ekizu::Message &message, + const std::string &command) { + bot->commands.get_commands( + [this, &command, &message]( + const std::unordered_map > + &commands) { + if (commands.find(command) == commands.end()) { + (void)bot->http.create_message(message.channel_id) + .content("Command not found.") + .send(); + return; + } + + const auto &cmd = commands.at(command); + auto builder = + ekizu::EmbedBuilder{}.set_title(cmd->options.name); + + if (!cmd->options.description.empty()) { + builder.set_description(cmd->options.description); + } + + if (!cmd->options.examples.empty()) { + std::string examples{}; + examples.reserve((bot->prefix.length() + 3) * + cmd->options.examples.size()); + for (const auto &example : cmd->options.examples) { + examples += + fmt::format("`{}{}`\n", bot->prefix, example); + } + + builder.add_field({"❯ Examples", examples}); + } + + if (!cmd->options.usage.empty()) { + builder.add_field( + {"❯ Usage", fmt::format("`{}{}`", bot->prefix, + cmd->options.usage)}); + } + + if (const auto &aliases = cmd->options.aliases; + !aliases.empty()) { + const auto aliases_str = std::accumulate( + std::next(aliases.begin()), aliases.end(), + fmt::format("`{}`", aliases[0]), + [](const auto &a, const auto &b) { + return fmt::format("{} | `{}`", a, b); + }); + + builder.add_field({"❯ Aliases", aliases_str}); + } + + builder.add_field( + {"❯ Cooldown", fmt::format("{}ms", cmd->options.cooldown)}); + builder.add_field( + {"❯ Legend", fmt::format("`<> required, [] optional`")}); + builder.set_footer({fmt::format("Prefix: `{}`", bot->prefix)}); + + (void)bot->http.create_message(message.channel_id) + .embeds({builder.build()}) + .send(); + }); + } +}; + +COMMAND_ALLOC(Help) +COMMAND_FREE(Help) diff --git a/include/saber/commands.hpp b/include/saber/commands.hpp index e6a51de..51b3691 100644 --- a/include/saber/commands.hpp +++ b/include/saber/commands.hpp @@ -11,8 +11,7 @@ #include #include -namespace saber -{ +namespace saber { struct Command; struct Saber; @@ -28,27 +27,29 @@ struct CommandLoader { void load_all(); void process_commands(const ekizu::Message &message); void unload(const std::string &name); + void get_commands( + ekizu::FunctionView > &)>) + const; - private: + private: mutable std::mutex m_mtx; Saber *m_parent{}; std::unordered_map commands; std::unordered_map > command_map; std::unordered_map > alias_map; - std::unordered_map > - slash_commands; + std::unordered_map > slash_commands; std::unordered_map > user_commands; std::unordered_map< std::string, - std::unordered_map > + std::unordered_map > cooldowns; }; struct CommandOptions { std::string name; std::string dirname; - bool enabled{ true }; + bool enabled{true}; bool init{}; bool guild_only{}; bool slash{}; @@ -61,24 +62,22 @@ struct CommandOptions { std::vector bot_permissions; bool nsfw{}; bool owner_only{}; - uint32_t cooldown{ 3000 }; - std::vector examples; - std::string subcommands; + uint32_t cooldown{3000}; + std::vector examples{}; + std::string subcommands{}; bool activity{}; bool voice_only{}; - std::string category; + std::string category{}; }; struct Command { Command(Saber *instigator, CommandOptions options_) - : bot{ instigator } - , options{ std::move(options_) } - { + : bot{instigator}, options{std::move(options_)} { // Determine category based on the name of the folder the command is in. - options.category = !options.dirname.empty() ? options.dirname : - "Other"; + options.category = !options.dirname.empty() ? options.dirname : "Other"; - // If the command is a slash command, make sure to make the appropriate JSON data. + // If the command is a slash command, make sure to make the appropriate + // JSON data. if (options.slash) { // slash_data.name = options.name; @@ -102,53 +101,49 @@ struct Command { virtual void setup() = 0; /// Method reserved for message commands' execution. virtual void execute(const ekizu::Message &message, - const std::vector &args) = 0; + const std::vector &args) = 0; Saber *bot{}; CommandOptions options; }; #if defined(__linux__) || defined(__APPLE__) -#define COMMAND_ALLOC(name) \ - extern "C" { \ - __attribute__((visibility("default"))) Command * \ - init_command(Saber *parent) \ - { \ - return new name(parent); \ - } \ +#define COMMAND_ALLOC(name) \ + extern "C" { \ + __attribute__((visibility("default"))) saber::Command *init_command( \ + saber::Saber *parent) { \ + return new name(parent); \ + } \ } #elif defined(_WIN32) -#define COMMAND_ALLOC(name) \ - extern "C" { \ - __declspec(dllexport) Command *init_command(Saber *parent) \ - { \ - return new name(parent); \ - } \ +#define COMMAND_ALLOC(name) \ + extern "C" { \ + __declspec(dllexport) saber::Command *init_command(saber::Saber *parent) { \ + return new name(parent); \ + } \ } #endif #if defined(__linux__) || defined(__APPLE__) -#define COMMAND_FREE(name) \ - extern "C" { \ - __attribute__((visibility("default"))) void \ - free_command(Command *command) \ - { \ - delete command; \ - } \ - } -#elif defined(_WIN32) -#define COMMAND_FREE(name) \ +#define COMMAND_FREE(name) \ extern "C" { \ - __declspec(dllexport) void free_command(Command *command) \ - { \ - delete command; \ + __attribute__((visibility("default"))) void free_command( \ + saber::Command *command) { \ + delete command; \ } \ } +#elif defined(_WIN32) +#define COMMAND_FREE(name) \ + extern "C" { \ + __declspec(dllexport) void free_command(saber::Command *command) { \ + delete command; \ + } \ + } #endif // I miss javascript #define DIRNAME \ std::filesystem::path(__FILE__).parent_path().filename().string() -} // namespace saber +} // namespace saber -#endif // SABER_COMMANDS_HPP +#endif // SABER_COMMANDS_HPP diff --git a/include/saber/library.hpp b/include/saber/library.hpp index a64278c..031c669 100644 --- a/include/saber/library.hpp +++ b/include/saber/library.hpp @@ -6,7 +6,7 @@ #include #ifdef _WIN32 -#define _WINSOCKAPI_ // stops windows.h including winsock.h +#define _WINSOCKAPI_ // stops windows.h including winsock.h #include using dlopen_handle_t = HMODULE; #else @@ -14,13 +14,14 @@ using dlopen_handle_t = HMODULE; using dlopen_handle_t = void *; #endif -namespace saber -{ -template using Result = ekizu::Result; +namespace saber { +template +using Result = ekizu::Result; /** - * @brief Simple cross-platform wrapper for platform-specific dlopen()-like functions. Supports loading instances of - * objects with supporting allocater and deallocater functions. + * @brief Simple cross-platform wrapper for platform-specific dlopen()-like + * functions. Supports loading instances of objects with supporting allocater + * and deallocater functions. * * @tparam T The type of object to load and manage. * @tparam Args The types of the arguments for the allocater. @@ -28,15 +29,15 @@ template using Result = ekizu::Result; struct Library { static Result create(std::string_view path); - template Result get(std::string_view name) const - { + template + Result get(std::string_view name) const { if (m_handle == nullptr) { return tl::make_unexpected( std::make_error_code(std::errc::bad_address)); } #ifdef _WIN32 - auto *ptr = ::GetProcAddress(name.data(), nullptr); + auto *ptr = ::GetProcAddress(m_handle, name.data()); #else auto *ptr = dlsym(m_handle, name.data()); #endif @@ -55,12 +56,12 @@ struct Library { Library &operator=(Library &&) noexcept; ~Library(); - private: + private: Library(dlopen_handle_t handle); /// Our handle to the shared library. dlopen_handle_t m_handle{}; }; -} // namespace saber +} // namespace saber -#endif // SABER_LIBRARY_HPP +#endif // SABER_LIBRARY_HPP diff --git a/include/saber/saber.hpp b/include/saber/saber.hpp index c0c59cf..e1c478a 100644 --- a/include/saber/saber.hpp +++ b/include/saber/saber.hpp @@ -1,16 +1,16 @@ #ifndef SABER_SABER_HPP #define SABER_SABER_HPP +#include +#include + #include #include #include -#include -#include -namespace saber -{ +namespace saber { struct Saber { - Saber(std::string_view token); + explicit Saber(std::string_view token); void run(); void handle_event(ekizu::Event ev); @@ -19,8 +19,13 @@ struct Saber { CommandLoader commands; ekizu::HttpClient http; std::shared_ptr logger; + ekizu::LruCache messages_cache{500}; + ekizu::Snowflake owner_id{155780111197536256}; + std::string prefix{">"}; ekizu::Shard shard; + ekizu::CurrentUser user; + ekizu::LruCache users_cache{500}; }; -} // namespace saber +} // namespace saber -#endif // SABER_SABER_HPP +#endif // SABER_SABER_HPP diff --git a/include/saber/util.hpp b/include/saber/util.hpp index 4d9f319..8a15a7f 100644 --- a/include/saber/util.hpp +++ b/include/saber/util.hpp @@ -6,12 +6,10 @@ #include #include -namespace saber::util -{ +namespace saber::util { template T get_random_number(T begin = (std::numeric_limits::min)(), - T end = (std::numeric_limits::max)()) -{ + T end = (std::numeric_limits::max)()) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dis(begin, end); @@ -22,6 +20,6 @@ std::vector split(std::string_view s, std::string_view delimiter); std::string <rim(std::string &s); std::string &rtrim(std::string &s); std::string &trim(std::string &s); -} // namespace saber::util +} // namespace saber::util -#endif // SABER_UTIL_HPP +#endif // SABER_UTIL_HPP diff --git a/src/commands.cpp b/src/commands.cpp index e5e3958..13e6e0c 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -2,56 +2,44 @@ #include #include -namespace -{ +namespace { #ifdef _WIN32 -constexpr std::string_view LIBRARY_EXTENSION{ ".dll" }; +constexpr std::string_view LIBRARY_EXTENSION{".dll"}; #elif __linux__ -constexpr std::string_view LIBRARY_EXTENSION{ ".so" }; +constexpr std::string_view LIBRARY_EXTENSION{".so"}; #elif __APPLE__ -constexpr std::string_view LIBRARY_EXTENSION{ ".dylib" }; +constexpr std::string_view LIBRARY_EXTENSION{".dylib"}; #endif -} // namespace +} // namespace -namespace saber -{ -CommandLoader::CommandLoader(Saber *parent) - : m_parent{ parent } -{ -} +namespace saber { +CommandLoader::CommandLoader(Saber *parent) : m_parent{parent} {} -void CommandLoader::load(std::string_view path) -{ +void CommandLoader::load(std::string_view path) { auto library = Library::create(path); if (!library) { + m_parent->logger->error( + "Failed to load command {}: {}", path, library.error().message()); return; } auto init_command = library->get("init_command"); auto free_command = library->get("free_command"); - if (!init_command || !free_command) { - return; - } + if (!init_command || !free_command) { return; } - std::scoped_lock lk{ m_mtx }; + std::scoped_lock lk{m_mtx}; auto *command_ptr = (*init_command)(m_parent); - if (command_ptr == nullptr) { - return; - } + if (command_ptr == nullptr) { return; } - if (command_ptr->options.init) { - command_ptr->setup(); - } + if (command_ptr->options.init) { command_ptr->setup(); } const auto &command_name = command_ptr->options.name; const std::shared_ptr loader{ - command_ptr, - [dealloc = *free_command](Command *ptr) { dealloc(ptr); } - }; + command_ptr, [dealloc = *free_command](Command *ptr) { dealloc(ptr); }}; commands.insert_or_assign(command_name, std::move(*library)); command_map.insert_or_assign(command_name, loader); @@ -71,66 +59,53 @@ void CommandLoader::load(std::string_view path) m_parent->logger->info("Loaded command {}", command_name); } -void CommandLoader::load_all() -{ +void CommandLoader::load_all() { namespace fs = std::filesystem; for (const auto &file : fs::directory_iterator(".")) { const auto filename = file.path().filename().string(); if (filename.find("cmd_") != std::string::npos && - (filename.size() >= LIBRARY_EXTENSION.size() && - filename.compare(filename.size() - LIBRARY_EXTENSION.size(), - LIBRARY_EXTENSION.size(), - LIBRARY_EXTENSION) == 0)) { - load(fs::absolute(file.path()) - .lexically_normal() - .string()); + (filename.size() >= LIBRARY_EXTENSION.size() && + filename.compare( + filename.size() - LIBRARY_EXTENSION.size(), + LIBRARY_EXTENSION.size(), LIBRARY_EXTENSION) == 0)) { + load(fs::absolute(file.path()).lexically_normal().string()); } } m_parent->logger->info("Loaded {} commands", commands.size()); } -void CommandLoader::process_commands(const ekizu::Message &message) -{ - if (message.author.bot) { - return; - } +void CommandLoader::process_commands(const ekizu::Message &message) { + if (message.author.bot) { return; } - auto content = message.content; + auto content = message.content.substr(m_parent->prefix.size()); auto args = util::split(util::trim(content), " "); - if (args.empty()) { - return; - } + if (args.empty()) { return; } auto command_name = std::move(args.front()); args.erase(args.begin()); - std::transform(command_name.begin(), command_name.end(), - command_name.begin(), [](uint8_t c) { - return static_cast(std::tolower(c)); - }); + std::transform( + command_name.begin(), command_name.end(), command_name.begin(), + [](uint8_t c) { return static_cast(std::tolower(c)); }); - std::unique_lock lk{ m_mtx }; + std::unique_lock lk{m_mtx}; - if (command_map.find(command_name) == command_map.end()) { - return; - } + if (command_map.find(command_name) == command_map.end()) { return; } - // NOTE: I'm seeing a case in which the commands will need the lock so it should be unlocked here. i.e. an unload command or something. + // NOTE: I'm seeing a case in which the commands will need the lock so it + // should be unlocked here. i.e. an unload command or something. lk.unlock(); command_map.at(command_name)->execute(message, args); } -void CommandLoader::unload(const std::string &name) -{ - std::scoped_lock lk{ m_mtx }; +void CommandLoader::unload(const std::string &name) { + std::scoped_lock lk{m_mtx}; - if (commands.find(name) == commands.end()) { - return; - } + if (commands.find(name) == commands.end()) { return; } const auto command = std::move(command_map.at(name)); @@ -144,4 +119,13 @@ void CommandLoader::unload(const std::string &name) user_commands.erase(name); m_parent->logger->info("Unloaded command {}", name); } -} // namespace saber \ No newline at end of file + +void CommandLoader::get_commands( + ekizu::FunctionView > &)> + cb) const { + std::scoped_lock lk{m_mtx}; + + cb(command_map); +} +} // namespace saber \ No newline at end of file diff --git a/src/library.cpp b/src/library.cpp index 849c94d..e372a25 100644 --- a/src/library.cpp +++ b/src/library.cpp @@ -1,9 +1,9 @@ +#include + #include -namespace saber -{ -Result Library::create(std::string_view path) -{ +namespace saber { +Result Library::create(std::string_view path) { if (path.empty()) { return tl::make_unexpected( std::make_error_code(std::errc::bad_address)); @@ -11,30 +11,28 @@ Result Library::create(std::string_view path) #ifdef _WIN32 auto *handle = LoadLibrary(path.data()); + const auto last_error = GetLastError(); #else auto *handle = dlopen(path.data(), RTLD_NOW | RTLD_LOCAL); + const auto *last_error = dlerror(); #endif if (handle == nullptr) { + fmt::println("Failed to load library: {}", last_error); return tl::make_unexpected( std::make_error_code(std::errc::bad_address)); } - return Library{ handle }; + return Library{handle}; } -Library::Library(dlopen_handle_t handle) - : m_handle{ handle } -{ -} +Library::Library(dlopen_handle_t handle) : m_handle{handle} {} -Library::Library(Library &&other) noexcept : m_handle{ other.m_handle } -{ +Library::Library(Library &&other) noexcept : m_handle{other.m_handle} { other.m_handle = nullptr; } -Library &Library::operator=(Library &&other) noexcept -{ +Library &Library::operator=(Library &&other) noexcept { if (this != &other) { m_handle = other.m_handle; other.m_handle = nullptr; @@ -43,11 +41,8 @@ Library &Library::operator=(Library &&other) noexcept return *this; } -Library::~Library() -{ - if (m_handle == nullptr) { - return; - } +Library::~Library() { + if (m_handle == nullptr) { return; } #ifdef _WIN32 FreeLibrary(m_handle); @@ -55,4 +50,4 @@ Library::~Library() dlclose(m_handle); #endif } -} // namespace saber +} // namespace saber diff --git a/src/main.cpp b/src/main.cpp index 5fcbd79..4bed1b4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,15 @@ +#include #include -int main() -{ - auto saber = saber::Saber{ std::getenv("DISCORD_TOKEN") }; +int main() { + const auto* token = std::getenv("DISCORD_TOKEN"); + + if (token == nullptr) { + std::cerr << "Missing DISCORD_TOKEN environment variable\n"; + return 1; + } + + auto saber = saber::Saber{token}; saber.run(); } diff --git a/src/saber.cpp b/src/saber.cpp index 6539d29..1622e12 100644 --- a/src/saber.cpp +++ b/src/saber.cpp @@ -1,51 +1,49 @@ #include -namespace -{ -template struct overload : Func... { +namespace { +template +struct overload : Func... { using Func::operator()...; }; -template overload(Func...) -> overload; -} // namespace +template +overload(Func...) -> overload; +} // namespace -namespace saber -{ +namespace saber { Saber::Saber(std::string_view token) - : commands{ this } - , http{ token } - , shard{ ekizu::ShardId::ONE, token, ekizu::Intents::AllIntents } -{ + : commands{this}, + http{token}, + shard{ekizu::ShardId::ONE, token, ekizu::Intents::AllIntents} { logger = spdlog::stdout_color_mt("logger"); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v"); spdlog::set_level(spdlog::level::debug); } -void Saber::run() -{ +void Saber::run() { commands.load_all(); (void)shard.run([this](const ekizu::Event &ev) { handle_event(ev); }); } -void Saber::handle_event(ekizu::Event ev) -{ +void Saber::handle_event(ekizu::Event ev) { std::visit( - overload{ [this](const ekizu::Ready &r) { - const auto &user = r.user; - - logger->info("Logged in as {}", user.username); - bot_id = user.id; - logger->info("API version: {}", r.v); - logger->info("Guilds: {}", r.guilds.size()); - }, - [this](const ekizu::MessageCreate &m) { - commands.process_commands(m.message); - }, - [this](ekizu::Resumed) { logger->info("Resumed"); }, - [this](const auto &e) { - logger->warn("Unhandled event: {}", - typeid(e).name()); - } }, + overload{[this](const ekizu::Ready &r) { + user = r.user; + logger->info("Logged in as {}", user.username); + bot_id = user.id; + logger->info("API version: {}", r.v); + logger->info("Guilds: {}", r.guilds.size()); + }, + [this](const ekizu::MessageCreate &m) { + messages_cache.put(m.message.id, m.message); + users_cache.put(m.message.author.id, m.message.author); + commands.process_commands(m.message); + }, + [this](const ekizu::Log &l) { logger->debug(l.message); }, + [this](ekizu::Resumed) { logger->info("Resumed"); }, + [this](const auto &e) { + logger->warn("Unhandled event: {}", typeid(e).name()); + }}, ev); } -} // namespace saber +} // namespace saber diff --git a/src/util.cpp b/src/util.cpp index f6d6495..d59104a 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -1,10 +1,8 @@ #include #include -namespace saber::util -{ -std::vector split(std::string_view s, std::string_view delimiter) -{ +namespace saber::util { +std::vector split(std::string_view s, std::string_view delimiter) { std::vector ret; size_t pos{}; size_t prev{}; @@ -18,30 +16,22 @@ std::vector split(std::string_view s, std::string_view delimiter) return ret; } -std::string <rim(std::string &s) -{ - s.erase(s.begin(), - std::find_if(s.begin(), s.end(), [](unsigned char ch) { - return std::isspace(ch) == 0; - })); +std::string <rim(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { + return std::isspace(ch) == 0; + })); return s; } -std::string &rtrim(std::string &s) -{ +std::string &rtrim(std::string &s) { s.erase(std::find_if(s.rbegin(), s.rend(), - [](unsigned char ch) { - return std::isspace(ch) == 0; - }) - .base(), - s.end()); + [](unsigned char ch) { return std::isspace(ch) == 0; }) + .base(), + s.end()); return s; } -std::string &trim(std::string &s) -{ - return ltrim(rtrim(s)); -} -} // namespace saber::util \ No newline at end of file +std::string &trim(std::string &s) { return ltrim(rtrim(s)); } +} // namespace saber::util \ No newline at end of file