From e584be6be4b99efd3591263849141aba2f1bf747 Mon Sep 17 00:00:00 2001 From: Valithor Obsidion Date: Fri, 27 Sep 2024 10:38:06 -0400 Subject: [PATCH] Poll permissions, events, and other related changes (#1226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Implement the Poll object `Parts` * 🧑‍💻 Add a `poll` attribute to the `Message` part * 🧑‍💻 Add poll support to the `MessageBuilder` * ✨ Add event support to polls * 🧑‍💻 Allow passing an array to `Poll::addAnswer()` * 🎨 Clean up classes * 🎨 Update the poll expire endpoint constant * 🎨 Improve docblock wording * 🚚 Move the `Poll` object part to the `Channel` namespace * 🚚 Move the `Poll` create object art to the `Poll` namespace * 🎨 Improve `Poll::addAnswer` when passed an array * 🎨 Fix total answer count in `Poll:addAnswer` * 🎨 Push answers into a collection * ♻ Move `Poll::getAnswerVoters()` to `PollAnswer::getVoters()` * 🎨 Improve docblock wording * ⏪ Revert version change * 🩹 Fix return types * 🩹 Remove unused prop docblock * 🎨 Update `PollAnswer::getVoters` to be uniform with `Reaction::getUsers` * 🚧 Utilize caching in the `MESSAGE_POLL_VOTE_ADD` and `MESSAGE_POLL_VOTE_REMOVE` events * 🔧 Update the endpoint for `PollAnswer::getVoters` * 🚧 Create a repository for poll answers * 🎨 Add docblock for event fillable attributes * 🎨 Improve return types * 🎨 Rename `Poll::end` to `Poll::expire` for parity with the Discord API * 🎨 Remove unnecessary linebreak * 🔧 Update the `Poll::expire` endpoint * 🎨 Handle setting the `answers` attribute --------- Co-authored-by: Brandon --- src/Discord/Builders/MessageBuilder.php | 25 ++- src/Discord/Parts/Channel/Message.php | 17 ++ src/Discord/Parts/Channel/Poll.php | 140 ++++++++++++++ src/Discord/Parts/Channel/Poll/Poll.php | 173 +++++++++++++++++ src/Discord/Parts/Channel/Poll/PollAnswer.php | 180 ++++++++++++++++++ .../Parts/Channel/Poll/PollAnswerCount.php | 37 ++++ src/Discord/Parts/Channel/Poll/PollMedia.php | 96 ++++++++++ .../Parts/Channel/Poll/PollResults.php | 45 +++++ .../Parts/Permissions/ChannelPermission.php | 2 + src/Discord/Parts/Permissions/Permission.php | 2 + .../Parts/Permissions/RolePermission.php | 2 + .../Channel/PollAnswerRepository.php | 47 +++++ src/Discord/WebSockets/Event.php | 2 + .../WebSockets/Events/MessagePollVoteAdd.php | 68 +++++++ .../Events/MessagePollVoteRemove.php | 61 ++++++ src/Discord/WebSockets/Handlers.php | 2 + src/Discord/WebSockets/Intents.php | 16 ++ 17 files changed, 910 insertions(+), 5 deletions(-) create mode 100644 src/Discord/Parts/Channel/Poll.php create mode 100644 src/Discord/Parts/Channel/Poll/Poll.php create mode 100644 src/Discord/Parts/Channel/Poll/PollAnswer.php create mode 100644 src/Discord/Parts/Channel/Poll/PollAnswerCount.php create mode 100644 src/Discord/Parts/Channel/Poll/PollMedia.php create mode 100644 src/Discord/Parts/Channel/Poll/PollResults.php create mode 100644 src/Discord/Repository/Channel/PollAnswerRepository.php create mode 100644 src/Discord/WebSockets/Events/MessagePollVoteAdd.php create mode 100644 src/Discord/WebSockets/Events/MessagePollVoteRemove.php diff --git a/src/Discord/Builders/MessageBuilder.php b/src/Discord/Builders/MessageBuilder.php index bd5649d15..4566b6508 100644 --- a/src/Discord/Builders/MessageBuilder.php +++ b/src/Discord/Builders/MessageBuilder.php @@ -19,6 +19,7 @@ use Discord\Http\Exceptions\RequestFailedException; use Discord\Parts\Channel\Attachment; use Discord\Parts\Channel\Message; +use Discord\Parts\Channel\Poll\Poll; use Discord\Parts\Embed\Embed; use Discord\Parts\Guild\Sticker; use JsonSerializable; @@ -61,6 +62,7 @@ class MessageBuilder implements JsonSerializable * @var string|null */ private $avatar_url; + /** * Whether the message is text-to-speech. * @@ -132,9 +134,9 @@ class MessageBuilder implements JsonSerializable private $enforce_nonce; /** - * A poll! + * The poll for the message. * - * @var mixed + * @var Poll|null */ private $poll; @@ -207,11 +209,11 @@ public function setEnforceNonce(bool $enforce_nonce = true): self /** * Sets the poll of the message. * - * @param mixed $poll Poll object. + * @param Poll|null $poll * * @return $this */ - public function setPoll($poll): self + public function setPoll(Poll|null $poll): self { $this->poll = $poll; @@ -703,6 +705,11 @@ public function jsonSerialize(): ?array $empty = false; } + if (isset($this->poll)) { + $body['poll'] = $this->poll; + $empty = false; + } + if (isset($this->allowed_mentions)) { $body['allowed_mentions'] = $this->allowed_mentions; } @@ -732,7 +739,15 @@ public function jsonSerialize(): ?array if (isset($this->flags)) { $body['flags'] = $this->flags; } elseif ($empty) { - throw new RequestFailedException('You cannot send an empty message. Set the content or add an embed or file.'); + throw new RequestFailedException('You cannot send an empty message. Set the content or add an embed, file or poll.'); + } + + if (isset($this->enforce_nonce)) { + $body['enforce_nonce'] = $this->enforce_nonce; + } + + if (isset($this->poll)) { + $body['poll'] = $this->poll; } if (isset($this->enforce_nonce)) { diff --git a/src/Discord/Parts/Channel/Message.php b/src/Discord/Parts/Channel/Message.php index 3fa2793eb..a4ba657de 100644 --- a/src/Discord/Parts/Channel/Message.php +++ b/src/Discord/Parts/Channel/Message.php @@ -14,6 +14,7 @@ use Carbon\Carbon; use Discord\Builders\MessageBuilder; use Discord\Helpers\Collection; +use Discord\Parts\Channel\Poll; use Discord\Parts\Embed\Embed; use Discord\Parts\Guild\Emoji; use Discord\Parts\Guild\Role; @@ -76,6 +77,7 @@ * @property Collection|Sticker[]|null $sticker_items Stickers attached to the message. * @property int|null $position A generally increasing integer (there may be gaps or duplicates) that represents the approximate position of the message in a thread, it can be used to estimate the relative position of the message in a thread in company with `total_message_sent` on parent thread. * @property object|null $role_subscription_data Data of the role subscription purchase or renewal that prompted this `ROLE_SUBSCRIPTION_PURCHASE` message. + * @property Poll|null $poll The poll attached to the message. * * @property-read bool $crossposted Message has been crossposted. * @property-read bool $is_crosspost Message is a crosspost from another channel. @@ -212,6 +214,7 @@ class Message extends Part 'sticker_items', 'position', 'role_subscription_data', + 'poll', // @internal 'guild_id', @@ -715,6 +718,20 @@ protected function getStickerItemsAttribute(): ?Collection return $sticker_items; } + /** + * Returns the poll attribute. + * + * @return Poll|null + */ + protected function getPollAttribute(): ?Poll + { + if (! isset($this->attributes['poll'])) { + return null; + } + + return $this->factory->part(Poll::class, (array) $this->attributes['poll'] + ['channel_id' => $this->channel_id, 'message_id' => $this->id], true); + } + /** * Returns the message link attribute. * diff --git a/src/Discord/Parts/Channel/Poll.php b/src/Discord/Parts/Channel/Poll.php new file mode 100644 index 000000000..f5648caf0 --- /dev/null +++ b/src/Discord/Parts/Channel/Poll.php @@ -0,0 +1,140 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Parts\Channel; + +use Carbon\Carbon; +use Discord\Helpers\Collection; +use Discord\Http\Endpoint; +use Discord\Parts\Channel\Poll\PollAnswer; +use Discord\Parts\Channel\Poll\PollMedia; +use Discord\Parts\Channel\Poll\PollResults; +use Discord\Parts\Part; +use Discord\Repository\Channel\PollAnswerRepository; +use React\Promise\ExtendedPromiseInterface; + +/** + * A message poll. + * + * @link https://discord.com/developers/docs/resources/poll#poll-object + * + * @since 10.0.0 + * + * @property PollMedia $question The question of the poll. Only text is supported. + * @property PollAnswerRepository $answers Each of the answers available in the poll. + * @property Carbon $expiry The time when the poll ends. + * @property bool $allow_multiselect Whether a user can select multiple answers. + * @property int $layout_type The layout type of the poll. + * @property PollResults|null $results The results of the poll. + * + * @property string $channel_id The ID of the channel the poll is in. + * @property string $message_id The ID of the message the poll is in. + */ +class Poll extends Part +{ + /** + * {@inheritdoc} + */ + protected $fillable = [ + 'question', + 'expiry', + 'allow_multiselect', + 'layout_type', + 'results', + + // events + 'channel_id', + 'message_id', + + // repositories + 'answers', + ]; + + /** + * {@inheritdoc} + */ + protected $repositories = [ + 'answers' => PollAnswerRepository::class, + ]; + + /** + * Sets the answers attribute. + * + * @param array $answers + */ + protected function setAnswersAttribute(array $answers): void + { + foreach ($answers as $answer) { + /** @var ?PollAnswer */ + if ($part = $this->answers->offsetGet($answer->answer_id)) { + $part->fill($answer); + } else { + /** @var PollAnswer */ + $part = $this->answers->create($answer); + } + + $this->answers->pushItem($part); + } + + $this->attributes['answers'] = $answers; + } + + /** + * Returns the question attribute. + * + * @return PollMedia + */ + protected function getQuestionAttribute(): PollMedia + { + return $this->factory->part(PollMedia::class, (array) $this->attributes['question'], true); + } + + /** + * Return the expiry attribute. + * + * @return Carbon + * + * @throws \Exception + */ + protected function getExpiryAttribute(): Carbon + { + return Carbon::parse($this->attributes['expiry']); + } + + /** + * Returns the results attribute. + * + * @return PollResults|null + */ + protected function getResultsAttribute(): ?PollResults + { + if (! isset($this->attributes['results'])) { + return null; + } + + return $this->factory->part(PollResults::class, (array) $this->attributes['results'], true); + } + + /** + * Expire the poll. + * + * @link https://discord.com/developers/docs/resources/poll#end-poll + * + * @return ExtendedPromiseInterface + */ + public function expire(): ExtendedPromiseInterface + { + return $this->http->post(Endpoint::bind(Endpoint::MESSAGE_POLL_EXPIRE, $this->channel_id, $this->message_id)) + ->then(function ($response) { + return $this->factory->create(Message::class, (array) $response, true); + }); + } +} diff --git a/src/Discord/Parts/Channel/Poll/Poll.php b/src/Discord/Parts/Channel/Poll/Poll.php new file mode 100644 index 000000000..2523f268b --- /dev/null +++ b/src/Discord/Parts/Channel/Poll/Poll.php @@ -0,0 +1,173 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Parts\Channel\Poll; + +use Discord\Parts\Part; + +use function Discord\poly_strlen; + +/** + * A poll that can be attached to a message. + * + * @link https://discord.com/developers/docs/resources/poll#poll-create-request-object-poll-create-request-object-structure + * + * @since 10.0.0 + * + * @property PollMedia $question The question of the poll. Only text is supported. + * @property PollAnswer[] $answers Each of the answers available in the poll, up to 10. + * @property int $duration Number of hours the poll should be open for, up to 7 days. + * @property bool $allow_multiselect Whether a user can select multiple answers. + * @property int|null $layout_type? The layout type of the poll. Defaults to... DEFAULT! + */ +class Poll extends Part +{ + public const LAYOUT_DEFAULT = 1; + + /** + * {@inheritdoc} + */ + protected $fillable = [ + 'question', + 'answers', + 'duration', + 'allow_multiselect', + 'layout_type', + ]; + + /** + * Set the question attribute. + * + * @param PollMedia|string $question The question of the poll. + * + * @throws \LengthException + * + * @return $this + */ + public function setQuestion(PollMedia|string $question): self + { + $question = $question instanceof PollMedia + ? $question + : new PollMedia($this->discord, [ + 'text' => $question, + ]); + + if (poly_strlen($question->text) > 300) { + throw new \LengthException('Question must be maximum 300 characters.'); + } + + $this->attributes['question'] = $question; + + return $this; + } + + /** + * Set the answers attribute. + * + * @param PollAnswer[] $answers Each of the answers available in the poll. + * + * @return $this + */ + public function setAnswers(array $answers): self + { + foreach ($answers as $answer) { + $this->addAnswer($answer); + } + + return $this; + } + + /** + * Add an answer to the poll. + */ + public function addAnswer(PollAnswer|PollMedia|array|string $answer): self + { + if (count($this->answers ?? []) >= 10) { + throw new \OutOfRangeException('Polls can only have up to 10 answers.'); + } + + if ($answer instanceof PollAnswer) { + $this->attributes['answers'][] = $answer; + + return $this; + } + + if (! $answer instanceof PollMedia) { + $text = is_string($answer) + ? $answer + : $answer['text']; + + $emoji = $answer['emoji'] ?? null; + + $answer = (new PollMedia($this->discord)) + ->setText($text) + ->setEmoji($emoji); + } + + if (poly_strlen($answer->text) > 55) { + throw new \LengthException('Answer must be maximum 55 characters.'); + } + + $this->attributes['answers'][] = new PollAnswer($this->discord, [ + 'poll_media' => $answer, + ]); + + return $this; + } + + /** + * Set the duration of the poll. + * + * @param int $duration Number of hours the poll should be open for, up to 32 days. Defaults to 24 + * + * @throws \OutOfRangeException + * + * @return $this + */ + public function setDuration(int $duration): self + { + if ($duration < 1 || $duration > 32 * 24) { + throw new \OutOfRangeException('Duration must be between 1 and 32 days.'); + } + + $this->attributes['duration'] = $duration; + + return $this; + } + + /** + * Determine whether a user can select multiple answers. + * + * @param bool $multiselect Whether a user can select multiple answers. + * + * @return $this + */ + public function setAllowMultiselect(bool $multiselect): self + { + $this->attributes['allow_multiselect'] = $multiselect; + + return $this; + } + + /** + * Set the layout type of the poll. + * + * @param int $type The layout type of the poll. + * + * @return $this + */ + protected function setLayoutType(int $type): self + { + $this->attributes['layout_type'] = $type; + + return $this; + } +} diff --git a/src/Discord/Parts/Channel/Poll/PollAnswer.php b/src/Discord/Parts/Channel/Poll/PollAnswer.php new file mode 100644 index 000000000..5f0eb863d --- /dev/null +++ b/src/Discord/Parts/Channel/Poll/PollAnswer.php @@ -0,0 +1,180 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Parts\Channel\Poll; + +use Discord\Helpers\Collection; +use Discord\Http\Endpoint; +use Discord\Parts\Channel\Channel; +use Discord\Parts\Channel\Message; +use Discord\Parts\Guild\Guild; +use Discord\Parts\Part; +use Discord\Parts\Thread\Thread; +use Discord\Parts\User\User; +use React\Promise\ExtendedPromiseInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +use function Discord\normalizePartId; + +/** + * An answer to a poll. + * + * @link https://discord.com/developers/docs/resources/poll#poll-answer-object + * + * @since 10.0.0 + * + * @property int $answer_id The ID of the answer. Only sent as part of responses from Discord's API/Gateway. + * @property PollMedia $poll_media The data of the answer + * + * @property string $user_id The user ID that voted for the answer. + * @property-read User $user The user that voted for the answer. + * @property string $channel_id The channel ID that the poll belongs to. + * @property-read Channel|Thread $channel The channel that the poll belongs to. + * @property string $message_id The message ID that the poll belongs to. + * @property Message|null $message The message the poll belongs to. + * @property string|null $guild_id The guild ID of the guild that owns the channel the poll message is in. + * @property-read Guild|null $guild The guild that owns the channel the poll belongs in. + */ +class PollAnswer extends Part +{ + /** + * {@inheritdoc} + */ + protected $fillable = [ + 'answer_id', + 'poll_media', + + // events + 'user_id', + 'channel_id', + 'message_id', + 'guild_id', + ]; + + /** + * Gets the user attribute. + * + * @return User + */ + protected function getUserAttribute(): User + { + return $this->discord->users->get('id', $this->user_id); + } + + /** + * Gets the channel attribute. + * + * @return Channel|Thread + */ + protected function getChannelAttribute(): Part + { + if ($guild = $this->guild) { + $channels = $guild->channels; + + if ($channel = $channels->get('id', $this->channel_id)) { + return $channel; + } + + foreach ($channels as $parent) { + if ($thread = $parent->threads->get('id', $this->channel_id)) { + return $thread; + } + } + } + + // @todo potentially slow + if ($channel = $this->discord->getChannel($this->channel_id)) { + return $channel; + } + + return $this->factory->part(Channel::class, [ + 'id' => $this->channel_id, + 'type' => Channel::TYPE_DM, + ], true); + } + + /** + * Gets the message attribute. + * + * @return Message|null + */ + protected function getMessageAttribute(): ?Message + { + if ($channel = $this->channel) { + return $channel->messages->get('id', $this->message_id); + } + + return $this->attributes['message'] ?? null; + } + + /** + * Returns the guild that owns the channel the message was sent in. + * + * @return Guild|null + */ + protected function getGuildAttribute(): ?Guild + { + if (! isset($this->guild_id)) { + return null; + } + + return $this->discord->guilds->get('id', $this->guild_id); + } + + /** + * Returns the users that voted for the answer. + * + * @param array $options An array of options. All fields are optional. + * @param string|null $options['after'] Get users after this user ID. + * @param int|null $options['limit'] Max number of users to return (1-100). + * + * @link https://discord.com/developers/docs/resources/poll#get-answer-voters + * + * @throws \OutOfRangeException + * + * @return ExtendedPromiseInterface + */ + public function getVoters(array $options = []): ExtendedPromiseInterface + { + $query = Endpoint::bind(Endpoint::MESSAGE_POLL_ANSWER, $this->channel_id, $this->message_id, $this->answer_id); + + $resolver = new OptionsResolver(); + $resolver + ->setDefined(['after', 'limit']) + ->setAllowedTypes('after', ['int', 'string', User::class]) + ->setAllowedTypes('limit', 'int') + ->setNormalizer('after', normalizePartId()) + ->setAllowedValues('limit', fn ($value) => ($value >= 1 && $value <= 100)); + + $options = $resolver->resolve($options); + + foreach ($options as $key => $value) { + $query->addQuery($key, $value); + } + + return $this->http->get($query) + ->then(function ($response) { + $users = Collection::for(User::class); + + foreach ($response->users ?? [] as $user) { + if (! $part = $this->discord->users->get('id', $user->id)) { + $part = $this->discord->users->create($user, true); + + $this->discord->users->pushItem($part); + } + + $users->pushItem($part); + } + + return $users; + }); + } +} diff --git a/src/Discord/Parts/Channel/Poll/PollAnswerCount.php b/src/Discord/Parts/Channel/Poll/PollAnswerCount.php new file mode 100644 index 000000000..18e1558f0 --- /dev/null +++ b/src/Discord/Parts/Channel/Poll/PollAnswerCount.php @@ -0,0 +1,37 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Parts\Channel\Poll; + +use Discord\Parts\Part; + +/** + * The number of votes for an answer in a poll. + * + * @link https://discord.com/developers/docs/resources/poll#poll-results-object-poll-answer-count-object-structure + * + * @since 10.0.0 + * + * @property int $id The answer_id + * @property int $count The number of votes for this answer + * @property bool $me_voted Whether the current user voted for this answer + */ +class PollAnswerCount extends Part +{ + /** + * {@inheritdoc} + */ + protected $fillable = [ + 'id', + 'count', + 'me_voted', + ]; +} diff --git a/src/Discord/Parts/Channel/Poll/PollMedia.php b/src/Discord/Parts/Channel/Poll/PollMedia.php new file mode 100644 index 000000000..fa8e59662 --- /dev/null +++ b/src/Discord/Parts/Channel/Poll/PollMedia.php @@ -0,0 +1,96 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Parts\Channel\Poll; + +use Discord\Parts\Part; +use Discord\Parts\Guild\Emoji; + +/** + * The poll media object is a common object that backs both the question and answers. + * + * @link https://discord.com/developers/docs/resources/poll#poll-media-object + * + * @since 10.0.0 + * + * @property string|null $text The text of the field. Text should always be non-null for both questions and answers, but please do not depend on that in the future. The maximum length of text is 300 for the question, and 55 for any answer. + * @property Emoji|string|null $emoji The emoji of the field. When creating a poll answer with an emoji, one only needs to send either the id (custom emoji) or name (default emoji) as the only field. + */ +class PollMedia extends Part +{ + /** + * {@inheritdoc} + */ + protected $fillable = [ + 'text', + 'emoji', + ]; + + /** + * Sets the text of the poll media. + * + * @param string|null $text Text of the button. Maximum 300 characters for the question, and 55 for any answer. + * + * @throws \LengthException + * + * @return $this + */ + public function setText(?string $text): self + { + $this->text = $text; + + return $this; + } + + /** + * Sets the emoji of the poll media. + * + * @param Emoji|string|null $emoji Emoji to set. `null` to clear. + * + * @return $this + */ + public function setEmoji($emoji): self + { + $this->emoji = (function () use ($emoji) { + if ($emoji === null) { + return null; + } + + if ($emoji instanceof Emoji) { + return [ + 'id' => $emoji->id, + 'name' => $emoji->name, + 'animated' => $emoji->animated, + ]; + } + + $parts = explode(':', $emoji, 3); + + if (count($parts) < 3) { + return [ + 'id' => null, + 'name' => $emoji, + 'animated' => false, + ]; + } + + [$animated, $name, $id] = $parts; + + return [ + 'id' => $id, + 'name' => $name, + 'animated' => $animated == 'a', + ]; + })(); + + return $this; + } +} diff --git a/src/Discord/Parts/Channel/Poll/PollResults.php b/src/Discord/Parts/Channel/Poll/PollResults.php new file mode 100644 index 000000000..ed6be7650 --- /dev/null +++ b/src/Discord/Parts/Channel/Poll/PollResults.php @@ -0,0 +1,45 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Parts\Channel\Poll; + +use Discord\Parts\Part; + +/** + * The current results of a poll. + * + * @link https://discord.com/developers/docs/resources/poll#poll-results-object + * + * @since 10.0.0 + * + * @property boolean $is_finalized Whether the votes have been precisely counted + * @property PollAnswerCount[] $answer_counts The counts for each answer + */ +class PollResults extends Part +{ + /** + * {@inheritdoc} + */ + protected $fillable = [ + 'is_finalized', + 'answer_counts', + ]; + + /** + * Returns the answer counts attribute. + * + * @return PollAnswerCount + */ + protected function getAnswerCountsAttribute(): PollAnswerCount + { + return $this->factory->part(PollAnswerCount::class, (array) $this->attributes['answer_counts'], true); + } +} diff --git a/src/Discord/Parts/Permissions/ChannelPermission.php b/src/Discord/Parts/Permissions/ChannelPermission.php index 1f7ce3bb3..2d715ebf0 100644 --- a/src/Discord/Parts/Permissions/ChannelPermission.php +++ b/src/Discord/Parts/Permissions/ChannelPermission.php @@ -34,6 +34,8 @@ * @property bool $use_soundboard Allows for using soundboard in a voice channel * @property bool $create_events Allows for creating scheduled events * @property bool $use_external_sounds Allows the usage of custom soundboard sounds from other servers + * @property bool $send_voice_messages Allows sending voice messages + * @property bool $send_polls Allows sending polls */ class ChannelPermission extends Permission { diff --git a/src/Discord/Parts/Permissions/Permission.php b/src/Discord/Parts/Permissions/Permission.php index d1cf52d46..28a2de349 100644 --- a/src/Discord/Parts/Permissions/Permission.php +++ b/src/Discord/Parts/Permissions/Permission.php @@ -85,6 +85,8 @@ abstract class Permission extends Part 'use_soundboard' => 42, 'create_events' => 44, 'use_external_sounds' => 45, + 'send_voice_messages' => 46, + 'send_polls' => 49, ]; /** diff --git a/src/Discord/Parts/Permissions/RolePermission.php b/src/Discord/Parts/Permissions/RolePermission.php index 376fe8411..0e0aaf3fb 100644 --- a/src/Discord/Parts/Permissions/RolePermission.php +++ b/src/Discord/Parts/Permissions/RolePermission.php @@ -46,6 +46,8 @@ * @property bool $use_soundboard * @property bool $create_events * @property bool $use_external_sounds + * @property bool $send_voice_messages + * @property bool $send_polls */ class RolePermission extends Permission { diff --git a/src/Discord/Repository/Channel/PollAnswerRepository.php b/src/Discord/Repository/Channel/PollAnswerRepository.php new file mode 100644 index 000000000..782ac5535 --- /dev/null +++ b/src/Discord/Repository/Channel/PollAnswerRepository.php @@ -0,0 +1,47 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Repository\Channel; + +use Discord\Http\Endpoint; +use Discord\Parts\Channel\Poll\PollAnswer; +use Discord\Repository\AbstractRepository; +use React\Promise\ExtendedPromiseInterface; + +/** + * Contains poll answers on a poll in a message. + * + * @see PollAnswer + * @see \Discord\Parts\Channel\Poll + * @see \Discord\Parts\Channel\Message + * + * @since 10.0.0 + * + * @method PollAnswer|null get(string $discrim, $key) + * @method PollAnswer|null pull(string|int $key, $default = null) + * @method PollAnswer|null first() + * @method PollAnswer|null last() + * @method PollAnswer|null find(callable $callback) + */ +class PollAnswerRepository extends AbstractRepository +{ + /** + * {@inheritDoc} + */ + protected $endpoints = [ + 'get' => Endpoint::MESSAGE_POLL_ANSWER, + ]; + + /** + * {@inheritDoc} + */ + protected $class = PollAnswer::class; +} diff --git a/src/Discord/WebSockets/Event.php b/src/Discord/WebSockets/Event.php index a96674dd9..05338b16c 100644 --- a/src/Discord/WebSockets/Event.php +++ b/src/Discord/WebSockets/Event.php @@ -112,6 +112,8 @@ abstract class Event public const MESSAGE_REACTION_REMOVE = 'MESSAGE_REACTION_REMOVE'; public const MESSAGE_REACTION_REMOVE_ALL = 'MESSAGE_REACTION_REMOVE_ALL'; public const MESSAGE_REACTION_REMOVE_EMOJI = 'MESSAGE_REACTION_REMOVE_EMOJI'; + public const MESSAGE_POLL_VOTE_ADD = 'MESSAGE_POLL_VOTE_ADD'; + public const MESSAGE_POLL_VOTE_REMOVE = 'MESSAGE_POLL_VOTE_REMOVE'; /** * The Discord client instance. diff --git a/src/Discord/WebSockets/Events/MessagePollVoteAdd.php b/src/Discord/WebSockets/Events/MessagePollVoteAdd.php new file mode 100644 index 000000000..803663d7f --- /dev/null +++ b/src/Discord/WebSockets/Events/MessagePollVoteAdd.php @@ -0,0 +1,68 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\WebSockets\Events; + +use Discord\Parts\Channel\Poll\PollAnswer; +use Discord\WebSockets\Event; +use Discord\Parts\Channel\Channel; +use Discord\Parts\Channel\Message; +use Discord\Parts\Channel\Reaction; +use Discord\Parts\Guild\Guild; +use Discord\Parts\Thread\Thread; + +/** + * @link https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-add-message-poll-vote-add-fields + * + * @since 10.0.0 + */ +class MessagePollVoteAdd extends Event +{ + /** + * {@inheritDoc} + */ + public function handle($data) + { + $guild = null; + + /** @var ?Guild */ + if (isset($data->guild_id) && $guild = yield $this->discord->guilds->cacheGet($data->guild_id)) { + /** @var ?Channel */ + if (! $channel = yield $guild->channels->cacheGet($data->channel_id)) { + /** @var Channel */ + foreach ($guild->channels as $channel) { + /** @var ?Thread */ + if ($thread = yield $channel->threads->cacheGet($data->channel_id)) { + $channel = $thread; + break; + } + } + } + } else { + /** @var ?Channel */ + $channel = yield $this->discord->private_channels->cacheGet($data->channel_id); + } + + $answer = new PollAnswer($this->discord, (array) $data, true); + + /** @var ?Message */ + if (isset($channel) && $message = yield $channel->messages->cacheGet($data->message_id)) { + $message->poll->answers->pushItem($answer); + } + + if (isset($data->member) && $guild) { + $this->cacheMember($guild->members, (array) $data->member); + $this->cacheUser($data->member->user); + } + + return $answer; + } +} diff --git a/src/Discord/WebSockets/Events/MessagePollVoteRemove.php b/src/Discord/WebSockets/Events/MessagePollVoteRemove.php new file mode 100644 index 000000000..c0b690c8f --- /dev/null +++ b/src/Discord/WebSockets/Events/MessagePollVoteRemove.php @@ -0,0 +1,61 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\WebSockets\Events; + +use Discord\Parts\Channel\Poll\PollAnswer; +use Discord\WebSockets\Event; +use Discord\Parts\Channel\Channel; +use Discord\Parts\Channel\Message; +use Discord\Parts\Channel\Reaction; +use Discord\Parts\Guild\Guild; +use Discord\Parts\Thread\Thread; + +/** + * @link https://discord.com/developers/docs/topics/gateway-events#message-poll-vote-remove-message-poll-vote-remove-fields + * + * @since 10.0.0 + */ +class MessagePollVoteRemove extends Event +{ + /** + * {@inheritDoc} + */ + public function handle($data) + { + /** @var ?Guild */ + if (isset($data->guild_id) && $guild = yield $this->discord->guilds->cacheGet($data->guild_id)) { + /** @var ?Channel */ + if (! $channel = yield $guild->channels->cacheGet($data->channel_id)) { + /** @var Channel */ + foreach ($guild->channels as $channel) { + /** @var ?Thread */ + if ($thread = yield $channel->threads->cacheGet($data->channel_id)) { + $channel = $thread; + break; + } + } + } + } else { + /** @var ?Channel */ + $channel = yield $this->discord->private_channels->cacheGet($data->channel_id); + } + + $answer = new PollAnswer($this->discord, (array) $data, true); + + /** @var ?Message */ + if (isset($channel) && $message = yield $channel->messages->cacheGet($data->message_id)) { + yield $message->poll->answers->cache->delete($answer->answer_id); + } + + return $answer; + } +} diff --git a/src/Discord/WebSockets/Handlers.php b/src/Discord/WebSockets/Handlers.php index b17c8f5e2..26b994ddb 100644 --- a/src/Discord/WebSockets/Handlers.php +++ b/src/Discord/WebSockets/Handlers.php @@ -79,6 +79,8 @@ public function __construct() $this->addHandler(Event::MESSAGE_REACTION_REMOVE, \Discord\WebSockets\Events\MessageReactionRemove::class); $this->addHandler(Event::MESSAGE_REACTION_REMOVE_ALL, \Discord\WebSockets\Events\MessageReactionRemoveAll::class); $this->addHandler(Event::MESSAGE_REACTION_REMOVE_EMOJI, \Discord\WebSockets\Events\MessageReactionRemoveEmoji::class); + $this->addHandler(Event::MESSAGE_POLL_VOTE_ADD, \Discord\WebSockets\Events\MessagePollVoteAdd::class); + $this->addHandler(Event::MESSAGE_POLL_VOTE_REMOVE, \Discord\WebSockets\Events\MessagePollVoteRemove::class); // New Member Event handlers $this->addHandler(Event::GUILD_MEMBER_ADD, \Discord\WebSockets\Events\GuildMemberAdd::class); diff --git a/src/Discord/WebSockets/Intents.php b/src/Discord/WebSockets/Intents.php index 9b766e0d7..8ebb3fdb4 100644 --- a/src/Discord/WebSockets/Intents.php +++ b/src/Discord/WebSockets/Intents.php @@ -195,6 +195,22 @@ class Intents */ public const AUTO_MODERATION_EXECUTION = (1 << 21); + /** + * Guild message poll events. + * + * - MESSAGE_POLL_VOTE_ADD + * - MESSAGE_POLL_VOTE_REMOVE + */ + public const GUILD_MESSAGE_POLLS = (1 << 24); + + /** + * Direct message poll events. + * + * - MESSAGE_POLL_VOTE_ADD + * - MESSAGE_POLL_VOTE_REMOVE + */ + public const DIRECT_MESSAGE_POLLS = (1 << 25); + /** * Returns an array of valid intents. *