From fe2e6110439aa27fcf08851f49c3b83d4431b486 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 13:02:29 +0100 Subject: [PATCH 01/10] build(deps): bump github/codeql-action from 3.24.9 to 3.25.3 (#1137) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 54a2e2f121..82684a0691 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,6 +65,6 @@ jobs: make -j2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 6394c3a570..77b426090d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -72,6 +72,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 with: sarif_file: results.sarif From b63c7d5e61e6f8ae232dea6add4077a93b4a4720 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 13:02:42 +0100 Subject: [PATCH 02/10] build(deps): bump actions/dependency-review-action from 4.1.3 to 4.3.2 (#1138) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 671cb47878..e9ecaf4427 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3 + uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2 From 949bf63246c425cc92512af6abba07779e89b03b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 13:02:52 +0100 Subject: [PATCH 03/10] build(deps): bump shivammathur/setup-php from 2.29.0 to 2.30.4 (#1139) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/construct-vcpkg-info.yml | 2 +- .github/workflows/documentation.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/construct-vcpkg-info.yml b/.github/workflows/construct-vcpkg-info.yml index 18a92e24bd..14d63d364a 100644 --- a/.github/workflows/construct-vcpkg-info.yml +++ b/.github/workflows/construct-vcpkg-info.yml @@ -20,7 +20,7 @@ jobs: egress-policy: audit - name: Setup PHP - uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d # v2 + uses: shivammathur/setup-php@c665c7a15b5295c2488ac8a87af9cb806cd72198 # v2 with: php-version: '8.1' diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 68658a10af..856cd58b8f 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -30,7 +30,7 @@ jobs: egress-policy: audit - name: Setup PHP - uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d # v2 + uses: shivammathur/setup-php@c665c7a15b5295c2488ac8a87af9cb806cd72198 # v2 with: php-version: '8.0' From b80b44fea48343c22f1796f23f283c40fae8b023 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 13:03:02 +0100 Subject: [PATCH 04/10] build(deps): bump docker/build-push-action from 5.1.0 to 5.3.0 (#1140) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0625bc0929..f7324ba5b3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -47,7 +47,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 with: push: true tags: brainboxdotcc/dpp From d69f373a7159b1f039bd0c162faa28e271fcbf76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 13:03:15 +0100 Subject: [PATCH 05/10] build(deps): bump step-security/harden-runner from 2.7.0 to 2.7.1 (#1141) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/codeql.yml | 2 +- .github/workflows/construct-vcpkg-info.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/documentation-check.yml | 2 +- .github/workflows/documentation.yml | 2 +- .github/workflows/gitguardian.yml | 2 +- .github/workflows/labeler.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/sitemap.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/target-master.yml | 2 +- .github/workflows/test-docs-examples.yml | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1aa3e3a05e..c703480ee0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: - { arch: 'arm64', concurrency: 4, os: [self-hosted, linux, ARM64], package: g++-12, cpp-version: g++-12, cmake-flags: '', cpack: 'yes', ctest: 'no' } steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit @@ -117,7 +117,7 @@ jobs: - { arch: 'arm64', concurrency: 2, os: [self-hosted, ARM64, macOS], cpp-version: clang++-15, cmake-flags: ''} steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit @@ -166,7 +166,7 @@ jobs: runs-on: ${{matrix.cfg.os}} steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit @@ -231,7 +231,7 @@ jobs: runs-on: ${{matrix.cfg.os}} steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 82684a0691..83de763be1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/construct-vcpkg-info.yml b/.github/workflows/construct-vcpkg-info.yml index 14d63d364a..17862fcadd 100644 --- a/.github/workflows/construct-vcpkg-info.yml +++ b/.github/workflows/construct-vcpkg-info.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index e9ecaf4427..bd74c3d5ec 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f7324ba5b3..a9c723dde0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,7 +22,7 @@ jobs: cancel-in-progress: false steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/documentation-check.yml b/.github/workflows/documentation-check.yml index fee18fd920..cb216a5a24 100644 --- a/.github/workflows/documentation-check.yml +++ b/.github/workflows/documentation-check.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 856cd58b8f..1351814535 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/gitguardian.yml b/.github/workflows/gitguardian.yml index 8e519a7186..5029b6c490 100644 --- a/.github/workflows/gitguardian.yml +++ b/.github/workflows/gitguardian.yml @@ -14,7 +14,7 @@ jobs: cancel-in-progress: true steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 0027d6a3f2..4479375c82 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 77b426090d..bf562859e2 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/sitemap.yml b/.github/workflows/sitemap.yml index b5ee2028e0..588fb1a052 100644 --- a/.github/workflows/sitemap.yml +++ b/.github/workflows/sitemap.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d11167f341..bd479cfb61 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/target-master.yml b/.github/workflows/target-master.yml index f92c20a53a..2d5ee1e1f4 100644 --- a/.github/workflows/target-master.yml +++ b/.github/workflows/target-master.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit diff --git a/.github/workflows/test-docs-examples.yml b/.github/workflows/test-docs-examples.yml index 7a9bff2112..423defd86f 100644 --- a/.github/workflows/test-docs-examples.yml +++ b/.github/workflows/test-docs-examples.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit From 941dc273d3edf294e8f890e003a5f52f90a4a036 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 13:03:24 +0100 Subject: [PATCH 06/10] build(deps): bump doxygen-awesome-css from `5b27b3a` to `9f97817` (#1143) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doxygen-awesome-css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doxygen-awesome-css b/doxygen-awesome-css index 5b27b3a747..9f97817e70 160000 --- a/doxygen-awesome-css +++ b/doxygen-awesome-css @@ -1 +1 @@ -Subproject commit 5b27b3a747ca1e559fa54149762cca0bad6036fb +Subproject commit 9f97817e703aa2c15503067b2a72c97f9d37f46e From bf63f66eb1ce79c06e2fb38537c75a89b118f755 Mon Sep 17 00:00:00 2001 From: Miuna <809711+Mishura4@users.noreply.github.com> Date: Wed, 1 May 2024 08:03:39 -0400 Subject: [PATCH 07/10] feat: poll support (#1136) --- include/dpp/cluster.h | 66 +++++ include/dpp/cluster_coro_calls.h | 52 ++++ include/dpp/dispatcher.h | 66 +++++ include/dpp/event.h | 2 + include/dpp/message.h | 294 ++++++++++++++++++-- include/dpp/restrequest.h | 33 +++ src/dpp/cluster/message.cpp | 32 +++ src/dpp/cluster_coro_calls.cpp | 16 ++ src/dpp/discordevents.cpp | 2 + src/dpp/events/message_poll_vote_add.cpp | 53 ++++ src/dpp/events/message_poll_vote_remove.cpp | 53 ++++ src/dpp/message.cpp | 172 +++++++++++- src/unittest/test.cpp | 95 ++++++- src/unittest/test.h | 3 + 14 files changed, 919 insertions(+), 20 deletions(-) create mode 100644 src/dpp/events/message_poll_vote_add.cpp create mode 100644 src/dpp/events/message_poll_vote_remove.cpp diff --git a/include/dpp/cluster.h b/include/dpp/cluster.h index d7b7e27469..b27d068e9d 100644 --- a/include/dpp/cluster.h +++ b/include/dpp/cluster.h @@ -992,6 +992,24 @@ class DPP_EXPORT cluster { */ event_router_t on_message_create; + /** + * @brief Called when a vote is added to a message poll. + * + * @see https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-add + * @note Use operator() to attach a lambda to this event, and the detach method to detach the listener using the returned ID. + * The function signature for this event takes a single `const` reference of type message_poll_vote_add_t&, and returns void. + */ + event_router_t on_message_poll_vote_add; + + /** + * @brief Called when a vote is removed from a message poll. + * + * @see https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-remove + * @note Use operator() to attach a lambda to this event, and the detach method to detach the listener using the returned ID. + * The function signature for this event takes a single `const` reference of type message_poll_vote_remove_t&, and returns void. + */ + event_router_t on_message_poll_vote_remove; + /** * @brief Called when a guild audit log entry is created. * @@ -1948,6 +1966,54 @@ class DPP_EXPORT cluster { */ void message_delete_bulk(const std::vector &message_ids, snowflake channel_id, command_completion_event_t callback = utility::log_error()); + /** + * @brief Get a list of users that voted for this specific answer. + * + * @param m Message that contains the poll to retrieve the answers from + * @param answer_id ID of the answer to retrieve votes from (see poll_answer::answer_id) + * @param after Users after this ID should be retrieved if this is set to non-zero + * @param limit This number of users maximum should be returned, up to 100 + * @param callback Function to call when the API call completes. + * @see https://discord.com/developers/docs/resources/poll#get-answer-voters + * On success the callback will contain a dpp::user_map object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error(). + */ + void poll_get_answer_voters(const message& m, uint32_t answer_id, snowflake after, uint64_t limit, command_completion_event_t callback = utility::log_error()); + + /** + * @brief Get a list of users that voted for this specific answer. + * + * @param message_id ID of the message with the poll to retrieve the answers from + * @param channel_id ID of the channel with the poll to retrieve the answers from + * @param answer_id ID of the answer to retrieve votes from (see poll_answer::answer_id) + * @param after Users after this ID should be retrieved if this is set to non-zero + * @param limit This number of users maximum should be returned, up to 100 + * @param callback Function to call when the API call completes. + * @see https://discord.com/developers/docs/resources/poll#get-answer-voters + * On success the callback will contain a dpp::user_map object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error(). + */ + void poll_get_answer_voters(snowflake message_id, snowflake channel_id, uint32_t answer_id, snowflake after, uint64_t limit, command_completion_event_t callback = utility::log_error()); + + /** + * @brief Immediately end a poll. + * + * @param m Message that contains the poll + * @param callback Function to call when the API call completes. + * @see https://discord.com/developers/docs/resources/poll#end-poll + * On success the callback will contain a dpp::message object representing the message containing the poll in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error(). + */ + void poll_end(const message &m, command_completion_event_t callback = utility::log_error()); + + /** + * @brief Immediately end a poll. + * + * @param message_id ID of the message with the poll to end + * @param channel_id ID of the channel with the poll to end + * @param callback Function to call when the API call completes. + * @see https://discord.com/developers/docs/resources/poll#end-poll + * On success the callback will contain a dpp::message object representing the message containing the poll in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error(). + */ + void poll_end(snowflake message_id, snowflake channel_id, command_completion_event_t callback = utility::log_error()); + /** * @brief Get a channel * diff --git a/include/dpp/cluster_coro_calls.h b/include/dpp/cluster_coro_calls.h index f5a41fa1ea..1e4af10fe5 100644 --- a/include/dpp/cluster_coro_calls.h +++ b/include/dpp/cluster_coro_calls.h @@ -1621,6 +1621,58 @@ */ [[nodiscard]] async co_message_unpin(snowflake channel_id, snowflake message_id); +/** + * @brief Get a list of users that voted for this specific answer. + * + * @param m Message that contains the poll to retrieve the answers from + * @param answer_id ID of the answer to retrieve votes from (see poll_answer::answer_id) + * @param after Users after this ID should be retrieved if this is set to non-zero + * @param limit This number of users maximum should be returned, up to 100 + * @return user_map returned object on completion + * @see dpp::cluster::poll_get_answer_voters + * @see https://discord.com/developers/docs/resources/poll#get-answer-voters + * \memberof dpp::cluster + */ +[[nodiscard]] async co_poll_get_answer_voters(const message& m, uint32_t answer_id, snowflake after, uint64_t limit); + +/** + * @brief Get a list of users that voted for this specific answer. + * + * @param message_id ID of the message with the poll to retrieve the answers from + * @param channel_id ID of the channel with the poll to retrieve the answers from + * @param answer_id ID of the answer to retrieve votes from (see poll_answer::answer_id) + * @param after Users after this ID should be retrieved if this is set to non-zero + * @param limit This number of users maximum should be returned, up to 100 + * @return user_map returned object on completion + * @see dpp::cluster::poll_get_answer_voters + * @see https://discord.com/developers/docs/resources/poll#get-answer-voters + * \memberof dpp::cluster + */ +[[nodiscard]] async co_poll_get_answer_voters(snowflake message_id, snowflake channel_id, uint32_t answer_id, snowflake after, uint64_t limit); + +/** + * @brief Immediately end a poll. + * + * @param m Message that contains the poll + * @return message returned object on completion + * @see dpp::cluster::poll_end + * @see https://discord.com/developers/docs/resources/poll#end-poll + * \memberof dpp::cluster + */ +[[nodiscard]] async co_poll_end(const message &m); + +/** + * @brief Immediately end a poll. + * + * @param message_id ID of the message with the poll to end + * @param channel_id ID of the channel with the poll to end + * @return message returned object on completion + * @see dpp::cluster::poll_end + * @see https://discord.com/developers/docs/resources/poll#end-poll + * \memberof dpp::cluster + */ +[[nodiscard]] async co_poll_end(snowflake message_id, snowflake channel_id); + /** * @brief Get a channel's pins * @see dpp::cluster::channel_pins_get diff --git a/include/dpp/dispatcher.h b/include/dpp/dispatcher.h index fef838fba7..6310aef3d4 100644 --- a/include/dpp/dispatcher.h +++ b/include/dpp/dispatcher.h @@ -1687,6 +1687,72 @@ struct DPP_EXPORT message_create_t : public event_dispatch_t { void reply(message&& msg, bool mention_replied_user = false, command_completion_event_t callback = utility::log_error()) const; }; +/** + * @brief Message poll vote add + */ +struct DPP_EXPORT message_poll_vote_add_t : public event_dispatch_t { + using event_dispatch_t::event_dispatch_t; + using event_dispatch_t::operator=; + + /** + * @brief ID of the user who added the vote + */ + snowflake user_id; + + /** + * @brief ID of the channel containing the vote + */ + snowflake channel_id; + + /** + * @brief ID of the message containing the vote + */ + snowflake message_id; + + /** + * @brief ID of the guild containing the vote or 0 for DMs + */ + snowflake guild_id; + + /** + * @brief ID of the answer in the message poll object + */ + uint32_t answer_id; +}; + +/** + * @brief Message poll vote remove + */ +struct DPP_EXPORT message_poll_vote_remove_t : public event_dispatch_t { + using event_dispatch_t::event_dispatch_t; + using event_dispatch_t::operator=; + + /** + * @brief ID of the user who added the vote + */ + snowflake user_id; + + /** + * @brief ID of the channel containing the vote + */ + snowflake channel_id; + + /** + * @brief ID of the message containing the vote + */ + snowflake message_id; + + /** + * @brief ID of the guild containing the vote or 0 for DMs + */ + snowflake guild_id; + + /** + * @brief ID of the answer in the message poll object + */ + uint32_t answer_id; +}; + /** * @brief Guild audit log entry create */ diff --git a/include/dpp/event.h b/include/dpp/event.h index fe6df9bb45..6921586755 100644 --- a/include/dpp/event.h +++ b/include/dpp/event.h @@ -100,6 +100,8 @@ event_decl(message_create,MESSAGE_CREATE); event_decl(message_update,MESSAGE_UPDATE); event_decl(message_delete,MESSAGE_DELETE); event_decl(message_delete_bulk,MESSAGE_DELETE_BULK); +event_decl(message_poll_vote_add,MESSAGE_POLL_VOTE_ADD); +event_decl(message_poll_vote_remove,MESSAGE_POLL_VOTE_REMOVE); /* Presence/typing */ event_decl(presence_update,PRESENCE_UPDATE); diff --git a/include/dpp/message.h b/include/dpp/message.h index 6455540e62..d64b481e82 100644 --- a/include/dpp/message.h +++ b/include/dpp/message.h @@ -79,14 +79,14 @@ enum component_type : uint8_t { }; /** - * @brief An emoji for a component (select menus included). + * @brief An emoji reference for a component (select menus included) or a poll. * - * To set an emoji on your button, you must set one of either the name or id fields. - * The easiest way is to use the dpp::component::set_emoji method. + * To set an emoji on your button or poll answer, you must set one of either the name or id fields. + * The easiest way for buttons is to use the dpp::component::set_emoji method. * * @note This is a **very** scaled down version of dpp::emoji, we advise that you refrain from using this. */ -struct component_emoji { +struct partial_emoji { /** * @brief The name of the emoji. * @@ -94,7 +94,7 @@ struct component_emoji { * actual unicode value of the emoji e.g. "😄" * and not for example ":smile:" */ - std::string name{""}; + std::string name{}; /** * @brief The emoji ID value for emojis that are custom @@ -115,6 +115,13 @@ struct component_emoji { bool animated{false}; }; +/** + * @brief An emoji for a component. Alias to partial_emoji, for backwards compatibility. + * + * @see partial_emoji + */ +using component_emoji = partial_emoji; + /** * @brief The data for a file attached to a message. * @@ -247,7 +254,7 @@ struct DPP_EXPORT select_option : public json_interface { /** * @brief The emoji for the select option. */ - component_emoji emoji; + partial_emoji emoji; /** * @brief Construct a new select option object @@ -449,7 +456,7 @@ class DPP_EXPORT component : public json_interface { /** * @brief The emoji for this component. */ - component_emoji emoji; + partial_emoji emoji; /** * @brief Constructor @@ -1365,39 +1372,267 @@ struct DPP_EXPORT sticker_pack : public managed, public json_interface stickers; + std::map stickers{}; /** * @brief Name of the sticker pack. */ - std::string name; + std::string name{}; /** * @brief ID of the pack's SKU. */ - snowflake sku_id; + snowflake sku_id{0}; /** * @brief Optional: ID of a sticker in the pack which is shown as the pack's icon. */ - snowflake cover_sticker_id; + snowflake cover_sticker_id{0}; /** * @brief Description of the sticker pack. */ - std::string description; + std::string description{}; /** * @brief ID of the sticker pack's banner image. */ - snowflake banner_asset_id; + snowflake banner_asset_id{}; +}; + +/** + * @brief Poll layout types + * + * @note At the time of writing Discord only has 1, "The, uhm, default layout type." + * @see https://discord.com/developers/docs/resources/poll#layout-type + */ +enum poll_layout_type { + /** + * @brief According to Discord, quote, "The, uhm, default layout type." + */ + pl_default = 1 +}; + +/** + * @brief Structure representing a poll media, for example the poll question or a possible poll answer. + * + * @see https://discord.com/developers/docs/resources/poll#poll-media-object-poll-media-object-structure + */ +struct poll_media { + /** + * @brief Text of the media + */ + std::string text{}; + + /** + * @brief Emoji of the media. + */ + partial_emoji emoji{}; +}; + +/** + * @brief Represents an answer in a poll. + * + * @see https://discord.com/developers/docs/resources/poll#poll-answer-object-poll-answer-object-structure + */ +struct poll_answer { + /** + * @brief ID of the answer. Only sent by the Discord API, this is a dead field when creating a poll. + * + * @warn At the time of writing the Discord API warns users not to rely on anything regarding sequence or "first value" of this field. + */ + uint32_t id{0}; + + /** + * @brief Data of the answer. + * + * @see poll_media + */ + poll_media media{}; +}; + +/** + * @brief Represents the results of a poll + * + * @see https://discord.com/developers/docs/resources/poll#poll-results-object-poll-results-object-structure + */ +struct poll_results { + /** + * @brief Represents a reference to an answer and its count of votes + * + * @see https://discord.com/developers/docs/resources/poll#poll-results-object-poll-answer-count-object-structure + */ + struct answer_count { + /** + * @brief ID of the answer. Relates to an answer in the answers field + * + * @see poll_answer::answer_id + */ + uint32_t answer_id{0}; + + /** + * @brief Number of votes for this answer + */ + uint32_t count{0}; + + /** + * @brief Whether the current user voted + */ + bool me_voted{false}; + }; + + /** + * @brief Whether the poll has finalized, and the answers are precisely counted + * + * @note Discord states that due to the way they count and cache answers, + * while a poll is running the count of answers might not be accurate. + */ + bool is_finalized{false}; + + /** + * @brief Count of votes for each answer. If an answer is not present in this list, + * then its vote count is 0 + */ + std::map answer_counts; +}; + +/** + * @brief Represents a poll. + * + * @see https://discord.com/developers/docs/resources/poll + */ +struct DPP_EXPORT poll { + /** + * @brief Poll question. At the time of writing only the text field is supported by Discord + * + * @see media + */ + poll_media question{}; + + /** + * @brief List of answers of the poll. + * + * @note At the time of writing this can contain up to 10 answers + * @see answer + */ + std::map answers{}; + + /** + * @brief When retriving a poll from the API, this is the timestamp at which the poll will expire. + * When creating a poll, this is the number of hours the poll should be up for, up to 7 days (168 hours), and this field will be rounded. + */ + double expiry{24.0}; + + /** + * @brief Whether a user can select multiple answers + */ + bool allow_multiselect{false}; + + /** + * @brief Layout type of the poll. Defaults to, well, pl_default + * + * @see poll_layout_type + */ + poll_layout_type layout_type{pl_default}; + + /** + * @brief The (optional) results of the poll. This field may or may not be present, and its absence means "unknown results", not "no results". + * + * @note Quote from Discord: "The results field may be not present in certain responses where, as an implementation detail, + * we do not fetch the poll results in our backend. This should be treated as "unknown results", + * as opposed to "no results". You can keep using the results if you have previously received them through other means." + * + * @see https://discord.com/developers/docs/resources/poll#poll-results-object + */ + std::optional results{std::nullopt}; /** - * @brief Construct a new sticker pack object + * @brief Set the question for this poll + * + * @param text Text for the question + * @return self for method chaining + */ + poll& set_question(const std::string& text); + + /** + * @brief Set the duration of the poll in hours + * + * @param hours Duration of the poll in hours, max 7 days (168 hours) at the time of writing + * @return self for method chaining + */ + poll& set_duration(uint32_t hours) noexcept; + + /** + * @brief Set the duration of the poll in hours + * + * @param hours Duration of the poll in hours + * @return self for method chaining + */ + poll& set_allow_multiselect(bool allow) noexcept; + + /** + * @brief Add an answer to this poll + * + * @note At the time of writing this, a poll can have up to 10 answers + * @param media Data of the answer + * @return self for method chaining + */ + poll& add_answer(const poll_media& media); + + /** + * @brief Add an answer to this poll + * + * @note At the time of writing this, a poll can have up to 10 answers + * @param text Text for the answer + * @param emoji_id Optional emoji + * @param is_animated Whether the emoji is animated + * @return self for method chaining */ - sticker_pack(); + poll& add_answer(const std::string& text, snowflake emoji_id = 0, bool is_animated = false); - virtual ~sticker_pack() = default; + /** + * @brief Add an answer to this poll + * + * @note At the time of writing this, a poll can have up to 10 answers + * @param text Text for the answer + * @param emoji Optional emoji + * @return self for method chaining + */ + poll& add_answer(const std::string& text, const std::string& emoji); + + /** + * @brief Add an answer to this poll + * + * @note At the time of writing this, a poll can have up to 10 answers + * @param text Text for the answer + * @param e Optional emoji + * @return self for method chaining + */ + poll& add_answer(const std::string& text, const emoji& e); + + /** + * @brief Helper to get the question text + * + * @return question.text + */ + [[nodiscard]] const std::string& get_question_text() const noexcept; + + /** + * @brief Helper to find an answer by ID + * + * @param id ID to find + * @return Pointer to the answer with the matching ID, or nullptr if not found + */ + [[nodiscard]] const poll_media* find_answer(uint32_t id) const noexcept; + + /** + * @brief Helper to find the vote count in the results + * + * @param answer_id ID of the answer to find + * @return std::optional Optional count of votes. An empty optional means Discord did not send the results, it does not mean 0. It can also mean the poll does not have an answer with this ID + * @see https://discord.com/developers/docs/resources/poll#poll-results-object + */ + [[nodiscard]] std::optional get_vote_count(uint32_t answer_id) const noexcept; }; /** @@ -1989,6 +2224,11 @@ struct DPP_EXPORT message : public managed, json_interface { */ bool mention_everyone; + /** + * @brief Optional poll attached to this message + */ + std::optional attached_poll; + /** * @brief Construct a new message object */ @@ -2316,6 +2556,28 @@ struct DPP_EXPORT message : public managed, json_interface { * @return string of URL to message */ std::string get_url() const; + + /** + * @brief Convenience method to set the poll + * + * @return message& Self reference for method chaining + */ + message& set_poll(const poll& p); + + /** + * @brief Convenience method to get the poll attached to this message + * + * @throw std::bad_optional_access if has_poll() == false + * @return const poll& Poll attached to this object + */ + [[nodiscard]] const poll& get_poll() const; + + /** + * @brief Method to check if the message has a poll + * + * @return bool Whether the message has a poll + */ + [[nodiscard]] bool has_poll() const noexcept; }; /** diff --git a/include/dpp/restrequest.h b/include/dpp/restrequest.h index e948713b17..93fdd27071 100644 --- a/include/dpp/restrequest.h +++ b/include/dpp/restrequest.h @@ -144,6 +144,7 @@ template<> inline void rest_request_list(dpp::cluster* c, const char* ba } }); } + /** * @brief Templated REST request helper to save on typing (for returned lists, specialised for voiceregions) * @@ -172,6 +173,7 @@ template<> inline void rest_request_list(dpp::cluster* c, const cha } }); } + /** * @brief Templated REST request helper to save on typing (for returned lists, specialised for bans) * @@ -201,6 +203,7 @@ template<> inline void rest_request_list(dpp::cluster* c, const char* basep } }); } + /** * @brief Templated REST request helper to save on typing (for returned lists, specialised for sticker packs) * @@ -232,6 +235,36 @@ template<> inline void rest_request_list(dpp::cluster* c, const ch }); } +/** + * @brief Templated REST request helper to save on typing (for returned lists) + * + * @tparam T singular type to return in lambda callback + * @tparam T map type to return in lambda callback + * @param c calling cluster + * @param basepath base path for API call + * @param major major API function + * @param minor minor API function + * @param method HTTP method + * @param postdata Post data or empty string + * @param key Key name of elements in the json list + * @param root Root element to look for + * @param callback Callback lambda + */ +template inline void rest_request_list(dpp::cluster* c, const char* basepath, const std::string &major, const std::string &minor, http_method method, const std::string& postdata, command_completion_event_t callback, const std::string& key, const std::string& root) { + c->post_rest(basepath, major, minor, method, postdata, [c, root, key, callback](json &j, const http_request_completion_t& http) { + std::unordered_map list; + confirmation_callback_t e(c, confirmation(), http); + if (!e.is_error()) { + for (auto & curr_item : j[root]) { + list[snowflake_not_null(&curr_item, key.c_str())] = T().fill_from_json(&curr_item); + } + } + if (callback) { + callback(confirmation_callback_t(c, list, http)); + } + }); +} + /** * @brief Templated REST request helper to save on typing (for returned lists, specialised for objects which doesn't have ids) * diff --git a/src/dpp/cluster/message.cpp b/src/dpp/cluster/message.cpp index 06fc23faba..824060c3e0 100644 --- a/src/dpp/cluster/message.cpp +++ b/src/dpp/cluster/message.cpp @@ -172,6 +172,38 @@ void cluster::message_unpin(snowflake channel_id, snowflake message_id, command_ } +void cluster::poll_get_answer_voters(const message& m, uint32_t answer_id, snowflake after, uint64_t limit, command_completion_event_t callback) { + std::map parameters { + {"limit", std::to_string(limit > 100 ? 100 : limit)} + }; + + if (after > 0) { + parameters["after"] = after; + } + rest_request_list(this, API_PATH "/channels", std::to_string(m.channel_id), "polls/" + std::to_string(m.id) + "/answers/" + std::to_string(answer_id) + utility::make_url_parameters(parameters), m_get, "", std::move(callback), "id", "users"); +} + +void cluster::poll_get_answer_voters(snowflake message_id, snowflake channel_id, uint32_t answer_id, snowflake after, uint64_t limit, command_completion_event_t callback) { + std::map parameters { + {"limit", std::to_string(limit > 100 ? 100 : limit)} + }; + + if (after > 0) { + parameters["after"] = after; + } + rest_request_list(this, API_PATH "/channels", std::to_string(channel_id), "polls/" + std::to_string(message_id) + "/answers/" + std::to_string(answer_id) + utility::make_url_parameters(parameters), m_get, "", std::move(callback), "id", "users"); +} + + +void cluster::poll_end(const message &m, command_completion_event_t callback) { + rest_request(this, API_PATH "/channels", std::to_string(m.channel_id), "polls/" + std::to_string(m.id) + "/expire", m_post, "", std::move(callback)); +} + +void cluster::poll_end(snowflake message_id, snowflake channel_id, command_completion_event_t callback) { + rest_request(this, API_PATH "/channels", std::to_string(channel_id), "polls/" + std::to_string(message_id) + "/expire", m_post, "", std::move(callback)); +} + + void cluster::channel_pins_get(snowflake channel_id, command_completion_event_t callback) { rest_request_list(this, API_PATH "/channels", std::to_string(channel_id), "pins", m_get, "", callback); } diff --git a/src/dpp/cluster_coro_calls.cpp b/src/dpp/cluster_coro_calls.cpp index dbf3da3757..dee4daef50 100644 --- a/src/dpp/cluster_coro_calls.cpp +++ b/src/dpp/cluster_coro_calls.cpp @@ -531,6 +531,22 @@ async cluster::co_message_unpin(snowflake channel_id, s return async{ this, static_cast(&cluster::message_unpin), channel_id, message_id }; } +async cluster::co_poll_get_answer_voters(const message& m, uint32_t answer_id, snowflake after, uint64_t limit) { + return async{ this, static_cast(&cluster::poll_get_answer_voters), m, answer_id, after, limit }; +} + +async cluster::co_poll_get_answer_voters(snowflake message_id, snowflake channel_id, uint32_t answer_id, snowflake after, uint64_t limit) { + return async{ this, static_cast(&cluster::poll_get_answer_voters), message_id, channel_id, answer_id, after, limit }; +} + +async cluster::co_poll_end(const message &m) { + return async{ this, static_cast(&cluster::poll_end), m }; +} + +async cluster::co_poll_end(snowflake message_id, snowflake channel_id) { + return async{ this, static_cast(&cluster::poll_end), message_id, channel_id }; +} + async cluster::co_channel_pins_get(snowflake channel_id) { return async{ this, static_cast(&cluster::channel_pins_get), channel_id }; } diff --git a/src/dpp/discordevents.cpp b/src/dpp/discordevents.cpp index 72c9596b77..bdaa51afe4 100644 --- a/src/dpp/discordevents.cpp +++ b/src/dpp/discordevents.cpp @@ -363,6 +363,8 @@ static const std::map event_map = { { "MESSAGE_REACTION_REMOVE", make_static_event() }, { "MESSAGE_REACTION_REMOVE_ALL", make_static_event() }, { "MESSAGE_REACTION_REMOVE_EMOJI", make_static_event() }, + { "MESSAGE_POLL_VOTE_ADD", make_static_event() }, + { "MESSAGE_POLL_VOTE_REMOVE", make_static_event() }, { "CHANNEL_PINS_UPDATE", make_static_event() }, { "GUILD_BAN_ADD", make_static_event() }, { "GUILD_BAN_REMOVE", make_static_event() }, diff --git a/src/dpp/events/message_poll_vote_add.cpp b/src/dpp/events/message_poll_vote_add.cpp new file mode 100644 index 0000000000..319d809e9c --- /dev/null +++ b/src/dpp/events/message_poll_vote_add.cpp @@ -0,0 +1,53 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * 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. + * + ************************************************************************************/ +#include +#include +#include +#include +#include + + +namespace dpp::events { + + +/** + * @brief Handle event + * + * @param client Websocket client (current shard) + * @param j JSON data for the event + * @param raw Raw JSON string + */ +void message_poll_vote_add::handle(discord_client* client, json &j, const std::string &raw) { + + if (!client->creator->on_message_poll_vote_add.empty()) { + json d = j["d"]; + dpp::message_poll_vote_add_t vote(client, raw); + vote.user_id = snowflake_not_null(&j, "user_id"); + vote.message_id = snowflake_not_null(&j, "message_id"); + vote.channel_id = snowflake_not_null(&j, "channel_id"); + vote.guild_id = snowflake_not_null(&j, "guild_id"); + vote.answer_id = int32_not_null(&j, "answer_id"); + client->creator->on_message_poll_vote_add.call(vote); + } +} + +}; diff --git a/src/dpp/events/message_poll_vote_remove.cpp b/src/dpp/events/message_poll_vote_remove.cpp new file mode 100644 index 0000000000..55c243e767 --- /dev/null +++ b/src/dpp/events/message_poll_vote_remove.cpp @@ -0,0 +1,53 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * 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. + * + ************************************************************************************/ +#include +#include +#include +#include +#include + + +namespace dpp::events { + + +/** + * @brief Handle event + * + * @param client Websocket client (current shard) + * @param j JSON data for the event + * @param raw Raw JSON string + */ +void message_poll_vote_remove::handle(discord_client* client, json &j, const std::string &raw) { + + if (!client->creator->on_message_poll_vote_add.empty()) { + json d = j["d"]; + dpp::message_poll_vote_remove_t vote(client, raw); + vote.user_id = snowflake_not_null(&j, "user_id"); + vote.message_id = snowflake_not_null(&j, "message_id"); + vote.channel_id = snowflake_not_null(&j, "channel_id"); + vote.guild_id = snowflake_not_null(&j, "guild_id"); + vote.answer_id = int32_not_null(&j, "answer_id"); + client->creator->on_message_poll_vote_remove.call(vote); + } +} + +}; diff --git a/src/dpp/message.cpp b/src/dpp/message.cpp index 31e53ea3fb..ed7709a413 100644 --- a/src/dpp/message.cpp +++ b/src/dpp/message.cpp @@ -485,6 +485,155 @@ component &component::add_default_value(const snowflake id, const component_defa return *this; } +namespace { + +poll_media get_poll_media(const nlohmann::json& obj, std::string_view key) { + poll_media retval{}; + + if (auto it = obj.find(key); it != obj.end()) { + const json& media_json = *it; + + retval.text = string_not_null(&media_json, "text"); + if (it = media_json.find("emoji"); it != media_json.end()) { + const json& emoji_json = *it; + + retval.emoji.animated = bool_not_null(&emoji_json, "animated"); + retval.emoji.name = string_not_null(&emoji_json, "name"); + retval.emoji.id = snowflake_not_null(&emoji_json, "id"); + } + } + return retval; +}; + +json make_json(const poll_media &media) { + json retval{}; + + if (media.emoji.id != 0) { + json& emoji_json = retval["emoji"]; + emoji_json["id"] = media.emoji.id; + emoji_json["animated"] = media.emoji.animated; + } else if (!media.emoji.name.empty()) { + json& emoji_json = retval["emoji"]; + emoji_json["name"] = media.emoji.name; + emoji_json["animated"] = media.emoji.animated; + } + retval["text"] = media.text; + return retval; +} + +} + +void from_json(const nlohmann::json& j, poll& p) { + p.question = get_poll_media(j, "question"); + if (auto it = j.find("answers"); it != j.end() && it->is_array()) { + for (const json& element : *it) { + auto id = int32_not_null(&element, "answer_id"); + p.answers.emplace(id, poll_answer{ + id, + get_poll_media(element, "poll_media") + }); + } + } + p.expiry = double_not_null(&j, "expiry"); + p.allow_multiselect = bool_not_null(&j, "allow_multiselect"); + p.layout_type = static_cast(int32_not_null(&j, "layout_type")); + if (auto it = j.find("results"); it != j.end()) { + const json& results_json = *it; + poll_results p_results{}; + + p_results.is_finalized = bool_not_null(&results_json, "is_finalized"); + if (it = results_json.find("answer_counts"); it != results_json.end() && it->is_array()) { + for (const json& answer_count_json : *it) { + auto id = int32_not_null(&answer_count_json, "id"); + p_results.answer_counts.emplace(id, poll_results::answer_count{ + id, + int32_not_null(&answer_count_json, "count"), + bool_not_null(&answer_count_json, "me_voted") + }); + } + } + p.results = std::move(p_results); + } +} + +void to_json(json& j, const poll &p) { + j["question"] = make_json(p.question); + + json& answers_json = j["answers"]; + for (const auto& [_, answer] : p.answers) { + answers_json.emplace_back()["poll_media"] = make_json(answer.media); + } + /* When sending a poll object expiry is a duration in hours so we clamp it to positive and round */ + j["duration"] = (p.expiry < 0.0 ? uint32_t{0} : static_cast(p.expiry + 0.5)); + j["allow_multiselect"] = p.allow_multiselect; + j["layout_type"] = static_cast(p.layout_type); +} + +poll& poll::set_question(const std::string& text) { + question.text = text; + return *this; +} + +poll& poll::set_duration(uint32_t hours) noexcept { + expiry = static_cast(hours); + return *this; +} + +poll& poll::set_allow_multiselect(bool allow) noexcept { + allow_multiselect = allow; + return *this; +} + +poll& poll::add_answer(const poll_media& media) { + uint32_t max = 0; + for (const auto &pair : answers) { + if (pair.first > max) { + max = pair.first; + } + } + answers.emplace(max + 1, poll_answer{max + 1, media}); + return *this; +} + +poll& poll::add_answer(const std::string& text, snowflake emoji_id, bool is_animated) { + return add_answer(poll_media{text, partial_emoji{{}, emoji_id, is_animated}}); +} + +poll& poll::add_answer(const std::string& text, const std::string& emoji) { + return add_answer(poll_media{text, partial_emoji{emoji, {}, false}}); +} + +poll& poll::add_answer(const std::string& text, const emoji& e) { + return add_answer(poll_media{text, partial_emoji{e.name, e.id, e.is_animated()}}); +} + +const std::string& poll::get_question_text() const noexcept { + return question.text; +} + +const poll_media *poll::find_answer(uint32_t id) const noexcept { + if (auto it = answers.find(id); it != answers.end()) { + return &it->second.media; + } + return nullptr; +} + +std::optional poll::get_vote_count(uint32_t answer_id) const noexcept { + if (!results.has_value()) { + return std::nullopt; + } + if (auto it = results->answer_counts.find(answer_id); it != results->answer_counts.end()) { + return it->second.count; + } + /* Answers not present can mean 0 */ + if (find_answer(answer_id) == nullptr) { + return std::nullopt; + } + return 0; +} + + + embed::~embed() = default; embed::embed() : timestamp(0) { @@ -617,6 +766,19 @@ message& message::set_guild_id(snowflake _guild_id) { return *this; } +message& message::set_poll(const poll& p) { + attached_poll = p; + return *this; +} + +const poll &message::get_poll() const { + return attached_poll.value(); +} + +bool message::has_poll() const noexcept { + return attached_poll.has_value(); +} + message::message(const std::string &_content, message_type t) : message() { content = utility::utf8substr(_content, 0, 4000); type = t; @@ -1052,6 +1214,10 @@ json message::to_json(bool with_id, bool is_interaction_response) const { j["embeds"].push_back(e); } + if (attached_poll.has_value()) { + dpp::to_json(j["poll"], *attached_poll); + } + return j; } @@ -1230,6 +1396,9 @@ message& message::fill_from_json(json* d, cache_policy_t cp) { message_reference.message_id = snowflake_not_null(&mr, "message_id"); message_reference.fail_if_not_exists = bool_not_null(&mr, "fail_if_not_exists"); } + if (auto it = d->find("poll"); it != d->end()) { + from_json(*it, attached_poll.emplace()); + } return *this; } @@ -1294,9 +1463,6 @@ json sticker::to_json_impl(bool with_id) const { return j; } -sticker_pack::sticker_pack() : managed(0), sku_id(0), cover_sticker_id(0), banner_asset_id(0) { -} - sticker_pack& sticker_pack::fill_from_json_impl(nlohmann::json* j) { this->id = snowflake_not_null(j, "id"); this->sku_id = snowflake_not_null(j, "sku_id"); diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 1b76713535..1d12d64d28 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -2093,7 +2093,7 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b } }); } - + set_test(THREAD_CREATE, false); if (!offline) { bot.thread_create("thread test", TEST_TEXT_CHANNEL_ID, 60, dpp::channel_type::CHANNEL_PUBLIC_THREAD, true, 60, [&](const dpp::confirmation_callback_t &event) { @@ -2105,6 +2105,99 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b }); } + start_test(POLL_CREATE); + if (!offline) { + dpp::message poll_msg{}; + + poll_msg.set_poll(dpp::poll{} + .set_question("hello!") + .add_answer("one", dpp::unicode_emoji::one) + .add_answer("two", dpp::unicode_emoji::two) + .add_answer("three", dpp::unicode_emoji::three) + .add_answer("four") + .set_duration(48) + .set_allow_multiselect(true) + ).set_channel_id(TEST_TEXT_CHANNEL_ID); + + bot.message_create(poll_msg, [&bot, poll_msg](const dpp::confirmation_callback_t& result) { + if (result.is_error()) { + set_status(POLL_CREATE, ts_failed, result.get_error().human_readable); + return; + } + + const dpp::message& m = std::get(result.value); + + if (!m.attached_poll.has_value()) { + set_status(POLL_CREATE, ts_failed, "poll missing in received message"); + return; + } + + if (m.attached_poll->find_answer(std::numeric_limits::max()) != nullptr) { + set_status(POLL_CREATE, ts_failed, "poll::find_answer failed to return nullptr"); + return; + } + + std::array correct = {false, false, false, false}; + int i = 0; + for (const auto& [_, answer] : m.attached_poll->answers) { + if (m.attached_poll->find_answer(answer.id) != &answer.media) { + set_status(POLL_CREATE, ts_failed, "poll::find_answer failed to return valid answer"); + return; + } + if (answer.media.text == "one" && answer.media.emoji.name == dpp::unicode_emoji::one) { + if (correct[i]) { + set_status(POLL_CREATE, ts_failed, "poll answer found twice"); + return; + } + correct[i] = true; + } + if (answer.media.text == "two" && answer.media.emoji.name == dpp::unicode_emoji::two) { + if (correct[i]) { + set_status(POLL_CREATE, ts_failed, "poll answer found twice"); + return; + } + correct[i] = true; + } + if (answer.media.text == "three" && answer.media.emoji.name == dpp::unicode_emoji::three) { + if (correct[i]) { + set_status(POLL_CREATE, ts_failed, "poll answer found twice"); + return; + } + correct[i] = true; + } + if (answer.media.text == "four" && answer.media.emoji.name.empty()) { + if (correct[i]) { + set_status(POLL_CREATE, ts_failed, "poll answer found twice"); + return; + } + correct[i] = true; + bot.poll_get_answer_voters(m, answer.id, 0, 100, [m, &bot](const dpp::confirmation_callback_t& result) { + if (result.is_error()) { + set_status(POLL_CREATE, ts_failed, "poll_get_answer_voters: " + result.get_error().human_readable); + return; + } + + start_test(POLL_END); + bot.poll_end(m, [message_id = m.id, channel_id = m.channel_id, &bot](const dpp::confirmation_callback_t& result) { + if (result.is_error()) { + set_status(POLL_END, ts_failed, result.get_error().human_readable); + return; + } + set_status(POLL_END, ts_success); + bot.message_delete(message_id, channel_id); + }); + }); + } + ++i; + } + if (correct == std::array{true, true, true, true}) { + set_status(POLL_CREATE, ts_success); + } else { + set_status(POLL_CREATE, ts_failed, "failed to find the submitted answers"); + } + }); + } + set_test(MEMBER_GET, false); if (!offline) { bot.guild_get_member(TEST_GUILD_ID, TEST_USER_ID, [](const dpp::confirmation_callback_t &event){ diff --git a/src/unittest/test.h b/src/unittest/test.h index 2cdb80ac77..51a707a06e 100644 --- a/src/unittest/test.h +++ b/src/unittest/test.h @@ -233,6 +233,9 @@ DPP_TEST(VOICESEND, "Send audio to voice channel", tf_online | tf_extended); // DPP_TEST(MESSAGEPIN, "Pinning a channel message", tf_online | tf_extended); DPP_TEST(MESSAGEUNPIN, "Unpinning a channel message", tf_online | tf_extended); +DPP_TEST(POLL_CREATE, "Creating a poll", tf_online); +DPP_TEST(POLL_END, "Ending a poll", tf_online); + DPP_TEST(THREAD_MEMBER_ADD, "cluster::thread_member_add", tf_online | tf_extended); DPP_TEST(THREAD_MEMBER_GET, "cluster::thread_member_get", tf_online | tf_extended); DPP_TEST(THREAD_MEMBERS_GET, "cluster::thread_members_get", tf_online | tf_extended); From 698c14ff4732e5b5ca1a327c696c6fc9f4c6e173 Mon Sep 17 00:00:00 2001 From: Neko Life Date: Wed, 1 May 2024 23:39:04 +0700 Subject: [PATCH 08/10] fix: fix uncleared track meta on stop audio (#1127) --- src/dpp/discordvoiceclient.cpp | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/dpp/discordvoiceclient.cpp b/src/dpp/discordvoiceclient.cpp index 5e514d7331..8727992a1c 100644 --- a/src/dpp/discordvoiceclient.cpp +++ b/src/dpp/discordvoiceclient.cpp @@ -692,6 +692,8 @@ dpp::utility::uptime discord_voice_client::get_remaining() { discord_voice_client& discord_voice_client::stop_audio() { std::lock_guard lock(this->stream_mutex); outbuf.clear(); + track_meta.clear(); + tracks = 0; return *this; } @@ -847,7 +849,7 @@ void discord_voice_client::write_ready() std::lock_guard lock(this->stream_mutex); if (!this->paused && outbuf.size()) { type = send_audio_type; - if (outbuf[0].packet.size() == 2 && (*((uint16_t*)(outbuf[0].packet.data()))) == AUDIO_TRACK_MARKER) { + if (outbuf[0].packet.size() == sizeof(uint16_t) && (*((uint16_t*)(outbuf[0].packet.data()))) == AUDIO_TRACK_MARKER) { outbuf.erase(outbuf.begin()); track_marker_found = true; if (tracks > 0) { @@ -1165,20 +1167,29 @@ uint32_t discord_voice_client::get_tracks_remaining() { discord_voice_client& discord_voice_client::skip_to_next_marker() { std::lock_guard lock(this->stream_mutex); - /* Keep popping the first entry off the outbuf until the first entry is a track marker */ - while (!outbuf.empty() && outbuf[0].packet.size() != sizeof(uint16_t) && (*((uint16_t*)(outbuf[0].packet.data()))) != AUDIO_TRACK_MARKER) { - outbuf.erase(outbuf.begin()); - } - if (outbuf.size()) { - /* Remove the actual track marker out of the buffer */ - outbuf.erase(outbuf.begin()); + if (!outbuf.empty()) { + /* Find the first marker to skip to */ + auto i = std::find_if(outbuf.begin(), outbuf.end(), [](const voice_out_packet &v){ + return v.packet.size() == sizeof(uint16_t) && (*((uint16_t*)(v.packet.data()))) == AUDIO_TRACK_MARKER; + }); + + if (i != outbuf.end()) { + /* Skip queued packets until including found marker */ + outbuf.erase(outbuf.begin(), i+1); + } else { + /* No market found, skip the whole queue */ + outbuf.clear(); + } } + if (tracks > 0) { tracks--; } + if (!track_meta.empty()) { track_meta.erase(track_meta.begin()); } + return *this; } From 2522c214cc6641b5b125d8dfcc5396dbe91c0ed8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 18:02:10 +0100 Subject: [PATCH 09/10] build(deps): bump ubuntu from `80ef4a4` to `71b82b8` (#1130) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e353f81393..416d640a81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:focal@sha256:80ef4a44043dec4490506e6cc4289eeda2d106a70148b74b5ae91ee670e9c35d +FROM ubuntu:focal@sha256:71b82b8e734f5cd0b3533a16f40ca1271f28d87343972bb4cd6bd6c38f1bd38e ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install --no-install-recommends -y libssl-dev zlib1g-dev libsodium-dev libopus-dev cmake pkg-config g++ gcc git make && apt-get clean && rm -rf /var/lib/apt/lists/* From 70077e8d84621893be079e07b6a01a554a9a27eb Mon Sep 17 00:00:00 2001 From: Nidhoegger <117999161+Nidhoegger@users.noreply.github.com> Date: Wed, 1 May 2024 19:13:50 +0200 Subject: [PATCH 10/10] fix: Signal Handlers on Non-Windows Platforms in sslclient (#1123) Co-authored-by: Craig Edwards (Brain) --- src/dpp/sslclient.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/dpp/sslclient.cpp b/src/dpp/sslclient.cpp index 428bdebe32..f9aa2df1ac 100644 --- a/src/dpp/sslclient.cpp +++ b/src/dpp/sslclient.cpp @@ -223,6 +223,18 @@ int connect_with_timeout(dpp::socket sockfd, const struct sockaddr *addr, sockle #endif } +#ifndef _WIN32 +void set_signal_handler(int signal) +{ + struct sigaction sa; + sigaction(signal, nullptr, &sa); + if (sa.sa_flags == 0 && sa.sa_handler == nullptr) { + sa = {}; + sigaction(signal, &sa, nullptr); + } +} +#endif + ssl_client::ssl_client(const std::string &_hostname, const std::string &_port, bool plaintext_downgrade, bool reuse) : nonblocking(false), sfd(INVALID_SOCKET), @@ -237,11 +249,11 @@ ssl_client::ssl_client(const std::string &_hostname, const std::string &_port, b keepalive(reuse) { #ifndef WIN32 - signal(SIGALRM, SIG_IGN); + set_signal_handler(SIGALRM); + set_signal_handler(SIGXFSZ); + set_signal_handler(SIGCHLD); signal(SIGHUP, SIG_IGN); signal(SIGPIPE, SIG_IGN); - signal(SIGCHLD, SIG_IGN); - signal(SIGXFSZ, SIG_IGN); #else // Set up winsock. WSADATA wsadata;