diff --git a/docpages/example_code/CMakeLists.txt b/docpages/example_code/CMakeLists.txt index e0fc2f9baf..a5d1309729 100644 --- a/docpages/example_code/CMakeLists.txt +++ b/docpages/example_code/CMakeLists.txt @@ -1,9 +1,39 @@ -cmake_minimum_required (VERSION 3.6) +# +# 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} -std=c++17 -pthread -O0 -fPIC -rdynamic -DFMT_HEADER_ONLY -Wall -Werror") +set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDPP_CORO -std=c++20 -pthread -O0 -fPIC -rdynamic -DFMT_HEADER_ONLY -Wall -Werror") set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0") file(GLOB example_list ./*.cpp) @@ -11,6 +41,5 @@ 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) + target_link_libraries(${examplename}_out dl dpp mpg123 oggz) endforeach(example) - 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/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/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/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/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/mp3.cpp b/docpages/example_code/mp3.cpp new file mode 100644 index 0000000000..a085df531e --- /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/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/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_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/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/editing-channels-and-messages.md b/docpages/example_programs/the_basics/editing-channels-and-messages.md index 0cd2099953..e36258d358 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; -} -~~~~~~~~~~ +\example{cpp} editing_messages.cpp Before editing: 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/using_coroutines/awaiting_events.md b/docpages/example_programs/using_coroutines/awaiting_events.md index 869c72e975..423d840f84 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); -} -~~~~~~~~~~ +\example{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.