Repository: hikari-py/hikari
Pull request #1609 (2023-05-03 12:39:53)

Implement voice messages


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d393606..add1bf54 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+## 2.0.0.dev119 (2023-05-08)
+
+### Breaking Changes
+
+- Remove deprecated functionality for 2.0.0.dev119
+  - Removed `TextInputBuilder.required` in favour of `TextInputBuilder.is_required`.
+  - Removed the ability to pass `CommandChoices` instead of `AutocompleteChoiceBuilders` when making autocomplete responses. ([#1580](https://github.com/hikari-py/hikari/issues/1580))
+
+### Bugfixes
+
+- Fix `messages` argument typing for `RESTClient.delete_messages`. ([#1581](https://github.com/hikari-py/hikari/issues/1581))
+- Default `HTTPSettings.enable_cleanup_closed` to `False`.
+  - CPython3.11 changes around SSLProto have made this quite unstable and prone to errors when dealing with unclosed TLS transports, which ends up in aiohttp calling close and abort twice. ([#1585](https://github.com/hikari-py/hikari/issues/1585))
+- `Guild.get_channel`, `Guild.get_emoji`, `Guild.get_sticker` and `Guild.get_role` now only return entries from the relevant guild. ([#1608](https://github.com/hikari-py/hikari/issues/1608))
+
+---
+
+
 ## 2.0.0.dev118 (2023-04-02)
 
 ### Breaking Changes
diff --git a/changes/1580.breaking.md b/changes/1580.breaking.md
deleted file mode 100644
index d1391854..00000000
--- a/changes/1580.breaking.md
+++ /dev/null
@@ -1,3 +0,0 @@
-Remove deprecated functionality for 2.0.0.dev119
-- Removed `TextInputBuilder.required` in favour of `TextInputBuilder.is_required`.
-- Removed the ability to pass `CommandChoices` instead of `AutocompleteChoiceBuilders` when making autocomplete responses.
diff --git a/changes/1581.bugfix.md b/changes/1581.bugfix.md
deleted file mode 100644
index d9006db3..00000000
--- a/changes/1581.bugfix.md
+++ /dev/null
@@ -1,1 +0,0 @@
-Fix `messages` argument typing for `RESTClient.delete_messages`.
diff --git a/changes/1585.bugfix.md b/changes/1585.bugfix.md
deleted file mode 100644
index 428176b3..00000000
--- a/changes/1585.bugfix.md
+++ /dev/null
@@ -1,2 +0,0 @@
-Default `HTTPSettings.enable_cleanup_closed` to `False`.
-- CPython3.11 changes around SSLProto have made this quite unstable and prone to errors when dealing with unclosed TLS transports, which ends up in aiohttp calling close and abort twice.
diff --git a/changes/1608.bugfix.md b/changes/1608.bugfix.md
deleted file mode 100644
index 11faecb4..00000000
--- a/changes/1608.bugfix.md
+++ /dev/null
@@ -1,1 +0,0 @@
-`Guild.get_channel`, `Guild.get_emoji`, `Guild.get_sticker` and `Guild.get_role` now only return entries from the relevant guild.
diff --git a/changes/1609.feature.md b/changes/1609.feature.md
new file mode 100644
index 00000000..f58648ce
--- /dev/null
+++ b/changes/1609.feature.md
@@ -0,0 +1,1 @@
+Implement voice messages
diff --git a/changes/1612.breaking.md b/changes/1612.breaking.md
new file mode 100644
index 00000000..46e3b295
--- /dev/null
+++ b/changes/1612.breaking.md
@@ -0,0 +1,1 @@
+Remove previously deprecated `hikari.impl.bot` module
diff --git a/changes/1627.bugfix.md b/changes/1627.bugfix.md
new file mode 100644
index 00000000..6520ff8b
--- /dev/null
+++ b/changes/1627.bugfix.md
@@ -0,0 +1,1 @@
+Fix a bug in `RESTClient.edit_guild` which load to closed stream errors
diff --git a/dev-requirements/coverage.txt b/dev-requirements/coverage.txt
index d92b75a6..856380d3 100644
--- a/dev-requirements/coverage.txt
+++ b/dev-requirements/coverage.txt
@@ -1,1 +1,1 @@
-coverage[toml]==7.2.5
+coverage[toml]==7.2.7
diff --git a/dev-requirements/mypy.txt b/dev-requirements/mypy.txt
index 204ac546..a45e2c90 100644
--- a/dev-requirements/mypy.txt
+++ b/dev-requirements/mypy.txt
@@ -1,1 +1,1 @@
-mypy==1.2.0
+mypy==1.3.0
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index e84b01d3..b491675a 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.306
+pyright==1.1.311
diff --git a/dev-requirements/sphinx.txt b/dev-requirements/sphinx.txt
index 3ae88af7..b69cad77 100644
--- a/dev-requirements/sphinx.txt
+++ b/dev-requirements/sphinx.txt
@@ -1,7 +1,7 @@
 sphinx==6.2.1
 
 # Theme
-furo==2023.3.27
+furo==2023.5.20
 
 # Autodocumentation
 sphinx-autoapi==2.1.0
diff --git a/hikari/_about.py b/hikari/_about.py
index 9aa4ecd3..f774bec2 100644
--- a/hikari/_about.py
+++ b/hikari/_about.py
@@ -39,5 +39,5 @@
 __issue_tracker__: typing.Final[str] = "https://github.com/hikari-py/hikari/issues"
 __license__: typing.Final[str] = "MIT"
 __url__: typing.Final[str] = "https://github.com/hikari-py/hikari"
-__version__: typing.Final[str] = "2.0.0.dev119"
+__version__: typing.Final[str] = "2.0.0.dev120"
 __git_sha1__: typing.Final[str] = "HEAD"
diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py
deleted file mode 100644
index 69e77b2e..00000000
--- a/hikari/impl/bot.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# -*- coding: utf-8 -*-
-# cython: language_level=3
-# Copyright (c) 2020 Nekokatt
-# Copyright (c) 2021-present davfsa
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-"""Deprecated alias for `hikari.impl.gateway_bot`."""
-
-from hikari.internal import deprecation as __deprecation
-
-__deprecation.warn_deprecated(
-    "'hikari.impl.bot' module",
-    removal_version="2.0.0.dev120",
-    additional_info="The module was renamed to 'hikari.impl.gateway_bot'",
-    quote=False,
-)
-
-from hikari.impl.gateway_bot import *  # noqa: F403, F401
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 32e47625..48d457d3 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -2946,6 +2946,8 @@ def _deserialize_message_attachment(self, payload: data_binding.JSONObject) -> m
             height=payload.get("height"),
             width=payload.get("width"),
             is_ephemeral=payload.get("ephemeral", False),
+            duration=payload.get("duration_secs"),
+            waveform=payload.get("waveform"),
         )
 
     def _deserialize_message_reaction(self, payload: data_binding.JSONObject) -> message_models.Reaction:
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 0df1fee5..77030bdc 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -2537,36 +2537,41 @@ async def edit_guild(
         body.put_snowflake("rules_channel_id", rules_channel)
         body.put_snowflake("public_updates_channel_id", public_updates_channel)
 
+        stack = contextlib.AsyncExitStack()
         tasks: typing.List[asyncio.Task[str]] = []
 
-        if icon is None:
-            body.put("icon", None)
-        elif icon is not undefined.UNDEFINED:
-            icon_resource = files.ensure_resource(icon)
-            async with icon_resource.stream(executor=self._executor) as stream:
+        async with stack:
+            if icon is None:
+                body.put("icon", None)
+            elif icon is not undefined.UNDEFINED:
+                icon_resource = files.ensure_resource(icon)
+                stream = await stack.enter_async_context(icon_resource.stream(executor=self._executor))
+
                 task = asyncio.create_task(stream.data_uri())
                 task.add_done_callback(lambda future: body.put("icon", future.result()))
                 tasks.append(task)
 
-        if splash is None:
-            body.put("splash", None)
-        elif splash is not undefined.UNDEFINED:
-            splash_resource = files.ensure_resource(splash)
-            async with splash_resource.stream(executor=self._executor) as stream:
+            if splash is None:
+                body.put("splash", None)
+            elif splash is not undefined.UNDEFINED:
+                splash_resource = files.ensure_resource(splash)
+                stream = await stack.enter_async_context(splash_resource.stream(executor=self._executor))
+
                 task = asyncio.create_task(stream.data_uri())
                 task.add_done_callback(lambda future: body.put("splash", future.result()))
                 tasks.append(task)
 
-        if banner is None:
-            body.put("banner", None)
-        elif banner is not undefined.UNDEFINED:
-            banner_resource = files.ensure_resource(banner)
-            async with banner_resource.stream(executor=self._executor) as stream:
+            if banner is None:
+                body.put("banner", None)
+            elif banner is not undefined.UNDEFINED:
+                banner_resource = files.ensure_resource(banner)
+                stream = await stack.enter_async_context(banner_resource.stream(executor=self._executor))
+
                 task = asyncio.create_task(stream.data_uri())
                 task.add_done_callback(lambda future: body.put("banner", future.result()))
                 tasks.append(task)
 
-        await asyncio.gather(*tasks)
+            await asyncio.gather(*tasks)
 
         response = await self._request(route, json=body, reason=reason)
         assert isinstance(response, dict)
diff --git a/hikari/messages.py b/hikari/messages.py
index 5bf55a12..e191a4e4 100644
--- a/hikari/messages.py
+++ b/hikari/messages.py
@@ -171,6 +171,9 @@ class MessageFlag(enums.Flag):
     SUPPRESS_NOTIFICATIONS = 1 << 12
     """This message will not trigger push and desktop notifications."""
 
+    IS_VOICE_MESSAGE = 1 << 13
+    """This message is a voice message."""
+
 
 @typing.final
 class MessageActivityType(int, enums.Enum):
@@ -235,6 +238,12 @@ class Attachment(snowflakes.Unique, files.WebResource):
     time (but will exist as long as their relevant message exists).
     """
 
+    duration: typing.Optional[float] = attrs.field(hash=False, eq=False, repr=False)
+    """The duration (in seconds) of the voice message."""
+
+    waveform: typing.Optional[str] = attrs.field(hash=False, eq=False, repr=False)
+    """A base64 encoded representation of the sampled waveform for the voice message."""
+
     def __str__(self) -> str:
         return self.filename
 
diff --git a/hikari/permissions.py b/hikari/permissions.py
index 480fb55a..c6d1aa46 100644
--- a/hikari/permissions.py
+++ b/hikari/permissions.py
@@ -278,6 +278,15 @@ class Permissions(enums.Flag):
     MODERATE_MEMBERS = 1 << 40
     """Allows for timing out members."""
 
+    VIEW_CREATOR_MONETIZATION_ANALYTICS = 1 << 41
+    """Allows for viewing role subscription insights"""
+
+    USE_SOUNDBOARD = 1 << 42
+    """Allows the use of soundboard in a voice chat."""
+
+    SEND_VOICE_MESSAGES = 1 << 46
+    """Allows sending voice messages."""
+
     @classmethod
     def all_permissions(cls) -> Permissions:
         """Get an instance of `Permissions` with all the known permissions.
diff --git a/speedup-requirements.txt b/speedup-requirements.txt
index be31fc8a..1084ff45 100644
--- a/speedup-requirements.txt
+++ b/speedup-requirements.txt
@@ -1,3 +1,3 @@
 aiohttp[speedups]~=3.8
 ciso8601~=2.3
-orjson~=3.8
+orjson~=3.9


Test patch:
diff --git a/dev-requirements/pytest.txt b/dev-requirements/pytest.txt
index 07c610dc..8ff6f832 100644
--- a/dev-requirements/pytest.txt
+++ b/dev-requirements/pytest.txt
@@ -3,7 +3,7 @@ mock==5.0.2
 
 pytest==7.3.1
 pytest-asyncio==0.21.0
-pytest-cov==4.0.0
+pytest-cov==4.1.0
 pytest-randomly==3.12.0
 
 async-timeout==4.0.2  # Used for timeouts in some test cases.
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 2c1e0214..468a79f9 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -5537,6 +5537,8 @@ def attachment_payload(self):
             "width": 1844,
             "height": 2638,
             "ephemeral": True,
+            "duration_secs": 1000.123,
+            "waveform": "some encoded string",
         }
 
     @pytest.fixture()
@@ -5604,6 +5606,8 @@ def test__deserialize_message_attachment(self, entity_factory_impl, attachment_p
         assert attachment.width == 1844
         assert attachment.height == 2638
         assert attachment.is_ephemeral is True
+        assert attachment.duration == 1000.123
+        assert attachment.waveform == "some encoded string"
         assert isinstance(attachment, message_models.Attachment)
 
     def test__deserialize_message_attachment_with_null_fields(self, entity_factory_impl, attachment_payload):
@@ -5621,6 +5625,8 @@ def test__deserialize_message_attachment_with_unset_fields(self, entity_factory_
         del attachment_payload["height"]
         del attachment_payload["width"]
         del attachment_payload["ephemeral"]
+        del attachment_payload["duration_secs"]
+        del attachment_payload["waveform"]
 
         attachment = entity_factory_impl._deserialize_message_attachment(attachment_payload)
 
@@ -5628,6 +5634,8 @@ def test__deserialize_message_attachment_with_unset_fields(self, entity_factory_
         assert attachment.height is None
         assert attachment.width is None
         assert attachment.is_ephemeral is False
+        assert attachment.duration is None
+        assert attachment.waveform is None
 
     def test_deserialize_partial_message(
         self,
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 17a6b305..972251b6 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -430,16 +430,21 @@ def rest_client(rest_client_class, mock_cache):
 def file_resource():
     class Stream:
         def __init__(self, data):
+            self.open = False
             self.data = data
 
         async def data_uri(self):
+            if not self.open:
+                raise RuntimeError("Tried to read off a closed stream")
+
             return self.data
 
         async def __aenter__(self):
+            self.open = True
             return self
 
         async def __aexit__(self, exc_type, exc, exc_tb) -> None:
-            pass
+            self.open = False
 
     class FileResource(files.Resource):
         filename = None
diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py
index f96f569b..881d8a18 100644
--- a/tests/hikari/test_messages.py
+++ b/tests/hikari/test_messages.py
@@ -46,6 +46,8 @@ def test_str_operator(self):
             size=543,
             url="htttt",
             is_ephemeral=False,
+            duration=1,
+            waveform="122111",
         )
         assert str(attachment) == "super_cool_file.cool"
 
diff --git a/tests/hikari/test_permissions.py b/tests/hikari/test_permissions.py
index fd96424b..c119ddd5 100644
--- a/tests/hikari/test_permissions.py
+++ b/tests/hikari/test_permissions.py
@@ -27,4 +27,4 @@ def test_all_permissions(self):
         all_perms = permissions.Permissions.all_permissions()
 
         assert isinstance(all_perms, permissions.Permissions)
-        assert all_perms == 2199023255551
+        assert all_perms == 79164837199871