commit ab3a1cb6b1bfcae510dc879efb4b46441763c0bb Author: L-Nafaryus Date: Thu Mar 14 13:06:52 2024 +0500 stage changes diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..fdb33e6 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 4 +--- +Language: Cpp diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..ce1bcab --- /dev/null +++ b/.containerignore @@ -0,0 +1,6 @@ +** +!lib/** +!CMakeLists.txt +!main.cc +!include.hh +!extern.cc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32d15b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +build*/ +.floo +token.dat +cmake-build-*/ +scratch.txt +env + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Created by https://www.gitignore.io/api/jetbrains+all + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### JetBrains+all Patch ### +# Ignores the whole idea folder +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# End of https://www.gitignore.io/api/jetbrains+all + +# Created by https://www.gitignore.io/api/visualstudiocode +# Edit at https://www.gitignore.io/?templates=visualstudiocode + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.gitignore.io/api/visualstudiocode + +*.log diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2ccd7cc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.16) + +project(discord-oscuro LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic") + +include(cmake/macros.cmake) +include(cmake/CPM.cmake) + +CPMAddPackage("gh:DiscordPP/discordpp#daf04ea") +CPMAddPackage("gh:DiscordPP/rest-simpleweb#4cf911b") +CPMAddPackage("gh:DiscordPP/websocket-simpleweb#9082991") +CPMAddPackage("gh:DiscordPP/plugin-native#238b7e8") +CPMAddPackage("gh:DiscordPP/plugin-overload#8862ac5") +CPMAddPackage("gh:DiscordPP/plugin-responder#3c2c485") +CPMAddPackage("gh:DiscordPP/plugin-interactionhandler#df07fd3") +CPMAddPackage("gh:DiscordPP/plugin-ratelimit#2c8cb34") +CPMAddPackage("gh:libcpr/cpr#1.10.4") + +set(DISCORDPP_USE_BOOST OFF CACHE BOOL "Override option" FORCE) + +CREATE_DISCORDPP_DEFINITIONS() +CREATE_DISCORDPP_INCLUDE() + + +set(THREADS_PREFER_PTHREAD_FLAG ON) + +if(${DISCORDPP_USE_BOOST}) + find_package(Boost 1.71.0 REQUIRED system date_time) +endif() + +find_package(Threads REQUIRED) +find_package(OpenSSL REQUIRED) + + +add_executable(${PROJECT_NAME} + source/main.cpp +) + +#target_precompile_headers(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/source/main.hpp) + +target_link_libraries(${PROJECT_NAME} + ${Boost_LIBRARIES} + Threads::Threads + ${OPENSSL_LIBRARIES} + discordpp + discordpp-rest-simpleweb + discordpp-websocket-simpleweb + discordpp-plugin-native + discordpp-plugin-overload + discordpp-plugin-responder + discordpp-plugin-interactionhandler + discordpp-plugin-ratelimit + cpr::cpr +) + +copy_file("env") + diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..7c36e91 --- /dev/null +++ b/Containerfile @@ -0,0 +1,29 @@ +FROM alpine:latest AS build + +RUN apk update +RUN apk add --no-cache \ + build-base \ + cmake \ + samurai \ + boost-dev \ + git \ + openssl-dev + +WORKDIR /echo_bot +COPY . . +RUN cmake -B build -G Ninja . +RUN cmake --build build + + +FROM alpine:latest + +RUN apk update +RUN apk add --no-cache \ + libgcc \ + libstdc++ \ + libc6-compat \ + openssl-dev + +WORKDIR /echo_bot +COPY --from=build /echo_bot/build/echo_bot . +CMD source /run/secrets/discord-oscuro-env; ./echo_bot diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4ed0e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 George Kusayko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d8d855 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Oscuro - Discord Bot (c++) + +## Note +- Create `env` file: +```sh +export BOT_TOKEN="Bot ....." +``` + +# License + +**oscuro** is licensed under the [MIT License](LICENSE). diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..842547c --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,23 @@ +set(CPM_DOWNLOAD_VERSION 0.38.1) + +if(CPM_SOURCE_CACHE) + # Expand relative path. This is important if the provided path contains a tilde (~) + get_filename_component(CPM_SOURCE_CACHE ${CPM_SOURCE_CACHE} ABSOLUTE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") + +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") + +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) + message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") + file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} + ) +endif() + +include(${CPM_DOWNLOAD_LOCATION}) \ No newline at end of file diff --git a/cmake/macros.cmake b/cmake/macros.cmake new file mode 100644 index 0000000..ee5d821 --- /dev/null +++ b/cmake/macros.cmake @@ -0,0 +1,8 @@ + +function(copy_file FILE) + if (EXISTS ${CMAKE_SOURCE_DIR}/${FILE}) + configure_file(${FILE} ${CMAKE_CURRENT_BINARY_DIR} COPYONLY) + elseif (EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${FILE}) + file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/${FILE}) + endif () +endfunction() \ No newline at end of file diff --git a/source/bot.hpp b/source/bot.hpp new file mode 100644 index 0000000..cb09ca3 --- /dev/null +++ b/source/bot.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + + +using Bot = discordpp::PluginRateLimit + >>>>>>; + +namespace dpp = discordpp; \ No newline at end of file diff --git a/source/commands.hpp b/source/commands.hpp new file mode 100644 index 0000000..d572f5c --- /dev/null +++ b/source/commands.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include "bot.hpp" + +using json = nlohmann::json; + +using event = std::function; + + +auto help(const std::shared_ptr& bot) +{ + return [&bot](const dpp::MessageCreateEvent& msg) + { + + }; +} \ No newline at end of file diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..b6a71f8 --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,218 @@ + +#include +#include +#include + +#ifdef ASIO_STANDALONE +#include +#else +#include +namespace asio = boost::asio; +#endif + +#include "bot.hpp" + +using json = nlohmann::json; + +#include +#include "utils.hpp" +#include "commands.hpp" + + +int main() { + dpp::log::filter = dpp::log::debug; + dpp::log::out = &std::cerr; + + std::cout << "Starting bot ..." << std::endl; + + std::string token = getToken(); + if (token.empty()) { + std::cerr << "Failed to read token from environment. Exiting ..." << std::endl; + exit(1); + } + + dpp::User self; + auto bot = std::make_shared(); + + bot->debugUnhandled = true; + bot->intents = dpp::intents::NONE | dpp::intents::GUILD_MESSAGES; + + bot->handlers.insert({ + "READY", + [&self](dpp::ReadyEvent ready){ self = *ready.user; } + }); + + bot->prefix = "/"; + + bot->respond("test", help(bot)); + bot->respond("help", "Mention me and I'll echo your message back!"); + + bot->respond("about", [&bot](dpp::MessageCreateEvent msg) { + std::ostringstream content; + content << "Sure thing, " + << *(msg.member->nick ? msg.author->username : msg.member->nick) + << "!\n" + << "I'm a simple bot meant to demonstrate the " + "Discord++ library.\n" + << "You can learn more about Discord++ at " + "https://discord.gg/VHAyrvspCx"; + bot->createMessage() + ->channel_id(*msg.channel_id) + ->content(content.str()) + ->run(); + }); + + bot->respond("lookatthis", [&bot](dpp::MessageCreateEvent msg) { + std::ifstream ifs("image.jpg", std::ios::binary); + if (!ifs) { + std::cerr << "Couldn't load file 'image.jpg'!\n"; + return; + } + ifs.seekg(0, std::ios::end); + std::ifstream::pos_type fileSize = ifs.tellg(); + ifs.seekg(0, std::ios::beg); + auto file = std::make_shared(fileSize, '\0'); + ifs.read(file->data(), fileSize); + + bot->createMessage() + ->channel_id(*msg.channel_id) + ->content("Look at this photograph") + ->filename("image.jpg") + ->filetype("image/jpg") + ->file(file) + ->run(); + }); + + bot->respond("notacat", [&bot](dpp::MessageCreateEvent msg) + { + cpr::Response res = cpr::Get(cpr::Url{"https://cataas.com/cat"}); + std::cout << "Fetching a cat: " << res.status_code << ", " << res.header["content-type"] << std::endl; + bot->createMessage() + ->channel_id(*msg.channel_id) + ->filename("cat.jpg") + ->filetype(res.header["content-type"]) + ->file(res.text) + ->run(); + }); + + bot->respond("channelinfo", [&bot](dpp::MessageCreateEvent msg) { + bot->getChannel() + ->channel_id(*msg.channel_id) + ->onRead([&bot, msg](bool error, json res) { + bot->createMessage() + ->channel_id(*msg.channel_id) + ->content("```json\n" + res["body"].dump(4) + "\n```") + ->run(); + }) + ->run(); + }); + + bot->respond("register", [&bot, &self](dpp::MessageCreateEvent msg) { + if (*msg.author->id == 272712928074006528) { + bot->createGuildApplicationCommand() + ->application_id(*self.id) + ->guild_id(*msg.guild_id) + ->name("echo") + ->description("Echoes what you say") + ->options({dpp::ApplicationCommandOption( + dpp::ApplicationCommandOptionType::STRING, + std::string("message"), dpp::omitted, std::string("The message to echo"), + dpp::omitted, true)}) + ->command_type(dpp::ApplicationCommandType::CHAT_INPUT) + ->onRead([](bool error, json res) { + std::cout << res.dump(4) << std::endl; + }) + ->run(); + } + + bot->createGuildApplicationCommand() + ->application_id(*self.id) + ->guild_id(*msg.guild_id) + ->name("lookatthis") + ->description("Help information") + ->options({dpp::ApplicationCommandOption( + dpp::ApplicationCommandOptionType::STRING, + std::string("message"), dpp::omitted, std::string("The message to echo"), + dpp::omitted, true)}) + ->command_type(dpp::ApplicationCommandType::CHAT_INPUT) + ->onRead([](bool error, json res) { + std::cout << res.dump(4) << std::endl; + }) + ->run(); + + }); + + bot->interactionHandlers.insert( + {1102290596896460850, [&bot](dpp::Interaction msg) { + bot->createResponse() + ->interaction_id(*msg.id) + ->interaction_token(*msg.token) + ->interaction_type( + dpp::InteractionCallbackType::CHANNEL_MESSAGE_WITH_SOURCE) + ->data({{ + "content", + *std::get(*msg.data).options->at(0).value + }}) + ->run(); + }}); + + // Create handler for the MESSAGE_CREATE payload, this receives all messages + // sent that the bot can see. + bot->handlers.insert( + {"MESSAGE_CREATE", [&bot, &self](const dpp::MessageCreateEvent msg) { + // Ignore messages from other bots + if (msg.webhook_id || (msg.author->bot && *msg.author->bot)) { + return; + } + + // Scan through mentions in the message for self + bool mentioned = false; + for (const dpp::User &mention : *msg.mentions) { + mentioned = mentioned || (*mention.id == *self.id); + } + if (mentioned) { + // Identify and remove mentions of self from the message + std::stringstream content; + content << "чё тебе надо, ";//*msg.content; + /*unsigned int oldlength, length = content.length(); + do { + oldlength = length; + content = std::regex_replace( + content, + std::regex(R"(<@!?)" + std::to_string(*self.id) + + R"(> ?)"), + ""); + length = content.length(); + } while (oldlength > length); +*/ + // Get the target user's display name + std::string name = *(msg.member->nick ? msg.member->nick + : msg.author->username); + content << name << "?"; + //std::cout << "Echoing " << name << '\n'; + + // Echo the created message + bot->createMessage() + ->channel_id(*msg.channel_id) + ->content(content.str()) + ->run(); + + // Set status to Playing "with [author]" + /*bot->send(3, + {{"game", {{"name", "with " + name}, {"type", 0}}}, + {"status", "online"}, + {"afk", false}, + {"since", "null"}});*/ + } + }}); + + + auto asioCtx = std::make_shared(); + + bot->initBot(9, token, asioCtx); + bot->run(); + + return 0; +} + + diff --git a/source/utils.hpp b/source/utils.hpp new file mode 100644 index 0000000..7ed176c --- /dev/null +++ b/source/utils.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + + +/*/ + * Source: https://stackoverflow.com/a/6089413/1526048 +/*/ +std::istream &safeGetline(std::istream& is, std::string& str) +{ + str.clear(); + + // The characters in the stream are read one-by-one using a std::streambuf. + // That is faster than reading them one-by-one using the std::istream. + // Code that uses streambuf this way must be guarded by a sentry object. + // The sentry object performs various tasks, + // such as thread synchronization and updating the stream state. + + std::istream::sentry se(is, true); + std::streambuf *sb = is.rdbuf(); + + for (;;) + { + int c = sb->sbumpc(); + + switch (c) + { + case '\n': + return is; + + case '\r': + if (sb->sgetc() == '\n') { + sb->sbumpc(); + } + return is; + + case std::streambuf::traits_type::eof(): + // Also handle the case when the last line has no line ending + if (str.empty()) + is.setstate(std::ios::eofbit); + + return is; + + default: + str += (char)c; + } + } +} + +std::string getToken(const std::string& filename = "") +{ + std::string token; + char const *env = std::getenv("BOT_TOKEN"); + + if (env != nullptr) + { + token = std::string(env); + } + else + { + std::ifstream tokenFile(filename); + + if (!tokenFile) + return ""; + + safeGetline(tokenFile, token); + tokenFile.close(); + } + + return token; +} + +