From 3d01c4d8b0bae0ec3b30e992c8aeaeee7c52148a Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Sun, 10 Sep 2023 15:14:18 +0100 Subject: [PATCH 1/7] docs: cleaned up windows_wsl.md (#851) Co-authored-by: Jakub 'Eremiell' Marek --- docpages/make_a_bot/windows_wsl.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docpages/make_a_bot/windows_wsl.md b/docpages/make_a_bot/windows_wsl.md index d811af9bd0..455073bb20 100644 --- a/docpages/make_a_bot/windows_wsl.md +++ b/docpages/make_a_bot/windows_wsl.md @@ -1,18 +1,19 @@ -\page build-a-discord-bot-windows-wsl Building a discord bot in Windows using WSL (Windows Subsystem for Linux) +\page build-a-discord-bot-windows-wsl Building a Discord Bot on Windows Using WSL (Windows Subsystem for Linux) -This tutorial teaches you how to create a lightweight environment for D++-development using **WSL** and **Visual Studio Code** +This tutorial teaches you how to create a lightweight environment for D++ development using **WSL** and **Visual Studio Code** -This Tutorial will use WSL's default distribution, **Ubuntu**! You might use other Distros if you prefer, but keep in mind the setup process might be different! +\note **This Tutorial will use WSL's default distribution, Ubuntu**! You can use other distros if you wish, **but keep in mind the setup process might be different!** If you're aiming for production, we recommend you continue your path of becoming the master of all Discord bots \ref buildcmake "by visiting this page", otherwise keep following this guide! 1. Make sure you have installed your WSL 2 environment properly using [this guide to setup up WSL](https://docs.microsoft.com/en-us/windows/wsl/install) and [this guide to connect to Visual Studio Code](https://docs.microsoft.com/en-us/windows/wsl/tutorials/wsl-vscode). -2. Now open PowerShell as an Admin and type `wsl` to start up your subsystem. If you want to set up a CMake project (recommended for production bots) now, consider continuing your path of becoming the master of all Discord bots \ref buildcmake "here", otherwise keep following this guide! -3. Go to your home directory using `cd ~` -4. Download the latest build for your Distro using `wget [url here]`. In this guide we will use the latest build for 64 bit Ubuntu: `wget -O libdpp.deb https://dl.dpp.dev/latest` -5. Finally install all required deps and the library using `sudo apt-get install libopus0 && sudo apt-get install -y libopus-dev && sudo apt-get install -y libsodium-dev && sudo dpkg -i libdpp.deb && rm libdpp.deb` +2. Now open PowerShell as Administrator and type `wsl` to start up your subsystem. You may also type `ubuntu` into your search bar and open it that way. +3. Head on over to your home directory using `cd ~`. +4. Download the latest build for your distro using `wget [url here]`. In this guide we will use the latest build for 64 bit Ubuntu: `wget -O libdpp.deb https://dl.dpp.dev/latest`. +5. Finally install all required dependencies and the library itself using `sudo apt-get install libopus0 libopus-dev libsodium-dev && sudo dpkg -i libdpp.deb && rm libdpp.deb`. 6. Congratulations, you've successfully installed all dependencies! Now comes the real fun: Setting up the environment! For this tutorial we'll use a as small as possible setup, so you might create a more advanced one for production bots. -7. Navigate to a folder of your choice using `cd your/path/here` or create a new directory using `mkdir MyBot && cd MyBot` -8. Now that you've a folder to work in type `> mybot.cxx` to create a file you can work in! -9. Now you can open this file in Visual Studio Code by pressing `CTRL+SHIFT+P` and typing `Remote-WSL: New WSL Window`. This will bring up a new window. In the new window, choose `open folder` and choose the folder you've created prior. Press OK and now you have your Folder opened as a Workspace! -10. Add code to your CXX file and compile it by running `g++ -std=c++17 *.cxx -o bot -ldpp` in the same folder as your cxx file. -11. start your bot by typing `./bot`! +7. Create a new directory, inside your home directory, using `mkdir MyBot`. Then, you want to open that directory using `cd MyBot`. +8. Now that you've a directory to work in, type `touch mybot.cxx` to create a file you can work in! +9. Now, head on over to Visual Studio Code. Press `CTRL+SHIFT+P` and type `Remote-WSL: New WSL Window` (You don't have to type all of it, it will auto-suggest it!). This will bring up a new window. In the new window, choose `open folder` and choose the directory you've created prior (It should be within your home directory). Press OK and now you have your Folder opened as a Workspace! +10. Add code to your CXX file (We suggest using the \ref firstbot "first bot page" if this is your first time!) and compile it by running `g++ -std=c++17 *.cxx -o bot -ldpp` in the same folder as your cxx file. This will create a "bot" file! +11. You can now start your bot by typing `./bot`! +If everything was done right, you should be able to see your bot working! From 1a9a492564b9f5ccc3a5ba2732e6ff75fd91226d Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Wed, 13 Sep 2023 22:33:18 +0100 Subject: [PATCH 2/7] docs: created a page about making threads (channel threads) (#850) --- .../interactions_and_components.md | 1 + .../making_threads.md | 115 ++++++++++++++++++ docpages/images/creating_thread.png | Bin 0 -> 39925 bytes docpages/images/creating_thread_2.png | Bin 0 -> 87795 bytes 4 files changed, 116 insertions(+) create mode 100644 docpages/example_programs/interactions_and_components/making_threads.md create mode 100644 docpages/images/creating_thread.png create mode 100644 docpages/images/creating_thread_2.png diff --git a/docpages/example_programs/interactions_and_components.md b/docpages/example_programs/interactions_and_components.md index 23fe5411e6..e371a6d792 100644 --- a/docpages/example_programs/interactions_and_components.md +++ b/docpages/example_programs/interactions_and_components.md @@ -15,3 +15,4 @@ The example programs listed here demonstrate lots of things to do with interacti * \subpage application-command-autocomplete "Slash command auto completion" * \subpage discord-application-command-file-upload "Using file parameters in slash commands" * \subpage private-messaging +* \subpage making_threads diff --git a/docpages/example_programs/interactions_and_components/making_threads.md b/docpages/example_programs/interactions_and_components/making_threads.md new file mode 100644 index 0000000000..25822e0ba6 --- /dev/null +++ b/docpages/example_programs/interactions_and_components/making_threads.md @@ -0,0 +1,115 @@ +\page making_threads Creating and talking in a thread + +A new feature added to Discord recently is `Threads`, these allow you to break off a message into a different "channel", without creating a whole new channel. There are also other types of "thread channels", one example being a `forums channel`. This type of channel only contains threads, meaning you can't send messages in it so if you want to make one of them, be careful about trying to send a message in it! + +In this tutorial, we'll be going through how to create a thread and how to talk in a thread. + +First, let's go through creating a thread. +~~~~~~~~~~{.cpp} +#include + +int main() +{ + /* Create the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when the bot detects a message in any server and any channel it has access to. */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "create-thread") { + /* Here we create a thread in the current channel. It will expire after 60 minutes of inactivity. We'll also allow other mods to join, and we won't add a slowdown timer. */ + bot.thread_create("Cool thread!", event.command.channel_id, 60, dpp::channel_type::CHANNEL_PUBLIC_THREAD, true, 0, [event](const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) { + event.reply("Failed to create a thread!"); + return; + } + + event.reply("Created a thread for you!"); + }); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + /* Create and register the command */ + bot.global_command_create(dpp::slashcommand("create-thread", "Create a thread!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} +~~~~~~~~~~ + +If all went well, you'll see that the bot has successfully created a thread! + +\image html creating_thread.png + +Now, let's cover talking in that thread from a channel. It's worth noting that we will be assuming that the thread you just created is the only thread in your server! +~~~~~~~~~~{.cpp} + +#include + +int main() +{ + /* Create the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when the bot detects a message in any server and any channel it has access to. */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "message-thread") { + /* Get all active threads in a guild. */ + bot.threads_get_active(event.command.guild_id, [&bot, event](const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) { + event.reply("Failed to get threads!"); + return; + } + + /* Get the list of active threads in the guild. */ + auto threads = callback.get(); + + dpp::snowflake thread_id; + + /* Loop through the threads, getting each value in the map. Then we get the first value and then break off. + * The reason we're getting only the first value is because, for this example, we'll just assume you've only got a single active thread (the one created by the bot) + */ + for (const auto& _thread : threads) { + thread_id = _thread.first; + break; + } + + /* Send a message in the first thread we find. */ + bot.message_create(dpp::message(thread_id, "Hey, I'm first to message in a cool thread!"), [event](const dpp::confirmation_callback_t& callback2) { + if (callback2.is_error()) { + event.reply("Failed to send a message in a thread."); + return; + } + + event.reply("I've sent a message in the specified thread."); + }); + }); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + /* Create and register the command */ + bot.global_command_create(dpp::slashcommand("message-thread", "Message a thread!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} +~~~~~~~~~~ + +After that, you'll be able to see your bot send a message in your thread! + +\image html creating_thread_2.png \ No newline at end of file diff --git a/docpages/images/creating_thread.png b/docpages/images/creating_thread.png new file mode 100644 index 0000000000000000000000000000000000000000..da0cefe591173af4b6b02fe672228dcd91d6d6e0 GIT binary patch literal 39925 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 bFTT;<9PKH8C1F#kxq4f$8Omx4a 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±m7zos_2^bIy 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 GLK6T8XUx6; literal 0 HcmV?d00001 From b5034766cc7b5fe59635f246da3ead9a23367087 Mon Sep 17 00:00:00 2001 From: Neko Life Date: Thu, 14 Sep 2023 04:50:39 +0700 Subject: [PATCH 3/7] fix: wrong size assignment (#853) --- src/dpp/discordvoiceclient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dpp/discordvoiceclient.cpp b/src/dpp/discordvoiceclient.cpp index efe729fe82..4486ad7f72 100644 --- a/src/dpp/discordvoiceclient.cpp +++ b/src/dpp/discordvoiceclient.cpp @@ -826,8 +826,8 @@ void discord_voice_client::write_ready() if (outbuf.size()) { if (this->udp_send(outbuf[0].packet.data(), outbuf[0].packet.length()) == (int)outbuf[0].packet.length()) { duration = outbuf[0].duration * timescale; + bufsize = outbuf[0].packet.length(); outbuf.erase(outbuf.begin()); - bufsize = outbuf.size(); } } } From faf4cbc5247e8c27bc596aba2000d3b962087d90 Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Thu, 14 Sep 2023 07:57:21 +0100 Subject: [PATCH 4/7] docs: merged coding_style_standards.md with #820 (#852) Co-authored-by: Jakub 'Eremiell' Marek --- docpages/06_advanced_reference.md | 2 +- .../coding_style_standards.md | 120 ++++++++++++------ 2 files changed, 79 insertions(+), 43 deletions(-) diff --git a/docpages/06_advanced_reference.md b/docpages/06_advanced_reference.md index 7cd4009216..4c2f5cf419 100644 --- a/docpages/06_advanced_reference.md +++ b/docpages/06_advanced_reference.md @@ -2,7 +2,7 @@ * \subpage clusters-shards-guilds "Clusters, Shards and Guilds" * \subpage thread-model "Thread Model" -* \subpage coding-standards "Coding Style Standards" +* \subpage coding-standards * \subpage unit-tests "Unit Tests" * \subpage lambdas-and-locals "Ownership of local variables and safely transferring into a lambda" * \subpage governance "Project Governance" diff --git a/docpages/advanced_reference/coding_style_standards.md b/docpages/advanced_reference/coding_style_standards.md index f74ad16c99..4fb733cc17 100644 --- a/docpages/advanced_reference/coding_style_standards.md +++ b/docpages/advanced_reference/coding_style_standards.md @@ -1,29 +1,43 @@ \page coding-standards Coding Style Standards -This page lists the coding style we stick to when maintaining the D++ library. If you are submitting a pull request or other code contribution to the library, you should stick to the styles listed below. If something is not covered here, ask on the [official discord server](https://discord.gg/dpp)! +This page lists the coding style we stick to when maintaining the D++ library. If you are submitting a pull request or other code contribution to the library, you should stick to the styles listed below. If something is not covered here, ask on the [official Discord server](https://discord.gg/dpp)! + +## Class Names, Function Names, and Method Names -## Class names, function names and method names All class, variable/member, function and method names should use `snake_case`, similar to the style of the C++ standard library. ## Enums -Enums and their values should be `snake_case` as with class, function and method names. You do not need to use `enum class`, so make sure that enum values are prefixed with a prefix to make them unique and grouped within the IDE, e.g. `ll_debug`, `ll_trace` etc. +Enums and their values should be `snake_case` as with class, function and method names. You do not need to use `enum class`, so make sure that enum values are prefixed with a prefix to make them unique and grouped within the IDE, e.g. `ll_debug`, `ll_trace`, etc. + +## Curly Braces, Brackets, etc. -## Curly Braces, Brackets etc -This covers your standard Curly Braces (commonly known as squiggly brackets), and Lists. +This covers your standard Curly Braces (commonly known as squiggly brackets), and lists. ### Curly Braces + Curly Braces should be on the same line as the keyword, for example: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp void foo() { - if (a == b) { - c(); - } - - while(true) { - // ... - } + if (a == b) { + c(); + } else { + d(); + } + + while (true) { + // ... + } + + switch (a) { + case 1: + c(); + break; + case 2: + d(); + break; + } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -33,28 +47,42 @@ This applies to functions, `while` statements, `if` statements, lambdas, nearly Lists should have a space after the comma in parameter lists, and after opening brackets and before closing brackets except when calling a function, for example: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp std::vector clowns = { "pennywise", "bobo" }; evaluate_clown(clowns[0], evilness(2.5, factor)); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +## Dot (.) Notation +When using the dot notation repeatedly (For example, creating an embed.), you should start each `.function()` on a new line, as such: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp +stuff{} + .add_stuff() + .add_stuff(); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ## Indentation + Indentation should always be tab characters. It is up to you how wide you set tab characters in your editor for your personal tastes. All code blocks delimited within curly braces should be indented neatly and uniformly. -## Constants and \#define macros +## Constants and \#define Macros + Constants and macros should be all `UPPERCASE` with `SNAKE_CASE` to separate words. Macros should not have any unexpected side effects. ## Comments + All comments should be in `doxygen` format (similar to javadoc). Please see existing class definitions for an example. You should use doxygen style comments in a class definition inside a header file, and can use any other comment types within the .cpp file. Be liberal with comments, especially if your code makes any assumptions! -## Spell checks +## Spell Checks + To prevent typos, a GitHub-Action checks the documentation. If it fails for a word that was falsely flagged, you can add them to `.cspell.json`. -## Symbol exporting +## Symbol Exporting + If you export a class which is to be accessible to users, be sure to prefix it with the `DPP_EXPORT` macro, for example: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp class DPP_EXPORT my_new_class { public: int hats; @@ -64,38 +92,47 @@ public: The `DPP_EXPORT` macro ensures that on certain platforms (notably Windows) the symbol is exported to be available to the library user. -## Public vs private vs protected +## Public vs Private vs Protected + It is a design philosophy of D++ that everything possible in a class should be public, unless the user really does not need it (you should consider justifying in comments why) or user adjustment of the variable could badly break the functioning of the library. Avoid the use of accessors for setting/getting values in a class, except for bit fields, where you should provide accessors for setting and getting individual bits (for example, see `user.h`), or in the event you want to provide a "fluent" interface. The exception to this is where you want to provide a logic validation of a field, for example if you have a string field with a minimum and maximum length, you can provide a setter the user can *optionally use* which will validate their input. ## Exceptions -All exceptions thrown should derive from dpp::exception (see dpp/exception.h) - when validating string lengths, a string which is too long should be truncated using dpp::utility::utf8substr and any strings that are too short should throw a dpp::length_exception. + +All exceptions thrown should derive from dpp::exception (see `dpp/exception.h`) - when validating string lengths, a string which is too long should be truncated using dpp::utility::utf8substr and any strings that are too short should throw a dpp::length_exception. ## Inheritance + Keep levels of inheritance low. If you need to inherit more than 3 levels deep, it is probable that the design could be simplified. Remember that at scale, there can be tens of millions of certain classes and each level of virtual nesting adds to the `vtable` of that object's instance in RAM. -## Bit field packing -Where discord provides boolean flags, if the user is expected to store many of the object in RAM, or in cache, you should pack all these booleans into bit fields (see `user.h` and `channel.h` for examples). In the event that the object is transient, such as an interaction or a message, packing the data into bit fields is counter intuitive. Remember that you should provide specific accessors for bit field values! +## Bit Field Packing + +Where Discord provides boolean flags, if the user is expected to store many of the objects in RAM, or in cache, you should pack all these booleans into bit fields (see `user.h` and `channel.h` for examples). In the event that the object is transient, such as an interaction or a message, packing the data into bit fields is counter intuitive. Remember that you should provide specific accessors for bit field values! + +## Keep Dependencies Internal! -## Keep dependencies internal! Where you are making use of an external dependency such as `opus` or `libssl`, do not place references to the types/structs, or the header files of these external libraries within the header files of D++. Doing so adds that library as a public dependency to the project (which is bad!). Instead make an opaque class, and/or forward-declare the structs (for examples see `sslclient.h` and `discordvoiceclient.h`). -## API type names -Where discord provide a name in PascalCase we should stick as closely to that name as possible but convert it to `snake_case`. For example, GuildMember would become `guild_member`. +## API Type Names + +Where Discord provides a name in PascalCase we should stick as closely to that name as possible but convert it to `snake_case`. For example, GuildMember would become `guild_member`. -## Don't introduce any platform-specific code -Do not introduce platform specific (e.g. windows only) code or libc functions. If you really must use these functions safely wrap them e.g. in `#ifdef _WIN32` and provide a cross-platform alternative so that it works for everyone. +## Don't Introduce Any Platform-Specific Code -## C++ version +Do not introduce platform specific (e.g. Windows only) code or libc functions. If you really must use these functions safely wrap them e.g. in `#ifdef _WIN32` and provide a cross-platform alternative so that it works for everyone. -The code must work with the C++17 standard. +## C++ Version -## Select the right size type for numeric types -If a value will only hold values up to 255, use `uint8_t`. If a value cannot hold over 65536, use `uint16_t`. These types can help use a lot less ram at scale. +The code must work with the C++17 standard, unless, for an optional feature that can be enabled and that uses a more recent standard (e.g. Coroutines). -## Fluent design -Where possible, if you are adding methods to a class you should consider fluent design. Fluent design is the use of class methods tha return a reference to self (via `return *this`), so that you can chain object method calls together (in the way `dpp::message` and `dpp::embed` do). For example: +## Select the Right Size Type for Numeric Types -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +If a value will only hold values up to 255, use `uint8_t`. If a value cannot hold over 65536, use `uint16_t`. These types can help use a lot less RAM at scale. + +## Fluent Design + +Where possible, if you are adding methods to a class you should consider fluent design. Fluent design is the use of class methods that return a reference to self (via `return *this`), so that you can chain object method calls together (in the way dpp::message and dpp::embed do). For example: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp class DPP_EXPORT my_new_class { public: int hats; @@ -114,27 +151,26 @@ my_new_class& my_new_class::set_clowns(int new_clowns) { clowns = new_clowns; return *this; } - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This would allow the user to do this: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp dpp::my_new_class nc; nc.set_hats(3).set_clowns(9001); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -## Keep all D++ related types in the dpp namespace +## Keep All D++ Related Types in the dpp Namespace -All types for the library should be within the `dpp` namespace. There are a couple of additional namespaces, e.g. `dpp::utility` for static standalone helper functions and helper classes, and `dpp::events` for internal websocket event handlers. +All types for the library should be within the `dpp` namespace. There are a couple of additional namespaces, e.g. dpp::utility for static standalone helper functions and helper classes, and dpp::events for internal websocket event handlers. -## Commit messages and Git +## Commit Messages and Git All pull requests ("PRs") should be submitted against the `dev` branch in GitHub. -### Naming conventions +### Naming Conventions -It’s good to have descriptive commit messages, or PR titles so that other contributors can understand about your commit or the PR Created. Commits must be prefixed with a type, which consists of a noun, `feat`, `fix`, etc., followed by a colon and a space. Other commit types can be `breaking`, `docs`, `refactor`, `deprecate`, `perf`, `test`, `chore` and `misc`. Read [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.3/) for more information on how we like to format commit messages. +It’s good to have descriptive commit messages, or PR titles so that other contributors can understand about your commit or the PR Created. Commits must be prefixed with a type, which consists of a noun, `feat`, `fix`, etc., followed by a colon and a space. Other commit types can be `breaking`, `docs`, `refactor`, `deprecate`, `perf`, `test`, `chore`, and `misc`. Read [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.3/) for more information on how we like to format commit messages. ### GitHub Actions @@ -175,4 +211,4 @@ By making a contribution to this project, I certify that: personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. -``` \ No newline at end of file +``` From fd3f87522a3647670e7f95afbf27b5ab92af0fe9 Mon Sep 17 00:00:00 2001 From: Brain Date: Thu, 14 Sep 2023 20:35:15 +0100 Subject: [PATCH 5/7] docs: add auto testing of examples (#854) --- .github/workflows/test-docs-examples.yml | 45 ++++ .gitignore | 3 +- Doxyfile | 6 +- docpages/example_code/CMakeLists.txt | 45 ++++ docpages/example_code/attachments1.cpp | 34 +++ docpages/example_code/attachments2.cpp | 41 ++++ docpages/example_code/attachments3.cpp | 41 ++++ docpages/example_code/autocomplete.cpp | 67 ++++++ docpages/example_code/cache_messages.cpp | 60 ++++++ docpages/example_code/callbacks.cpp | 88 ++++++++ docpages/example_code/collect_reactions.cpp | 46 +++++ docpages/example_code/commandhandler.cpp | 53 +++++ docpages/example_code/components.cpp | 57 +++++ docpages/example_code/components2.cpp | 62 ++++++ docpages/example_code/components3.cpp | 57 +++++ docpages/example_code/context_menus.cpp | 39 ++++ .../example_code/coro_awaiting_events.cpp | 39 ++++ .../example_code/coro_expiring_buttons.cpp | 43 ++++ docpages/example_code/coro_intro.cpp | 35 ++++ .../example_code/coro_simple_commands1.cpp | 63 ++++++ .../example_code/coro_simple_commands2.cpp | 82 ++++++++ docpages/example_code/detecting_messages.cpp | 24 +++ docpages/example_code/editing_messages.cpp | 73 +++++++ docpages/example_code/embeds.cpp | 56 +++++ docpages/example_code/ephemeral.cpp | 32 +++ docpages/example_code/eval.cpp | 175 ++++++++++++++++ docpages/example_code/eval.h | 14 ++ docpages/example_code/firstbot.cpp | 25 +++ docpages/example_code/firstbot1.cpp | 4 + docpages/example_code/firstbot2.cpp | 7 + docpages/example_code/firstbot3.cpp | 13 ++ docpages/example_code/firstbot4.cpp | 16 ++ docpages/example_code/firstbot5.cpp | 20 ++ docpages/example_code/firstbot6.cpp | 23 +++ docpages/example_code/http_request.cpp | 28 +++ docpages/example_code/join_voice.cpp | 87 ++++++++ docpages/example_code/making_threads1.cpp | 36 ++++ docpages/example_code/making_threads2.cpp | 57 +++++ .../modal_dialog_interactions.cpp | 73 +++++++ docpages/example_code/mp3.cpp | 117 +++++++++++ docpages/example_code/oggopus.cpp | 117 +++++++++++ docpages/example_code/private_messaging.cpp | 66 ++++++ docpages/example_code/record_user.cpp | 72 +++++++ docpages/example_code/setting_status1.cpp | 20 ++ docpages/example_code/setting_status2.cpp | 26 +++ docpages/example_code/slashcommands1.cpp | 47 +++++ docpages/example_code/slashcommands2.cpp | 47 +++++ docpages/example_code/slashcommands3.cpp | 41 ++++ docpages/example_code/slashcommands4.cpp | 42 ++++ docpages/example_code/soundboard.cpp | 85 ++++++++ docpages/example_code/subcommands.cpp | 72 +++++++ docpages/example_code/upload_parameter.cpp | 43 ++++ docpages/example_code/webhooks.cpp | 16 ++ .../autocomplete.md | 71 +------ .../commandhandler.md | 57 +---- .../interactions_and_components/components.md | 60 +----- .../components2.md | 65 +----- .../components3.md | 60 +----- .../context_menus.md | 42 +--- .../detecting-messages.md | 27 +-- .../making_threads.md | 98 +-------- .../modal_dialog_interactions.md | 76 +------ .../private-messaging.md | 69 +------ .../slashcommands.md | 189 +---------------- .../subcommands.md | 75 +------ .../upload_parameter.md | 46 +---- .../user-only-messages.md | 35 +--- .../example_programs/misc/cache_messages.md | 64 +----- .../misc/collect_reactions.md | 49 +---- docpages/example_programs/misc/eval.md | 195 +----------------- .../example_programs/misc/http_request.md | 30 +-- .../example_programs/misc/setting_status.md | 52 +---- .../music_and_audio/join_voice.md | 91 +------- .../example_programs/music_and_audio/mp3.md | 122 +---------- .../music_and_audio/oggopus.md | 120 +---------- .../music_and_audio/record_user.md | 76 +------ .../music_and_audio/soundboard.md | 88 +------- .../the_basics/attachments.md | 125 +---------- .../editing-channels-and-messages.md | 76 +------ .../example_programs/the_basics/embeds.md | 59 +----- .../example_programs/the_basics/firstbot.md | 133 +----------- .../the_basics/using_callback_functions.md | 91 +------- .../example_programs/the_basics/webhooks.md | 19 +- .../using_coroutines/awaiting_events.md | 42 +--- .../using_coroutines/coro_introduction.md | 39 +--- .../using_coroutines/coro_simple_commands.md | 151 +------------- .../using_coroutines/expiring_buttons.md | 46 +---- 87 files changed, 2627 insertions(+), 2591 deletions(-) create mode 100644 .github/workflows/test-docs-examples.yml create mode 100644 docpages/example_code/CMakeLists.txt create mode 100644 docpages/example_code/attachments1.cpp create mode 100644 docpages/example_code/attachments2.cpp create mode 100644 docpages/example_code/attachments3.cpp create mode 100644 docpages/example_code/autocomplete.cpp create mode 100644 docpages/example_code/cache_messages.cpp create mode 100644 docpages/example_code/callbacks.cpp create mode 100644 docpages/example_code/collect_reactions.cpp create mode 100644 docpages/example_code/commandhandler.cpp create mode 100644 docpages/example_code/components.cpp create mode 100644 docpages/example_code/components2.cpp create mode 100644 docpages/example_code/components3.cpp create mode 100644 docpages/example_code/context_menus.cpp create mode 100644 docpages/example_code/coro_awaiting_events.cpp create mode 100644 docpages/example_code/coro_expiring_buttons.cpp create mode 100644 docpages/example_code/coro_intro.cpp create mode 100644 docpages/example_code/coro_simple_commands1.cpp create mode 100644 docpages/example_code/coro_simple_commands2.cpp create mode 100644 docpages/example_code/detecting_messages.cpp create mode 100644 docpages/example_code/editing_messages.cpp create mode 100644 docpages/example_code/embeds.cpp create mode 100644 docpages/example_code/ephemeral.cpp create mode 100644 docpages/example_code/eval.cpp create mode 100644 docpages/example_code/eval.h create mode 100644 docpages/example_code/firstbot.cpp create mode 100644 docpages/example_code/firstbot1.cpp create mode 100644 docpages/example_code/firstbot2.cpp create mode 100644 docpages/example_code/firstbot3.cpp create mode 100644 docpages/example_code/firstbot4.cpp create mode 100644 docpages/example_code/firstbot5.cpp create mode 100644 docpages/example_code/firstbot6.cpp create mode 100644 docpages/example_code/http_request.cpp create mode 100644 docpages/example_code/join_voice.cpp create mode 100644 docpages/example_code/making_threads1.cpp create mode 100644 docpages/example_code/making_threads2.cpp create mode 100644 docpages/example_code/modal_dialog_interactions.cpp create mode 100644 docpages/example_code/mp3.cpp create mode 100644 docpages/example_code/oggopus.cpp create mode 100644 docpages/example_code/private_messaging.cpp create mode 100644 docpages/example_code/record_user.cpp create mode 100644 docpages/example_code/setting_status1.cpp create mode 100644 docpages/example_code/setting_status2.cpp create mode 100644 docpages/example_code/slashcommands1.cpp create mode 100644 docpages/example_code/slashcommands2.cpp create mode 100644 docpages/example_code/slashcommands3.cpp create mode 100644 docpages/example_code/slashcommands4.cpp create mode 100644 docpages/example_code/soundboard.cpp create mode 100644 docpages/example_code/subcommands.cpp create mode 100644 docpages/example_code/upload_parameter.cpp create mode 100644 docpages/example_code/webhooks.cpp diff --git a/.github/workflows/test-docs-examples.yml b/.github/workflows/test-docs-examples.yml new file mode 100644 index 0000000000..64a3044722 --- /dev/null +++ b/.github/workflows/test-docs-examples.yml @@ -0,0 +1,45 @@ +name: Test compile documentation examples +on: + push: + branches: + - 'dev' + files: + - '**Doxyfile' + - '**docpages/example_code/**' + pull_request: + files: + - '**Doxyfile' + - '**docpages/example_code/**' + workflow_dispatch: + +jobs: + test_docs_examples: + name: Test build examples + runs-on: ubuntu-22.04 + + steps: + - name: Harden Runner + uses: step-security/harden-runner@8ca2b8b2ece13480cda6dacd3511b49857a23c09 # v2.5.1 + with: + egress-policy: audit + + - name: Checkout D++ + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + with: + submodules: recursive + + - name: Install apt packages + run: sudo sed -i 's/azure\.//' /etc/apt/sources.list && sudo apt-get update && sudo apt-get install -y g++-12 libsodium-dev libopus-dev zlib1g-dev libmpg123-dev liboggz-dev cmake libfmt-dev + + - name: Generate CMake + run: mkdir build && cd build && cmake -DDPP_NO_VCPKG=ON -DAVX_TYPE=T_fallback -DDPP_CORO=ON -DCMAKE_BUILD_TYPE=Debug .. + env: + CXX: g++-12 + + - name: Build Project + run: cd build && make -j2 && sudo make install + + - name: Test compile examples + run: cd docpages/example_code && mkdir build && cd build && cmake .. && make -j2 + env: + CXX: g++-12 diff --git a/.gitignore b/.gitignore index b641ee933f..21df953333 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build buildtools/composer.phar src/build cmake-build-debug +docpages/example_code/build # tests test @@ -28,4 +29,4 @@ core config.json .misspell-fixer.ignore compile_commands.json -src/dpp/dpp.rc \ No newline at end of file +src/dpp/dpp.rc diff --git a/Doxyfile b/Doxyfile index 18dbbc539c..e0098049d0 100644 --- a/Doxyfile +++ b/Doxyfile @@ -790,7 +790,8 @@ EXCLUDE = deps \ build \ include/dpp/format \ include/dpp/nlohmann \ - docpages/include + docpages/include \ + docpages/example_code # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded @@ -830,7 +831,8 @@ EXCLUDE_SYMBOLS = nlohmann::* \ # that contain example code fragments that are included (see the \include # command). -EXAMPLE_PATH =docpages/include +EXAMPLE_PATH = docpages/include \ + docpages/example_code # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and diff --git a/docpages/example_code/CMakeLists.txt b/docpages/example_code/CMakeLists.txt new file mode 100644 index 0000000000..2f127da942 --- /dev/null +++ b/docpages/example_code/CMakeLists.txt @@ -0,0 +1,45 @@ +# +# D++ (DPP), The Lightweight C++ Discord Library +# +# Copyright 2021 Craig Edwards +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example programs test compilation +# This build script is executed by a GitHub action to ensure all example +# programs compile correctly. It does not attempt to run them, as there +# is no way to know if the program successfully did its thing, plus +# examples do not have a valid token. This build script assumes the +# following system dependencies are available: +# +# g++-12 or later +# liboggz-dev +# libmpg123-dev +# dpp latest master with -DDPP_CORO=ON installed sytemwide + +cmake_minimum_required (VERSION 3.16) +project(documentation_tests) + +string(ASCII 27 Esc) + +set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDPP_CORO -std=c++20 -pthread -O0 -fPIC -rdynamic -DFMT_HEADER_ONLY -Wall -Wextra -Wpedantic -Werror -Wno-unused-parameter") +set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0") + +file(GLOB example_list ./*.cpp) +foreach (example ${example_list}) + get_filename_component(examplename ${example} NAME) + message(STATUS "Found example '${Esc}[1;34m${examplename}${Esc}[m'") + add_executable(${examplename}_out ${example}) + target_link_libraries(${examplename}_out dl dpp mpg123 oggz) +endforeach(example) diff --git a/docpages/example_code/attachments1.cpp b/docpages/example_code/attachments1.cpp new file mode 100644 index 0000000000..98294e24fd --- /dev/null +++ b/docpages/example_code/attachments1.cpp @@ -0,0 +1,34 @@ +#include + +int main() { + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "file") { + + dpp::message msg(event.command.channel_id, "Hey there, I've got a new file!"); + + /* attach the file to the message */ + msg.add_file("foobar.txt", dpp::utility::read_file("path_to_your_file.txt")); + + /* Reply to the user with the message, with our file attached. */ + event.reply(msg); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Create and register a command when the bot is ready */ + bot.global_command_create(dpp::slashcommand("file", "Send a message with a file attached!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/attachments2.cpp b/docpages/example_code/attachments2.cpp new file mode 100644 index 0000000000..2bef4d0cc8 --- /dev/null +++ b/docpages/example_code/attachments2.cpp @@ -0,0 +1,41 @@ +#include + +int main() { + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "file") { + + /* Request the image from the URL specified and capture the event in a lambda. */ + bot.request("https://dpp.dev/DPP-Logo.png", dpp::m_get, [event](const dpp::http_request_completion_t & httpRequestCompletion) { + + /* Create a message */ + dpp::message msg(event.command.channel_id, "This is my new attachment:"); + + /* Attach the image to the message, only on success (Code 200). */ + if (httpRequestCompletion.status == 200) { + msg.add_file("logo.png", httpRequestCompletion.body); + } + + /* Send the message, with our attachment. */ + event.reply(msg); + }); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Create and register a command when the bot is ready */ + bot.global_command_create(dpp::slashcommand("file", "Send a message with an image attached from the internet!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/attachments3.cpp b/docpages/example_code/attachments3.cpp new file mode 100644 index 0000000000..7b8b673cef --- /dev/null +++ b/docpages/example_code/attachments3.cpp @@ -0,0 +1,41 @@ +#include + +int main() { + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "file") { + + /* Create a message. */ + dpp::message msg(event.command.channel_id, ""); + + /* Attach the image to the message we just created. */ + msg.add_file("image.jpg", dpp::utility::read_file("path_to_your_image.jpg")); + + /* Create an embed. */ + dpp::embed embed; + embed.set_image("attachment://image.jpg"); /* Set the image of the embed to the attached image. */ + + /* Add the embed to the message. */ + msg.add_embed(embed); + + event.reply(msg); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Create and register a command when the bot is ready */ + bot.global_command_create(dpp::slashcommand("file", "Send a local image along with an embed with the image!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/autocomplete.cpp b/docpages/example_code/autocomplete.cpp new file mode 100644 index 0000000000..d0204abdb2 --- /dev/null +++ b/docpages/example_code/autocomplete.cpp @@ -0,0 +1,67 @@ +#include + +int main() +{ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new global command once on ready event */ + bot.global_command_create(dpp::slashcommand("blep", "Send a random adorable animal photo", bot.me.id) + .add_option( + /* If you set the auto complete setting on a command option, it will trigger the on_autocomplete + * event whenever discord needs to fill information for the choices. You cannot set any choices + * here if you set the auto complete value to true. + */ + dpp::command_option(dpp::co_string, "animal", "The type of animal").set_auto_complete(true) + ) + ); + } + }); + + /* The interaction create event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "blep") { + /* Fetch a parameter value from the command parameters */ + std::string animal = std::get(event.get_parameter("animal")); + /* Reply to the command. There is an overloaded version of this + * call that accepts a dpp::message so you can send embeds. + */ + event.reply("Blep! You chose " + animal); + } + }); + + /* The on_autocomplete event is fired whenever discord needs information to fill in a command options's choices. + * You must reply with a REST event within 500ms, so make it snappy! + */ + bot.on_autocomplete([&bot](const dpp::autocomplete_t & event) { + for (auto & opt : event.options) { + /* The option which has focused set to true is the one the user is typing in */ + if (opt.focused) { + /* In a real world usage of this function you should return values that loosely match + * opt.value, which contains what the user has typed so far. The opt.value is a variant + * and will contain the type identical to that of the slash command parameter. + * Here we can safely know it is string. + */ + std::string uservalue = std::get(opt.value); + bot.interaction_response_create(event.command.id, event.command.token, dpp::interaction_response(dpp::ir_autocomplete_reply) + .add_autocomplete_choice(dpp::command_option_choice("squids", "lots of squids")) + .add_autocomplete_choice(dpp::command_option_choice("cats", "a few cats")) + .add_autocomplete_choice(dpp::command_option_choice("dogs", "bucket of dogs")) + .add_autocomplete_choice(dpp::command_option_choice("elephants", "bottle of elephants")) + ); + bot.log(dpp::ll_debug, "Autocomplete " + opt.name + " with value '" + uservalue + "' in field " + event.name); + break; + } + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/cache_messages.cpp b/docpages/example_code/cache_messages.cpp new file mode 100644 index 0000000000..8267971bb1 --- /dev/null +++ b/docpages/example_code/cache_messages.cpp @@ -0,0 +1,60 @@ +#include +#include + +int main() { + /* Create bot */ + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); /* Because we're handling messages, we need to use the "i_message_content" intent! */ + + /* Create a cache to contain types of dpp::message */ + dpp::cache message_cache; + + bot.on_log(dpp::utility::cout_logger()); + + /* Message handler */ + bot.on_message_create([&](const dpp::message_create_t &event) { + /* Make a permanent pointer using new, for each message to be cached */ + dpp::message* m = new dpp::message(); + + /* Store the message into the pointer by copying it */ + *m = event.msg; + + /* Store the new pointer to the cache using the store() method */ + message_cache.store(m); + }); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot, &message_cache](const dpp::slashcommand_t& event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "get") { + + dpp::message* find_msg = message_cache.find(std::get(event.get_parameter("message_id"))); + + /* If find_msg is null, tell the user and return. */ + if (!find_msg) { + event.reply("There is no message cached with this ID"); + return; + } + + event.reply("This message had the following content: " + find_msg->content); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Create a new command. */ + dpp::slashcommand newcommand("get", "Get the contents of a message that was cached via an id", bot.me.id); + + /* Add a parameter option. */ + newcommand.add_option(dpp::command_option(dpp::co_string, "message_id", "The ID of the message you want to find", true)); + + /* Register the command */ + bot.global_command_create(newcommand); + } + }); + + /* Start bot */ + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/callbacks.cpp b/docpages/example_code/callbacks.cpp new file mode 100644 index 0000000000..fc5d236cd5 --- /dev/null +++ b/docpages/example_code/callbacks.cpp @@ -0,0 +1,88 @@ +#include + +int main() { + dpp::cluster bot("Token Was Here", dpp::i_default_intents | dpp::i_message_content); + /* the second argument is a bitmask of intents - i_message_content is needed to get messages */ + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) -> void { + if (event.command.get_command_name() == "msgs-get") { + int64_t limit = std::get(event.get_parameter("quantity")); + + /* get messages using ID of the channel the command was issued in */ + bot.messages_get(event.command.channel_id, 0, 0, 0, limit, [event](const dpp::confirmation_callback_t& callback) -> void { + if (callback.is_error()) { /* catching an error to log it */ + std::cout << callback.get_error().message << std::endl; + return; + } + + auto messages = callback.get(); + /* std::get(callback.value) would give the same result */ + + std::string contents; + for (const auto& x : messages) { /* here we iterate through the dpp::message_map we got from callback... */ + contents += x.second.content + '\n'; /* ...where x.first is ID of the current message and x.second is the message itself. */ + } + + event.reply(contents); /* we will see all those messages we got, united as one! */ + }); + } else if (event.command.get_command_name() == "channel-create") { + /* create a text channel */ + dpp::channel channel = dpp::channel() + .set_name("test") + .set_guild_id(event.command.guild_id); + + bot.channel_create(channel, [&bot, event](const dpp::confirmation_callback_t& callback) -> void { + if (callback.is_error()) { /* catching an error to log it */ + bot.log(dpp::loglevel::ll_error, callback.get_error().message); + return; + } + + auto channel = callback.get(); + /* std::get(callback.value) would give the same result */ + + /* reply with the created channel information */ + dpp::message message = dpp::message("The channel's name is `" + channel.name + "`, ID is `" + std::to_string(channel.id) + " and type is `" + std::to_string(channel.get_type()) + "`."); + /* note that channel types are represented as numbers */ + event.reply(message); + }); + } else if (event.command.get_command_name() == "msg-error") { + bot.message_get(0, 0, [event](const dpp::confirmation_callback_t& callback) -> void { + /* the error will occur since there is no message with ID '0' that is in a channel with ID '0' (I'm not explaining why) */ + if (callback.is_error()) { + event.reply(callback.get_error().message); + return; + } + + /* we won't be able to get here because of the return; statement */ + auto message = callback.get(); + event.reply(message); + }); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once ()) { + dpp::slashcommand msgs_get("msgs-get", "Get messages", bot.me.id); + + constexpr int64_t min_val{1}; + constexpr int64_t max_val{100}; + + msgs_get.add_option( + dpp::command_option(dpp::co_integer, "quantity", "Quantity of messages to get. Max - 100.") + .set_min_value(min_val) + .set_max_value(max_val) + ); + + dpp::slashcommand channel_create("channel-create", "Create a channel", bot.me.id); + dpp::slashcommand msg_error("msg-error", "Get an error instead of message :)", bot.me.id); + + bot.global_bulk_command_create({ msgs_get, channel_create, msg_error }); + } + }); + + bot.start(dpp::st_wait); + return 0; +} diff --git a/docpages/example_code/collect_reactions.cpp b/docpages/example_code/collect_reactions.cpp new file mode 100644 index 0000000000..4d796643cf --- /dev/null +++ b/docpages/example_code/collect_reactions.cpp @@ -0,0 +1,46 @@ +#include + +/* To create a collector we must derive from dpp::collector. As dpp::collector is a complicated template, + * various pre-made forms exist such as this one, reaction_collector. + */ +class react_collector : public dpp::reaction_collector { +public: + /* Collector will run for 20 seconds */ + react_collector(dpp::cluster* cl, dpp::snowflake id) : dpp::reaction_collector(cl, 20, id) { } + + /* Override the "completed" event and then output the number of collected reactions as a message. */ + virtual void completed(const std::vector& list) override { + if (list.size()) { + owner->message_create(dpp::message(list[0].react_channel->id, "I collected " + std::to_string(list.size()) + " reactions!")); + } else { + owner->message_create(dpp::message("... I got nothin'.")); + } + } +}; + + +int main() { + /* Create bot */ + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + /* Pointer to reaction collector */ + react_collector* r = nullptr; + + bot.on_log(dpp::utility::cout_logger()); + + /* Message handler */ + bot.on_message_create([&r, &bot](const dpp::message_create_t& event) { + + /* If someone sends a message that has the text 'collect reactions!' start a reaction collector */ + if (event.msg.content == "collect reactions!" && r == nullptr) { + /* Create a new reaction collector to collect reactions */ + r = new react_collector(&bot, event.msg.id); + } + + }); + + /* Start bot */ + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/commandhandler.cpp b/docpages/example_code/commandhandler.cpp new file mode 100644 index 0000000000..ce7c6fb378 --- /dev/null +++ b/docpages/example_code/commandhandler.cpp @@ -0,0 +1,53 @@ +#include + +int main() +{ + /* If your bot only uses the "/" prefix, you can remove the intents here. */ + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + /* Create command handler, and specify prefixes */ + dpp::commandhandler command_handler(&bot); + /* Specifying a prefix of "/" tells the command handler it should also expect slash commands. Remove the .add_prefix(".") if you wish to only make it a slash command */ + command_handler.add_prefix(".").add_prefix("/"); + + bot.on_ready([&command_handler](const dpp::ready_t &event) { + + command_handler.add_command( + /* Command name */ + "ping", + + /* Parameters */ + { + {"testparameter", dpp::param_info(dpp::pt_string, true, "Optional test parameter") } + }, + + /* Command handler */ + [&command_handler](const std::string& command, const dpp::parameter_list_t& parameters, dpp::command_source src) { + std::string got_param; + if (!parameters.empty()) { + got_param = std::get(parameters[0].second); + } + command_handler.reply(dpp::message("Pong! -> " + got_param), src); + }, + + /* Command description */ + "A test ping command", + + /* Guild id (omit for a guild command) */ + 819556414099554344 + ); + + /* NOTE: We must call this to ensure slash commands are registered. + * This does a bulk register, which will replace other commands + * that are registered already! + */ + command_handler.register_commands(); + + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/components.cpp b/docpages/example_code/components.cpp new file mode 100644 index 0000000000..5caa90b23b --- /dev/null +++ b/docpages/example_code/components.cpp @@ -0,0 +1,57 @@ +#include +#include + +int main() { + + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "button") { + + /* Create a message */ + dpp::message msg(event.command.channel_id, "this text has a button"); + + /* Add an action row, and then a button within the action row. */ + msg.add_component( + dpp::component().add_component( + dpp::component(). + set_label("Click me!"). + set_type(dpp::cot_button). + set_emoji(dpp::unicode_emoji::smile). + set_style(dpp::cos_danger). + set_id("myid") + ) + ); + + /* Reply to the user with our message. */ + event.reply(msg); + } + }); + + /* When a user clicks your button, the on_button_click event will fire, + * containing the custom_id you defined in your button. + */ + bot.on_button_click([&bot](const dpp::button_click_t& event) { + /* Button clicks are still interactions, and must be replied to in some form to + * prevent the "this interaction has failed" message from Discord to the user. + */ + event.reply("You clicked: " + event.custom_id); + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Create and register a command when the bot is ready */ + bot.global_command_create(dpp::slashcommand("button", "Send a message with a button!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/components2.cpp b/docpages/example_code/components2.cpp new file mode 100644 index 0000000000..d5d610bf20 --- /dev/null +++ b/docpages/example_code/components2.cpp @@ -0,0 +1,62 @@ +#include + +int main() { + + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "math") { + + /* Create a message */ + dpp::message msg(event.command.channel_id, "What is 5+5?"); + + /* Add an action row, and then 3 buttons within the action row. */ + msg.add_component( + dpp::component().add_component( + dpp::component(). + set_label("9"). + set_style(dpp::cos_primary). + set_id("9") + ).add_component( + dpp::component(). + set_label("10"). + set_style(dpp::cos_primary). + set_id("10") + ).add_component( + dpp::component(). + set_label("11"). + set_style(dpp::cos_primary). + set_id("11") + ) + ); + + /* Reply to the user with our message. */ + event.reply(msg); + } + }); + + bot.on_button_click([&bot](const dpp::button_click_t & event) { + if (event.custom_id == "10") { + event.reply(dpp::message("You got it right!").set_flags(dpp::m_ephemeral)); + } else { + event.reply(dpp::message("Wrong! Try again.").set_flags(dpp::m_ephemeral)); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Create and register a command when the bot is ready */ + bot.global_command_create(dpp::slashcommand("math", "A quick maths question!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/components3.cpp b/docpages/example_code/components3.cpp new file mode 100644 index 0000000000..b69d48899d --- /dev/null +++ b/docpages/example_code/components3.cpp @@ -0,0 +1,57 @@ +#include +#include + +int main() { + + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "select") { + + /* Create a message */ + dpp::message msg(event.command.channel_id, "This text has a select menu!"); + + /* Add an action row, and a select menu within the action row. */ + msg.add_component( + dpp::component().add_component( + dpp::component(). + set_type(dpp::cot_selectmenu). + set_placeholder("Pick something"). + add_select_option(dpp::select_option("label1","value1","description1").set_emoji(dpp::unicode_emoji::smile)). + add_select_option(dpp::select_option("label2","value2","description2").set_emoji(dpp::unicode_emoji::slight_smile)). + set_id("myselectid") + ) + ); + + /* Reply to the user with our message. */ + event.reply(msg); + } + }); + + /* When a user clicks your select menu , the on_select_click event will fire, + * containing the custom_id you defined in your select menu. + */ + bot.on_select_click([&bot](const dpp::select_click_t & event) { + /* Select clicks are still interactions, and must be replied to in some form to + * prevent the "this interaction has failed" message from Discord to the user. + */ + event.reply("You clicked " + event.custom_id + " and chose: " + event.values[0]); + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Create and register a command when the bot is ready */ + bot.global_command_create(dpp::slashcommand("select", "Select something at random!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/context_menus.cpp b/docpages/example_code/context_menus.cpp new file mode 100644 index 0000000000..ff7a32a555 --- /dev/null +++ b/docpages/example_code/context_menus.cpp @@ -0,0 +1,39 @@ +#include +#include + +int main() +{ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* Use the on_user_context_menu event to look for user context menu actions */ + bot.on_user_context_menu([&](const dpp::user_context_menu_t& event) { + + /* check if the context menu name is High Five */ + if (event.command.get_command_name() == "high five") { + dpp::user user = event.get_user(); // the user who the command has been issued on + dpp::user author = event.command.get_issuing_user(); // the user who clicked on the context menu + event.reply(author.get_mention() + " slapped " + user.get_mention()); + } + }); + + bot.on_ready([&bot](const dpp::ready_t &event) { + if (dpp::run_once()) { + + /* Create the command */ + dpp::slashcommand command; + command.set_name("High Five"); + command.set_application_id(bot.me.id); + command.set_type(dpp::ctxm_user); + + /* Register the command */ + bot.guild_command_create(command, 857692897221033129); /* Replace this with the guild id you want */ + } + }); + + /* Start bot */ + bot.start(dpp::st_wait); + + return 0; +} \ No newline at end of file diff --git a/docpages/example_code/coro_awaiting_events.cpp b/docpages/example_code/coro_awaiting_events.cpp new file mode 100644 index 0000000000..3f62f27e9d --- /dev/null +++ b/docpages/example_code/coro_awaiting_events.cpp @@ -0,0 +1,39 @@ +#include + +int main() { + dpp::cluster bot{"token"}; + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + if (event.command.get_command_name() == "test") { + // Make a message and add a button with its custom ID set to the command interaction's ID so we can identify it + dpp::message m{"Test"}; + std::string id{event.command.id.str()}; + m.add_component( + dpp::component{}.add_component(dpp::component{}.set_type(dpp::cot_button).set_label("Click me!").set_id(id)) + ); + co_await event.co_reply(m); + + dpp::button_click_t click_event = co_await event.from->creator->on_button_click.when( + // Note!! Due to a bug in g++11 and g++12, id must be captured as a reference here or the compiler will destroy it twice. This is fixed in g++13 + [&id] (dpp::button_click_t const &b) { + return b.custom_id == id; + } + ); + // Acknowledge the click and edit the original response, removing the button + click_event.reply(); + event.edit_original_response(dpp::message{"You clicked the button!"}); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + dpp::slashcommand command{"test", "Test awaiting for an event", bot.me.id}; + + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); +} diff --git a/docpages/example_code/coro_expiring_buttons.cpp b/docpages/example_code/coro_expiring_buttons.cpp new file mode 100644 index 0000000000..01a7958356 --- /dev/null +++ b/docpages/example_code/coro_expiring_buttons.cpp @@ -0,0 +1,43 @@ +#include + +int main() { + dpp::cluster bot{"token"}; + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + if (event.command.get_command_name() == "test") { + // Make a message and add a button with its custom ID set to the command interaction's ID so we can identify it + dpp::message m{"Test"}; + std::string id{event.command.id.str()}; + m.add_component( + dpp::component{}.add_component(dpp::component{}.set_type(dpp::cot_button).set_label("Click me!").set_id(id)) + ); + co_await event.co_reply(m); + + auto result = co_await dpp::when_any{ // Whichever completes first... + event.from->creator->on_button_click.when([&id](const dpp::button_click_t &b) { return b.custom_id == id; }), // Button clicked + event.from->creator->co_sleep(5) // Or sleep 5 seconds + }; + // Note!! Due to a bug in g++11 and g++12, id must be captured as a reference above or the compiler will destroy it twice. This is fixed in g++13 + if (result.index() == 0) { // Awaitable #0 completed first, that is the button click event + // Acknowledge the click and edit the original response, removing the button + const dpp::button_click_t &click_event = result.get<0>(); + click_event.reply(); + event.edit_original_response(dpp::message{"You clicked the button with the id " + click_event.custom_id}); + } else { // Here index() is 1, the timer expired + event.edit_original_response(dpp::message{"I haven't got all day!"}); + } + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + dpp::slashcommand command{"test", "Test awaiting for an event", bot.me.id}; + + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); +} diff --git a/docpages/example_code/coro_intro.cpp b/docpages/example_code/coro_intro.cpp new file mode 100644 index 0000000000..d4177c7f02 --- /dev/null +++ b/docpages/example_code/coro_intro.cpp @@ -0,0 +1,35 @@ +#include + +int main() { + dpp::cluster bot{"token"}; + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + /* Make note of passing the event by value, this is important (explained below) */ + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + if (event.command.get_command_name() == "file") { + /* Request the image from the URL specified and co_await the response */ + dpp::http_request_completion_t result = co_await event.from->creator->co_request("https://dpp.dev/DPP-Logo.png", dpp::m_get); + + /* Create a message and attach the image on success */ + dpp::message msg(event.command.channel_id, "This is my new attachment:"); + if (result.status == 200) { + msg.add_file("logo.png", result.body); + } + + /* Send the message, with our attachment. */ + event.reply(msg); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + /* Create and register a command when the bot is ready */ + bot.global_command_create(dpp::slashcommand{"file", "Send a message with an image attached from the internet!", bot.me.id}); + } + }); + + bot.start(dpp::st_wait); + return 0; +} diff --git a/docpages/example_code/coro_simple_commands1.cpp b/docpages/example_code/coro_simple_commands1.cpp new file mode 100644 index 0000000000..8eb8b90dbc --- /dev/null +++ b/docpages/example_code/coro_simple_commands1.cpp @@ -0,0 +1,63 @@ +#include + +int main() { + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + if (event.command.get_command_name() == "addemoji") { + dpp::cluster *cluster = event.from->creator; + // Retrieve parameter values + dpp::snowflake file_id = std::get(event.get_parameter("file")); + std::string emoji_name = std::get(event.get_parameter("name")); + + // Get the attachment from the resolved list + const dpp::attachment &attachment = event.command.get_resolved_attachment(file_id); + + // For simplicity for this example we only support PNG + if (attachment.content_type != "image/png") { + // While we could use event.co_reply, we can just use event.reply, as we will exit the command anyway and don't need to wait on the result + event.reply("Error: type " + attachment.content_type + " not supported"); + co_return; + } + // Send a " is thinking..." message, to wait on later so we can edit + dpp::async thinking = event.co_thinking(false); + + // Download and co_await the result + dpp::http_request_completion_t response = co_await cluster->co_request(attachment.url, dpp::m_get); + + if (response.status != 200) { // Page didn't send the image + co_await thinking; // Wait for the thinking response to arrive so we can edit + event.edit_response("Error: could not download the attachment"); + } else { + // Load the image data in a dpp::emoji + dpp::emoji emoji(emoji_name); + emoji.load_image(response.body, dpp::image_type::i_png); + + // Create the emoji and co_await the response + dpp::confirmation_callback_t confirmation = co_await cluster->co_guild_emoji_create(event.command.guild_id, emoji); + + co_await thinking; // Wait for the thinking response to arrive so we can edit + if (confirmation.is_error()) { + event.edit_response("Error: could not add emoji: " + confirmation.get_error().message); + } else { // Success + event.edit_response("Successfully added " + confirmation.get().get_mention()); // Show the new emoji + } + } + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + dpp::slashcommand command("addemoji", "Add an emoji", bot.me.id); + // Add file and name as required parameters + command.add_option(dpp::command_option(dpp::co_attachment, "file", "Select an image", true)); + command.add_option(dpp::command_option(dpp::co_string, "name", "Name of the emoji to add", true)); + + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); +} diff --git a/docpages/example_code/coro_simple_commands2.cpp b/docpages/example_code/coro_simple_commands2.cpp new file mode 100644 index 0000000000..5858286eed --- /dev/null +++ b/docpages/example_code/coro_simple_commands2.cpp @@ -0,0 +1,82 @@ +#include + +int main() { + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + if (event.command.get_command_name() == "avatar") { + // Make a nested coroutine to fetch the guild member requested, that returns it as an optional + constexpr auto resolve_member = [](const dpp::slashcommand_t &event) -> dpp::task> { + const dpp::command_value &user_param = event.get_parameter("user"); + dpp::snowflake user_id; + if (std::holds_alternative(user_param)) { + user_id = event.command.usr.id; // Parameter is empty so user is sender + } + else if (std::holds_alternative(user_param)) { + user_id = std::get(user_param); // Parameter has a user + } + + // If we have the guild member in the command's resolved data, return it + const auto &member_map = event.command.resolved.members; + if (auto member = member_map.find(user_id); member != member_map.end()) + co_return member->second; + // Try looking in guild cache + dpp::guild *guild = dpp::find_guild(event.command.guild_id); + if (guild) { + // Look in guild's member cache + if (auto member = guild->members.find(user_id); member != guild->members.end()) { + co_return member->second; + } + } + // Finally if everything else failed, request API + dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_guild_get_member(event.command.guild_id, user_id); + if (confirmation.is_error()) { + co_return std::nullopt; // Member not found, return empty + } else { + co_return confirmation.get(); + } + }; + + // Send a " is thinking..." message, to wait on later so we can edit + dpp::async thinking = event.co_thinking(false); + // Call our coroutine defined above to retrieve the member requested + std::optional member = co_await resolve_member(event); + if (!member.has_value()) { + // Wait for the thinking response to arrive to make sure we can edit + co_await thinking; + event.edit_original_response(dpp::message{"User not found in this server!"}); + co_return; + } + + std::string avatar_url = member->get_avatar_url(512); + if (avatar_url.empty()) { // Member does not have a custom avatar for this server, get their user avatar + dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_user_get_cached(member->user_id); + if (confirmation.is_error()) { + // Wait for the thinking response to arrive to make sure we can edit + co_await thinking; + event.edit_original_response(dpp::message{"User not found!"}); + co_return; + } + avatar_url = confirmation.get().get_avatar_url(512); + } + + // Wait for the thinking response to arrive to make sure we can edit + co_await thinking; + event.edit_original_response(dpp::message{avatar_url}); + } + }); + + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + dpp::slashcommand command("avatar", "Get your or another user's avatar image", bot.me.id); + command.add_option(dpp::command_option(dpp::co_user, "user", "User to fetch the avatar from")); + + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); +} diff --git a/docpages/example_code/detecting_messages.cpp b/docpages/example_code/detecting_messages.cpp new file mode 100644 index 0000000000..ecb0805230 --- /dev/null +++ b/docpages/example_code/detecting_messages.cpp @@ -0,0 +1,24 @@ +#include + +int main() +{ + /* Create the bot, but with our intents so we can use messages. */ + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when the bot detects a message in any server and any channel it has access to. */ + bot.on_message_create([&bot](const dpp::message_create_t& event) { + + /* See if the message contains the phrase we want to check for. + * If there's at least a single match, we reply and say it's not allowed. + */ + if (event.msg.content.find("bad word") != std::string::npos) { + event.reply("That is not allowed here. Please, mind your language!", true); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} \ No newline at end of file diff --git a/docpages/example_code/editing_messages.cpp b/docpages/example_code/editing_messages.cpp new file mode 100644 index 0000000000..5bca1afe85 --- /dev/null +++ b/docpages/example_code/editing_messages.cpp @@ -0,0 +1,73 @@ +#include + +int main() { + dpp::cluster bot("Token", dpp::i_default_intents | dpp::i_message_content); + /* the second argument is a bitmask of intents - i_message_content is needed to get messages */ + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + if (event.command.get_command_name() == "msg-send") { + event.reply("That's a message"); + } else if (event.command.get_command_name() == "msg-edit") { + const auto content = std::get(event.get_parameter("content")); + + /* get message to edit it after */ + const dpp::snowflake msg_id = std::get(event.get_parameter("msg-id")); + /* here string will automatically be converted to snowflake */ + + bot.message_get(msg_id, event.command.channel_id, [&bot, content, event](const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) { + event.reply("error"); + return; + } + auto message = callback.get(); + + /* change the message content and edit the message itself */ + message.set_content(content); + bot.message_edit(message); + event.reply("Message content is now `" + content + "`."); + }); + } else if (event.command.get_command_name() == "channel-edit") { + const auto name = std::get(event.get_parameter("name")); + + /* get the channel to edit it after */ + const auto channel_id = std::get(event.get_parameter("channel")); + bot.channel_get(channel_id, [&bot, name, event](const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) { + event.reply("error"); + return; + } + auto channel = callback.get(); + + /* change the channel name and edit the channel itself */ + channel.set_name(name); + bot.channel_edit(channel); + event.reply("Channel name is now `" + name + "`."); + }); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + + if (dpp::run_once ()) { + dpp::slashcommand msg_edit("msg-edit", "Edit a message sent by the bot", bot.me.id); + + msg_edit.add_option(dpp::command_option(dpp::co_string, "msg-id", "ID of the message to edit", true)); /* true for required option */ + msg_edit.add_option(dpp::command_option(dpp::co_string, "content", "New content for the message", true)); /* same here */ + + dpp::slashcommand channel_edit("channel-edit", "Edit the name of channel specified", bot.me.id); + + channel_edit.add_option(dpp::command_option(dpp::co_channel, "channel", "Channel to edit", true)); + channel_edit.add_option(dpp::command_option(dpp::co_string, "name", "New name for the channel", true)); + + dpp::slashcommand msg_send("msg-send", "Send my message", bot.me.id); + + bot.global_bulk_command_create({ msg_edit, channel_edit, msg_send }); + } + }); + + bot.start(dpp::st_wait); + return 0; +} diff --git a/docpages/example_code/embeds.cpp b/docpages/example_code/embeds.cpp new file mode 100644 index 0000000000..619d1e63ed --- /dev/null +++ b/docpages/example_code/embeds.cpp @@ -0,0 +1,56 @@ +#include + +int main() { + /* Setup the bot */ + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "embed") { + + /* Create an embed */ + dpp::embed embed = dpp::embed(). + set_color(dpp::colors::sti_blue). + set_title("Some name"). + set_url("https://dpp.dev/"). + set_author("Some name", "https://dpp.dev/", "https://dpp.dev/DPP-Logo.png"). + set_description("Some description here"). + set_thumbnail("https://dpp.dev/DPP-Logo.png"). + add_field( + "Regular field title", + "Some value here" + ). + add_field( + "Inline field title", + "Some value here", + true + ). + add_field( + "Inline field title", + "Some value here", + true + ). + set_image("https://dpp.dev/DPP-Logo.png"). + set_footer(dpp::embed_footer().set_text("Some footer text here").set_icon("https://dpp.dev/DPP-Logo.png")). + set_timestamp(time(0)); + + /* Create a message with the content as our new embed. */ + dpp::message msg(event.command.channel_id, embed); + + /* Reply to the user with the message, containing our embed. */ + event.reply(msg); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Create and register a command when the bot is ready */ + bot.global_command_create(dpp::slashcommand("embed", "Send a test embed!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + return 0; +} diff --git a/docpages/example_code/ephemeral.cpp b/docpages/example_code/ephemeral.cpp new file mode 100644 index 0000000000..5fb4f7dc0c --- /dev/null +++ b/docpages/example_code/ephemeral.cpp @@ -0,0 +1,32 @@ +#include + +int main() +{ + /* Create the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "hello") { + + /* Reply to the user, but only let them see the response. */ + event.reply(dpp::message("Hello! How are you today?").set_flags(dpp::m_ephemeral)); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create and Register the command */ + bot.global_command_create(dpp::slashcommand("hello", "Hello there!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/eval.cpp b/docpages/example_code/eval.cpp new file mode 100644 index 0000000000..46af646ce4 --- /dev/null +++ b/docpages/example_code/eval.cpp @@ -0,0 +1,175 @@ +/** + * D++ eval command example. + * This is dangerous and for educational use only, here be dragons! + */ + +#include +#include +#include +#include +/* We have to define this to make certain functions visible */ +#ifndef _GNU_SOURCE + #define _GNU_SOURCE +#endif +#include +#include +#include "eval.h" + +/* This is an example function you can expose to your eval command */ +int test_function() { + return 42; +} + +/* Important: This code is for UNIX-like systems only, e.g. + * Linux, BSD, OSX. It will NOT work on Windows! + * Note for OSX you'll probably have to change all references + * from .so to .dylib. + */ +int main() +{ + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + /* This won't work in a slash command very well yet, as there is not yet + * a multi-line slash command input type. + */ + bot.on_message_create([&bot](const auto & event) { + if (dpp::utility::utf8substr(event.msg.content, 0, 5) == "!eval") { + + /** + * THIS IS CRITICALLY IMPORTANT! + * Never EVER make an eval command that isn't restricted to a specific developer by user id. + * With access to this command the person who invokes it has at best full control over + * your bot's user account and at worst, full control over your entire network!!! + * Eval commands are Evil (pun intended) and could even be considered a security + * vulnerability. YOU HAVE BEEN WARNED! + */ + if (event.msg.author.id != dpp::snowflake(MY_DEVELOPER)) { + bot.message_create(dpp::message(event.msg.channel_id, "On the day i do this for you, Satan will be ice skating to work.")); + return; + } + + /* We start by creating a string that contains a cpp program for a simple library. + * The library will contain one exported function called so_exec() that is called + * containing the raw C++ code to eval. + */ + std::string code = "#include \n\ + #include \n\ + #include \n\ + #include \n\ + #include \n\ + #include \n\ + #include \n\ + #include \n\ + #include \"eval.h\"\n\ + extern \"C\" void so_exec(dpp::cluster& bot, dpp::message_create_t event) {\n\ + " + dpp::utility::utf8substr( + event.msg.content, + 6, + dpp::utility::utf8len(event.msg.content) + ) + ";\n\ + return;\n\ + }"; + + /* Next we output this string full of C++ to a cpp file on disk. + * This code assumes the current directory is writeable. The file will have a + * unique name made from the user's id and the message id. + */ + std::string source_filename = std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".cpp"; + std::fstream code_file(source_filename, std::fstream::binary | std::fstream::out); + if (!code_file.is_open()) { + bot.message_create(dpp::message(event.msg.channel_id, "Unable to create source file for `eval`")); + return; + } + code_file << code; + code_file.close(); + + /* Now to actually compile the file. We use dpp::utility::exec to + * invoke a compiler. This assumes you are using g++, and it is in your path. + */ + double compile_start = dpp::utility::time_f(); + dpp::utility::exec("g++", { + "-std=c++17", + "-shared", /* Build the output as a .so file */ + "-fPIC", + std::string("-o") + std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".so", + std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".cpp", + "-ldpp", + "-ldl" + }, [event, &bot, source_filename, compile_start](const std::string &output) { + + /* After g++ is ran we end up inside this lambda with the output as a string */ + double compile_time = dpp::utility::time_f() - compile_start; + + /* Delete our cpp file, we don't need it any more */ + std::string del_file = std::string(getenv("PWD")) + std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".cpp"; + unlink(del_file.c_str()); + + /* On successful compilation g++ outputs nothing, so any output here is error output */ + if (output.length()) { + bot.message_create(dpp::message(event.msg.channel_id, "Compile error: ```\n" + output + "\n```")); + } else { + + /* Now for the meat of the function. To actually load + * our shared object we use dlopen() to load it into the + * memory space of our bot. If dlopen() returns a nullptr, + * the shared object could not be loaded. The user probably + * did something odd with the symbols inside their eval. + */ + std::string dl = std::string(getenv("PWD")) + std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".so"; + auto shared_object_handle = dlopen(dl.c_str(), RTLD_NOW); + if (!shared_object_handle) { + const char *dlsym_error = dlerror(); + bot.message_create(dpp::message(event.msg.channel_id, "Shared object load error: ```\n" + + std::string(dlsym_error ? dlsym_error : "Unknown error") +"\n```")); + return; + } + + /* This type represents the "void so_exec()" function inside + * the shared object library file. + */ + using function_pointer = void(*)(dpp::cluster&, dpp::message_create_t); + + /* Attempt to find the function called so_exec() inside the + * library we just loaded. If we can't find it, then the user + * did something really strange in their eval. Also note it's + * important we call dlerror() here to reset it before trying + * to use it a second time. It's weird-ass C code and is just + * like that. + */ + dlerror(); + function_pointer exec_run = (function_pointer)dlsym(shared_object_handle, "so_exec"); + const char *dlsym_error = dlerror(); + if (dlsym_error) { + bot.message_create(dpp::message(event.msg.channel_id, "Shared object load error: ```\n" + std::string(dlsym_error) +"\n```")); + dlclose(shared_object_handle); + return; + } + + /* Now we have a function pointer to our actual exec code in + * 'exec_run', so lets call it, and pass it a reference to + * the cluster, and also a copy of the message_create_t. + */ + double run_start = dpp::utility::time_f(); + exec_run(bot, event); + double run_time = dpp::utility::time_f() - run_start; + + /* When we're done with a .so file we must always dlclose() it */ + dlclose(shared_object_handle); + + /* We are now done with the compiled code too */ + unlink(dl.c_str()); + + /* Output some statistics */ + bot.message_create(dpp::message(event.msg.channel_id, + "Execution completed. Compile time: " + std::to_string(compile_time) + + "s, execution time " + std::to_string(run_time) + "s")); + } + }); + } + }); + + bot.start(dpp::st_wait); + return 0; +} diff --git a/docpages/example_code/eval.h b/docpages/example_code/eval.h new file mode 100644 index 0000000000..0ce683e3f7 --- /dev/null +++ b/docpages/example_code/eval.h @@ -0,0 +1,14 @@ +#pragma once + +/* This is the snowflake ID of the bot's developer. + * The eval command will be restricted to this user. + */ +#define MY_DEVELOPER 189759562910400512ULL + +/* Any functions you want to be usable from within an eval, + * that are not part of D++ itself or the message event, you + * can put here as forward declarations. The test_function() + * serves as an example. + */ + +int test_function(); diff --git a/docpages/example_code/firstbot.cpp b/docpages/example_code/firstbot.cpp new file mode 100644 index 0000000000..fd7bb43957 --- /dev/null +++ b/docpages/example_code/firstbot.cpp @@ -0,0 +1,25 @@ +#include + +const std::string BOT_TOKEN = "add your token here"; + +int main() { + dpp::cluster bot(BOT_TOKEN); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](const dpp::slashcommand_t& event) { + if (event.command.get_command_name() == "ping") { + event.reply("Pong!"); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + bot.global_command_create( + dpp::slashcommand("ping", "Ping pong!", bot.me.id) + ); + } + }); + + bot.start(dpp::st_wait); +} diff --git a/docpages/example_code/firstbot1.cpp b/docpages/example_code/firstbot1.cpp new file mode 100644 index 0000000000..321e3015b4 --- /dev/null +++ b/docpages/example_code/firstbot1.cpp @@ -0,0 +1,4 @@ +#include + +int main() { +} diff --git a/docpages/example_code/firstbot2.cpp b/docpages/example_code/firstbot2.cpp new file mode 100644 index 0000000000..5b00e5526b --- /dev/null +++ b/docpages/example_code/firstbot2.cpp @@ -0,0 +1,7 @@ +#include + +const std::string BOT_TOKEN = "add your token here"; + +int main() { + dpp::cluster bot(BOT_TOKEN); +} diff --git a/docpages/example_code/firstbot3.cpp b/docpages/example_code/firstbot3.cpp new file mode 100644 index 0000000000..ad7ee22099 --- /dev/null +++ b/docpages/example_code/firstbot3.cpp @@ -0,0 +1,13 @@ +#include + +const std::string BOT_TOKEN = "add your token here"; + +int main() { + dpp::cluster bot(BOT_TOKEN); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); + } + }); +} diff --git a/docpages/example_code/firstbot4.cpp b/docpages/example_code/firstbot4.cpp new file mode 100644 index 0000000000..cbd55f1860 --- /dev/null +++ b/docpages/example_code/firstbot4.cpp @@ -0,0 +1,16 @@ +#include + +const std::string BOT_TOKEN = "add your token here"; + +int main() { + dpp::cluster bot(BOT_TOKEN); + + bot.on_slashcommand([](const dpp::slashcommand_t& event) { + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); + } + }); +} diff --git a/docpages/example_code/firstbot5.cpp b/docpages/example_code/firstbot5.cpp new file mode 100644 index 0000000000..279a32bbe6 --- /dev/null +++ b/docpages/example_code/firstbot5.cpp @@ -0,0 +1,20 @@ +#include + +const std::string BOT_TOKEN = "add your token here"; + +int main() { + dpp::cluster bot(BOT_TOKEN); + + bot.on_slashcommand([](const dpp::slashcommand_t& event) { + if (event.command.get_command_name() == "ping") { + event.reply("Pong!"); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); + } + }); + +} diff --git a/docpages/example_code/firstbot6.cpp b/docpages/example_code/firstbot6.cpp new file mode 100644 index 0000000000..6a626ff774 --- /dev/null +++ b/docpages/example_code/firstbot6.cpp @@ -0,0 +1,23 @@ +#include + +const std::string BOT_TOKEN = "add your token here"; + +int main() { + dpp::cluster bot(BOT_TOKEN); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](const dpp::slashcommand_t& event) { + if (event.command.get_command_name() == "ping") { + event.reply("Pong!"); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); +} diff --git a/docpages/example_code/http_request.cpp b/docpages/example_code/http_request.cpp new file mode 100644 index 0000000000..67252bdc88 --- /dev/null +++ b/docpages/example_code/http_request.cpp @@ -0,0 +1,28 @@ +#include +#include + +int main() { + dpp::cluster bot("TOKEN GOES HERE"); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_ready([&bot](const dpp::ready_t& event) { + // Arbitrary post data as a string + std::string mypostdata = "{\"value\": 42}"; + // Make a HTTP POST request. HTTP and HTTPS are supported here. + bot.request( + "http://www.somebotlist.com/api/servers", dpp::m_post, [](const dpp::http_request_completion_t & cc) { + // This callback is called when the HTTP request completes. See documentation of + // dpp::http_request_completion_t for information on the fields in the parameter. + std::cout << "I got reply: " << cc.body << " with HTTP status code: " << cc.status << "\n"; + }, + mypostdata, + "application/json", + { + {"Authorization", "Bearer tokengoeshere"} + } + ); + }); + + bot.start(dpp::st_wait); +} diff --git a/docpages/example_code/join_voice.cpp b/docpages/example_code/join_voice.cpp new file mode 100644 index 0000000000..266917afde --- /dev/null +++ b/docpages/example_code/join_voice.cpp @@ -0,0 +1,87 @@ +#include +#include +#include + +int main(int argc, char const *argv[]) +{ + /* Setup the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "join") { + + /* Get the guild */ + dpp::guild* g = dpp::find_guild(event.command.guild_id); + + /* Get the voice channel that the bot is currently in from this server (will return nullptr if we're not in a voice channel!) */ + auto current_vc = event.from->get_voice(event.command.guild_id); + + bool join_vc = true; + + /* Are we in a voice channel? If so, let's see if we're in the right channel. */ + if (current_vc) { + /* Find the channel id that the user is currently in */ + auto users_vc = g->voice_members.find(event.command.get_issuing_user().id); + + if (users_vc != g->voice_members.end() && current_vc->channel_id == users_vc->second.channel_id) { + join_vc = false; + + /* We are on this voice channel, at this point we can send any audio instantly to vc: + + * current_vc->send_audio_raw(...) + */ + } else { + /* We are on a different voice channel. We should leave it, then join the new one + * by falling through to the join_vc branch below. + */ + event.from->disconnect_voice(event.command.guild_id); + + join_vc = true; + } + } + + /* If we need to join a vc at all, join it here if join_vc == true */ + if(join_vc) { + /* Attempt to connect to a voice channel, returns false if we fail to connect. */ + + /* The user issuing the command is not on any voice channel, we can't do anything */ + if (!g->connect_member_voice(event.command.get_issuing_user().id)) { + event.reply("You don't seem to be in a voice channel!"); + return; + } + + /* We are now connecting to a vc. Wait for on_voice_ready + * event, and then send the audio within that event: + * + * event.voice_client->send_audio_raw(...); + * + * NOTE: We can't instantly send audio, as we have to wait for + * the connection to the voice server to be established! + */ + + /* Tell the user we joined their channel. */ + event.reply("Joined your channel!"); + } else { + event.reply("Don't need to join your channel as i'm already there with you!"); + } + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new command. */ + bot.global_command_create(dpp::slashcommand("join", "Joins your voice channel.", bot.me.id)); + } + }); + + /* Start bot */ + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/making_threads1.cpp b/docpages/example_code/making_threads1.cpp new file mode 100644 index 0000000000..36f6db627d --- /dev/null +++ b/docpages/example_code/making_threads1.cpp @@ -0,0 +1,36 @@ +#include + +int main() +{ + /* Create the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when the bot detects a message in any server and any channel it has access to. */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "create-thread") { + /* Here we create a thread in the current channel. It will expire after 60 minutes of inactivity. We'll also allow other mods to join, and we won't add a slowdown timer. */ + bot.thread_create("Cool thread!", event.command.channel_id, 60, dpp::channel_type::CHANNEL_PUBLIC_THREAD, true, 0, [event](const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) { + event.reply("Failed to create a thread!"); + return; + } + + event.reply("Created a thread for you!"); + }); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + /* Create and register the command */ + bot.global_command_create(dpp::slashcommand("create-thread", "Create a thread!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/making_threads2.cpp b/docpages/example_code/making_threads2.cpp new file mode 100644 index 0000000000..21037ff233 --- /dev/null +++ b/docpages/example_code/making_threads2.cpp @@ -0,0 +1,57 @@ +#include + +int main() +{ + /* Create the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when the bot detects a message in any server and any channel it has access to. */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + /* Check which command they ran */ + if (event.command.get_command_name() == "message-thread") { + /* Get all active threads in a guild. */ + bot.threads_get_active(event.command.guild_id, [&bot, event](const dpp::confirmation_callback_t& callback) { + if (callback.is_error()) { + event.reply("Failed to get threads!"); + return; + } + + /* Get the list of active threads in the guild. */ + auto threads = callback.get(); + + dpp::snowflake thread_id; + + /* Loop through the threads, getting each value in the map. Then we get the first value and then break off. + * The reason we're getting only the first value is because, for this example, we'll just assume you've only got a single active thread (the one created by the bot) + */ + for (const auto& _thread : threads) { + thread_id = _thread.first; + break; + } + + /* Send a message in the first thread we find. */ + bot.message_create(dpp::message(thread_id, "Hey, I'm first to message in a cool thread!"), [event](const dpp::confirmation_callback_t& callback2) { + if (callback2.is_error()) { + event.reply("Failed to send a message in a thread."); + return; + } + + event.reply("I've sent a message in the specified thread."); + }); + }); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + /* Create and register the command */ + bot.global_command_create(dpp::slashcommand("message-thread", "Message a thread!", bot.me.id)); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/modal_dialog_interactions.cpp b/docpages/example_code/modal_dialog_interactions.cpp new file mode 100644 index 0000000000..764581d468 --- /dev/null +++ b/docpages/example_code/modal_dialog_interactions.cpp @@ -0,0 +1,73 @@ +#include +#include + +int main(int argc, char const *argv[]) +{ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + /* Check for our /dialog command */ + if (event.command.get_command_name() == "dialog") { + + /* Instantiate an interaction_modal_response object */ + dpp::interaction_modal_response modal("my_modal", "Please enter stuff"); + + /* Add a text component */ + modal.add_component( + dpp::component(). + set_label("Short type rammel"). + set_id("field_id"). + set_type(dpp::cot_text). + set_placeholder("gumd"). + set_min_length(5). + set_max_length(50). + set_text_style(dpp::text_short) + ); + + /* Add another text component in the next row, as required by Discord */ + modal.add_row(); + modal.add_component( + dpp::component(). + set_label("Type rammel"). + set_id("field_id2"). + set_type(dpp::cot_text). + set_placeholder("gumf"). + set_min_length(1). + set_max_length(2000). + set_text_style(dpp::text_paragraph) + ); + + /* Trigger the dialog box. All dialog boxes are ephemeral */ + event.dialog(modal); + } + }); + + /* This event handles form submission for the modal dialog we create above */ + bot.on_form_submit([&](const dpp::form_submit_t & event) { + + /* For this simple example we know the first element of the first row ([0][0]) is value type string. + * In the real world it may not be safe to make such assumptions! + */ + std::string v = std::get(event.components[0].components[0].value); + + dpp::message m; + m.set_content("You entered: " + v).set_flags(dpp::m_ephemeral); + + /* Emit a reply. Form submission is still an interaction and must generate some form of reply! */ + event.reply(m); + }); + + bot.on_ready([&](const dpp::ready_t & event) { + if (dpp::run_once()) { + /* Create a slash command and register it as a global command */ + bot.global_command_create(dpp::slashcommand("dialog", "Make a modal dialog box", bot.me.id)); + } + }); + + /* Start bot */ + + bot.start(dpp::st_wait); + return 0; +} diff --git a/docpages/example_code/mp3.cpp b/docpages/example_code/mp3.cpp new file mode 100644 index 0000000000..db15cb489d --- /dev/null +++ b/docpages/example_code/mp3.cpp @@ -0,0 +1,117 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* For an example we will hardcode a path to some awesome music here */ +#define MUSIC_FILE "/media/music/Rick Astley/Whenever You Need Somebody/Never Gonna Give You Up.mp3" + +int main(int argc, char const *argv[]) +{ + /* This will hold the decoded MP3. + * The D++ library expects PCM format, which are raw sound + * data, 2 channel stereo, 16 bit signed 48000Hz. + */ + std::vector pcmdata; + + mpg123_init(); + + int err = 0; + unsigned char* buffer; + size_t buffer_size, done; + int channels, encoding; + long rate; + + /* Note it is important to force the frequency to 48000 for Discord compatibility */ + mpg123_handle *mh = mpg123_new(NULL, &err); + mpg123_param(mh, MPG123_FORCE_RATE, 48000, 48000.0); + + /* Decode entire file into a vector. You could do this on the fly, but if you do that + * you may get timing issues if your CPU is busy at the time and you are streaming to + * a lot of channels/guilds. + */ + buffer_size = mpg123_outblock(mh); + buffer = new unsigned char[buffer_size]; + + /* Note: In a real world bot, this should have some error logging */ + mpg123_open(mh, MUSIC_FILE); + mpg123_getformat(mh, &rate, &channels, &encoding); + + unsigned int counter = 0; + for (int totalBytes = 0; mpg123_read(mh, buffer, buffer_size, &done) == MPG123_OK; ) { + for (size_t i = 0; i < buffer_size; i++) { + pcmdata.push_back(buffer[i]); + } + counter += buffer_size; + totalBytes += done; + } + delete[] buffer; + mpg123_close(mh); + mpg123_delete(mh); + + /* Setup the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot, &pcmdata](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "join") { + + /* Get the guild */ + dpp::guild* g = dpp::find_guild(event.command.guild_id); + + /* Attempt to connect to a voice channel, returns false if we fail to connect. */ + if (!g->connect_member_voice(event.command.get_issuing_user().id)) { + event.reply("You don't seem to be in a voice channel!"); + return; + } + + /* Tell the user we joined their channel. */ + event.reply("Joined your channel!"); + } else if (event.command.get_command_name() == "mp3") { + + /* Get the voice channel the bot is in, in this current guild. */ + dpp::voiceconn* v = event.from->get_voice(event.command.guild_id); + + /* If the voice channel was invalid, or there is an issue with it, then tell the user. */ + if (!v || !v->voiceclient || !v->voiceclient->is_ready()) { + event.reply("There was an issue with getting the voice channel. Make sure I'm in a voice channel!"); + return; + } + + /* Stream the already decoded MP3 file. This passes the PCM data to the library to be encoded to OPUS */ + v->voiceclient->send_audio_raw((uint16_t*)pcmdata.data(), pcmdata.size()); + + event.reply("Played the mp3 file."); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new command. */ + dpp::slashcommand joincommand("join", "Joins your voice channel.", bot.me.id); + + dpp::slashcommand mp3command("mp3", "Plays an mp3 file.", bot.me.id); + + bot.global_bulk_command_create({joincommand, mp3command}); + } + }); + + /* Start bot */ + bot.start(dpp::st_wait); + + /* Clean up */ + mpg123_exit(); + + return 0; +} diff --git a/docpages/example_code/oggopus.cpp b/docpages/example_code/oggopus.cpp new file mode 100644 index 0000000000..6dd6ca4498 --- /dev/null +++ b/docpages/example_code/oggopus.cpp @@ -0,0 +1,117 @@ +#include +#include +#include + +#include +#include +#include +#include + +int main(int argc, char const *argv[]) +{ + /* Load an ogg opus file into memory. + * The bot expects opus packets to be 2 channel stereo, 48000Hz. + * + * You may use ffmpeg to encode songs to ogg opus: + * ffmpeg -i /path/to/song -c:a libopus -ar 48000 -ac 2 -vn -b:a 96K /path/to/opus.ogg + */ + + /* Setup the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "join") { + + /* Get the guild */ + dpp::guild* g = dpp::find_guild(event.command.guild_id); + + /* Attempt to connect to a voice channel, returns false if we fail to connect. */ + if (!g->connect_member_voice(event.command.get_issuing_user().id)) { + event.reply("You don't seem to be in a voice channel!"); + return; + } + + /* Tell the user we joined their channel. */ + event.reply("Joined your channel!"); + } else if (event.command.get_command_name() == "play") { + + /* Get the voice channel the bot is in, in this current guild. */ + dpp::voiceconn* v = event.from->get_voice(event.command.guild_id); + + /* If the voice channel was invalid, or there is an issue with it, then tell the user. */ + if (!v || !v->voiceclient || !v->voiceclient->is_ready()) { + event.reply("There was an issue with getting the voice channel. Make sure I'm in a voice channel!"); + return; + } + + // load the audio file with oggz + OGGZ *track_og = oggz_open("/path/to/opus.ogg", OGGZ_READ); + + /* If there was an issue reading the file, tell the user and stop */ + if (!track_og) { + fprintf(stderr, "Error opening file\n"); + event.reply("There was an issue opening the file!"); + return; + } + + // set read callback, this callback will be called on packets with the serialno, + // -1 means every packet will be handled with this callback + oggz_set_read_callback( + track_og, -1, + [](OGGZ *oggz, oggz_packet *packet, long serialno, + void *user_data) { + dpp::voiceconn *voiceconn = (dpp::voiceconn *)user_data; + + // send the audio + voiceconn->voiceclient->send_audio_opus(packet->op.packet, + packet->op.bytes); + + // make sure to always return 0 here, read more on oggz documentation + return 0; + }, + // this will be the value of void *user_data + (void *)v + ); + + // read loop + while (v && v->voiceclient && !v->voiceclient->terminating) { + // you can tweak this to whatever. Here I use BUFSIZ, defined in + // stdio.h as 8192 + static const constexpr long CHUNK_READ = BUFSIZ * 2; + + const long read_bytes = oggz_read(track_og, CHUNK_READ); + + // break on eof + if (!read_bytes) + break; + } + + // don't forget to free the memory + oggz_close(track_og); + + event.reply("Finished playing the audio file!"); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new command. */ + dpp::slashcommand joincommand("join", "Joins your voice channel.", bot.me.id); + + dpp::slashcommand playcommand("play", "Plays an ogg file.", bot.me.id); + + bot.global_bulk_command_create({joincommand, playcommand}); + } + }); + + /* Start bot */ + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/private_messaging.cpp b/docpages/example_code/private_messaging.cpp new file mode 100644 index 0000000000..3ec2a38b20 --- /dev/null +++ b/docpages/example_code/private_messaging.cpp @@ -0,0 +1,66 @@ +#include + +int main() +{ + /* Create the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "pm") { + + dpp::snowflake user; + + /* If there was no specified user, we set the "user" variable to the command author (issuing user). */ + if (event.get_parameter("user").index() == 0) { + user = event.command.get_issuing_user().id; + } else { /* Otherwise, we set it to the specified user! */ + user = std::get(event.get_parameter("user")); + } + + /* Send a message to the user set above. */ + bot.direct_message_create(user, dpp::message("Here's a private message!"), [event, user](const dpp::confirmation_callback_t& callback){ + /* If the callback errors, we want to send a message telling the author that something went wrong. */ + if (callback.is_error()) { + /* Here, we want the error message to be different if the user we're trying to send a message to is the command author. */ + if (user == event.command.get_issuing_user().id) { + event.reply(dpp::message("I couldn't send you a message.").set_flags(dpp::m_ephemeral)); + } else { + event.reply(dpp::message("I couldn't send a message to that user. Please check that is a valid user!").set_flags(dpp::m_ephemeral)); + } + + return; + } + + /* We do the same here, so the message is different if it's to the command author or if it's to a specified user. */ + if (user == event.command.get_issuing_user().id) { + event.reply(dpp::message("I've sent you a private message.").set_flags(dpp::m_ephemeral)); + } else { + event.reply(dpp::message("I've sent a message to that user.").set_flags(dpp::m_ephemeral)); + } + }); + } + }); + + bot.on_ready([&bot](const dpp::ready_t& event) { + if (dpp::run_once()) { + + /* Register the command */ + dpp::slashcommand command("pm", "Send a private message.", bot.me.id); + + /* Add the option for a user mention that isn't required */ + command.add_option(dpp::command_option(dpp::co_mentionable, "user", "The user to message", false)); + + /* Register the command */ + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/record_user.cpp b/docpages/example_code/record_user.cpp new file mode 100644 index 0000000000..7d17bb1f26 --- /dev/null +++ b/docpages/example_code/record_user.cpp @@ -0,0 +1,72 @@ +#include +#include +#include + +int main(int argc, char const *argv[]) +{ + /* Example to record a user in a VC + * + * Recording is output as './me.pcm' and you can play it via the soundboard example + * or use ffmpeg 'ffplay -f s16le -ar 48000 -ac 2 -i ./me.pcm' + */ + + /* Replace with the user's id you wish to record */ + dpp::snowflake user_id = 407877550216314882; + + /* Setup the bot */ + dpp::cluster bot("token"); + + FILE *fd; + fd = fopen("./me.pcm", "wb"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot, &fd](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "record") { + + /* Get the guild */ + dpp::guild* g = dpp::find_guild(event.command.guild_id); + + /* Attempt to connect to a voice channel, returns false if we fail to connect. */ + if (!g->connect_member_voice(event.command.get_issuing_user().id)) { + event.reply("You don't seem to be in a voice channel!"); + return; + } + + /* Tell the user we joined their channel. */ + event.reply("Joined your channel, now recording!"); + } else if (event.command.get_command_name() == "stop") { + + event.from->disconnect_voice(event.command.guild_id); + fclose(fd); + + event.reply("Stopped recording."); + } + }); + + bot.on_voice_receive([&bot, &fd, &user_id](const dpp::voice_receive_t &event) { + if (event.user_id == user_id) { + fwrite((char *)event.audio, 1, event.audio_size, fd); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new command. */ + dpp::slashcommand recordcommand("record", "Joins your voice channel and records you.", bot.me.id); + + dpp::slashcommand stopcommand("stop", "Stops recording you.", bot.me.id); + + bot.global_bulk_command_create({recordcommand, stopcommand}); + } + }); + + /* Start bot */ + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/setting_status1.cpp b/docpages/example_code/setting_status1.cpp new file mode 100644 index 0000000000..0c1cc0a451 --- /dev/null +++ b/docpages/example_code/setting_status1.cpp @@ -0,0 +1,20 @@ +#include + +int main() +{ + /* Create the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_ready([&bot](const dpp::ready_t& event) { + /* We don't need the run_once here as we're not registering commands! */ + + /* Set the bot presence as online and "Playing..."! */ + bot.set_presence(dpp::presence(dpp::ps_online, dpp::at_game, "games!")); + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/setting_status2.cpp b/docpages/example_code/setting_status2.cpp new file mode 100644 index 0000000000..947ac4c72c --- /dev/null +++ b/docpages/example_code/setting_status2.cpp @@ -0,0 +1,26 @@ +#include + +int main() +{ + /* Create the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_ready([&bot](const dpp::ready_t& event) { + /* We put our status updating inside "run_once" so that multiple shards don't try do this as "set_presence" updates all the shards. */ + if (dpp::run_once()) { + /* We update the presence now as the timer will do the first execution after the x amount of seconds we specify */ + bot.set_presence(dpp::presence(dpp::presence_status::ps_online, dpp::activity_type::at_game, "with " + std::to_string(dpp::get_guild_cache()->count()) + " guilds!")); + + /* Create a timer that runs every 120 seconds, that sets the status */ + bot.start_timer([&bot](const dpp::timer& timer) { + bot.set_presence(dpp::presence(dpp::presence_status::ps_online, dpp::activity_type::at_game, "with " + std::to_string(dpp::get_guild_cache()->count()) + " guilds!")); + }, 120); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/slashcommands1.cpp b/docpages/example_code/slashcommands1.cpp new file mode 100644 index 0000000000..0f5f4bbf9c --- /dev/null +++ b/docpages/example_code/slashcommands1.cpp @@ -0,0 +1,47 @@ +#include + +int main() +{ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "blep") { + + /* Fetch a parameter value from the command parameters */ + std::string animal = std::get(event.get_parameter("animal")); + + /* Reply to the command. There is an overloaded version of this + * call that accepts a dpp::message so you can send embeds. + */ + event.reply(std::string("Blep! You chose") + animal); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new global command on ready event */ + dpp::slashcommand newcommand("blep", "Send a random adorable animal photo", bot.me.id); + newcommand.add_option( + dpp::command_option(dpp::co_string, "animal", "The type of animal", true). + add_choice(dpp::command_option_choice("Dog", std::string("animal_dog"))). + add_choice(dpp::command_option_choice("Cat", std::string("animal_cat"))). + add_choice(dpp::command_option_choice("Penguin", std::string("animal_penguin") + ) + ) + ); + + /* Register the command */ + bot.global_command_create(newcommand); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/slashcommands2.cpp b/docpages/example_code/slashcommands2.cpp new file mode 100644 index 0000000000..1eb9c4159e --- /dev/null +++ b/docpages/example_code/slashcommands2.cpp @@ -0,0 +1,47 @@ +#include + +int main() +{ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "blep") { + + /* Fetch a parameter value from the command parameters */ + std::string animal = std::get(event.get_parameter("animal")); + + /* Reply to the command. There is an overloaded version of this + * call that accepts a dpp::message so you can send embeds. + */ + event.reply(std::string("Blep! You chose") + animal); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new global command on ready event */ + dpp::slashcommand newcommand("blep", "Send a random adorable animal photo", bot.me.id); + newcommand.add_option( + dpp::command_option(dpp::co_string, "animal", "The type of animal", true). + add_choice(dpp::command_option_choice("Dog", std::string("animal_dog"))). + add_choice(dpp::command_option_choice("Cat", std::string("animal_cat"))). + add_choice(dpp::command_option_choice("Penguin", std::string("animal_penguin") + ) + ) + ); + + /* Register the command */ + bot.guild_command_create(newcommand, 857692897221033129); /* Replace this with the guild id you want */ + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/slashcommands3.cpp b/docpages/example_code/slashcommands3.cpp new file mode 100644 index 0000000000..578d3c3814 --- /dev/null +++ b/docpages/example_code/slashcommands3.cpp @@ -0,0 +1,41 @@ +#include + +int main() +{ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "ping") { + event.reply("Pong!"); + } else if (event.command.get_command_name() == "pong") { + event.reply("Ping!"); + } else if (event.command.get_command_name() == "ding") { + event.reply("Dong!"); + } else if (event.command.get_command_name() == "dong") { + event.reply("Ding!"); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create some commands */ + dpp::slashcommand pingcommand("ping", "Pong!", bot.me.id); + dpp::slashcommand pongcommand("pong", "Ping!", bot.me.id); + dpp::slashcommand dingcommand("ding", "Dong!", bot.me.id); + dpp::slashcommand dongcommand("dong", "Ding!", bot.me.id); + + /* Register our commands in a list using bulk */ + bot.global_bulk_command_create({ pingcommand, pongcommand, dingcommand, dongcommand }); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/slashcommands4.cpp b/docpages/example_code/slashcommands4.cpp new file mode 100644 index 0000000000..3f10950d6f --- /dev/null +++ b/docpages/example_code/slashcommands4.cpp @@ -0,0 +1,42 @@ +#include + +int main() +{ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "ping") { + event.reply("Pong!"); + } else if (event.command.get_command_name() == "pong") { + event.reply("Ping!"); + } else if (event.command.get_command_name() == "ding") { + event.reply("Dong!"); + } else if (event.command.get_command_name() == "dong") { + event.reply("Ding!"); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create some commands */ + dpp::slashcommand pingcommand("ping", "Pong!", bot.me.id); + dpp::slashcommand pongcommand("pong", "Ping!", bot.me.id); + dpp::slashcommand dingcommand("ding", "Dong!", bot.me.id); + dpp::slashcommand dongcommand("dong", "Ding!", bot.me.id); + + /* Register our commands in a list using bulk */ + bot.guild_bulk_command_create({ pingcommand, pongcommand, dingcommand, dongcommand }, 857692897221033129); + + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/soundboard.cpp b/docpages/example_code/soundboard.cpp new file mode 100644 index 0000000000..df1d326dab --- /dev/null +++ b/docpages/example_code/soundboard.cpp @@ -0,0 +1,85 @@ +#include +#include +#include + +int main(int argc, char const *argv[]) +{ + /* Load a sound file called Robot.pcm into memory. + * The bot expects PCM format, which are raw sound data, + * 2 channel stereo, 16 bit signed 48000Hz. + * + * You can use audacity to export these from WAV or MP3 etc. + * + * If you wanted to send a more complicated format, you could + * use a separate library to decode that audio to PCM. For + * example purposes, a raw PCM will suffice. This PCM file can + * be found within the bot's github repo. + */ + uint8_t* robot = nullptr; + size_t robot_size = 0; + std::ifstream input ("../testdata/Robot.pcm", std::ios::in|std::ios::binary|std::ios::ate); + if (input.is_open()) { + robot_size = input.tellg(); + robot = new uint8_t[robot_size]; + input.seekg (0, std::ios::beg); + input.read ((char*)robot, robot_size); + input.close(); + } + + /* Setup the bot */ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot, robot, robot_size](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "join") { + + /* Get the guild */ + dpp::guild* g = dpp::find_guild(event.command.guild_id); + + /* Attempt to connect to a voice channel, returns false if we fail to connect. */ + if (!g->connect_member_voice(event.command.get_issuing_user().id)) { + event.reply("You don't seem to be in a voice channel!"); + return; + } + + /* Tell the user we joined their channel. */ + event.reply("Joined your channel!"); + } else if (event.command.get_command_name() == "robot") { + + /* Get the voice channel the bot is in, in this current guild. */ + dpp::voiceconn* v = event.from->get_voice(event.command.guild_id); + + /* If the voice channel was invalid, or there is an issue with it, then tell the user. */ + if (!v || !v->voiceclient || !v->voiceclient->is_ready()) { + event.reply("There was an issue with getting the voice channel. Make sure I'm in a voice channel!"); + return; + } + + /* Tell the bot to play the sound file 'Robot.pcm' in the current voice channel. */ + v->voiceclient->send_audio_raw((uint16_t*)robot, robot_size); + + event.reply("Played robot."); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new command. */ + dpp::slashcommand joincommand("join", "Joins your voice channel.", bot.me.id); + + dpp::slashcommand robotcommand("robot", "Plays a robot noise in your voice channel.", bot.me.id); + + bot.global_bulk_command_create({joincommand, robotcommand}); + } + }); + + /* Start bot */ + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/subcommands.cpp b/docpages/example_code/subcommands.cpp new file mode 100644 index 0000000000..ac8c65da18 --- /dev/null +++ b/docpages/example_code/subcommands.cpp @@ -0,0 +1,72 @@ +#include +#include + +int main() { + + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* Executes on ready. */ + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + /* Define a slash command. */ + dpp::slashcommand image("image", "Send a specific image.", bot.me.id); + image.add_option( + /* Create a subcommand type option for "dog". */ + dpp::command_option(dpp::co_sub_command, "dog", "Send a picture of a dog."). + add_option(dpp::command_option(dpp::co_user, "user", "User to turn into a dog.", false)) + ); + image.add_option( + /* Create another subcommand type option for "cat". */ + dpp::command_option(dpp::co_sub_command, "cat", "Send a picture of a cat."). + add_option(dpp::command_option(dpp::co_user, "user", "User to turn into a cat.", false)) + ); + /* Create command */ + bot.global_command_create(image); + } + }); + + /* Use the on_slashcommand event to look for commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { + dpp::interaction interaction = event.command; + dpp::command_interaction cmd_data = interaction.get_command_interaction(); + /* Check if the command is the image command. */ + if (interaction.get_command_name() == "image") { + /* Get the sub command */ + auto subcommand = cmd_data.options[0]; + /* Check if the subcommand is "dog" */ + if (subcommand.name == "dog") { + /* Checks if the subcommand has any options. */ + if (!subcommand.options.empty()) { + /* Get the user from the parameter */ + dpp::user user = interaction.get_resolved_user( + subcommand.get_value(0) + ); + event.reply(user.get_mention() + " has now been turned into a dog."); + } else { + /* Reply if there were no options.. */ + event.reply("No user specified"); + } + } + /* Check if the subcommand is "cat" */ + if (subcommand.name == "cat") { + /* Checks if the subcommand has any options. */ + if (!subcommand.options.empty()) { + /* Get the user from the parameter */ + dpp::user user = interaction.get_resolved_user( + subcommand.get_value(0) + ); + event.reply(user.get_mention() + " has now been turned into a cat."); + } else { + /* Reply if there were no options.. */ + event.reply("No user specified"); + } + } + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/upload_parameter.cpp b/docpages/example_code/upload_parameter.cpp new file mode 100644 index 0000000000..dc78829c44 --- /dev/null +++ b/docpages/example_code/upload_parameter.cpp @@ -0,0 +1,43 @@ +#include + +int main() +{ + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + /* The event is fired when someone issues your commands */ + bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { + + /* Check which command they ran */ + if (event.command.get_command_name() == "show") { + + /* Get the file id from the parameter attachment. */ + dpp::snowflake file_id = std::get(event.get_parameter("file")); + + /* Get the attachment that the user inputted from the file id. */ + dpp::attachment att = event.command.get_resolved_attachment(file_id); + + /* Reply with the file as a URL. */ + event.reply(att.url); + } + }); + + bot.on_ready([&bot](const dpp::ready_t & event) { + if (dpp::run_once()) { + + /* Create a new command. */ + dpp::slashcommand newcommand("show", "Show an uploaded file", bot.me.id); + + /* Add a parameter option. */ + newcommand.add_option(dpp::command_option(dpp::co_attachment, "file", "Select an image")); + + /* Register the command */ + bot.global_command_create(newcommand); + } + }); + + bot.start(dpp::st_wait); + + return 0; +} diff --git a/docpages/example_code/webhooks.cpp b/docpages/example_code/webhooks.cpp new file mode 100644 index 0000000000..f35452328c --- /dev/null +++ b/docpages/example_code/webhooks.cpp @@ -0,0 +1,16 @@ +#include + +int main() +{ + dpp::cluster bot(""); // normally, you put your bot token in here. But to just run a webhook its not required + + bot.on_log(dpp::utility::cout_logger()); + + /* construct a webhook object using the URL you got from Discord */ + dpp::webhook wh("https://discord.com/api/webhooks/833047646548133537/ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE"); + + /* send a message with this webhook */ + bot.execute_webhook_sync(wh, dpp::message("Have a great time here :smile:")); + + return 0; +} diff --git a/docpages/example_programs/interactions_and_components/autocomplete.md b/docpages/example_programs/interactions_and_components/autocomplete.md index 87bee8b4fe..6ee9230ce6 100644 --- a/docpages/example_programs/interactions_and_components/autocomplete.md +++ b/docpages/example_programs/interactions_and_components/autocomplete.md @@ -2,73 +2,4 @@ Discord now supports sending auto completion lists for slash command choices. To use this feature you can use code such as the example below: -~~~~~~~~~~{.cpp} -#include - -int main() -{ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new global command once on ready event */ - bot.global_command_create(dpp::slashcommand("blep", "Send a random adorable animal photo", bot.me.id) - .add_option( - /* If you set the auto complete setting on a command option, it will trigger the on_autocomplete - * event whenever discord needs to fill information for the choices. You cannot set any choices - * here if you set the auto complete value to true. - */ - dpp::command_option(dpp::co_string, "animal", "The type of animal").set_auto_complete(true) - ) - ); - } - }); - - /* The interaction create event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "blep") { - /* Fetch a parameter value from the command parameters */ - std::string animal = std::get(event.get_parameter("animal")); - /* Reply to the command. There is an overloaded version of this - * call that accepts a dpp::message so you can send embeds. - */ - event.reply("Blep! You chose " + animal); - } - }); - - /* The on_autocomplete event is fired whenever discord needs information to fill in a command options's choices. - * You must reply with a REST event within 500ms, so make it snappy! - */ - bot.on_autocomplete([&bot](const dpp::autocomplete_t & event) { - for (auto & opt : event.options) { - /* The option which has focused set to true is the one the user is typing in */ - if (opt.focused) { - /* In a real world usage of this function you should return values that loosely match - * opt.value, which contains what the user has typed so far. The opt.value is a variant - * and will contain the type identical to that of the slash command parameter. - * Here we can safely know it is string. - */ - std::string uservalue = std::get(opt.value); - bot.interaction_response_create(event.command.id, event.command.token, dpp::interaction_response(dpp::ir_autocomplete_reply) - .add_autocomplete_choice(dpp::command_option_choice("squids", "lots of squids")) - .add_autocomplete_choice(dpp::command_option_choice("cats", "a few cats")) - .add_autocomplete_choice(dpp::command_option_choice("dogs", "bucket of dogs")) - .add_autocomplete_choice(dpp::command_option_choice("elephants", "bottle of elephants")) - ); - bot.log(dpp::ll_debug, "Autocomplete " + opt.name + " with value '" + uservalue + "' in field " + event.name); - break; - } - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ - +\include{cpp} autocomplete.cpp diff --git a/docpages/example_programs/interactions_and_components/commandhandler.md b/docpages/example_programs/interactions_and_components/commandhandler.md index 140fc9aab3..69b84caf6b 100644 --- a/docpages/example_programs/interactions_and_components/commandhandler.md +++ b/docpages/example_programs/interactions_and_components/commandhandler.md @@ -12,59 +12,4 @@ and allows you to decide how and where commands should be routed, either to an o \warning As of [August 30th, 2022](https://support-dev.discord.com/hc/en-us/articles/6025578854295-Why-We-Moved-to-Slash-Commands), you are advised to only be using slash commands, not messages for commands. To prevent the command handler from handling commands with messages, you should only use the "/" prefix. If you wish to still use messages for commands, this tutorial will still cover it but, again, it is discouraged by Discord. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include - -int main() -{ - /* If your bot only uses the "/" prefix, you can remove the intents here. */ - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); - - bot.on_log(dpp::utility::cout_logger()); - - /* Create command handler, and specify prefixes */ - dpp::commandhandler command_handler(&bot); - /* Specifying a prefix of "/" tells the command handler it should also expect slash commands. Remove the .add_prefix(".") if you wish to only make it a slash command */ - command_handler.add_prefix(".").add_prefix("/"); - - bot.on_ready([&command_handler](const dpp::ready_t &event) { - - command_handler.add_command( - /* Command name */ - "ping", - - /* Parameters */ - { - {"testparameter", dpp::param_info(dpp::pt_string, true, "Optional test parameter") } - }, - - /* Command handler */ - [&command_handler](const std::string& command, const dpp::parameter_list_t& parameters, dpp::command_source src) { - std::string got_param; - if (!parameters.empty()) { - got_param = std::get(parameters[0].second); - } - command_handler.reply(dpp::message("Pong! -> " + got_param), src); - }, - - /* Command description */ - "A test ping command", - - /* Guild id (omit for a guild command) */ - 819556414099554344 - ); - - /* NOTE: We must call this to ensure slash commands are registered. - * This does a bulk register, which will replace other commands - * that are registered already! - */ - command_handler.register_commands(); - - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - +\include{cpp} commandhandler.cpp diff --git a/docpages/example_programs/interactions_and_components/components.md b/docpages/example_programs/interactions_and_components/components.md index 90eeca928a..dfadf95004 100644 --- a/docpages/example_programs/interactions_and_components/components.md +++ b/docpages/example_programs/interactions_and_components/components.md @@ -3,65 +3,7 @@ Discord's newest features support sending buttons alongside messages, which when clicked by the user trigger an interaction which is routed by D++ as an `on_button_click` event. To make use of this, use this code as in this example. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include -#include - -int main() { - - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "button") { - - /* Create a message */ - dpp::message msg(event.command.channel_id, "this text has a button"); - - /* Add an action row, and then a button within the action row. */ - msg.add_component( - dpp::component().add_component( - dpp::component(). - set_label("Click me!"). - set_type(dpp::cot_button). - set_emoji(dpp::unicode_emoji::smile). - set_style(dpp::cos_danger). - set_id("myid") - ) - ); - - /* Reply to the user with our message. */ - event.reply(msg); - } - }); - - /* When a user clicks your button, the on_button_click event will fire, - * containing the custom_id you defined in your button. - */ - bot.on_button_click([&bot](const dpp::button_click_t& event) { - /* Button clicks are still interactions, and must be replied to in some form to - * prevent the "this interaction has failed" message from Discord to the user. - */ - event.reply("You clicked: " + event.custom_id); - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Create and register a command when the bot is ready */ - bot.global_command_create(dpp::slashcommand("button", "Send a message with a button!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} components.cpp When the feature is functioning, the code below will produce buttons on the reply message like in the image below: diff --git a/docpages/example_programs/interactions_and_components/components2.md b/docpages/example_programs/interactions_and_components/components2.md index 1bc4ae8599..5d306ac47c 100644 --- a/docpages/example_programs/interactions_and_components/components2.md +++ b/docpages/example_programs/interactions_and_components/components2.md @@ -2,70 +2,7 @@ This example demonstrates adding multiple buttons, receiving button clicks and sending response messages. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include - -int main() { - - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "math") { - - /* Create a message */ - dpp::message msg(event.command.channel_id, "What is 5+5?"); - - /* Add an action row, and then 3 buttons within the action row. */ - msg.add_component( - dpp::component().add_component( - dpp::component(). - set_label("9"). - set_style(dpp::cos_primary). - set_id("9") - ).add_component( - dpp::component(). - set_label("10"). - set_style(dpp::cos_primary). - set_id("10") - ).add_component( - dpp::component(). - set_label("11"). - set_style(dpp::cos_primary). - set_id("11") - ) - ); - - /* Reply to the user with our message. */ - event.reply(msg); - } - }); - - bot.on_button_click([&bot](const dpp::button_click_t & event) { - if (event.custom_id == "10") { - event.reply(dpp::message("You got it right!").set_flags(dpp::m_ephemeral)); - } else { - event.reply(dpp::message("Wrong! Try again.").set_flags(dpp::m_ephemeral)); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Create and register a command when the bot is ready */ - bot.global_command_create(dpp::slashcommand("math", "A quick maths question!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} components2.cpp This code will send a different message for correct and incorrect answers. diff --git a/docpages/example_programs/interactions_and_components/components3.md b/docpages/example_programs/interactions_and_components/components3.md index 359a101c6b..e3ac023c23 100644 --- a/docpages/example_programs/interactions_and_components/components3.md +++ b/docpages/example_programs/interactions_and_components/components3.md @@ -2,62 +2,4 @@ This example demonstrates creating a select menu, receiving select menu clicks and sending a response message. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include -#include - -int main() { - - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "select") { - - /* Create a message */ - dpp::message msg(event.command.channel_id, "This text has a select menu!"); - - /* Add an action row, and a select menu within the action row. */ - msg.add_component( - dpp::component().add_component( - dpp::component(). - set_type(dpp::cot_selectmenu). - set_placeholder("Pick something"). - add_select_option(dpp::select_option("label1","value1","description1").set_emoji(dpp::unicode_emoji::smile)). - add_select_option(dpp::select_option("label2","value2","description2").set_emoji(dpp::unicode_emoji::slight_smile)). - set_id("myselectid") - ) - ); - - /* Reply to the user with our message. */ - event.reply(msg); - } - }); - - /* When a user clicks your select menu , the on_select_click event will fire, - * containing the custom_id you defined in your select menu. - */ - bot.on_select_click([&bot](const dpp::select_click_t & event) { - /* Select clicks are still interactions, and must be replied to in some form to - * prevent the "this interaction has failed" message from Discord to the user. - */ - event.reply("You clicked " + event.custom_id + " and chose: " + event.values[0]); - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Create and register a command when the bot is ready */ - bot.global_command_create(dpp::slashcommand("select", "Select something at random!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} components3.cpp diff --git a/docpages/example_programs/interactions_and_components/context_menus.md b/docpages/example_programs/interactions_and_components/context_menus.md index d810fcc0a0..cd451998d6 100644 --- a/docpages/example_programs/interactions_and_components/context_menus.md +++ b/docpages/example_programs/interactions_and_components/context_menus.md @@ -8,47 +8,7 @@ Context menus are application commands that appear on the context menu (right cl The following example shows how to create and handle **user context menus** for message context menus, read the notice above. -~~~~~~~~~~{.cpp} -#include -#include - -int main() -{ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* Use the on_user_context_menu event to look for user context menu actions */ - bot.on_user_context_menu([&](const dpp::user_context_menu_t& event) { - - /* check if the context menu name is High Five */ - if (event.command.get_command_name() == "high five") { - dpp::user user = event.get_user(); // the user who the command has been issued on - dpp::user author = event.command.get_issuing_user(); // the user who clicked on the context menu - event.reply(author.get_mention() + " slapped " + user.get_mention()); - } - }); - - bot.on_ready([&bot](const dpp::ready_t &event) { - if (dpp::run_once()) { - - /* Create the command */ - dpp::slashcommand command; - command.set_name("High Five"); - command.set_application_id(bot.me.id); - command.set_type(dpp::ctxm_user); - - /* Register the command */ - bot.guild_command_create(command, 857692897221033129); /* Replace this with the guild id you want */ - } - }); - - /* Start bot */ - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} context_menus.cpp It registers a guild command that can be called by right-clicking a user and clicking on the created menu. diff --git a/docpages/example_programs/interactions_and_components/detecting-messages.md b/docpages/example_programs/interactions_and_components/detecting-messages.md index a3278c85a5..2964f29ea3 100644 --- a/docpages/example_programs/interactions_and_components/detecting-messages.md +++ b/docpages/example_programs/interactions_and_components/detecting-messages.md @@ -4,32 +4,7 @@ Sometimes, you may want to listen out for a message, rather than a command. This \warning As of August 30th, 2022, Discord made Message Content a privileged intent. Whilst this means you can still use prefixed messages as commands, Discord does not encourage this and heavily suggests you use \ref slashcommands "slash commands". If you wish to create commands, use \ref slashcommands "slash commands", not messages. -~~~~~~~~~~{.cpp} -#include - -int main() -{ - /* Create the bot, but with our intents so we can use messages. */ - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when the bot detects a message in any server and any channel it has access to. */ - bot.on_message_create([&bot](const dpp::message_create_t& event) { - - /* See if the message contains the phrase we want to check for. - * If there's at least a single match, we reply and say it's not allowed. - */ - if (event.msg.content.find("bad word") != std::string::npos) { - event.reply("That is not allowed here. Please, mind your language!", true); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} detecting_messages.cpp If all went well, you should have something like this! diff --git a/docpages/example_programs/interactions_and_components/making_threads.md b/docpages/example_programs/interactions_and_components/making_threads.md index 25822e0ba6..ee24e614d6 100644 --- a/docpages/example_programs/interactions_and_components/making_threads.md +++ b/docpages/example_programs/interactions_and_components/making_threads.md @@ -5,110 +5,16 @@ A new feature added to Discord recently is `Threads`, these allow you to break o In this tutorial, we'll be going through how to create a thread and how to talk in a thread. First, let's go through creating a thread. -~~~~~~~~~~{.cpp} -#include -int main() -{ - /* Create the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when the bot detects a message in any server and any channel it has access to. */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - /* Check which command they ran */ - if (event.command.get_command_name() == "create-thread") { - /* Here we create a thread in the current channel. It will expire after 60 minutes of inactivity. We'll also allow other mods to join, and we won't add a slowdown timer. */ - bot.thread_create("Cool thread!", event.command.channel_id, 60, dpp::channel_type::CHANNEL_PUBLIC_THREAD, true, 0, [event](const dpp::confirmation_callback_t& callback) { - if (callback.is_error()) { - event.reply("Failed to create a thread!"); - return; - } - - event.reply("Created a thread for you!"); - }); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - /* Create and register the command */ - bot.global_command_create(dpp::slashcommand("create-thread", "Create a thread!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} making_threads1.cpp If all went well, you'll see that the bot has successfully created a thread! \image html creating_thread.png Now, let's cover talking in that thread from a channel. It's worth noting that we will be assuming that the thread you just created is the only thread in your server! -~~~~~~~~~~{.cpp} - -#include - -int main() -{ - /* Create the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when the bot detects a message in any server and any channel it has access to. */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - /* Check which command they ran */ - if (event.command.get_command_name() == "message-thread") { - /* Get all active threads in a guild. */ - bot.threads_get_active(event.command.guild_id, [&bot, event](const dpp::confirmation_callback_t& callback) { - if (callback.is_error()) { - event.reply("Failed to get threads!"); - return; - } - - /* Get the list of active threads in the guild. */ - auto threads = callback.get(); - - dpp::snowflake thread_id; - - /* Loop through the threads, getting each value in the map. Then we get the first value and then break off. - * The reason we're getting only the first value is because, for this example, we'll just assume you've only got a single active thread (the one created by the bot) - */ - for (const auto& _thread : threads) { - thread_id = _thread.first; - break; - } - - /* Send a message in the first thread we find. */ - bot.message_create(dpp::message(thread_id, "Hey, I'm first to message in a cool thread!"), [event](const dpp::confirmation_callback_t& callback2) { - if (callback2.is_error()) { - event.reply("Failed to send a message in a thread."); - return; - } - - event.reply("I've sent a message in the specified thread."); - }); - }); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - /* Create and register the command */ - bot.global_command_create(dpp::slashcommand("message-thread", "Message a thread!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - return 0; -} -~~~~~~~~~~ +\include{cpp} making_threads2.cpp After that, you'll be able to see your bot send a message in your thread! diff --git a/docpages/example_programs/interactions_and_components/modal_dialog_interactions.md b/docpages/example_programs/interactions_and_components/modal_dialog_interactions.md index f9a99bcde6..bb63048202 100644 --- a/docpages/example_programs/interactions_and_components/modal_dialog_interactions.md +++ b/docpages/example_programs/interactions_and_components/modal_dialog_interactions.md @@ -4,81 +4,7 @@ Modal dialog interactions are a new Discord API feature that allow you to have p Each dialog box may have up to five rows of input fields. The example below demonstrates a simple setup with just one text input: -~~~~~~~~~~{.cpp} -#include -#include - -int main(int argc, char const *argv[]) -{ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - /* Check for our /dialog command */ - if (event.command.get_command_name() == "dialog") { - - /* Instantiate an interaction_modal_response object */ - dpp::interaction_modal_response modal("my_modal", "Please enter stuff"); - - /* Add a text component */ - modal.add_component( - dpp::component(). - set_label("Short type rammel"). - set_id("field_id"). - set_type(dpp::cot_text). - set_placeholder("gumd"). - set_min_length(5). - set_max_length(50). - set_text_style(dpp::text_short) - ); - - /* Add another text component in the next row, as required by Discord */ - modal.add_row(); - modal.add_component( - dpp::component(). - set_label("Type rammel"). - set_id("field_id2"). - set_type(dpp::cot_text). - set_placeholder("gumf"). - set_min_length(1). - set_max_length(2000). - set_text_style(dpp::text_paragraph) - ); - - /* Trigger the dialog box. All dialog boxes are ephemeral */ - event.dialog(modal); - } - }); - - /* This event handles form submission for the modal dialog we create above */ - bot.on_form_submit([&](const dpp::form_submit_t & event) { - - /* For this simple example we know the first element of the first row ([0][0]) is value type string. - * In the real world it may not be safe to make such assumptions! - */ - std::string v = std::get(event.components[0].components[0].value); - - dpp::message m; - m.set_content("You entered: " + v).set_flags(dpp::m_ephemeral); - - /* Emit a reply. Form submission is still an interaction and must generate some form of reply! */ - event.reply(m); - }); - - bot.on_ready([&](const dpp::ready_t & event) { - if (dpp::run_once()) { - /* Create a slash command and register it as a global command */ - bot.global_command_create(dpp::slashcommand("dialog", "Make a modal dialog box", bot.me.id)); - } - }); - - /* Start bot */ - - bot.start(dpp::st_wait); - return 0; -} -~~~~~~~~~~ +\include{cpp} modal_dialog_interactions.cpp If you compile and run this program and wait for the global command to register, typing `/dialog` will present you with a dialog box like the one below: diff --git a/docpages/example_programs/interactions_and_components/private-messaging.md b/docpages/example_programs/interactions_and_components/private-messaging.md index 1d6c4e7037..9460449e8e 100644 --- a/docpages/example_programs/interactions_and_components/private-messaging.md +++ b/docpages/example_programs/interactions_and_components/private-messaging.md @@ -4,74 +4,7 @@ Sometimes it's simply not enough to ping someone in a server with a message, and \note This tutorial makes use of callbacks. For more information about that, visit \ref callback-functions "Using Callback Functions". -~~~~~~~~~~{.cpp} -#include - -int main() -{ - /* Create the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "pm") { - - dpp::snowflake user; - - /* If there was no specified user, we set the "user" variable to the command author (issuing user). */ - if (event.get_parameter("user").index() == 0) { - user = event.command.get_issuing_user().id; - } else { /* Otherwise, we set it to the specified user! */ - user = std::get(event.get_parameter("user")); - } - - /* Send a message to the user set above. */ - bot.direct_message_create(user, dpp::message("Here's a private message!"), [event, user](const dpp::confirmation_callback_t& callback){ - /* If the callback errors, we want to send a message telling the author that something went wrong. */ - if (callback.is_error()) { - /* Here, we want the error message to be different if the user we're trying to send a message to is the command author. */ - if (user == event.command.get_issuing_user().id) { - event.reply(dpp::message("I couldn't send you a message.").set_flags(dpp::m_ephemeral)); - } else { - event.reply(dpp::message("I couldn't send a message to that user. Please check that is a valid user!").set_flags(dpp::m_ephemeral)); - } - - return; - } - - /* We do the same here, so the message is different if it's to the command author or if it's to a specified user. */ - if (user == event.command.get_issuing_user().id) { - event.reply(dpp::message("I've sent you a private message.").set_flags(dpp::m_ephemeral)); - } else { - event.reply(dpp::message("I've sent a message to that user.").set_flags(dpp::m_ephemeral)); - } - }); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Register the command */ - dpp::slashcommand command("pm", "Send a private message.", bot.me.id); - - /* Add the option for a user mention that isn't required */ - command.add_option(dpp::command_option(dpp::co_mentionable, "user", "The user to message", false)); - - /* Register the command */ - bot.global_command_create(command); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} private_messaging.cpp That's it! Now, you should have something like this: diff --git a/docpages/example_programs/interactions_and_components/slashcommands.md b/docpages/example_programs/interactions_and_components/slashcommands.md index 57f3bac606..a6d7ea0bdc 100644 --- a/docpages/example_programs/interactions_and_components/slashcommands.md +++ b/docpages/example_programs/interactions_and_components/slashcommands.md @@ -12,199 +12,18 @@ dpp::interaction_create_t::reply has two overloaded versions of the method, one This first example goes over creating a single command globally. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include - -int main() -{ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "blep") { - - /* Fetch a parameter value from the command parameters */ - std::string animal = std::get(event.get_parameter("animal")); - - /* Reply to the command. There is an overloaded version of this - * call that accepts a dpp::message so you can send embeds. - */ - event.reply(std::string("Blep! You chose") + animal); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new global command on ready event */ - dpp::slashcommand newcommand("blep", "Send a random adorable animal photo", bot.me.id); - newcommand.add_option( - dpp::command_option(dpp::co_string, "animal", "The type of animal", true). - add_choice(dpp::command_option_choice("Dog", std::string("animal_dog"))). - add_choice(dpp::command_option_choice("Cat", std::string("animal_cat"))). - add_choice(dpp::command_option_choice("Penguin", std::string("animal_penguin") - ) - ) - ); - - /* Register the command */ - bot.global_command_create(newcommand); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} slashcommands1.cpp This second example goes over creating a single command but only for a guild, this means that the command can not be accessed anywhere else but the guild specified. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include - -int main() -{ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "blep") { - - /* Fetch a parameter value from the command parameters */ - std::string animal = std::get(event.get_parameter("animal")); - - /* Reply to the command. There is an overloaded version of this - * call that accepts a dpp::message so you can send embeds. - */ - event.reply(std::string("Blep! You chose") + animal); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new global command on ready event */ - dpp::slashcommand newcommand("blep", "Send a random adorable animal photo", bot.me.id); - newcommand.add_option( - dpp::command_option(dpp::co_string, "animal", "The type of animal", true). - add_choice(dpp::command_option_choice("Dog", std::string("animal_dog"))). - add_choice(dpp::command_option_choice("Cat", std::string("animal_cat"))). - add_choice(dpp::command_option_choice("Penguin", std::string("animal_penguin") - ) - ) - ); - - /* Register the command */ - bot.guild_command_create(newcommand, 857692897221033129); /* Replace this with the guild id you want */ - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} slashcommands2.cpp This third example goes over creating four commands globally, using the bulk create method. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include - -int main() -{ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "ping") { - event.reply("Pong!"); - } else if (event.command.get_command_name() == "pong") { - event.reply("Ping!"); - } else if (event.command.get_command_name() == "ding") { - event.reply("Dong!"); - } else if (event.command.get_command_name() == "dong") { - event.reply("Ding!"); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create some commands */ - dpp::slashcommand pingcommand("ping", "Pong!", bot.me.id); - dpp::slashcommand pongcommand("pong", "Ping!", bot.me.id); - dpp::slashcommand dingcommand("ding", "Dong!", bot.me.id); - dpp::slashcommand dongcommand("dong", "Ding!", bot.me.id); - - /* Register our commands in a list using bulk */ - bot.global_bulk_command_create({ pingcommand, pongcommand, dingcommand, dongcommand }); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} slashcommands3.cpp This fourth example goes over creating four commands but only for a guild. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include - -int main() -{ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "ping") { - event.reply("Pong!"); - } else if (event.command.get_command_name() == "pong") { - event.reply("Ping!"); - } else if (event.command.get_command_name() == "ding") { - event.reply("Dong!"); - } else if (event.command.get_command_name() == "dong") { - event.reply("Ding!"); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create some commands */ - dpp::slashcommand pingcommand("ping", "Pong!", bot.me.id); - dpp::slashcommand pongcommand("pong", "Ping!", bot.me.id); - dpp::slashcommand dingcommand("ding", "Dong!", bot.me.id); - dpp::slashcommand dongcommand("dong", "Ding!", bot.me.id); - - /* Register our commands in a list using bulk */ - bot.guild_bulk_command_create({ pingcommand, pongcommand, dingcommand, dongcommand }, 857692897221033129); - - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} slashcommands4.cpp \note For demonstration purposes, and small bots, this code is OK, but in the real world once your bot gets big, it's not recommended to create slash commands in the `on_ready` event even when it's inside dpp::run_once as, if you re-run your bot multiple times or start multiple clusters, you will quickly get rate-limited! You could, for example, add a commandline parameter to your bot (`argc`, `argv`) so that if you want the bot to register commands it must be launched with a specific command line argument. \ No newline at end of file diff --git a/docpages/example_programs/interactions_and_components/subcommands.md b/docpages/example_programs/interactions_and_components/subcommands.md index 250463332d..68755bd682 100644 --- a/docpages/example_programs/interactions_and_components/subcommands.md +++ b/docpages/example_programs/interactions_and_components/subcommands.md @@ -2,77 +2,4 @@ This demonstrates how to use sub-commands within slash commands. Also shown below is an example of how to get a "resolved" parameter without having to use the cache or an extra API call. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include -#include - -int main() { - - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* Executes on ready. */ - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - /* Define a slash command. */ - dpp::slashcommand image("image", "Send a specific image.", bot.me.id); - image.add_option( - /* Create a subcommand type option for "dog". */ - dpp::command_option(dpp::co_sub_command, "dog", "Send a picture of a dog."). - add_option(dpp::command_option(dpp::co_user, "user", "User to turn into a dog.", false)) - ); - image.add_option( - /* Create another subcommand type option for "cat". */ - dpp::command_option(dpp::co_sub_command, "cat", "Send a picture of a cat."). - add_option(dpp::command_option(dpp::co_user, "user", "User to turn into a cat.", false)) - ); - /* Create command */ - bot.global_command_create(image); - } - }); - - /* Use the on_slashcommand event to look for commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - dpp::interaction interaction = event.command; - dpp::command_interaction cmd_data = interaction.get_command_interaction(); - /* Check if the command is the image command. */ - if (interaction.get_command_name() == "image") { - /* Get the sub command */ - auto subcommand = cmd_data.options[0]; - /* Check if the subcommand is "dog" */ - if (subcommand.name == "dog") { - /* Checks if the subcommand has any options. */ - if (!subcommand.options.empty()) { - /* Get the user from the parameter */ - dpp::user user = interaction.get_resolved_user( - subcommand.get_value(0) - ); - event.reply(user.get_mention() + " has now been turned into a dog."); - } else { - /* Reply if there were no options.. */ - event.reply("No user specified"); - } - } - /* Check if the subcommand is "cat" */ - if (subcommand.name == "cat") { - /* Checks if the subcommand has any options. */ - if (!subcommand.options.empty()) { - /* Get the user from the parameter */ - dpp::user user = interaction.get_resolved_user( - subcommand.get_value(0) - ); - event.reply(user.get_mention() + " has now been turned into a cat."); - } else { - /* Reply if there were no options.. */ - event.reply("No user specified"); - } - } - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} subcommands.cpp \ No newline at end of file diff --git a/docpages/example_programs/interactions_and_components/upload_parameter.md b/docpages/example_programs/interactions_and_components/upload_parameter.md index f971f20926..d73e878835 100644 --- a/docpages/example_programs/interactions_and_components/upload_parameter.md +++ b/docpages/example_programs/interactions_and_components/upload_parameter.md @@ -7,48 +7,4 @@ section, `event.command.resolved`. The file is uploaded to Discord's CDN so if you need it locally you should fetch the `.url` value, e.g. by using something like dpp::cluster::request(). -~~~~~~~~~~~~~~~~{.cpp} -#include - -int main() -{ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "show") { - - /* Get the file id from the parameter attachment. */ - dpp::snowflake file_id = std::get(event.get_parameter("file")); - - /* Get the attachment that the user inputted from the file id. */ - dpp::attachment att = event.command.get_resolved_attachment(file_id); - - /* Reply with the file as a URL. */ - event.reply(att.url); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new command. */ - dpp::slashcommand newcommand("show", "Show an uploaded file", bot.me.id); - - /* Add a parameter option. */ - newcommand.add_option(dpp::command_option(dpp::co_attachment, "file", "Select an image")); - - /* Register the command */ - bot.global_command_create(newcommand); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~ +\include{cpp} upload_parameter.cpp diff --git a/docpages/example_programs/interactions_and_components/user-only-messages.md b/docpages/example_programs/interactions_and_components/user-only-messages.md index 36b182cfe2..7576efc0d1 100644 --- a/docpages/example_programs/interactions_and_components/user-only-messages.md +++ b/docpages/example_programs/interactions_and_components/user-only-messages.md @@ -4,40 +4,7 @@ If you've used a discord bot, there's a chance that you've encountered a message Here's how you can do exactly that! -~~~~~~~~~~{.cpp} -#include - -int main() -{ - /* Create the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t & event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "hello") { - - /* Reply to the user, but only let them see the response. */ - event.reply(dpp::message("Hello! How are you today?").set_flags(dpp::m_ephemeral)); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create and Register the command */ - bot.global_command_create(dpp::slashcommand("hello", "Hello there!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} ephemeral.cpp That's it! If everything went well, it should look like this: diff --git a/docpages/example_programs/misc/cache_messages.md b/docpages/example_programs/misc/cache_messages.md index 865cedd8f5..25c9782724 100644 --- a/docpages/example_programs/misc/cache_messages.md +++ b/docpages/example_programs/misc/cache_messages.md @@ -8,66 +8,4 @@ This can be adjusted to cache any type derived from dpp::managed including types you should use the dpp::cache::remove() method periodically to remove stale items. This is left out of this example as a learning exercise to the reader. For further reading please see the documentation of dpp::cache -~~~~~~~~~~{.cpp} -#include -#include - -int main() { - /* Create bot */ - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); /* Because we're handling messages, we need to use the "i_message_content" intent! */ - - /* Create a cache to contain types of dpp::message */ - dpp::cache message_cache; - - bot.on_log(dpp::utility::cout_logger()); - - /* Message handler */ - bot.on_message_create([&](const dpp::message_create_t &event) { - /* Make a permanent pointer using new, for each message to be cached */ - dpp::message* m = new dpp::message(); - - /* Store the message into the pointer by copying it */ - *m = event.msg; - - /* Store the new pointer to the cache using the store() method */ - message_cache.store(m); - }); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - /* Check which command they ran */ - if (event.command.get_command_name() == "get") { - - dpp::message* find_msg = message_cache.find(std::get(event.get_parameter("message_id"))); - - /* If find_msg is null, tell the user and return. */ - if (!find_msg) { - event.reply("There is no message cached with this ID"); - return; - } - - event.reply("This message had the following content: " + find_msg->content); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Create a new command. */ - dpp::slashcommand newcommand("get", "Get the contents of a message that was cached via an id", bot.me.id); - - /* Add a parameter option. */ - newcommand.add_option(dpp::command_option(dpp::co_string, "message_id", "The ID of the message you want to find", true)); - - /* Register the command */ - bot.global_command_create(newcommand); - } - }); - - /* Start bot */ - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ - +\include{cpp} cache_messages.cpp diff --git a/docpages/example_programs/misc/collect_reactions.md b/docpages/example_programs/misc/collect_reactions.md index 6a290e43d5..cbd671e5fa 100644 --- a/docpages/example_programs/misc/collect_reactions.md +++ b/docpages/example_programs/misc/collect_reactions.md @@ -4,52 +4,5 @@ D++ comes with many useful helper classes, but amongst these is something called In the example below we will use it to collect all reactions on a message. -~~~~~~~~~~{.cpp} -#include - -/* To create a collector we must derive from dpp::collector. As dpp::collector is a complicated template, - * various pre-made forms exist such as this one, reaction_collector. - */ -class react_collector : public dpp::reaction_collector { -public: - /* Collector will run for 20 seconds */ - react_collector(dpp::cluster* cl, snowflake id) : dpp::reaction_collector(cl, 20, id) { } - - /* Override the "completed" event and then output the number of collected reactions as a message. */ - virtual void completed(const std::vector& list) override { - if (list.size()) { - owner->message_create(dpp::message(list[0].react_channel->id, "I collected " + std::to_string(list.size()) + " reactions!")); - } else { - owner->message_create(dpp::message("... I got nothin'.")); - } - } -}; - - -int main() { - /* Create bot */ - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); - - /* Pointer to reaction collector */ - react_collector* r = nullptr; - - bot.on_log(dpp::utility::cout_logger()); - - /* Message handler */ - bot.on_message_create([&](const dpp::message_create_t& event) { - - /* If someone sends a message that has the text 'collect reactions!' start a reaction collector */ - if (event.msg.content == "collect reactions!" && r == nullptr) { - /* Create a new reaction collector to collect reactions */ - r = new react_collector(&bot, event.msg.id); - } - - }); - - /* Start bot */ - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} collect_reactions.cpp diff --git a/docpages/example_programs/misc/eval.md b/docpages/example_programs/misc/eval.md index 6f65bb300f..4eb4972eb7 100644 --- a/docpages/example_programs/misc/eval.md +++ b/docpages/example_programs/misc/eval.md @@ -31,204 +31,13 @@ Docker is definitely recommended if you code on Windows/Mac OS, because docker d Remember that `eval.h` contains forward-declarations of any functions you want to expose to the eval command. It is included both by the bot itself, and by any shared object files compiled for evaluation. -~~~~~~~~~~~~~~~~{.cpp} -#pragma once - -/* This is the snowflake ID of the bot's developer. - * The eval command will be restricted to this user. - */ -#define MY_DEVELOPER 189759562910400512 - -/* Any functions you want to be usable from within an eval, - * that are not part of D++ itself or the message event, you - * can put here as forward declarations. The test_function() - * serves as an example. - */ - -int test_function(); -~~~~~~~~~~~~~~~~ +\include{cpp} eval.h #### eval.cpp This is the main body of the example program. -~~~~~~~~~~~~~~~~{.cpp} -/** - * D++ eval command example. - * This is dangerous and for educational use only, here be dragons! - */ - -#include -#include -#include -#include -/* We have to define this to make certain functions visible */ -#ifndef _GNU_SOURCE - #define _GNU_SOURCE -#endif -#include -#include -#include "eval.h" - -/* This is an example function you can expose to your eval command */ -int test_function() { - return 42; -} - -/* Important: This code is for UNIX-like systems only, e.g. - * Linux, BSD, OSX. It will NOT work on Windows! - * Note for OSX you'll probably have to change all references - * from .so to .dylib. - */ -int main() -{ - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); - - bot.on_log(dpp::utility::cout_logger()); - - /* This won't work in a slash command very well yet, as there is not yet - * a multi-line slash command input type. - */ - bot.on_message_create([&bot](const auto & event) { - if (dpp::utility::utf8substr(event.msg.content, 0, 5) == "!eval") { - - /** - * THIS IS CRITICALLY IMPORTANT! - * Never EVER make an eval command that isn't restricted to a specific developer by user id. - * With access to this command the person who invokes it has at best full control over - * your bot's user account and at worst, full control over your entire network!!! - * Eval commands are Evil (pun intended) and could even be considered a security - * vulnerability. YOU HAVE BEEN WARNED! - */ - if (event.msg.author.id != MY_DEVELOPER) { - bot.message_create(dpp::message(event.msg.channel_id, "On the day i do this for you, Satan will be ice skating to work.")); - return; - } - - /* We start by creating a string that contains a cpp program for a simple library. - * The library will contain one exported function called so_exec() that is called - * containing the raw C++ code to eval. - */ - std::string code = "#include \n\ - #include \n\ - #include \n\ - #include \n\ - #include \n\ - #include \n\ - #include \n\ - #include \n\ - #include \"eval.h\"\n\ - extern \"C\" void so_exec(dpp::cluster& bot, dpp::message_create_t event) {\n\ - " + dpp::utility::utf8substr( - event.msg.content, - 6, - dpp::utility::utf8len(event.msg.content) - ) + ";\n\ - return;\n\ - }"; - - /* Next we output this string full of C++ to a cpp file on disk. - * This code assumes the current directory is writeable. The file will have a - * unique name made from the user's id and the message id. - */ - std::string source_filename = std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".cpp"; - std::fstream code_file(source_filename, std::fstream::binary | std::fstream::out); - if (!code_file.is_open()) { - bot.message_create(dpp::message(event.msg.channel_id, "Unable to create source file for `eval`")); - return; - } - code_file << code; - code_file.close(); - - /* Now to actually compile the file. We use dpp::utility::exec to - * invoke a compiler. This assumes you are using g++, and it is in your path. - */ - double compile_start = dpp::utility::time_f(); - dpp::utility::exec("g++", { - "-std=c++17", - "-shared", /* Build the output as a .so file */ - "-fPIC", - std::string("-o") + std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".so", - std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".cpp", - "-ldpp", - "-ldl" - }, [event, &bot, source_filename, compile_start](const std::string &output) { - - /* After g++ is ran we end up inside this lambda with the output as a string */ - double compile_time = dpp::utility::time_f() - compile_start; - - /* Delete our cpp file, we don't need it any more */ - std::string del_file = std::string(getenv("PWD")) + std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".cpp"; - unlink(del_file.c_str()); - - /* On successful compilation g++ outputs nothing, so any output here is error output */ - if (output.length()) { - bot.message_create(dpp::message(event.msg.channel_id, "Compile error: ```\n" + output + "\n```")); - } else { - - /* Now for the meat of the function. To actually load - * our shared object we use dlopen() to load it into the - * memory space of our bot. If dlopen() returns a nullptr, - * the shared object could not be loaded. The user probably - * did something odd with the symbols inside their eval. - */ - std::string dl = std::string(getenv("PWD")) + std::to_string(event.msg.author.id) + "_" + std::to_string(event.msg.id) + ".so"; - auto shared_object_handle = dlopen(dl.c_str(), RTLD_NOW); - if (!shared_object_handle) { - const char *dlsym_error = dlerror(); - bot.message_create(dpp::message(event.msg.channel_id, "Shared object load error: ```\n" + - std::string(dlsym_error ? dlsym_error : "Unknown error") +"\n```")); - return; - } - - /* This type represents the "void so_exec()" function inside - * the shared object library file. - */ - using function_pointer = void(*)(dpp::cluster&, dpp::message_create_t); - - /* Attempt to find the function called so_exec() inside the - * library we just loaded. If we can't find it, then the user - * did something really strange in their eval. Also note it's - * important we call dlerror() here to reset it before trying - * to use it a second time. It's weird-ass C code and is just - * like that. - */ - dlerror(); - function_pointer exec_run = (function_pointer)dlsym(shared_object_handle, "so_exec"); - const char *dlsym_error = dlerror(); - if (dlsym_error) { - bot.message_create(dpp::message(event.msg.channel_id, "Shared object load error: ```\n" + std::string(dlsym_error) +"\n```")); - dlclose(shared_object_handle); - return; - } - - /* Now we have a function pointer to our actual exec code in - * 'exec_run', so lets call it, and pass it a reference to - * the cluster, and also a copy of the message_create_t. - */ - double run_start = dpp::utility::time_f(); - exec_run(bot, event); - double run_time = dpp::utility::time_f() - run_start; - - /* When we're done with a .so file we must always dlclose() it */ - dlclose(shared_object_handle); - - /* We are now done with the compiled code too */ - unlink(dl.c_str()); - - /* Output some statistics */ - bot.message_create(dpp::message(event.msg.channel_id, - "Execution completed. Compile time: " + std::to_string(compile_time) + - "s, execution time " + std::to_string(run_time) + "s")); - } - }); - } - }); - - bot.start(dpp::st_wait); - return 0; -} -~~~~~~~~~~~~~~~~ +\include{cpp} eval.cpp ### Compilation diff --git a/docpages/example_programs/misc/http_request.md b/docpages/example_programs/misc/http_request.md index ff01ba5e0c..523a781fda 100644 --- a/docpages/example_programs/misc/http_request.md +++ b/docpages/example_programs/misc/http_request.md @@ -2,32 +2,4 @@ If you wish to make arbitrary HTTP(S) requests to websites and APIs, e.g. to update statistics on bot lists, you can use code similar to the code below. You may pass any arbitrary POST data: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("TOKEN GOES HERE"); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_ready([&bot](const dpp::ready_t& event) { - // Arbitrary post data as a string - std::string mypostdata = "{\"value\": 42}"; - // Make a HTTP POST request. HTTP and HTTPS are supported here. - bot.request( - "http://www.somebotlist.com/api/servers", dpp::m_post, [](const dpp::http_request_completion_t & cc) { - // This callback is called when the HTTP request completes. See documentation of - // dpp::http_request_completion_t for information on the fields in the parameter. - std::cout << "I got reply: " << cc.body << " with HTTP status code: " << cc.status << "\n"; - }, - mypostdata, - "application/json", - { - {"Authorization", "Bearer tokengoeshere"} - } - ); - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} http_request.cpp diff --git a/docpages/example_programs/misc/setting_status.md b/docpages/example_programs/misc/setting_status.md index b4f6dc0253..a487ec8a54 100644 --- a/docpages/example_programs/misc/setting_status.md +++ b/docpages/example_programs/misc/setting_status.md @@ -4,28 +4,7 @@ A bot status is pretty cool, and it'd be cooler if you knew how to do it! This t First, we'll cover setting the bot status to `Playing games!`. -~~~~~~~~~~{.cpp} -#include - -int main() -{ - /* Create the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_ready([&bot](const dpp::ready_t& event) { - /* We don't need the run_once here as we're not registering commands! */ - - /* Set the bot presence as online and "Playing..."! */ - bot.set_presence(dpp::presence(dpp::presence_status::ps_online, dpp::activity_type::at_game, "games!")); - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} setting_status1.cpp If all went well, your bot should now be online and say this on members list! @@ -33,34 +12,7 @@ If all went well, your bot should now be online and say this on members list! Now, let's cover setting the bot status to say `Playing with x guilds!` every two minutes. -~~~~~~~~~~{.cpp} -#include - -int main() -{ - /* Create the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_ready([&bot](const dpp::ready_t& event) { - /* We put our status updating inside "run_once" so that multiple shards don't try do this as "set_presence" updates all the shards. */ - if (dpp::run_once()) { - /* We update the presence now as the timer will do the first execution after the x amount of seconds we specify */ - bot.set_presence(dpp::presence(dpp::presence_status::ps_online, dpp::activity_type::at_game, "with " + std::to_string(dpp::get_guild_cache()->count()) + " guilds!")); - - /* Create a timer that runs every 120 seconds, that sets the status */ - bot.start_timer([&bot](const dpp::timer& timer) { - bot.set_presence(dpp::presence(dpp::presence_status::ps_online, dpp::activity_type::at_game, "with " + std::to_string(dpp::get_guild_cache()->count()) + " guilds!")); - }, 120); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} setting_status2.cpp If you followed that well, your bot should now say this on members list! diff --git a/docpages/example_programs/music_and_audio/join_voice.md b/docpages/example_programs/music_and_audio/join_voice.md index 2e0920e64f..cfbf3d0f97 100644 --- a/docpages/example_programs/music_and_audio/join_voice.md +++ b/docpages/example_programs/music_and_audio/join_voice.md @@ -4,93 +4,4 @@ When a user issues a command you may want to join their voice channel, e.g. in a \note Please be aware this example sends no audio, but indicates clearly in the comments where and how you should do so. -~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include -#include -#include - -int main(int argc, char const *argv[]) -{ - /* Setup the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "join") { - - /* Get the guild */ - dpp::guild* g = dpp::find_guild(event.command.guild_id); - - /* Get the voice channel that the bot is currently in from this server (will return nullptr if we're not in a voice channel!) */ - auto current_vc = event.from->get_voice(event.command.guild_id); - - bool join_vc = true; - - /* Are we in a voice channel? If so, let's see if we're in the right channel. */ - if (current_vc) { - /* Find the channel id that the user is currently in */ - auto users_vc = g->voice_members.find(event.command.get_issuing_user().id); - - if (users_vc != g->voice_members.end() && current_vc->channel_id == users_vc->second.channel_id) { - join_vc = false; - - /* We are on this voice channel, at this point we can send any audio instantly to vc: - - * current_vc->send_audio_raw(...) - */ - } else { - /* We are on a different voice channel. We should leave it, then join the new one - * by falling through to the join_vc branch below. - */ - event.from->disconnect_voice(event.command.guild_id); - - join_vc = true; - } - } - - /* If we need to join a vc at all, join it here if join_vc == true */ - if(join_vc) { - /* Attempt to connect to a voice channel, returns false if we fail to connect. */ - - /* The user issuing the command is not on any voice channel, we can't do anything */ - if (!g->connect_member_voice(event.command.get_issuing_user().id)) { - event.reply("You don't seem to be in a voice channel!"); - return; - } - - /* We are now connecting to a vc. Wait for on_voice_ready - * event, and then send the audio within that event: - * - * event.voice_client->send_audio_raw(...); - * - * NOTE: We can't instantly send audio, as we have to wait for - * the connection to the voice server to be established! - */ - - /* Tell the user we joined their channel. */ - event.reply("Joined your channel!"); - } else { - event.reply("Don't need to join your channel as i'm already there with you!"); - } - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new command. */ - bot.global_command_create(dpp::slashcommand("join", "Joins your voice channel.", bot.me.id)); - } - }); - - /* Start bot */ - bot.start(dpp::st_wait); - - return 0; -} - -~~~~~~~~~~~~~~~~~~~~~~~~~ \ No newline at end of file +\include{cpp} join_voice.cpp \ No newline at end of file diff --git a/docpages/example_programs/music_and_audio/mp3.md b/docpages/example_programs/music_and_audio/mp3.md index a6181a75d2..ee7db3212d 100644 --- a/docpages/example_programs/music_and_audio/mp3.md +++ b/docpages/example_programs/music_and_audio/mp3.md @@ -2,127 +2,7 @@ To stream MP3 files via D++ you need to link an additional dependency to your bot, namely `libmpg123`. It is relatively simple when linking this library to your bot to then decode audio to PCM and send it to the dpp::discord_voice_client::send_audio_raw function as shown below: - -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -/* For an example we will hardcode a path to some awesome music here */ -#define MUSIC_FILE "/media/music/Rick Astley/Whenever You Need Somebody/Never Gonna Give You Up.mp3" - -int main(int argc, char const *argv[]) -{ - /* This will hold the decoded MP3. - * The D++ library expects PCM format, which are raw sound - * data, 2 channel stereo, 16 bit signed 48000Hz. - */ - std::vector pcmdata; - - mpg123_init(); - - int err = 0; - unsigned char* buffer; - size_t buffer_size, done; - int channels, encoding; - long rate; - - /* Note it is important to force the frequency to 48000 for Discord compatibility */ - mpg123_handle *mh = mpg123_new(NULL, &err); - mpg123_param(mh, MPG123_FORCE_RATE, 48000, 48000.0); - - /* Decode entire file into a vector. You could do this on the fly, but if you do that - * you may get timing issues if your CPU is busy at the time and you are streaming to - * a lot of channels/guilds. - */ - buffer_size = mpg123_outblock(mh); - buffer = new unsigned char[buffer_size]; - - /* Note: In a real world bot, this should have some error logging */ - mpg123_open(mh, MUSIC_FILE); - mpg123_getformat(mh, &rate, &channels, &encoding); - - unsigned int counter = 0; - for (int totalBytes = 0; mpg123_read(mh, buffer, buffer_size, &done) == MPG123_OK; ) { - for (auto i = 0; i < buffer_size; i++) { - pcmdata.push_back(buffer[i]); - } - counter += buffer_size; - totalBytes += done; - } - delete buffer; - mpg123_close(mh); - mpg123_delete(mh); - - /* Setup the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot, &pcmdata](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "join") { - - /* Get the guild */ - dpp::guild* g = dpp::find_guild(event.command.guild_id); - - /* Attempt to connect to a voice channel, returns false if we fail to connect. */ - if (!g->connect_member_voice(event.command.get_issuing_user().id)) { - event.reply("You don't seem to be in a voice channel!"); - return; - } - - /* Tell the user we joined their channel. */ - event.reply("Joined your channel!"); - } else if (event.command.get_command_name() == "mp3") { - - /* Get the voice channel the bot is in, in this current guild. */ - dpp::voiceconn* v = event.from->get_voice(event.channel.guild_id); - - /* If the voice channel was invalid, or there is an issue with it, then tell the user. */ - if (!v || !v->voiceclient || !v->voiceclient->is_ready()) { - event.reply("There was an issue with getting the voice channel. Make sure I'm in a voice channel!"); - return; - } - - /* Stream the already decoded MP3 file. This passes the PCM data to the library to be encoded to OPUS */ - v->voiceclient->send_audio_raw((uint16_t*)pcmdata.data(), pcmdata.size()); - - event.reply("Played the mp3 file."); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new command. */ - dpp::slashcommand joincommand("join", "Joins your voice channel.", bot.me.id); - - dpp::slashcommand mp3command("mp3", "Plays an mp3 file.", bot.me.id); - - bot.global_bulk_command_create({joincommand, mp3command}); - } - }); - - /* Start bot */ - bot.start(dpp::st_wait); - - /* Clean up */ - mpg123_exit(); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} mp3.cpp To compile this program you must remember to specify `libmpg123` alongside `libdpp` in the build command, for example: diff --git a/docpages/example_programs/music_and_audio/oggopus.md b/docpages/example_programs/music_and_audio/oggopus.md index 991597b6c5..c41113dd54 100644 --- a/docpages/example_programs/music_and_audio/oggopus.md +++ b/docpages/example_programs/music_and_audio/oggopus.md @@ -194,125 +194,7 @@ You can use `liboggz` to stream an Ogg Opus file to discord voice channel. `liboggz` provides higher level abstraction and useful APIs. Some features `liboggz` provides include: seeking and timestamp interpretation. Read more on the [documentation](https://www.xiph.org/oggz/doc/). -~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include -#include -#include - -#include -#include -#include -#include - -int main(int argc, char const *argv[]) -{ - /* Load an ogg opus file into memory. - * The bot expects opus packets to be 2 channel stereo, 48000Hz. - * - * You may use ffmpeg to encode songs to ogg opus: - * ffmpeg -i /path/to/song -c:a libopus -ar 48000 -ac 2 -vn -b:a 96K /path/to/opus.ogg - */ - - /* Setup the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "join") { - - /* Get the guild */ - dpp::guild* g = dpp::find_guild(event.command.guild_id); - - /* Attempt to connect to a voice channel, returns false if we fail to connect. */ - if (!g->connect_member_voice(event.command.get_issuing_user().id)) { - event.reply("You don't seem to be in a voice channel!"); - return; - } - - /* Tell the user we joined their channel. */ - event.reply("Joined your channel!"); - } else if (event.command.get_command_name() == "play") { - - /* Get the voice channel the bot is in, in this current guild. */ - dpp::voiceconn* v = event.from->get_voice(event.channel.guild_id); - - /* If the voice channel was invalid, or there is an issue with it, then tell the user. */ - if (!v || !v->voiceclient || !v->voiceclient->is_ready()) { - event.reply("There was an issue with getting the voice channel. Make sure I'm in a voice channel!"); - return; - } - - // load the audio file with oggz - OGGZ *track_og = oggz_open("/path/to/opus.ogg", OGGZ_READ); - - /* If there was an issue reading the file, tell the user and stop */ - if (!track_og) { - fprintf(stderr, "Error opening file\n"); - event.reply("There was an issue opening the file!"); - return; - } - - // set read callback, this callback will be called on packets with the serialno, - // -1 means every packet will be handled with this callback - oggz_set_read_callback( - track_og, -1, - [](OGGZ *oggz, oggz_packet *packet, long serialno, - void *user_data) { - dpp::voiceconn *voiceconn = (dpp::voiceconn *)user_data; - - // send the audio - voiceconn->voiceclient->send_audio_opus(packet->op.packet, - packet->op.bytes); - - // make sure to always return 0 here, read more on oggz documentation - return 0; - }, - // this will be the value of void *user_data - (void *)v - ); - - // read loop - while (v && v->voiceclient && !v->voiceclient->terminating) { - // you can tweak this to whatever. Here I use BUFSIZ, defined in - // stdio.h as 8192 - static const constexpr long CHUNK_READ = BUFSIZ * 2; - - const long read_bytes = oggz_read(track_og, CHUNK_READ); - - // break on eof - if (!read_bytes) - break; - } - - // don't forget to free the memory - oggz_close(track_og); - - event.reply("Finished playing the audio file!"); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new command. */ - dpp::slashcommand joincommand("join", "Joins your voice channel.", bot.me.id); - - dpp::slashcommand playcommand("play", "Plays an ogg file.", bot.me.id); - - bot.global_bulk_command_create({joincommand, playcommand}); - } - }); - - /* Start bot */ - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} oggopus.cpp You can compile this example using the following command: diff --git a/docpages/example_programs/music_and_audio/record_user.md b/docpages/example_programs/music_and_audio/record_user.md index f80f53f38e..022680d82c 100644 --- a/docpages/example_programs/music_and_audio/record_user.md +++ b/docpages/example_programs/music_and_audio/record_user.md @@ -4,78 +4,4 @@ DPP supports receiving audio. This examples show how to use it to record some us \note Voice receiving by bots is not officially supported by the Discord API. We cannot guarantee that this feature will work in the future. -~~~~~~~~~~{.cpp} -#include -#include -#include - -int main(int argc, char const *argv[]) -{ - /* Example to record a user in a VC - * - * Recording is output as './me.pcm' and you can play it via the soundboard example - * or use ffmpeg 'ffplay -f s16le -ar 48000 -ac 2 -i ./me.pcm' - */ - - /* Replace with the user's id you wish to record */ - dpp::snowflake user_id = 407877550216314882; - - /* Setup the bot */ - dpp::cluster bot("token"); - - FILE *fd; - fd = fopen("./me.pcm", "wb"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot, &fd](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "record") { - - /* Get the guild */ - dpp::guild* g = dpp::find_guild(event.command.guild_id); - - /* Attempt to connect to a voice channel, returns false if we fail to connect. */ - if (!g->connect_member_voice(event.command.get_issuing_user().id)) { - event.reply("You don't seem to be in a voice channel!"); - return; - } - - /* Tell the user we joined their channel. */ - event.reply("Joined your channel, now recording!"); - } else if (event.command.get_command_name() == "stop") { - - event.from->disconnect_voice(event.command.guild_id); - fclose(fd); - - event.reply("Stopped recording."); - } - }); - - bot.on_voice_receive([&bot, &fd, &user_id](const dpp::voice_receive_t &event) { - if (event.user_id == user_id) { - fwrite((char *)event.audio, 1, event.audio_size, fd); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new command. */ - dpp::slashcommand recordcommand("record", "Joins your voice channel and records you.", bot.me.id); - - dpp::slashcommand stopcommand("stop", "Stops recording you.", bot.me.id); - - bot.global_bulk_command_create({recordcommand, stopcommand}); - } - }); - - /* Start bot */ - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ - +\include{cpp} record_user.cpp diff --git a/docpages/example_programs/music_and_audio/soundboard.md b/docpages/example_programs/music_and_audio/soundboard.md index 88d6940b6e..52818f6cab 100644 --- a/docpages/example_programs/music_and_audio/soundboard.md +++ b/docpages/example_programs/music_and_audio/soundboard.md @@ -2,90 +2,4 @@ This example script shows how to send a sound file to a voice channel. A few shortcuts are taken here, for more advanced techniques for connecting to a voice channel see the tutorial \ref joinvc -~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include -#include -#include - -int main(int argc, char const *argv[]) -{ - /* Load a sound file called Robot.pcm into memory. - * The bot expects PCM format, which are raw sound data, - * 2 channel stereo, 16 bit signed 48000Hz. - * - * You can use audacity to export these from WAV or MP3 etc. - * - * If you wanted to send a more complicated format, you could - * use a separate library to decode that audio to PCM. For - * example purposes, a raw PCM will suffice. This PCM file can - * be found within the bot's github repo. - */ - uint8_t* robot = nullptr; - size_t robot_size = 0; - std::ifstream input ("../testdata/Robot.pcm", std::ios::in|std::ios::binary|std::ios::ate); - if (input.is_open()) { - robot_size = input.tellg(); - robot = new uint8_t[robot_size]; - input.seekg (0, std::ios::beg); - input.read ((char*)robot, robot_size); - input.close(); - } - - /* Setup the bot */ - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot, robot, robot_size](const dpp::slashcommand_t& event) { - - /* Check which command they ran */ - if (event.command.get_command_name() == "join") { - - /* Get the guild */ - dpp::guild* g = dpp::find_guild(event.command.guild_id); - - /* Attempt to connect to a voice channel, returns false if we fail to connect. */ - if (!g->connect_member_voice(event.command.get_issuing_user().id)) { - event.reply("You don't seem to be in a voice channel!"); - return; - } - - /* Tell the user we joined their channel. */ - event.reply("Joined your channel!"); - } else if (event.command.get_command_name() == "robot") { - - /* Get the voice channel the bot is in, in this current guild. */ - dpp::voiceconn* v = event.from->get_voice(event.channel.guild_id); - - /* If the voice channel was invalid, or there is an issue with it, then tell the user. */ - if (!v || !v->voiceclient || !v->voiceclient->is_ready()) { - event.reply("There was an issue with getting the voice channel. Make sure I'm in a voice channel!"); - return; - } - - /* Tell the bot to play the sound file 'Robot.pcm' in the current voice channel. */ - v->voiceclient->send_audio_raw((uint16_t*)robot, robot_size); - - event.reply("Played robot."); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - - /* Create a new command. */ - dpp::slashcommand joincommand("join", "Joins your voice channel.", bot.me.id); - - dpp::slashcommand robotcommand("robot", "Plays a robot noise in your voice channel.", bot.me.id); - - bot.global_bulk_command_create({joincommand, robotcommand}); - } - }); - - /* Start bot */ - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~~~~~~~~~~~~~~ \ No newline at end of file +\include{cpp} soundboard.cpp \ No newline at end of file diff --git a/docpages/example_programs/the_basics/attachments.md b/docpages/example_programs/the_basics/attachments.md index f25873430f..1a3668ae18 100644 --- a/docpages/example_programs/the_basics/attachments.md +++ b/docpages/example_programs/the_basics/attachments.md @@ -8,42 +8,7 @@ D++ has this helper function to read a file: dpp::utility::read_file. An example program: -~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - /* Check which command they ran */ - if (event.command.get_command_name() == "file") { - - dpp::message msg(event.command.channel_id, "Hey there, I've got a new file!"); - - /* attach the file to the message */ - msg.add_file("foobar.txt", dpp::utility::read_file("path_to_your_file.txt")); - - /* Reply to the user with the message, with our file attached. */ - event.reply(msg); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Create and register a command when the bot is ready */ - bot.global_command_create(dpp::slashcommand("file", "Send a message with a file attached!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} attachments1.cpp Attachments via an url aren't possible. But there's a workaround for. You can download the file and then attach it to the message. @@ -51,94 +16,10 @@ To make requests, D++ also has a helper function: dpp::cluster::request. The following example program shows how to request a file and attach it to a message. -~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - /* Check which command they ran */ - if (event.command.get_command_name() == "file") { - - /* Request the image from the URL specified and capture the event in a lambda. */ - bot.request("https://dpp.dev/DPP-Logo.png", dpp::m_get, [event](const dpp::http_request_completion_t & httpRequestCompletion) { - - /* Create a message */ - dpp::message msg(event.command.channel_id, "This is my new attachment:"); - - /* Attach the image to the message, only on success (Code 200). */ - if (httpRequestCompletion.status == 200) { - msg.add_file("logo.png", httpRequestCompletion.body); - } - - /* Send the message, with our attachment. */ - event.reply(msg); - }); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Create and register a command when the bot is ready */ - bot.global_command_create(dpp::slashcommand("file", "Send a message with an image attached from the internet!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} attachments2.cpp Here's another example of how to add a local image to an embed. Upload the image in the same message as the embed and then reference it in the embed. -~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - /* Check which command they ran */ - if (event.command.get_command_name() == "file") { - - /* Create a message. */ - dpp::message msg(event.command.channel_id, ""); - - /* Attach the image to the message we just created. */ - msg.add_file("image.jpg", dpp::utility::read_file("path_to_your_image.jpg")); - - /* Create an embed. */ - dpp::embed embed; - embed.set_image("attachment://image.jpg"); /* Set the image of the embed to the attached image. */ - - /* Add the embed to the message. */ - msg.add_embed(embed); - - event.reply(msg); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Create and register a command when the bot is ready */ - bot.global_command_create(dpp::slashcommand("file", "Send a local image along with an embed with the image!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - - return 0; -} -~~~~~~~~~~ +\include{cpp} attachments3.cpp diff --git a/docpages/example_programs/the_basics/editing-channels-and-messages.md b/docpages/example_programs/the_basics/editing-channels-and-messages.md index 0cd2099953..a94ab7ccd8 100644 --- a/docpages/example_programs/the_basics/editing-channels-and-messages.md +++ b/docpages/example_programs/the_basics/editing-channels-and-messages.md @@ -4,81 +4,7 @@ Sometimes we need to update an object, such as message or channel. At first, it \note This example uses callback functions. To see more information about them, visit \ref callback-functions. -~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("Token", dpp::i_default_intents | dpp::i_message_content); - /* the second argument is a bitmask of intents - i_message_content is needed to get messages */ - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - if (event.command.get_command_name() == "msg-send") { - event.reply("That's a message"); - } else if (event.command.get_command_name() == "msg-edit") { - const auto content = std::get(event.get_parameter("content")); - - /* get message to edit it after */ - const dpp::snowflake msg_id = std::get(event.get_parameter("msg-id")); - /* here string will automatically be converted to snowflake */ - - bot.message_get(msg_id, event.command.channel_id, [&bot, content, event](const dpp::confirmation_callback_t& callback) { - if (callback.is_error()) { - event.reply("error"); - return; - } - auto message = callback.get(); - - /* change the message content and edit the message itself */ - message.set_content(content); - bot.message_edit(message); - event.reply("Message content is now `" + content + "`."); - }); - } else if (event.command.get_command_name() == "channel-edit") { - const auto name = std::get(event.get_parameter("name")); - - /* get the channel to edit it after */ - const auto channel_id = std::get(event.get_parameter("channel")); - bot.channel_get(channel_id, [&bot, name, event](const dpp::confirmation_callback_t& callback) { - if (callback.is_error()) { - event.reply("error"); - return; - } - auto channel = callback.get(); - - /* change the channel name and edit the channel itself */ - channel.set_name(name); - bot.channel_edit(channel); - event.reply("Channel name is now `" + name + "`."); - }); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - - if (dpp::run_once ()) { - dpp::slashcommand msg_edit("msg-edit", "Edit a message sent by the bot", bot.me.id); - - msg_edit.add_option(dpp::command_option(dpp::co_string, "msg-id", "ID of the message to edit", true)); /* true for required option */ - msg_edit.add_option(dpp::command_option(dpp::co_string, "content", "New content for the message", true)); /* same here */ - - dpp::slashcommand channel_edit("channel-edit", "Edit the name of channel specified", bot.me.id); - - channel_edit.add_option(dpp::command_option(dpp::co_channel, "channel", "Channel to edit", true)); - channel_edit.add_option(dpp::command_option(dpp::co_string, "name", "New name for the channel", true)); - - dpp::slashcommand msg_send("msg-send", "Send my message", bot.me.id); - - bot.global_bulk_command_create({ msg_edit, channel_edit, msg_send }); - } - }); - - bot.start(dpp::st_wait); - return 0; -} -~~~~~~~~~~ +\include{cpp} editing_messages.cpp Before editing: diff --git a/docpages/example_programs/the_basics/embeds.md b/docpages/example_programs/the_basics/embeds.md index ca1934143c..ac27a6ec64 100644 --- a/docpages/example_programs/the_basics/embeds.md +++ b/docpages/example_programs/the_basics/embeds.md @@ -2,64 +2,7 @@ If you've been in a server and used a bot or even seen a message from a bot, you might have seen a special message type, often sent by these bots. These are called embeds! In this section, we will show how to create an embed and reply to a user, using our newly created embed! -~~~~~~~~~~{.cpp} -#include - -int main() { - /* Setup the bot */ - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) { - /* Check which command they ran */ - if (event.command.get_command_name() == "embed") { - - /* Create an embed */ - dpp::embed embed = dpp::embed(). - set_color(dpp::colors::sti_blue). - set_title("Some name"). - set_url("https://dpp.dev/"). - set_author("Some name", "https://dpp.dev/", "https://dpp.dev/DPP-Logo.png"). - set_description("Some description here"). - set_thumbnail("https://dpp.dev/DPP-Logo.png"). - add_field( - "Regular field title", - "Some value here" - ). - add_field( - "Inline field title", - "Some value here", - true - ). - add_field( - "Inline field title", - "Some value here", - true - ). - set_image("https://dpp.dev/DPP-Logo.png"). - set_footer(dpp::embed_footer().set_text("Some footer text here").set_icon("https://dpp.dev/DPP-Logo.png")). - set_timestamp(time(0)); - - /* Create a message with the content as our new embed. */ - dpp::message msg(event.command.channel_id, embed); - - /* Reply to the user with the message, containing our embed. */ - event.reply(msg); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - - /* Create and register a command when the bot is ready */ - bot.global_command_create(dpp::slashcommand("embed", "Send a test embed!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); - return 0; -} -~~~~~~~~~~ +\include{cpp} embeds.cpp The code will send the following message. diff --git a/docpages/example_programs/the_basics/firstbot.md b/docpages/example_programs/the_basics/firstbot.md index d80ca5ebad..76037907c5 100644 --- a/docpages/example_programs/the_basics/firstbot.md +++ b/docpages/example_programs/the_basics/firstbot.md @@ -12,41 +12,13 @@ The two programs can be seen side by side below: - -~~~~~~~~~~~~~~~{.cpp} -#include - -const std::string BOT_TOKEN = "add your token here"; - -int main() { - dpp::cluster bot(BOT_TOKEN); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([](const dpp::slashcommand_t& event) { - if (event.command.get_command_name() == "ping") { - event.reply("Pong!"); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - bot.global_command_create( - dpp::slashcommand("ping", "Ping pong!", bot.me.id) - ); - } - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~~~~~~ - +\include{cpp} firstbot.cpp -~~~~~~~~~~~~~~~{.cpp} +~~~~~~~~~~~~~~~{.js} let Discord = require('discord.js'); @@ -85,12 +57,7 @@ Let's break this program down step by step: Make sure to include the header file for the D++ library with the instruction \#include ``! -~~~~~~~~~~~~~~{.cpp} -#include - -int main() { -} -~~~~~~~~~~~~~~ +\include{cpp} firstbot1.cpp ### 2. Create an instance of dpp::cluster @@ -98,15 +65,7 @@ To make use of the library you must create a dpp::cluster object. This object is You can instantiate this class as shown below. Remember to put your bot token in the constant! -~~~~~~~~~~~~~~~{.cpp} -#include - -const std::string BOT_TOKEN = "add your token here"; - -int main() { - dpp::cluster bot(BOT_TOKEN); -} -~~~~~~~~~~~~~~~ +\include{cpp} firstbot2.cpp ### 3. Attach to an event @@ -114,71 +73,19 @@ To have a bot that does something, you should attach to some events. Let's start command called 'ping'. Note that we must wrap our registration of the command in a template called dpp::run_once which prevents it from being re-run every time your bot does a full reconnection (e.g. if the connection fails). -~~~~~~~~~~~~~~~~{.cpp} -#include - -const std::string BOT_TOKEN = "add your token here"; - -int main() { - dpp::cluster bot(BOT_TOKEN); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); - } - }); -} -~~~~~~~~~~~~~~~~ +\include{cpp} firstbot3.cpp ### 4. Attach to another event to receive slash commands If you want to handle a slash command, you should also attach your program to the `on_slashcommand` event (dpp::cluster::on_slashcommand) which is basically the same as the Discord.js `interactionCreate` event. Lets add this to the program before the `on_ready` event: -~~~~~~~~~~~~~~{.cpp} -#include - -const std::string BOT_TOKEN = "add your token here"; - -int main() { - dpp::cluster bot(BOT_TOKEN); - - bot.on_slashcommand([](const dpp::slashcommand_t& event) { - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); - } - }); -} -~~~~~~~~~~~~~~ +\include{cpp} firstbot4.cpp ### 5 . Add some content to the events Attaching to an event is a good start, but to make a bot you should actually put some program code into the interaction event. We will add some code to the `on_slashcommand` to look for our slash command '/ping' and reply with `Pong!`: -~~~~~~~~~~~~~~~~~~~~~~~{.cpp} -#include - -const std::string BOT_TOKEN = "add your token here"; - -int main() { - dpp::cluster bot(BOT_TOKEN); - - bot.on_slashcommand([](const dpp::slashcommand_t& event) { - if (event.command.get_command_name() == "ping") { - event.reply("Pong!"); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); - } - }); - -} -~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} firstbot5.cpp Let's break down the code in the `on_slashcommand` event so that we can discuss what it is doing: @@ -202,31 +109,7 @@ We also add a line to tell the library to output all its log information to the The parameter which we set to false indicates if the function should return once all shards are created. Passing `false` here tells the program you do not need to do anything once `bot.start` is called. -~~~~~~~~~~~~~~{.cpp} -#include - -const std::string BOT_TOKEN = "add your token here"; - -int main() { - dpp::cluster bot(BOT_TOKEN); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([](const dpp::slashcommand_t& event) { - if (event.command.get_command_name() == "ping") { - event.reply("Pong!"); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - bot.global_command_create(dpp::slashcommand("ping", "Ping pong!", bot.me.id)); - } - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~~~~~ +\include{cpp} firstbot6.cpp ### 7. Compile and run your bot diff --git a/docpages/example_programs/the_basics/using_callback_functions.md b/docpages/example_programs/the_basics/using_callback_functions.md index 9da5142eb4..587be849f9 100644 --- a/docpages/example_programs/the_basics/using_callback_functions.md +++ b/docpages/example_programs/the_basics/using_callback_functions.md @@ -2,96 +2,7 @@ When you create or get an object from Discord, you send the request to its API and in return you get either an error or the object you requested/created. You can pass a function to API calls as the callback function. This means that when the request completes, and you get a response from the API, your callback function executes. You must be careful with lambda captures! Good practice would be not capturing variables by reference unless you have to, since when the request completes and the function executes, the variables can already be destructed. Advanced reference can be found [here](https://dpp.dev/lambdas-and-locals.html). Now, let's see callback functions in action: -~~~~~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("Token Was Here", dpp::i_default_intents | dpp::i_message_content); - /* the second argument is a bitmask of intents - i_message_content is needed to get messages */ - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - bot.on_slashcommand([&bot](const dpp::slashcommand_t& event) -> void { - if (event.command.get_command_name() == "msgs-get") { - int64_t limit = std::get(event.get_parameter("quantity")); - - /* get messages using ID of the channel the command was issued in */ - bot.messages_get(event.command.channel_id, 0, 0, 0, limit, [event](const dpp::confirmation_callback_t& callback) -> void { - if (callback.is_error()) { /* catching an error to log it */ - std::cout << callback.get_error().message << std::endl; - return; - } - - auto messages = callback.get(); - /* std::get(callback.value) would give the same result */ - - std::string contents; - for (const auto& x : messages) { /* here we iterate through the dpp::message_map we got from callback... */ - contents += x.second.content + '\n'; /* ...where x.first is ID of the current message and x.second is the message itself. */ - } - - event.reply(contents); /* we will see all those messages we got, united as one! */ - }); - } else if (event.command.get_command_name() == "channel-create") { - /* create a text channel */ - dpp::channel channel = dpp::channel() - .set_name("test") - .set_guild_id(event.command.guild_id); - - bot.channel_create(channel, [&bot, event](const dpp::confirmation_callback_t& callback) -> void { - if (callback.is_error()) { /* catching an error to log it */ - bot.log(dpp::loglevel::ll_error, callback.get_error().message); - return; - } - - auto channel = callback.get(); - /* std::get(callback.value) would give the same result */ - - /* reply with the created channel information */ - dpp::message message = dpp::message("The channel's name is `" + channel.name + "`, ID is `" + std::to_string(channel.id) + " and type is `" + std::to_string(channel.get_type()) + "`."); - /* note that channel types are represented as numbers */ - event.reply(message); - }); - } else if (event.command.get_command_name() == "msg-error") { - bot.message_get(0, 0, [event](const dpp::confirmation_callback_t& callback) -> void { - /* the error will occur since there is no message with ID '0' that is in a channel with ID '0' (I'm not explaining why) */ - if (callback.is_error()) { - event.reply(callback.get_error().message); - return; - } - - /* we won't be able to get here because of the return; statement */ - auto message = callback.get(); - event.reply(message); - }); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once ()) { - dpp::slashcommand msgs_get("msgs-get", "Get messages", bot.me.id); - - constexpr int64_t min_val{1}; - constexpr int64_t max_val{100}; - - msgs_get.add_option( - dpp::command_option(dpp::co_integer, "quantity", "Quantity of messages to get. Max - 100.") - .set_min_value(min_val) - .set_max_value(max_val) - ); - - dpp::slashcommand channel_create("channel-create", "Create a channel", bot.me.id); - dpp::slashcommand msg_error("msg-error", "Get an error instead of message :)", bot.me.id); - - bot.global_bulk_command_create({ msgs_get, channel_create, msg_error }); - } - }); - - bot.start(dpp::st_wait); - return 0; -} -~~~~~~~~~~~~~~ +\include{cpp} callbacks.cpp This is the result: diff --git a/docpages/example_programs/the_basics/webhooks.md b/docpages/example_programs/the_basics/webhooks.md index 02e1c8c438..e121c2169e 100644 --- a/docpages/example_programs/the_basics/webhooks.md +++ b/docpages/example_programs/the_basics/webhooks.md @@ -4,23 +4,6 @@ Webhooks are a simple way to post messages from other apps and websites into Dis The following code shows how to send messages in a channel using a webhook. -~~~~~~~~~~{.cpp} -#include - -int main() -{ - dpp::cluster bot(""); // normally, you put your bot token in here. But to just run a webhook its not required - - bot.on_log(dpp::utility::cout_logger()); - - /* construct a webhook object using the URL you got from Discord */ - dpp::webhook wh("https://discord.com/api/webhooks/833047646548133537/ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE"); - - /* send a message with this webhook */ - bot.execute_webhook_sync(wh, dpp::message("Have a great time here :smile:")); - - return 0; -} -~~~~~~~~~~ +\include{cpp} webhooks.cpp The above is just a very simple example. You can also send embed messages. All you have to do is to add an embed to the message you want to send. If you want to, you can also send it into a thread. diff --git a/docpages/example_programs/using_coroutines/awaiting_events.md b/docpages/example_programs/using_coroutines/awaiting_events.md index 869c72e975..45b7a4dcfc 100644 --- a/docpages/example_programs/using_coroutines/awaiting_events.md +++ b/docpages/example_programs/using_coroutines/awaiting_events.md @@ -6,47 +6,7 @@ D++ makes it possible to await events: simple use `co_await` on any of the event \note When the event router resumes your coroutine, it will give you __a reference to the event object__. This will likely mean it will be destroyed after your next co_await, make sure to save it in a local variable if you need it for longer. -~~~~~~~~~~cpp -#include - -int main() { - dpp::cluster bot{"token"}; - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { - if (event.command.get_command_name() == "test") { - // Make a message and add a button with its custom ID set to the command interaction's ID so we can identify it - dpp::message m{"Test"}; - std::string id{event.command.id.str()}; - m.add_component( - dpp::component{}.add_component(dpp::component{}.set_type(dpp::cot_button).set_label("Click me!").set_id(id)) - ); - co_await event.co_reply(m); - - dpp::button_click_t click_event = co_await event.from->creator->on_button_click.when( - // Note!! Due to a bug in g++11 and g++12, id must be captured as a reference here or the compiler will destroy it twice. This is fixed in g++13 - [&id] (dpp::button_click_t const &b) { - return b.custom_id == id; - } - ); - // Acknowledge the click and edit the original response, removing the button - click_event.reply(); - event.edit_original_response(dpp::message{"You clicked the button!"}); - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - dpp::slashcommand command{"test", "Test awaiting for an event", bot.me.id}; - - bot.global_command_create(command); - } - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~ +\include{cpp} coro_awaiting_events.cpp Note that there is a problem with that! If the user never clicks your button, or if the message gets deleted, your coroutine will be stuck waiting... And waiting... Forever until your bot shuts down, occupying a space in memory. This is where the \ref expiring-buttons "next example" comes into play as a solution, with a button that expires with time. diff --git a/docpages/example_programs/using_coroutines/coro_introduction.md b/docpages/example_programs/using_coroutines/coro_introduction.md index 217f7be4bf..55c5e123c2 100644 --- a/docpages/example_programs/using_coroutines/coro_introduction.md +++ b/docpages/example_programs/using_coroutines/coro_introduction.md @@ -5,44 +5,7 @@ Introduced in C++20, coroutines are the solution to the impracticality of callba Let's revisit \ref attach-file "attaching a downloaded file", but this time with a coroutine: -~~~~~~~~~~~~~~~cpp -#include - -int main() { - dpp::cluster bot{"token"}; - - bot.on_log(dpp::utility::cout_logger()); - - /* The event is fired when someone issues your commands */ - /* Make note of passing the event by value, this is important (explained below) */ - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { - if (event.command.get_command_name() == "file") { - /* Request the image from the URL specified and co_await the response */ - dpp::http_request_completion_t result = co_await event.from->creator->co_request("https://dpp.dev/DPP-Logo.png", dpp::m_get); - - /* Create a message and attach the image on success */ - dpp::message msg(event.command.channel_id, "This is my new attachment:"); - if (result.status == 200) { - msg.add_file("logo.png", result.body); - } - - /* Send the message, with our attachment. */ - event.reply(msg); - } - }); - - bot.on_ready([&bot](const dpp::ready_t& event) { - if (dpp::run_once()) { - /* Create and register a command when the bot is ready */ - bot.global_command_create(dpp::slashcommand{"file", "Send a message with an image attached from the internet!", bot.me.id}); - } - }); - - bot.start(dpp::st_wait); - return 0; -} -~~~~~~~~~~~~~~~ - +\include{cpp} coro_intro.cpp Coroutines can make commands simpler by eliminating callbacks, which can be very handy in the case of complex commands that rely on a lot of different data or steps. diff --git a/docpages/example_programs/using_coroutines/coro_simple_commands.md b/docpages/example_programs/using_coroutines/coro_simple_commands.md index cb47dcca17..0ec97b8437 100644 --- a/docpages/example_programs/using_coroutines/coro_simple_commands.md +++ b/docpages/example_programs/using_coroutines/coro_simple_commands.md @@ -8,71 +8,7 @@ With coroutines, it becomes a lot easier to do several asynchronous requests for one task. As an example an "addemoji" command taking a file and a name as a parameter. This means downloading the emoji, submitting it to Discord, and finally replying, with some error handling along the way. Normally we would have to use callbacks and some sort of object keeping track of our state, but with coroutines, the function can simply pause and be resumed when we receive the response to our request : -~~~~~~~~~~cpp -#include - -int main() { - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { - if (event.command.get_command_name() == "addemoji") { - dpp::cluster *cluster = event.from->creator; - // Retrieve parameter values - dpp::snowflake file_id = std::get(event.get_parameter("file")); - std::string emoji_name = std::get(event.get_parameter("name")); - - // Get the attachment from the resolved list - const dpp::attachment &attachment = event.command.get_resolved_attachment(file_id); - - // For simplicity for this example we only support PNG - if (attachment.content_type != "image/png") { - // While we could use event.co_reply, we can just use event.reply, as we will exit the command anyway and don't need to wait on the result - event.reply("Error: type " + attachment.content_type + " not supported"); - co_return; - } - // Send a " is thinking..." message, to wait on later so we can edit - dpp::async thinking = event.co_thinking(false); - - // Download and co_await the result - dpp::http_request_completion_t response = co_await cluster->co_request(attachment.url, dpp::m_get); - - if (response.status != 200) { // Page didn't send the image - co_await thinking; // Wait for the thinking response to arrive so we can edit - event.edit_response("Error: could not download the attachment"); - } else { - // Load the image data in a dpp::emoji - dpp::emoji emoji(emoji_name); - emoji.load_image(response.body, dpp::image_type::i_png); - - // Create the emoji and co_await the response - dpp::confirmation_callback_t confirmation = co_await cluster->co_guild_emoji_create(event.command.guild_id, emoji); - - co_await thinking; // Wait for the thinking response to arrive so we can edit - if (confirmation.is_error()) { - event.edit_response("Error: could not add emoji: " + confirmation.get_error().message); - } else { // Success - event.edit_response("Successfully added " + confirmation.get().get_mention()); // Show the new emoji - } - } - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - dpp::slashcommand command("addemoji", "Add an emoji", bot.me.id); - // Add file and name as required parameters - command.add_option(dpp::command_option(dpp::co_attachment, "file", "Select an image", true)); - command.add_option(dpp::command_option(dpp::co_string, "name", "Name of the emoji to add", true)); - - bot.global_command_create(command); - } - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~ +\include{cpp} coro_simple_commands1.cpp ### I heard you liked tasks @@ -82,87 +18,4 @@ Earlier we mentioned two other types of coroutines provided by dpp: dpp::corouti Here is an example of a command making use of dpp::task to retrieve the avatar of a specified user, or if missing, the sender: -~~~~~~~~~~cpp -#include - -int main() { - dpp::cluster bot("token"); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { - if (event.command.get_command_name() == "avatar") { - // Make a nested coroutine to fetch the guild member requested, that returns it as an optional - constexpr auto resolve_member = [](const dpp::slashcommand_t &event) -> dpp::task> { - const dpp::command_value &user_param = event.get_parameter("user"); - dpp::snowflake user_id; - if (std::holds_alternative(user_param)) { - user_id = event.command.usr.id; // Parameter is empty so user is sender - } - else if (std::holds_alternative(user_param)) { - user_id = std::get(user_param); // Parameter has a user - } - - // If we have the guild member in the command's resolved data, return it - const auto &member_map = event.command.resolved.members; - if (auto member = member_map.find(user_id); member != member_map.end()) - co_return member->second; - // Try looking in guild cache - dpp::guild *guild = dpp::find_guild(event.command.guild_id); - if (guild) { - // Look in guild's member cache - if (auto member = guild->members.find(user_id); member != guild->members.end()) { - co_return member->second; - } - } - // Finally if everything else failed, request API - dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_guild_get_member(event.command.guild_id, user_id); - if (confirmation.is_error()) { - co_return std::nullopt; // Member not found, return empty - } else { - co_return confirmation.get(); - } - }; - - // Send a " is thinking..." message, to wait on later so we can edit - dpp::async thinking = event.co_thinking(false); - // Call our coroutine defined above to retrieve the member requested - std::optional member = co_await resolve_member(event); - if (!member.has_value()) { - // Wait for the thinking response to arrive to make sure we can edit - co_await thinking; - event.edit_original_response(dpp::message{"User not found in this server!"}); - co_return; - } - - std::string avatar_url = member->get_avatar_url(512); - if (avatar_url.empty()) { // Member does not have a custom avatar for this server, get their user avatar - dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_user_get_cached(member->user_id); - if (confirmation.is_error()) { - // Wait for the thinking response to arrive to make sure we can edit - co_await thinking; - event.edit_original_response(dpp::message{"User not found!"}); - co_return; - } - avatar_url = confirmation.get().get_avatar_url(512); - } - - // Wait for the thinking response to arrive to make sure we can edit - co_await thinking; - event.edit_original_response(dpp::message{avatar_url}); - } - }); - - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - dpp::slashcommand command("avatar", "Get your or another user's avatar image", bot.me.id); - command.add_option(dpp::command_option(dpp::co_user, "user", "User to fetch the avatar from")); - - bot.global_command_create(command); - } - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~ +\include{cpp} coro_simple_commands2.cpp diff --git a/docpages/example_programs/using_coroutines/expiring_buttons.md b/docpages/example_programs/using_coroutines/expiring_buttons.md index 1444d6b3c8..f07234714e 100644 --- a/docpages/example_programs/using_coroutines/expiring_buttons.md +++ b/docpages/example_programs/using_coroutines/expiring_buttons.md @@ -4,51 +4,7 @@ In the last example we've explored how to \ref awaiting-events "await events" using coroutines, we ran into the problem of the coroutine waiting forever if the button was never clicked. Wouldn't it be nice if we could add an "or" to our algorithm, for example wait for the button to be clicked *or* for a timer to expire? I'm glad you asked! D++ offers \ref dpp::when_any "when_any" which allows exactly that. It is a templated class that can take any number of awaitable objects and can be `co_await`-ed itself, will resume when the __first__ awaitable completes and return a \ref dpp::when_any::result "result" object that allows to retrieve which awaitable completed as well as its result, in a similar way as std::variant. -~~~~~~~~~~cpp -#include - -int main() { - dpp::cluster bot{"token"}; - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { - if (event.command.get_command_name() == "test") { - // Make a message and add a button with its custom ID set to the command interaction's ID so we can identify it - dpp::message m{"Test"}; - std::string id{event.command.id.str()}; - m.add_component( - dpp::component{}.add_component(dpp::component{}.set_type(dpp::cot_button).set_label("Click me!").set_id(id)) - ); - co_await event.co_reply(m); - - auto result = co_await dpp::when_any{ // Whichever completes first... - event.from->creator->on_button_click.when([&id](const dpp::button_click_t &b) { return b.custom_id == id; }), // Button clicked - event.from->creator->co_sleep(5) // Or sleep 5 seconds - }; - // Note!! Due to a bug in g++11 and g++12, id must be captured as a reference above or the compiler will destroy it twice. This is fixed in g++13 - if (result.index() == 0) { // Awaitable #0 completed first, that is the button click event - // Acknowledge the click and edit the original response, removing the button - const dpp::button_click_t &click_event = result.get<0>(); - click_event.reply(); - event.edit_original_response(dpp::message{"You clicked the button with the id " + click_event.custom_id}); - } else { // Here index() is 1, the timer expired - event.edit_original_response(dpp::message{"I haven't got all day!"}); - } - } - }); - - bot.on_ready([&bot](const dpp::ready_t & event) { - if (dpp::run_once()) { - dpp::slashcommand command{"test", "Test awaiting for an event", bot.me.id}; - - bot.global_command_create(command); - } - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~ +\include{cpp} coro_expiring_buttons.cpp Any awaitable can be used with when_any, even dpp::task, dpp::coroutine, dpp::async. When the when_any object is destroyed, any of its awaitables with a cancel() method (for example \ref dpp::task::cancel "dpp::task") will have it called. With this you can easily make commands that ask for input in several steps, or maybe a timed text game, the possibilities are endless! Note that if the first awaitable completes with an exception, result.get will throw it. From ff7635cecf372ae4e4e48ff478d04c6a34afc522 Mon Sep 17 00:00:00 2001 From: Mihael <51310077+1mi0@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:56:39 +0300 Subject: [PATCH 6/7] added [[maybe_unused]] in some places where it was needed (#856) --- include/dpp/collector.h | 2 +- include/dpp/json_interface.h | 4 ++-- include/dpp/timed_listener.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/dpp/collector.h b/include/dpp/collector.h index dbc8caf823..4e388971c4 100644 --- a/include/dpp/collector.h +++ b/include/dpp/collector.h @@ -81,7 +81,7 @@ template class collector stored.push_back(*v); } }; - tl = new dpp::timed_listener, std::function>(cl, duration, event, f, [this](dpp::timer timer_handle) { + tl = new dpp::timed_listener, std::function>(cl, duration, event, f, [this]([[maybe_unused]] dpp::timer timer_handle) { if (!triggered) { triggered = true; completed(stored); diff --git a/include/dpp/json_interface.h b/include/dpp/json_interface.h index 56d1c3e64a..430a1522a4 100644 --- a/include/dpp/json_interface.h +++ b/include/dpp/json_interface.h @@ -44,7 +44,7 @@ namespace dpp { * @param j nlohmann::json object * @return T& Reference to self for fluent calling */ - T& fill_from_json(nlohmann::json* j) { + T& fill_from_json([[maybe_unused]] nlohmann::json* j) { throw dpp::logic_exception("JSON interface doesn't implement parse_from_json"); } @@ -54,7 +54,7 @@ namespace dpp { * @param with_id Include the ID in the JSON * @return std::string JSON string version of object */ - virtual std::string build_json(bool with_id = false) const { + virtual std::string build_json([[maybe_unused]] bool with_id = false) const { throw dpp::logic_exception("JSON interface doesn't implement build_json"); } }; diff --git a/include/dpp/timed_listener.h b/include/dpp/timed_listener.h index 0ec9a4d7ea..d9031847a5 100644 --- a/include/dpp/timed_listener.h +++ b/include/dpp/timed_listener.h @@ -73,7 +73,7 @@ template class timed_listene /* Attach event */ listener_handle = ev(listener); /* Create timer */ - th = cl->start_timer([this](dpp::timer timer_handle) { + th = cl->start_timer([this]([[maybe_unused]] dpp::timer timer_handle) { /* Timer has finished, detach it from event. * Only allowed to tick once. */ From f490fd33fe3b903d3ddd450126d3fef52e87847f Mon Sep 17 00:00:00 2001 From: Ruslan <79337999+ruslan-ilesik@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:33:16 -0400 Subject: [PATCH 7/7] Created get_url methods for message, channel and user. (#855) --- include/dpp/channel.h | 7 +++ include/dpp/message.h | 7 +++ include/dpp/user.h | 7 +++ include/dpp/utility.h | 39 ++++++++++++++++ src/dpp/channel.cpp | 4 ++ src/dpp/message.cpp | 4 ++ src/dpp/user.cpp | 4 ++ src/dpp/utility.cpp | 25 ++++++++++ src/unittest/test.cpp | 106 ++++++++++++++++++++++++++++++++++++++++++ src/unittest/test.h | 7 +++ 10 files changed, 210 insertions(+) diff --git a/include/dpp/channel.h b/include/dpp/channel.h index d14037bc0d..afa09823d9 100644 --- a/include/dpp/channel.h +++ b/include/dpp/channel.h @@ -609,6 +609,13 @@ class DPP_EXPORT channel : public managed, public json_interface { */ std::string get_icon_url(uint16_t size = 0, const image_type format = i_png) const; + /** + * @brief Returns string of URL to channel + * + * @return string of URL to channel + */ + std::string get_url() const; + /** * @brief Returns true if the channel is NSFW gated * diff --git a/include/dpp/message.h b/include/dpp/message.h index ed7137eeeb..422722fb0c 100644 --- a/include/dpp/message.h +++ b/include/dpp/message.h @@ -1555,6 +1555,13 @@ struct DPP_EXPORT message : public managed { * @return true if message has remixed attachment */ bool has_remix_attachment() const; + + /** + * @brief Returns URL to message + * + * @return string of URL to message + */ + std::string get_url() const; }; /** A group of messages */ diff --git a/include/dpp/user.h b/include/dpp/user.h index 4595c8e9c8..71c0978e7e 100644 --- a/include/dpp/user.h +++ b/include/dpp/user.h @@ -164,6 +164,13 @@ class DPP_EXPORT user : public managed, public json_interface { */ std::string get_mention() const; + /** + * @brief Returns URL to user + * + * @return string of URL to user + */ + std::string get_url() const; + /** * @brief Return true if user has the active Developer badge * diff --git a/include/dpp/utility.h b/include/dpp/utility.h index 59093a9099..487a90d1de 100644 --- a/include/dpp/utility.h +++ b/include/dpp/utility.h @@ -130,6 +130,11 @@ namespace dpp { */ inline const std::string cdn_host = "https://cdn.discordapp.com"; + /** + * @brief The base URL for message/user/channel links. + */ + inline const std::string url_host = "https://discord.com"; + /** * @brief Callback for the results of a command executed via dpp::utility::exec */ @@ -531,6 +536,40 @@ namespace dpp { */ std::string DPP_EXPORT role_mention(const snowflake& id); + /** + * @brief Create a URL for message. + * @param guild_id The ID of the guild where message is written. + * @param channel_id The ID of the channel where message is written. + * @param message_id The ID of the message. + * @return std::string The URL to message or empty string if any of ids is 0. + */ + std::string DPP_EXPORT message_url(const snowflake& guild_id, const snowflake& channel_id, const snowflake& message_id); + + /** + * @brief Create a URL for message. + * @param guild_id The ID of the guild where channel is located. + * @param channel_id The ID of the channel. + * @return std::string The URL to message or empty string if any of ids is 0. + */ + std::string DPP_EXPORT channel_url(const snowflake& guild_id, const snowflake& channel_id); + + /** + * @brief Create a URL for message. + * @param guild_id The ID of the guild where thread is located. + * @param thread_id The ID of the thread. + * @return std::string The URL to message or empty string if any of ids is 0. + */ + std::string DPP_EXPORT thread_url(const snowflake& guild_id, const snowflake& thread_id); + + /** + * @brief Create a URL for message. + * @param user_id The ID of the guild where thread is located. + * @return std::string The URL to message or empty string if id is 0. + */ + std::string DPP_EXPORT user_url(const snowflake& user_id); + + + #ifdef _DOXYGEN_ /** * @brief Get the mime type for an image type. diff --git a/src/dpp/channel.cpp b/src/dpp/channel.cpp index 021428d17e..ddbdc35dd8 100644 --- a/src/dpp/channel.cpp +++ b/src/dpp/channel.cpp @@ -636,6 +636,10 @@ std::string channel::get_icon_url(uint16_t size, const image_type format) const } } +std::string channel::get_url() const{ + return utility::channel_url(guild_id, id); +} + channel_type channel::get_type() const { return static_cast(flags & CHANNEL_TYPE_MASK); } diff --git a/src/dpp/message.cpp b/src/dpp/message.cpp index e1fb150570..9e08c62d8d 100644 --- a/src/dpp/message.cpp +++ b/src/dpp/message.cpp @@ -1126,6 +1126,10 @@ bool message::has_remix_attachment() const { }); } +std::string message::get_url() const { + return utility::message_url(guild_id, channel_id, id); +} + sticker::sticker() : managed(0), pack_id(0), type(st_standard), format_type(sf_png), available(true), guild_id(0), sort_value(0) { } diff --git a/src/dpp/user.cpp b/src/dpp/user.cpp index 05a7726b44..d40b5eb401 100644 --- a/src/dpp/user.cpp +++ b/src/dpp/user.cpp @@ -130,6 +130,10 @@ std::string user::get_mention() const { return utility::user_mention(id); } +std::string user::get_url() const{ + return utility::user_url(id); +} + bool user::is_active_developer() const { return this->flags & u_active_developer; } diff --git a/src/dpp/utility.cpp b/src/dpp/utility.cpp index 657a034d4d..cd922b1c46 100644 --- a/src/dpp/utility.cpp +++ b/src/dpp/utility.cpp @@ -550,6 +550,31 @@ namespace dpp { return "<@&" + std::to_string(id) + ">"; } + std::string message_url(const snowflake& guild_id, const snowflake& channel_id, const snowflake& message_id){ + if (guild_id.empty() || channel_id.empty() || message_id.empty()) { + return ""; + } + return url_host + "/channels/" + std::to_string(guild_id) + "/" + std::to_string(channel_id) + "/" + std::to_string(message_id); + } + + std::string channel_url(const snowflake& guild_id, const snowflake& channel_id){ + if (guild_id.empty() || channel_id.empty()) { + return ""; + } + return url_host + "/channels/" + std::to_string(guild_id) + "/" + std::to_string(channel_id); + } + + std::string thread_url(const snowflake& guild_id, const snowflake& thread_id){ + return channel_url(guild_id, thread_id); + }; + + std::string user_url(const snowflake& user_id){ + if (user_id.empty()) { + return ""; + } + return url_host + "/users/" + std::to_string(user_id); + }; + template std::enable_if_t, std::string> mime_type(T type) { static constexpr auto get_image_mime = [](image_type t) constexpr noexcept { diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 63ee11aa82..f3adb662f9 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -343,6 +343,14 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b set_test(USER_GET_CREATION_TIME, false); set_test(USER_GET_CREATION_TIME, (uint64_t)user1.get_creation_time() == 1465312605); + + set_test(USER_GET_URL, false); + + dpp::user user2; + set_test(USER_GET_URL, + user1.get_url() == dpp::utility::url_host + "/users/189759562910400512" && + user2.get_url() == "" + ); } { // avatar size function @@ -422,6 +430,48 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b set_test(EMOJI_GET_URL, emoji.get_url() == dpp::utility::cdn_host + "/emojis/825407338755653641.png"); } + { // message methods + dpp::message m; + m.guild_id = 825407338755653642; + m.channel_id = 956230231277072415; + m.id = 1151617986541666386; + + dpp::message m2; + m2.guild_id = 825407338755653642; + m2.channel_id = 956230231277072415; + + dpp::message m3; + m3.guild_id = 825407338755653642; + m3.id = 1151617986541666386; + + dpp::message m4; + m4.channel_id = 956230231277072415; + m4.id = 1151617986541666386; + + dpp::message m5; + m5.guild_id = 825407338755653642; + + dpp::message m6; + m6.channel_id = 956230231277072415; + + dpp::message m7; + m7.id = 1151617986541666386; + + dpp::message m8; + + set_test(MESSAGE_GET_URL, false); + set_test(MESSAGE_GET_URL, + m.get_url() == dpp::utility::url_host + "/channels/825407338755653642/956230231277072415/1151617986541666386" && + m2.get_url() == "" && + m3.get_url() == "" && + m4.get_url() == "" && + m5.get_url() == "" && + m6.get_url() == "" && + m7.get_url() == "" && + m8.get_url() == "" + ); + } + { // channel methods set_test(CHANNEL_SET_TYPE, false); dpp::channel c; @@ -435,6 +485,24 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b set_test(CHANNEL_GET_MENTION, false); c.id = 825411707521728511; set_test(CHANNEL_GET_MENTION, c.get_mention() == "<#825411707521728511>"); + + set_test(CHANNEL_GET_URL, false); + c.guild_id = 825407338755653642; + + dpp::channel c2; + c2.id = 825411707521728511; + + dpp::channel c3; + c3.guild_id = 825407338755653642; + + dpp::channel c4; + + set_test(CHANNEL_GET_URL, + c.get_url() == dpp::utility::url_host + "/channels/825407338755653642/825411707521728511" && + c2.get_url() == "" && + c3.get_url() == "" && + c4.get_url() == "" + ); } { // utility methods @@ -507,6 +575,44 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b emoji_mention3 == ":white_check_mark:" && emoji_mention4 == ":white_check_mark:" ); + + set_test(UTILITY_USER_URL, false); + auto user_url = dpp::utility::user_url(123); + set_test(UTILITY_USER_URL, + user_url == dpp::utility::url_host + "/users/123" && + dpp::utility::user_url(0) == "" + ); + + set_test(UTILITY_MESSAGE_URL, false); + auto message_url = dpp::utility::message_url(1,2,3); + set_test(UTILITY_MESSAGE_URL, + message_url == dpp::utility::url_host+ "/channels/1/2/3" && + dpp::utility::message_url(0,2,3) == "" && + dpp::utility::message_url(1,0,3) == "" && + dpp::utility::message_url(1,2,0) == "" && + dpp::utility::message_url(0,0,3) == "" && + dpp::utility::message_url(0,2,0) == "" && + dpp::utility::message_url(1,0,0) == "" && + dpp::utility::message_url(0,0,0) == "" + ); + + set_test(UTILITY_CHANNEL_URL, false); + auto channel_url = dpp::utility::channel_url(1,2); + set_test(UTILITY_CHANNEL_URL, + channel_url == dpp::utility::url_host+ "/channels/1/2" && + dpp::utility::channel_url(0,2) == "" && + dpp::utility::channel_url(1,0) == "" && + dpp::utility::channel_url(0,0) == "" + ); + + set_test(UTILITY_THREAD_URL, false); + auto thread_url = dpp::utility::thread_url(1,2); + set_test(UTILITY_THREAD_URL, + thread_url == dpp::utility::url_host+ "/channels/1/2" && + dpp::utility::thread_url(0,2) == "" && + dpp::utility::thread_url(1,0) == "" && + dpp::utility::thread_url(0,0) == "" + ); } #ifndef _WIN32 diff --git a/src/unittest/test.h b/src/unittest/test.h index 9bd88d29b2..865a2abb47 100644 --- a/src/unittest/test.h +++ b/src/unittest/test.h @@ -169,11 +169,14 @@ DPP_TEST(USER_GET, "cluster::user_get", tf_online); DPP_TEST(USER_GET_FLAGS, "cluster::user_get flag parsing", tf_online); DPP_TEST(MEMBER_GET, "cluster::guild_get_member", tf_online); DPP_TEST(USER_GET_MENTION, "user::get_mention", tf_offline); +DPP_TEST(USER_GET_URL, "user::get_url", tf_offline); DPP_TEST(USER_FORMAT_USERNAME, "user::format_username", tf_offline); DPP_TEST(USER_GET_CREATION_TIME, "user::get_creation_time", tf_offline); DPP_TEST(USER_GET_AVATAR_URL, "user::get_avatar_url", tf_offline); DPP_TEST(CHANNEL_SET_TYPE, "channel::set_type", tf_offline); DPP_TEST(CHANNEL_GET_MENTION, "channel::get_mention", tf_offline); +DPP_TEST(CHANNEL_GET_URL, "channel::get_url", tf_offline); +DPP_TEST(MESSAGE_GET_URL,"message::get_url",tf_offline); DPP_TEST(UTILITY_GUILD_NAVIGATION, "utility::guild_navigation", tf_offline); DPP_TEST(UTILITY_ICONHASH, "utility::iconhash", tf_offline); DPP_TEST(UTILITY_MAKE_URL_PARAMETERS, "utility::make_url_parameters", tf_offline); @@ -185,6 +188,10 @@ DPP_TEST(UTILITY_CHANNEL_MENTION, "utility::channel_mention", tf_offline); DPP_TEST(UTILITY_USER_MENTION, "utility::user_mention", tf_offline); DPP_TEST(UTILITY_ROLE_MENTION, "utility::role_mention", tf_offline); DPP_TEST(UTILITY_EMOJI_MENTION, "utility::emoji_mention", tf_offline); +DPP_TEST(UTILITY_USER_URL, "utility::user_url", tf_offline); +DPP_TEST(UTILITY_MESSAGE_URL, "utility::message_url", tf_offline); +DPP_TEST(UTILITY_CHANNEL_URL, "utility::channel_url", tf_offline); +DPP_TEST(UTILITY_THREAD_URL, "utility::thread_url", tf_offline); DPP_TEST(UTILITY_AVATAR_SIZE, "utility::avatar_size", tf_offline); DPP_TEST(UTILITY_CDN_ENDPOINT_URL_HASH, "utility::cdn_endpoint_url_hash", tf_offline); DPP_TEST(STICKER_GET_URL, "sticker::get_url aka utility::cdn_endpoint_url_sticker", tf_offline);