├── .github └── workflows │ ├── build.yml │ ├── release.yml │ └── verify-types.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin ├── ffmpeg └── ffmpeg.exe ├── build.py ├── docs ├── readme.md ├── rest │ └── search.md └── websocket │ ├── payloads.md │ └── protocol.md ├── launcher.py ├── logo.png ├── native_voice ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── discord │ └── ext │ │ └── native_voice │ │ └── __init__.py ├── pyproject.toml ├── setup.py └── src │ ├── error.rs │ ├── lib.rs │ ├── payloads.rs │ ├── player.rs │ ├── protocol.rs │ └── state.rs ├── pyproject.toml ├── swish.toml ├── swish ├── __init__.py ├── app.py ├── config.py ├── logging.py ├── player.py ├── py.typed ├── rotator.py ├── types │ └── payloads.py └── utilities.py └── tests └── bot.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | 11 | jobs: 12 | Build: 13 | 14 | strategy: 15 | fail-fast: true 16 | matrix: 17 | os: 18 | - ubuntu-latest 19 | - windows-latest 20 | - macos-latest 21 | python-version: 22 | - "3.10" 23 | 24 | name: "Python v${{ matrix.python-version }} @ ${{ matrix.os }}" 25 | runs-on: ${{ matrix.os }} 26 | 27 | steps: 28 | - name: "Initialise environment" 29 | uses: actions/checkout@v3 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: "Setup Python v${{ matrix.python-version }}" 34 | uses: actions/setup-python@v4 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | 38 | - if: matrix.os == 'ubuntu-latest' 39 | name: "Install libopus-dev" 40 | run: | 41 | sudo apt update 42 | sudo apt install -y libopus-dev 43 | 44 | - name: "Install dependencies" 45 | run: | 46 | pip install .[build] 47 | pip install ./native_voice 48 | 49 | - name: "Build swish" 50 | run: python build.py --no-deps 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: build-release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | Windows: 9 | runs-on: windows-latest 10 | 11 | steps: 12 | - name: Pull source 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.x 19 | 20 | - name: Install Deps 21 | run: | 22 | python -m ensurepip 23 | pip install .[build] 24 | 25 | - name: Build swish 26 | run: | 27 | python build.py --no-deps 28 | 29 | - name: Upload binaries to release 30 | uses: svenstaro/upload-release-action@v2 31 | with: 32 | repo_token: ${{ secrets.GITHUB_TOKEN }} 33 | file: ./dist/swish.exe 34 | asset_name: swish-windows_x86-64.exe 35 | tag: ${{ github.ref }} 36 | 37 | 38 | Ubuntu: 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | - name: Pull source 43 | uses: actions/checkout@v2 44 | 45 | - name: Setup Python 46 | uses: actions/setup-python@v2 47 | with: 48 | python-version: 3.x 49 | 50 | - name: Install Deps 51 | run: | 52 | python -m ensurepip 53 | pip install .[build] 54 | 55 | - name: Build swish 56 | run: | 57 | python build.py --no-deps 58 | 59 | - name: Upload binaries to release 60 | uses: svenstaro/upload-release-action@v2 61 | with: 62 | repo_token: ${{ secrets.GITHUB_TOKEN }} 63 | file: ./dist/swish-linux 64 | asset_name: swish-linux_x86-64 65 | tag: ${{ github.ref }} 66 | 67 | 68 | MacOS: 69 | runs-on: macos-latest 70 | 71 | steps: 72 | - name: Pull source 73 | uses: actions/checkout@v2 74 | 75 | - name: Setup Python 76 | uses: actions/setup-python@v2 77 | with: 78 | python-version: 3.x 79 | 80 | - name: Install Deps 81 | run: | 82 | python -m ensurepip 83 | pip install .[build] 84 | 85 | - name: Build swish 86 | run: | 87 | python build.py --no-deps 88 | 89 | - name: Upload binaries to release 90 | uses: svenstaro/upload-release-action@v2 91 | with: 92 | repo_token: ${{ secrets.GITHUB_TOKEN }} 93 | file: ./dist/swish 94 | asset_name: swish-macOS_x86-64 95 | tag: ${{ github.ref }} 96 | -------------------------------------------------------------------------------- /.github/workflows/verify-types.yml: -------------------------------------------------------------------------------- 1 | name: Verify Types 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | 11 | jobs: 12 | Verify-Types: 13 | 14 | strategy: 15 | fail-fast: true 16 | matrix: 17 | python-version: 18 | - "3.10" 19 | 20 | name: "Python v${{ matrix.python-version }}" 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: "Initialise environment" 25 | uses: actions/checkout@v3 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: "Setup Python v${{ matrix.python-version }}" 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | 34 | - name: "Install dependencies" 35 | run: pip install .[build] 36 | 37 | - name: "Setup Node v16" 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 16 41 | 42 | - name: "Install pyright" 43 | run: npm install --location=global pyright 44 | 45 | - name: "Run pyright" 46 | run: pyright 47 | 48 | - name: "Verify Types" 49 | run: pyright --ignoreexternal --lib --verifytypes swish 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .eggs/ 3 | 4 | *.egg-info/ 5 | 6 | logs/ 7 | build/ 8 | dist/ 9 | target/ 10 | venv/ 11 | 12 | *.pyc 13 | *.spec 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU Affero General Public License 2 | ================================= 3 | 4 | _Version 3, 19 November 2007_ 5 | _Copyright © 2007 Free Software Foundation, Inc. <>_ 6 | 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | ## Preamble 11 | 12 | The GNU Affero General Public License is a free, copyleft license for 13 | software and other kinds of works, specifically designed to ensure 14 | cooperation with the community in the case of network server software. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | our General Public Licenses are intended to guarantee your freedom to 19 | share and change all versions of a program--to make sure it remains free 20 | software for all its users. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | Developers that use our General Public Licenses protect your rights 30 | with two steps: **(1)** assert copyright on the software, and **(2)** offer 31 | you this License which gives you legal permission to copy, distribute 32 | and/or modify the software. 33 | 34 | A secondary benefit of defending all users' freedom is that 35 | improvements made in alternate versions of the program, if they 36 | receive widespread use, become available for other developers to 37 | incorporate. Many developers of free software are heartened and 38 | encouraged by the resulting cooperation. However, in the case of 39 | software used on network servers, this result may fail to come about. 40 | The GNU General Public License permits making a modified version and 41 | letting the public access it on a server without ever releasing its 42 | source code to the public. 43 | 44 | The GNU Affero General Public License is designed specifically to 45 | ensure that, in such cases, the modified source code becomes available 46 | to the community. It requires the operator of a network server to 47 | provide the source code of the modified version running there to the 48 | users of that server. Therefore, public use of a modified version, on 49 | a publicly accessible server, gives the public access to the source 50 | code of the modified version. 51 | 52 | An older license, called the Affero General Public License and 53 | published by Affero, was designed to accomplish similar goals. This is 54 | a different license, not a version of the Affero GPL, but Affero has 55 | released a new version of the Affero GPL which permits relicensing under 56 | this license. 57 | 58 | The precise terms and conditions for copying, distribution and 59 | modification follow. 60 | 61 | ## TERMS AND CONDITIONS 62 | 63 | ### 0. Definitions 64 | 65 | “This License” refers to version 3 of the GNU Affero General Public License. 66 | 67 | “Copyright” also means copyright-like laws that apply to other kinds of 68 | works, such as semiconductor masks. 69 | 70 | “The Program” refers to any copyrightable work licensed under this 71 | License. Each licensee is addressed as “you”. “Licensees” and 72 | “recipients” may be individuals or organizations. 73 | 74 | To “modify” a work means to copy from or adapt all or part of the work 75 | in a fashion requiring copyright permission, other than the making of an 76 | exact copy. The resulting work is called a “modified version” of the 77 | earlier work or a work “based on” the earlier work. 78 | 79 | A “covered work” means either the unmodified Program or a work based 80 | on the Program. 81 | 82 | To “propagate” a work means to do anything with it that, without 83 | permission, would make you directly or secondarily liable for 84 | infringement under applicable copyright law, except executing it on a 85 | computer or modifying a private copy. Propagation includes copying, 86 | distribution (with or without modification), making available to the 87 | public, and in some countries other activities as well. 88 | 89 | To “convey” a work means any kind of propagation that enables other 90 | parties to make or receive copies. Mere interaction with a user through 91 | a computer network, with no transfer of a copy, is not conveying. 92 | 93 | An interactive user interface displays “Appropriate Legal Notices” 94 | to the extent that it includes a convenient and prominently visible 95 | feature that **(1)** displays an appropriate copyright notice, and **(2)** 96 | tells the user that there is no warranty for the work (except to the 97 | extent that warranties are provided), that licensees may convey the 98 | work under this License, and how to view a copy of this License. If 99 | the interface presents a list of user commands or options, such as a 100 | menu, a prominent item in the list meets this criterion. 101 | 102 | ### 1. Source Code 103 | 104 | The “source code” for a work means the preferred form of the work 105 | for making modifications to it. “Object code” means any non-source 106 | form of a work. 107 | 108 | A “Standard Interface” means an interface that either is an official 109 | standard defined by a recognized standards body, or, in the case of 110 | interfaces specified for a particular programming language, one that 111 | is widely used among developers working in that language. 112 | 113 | The “System Libraries” of an executable work include anything, other 114 | than the work as a whole, that **(a)** is included in the normal form of 115 | packaging a Major Component, but which is not part of that Major 116 | Component, and **(b)** serves only to enable use of the work with that 117 | Major Component, or to implement a Standard Interface for which an 118 | implementation is available to the public in source code form. A 119 | “Major Component”, in this context, means a major essential component 120 | (kernel, window system, and so on) of the specific operating system 121 | (if any) on which the executable work runs, or a compiler used to 122 | produce the work, or an object code interpreter used to run it. 123 | 124 | The “Corresponding Source” for a work in object code form means all 125 | the source code needed to generate, install, and (for an executable 126 | work) run the object code and to modify the work, including scripts to 127 | control those activities. However, it does not include the work's 128 | System Libraries, or general-purpose tools or generally available free 129 | programs which are used unmodified in performing those activities but 130 | which are not part of the work. For example, Corresponding Source 131 | includes interface definition files associated with source files for 132 | the work, and the source code for shared libraries and dynamically 133 | linked subprograms that the work is specifically designed to require, 134 | such as by intimate data communication or control flow between those 135 | subprograms and other parts of the work. 136 | 137 | The Corresponding Source need not include anything that users 138 | can regenerate automatically from other parts of the Corresponding 139 | Source. 140 | 141 | The Corresponding Source for a work in source code form is that 142 | same work. 143 | 144 | ### 2. Basic Permissions 145 | 146 | All rights granted under this License are granted for the term of 147 | copyright on the Program, and are irrevocable provided the stated 148 | conditions are met. This License explicitly affirms your unlimited 149 | permission to run the unmodified Program. The output from running a 150 | covered work is covered by this License only if the output, given its 151 | content, constitutes a covered work. This License acknowledges your 152 | rights of fair use or other equivalent, as provided by copyright law. 153 | 154 | You may make, run and propagate covered works that you do not 155 | convey, without conditions so long as your license otherwise remains 156 | in force. You may convey covered works to others for the sole purpose 157 | of having them make modifications exclusively for you, or provide you 158 | with facilities for running those works, provided that you comply with 159 | the terms of this License in conveying all material for which you do 160 | not control copyright. Those thus making or running the covered works 161 | for you must do so exclusively on your behalf, under your direction 162 | and control, on terms that prohibit them from making any copies of 163 | your copyrighted material outside their relationship with you. 164 | 165 | Conveying under any other circumstances is permitted solely under 166 | the conditions stated below. Sublicensing is not allowed; section 10 167 | makes it unnecessary. 168 | 169 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law 170 | 171 | No covered work shall be deemed part of an effective technological 172 | measure under any applicable law fulfilling obligations under article 173 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 174 | similar laws prohibiting or restricting circumvention of such 175 | measures. 176 | 177 | When you convey a covered work, you waive any legal power to forbid 178 | circumvention of technological measures to the extent such circumvention 179 | is effected by exercising rights under this License with respect to 180 | the covered work, and you disclaim any intention to limit operation or 181 | modification of the work as a means of enforcing, against the work's 182 | users, your or third parties' legal rights to forbid circumvention of 183 | technological measures. 184 | 185 | ### 4. Conveying Verbatim Copies 186 | 187 | You may convey verbatim copies of the Program's source code as you 188 | receive it, in any medium, provided that you conspicuously and 189 | appropriately publish on each copy an appropriate copyright notice; 190 | keep intact all notices stating that this License and any 191 | non-permissive terms added in accord with section 7 apply to the code; 192 | keep intact all notices of the absence of any warranty; and give all 193 | recipients a copy of this License along with the Program. 194 | 195 | You may charge any price or no price for each copy that you convey, 196 | and you may offer support or warranty protection for a fee. 197 | 198 | ### 5. Conveying Modified Source Versions 199 | 200 | You may convey a work based on the Program, or the modifications to 201 | produce it from the Program, in the form of source code under the 202 | terms of section 4, provided that you also meet all of these conditions: 203 | 204 | * **a)** The work must carry prominent notices stating that you modified 205 | it, and giving a relevant date. 206 | * **b)** The work must carry prominent notices stating that it is 207 | released under this License and any conditions added under section 7. 208 | This requirement modifies the requirement in section 4 to 209 | “keep intact all notices”. 210 | * **c)** You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | * **d)** If the work has interactive user interfaces, each must display 218 | Appropriate Legal Notices; however, if the Program has interactive 219 | interfaces that do not display Appropriate Legal Notices, your 220 | work need not make them do so. 221 | 222 | A compilation of a covered work with other separate and independent 223 | works, which are not by their nature extensions of the covered work, 224 | and which are not combined with it such as to form a larger program, 225 | in or on a volume of a storage or distribution medium, is called an 226 | “aggregate” if the compilation and its resulting copyright are not 227 | used to limit the access or legal rights of the compilation's users 228 | beyond what the individual works permit. Inclusion of a covered work 229 | in an aggregate does not cause this License to apply to the other 230 | parts of the aggregate. 231 | 232 | ### 6. Conveying Non-Source Forms 233 | 234 | You may convey a covered work in object code form under the terms 235 | of sections 4 and 5, provided that you also convey the 236 | machine-readable Corresponding Source under the terms of this License, 237 | in one of these ways: 238 | 239 | * **a)** Convey the object code in, or embodied in, a physical product 240 | (including a physical distribution medium), accompanied by the 241 | Corresponding Source fixed on a durable physical medium 242 | customarily used for software interchange. 243 | * **b)** Convey the object code in, or embodied in, a physical product 244 | (including a physical distribution medium), accompanied by a 245 | written offer, valid for at least three years and valid for as 246 | long as you offer spare parts or customer support for that product 247 | model, to give anyone who possesses the object code either **(1)** a 248 | copy of the Corresponding Source for all the software in the 249 | product that is covered by this License, on a durable physical 250 | medium customarily used for software interchange, for a price no 251 | more than your reasonable cost of physically performing this 252 | conveying of source, or **(2)** access to copy the 253 | Corresponding Source from a network server at no charge. 254 | * **c)** Convey individual copies of the object code with a copy of the 255 | written offer to provide the Corresponding Source. This 256 | alternative is allowed only occasionally and noncommercially, and 257 | only if you received the object code with such an offer, in accord 258 | with subsection 6b. 259 | * **d)** Convey the object code by offering access from a designated 260 | place (gratis or for a charge), and offer equivalent access to the 261 | Corresponding Source in the same way through the same place at no 262 | further charge. You need not require recipients to copy the 263 | Corresponding Source along with the object code. If the place to 264 | copy the object code is a network server, the Corresponding Source 265 | may be on a different server (operated by you or a third party) 266 | that supports equivalent copying facilities, provided you maintain 267 | clear directions next to the object code saying where to find the 268 | Corresponding Source. Regardless of what server hosts the 269 | Corresponding Source, you remain obligated to ensure that it is 270 | available for as long as needed to satisfy these requirements. 271 | * **e)** Convey the object code using peer-to-peer transmission, provided 272 | you inform other peers where the object code and Corresponding 273 | Source of the work are being offered to the general public at no 274 | charge under subsection 6d. 275 | 276 | A separable portion of the object code, whose source code is excluded 277 | from the Corresponding Source as a System Library, need not be 278 | included in conveying the object code work. 279 | 280 | A “User Product” is either **(1)** a “consumer product”, which means any 281 | tangible personal property which is normally used for personal, family, 282 | or household purposes, or **(2)** anything designed or sold for incorporation 283 | into a dwelling. In determining whether a product is a consumer product, 284 | doubtful cases shall be resolved in favor of coverage. For a particular 285 | product received by a particular user, “normally used” refers to a 286 | typical or common use of that class of product, regardless of the status 287 | of the particular user or of the way in which the particular user 288 | actually uses, or expects or is expected to use, the product. A product 289 | is a consumer product regardless of whether the product has substantial 290 | commercial, industrial or non-consumer uses, unless such uses represent 291 | the only significant mode of use of the product. 292 | 293 | “Installation Information” for a User Product means any methods, 294 | procedures, authorization keys, or other information required to install 295 | and execute modified versions of a covered work in that User Product from 296 | a modified version of its Corresponding Source. The information must 297 | suffice to ensure that the continued functioning of the modified object 298 | code is in no case prevented or interfered with solely because 299 | modification has been made. 300 | 301 | If you convey an object code work under this section in, or with, or 302 | specifically for use in, a User Product, and the conveying occurs as 303 | part of a transaction in which the right of possession and use of the 304 | User Product is transferred to the recipient in perpetuity or for a 305 | fixed term (regardless of how the transaction is characterized), the 306 | Corresponding Source conveyed under this section must be accompanied 307 | by the Installation Information. But this requirement does not apply 308 | if neither you nor any third party retains the ability to install 309 | modified object code on the User Product (for example, the work has 310 | been installed in ROM). 311 | 312 | The requirement to provide Installation Information does not include a 313 | requirement to continue to provide support service, warranty, or updates 314 | for a work that has been modified or installed by the recipient, or for 315 | the User Product in which it has been modified or installed. Access to a 316 | network may be denied when the modification itself materially and 317 | adversely affects the operation of the network or violates the rules and 318 | protocols for communication across the network. 319 | 320 | Corresponding Source conveyed, and Installation Information provided, 321 | in accord with this section must be in a format that is publicly 322 | documented (and with an implementation available to the public in 323 | source code form), and must require no special password or key for 324 | unpacking, reading or copying. 325 | 326 | ### 7. Additional Terms 327 | 328 | “Additional permissions” are terms that supplement the terms of this 329 | License by making exceptions from one or more of its conditions. 330 | Additional permissions that are applicable to the entire Program shall 331 | be treated as though they were included in this License, to the extent 332 | that they are valid under applicable law. If additional permissions 333 | apply only to part of the Program, that part may be used separately 334 | under those permissions, but the entire Program remains governed by 335 | this License without regard to the additional permissions. 336 | 337 | When you convey a copy of a covered work, you may at your option 338 | remove any additional permissions from that copy, or from any part of 339 | it. (Additional permissions may be written to require their own 340 | removal in certain cases when you modify the work.) You may place 341 | additional permissions on material, added by you to a covered work, 342 | for which you have or can give appropriate copyright permission. 343 | 344 | Notwithstanding any other provision of this License, for material you 345 | add to a covered work, you may (if authorized by the copyright holders of 346 | that material) supplement the terms of this License with terms: 347 | 348 | * **a)** Disclaiming warranty or limiting liability differently from the 349 | terms of sections 15 and 16 of this License; or 350 | * **b)** Requiring preservation of specified reasonable legal notices or 351 | author attributions in that material or in the Appropriate Legal 352 | Notices displayed by works containing it; or 353 | * **c)** Prohibiting misrepresentation of the origin of that material, or 354 | requiring that modified versions of such material be marked in 355 | reasonable ways as different from the original version; or 356 | * **d)** Limiting the use for publicity purposes of names of licensors or 357 | authors of the material; or 358 | * **e)** Declining to grant rights under trademark law for use of some 359 | trade names, trademarks, or service marks; or 360 | * **f)** Requiring indemnification of licensors and authors of that 361 | material by anyone who conveys the material (or modified versions of 362 | it) with contractual assumptions of liability to the recipient, for 363 | any liability that these contractual assumptions directly impose on 364 | those licensors and authors. 365 | 366 | All other non-permissive additional terms are considered “further 367 | restrictions” within the meaning of section 10. If the Program as you 368 | received it, or any part of it, contains a notice stating that it is 369 | governed by this License along with a term that is a further 370 | restriction, you may remove that term. If a license document contains 371 | a further restriction but permits relicensing or conveying under this 372 | License, you may add to a covered work material governed by the terms 373 | of that license document, provided that the further restriction does 374 | not survive such relicensing or conveying. 375 | 376 | If you add terms to a covered work in accord with this section, you 377 | must place, in the relevant source files, a statement of the 378 | additional terms that apply to those files, or a notice indicating 379 | where to find the applicable terms. 380 | 381 | Additional terms, permissive or non-permissive, may be stated in the 382 | form of a separately written license, or stated as exceptions; 383 | the above requirements apply either way. 384 | 385 | ### 8. Termination 386 | 387 | You may not propagate or modify a covered work except as expressly 388 | provided under this License. Any attempt otherwise to propagate or 389 | modify it is void, and will automatically terminate your rights under 390 | this License (including any patent licenses granted under the third 391 | paragraph of section 11). 392 | 393 | However, if you cease all violation of this License, then your 394 | license from a particular copyright holder is reinstated **(a)** 395 | provisionally, unless and until the copyright holder explicitly and 396 | finally terminates your license, and **(b)** permanently, if the copyright 397 | holder fails to notify you of the violation by some reasonable means 398 | prior to 60 days after the cessation. 399 | 400 | Moreover, your license from a particular copyright holder is 401 | reinstated permanently if the copyright holder notifies you of the 402 | violation by some reasonable means, this is the first time you have 403 | received notice of violation of this License (for any work) from that 404 | copyright holder, and you cure the violation prior to 30 days after 405 | your receipt of the notice. 406 | 407 | Termination of your rights under this section does not terminate the 408 | licenses of parties who have received copies or rights from you under 409 | this License. If your rights have been terminated and not permanently 410 | reinstated, you do not qualify to receive new licenses for the same 411 | material under section 10. 412 | 413 | ### 9. Acceptance Not Required for Having Copies 414 | 415 | You are not required to accept this License in order to receive or 416 | run a copy of the Program. Ancillary propagation of a covered work 417 | occurring solely as a consequence of using peer-to-peer transmission 418 | to receive a copy likewise does not require acceptance. However, 419 | nothing other than this License grants you permission to propagate or 420 | modify any covered work. These actions infringe copyright if you do 421 | not accept this License. Therefore, by modifying or propagating a 422 | covered work, you indicate your acceptance of this License to do so. 423 | 424 | ### 10. Automatic Licensing of Downstream Recipients 425 | 426 | Each time you convey a covered work, the recipient automatically 427 | receives a license from the original licensors, to run, modify and 428 | propagate that work, subject to this License. You are not responsible 429 | for enforcing compliance by third parties with this License. 430 | 431 | An “entity transaction” is a transaction transferring control of an 432 | organization, or substantially all assets of one, or subdividing an 433 | organization, or merging organizations. If propagation of a covered 434 | work results from an entity transaction, each party to that 435 | transaction who receives a copy of the work also receives whatever 436 | licenses to the work the party's predecessor in interest had or could 437 | give under the previous paragraph, plus a right to possession of the 438 | Corresponding Source of the work from the predecessor in interest, if 439 | the predecessor has it or can get it with reasonable efforts. 440 | 441 | You may not impose any further restrictions on the exercise of the 442 | rights granted or affirmed under this License. For example, you may 443 | not impose a license fee, royalty, or other charge for exercise of 444 | rights granted under this License, and you may not initiate litigation 445 | (including a cross-claim or counterclaim in a lawsuit) alleging that 446 | any patent claim is infringed by making, using, selling, offering for 447 | sale, or importing the Program or any portion of it. 448 | 449 | ### 11. Patents 450 | 451 | A “contributor” is a copyright holder who authorizes use under this 452 | License of the Program or a work on which the Program is based. The 453 | work thus licensed is called the contributor's “contributor version”. 454 | 455 | A contributor's “essential patent claims” are all patent claims 456 | owned or controlled by the contributor, whether already acquired or 457 | hereafter acquired, that would be infringed by some manner, permitted 458 | by this License, of making, using, or selling its contributor version, 459 | but do not include claims that would be infringed only as a 460 | consequence of further modification of the contributor version. For 461 | purposes of this definition, “control” includes the right to grant 462 | patent sublicenses in a manner consistent with the requirements of 463 | this License. 464 | 465 | Each contributor grants you a non-exclusive, worldwide, royalty-free 466 | patent license under the contributor's essential patent claims, to 467 | make, use, sell, offer for sale, import and otherwise run, modify and 468 | propagate the contents of its contributor version. 469 | 470 | In the following three paragraphs, a “patent license” is any express 471 | agreement or commitment, however denominated, not to enforce a patent 472 | (such as an express permission to practice a patent or covenant not to 473 | sue for patent infringement). To “grant” such a patent license to a 474 | party means to make such an agreement or commitment not to enforce a 475 | patent against the party. 476 | 477 | If you convey a covered work, knowingly relying on a patent license, 478 | and the Corresponding Source of the work is not available for anyone 479 | to copy, free of charge and under the terms of this License, through a 480 | publicly available network server or other readily accessible means, 481 | then you must either **(1)** cause the Corresponding Source to be so 482 | available, or **(2)** arrange to deprive yourself of the benefit of the 483 | patent license for this particular work, or **(3)** arrange, in a manner 484 | consistent with the requirements of this License, to extend the patent 485 | license to downstream recipients. “Knowingly relying” means you have 486 | actual knowledge that, but for the patent license, your conveying the 487 | covered work in a country, or your recipient's use of the covered work 488 | in a country, would infringe one or more identifiable patents in that 489 | country that you have reason to believe are valid. 490 | 491 | If, pursuant to or in connection with a single transaction or 492 | arrangement, you convey, or propagate by procuring conveyance of, a 493 | covered work, and grant a patent license to some of the parties 494 | receiving the covered work authorizing them to use, propagate, modify 495 | or convey a specific copy of the covered work, then the patent license 496 | you grant is automatically extended to all recipients of the covered 497 | work and works based on it. 498 | 499 | A patent license is “discriminatory” if it does not include within 500 | the scope of its coverage, prohibits the exercise of, or is 501 | conditioned on the non-exercise of one or more of the rights that are 502 | specifically granted under this License. You may not convey a covered 503 | work if you are a party to an arrangement with a third party that is 504 | in the business of distributing software, under which you make payment 505 | to the third party based on the extent of your activity of conveying 506 | the work, and under which the third party grants, to any of the 507 | parties who would receive the covered work from you, a discriminatory 508 | patent license **(a)** in connection with copies of the covered work 509 | conveyed by you (or copies made from those copies), or **(b)** primarily 510 | for and in connection with specific products or compilations that 511 | contain the covered work, unless you entered into that arrangement, 512 | or that patent license was granted, prior to 28 March 2007. 513 | 514 | Nothing in this License shall be construed as excluding or limiting 515 | any implied license or other defenses to infringement that may 516 | otherwise be available to you under applicable patent law. 517 | 518 | ### 12. No Surrender of Others' Freedom 519 | 520 | If conditions are imposed on you (whether by court order, agreement or 521 | otherwise) that contradict the conditions of this License, they do not 522 | excuse you from the conditions of this License. If you cannot convey a 523 | covered work so as to satisfy simultaneously your obligations under this 524 | License and any other pertinent obligations, then as a consequence you may 525 | not convey it at all. For example, if you agree to terms that obligate you 526 | to collect a royalty for further conveying from those to whom you convey 527 | the Program, the only way you could satisfy both those terms and this 528 | License would be to refrain entirely from conveying the Program. 529 | 530 | ### 13. Remote Network Interaction; Use with the GNU General Public License 531 | 532 | Notwithstanding any other provision of this License, if you modify the 533 | Program, your modified version must prominently offer all users 534 | interacting with it remotely through a computer network (if your version 535 | supports such interaction) an opportunity to receive the Corresponding 536 | Source of your version by providing access to the Corresponding Source 537 | from a network server at no charge, through some standard or customary 538 | means of facilitating copying of software. This Corresponding Source 539 | shall include the Corresponding Source for any work covered by version 3 540 | of the GNU General Public License that is incorporated pursuant to the 541 | following paragraph. 542 | 543 | Notwithstanding any other provision of this License, you have 544 | permission to link or combine any covered work with a work licensed 545 | under version 3 of the GNU General Public License into a single 546 | combined work, and to convey the resulting work. The terms of this 547 | License will continue to apply to the part which is the covered work, 548 | but the work with which it is combined will remain governed by version 549 | 3 of the GNU General Public License. 550 | 551 | ### 14. Revised Versions of this License 552 | 553 | The Free Software Foundation may publish revised and/or new versions of 554 | the GNU Affero General Public License from time to time. Such new versions 555 | will be similar in spirit to the present version, but may differ in detail to 556 | address new problems or concerns. 557 | 558 | Each version is given a distinguishing version number. If the 559 | Program specifies that a certain numbered version of the GNU Affero General 560 | Public License “or any later version” applies to it, you have the 561 | option of following the terms and conditions either of that numbered 562 | version or of any later version published by the Free Software 563 | Foundation. If the Program does not specify a version number of the 564 | GNU Affero General Public License, you may choose any version ever published 565 | by the Free Software Foundation. 566 | 567 | If the Program specifies that a proxy can decide which future 568 | versions of the GNU Affero General Public License can be used, that proxy's 569 | public statement of acceptance of a version permanently authorizes you 570 | to choose that version for the Program. 571 | 572 | Later license versions may give you additional or different 573 | permissions. However, no additional obligations are imposed on any 574 | author or copyright holder as a result of your choosing to follow a 575 | later version. 576 | 577 | ### 15. Disclaimer of Warranty 578 | 579 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 580 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 581 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY 582 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 583 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 584 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 585 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 586 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 587 | 588 | ### 16. Limitation of Liability 589 | 590 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 591 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 592 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 593 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 594 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 595 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 596 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 597 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 598 | SUCH DAMAGES. 599 | 600 | ### 17. Interpretation of Sections 15 and 16 601 | 602 | If the disclaimer of warranty and limitation of liability provided 603 | above cannot be given local legal effect according to their terms, 604 | reviewing courts shall apply local law that most closely approximates 605 | an absolute waiver of all civil liability in connection with the 606 | Program, unless a warranty or assumption of liability accompanies a 607 | copy of the Program in return for a fee. 608 | 609 | _END OF TERMS AND CONDITIONS_ 610 | 611 | ## How to Apply These Terms to Your New Programs 612 | 613 | If you develop a new program, and you want it to be of the greatest 614 | possible use to the public, the best way to achieve this is to make it 615 | free software which everyone can redistribute and change under these terms. 616 | 617 | To do so, attach the following notices to the program. It is safest 618 | to attach them to the start of each source file to most effectively 619 | state the exclusion of warranty; and each file should have at least 620 | the “copyright” line and a pointer to where the full notice is found. 621 | 622 | 623 | Copyright (C) 624 | 625 | This program is free software: you can redistribute it and/or modify 626 | it under the terms of the GNU Affero General Public License as published by 627 | the Free Software Foundation, either version 3 of the License, or 628 | (at your option) any later version. 629 | 630 | This program is distributed in the hope that it will be useful, 631 | but WITHOUT ANY WARRANTY; without even the implied warranty of 632 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 633 | GNU Affero General Public License for more details. 634 | 635 | You should have received a copy of the GNU Affero General Public License 636 | along with this program. If not, see . 637 | 638 | Also add information on how to contact you by electronic and paper mail. 639 | 640 | If your software can interact with users remotely through a computer 641 | network, you should also make sure that it provides a way for users to 642 | get its source. For example, if your program is a web application, its 643 | interface could display a “Source” link that leads users to an archive 644 | of the code. There are many ways you could offer source, and different 645 | solutions will be better for different programs; see section 13 for the 646 | specific requirements. 647 | 648 | You should also get your employer (if you work as a programmer) or school, 649 | if any, to sign a “copyright disclaimer” for the program, if necessary. 650 | For more information on this, and how to apply and follow the GNU AGPL, see 651 | <>. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt text](logo.png) 2 | 3 | [![Build](https://github.com/PythonistaGuild/Swish/actions/workflows/build.yml/badge.svg)](https://github.com/PythonistaGuild/Swish/actions/workflows/build.yml) 4 | ## Swish - The powerful little audio node for Discord. 5 | 6 | Swish is a standalone server that allows you to connect multiple bots and play audio 7 | across all your guilds/servers. With built-in YouTube, SoundCloud, ++ searching, IP Rotation, 8 | and more, with more being actively developed daily. 9 | 10 | Swish is currently **EARLY ALPHA** and should be used only by developers wishing to 11 | contribute either by code or by valuable feedback. 12 | 13 | Swish aims to provide an ease of use application with native builds for Windows, macOS and 14 | Linux. 15 | 16 | ## Development Installation 17 | - Download and install rust with rustup. 18 | - Run: `py -3.10 -m pip install -U -r requirements.txt` 19 | - Run: `py -3.10 -m pip install -U -r requirements-dev.txt` 20 | - Run: `py -3.10 launcher.py` 21 | - swish should now be up and running. 22 | 23 | ## Development distribution builds 24 | - Windows: 25 | - Run: `py -3.10 build.py` 26 | -------------------------------------------------------------------------------- /bin/ffmpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonistaGuild/Swish/3666bfd644cea28264703ae4820054e53ba89879/bin/ffmpeg -------------------------------------------------------------------------------- /bin/ffmpeg.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonistaGuild/Swish/3666bfd644cea28264703ae4820054e53ba89879/bin/ffmpeg.exe -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | from __future__ import annotations 19 | 20 | import platform 21 | import sys 22 | 23 | 24 | if '--no-deps' not in sys.argv: 25 | from pip._internal.commands import create_command 26 | create_command('install').main(['.[build]']) 27 | create_command('install').main(['./native_voice']) 28 | 29 | 30 | args: list[str] = [ 31 | 'launcher.py', 32 | '--name', f'swish-{platform.system().lower()}', 33 | '--distpath', 'dist', 34 | '--exclude-module', '_bootlocale', 35 | '--onefile', 36 | ] 37 | 38 | if platform != 'Linux': 39 | args.extend( 40 | ( 41 | '--add-binary', 42 | './bin/ffmpeg.exe;.' if platform.system() == 'Windows' else './bin/ffmpeg:.' 43 | ) 44 | ) 45 | 46 | 47 | import PyInstaller.__main__ 48 | PyInstaller.__main__.run(args) 49 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Swish API Documentation 2 | 3 | Swish does stuff and things, IDK, I don't write about stuff like this. 4 | 5 | ## WebSocket 6 | 7 | - [Protocol](websocket/protocol.md) 8 | - [Opening a connection](websocket/protocol.md#opening-a-connection) 9 | - [Close codes](websocket/protocol.md#close-codes) 10 | - [Payloads](websocket/payloads.md) 11 | - [Payload format](websocket/payloads.md#payload-format) 12 | - [Op codes](websocket/payloads.md#op-codes) 13 | 14 | ## Rest API 15 | 16 | - [Search](rest/search.md) 17 | -------------------------------------------------------------------------------- /docs/rest/search.md: -------------------------------------------------------------------------------- 1 | ## Search 2 | 3 | tbd 4 | -------------------------------------------------------------------------------- /docs/websocket/payloads.md: -------------------------------------------------------------------------------- 1 | # Payload Format 2 | 3 | Payloads being sent and received by Swish should be JSON objects that match the following format. 4 | 5 | ```json 6 | { 7 | "op": "", 8 | "d": {} 9 | } 10 | ``` 11 | 12 | Received payloads that do not match this format are ignored. 13 | 14 | - The `op` field should contain a string value indicating the payload type being sent or received. See [Op Codes](#op-codes) for a list of possible values. 15 | - The `d` field should contain another JSON object containing the payload data. 16 | 17 | # Op Codes 18 | 19 | | Op | Description | 20 | |:------------------------------------|:------------| 21 | | [voice_update](#voice_update) | TBD | 22 | | [destroy](#destroy) | TBD | 23 | | [play](#play) | TBD | 24 | | [stop](#stop) | TBD | 25 | | [set_pause_state](#set_pause_state) | TBD | 26 | | [set_position](#set_position) | TBD | 27 | | [set_filter](#set_filter) | TBD | 28 | | \***[event](#event)** | TBD | 29 | 30 | *payloads that are sent *from* Swish to clients. 31 | 32 | ## voice_update 33 | 34 | ```json 35 | { 36 | "op": "voice_update", 37 | "d": { 38 | "guild_id": "490948346773635102", 39 | "session_id": "e791a05f21b28e088e654865050b29bb", 40 | "token": "baa182102d236205", 41 | "endpoint": "rotterdam2601.discord.media:443" 42 | } 43 | } 44 | ``` 45 | 46 | - `guild_id`: The id of the player this `voice_update` is for. 47 | - `session_id`: voice state session id received from a 48 | discord [`VOICE_STATE_UPDATE`](https://discord.com/developers/docs/topics/gateway#voice-state-update) event. 49 | - `token`: voice connection token received from a 50 | discord [`VOICE_SERVER_UPDATE`](https://discord.com/developers/docs/topics/gateway#voice-server-update) event. 51 | - `endpoint`: voice server endpoint received from a 52 | discord [`VOICE_SERVER_UPDATE`](https://discord.com/developers/docs/topics/gateway#voice-server-update) event. 53 | 54 | the `endpoint` field of a [`VOICE_SERVER_UPDATE`](https://discord.com/developers/docs/topics/gateway#voice-server-update) event can sometimes be null when the voice server is unavailable. Make sure you check for this before sending a `voice_update` payload type. 55 | 56 | ## destroy 57 | 58 | ```json 59 | { 60 | "op": "destroy", 61 | "d": { 62 | "guild_id": "490948346773635102" 63 | } 64 | } 65 | ``` 66 | 67 | - `guild_id`: The id of the player you want to destroy. 68 | 69 | ## play 70 | 71 | ```json 72 | { 73 | "op": "play", 74 | "d": { 75 | "guild_id": "490948346773635102", 76 | "track_id": "eyJ0aXRsZSI6ICJEdWEgTGlwYSAtIFBoeXNpY2FsIChPZmZpY2lhbCBWaWRlbykiLCAiaWRlbnRpZmllciI6ICI5SERFSGoyeXpldyIsICJ1cmwiOiAiaHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj05SERFSGoyeXpldyIsICJsZW5ndGgiOiAyNDQwMDAsICJhdXRob3IiOiAiRHVhIExpcGEiLCAiYXV0aG9yX2lkIjogIlVDLUotS1pmUlY4YzEzZk9Da2hYZExpUSIsICJ0aHVtYm5haWwiOiBudWxsLCAiaXNfbGl2ZSI6IG51bGx9", 77 | "start_time": 0, 78 | "end_time": 0, 79 | "replace": true 80 | } 81 | } 82 | ``` 83 | 84 | - `guild_id`: The id of the player you want to play a track on. 85 | - `track_id`: The id of the track you want to play. 86 | - (optional) `start_time`: The time (in milliseconds) to start playing the given track at. 87 | - (optional) `end_time`: The time (in milliseconds) to stop playing the given track at. 88 | - (optional) `replace`: Whether this track should replace the current track or not. 89 | 90 | ## stop 91 | 92 | ```json 93 | { 94 | "op": "stop", 95 | "d": { 96 | "guild_id": "490948346773635102" 97 | } 98 | } 99 | ``` 100 | 101 | - `guild_id`: The id of the player you want to stop playing a track on. 102 | 103 | ## set_pause_state 104 | 105 | ```json 106 | { 107 | "op": "set_pause_state", 108 | "d": { 109 | "guild_id": "490948346773635102", 110 | "state": true 111 | } 112 | } 113 | ``` 114 | 115 | - `guild_id`: The id of the player you want to set the pause state for. 116 | - `state`: A true or false value indicating whether the player should be paused or not. 117 | 118 | ## set_position 119 | 120 | ```json 121 | { 122 | "op": "set_position", 123 | "d": { 124 | "guild_id": "490948346773635102", 125 | "position": 1000 126 | } 127 | } 128 | ``` 129 | 130 | - `guild_id`: The id of the player you want to the set the position for. 131 | - `position`: The position (in milliseconds) to set the current track to. 132 | 133 | ## set_filter 134 | 135 | Not implemented lol 136 | 137 | ## event 138 | 139 | ### track_start 140 | 141 | ```json 142 | { 143 | "op": "event", 144 | "d": { 145 | "guild_id": "490948346773635102", 146 | "type": "track_start" 147 | } 148 | } 149 | ``` 150 | 151 | ### track_end 152 | 153 | ```json 154 | { 155 | "op": "event", 156 | "d": { 157 | "guild_id": "490948346773635102", 158 | "type": "track_end" 159 | } 160 | } 161 | ``` 162 | 163 | ### track_error 164 | 165 | ```json 166 | { 167 | "op": "event", 168 | "d": { 169 | "guild_id": "490948346773635102", 170 | "type": "track_error" 171 | } 172 | } 173 | ``` 174 | 175 | ### player_update 176 | 177 | ```json 178 | { 179 | "op": "event", 180 | "d": { 181 | "guild_id": "490948346773635102", 182 | "type": "player_update" 183 | } 184 | } 185 | ``` 186 | 187 | ### player_debug 188 | 189 | ```json 190 | { 191 | "op": "event", 192 | "d": { 193 | "guild_id": "490948346773635102", 194 | "type": "player_debug" 195 | } 196 | } 197 | ``` 198 | -------------------------------------------------------------------------------- /docs/websocket/protocol.md: -------------------------------------------------------------------------------- 1 | # Opening a Connection 2 | 3 | Opening a websocket connection requires all of the following headers to be set. 4 | 5 | ``` 6 | Authorization: Password as defined in your swish.toml file. 7 | User-Id: The user id of the bot connecting to Swish. 8 | User-Agent: The client library and version used to connect to Swish. 9 | ``` 10 | 11 | # Close Codes 12 | 13 | All intentional websocket close codes are listed below. 14 | 15 | | Close code | Reason | 16 | |:-----------|:----------------------------------------------| 17 | | 4000 | `User-Id` or `User-Agent` header is missing. | 18 | | 4001 | `Authorization` header is missing or invalid. | 19 | -------------------------------------------------------------------------------- /launcher.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | from __future__ import annotations 19 | 20 | 21 | banner: str = """ 22 | ###################################################### 23 | ## (`-').-> .-> _ (`-').-> (`-').-> ## 24 | ## ( OO)_ (`(`-')/`) (_) ( OO)_ (OO )__ ## 25 | ## (_)--\_) ,-`( OO).', ,-(`-')(_)--\_) ,--. ,'-' ## 26 | ## / _ / | |\ | | | ( OO)/ _ / | | | | ## 27 | ## \_..`--. | | '.| | | | )\_..`--. | `-' | ## 28 | ## .-._) \| |.'.| |(| |_/ .-._) \| .-. | ## 29 | ## \ /| ,'. | | |'->\ /| | | | ## 30 | ## `-----' `--' '--' `--' `-----' `--' `--' ## 31 | ## VERSION: 0.0.1alpha0 - BUILD: N/A ## 32 | ###################################################### 33 | """ 34 | print(banner) 35 | 36 | 37 | import asyncio 38 | loop: asyncio.AbstractEventLoop = asyncio.new_event_loop() 39 | 40 | 41 | from swish.logging import setup_logging 42 | setup_logging() 43 | 44 | 45 | from swish.app import App 46 | app: App = App() 47 | 48 | 49 | try: 50 | loop.create_task(app.run()) 51 | loop.run_forever() 52 | except KeyboardInterrupt: 53 | pass 54 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonistaGuild/Swish/3666bfd644cea28264703ae4820054e53ba89879/logo.png -------------------------------------------------------------------------------- /native_voice/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aead" 7 | version = "0.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" 10 | dependencies = [ 11 | "generic-array 0.14.5", 12 | "heapless", 13 | ] 14 | 15 | [[package]] 16 | name = "as-slice" 17 | version = "0.1.5" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" 20 | dependencies = [ 21 | "generic-array 0.12.4", 22 | "generic-array 0.13.3", 23 | "generic-array 0.14.5", 24 | "stable_deref_trait", 25 | ] 26 | 27 | [[package]] 28 | name = "audiopus" 29 | version = "0.2.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "3743519567e9135cf6f9f1a509851cb0c8e4cb9d66feb286668afb1923bec458" 32 | dependencies = [ 33 | "audiopus_sys", 34 | ] 35 | 36 | [[package]] 37 | name = "audiopus_sys" 38 | version = "0.1.8" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "927791de46f70facea982dbfaf19719a41ce6064443403be631a85de6a58fff9" 41 | dependencies = [ 42 | "log", 43 | "pkg-config", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.1.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 51 | 52 | [[package]] 53 | name = "base64" 54 | version = "0.12.3" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" 57 | 58 | [[package]] 59 | name = "bitflags" 60 | version = "1.3.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 63 | 64 | [[package]] 65 | name = "block-buffer" 66 | version = "0.9.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 69 | dependencies = [ 70 | "generic-array 0.14.5", 71 | ] 72 | 73 | [[package]] 74 | name = "byteorder" 75 | version = "1.4.3" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 78 | 79 | [[package]] 80 | name = "bytes" 81 | version = "0.5.6" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" 84 | 85 | [[package]] 86 | name = "bytes" 87 | version = "1.2.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" 90 | 91 | [[package]] 92 | name = "cc" 93 | version = "1.0.73" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 96 | 97 | [[package]] 98 | name = "cfg-if" 99 | version = "0.1.10" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 102 | 103 | [[package]] 104 | name = "cfg-if" 105 | version = "1.0.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 108 | 109 | [[package]] 110 | name = "core-foundation" 111 | version = "0.9.3" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 114 | dependencies = [ 115 | "core-foundation-sys", 116 | "libc", 117 | ] 118 | 119 | [[package]] 120 | name = "core-foundation-sys" 121 | version = "0.8.3" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 124 | 125 | [[package]] 126 | name = "cpufeatures" 127 | version = "0.2.2" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" 130 | dependencies = [ 131 | "libc", 132 | ] 133 | 134 | [[package]] 135 | name = "cpuid-bool" 136 | version = "0.2.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" 139 | 140 | [[package]] 141 | name = "crossbeam-channel" 142 | version = "0.4.4" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" 145 | dependencies = [ 146 | "crossbeam-utils", 147 | "maybe-uninit", 148 | ] 149 | 150 | [[package]] 151 | name = "crossbeam-utils" 152 | version = "0.7.2" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 155 | dependencies = [ 156 | "autocfg", 157 | "cfg-if 0.1.10", 158 | "lazy_static", 159 | ] 160 | 161 | [[package]] 162 | name = "ctor" 163 | version = "0.1.22" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" 166 | dependencies = [ 167 | "quote", 168 | "syn", 169 | ] 170 | 171 | [[package]] 172 | name = "digest" 173 | version = "0.9.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 176 | dependencies = [ 177 | "generic-array 0.14.5", 178 | ] 179 | 180 | [[package]] 181 | name = "discord-ext-native-voice" 182 | version = "0.1.0" 183 | dependencies = [ 184 | "audiopus", 185 | "crossbeam-channel", 186 | "native-tls", 187 | "parking_lot", 188 | "pyo3", 189 | "rand", 190 | "serde", 191 | "serde_json", 192 | "tungstenite", 193 | "xsalsa20poly1305", 194 | ] 195 | 196 | [[package]] 197 | name = "fastrand" 198 | version = "1.7.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 201 | dependencies = [ 202 | "instant", 203 | ] 204 | 205 | [[package]] 206 | name = "fnv" 207 | version = "1.0.7" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 210 | 211 | [[package]] 212 | name = "foreign-types" 213 | version = "0.3.2" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 216 | dependencies = [ 217 | "foreign-types-shared", 218 | ] 219 | 220 | [[package]] 221 | name = "foreign-types-shared" 222 | version = "0.1.1" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 225 | 226 | [[package]] 227 | name = "form_urlencoded" 228 | version = "1.0.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 231 | dependencies = [ 232 | "matches", 233 | "percent-encoding", 234 | ] 235 | 236 | [[package]] 237 | name = "generic-array" 238 | version = "0.12.4" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" 241 | dependencies = [ 242 | "typenum", 243 | ] 244 | 245 | [[package]] 246 | name = "generic-array" 247 | version = "0.13.3" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" 250 | dependencies = [ 251 | "typenum", 252 | ] 253 | 254 | [[package]] 255 | name = "generic-array" 256 | version = "0.14.5" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" 259 | dependencies = [ 260 | "typenum", 261 | "version_check", 262 | ] 263 | 264 | [[package]] 265 | name = "getrandom" 266 | version = "0.1.16" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 269 | dependencies = [ 270 | "cfg-if 1.0.0", 271 | "libc", 272 | "wasi", 273 | ] 274 | 275 | [[package]] 276 | name = "ghost" 277 | version = "0.1.5" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "b93490550b1782c589a350f2211fff2e34682e25fed17ef53fc4fa8fe184975e" 280 | dependencies = [ 281 | "proc-macro2", 282 | "quote", 283 | "syn", 284 | ] 285 | 286 | [[package]] 287 | name = "hash32" 288 | version = "0.1.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" 291 | dependencies = [ 292 | "byteorder", 293 | ] 294 | 295 | [[package]] 296 | name = "heapless" 297 | version = "0.5.6" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1" 300 | dependencies = [ 301 | "as-slice", 302 | "generic-array 0.13.3", 303 | "hash32", 304 | "stable_deref_trait", 305 | ] 306 | 307 | [[package]] 308 | name = "http" 309 | version = "0.2.8" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 312 | dependencies = [ 313 | "bytes 1.2.0", 314 | "fnv", 315 | "itoa", 316 | ] 317 | 318 | [[package]] 319 | name = "httparse" 320 | version = "1.7.1" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" 323 | 324 | [[package]] 325 | name = "idna" 326 | version = "0.2.3" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 329 | dependencies = [ 330 | "matches", 331 | "unicode-bidi", 332 | "unicode-normalization", 333 | ] 334 | 335 | [[package]] 336 | name = "indoc" 337 | version = "0.3.6" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" 340 | dependencies = [ 341 | "indoc-impl", 342 | "proc-macro-hack", 343 | ] 344 | 345 | [[package]] 346 | name = "indoc-impl" 347 | version = "0.3.6" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" 350 | dependencies = [ 351 | "proc-macro-hack", 352 | "proc-macro2", 353 | "quote", 354 | "syn", 355 | "unindent", 356 | ] 357 | 358 | [[package]] 359 | name = "input_buffer" 360 | version = "0.3.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" 363 | dependencies = [ 364 | "bytes 0.5.6", 365 | ] 366 | 367 | [[package]] 368 | name = "instant" 369 | version = "0.1.12" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 372 | dependencies = [ 373 | "cfg-if 1.0.0", 374 | ] 375 | 376 | [[package]] 377 | name = "inventory" 378 | version = "0.1.11" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "f0eb5160c60ba1e809707918ee329adb99d222888155835c6feedba19f6c3fd4" 381 | dependencies = [ 382 | "ctor", 383 | "ghost", 384 | "inventory-impl", 385 | ] 386 | 387 | [[package]] 388 | name = "inventory-impl" 389 | version = "0.1.11" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "7e41b53715c6f0c4be49510bb82dee2c1e51c8586d885abe65396e82ed518548" 392 | dependencies = [ 393 | "proc-macro2", 394 | "quote", 395 | "syn", 396 | ] 397 | 398 | [[package]] 399 | name = "itoa" 400 | version = "1.0.2" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 403 | 404 | [[package]] 405 | name = "lazy_static" 406 | version = "1.4.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 409 | 410 | [[package]] 411 | name = "libc" 412 | version = "0.2.126" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 415 | 416 | [[package]] 417 | name = "lock_api" 418 | version = "0.4.7" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 421 | dependencies = [ 422 | "autocfg", 423 | "scopeguard", 424 | ] 425 | 426 | [[package]] 427 | name = "log" 428 | version = "0.4.17" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 431 | dependencies = [ 432 | "cfg-if 1.0.0", 433 | ] 434 | 435 | [[package]] 436 | name = "matches" 437 | version = "0.1.9" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 440 | 441 | [[package]] 442 | name = "maybe-uninit" 443 | version = "2.0.0" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 446 | 447 | [[package]] 448 | name = "native-tls" 449 | version = "0.2.10" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" 452 | dependencies = [ 453 | "lazy_static", 454 | "libc", 455 | "log", 456 | "openssl", 457 | "openssl-probe", 458 | "openssl-sys", 459 | "schannel", 460 | "security-framework", 461 | "security-framework-sys", 462 | "tempfile", 463 | ] 464 | 465 | [[package]] 466 | name = "once_cell" 467 | version = "1.13.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 470 | 471 | [[package]] 472 | name = "opaque-debug" 473 | version = "0.3.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 476 | 477 | [[package]] 478 | name = "openssl" 479 | version = "0.10.41" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" 482 | dependencies = [ 483 | "bitflags", 484 | "cfg-if 1.0.0", 485 | "foreign-types", 486 | "libc", 487 | "once_cell", 488 | "openssl-macros", 489 | "openssl-sys", 490 | ] 491 | 492 | [[package]] 493 | name = "openssl-macros" 494 | version = "0.1.0" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 497 | dependencies = [ 498 | "proc-macro2", 499 | "quote", 500 | "syn", 501 | ] 502 | 503 | [[package]] 504 | name = "openssl-probe" 505 | version = "0.1.5" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 508 | 509 | [[package]] 510 | name = "openssl-sys" 511 | version = "0.9.75" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" 514 | dependencies = [ 515 | "autocfg", 516 | "cc", 517 | "libc", 518 | "pkg-config", 519 | "vcpkg", 520 | ] 521 | 522 | [[package]] 523 | name = "parking_lot" 524 | version = "0.11.2" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 527 | dependencies = [ 528 | "instant", 529 | "lock_api", 530 | "parking_lot_core", 531 | ] 532 | 533 | [[package]] 534 | name = "parking_lot_core" 535 | version = "0.8.5" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 538 | dependencies = [ 539 | "cfg-if 1.0.0", 540 | "instant", 541 | "libc", 542 | "redox_syscall", 543 | "smallvec", 544 | "winapi", 545 | ] 546 | 547 | [[package]] 548 | name = "paste" 549 | version = "0.1.18" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" 552 | dependencies = [ 553 | "paste-impl", 554 | "proc-macro-hack", 555 | ] 556 | 557 | [[package]] 558 | name = "paste-impl" 559 | version = "0.1.18" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" 562 | dependencies = [ 563 | "proc-macro-hack", 564 | ] 565 | 566 | [[package]] 567 | name = "percent-encoding" 568 | version = "2.1.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 571 | 572 | [[package]] 573 | name = "pkg-config" 574 | version = "0.3.25" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 577 | 578 | [[package]] 579 | name = "poly1305" 580 | version = "0.6.2" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "4b7456bc1ad2d4cf82b3a016be4c2ac48daf11bf990c1603ebd447fe6f30fca8" 583 | dependencies = [ 584 | "cpuid-bool", 585 | "universal-hash", 586 | ] 587 | 588 | [[package]] 589 | name = "ppv-lite86" 590 | version = "0.2.16" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 593 | 594 | [[package]] 595 | name = "proc-macro-hack" 596 | version = "0.5.19" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 599 | 600 | [[package]] 601 | name = "proc-macro2" 602 | version = "1.0.40" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 605 | dependencies = [ 606 | "unicode-ident", 607 | ] 608 | 609 | [[package]] 610 | name = "pyo3" 611 | version = "0.12.4" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "bf6bbbe8f70d179260b3728e5d04eb012f4f0c7988e58c11433dd689cecaa72e" 614 | dependencies = [ 615 | "ctor", 616 | "indoc", 617 | "inventory", 618 | "libc", 619 | "parking_lot", 620 | "paste", 621 | "pyo3cls", 622 | "unindent", 623 | ] 624 | 625 | [[package]] 626 | name = "pyo3-derive-backend" 627 | version = "0.12.4" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "10ecd0eb6ed7b3d9965b4f4370b5b9e99e3e5e8742000e1c452c018f8c2a322f" 630 | dependencies = [ 631 | "proc-macro2", 632 | "quote", 633 | "syn", 634 | ] 635 | 636 | [[package]] 637 | name = "pyo3cls" 638 | version = "0.12.4" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "d344fdaa6a834a06dd1720ff104ea12fe101dad2e8db89345af9db74c0bb11a0" 641 | dependencies = [ 642 | "pyo3-derive-backend", 643 | "quote", 644 | "syn", 645 | ] 646 | 647 | [[package]] 648 | name = "quote" 649 | version = "1.0.20" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 652 | dependencies = [ 653 | "proc-macro2", 654 | ] 655 | 656 | [[package]] 657 | name = "rand" 658 | version = "0.7.3" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 661 | dependencies = [ 662 | "getrandom", 663 | "libc", 664 | "rand_chacha", 665 | "rand_core", 666 | "rand_hc", 667 | ] 668 | 669 | [[package]] 670 | name = "rand_chacha" 671 | version = "0.2.2" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 674 | dependencies = [ 675 | "ppv-lite86", 676 | "rand_core", 677 | ] 678 | 679 | [[package]] 680 | name = "rand_core" 681 | version = "0.5.1" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 684 | dependencies = [ 685 | "getrandom", 686 | ] 687 | 688 | [[package]] 689 | name = "rand_hc" 690 | version = "0.2.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 693 | dependencies = [ 694 | "rand_core", 695 | ] 696 | 697 | [[package]] 698 | name = "redox_syscall" 699 | version = "0.2.13" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 702 | dependencies = [ 703 | "bitflags", 704 | ] 705 | 706 | [[package]] 707 | name = "remove_dir_all" 708 | version = "0.5.3" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 711 | dependencies = [ 712 | "winapi", 713 | ] 714 | 715 | [[package]] 716 | name = "ryu" 717 | version = "1.0.10" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 720 | 721 | [[package]] 722 | name = "salsa20" 723 | version = "0.5.2" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "6fc17dc5eee5d3040d9f95a2d3ac42fb2c1829a80f417045da6cfd2befa66769" 726 | dependencies = [ 727 | "stream-cipher", 728 | "zeroize", 729 | ] 730 | 731 | [[package]] 732 | name = "schannel" 733 | version = "0.1.20" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" 736 | dependencies = [ 737 | "lazy_static", 738 | "windows-sys", 739 | ] 740 | 741 | [[package]] 742 | name = "scopeguard" 743 | version = "1.1.0" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 746 | 747 | [[package]] 748 | name = "security-framework" 749 | version = "2.6.1" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" 752 | dependencies = [ 753 | "bitflags", 754 | "core-foundation", 755 | "core-foundation-sys", 756 | "libc", 757 | "security-framework-sys", 758 | ] 759 | 760 | [[package]] 761 | name = "security-framework-sys" 762 | version = "2.6.1" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 765 | dependencies = [ 766 | "core-foundation-sys", 767 | "libc", 768 | ] 769 | 770 | [[package]] 771 | name = "serde" 772 | version = "1.0.139" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" 775 | dependencies = [ 776 | "serde_derive", 777 | ] 778 | 779 | [[package]] 780 | name = "serde_derive" 781 | version = "1.0.139" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" 784 | dependencies = [ 785 | "proc-macro2", 786 | "quote", 787 | "syn", 788 | ] 789 | 790 | [[package]] 791 | name = "serde_json" 792 | version = "1.0.82" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" 795 | dependencies = [ 796 | "itoa", 797 | "ryu", 798 | "serde", 799 | ] 800 | 801 | [[package]] 802 | name = "sha-1" 803 | version = "0.9.8" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" 806 | dependencies = [ 807 | "block-buffer", 808 | "cfg-if 1.0.0", 809 | "cpufeatures", 810 | "digest", 811 | "opaque-debug", 812 | ] 813 | 814 | [[package]] 815 | name = "smallvec" 816 | version = "1.9.0" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 819 | 820 | [[package]] 821 | name = "stable_deref_trait" 822 | version = "1.2.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 825 | 826 | [[package]] 827 | name = "stream-cipher" 828 | version = "0.4.1" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "09f8ed9974042b8c3672ff3030a69fcc03b74c47c3d1ecb7755e8a3626011e88" 831 | dependencies = [ 832 | "generic-array 0.14.5", 833 | ] 834 | 835 | [[package]] 836 | name = "subtle" 837 | version = "2.4.1" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 840 | 841 | [[package]] 842 | name = "syn" 843 | version = "1.0.98" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 846 | dependencies = [ 847 | "proc-macro2", 848 | "quote", 849 | "unicode-ident", 850 | ] 851 | 852 | [[package]] 853 | name = "tempfile" 854 | version = "3.3.0" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 857 | dependencies = [ 858 | "cfg-if 1.0.0", 859 | "fastrand", 860 | "libc", 861 | "redox_syscall", 862 | "remove_dir_all", 863 | "winapi", 864 | ] 865 | 866 | [[package]] 867 | name = "tinyvec" 868 | version = "1.6.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 871 | dependencies = [ 872 | "tinyvec_macros", 873 | ] 874 | 875 | [[package]] 876 | name = "tinyvec_macros" 877 | version = "0.1.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 880 | 881 | [[package]] 882 | name = "tungstenite" 883 | version = "0.11.1" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "f0308d80d86700c5878b9ef6321f020f29b1bb9d5ff3cab25e75e23f3a492a23" 886 | dependencies = [ 887 | "base64", 888 | "byteorder", 889 | "bytes 0.5.6", 890 | "http", 891 | "httparse", 892 | "input_buffer", 893 | "log", 894 | "native-tls", 895 | "rand", 896 | "sha-1", 897 | "url", 898 | "utf-8", 899 | ] 900 | 901 | [[package]] 902 | name = "typenum" 903 | version = "1.15.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 906 | 907 | [[package]] 908 | name = "unicode-bidi" 909 | version = "0.3.8" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 912 | 913 | [[package]] 914 | name = "unicode-ident" 915 | version = "1.0.2" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" 918 | 919 | [[package]] 920 | name = "unicode-normalization" 921 | version = "0.1.21" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" 924 | dependencies = [ 925 | "tinyvec", 926 | ] 927 | 928 | [[package]] 929 | name = "unindent" 930 | version = "0.1.9" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44" 933 | 934 | [[package]] 935 | name = "universal-hash" 936 | version = "0.4.1" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" 939 | dependencies = [ 940 | "generic-array 0.14.5", 941 | "subtle", 942 | ] 943 | 944 | [[package]] 945 | name = "url" 946 | version = "2.2.2" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 949 | dependencies = [ 950 | "form_urlencoded", 951 | "idna", 952 | "matches", 953 | "percent-encoding", 954 | ] 955 | 956 | [[package]] 957 | name = "utf-8" 958 | version = "0.7.6" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 961 | 962 | [[package]] 963 | name = "vcpkg" 964 | version = "0.2.15" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 967 | 968 | [[package]] 969 | name = "version_check" 970 | version = "0.9.4" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 973 | 974 | [[package]] 975 | name = "wasi" 976 | version = "0.9.0+wasi-snapshot-preview1" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 979 | 980 | [[package]] 981 | name = "winapi" 982 | version = "0.3.9" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 985 | dependencies = [ 986 | "winapi-i686-pc-windows-gnu", 987 | "winapi-x86_64-pc-windows-gnu", 988 | ] 989 | 990 | [[package]] 991 | name = "winapi-i686-pc-windows-gnu" 992 | version = "0.4.0" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 995 | 996 | [[package]] 997 | name = "winapi-x86_64-pc-windows-gnu" 998 | version = "0.4.0" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1001 | 1002 | [[package]] 1003 | name = "windows-sys" 1004 | version = "0.36.1" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 1007 | dependencies = [ 1008 | "windows_aarch64_msvc", 1009 | "windows_i686_gnu", 1010 | "windows_i686_msvc", 1011 | "windows_x86_64_gnu", 1012 | "windows_x86_64_msvc", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "windows_aarch64_msvc" 1017 | version = "0.36.1" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 1020 | 1021 | [[package]] 1022 | name = "windows_i686_gnu" 1023 | version = "0.36.1" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 1026 | 1027 | [[package]] 1028 | name = "windows_i686_msvc" 1029 | version = "0.36.1" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1032 | 1033 | [[package]] 1034 | name = "windows_x86_64_gnu" 1035 | version = "0.36.1" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1038 | 1039 | [[package]] 1040 | name = "windows_x86_64_msvc" 1041 | version = "0.36.1" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1044 | 1045 | [[package]] 1046 | name = "xsalsa20poly1305" 1047 | version = "0.4.2" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "a7a4120d688bcca2a2226223c83a8ca3dbf349c6a3c7bef0f4a1ca8404326dba" 1050 | dependencies = [ 1051 | "aead", 1052 | "poly1305", 1053 | "rand_core", 1054 | "salsa20", 1055 | "subtle", 1056 | "zeroize", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "zeroize" 1061 | version = "1.5.6" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "20b578acffd8516a6c3f2a1bdefc1ec37e547bb4e0fb8b6b01a4cafc886b4442" 1064 | -------------------------------------------------------------------------------- /native_voice/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "discord-ext-native-voice" 3 | version = "0.1.0" 4 | license = "MIT OR Apache-2.0" 5 | description = "A native voice implementation of voice send" 6 | authors = ["Rapptz "] 7 | edition = "2018" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | pyo3 = { version = "0.12", features = ["extension-module"] } 13 | native-tls = { version = "0.2.3"} 14 | tungstenite = { version = "0.11.1", features = ["tls"] } 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = { version = "1.0", features = ["raw_value"] } 17 | parking_lot = { version = "0.11" } 18 | crossbeam-channel = { version = "0.4" } 19 | xsalsa20poly1305 = { version = "0.4", features = ["heapless"] } 20 | rand = { version = "0.7" } 21 | audiopus = { version = "0.2" } 22 | 23 | [lib] 24 | name = "_native_voice" 25 | crate-type = ["cdylib"] 26 | -------------------------------------------------------------------------------- /native_voice/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /native_voice/discord/ext/native_voice/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonistaGuild/Swish/3666bfd644cea28264703ae4820054e53ba89879/native_voice/discord/ext/native_voice/__init__.py -------------------------------------------------------------------------------- /native_voice/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["wheel", "setuptools", "setuptools-rust"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /native_voice/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools_rust 2 | from setuptools import setup 3 | 4 | 5 | setup( 6 | name='discord-ext-native-voice', 7 | version='0.1.0', 8 | packages=[ 9 | 'discord.ext.native_voice' 10 | ], 11 | rust_extensions=[ 12 | setuptools_rust.RustExtension('discord.ext.native_voice.native_voice') 13 | ], 14 | setup_requires=[ 15 | 'setuptools-rust', 16 | 'wheel', 17 | ], 18 | zip_safe=False, 19 | ) 20 | -------------------------------------------------------------------------------- /native_voice/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::net::{AddrParseError, TcpStream}; 2 | 3 | #[derive(Debug)] 4 | pub enum ProtocolError { 5 | Serde(serde_json::error::Error), 6 | Opus(audiopus::error::Error), 7 | Nacl(xsalsa20poly1305::aead::Error), 8 | WebSocket(tungstenite::error::Error), 9 | Io(std::io::Error), 10 | Closed(u16), 11 | } 12 | 13 | pub(crate) fn custom_error(text: &str) -> ProtocolError { 14 | let inner = std::io::Error::new(std::io::ErrorKind::Other, text); 15 | ProtocolError::Io(inner) 16 | } 17 | 18 | impl std::fmt::Display for ProtocolError { 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | match self { 21 | ProtocolError::Serde(ref e) => e.fmt(f), 22 | ProtocolError::WebSocket(ref e) => e.fmt(f), 23 | ProtocolError::Opus(ref e) => e.fmt(f), 24 | ProtocolError::Nacl(ref e) => e.fmt(f), 25 | ProtocolError::Io(ref e) => e.fmt(f), 26 | ProtocolError::Closed(code) => { 27 | write!(f, "WebSocket connection closed (code: {})", code) 28 | } 29 | } 30 | } 31 | } 32 | 33 | impl std::error::Error for ProtocolError { 34 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 35 | match *self { 36 | ProtocolError::Serde(ref e) => Some(e), 37 | ProtocolError::WebSocket(ref e) => Some(e), 38 | ProtocolError::Opus(ref e) => Some(e), 39 | ProtocolError::Io(ref e) => Some(e), 40 | ProtocolError::Nacl(_) => None, 41 | ProtocolError::Closed(_) => None, 42 | } 43 | } 44 | } 45 | 46 | impl From for ProtocolError { 47 | fn from(err: serde_json::error::Error) -> Self { 48 | Self::Serde(err) 49 | } 50 | } 51 | 52 | impl From for ProtocolError { 53 | fn from(err: tungstenite::error::Error) -> Self { 54 | Self::WebSocket(err) 55 | } 56 | } 57 | 58 | impl From for ProtocolError { 59 | fn from(err: std::io::Error) -> Self { 60 | Self::Io(err) 61 | } 62 | } 63 | 64 | impl From for ProtocolError { 65 | fn from(_: AddrParseError) -> Self { 66 | custom_error("invalid IP address") 67 | } 68 | } 69 | 70 | impl From for ProtocolError { 71 | fn from(err: native_tls::Error) -> Self { 72 | let inner = std::io::Error::new(std::io::ErrorKind::Other, err.to_string()); 73 | Self::Io(inner) 74 | } 75 | } 76 | 77 | impl From> for ProtocolError { 78 | fn from(err: native_tls::HandshakeError) -> Self { 79 | let inner = std::io::Error::new(std::io::ErrorKind::Other, err.to_string()); 80 | Self::Io(inner) 81 | } 82 | } 83 | 84 | impl From for ProtocolError { 85 | fn from(err: audiopus::error::Error) -> Self { 86 | Self::Opus(err) 87 | } 88 | } 89 | 90 | impl From for ProtocolError { 91 | fn from(err: xsalsa20poly1305::aead::Error) -> Self { 92 | Self::Nacl(err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /native_voice/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::create_exception; 2 | use pyo3::prelude::*; 3 | use pyo3::types::{PyBytes, PyDict}; 4 | 5 | use std::sync::Arc; 6 | use std::thread; 7 | 8 | use parking_lot::Mutex; 9 | 10 | pub mod error; 11 | pub mod payloads; 12 | pub mod player; 13 | pub mod protocol; 14 | pub(crate) mod state; 15 | 16 | create_exception!(_native_voice, ReconnectError, pyo3::exceptions::PyException); 17 | create_exception!(_native_voice, ConnectionError, pyo3::exceptions::PyException); 18 | create_exception!(_native_voice, ConnectionClosed, pyo3::exceptions::PyException); 19 | 20 | fn code_can_be_handled(code: u16) -> bool { 21 | // Non-resumable close-codes are: 22 | // 1000 - normal closure 23 | // 4014 - voice channel deleted 24 | // 4015 - voice server crash 25 | code != 1000 && code != 4014 && code != 4015 26 | } 27 | 28 | impl std::convert::From for PyErr { 29 | fn from(err: error::ProtocolError) -> Self { 30 | match err { 31 | error::ProtocolError::Closed(code) if code_can_be_handled(code) => { 32 | ReconnectError::new_err(code) 33 | } 34 | error::ProtocolError::Closed(code) => ConnectionClosed::new_err(code), 35 | _ => ConnectionError::new_err(err.to_string()), 36 | } 37 | } 38 | } 39 | 40 | fn set_result(py: Python, loop_: PyObject, future: PyObject, result: PyObject) -> PyResult<()> { 41 | let set = future.getattr(py, "set_result")?; 42 | loop_.call_method1(py, "call_soon_threadsafe", (set, result))?; 43 | Ok(()) 44 | } 45 | 46 | fn set_exception(py: Python, loop_: PyObject, future: PyObject, exception: PyErr) -> PyResult<()> { 47 | let set = future.getattr(py, "set_exception")?; 48 | loop_.call_method1(py, "call_soon_threadsafe", (set, exception.to_object(py)))?; 49 | Ok(()) 50 | } 51 | 52 | #[pyclass] 53 | struct VoiceConnection { 54 | protocol: Arc>, 55 | player: Option, 56 | } 57 | 58 | #[pymethods] 59 | impl VoiceConnection { 60 | #[text_signature = "(loop, /)"] 61 | fn run(&mut self, py: Python, loop_: PyObject) -> PyResult { 62 | let (future, result): (PyObject, PyObject) = { 63 | let fut: PyObject = loop_.call_method0(py, "create_future")?.into(); 64 | (fut.clone_ref(py), fut) 65 | }; 66 | 67 | let proto = Arc::clone(&self.protocol); 68 | thread::spawn(move || { 69 | loop { 70 | let result = { 71 | // TODO: consider not using locks? 72 | let mut guard = proto.lock(); 73 | guard.poll() 74 | }; 75 | if let Err(e) = result { 76 | let gil = Python::acquire_gil(); 77 | let py = gil.python(); 78 | match e { 79 | error::ProtocolError::Closed(code) if code_can_be_handled(code) => { 80 | let _ = set_result(py, loop_, future, py.None()); 81 | break; 82 | } 83 | _ => { 84 | let _ = set_exception(py, loop_, future, PyErr::from(e)); 85 | break; 86 | } 87 | } 88 | } 89 | } 90 | }); 91 | Ok(result) 92 | } 93 | 94 | fn disconnect(&mut self) -> PyResult<()> { 95 | let mut guard = self.protocol.lock(); 96 | guard.close(1000)?; 97 | Ok(()) 98 | } 99 | 100 | fn stop(&mut self) { 101 | if let Some(player) = &self.player { 102 | player.stop(); 103 | } 104 | } 105 | 106 | fn play(&mut self, input: String) -> PyResult<()> { 107 | if let Some(player) = &self.player { 108 | player.stop(); 109 | } 110 | 111 | let source = Box::new(player::FFmpegPCMAudio::new(input.as_str())?); 112 | let player = player::AudioPlayer::new( 113 | |error| { 114 | // println!("Audio Player Error: {:?}", error); 115 | }, 116 | Arc::clone(&self.protocol), 117 | Arc::new(Mutex::new(source)), 118 | ); 119 | 120 | self.player = Some(player); 121 | Ok(()) 122 | } 123 | 124 | fn pause(&mut self) { 125 | if let Some(player) = &self.player { 126 | player.pause(); 127 | } 128 | } 129 | 130 | fn resume(&mut self) { 131 | if let Some(player) = &self.player { 132 | player.resume(); 133 | } 134 | } 135 | 136 | fn is_playing(&self) -> bool { 137 | if let Some(player) = &self.player { 138 | player.is_playing() 139 | } else { 140 | false 141 | } 142 | } 143 | 144 | fn is_paused(&self) -> bool { 145 | if let Some(player) = &self.player { 146 | player.is_paused() 147 | } else { 148 | false 149 | } 150 | } 151 | 152 | #[getter] 153 | fn encryption_mode(&self) -> PyResult { 154 | let encryption = { 155 | let proto = self.protocol.lock(); 156 | proto.encryption 157 | }; 158 | Ok(encryption.into()) 159 | } 160 | 161 | #[getter] 162 | fn secret_key(&self) -> PyResult> { 163 | let secret_key = { 164 | let proto = self.protocol.lock(); 165 | proto.secret_key 166 | }; 167 | Ok(secret_key.into()) 168 | } 169 | 170 | fn send_playing(&self) -> PyResult<()> { 171 | let mut proto = self.protocol.lock(); 172 | proto.speaking(payloads::SpeakingFlags::microphone())?; 173 | Ok(()) 174 | } 175 | 176 | fn get_state<'py>(&self, py: Python<'py>) -> PyResult<&'py PyDict> { 177 | let result = PyDict::new(py); 178 | let proto = self.protocol.lock(); 179 | result.set_item("secret_key", Vec::::from(proto.secret_key))?; 180 | result.set_item("encryption_mode", Into::::into(proto.encryption))?; 181 | result.set_item("endpoint", proto.endpoint.clone())?; 182 | result.set_item("endpoint_ip", proto.endpoint_ip.clone())?; 183 | result.set_item("port", proto.port)?; 184 | result.set_item("token", proto.token.clone())?; 185 | result.set_item("ssrc", proto.ssrc)?; 186 | result.set_item( 187 | "last_heartbeat", 188 | proto.last_heartbeat.elapsed().as_secs_f32(), 189 | )?; 190 | result.set_item("player_connected", self.player.is_some())?; 191 | Ok(result) 192 | } 193 | } 194 | 195 | #[pyclass] 196 | struct VoiceConnector { 197 | #[pyo3(get, set)] 198 | session_id: String, 199 | #[pyo3(get)] 200 | endpoint: String, 201 | #[pyo3(get)] 202 | server_id: String, 203 | #[pyo3(get, set)] 204 | user_id: u64, 205 | token: String, 206 | } 207 | 208 | // __new__ -> VoiceConnector 209 | // update_socket -> bool 210 | // connect -> Future<()> 211 | // disconnect -> None 212 | 213 | #[pymethods] 214 | impl VoiceConnector { 215 | #[new] 216 | fn new() -> Self { 217 | Self { 218 | session_id: String::new(), 219 | endpoint: String::new(), 220 | token: String::new(), 221 | server_id: String::new(), 222 | user_id: 0, 223 | } 224 | } 225 | 226 | fn update_socket( 227 | &mut self, 228 | token: String, 229 | server_id: String, 230 | endpoint: String, 231 | ) -> PyResult<()> { 232 | self.token = token; 233 | self.server_id = server_id; 234 | self.endpoint = endpoint; 235 | Ok(()) 236 | } 237 | 238 | #[text_signature = "(loop, /)"] 239 | fn connect(&mut self, py: Python, loop_: PyObject) -> PyResult { 240 | let (future, result): (PyObject, PyObject) = { 241 | let fut: PyObject = loop_.call_method0(py, "create_future")?.into(); 242 | (fut.clone_ref(py), fut) 243 | }; 244 | 245 | let mut builder = protocol::ProtocolBuilder::new(self.endpoint.clone()); 246 | builder 247 | .server(self.server_id.clone()) 248 | .session(self.session_id.clone()) 249 | .auth(self.token.clone()) 250 | .user(self.user_id.to_string()); 251 | 252 | thread::spawn(move || { 253 | let result = { 254 | match builder.connect() { 255 | Err(e) => Err(e), 256 | Ok(mut protocol) => protocol.finish_flow(false).and(Ok(protocol)), 257 | } 258 | }; 259 | let gil = Python::acquire_gil(); 260 | let py = gil.python(); 261 | let _ = match result { 262 | Err(e) => set_exception(py, loop_, future, PyErr::from(e)), 263 | Ok(protocol) => { 264 | let object = VoiceConnection { 265 | protocol: Arc::new(Mutex::new(protocol)), 266 | player: None, 267 | }; 268 | set_result(py, loop_, future, object.into_py(py)) 269 | } 270 | }; 271 | }); 272 | Ok(result) 273 | } 274 | } 275 | 276 | use xsalsa20poly1305::aead::{generic_array::GenericArray, Aead, AeadInPlace, Buffer, NewAead}; 277 | use xsalsa20poly1305::XSalsa20Poly1305; 278 | 279 | #[pyclass] 280 | struct Debugger { 281 | opus: audiopus::coder::Encoder, 282 | cipher: XSalsa20Poly1305, 283 | sequence: u16, 284 | timestamp: u32, 285 | #[pyo3(get, set)] 286 | ssrc: u32, 287 | lite_nonce: u32, 288 | } 289 | 290 | fn get_encoder() -> Result { 291 | let mut encoder = audiopus::coder::Encoder::new( 292 | audiopus::SampleRate::Hz48000, 293 | audiopus::Channels::Stereo, 294 | audiopus::Application::Audio, 295 | )?; 296 | 297 | encoder.set_bitrate(audiopus::Bitrate::BitsPerSecond(128 * 1024))?; 298 | encoder.enable_inband_fec()?; 299 | encoder.set_packet_loss_perc(15)?; 300 | encoder.set_bandwidth(audiopus::Bandwidth::Fullband)?; 301 | encoder.set_signal(audiopus::Signal::Auto)?; 302 | Ok(encoder) 303 | } 304 | 305 | #[pymethods] 306 | impl Debugger { 307 | #[new] 308 | fn new(secret_key: Vec) -> PyResult { 309 | let encoder = get_encoder()?; 310 | let key = GenericArray::clone_from_slice(secret_key.as_ref()); 311 | let cipher = XSalsa20Poly1305::new(&key); 312 | Ok(Self { 313 | opus: encoder, 314 | cipher, 315 | sequence: 0, 316 | timestamp: 0, 317 | ssrc: 0, 318 | lite_nonce: 0, 319 | }) 320 | } 321 | 322 | fn encode_opus<'py>(&self, py: Python<'py>, buffer: &PyBytes) -> PyResult<&'py PyBytes> { 323 | let bytes = buffer.as_bytes(); 324 | if bytes.len() != 3840 { 325 | return Err(pyo3::exceptions::PyValueError::new_err( 326 | "byte length must be 3840 bytes", 327 | )); 328 | } 329 | 330 | let as_i16: &[i16] = 331 | unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const i16, bytes.len() / 2) }; 332 | 333 | let mut output = [0u8; 2000]; 334 | match self.opus.encode(&as_i16, &mut output) { 335 | Ok(size) => Ok(PyBytes::new(py, &output[..size])), 336 | Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(e.to_string())), 337 | } 338 | } 339 | 340 | fn encrypt<'py>( 341 | &self, 342 | py: Python<'py>, 343 | nonce: &PyBytes, 344 | buffer: &PyBytes, 345 | ) -> PyResult<&'py PyBytes> { 346 | let nonce = GenericArray::from_slice(nonce.as_bytes()); 347 | match self.cipher.encrypt(nonce, buffer.as_bytes()) { 348 | Ok(text) => Ok(PyBytes::new(py, text.as_slice())), 349 | Err(_) => Err(pyo3::exceptions::PyRuntimeError::new_err( 350 | "Could not encrypt for whatever reason", 351 | )), 352 | } 353 | } 354 | 355 | fn prepare_packet<'py>(&mut self, py: Python<'py>, buffer: &PyBytes) -> PyResult<&'py PyBytes> { 356 | let bytes = buffer.as_bytes(); 357 | if bytes.len() != 3840 { 358 | return Err(pyo3::exceptions::PyValueError::new_err( 359 | "byte length must be 3840 bytes", 360 | )); 361 | } 362 | 363 | let pcm: &[i16] = 364 | unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const i16, bytes.len() / 2) }; 365 | 366 | let mut output = [0u8; player::MAX_BUFFER_SIZE]; 367 | let offset = match self.opus.encode(&pcm, &mut output[12..]) { 368 | Ok(size) => size, 369 | Err(e) => return Err(pyo3::exceptions::PyRuntimeError::new_err(e.to_string())), 370 | }; 371 | 372 | self.sequence = self.sequence.wrapping_add(1); 373 | output[0] = 0x80; 374 | output[1] = 0x78; 375 | output[2..4].copy_from_slice(&self.sequence.to_be_bytes()); 376 | output[4..8].copy_from_slice(&self.timestamp.to_be_bytes()); 377 | output[8..12].copy_from_slice(&self.ssrc.to_be_bytes()); 378 | 379 | let mut nonce = [0u8; 24]; 380 | nonce[0..4].copy_from_slice(&self.lite_nonce.to_be_bytes()); 381 | let mut buffer = player::InPlaceBuffer::new(&mut output[12..], offset); 382 | if let Err(e) = 383 | self.cipher 384 | .encrypt_in_place(GenericArray::from_slice(&nonce), b"", &mut buffer) 385 | { 386 | return Err(pyo3::exceptions::PyRuntimeError::new_err(e.to_string())); 387 | } 388 | 389 | if let Err(e) = buffer.extend_from_slice(&nonce) { 390 | return Err(pyo3::exceptions::PyRuntimeError::new_err(e.to_string())); 391 | } 392 | 393 | self.lite_nonce = self.lite_nonce.wrapping_add(1); 394 | self.timestamp = self.timestamp.wrapping_add(player::SAMPLES_PER_FRAME); 395 | let size = buffer.len(); 396 | Ok(PyBytes::new(py, &output[0..size])) 397 | } 398 | } 399 | 400 | #[pymodule] 401 | fn native_voice(py: Python, m: &PyModule) -> PyResult<()> { 402 | m.add_class::()?; 403 | m.add_class::()?; 404 | m.add_class::()?; 405 | m.add("ReconnectError", py.get_type::())?; 406 | m.add("ConnectionError", py.get_type::())?; 407 | m.add("ConnectionClosed", py.get_type::())?; 408 | Ok(()) 409 | } 410 | -------------------------------------------------------------------------------- /native_voice/src/payloads.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | use serde_json::value::RawValue; 3 | 4 | use std::{str::FromStr, time::{SystemTime, UNIX_EPOCH, Instant}}; 5 | use crate::error::{custom_error, ProtocolError}; 6 | 7 | // Static typed models to convert to 8 | // A lot of boilerplate lol 9 | 10 | pub struct Opcode; 11 | 12 | impl Opcode { 13 | pub const IDENTIFY: u8 = 0; 14 | pub const SELECT_PROTOCOL: u8 = 1; 15 | pub const READY: u8 = 2; 16 | pub const HEARTBEAT: u8 = 3; 17 | pub const SESSION_DESCRIPTION: u8 = 4; 18 | pub const SPEAKING: u8 = 5; 19 | pub const HEARTBEAT_ACK: u8 = 6; 20 | pub const RESUME: u8 = 7; 21 | pub const HELLO: u8 = 8; 22 | pub const RESUMED: u8 = 9; 23 | pub const CLIENT_CONNECT: u8 = 12; 24 | pub const CLIENT_DISCONNECT: u8 = 13; 25 | } 26 | 27 | // These are sent 28 | 29 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 30 | pub struct ResumeInfo { 31 | pub token: String, 32 | pub server_id: String, 33 | pub session_id: String, 34 | } 35 | 36 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 37 | pub struct Resume { 38 | pub op: u8, 39 | pub d: ResumeInfo, 40 | } 41 | 42 | impl Resume { 43 | pub fn new(info: ResumeInfo) -> Self { 44 | Self { 45 | op: Opcode::RESUME, 46 | d: info 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 52 | pub struct IdentifyInfo { 53 | pub server_id: String, 54 | pub user_id: String, 55 | pub session_id: String, 56 | pub token: String, 57 | } 58 | 59 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 60 | pub struct Identify { 61 | pub op: u8, 62 | pub d: IdentifyInfo, 63 | } 64 | 65 | impl Identify { 66 | pub(crate) fn new(info: IdentifyInfo) -> Self { 67 | Self { 68 | op: Opcode::IDENTIFY, 69 | d: info, 70 | } 71 | } 72 | } 73 | 74 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 75 | pub struct SelectProtocolInfo { 76 | pub address: String, 77 | pub port: u16, 78 | pub mode: String, 79 | } 80 | 81 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 82 | pub struct SelectProtocolWrapper { 83 | pub protocol: String, 84 | pub data: SelectProtocolInfo, 85 | } 86 | 87 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 88 | pub struct SelectProtocol { 89 | pub op: u8, 90 | pub d: SelectProtocolWrapper, 91 | } 92 | 93 | impl SelectProtocol { 94 | pub fn new(info: SelectProtocolInfo) -> Self { 95 | Self { 96 | op: Opcode::SELECT_PROTOCOL, 97 | d: SelectProtocolWrapper { 98 | protocol: "udp".to_string(), 99 | data: info, 100 | } 101 | } 102 | } 103 | 104 | pub fn from_addr(address: String, port: u16, mode: EncryptionMode) -> Self { 105 | Self { 106 | op: Opcode::SELECT_PROTOCOL, 107 | d: SelectProtocolWrapper { 108 | protocol: "udp".to_string(), 109 | data: SelectProtocolInfo { 110 | address, 111 | port, 112 | mode: mode.into(), 113 | }, 114 | } 115 | } 116 | } 117 | } 118 | 119 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 120 | pub struct Heartbeat { 121 | op: u8, 122 | d: u64, 123 | } 124 | 125 | impl Heartbeat { 126 | pub fn new(instant: Instant) -> Self { 127 | Self { 128 | op: Opcode::HEARTBEAT, 129 | d: instant.elapsed().as_millis() as u64, 130 | } 131 | } 132 | 133 | pub fn now() -> Self { 134 | let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards"); 135 | Self { 136 | op: Opcode::HEARTBEAT, 137 | d: now.as_millis() as u64, 138 | } 139 | } 140 | } 141 | 142 | // These can be received and sent 143 | 144 | #[derive(Debug, Clone, Eq, Hash, PartialEq)] 145 | pub struct SpeakingFlags { 146 | value: u8, 147 | } 148 | 149 | impl Default for SpeakingFlags { 150 | fn default() -> Self { 151 | Self { value: 0 } 152 | } 153 | } 154 | 155 | impl SpeakingFlags { 156 | pub const MICROPHONE: u8 = 1 << 0; 157 | pub const SOUNDSHARE: u8 = 1 << 1; 158 | pub const PRIORITY: u8 = 1 << 2; 159 | 160 | pub fn new(value: u8) -> Self { 161 | Self { 162 | value 163 | } 164 | } 165 | 166 | pub fn off() -> Self { 167 | Self { 168 | value: 0, 169 | } 170 | } 171 | 172 | pub fn microphone() -> Self { 173 | Self { 174 | value: Self::MICROPHONE, 175 | } 176 | } 177 | 178 | pub fn soundshare() -> Self { 179 | Self { 180 | value: Self::SOUNDSHARE, 181 | } 182 | } 183 | 184 | pub fn priority() -> Self { 185 | Self { 186 | value: Self::PRIORITY, 187 | } 188 | } 189 | 190 | pub fn toggle(&mut self, value: u8) -> &mut Self { 191 | self.value |= value; 192 | self 193 | } 194 | } 195 | 196 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 197 | pub struct SpeakingInfo { 198 | speaking: u8, 199 | delay: u8, 200 | } 201 | 202 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 203 | pub struct Speaking { 204 | op: u8, 205 | d: SpeakingInfo, 206 | } 207 | 208 | impl Speaking { 209 | pub fn new(flags: SpeakingFlags) -> Self { 210 | Self { 211 | op: Opcode::SPEAKING, 212 | d: SpeakingInfo { 213 | delay: 0, 214 | speaking: flags.value, 215 | } 216 | } 217 | } 218 | } 219 | 220 | // These are receive only 221 | 222 | #[derive(Debug, Serialize, Deserialize)] 223 | pub struct RawReceivedPayload<'a> { 224 | pub op: u8, 225 | #[serde(borrow)] 226 | pub d: &'a RawValue, 227 | } 228 | 229 | // This just has a data of null, so ignore it 230 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 231 | pub struct Resumed; 232 | 233 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 234 | pub struct HeartbeatAck(u64); 235 | 236 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 237 | pub struct SessionDescription { 238 | pub mode: String, 239 | pub secret_key: [u8; 32], 240 | } 241 | 242 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 243 | pub struct Ready { 244 | pub ssrc: u32, 245 | pub ip: String, 246 | pub port: u16, 247 | pub modes: Vec, 248 | #[serde(skip)] 249 | heartbeat_interval: u16, 250 | } 251 | 252 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 253 | pub struct Hello { 254 | pub heartbeat_interval: f64, 255 | } 256 | 257 | /// These are encryption modes ordered by priority 258 | #[derive(PartialOrd, Ord, Eq, PartialEq, Copy, Clone)] 259 | pub enum EncryptionMode { 260 | XSalsa20Poly1305 = 0, 261 | XSalsa20Poly1305Suffix = 1, 262 | XSalsa20Poly1305Lite = 2, 263 | } 264 | 265 | impl Default for EncryptionMode { 266 | fn default() -> Self { 267 | EncryptionMode::XSalsa20Poly1305 268 | } 269 | } 270 | 271 | impl Into for EncryptionMode { 272 | fn into(self) -> String { 273 | match self { 274 | EncryptionMode::XSalsa20Poly1305 => "xsalsa20_poly1305".to_owned(), 275 | EncryptionMode::XSalsa20Poly1305Suffix => "xsalsa20_poly1305_suffix".to_owned(), 276 | EncryptionMode::XSalsa20Poly1305Lite => "xsalsa20_poly1305_lite".to_owned(), 277 | } 278 | } 279 | } 280 | 281 | impl FromStr for EncryptionMode { 282 | type Err = ProtocolError; 283 | 284 | fn from_str(s: &str) -> Result { 285 | match s { 286 | "xsalsa20_poly1305_lite" => Ok(EncryptionMode::XSalsa20Poly1305Lite), 287 | "xsalsa20_poly1305_suffix" => Ok(EncryptionMode::XSalsa20Poly1305Suffix), 288 | "xsalsa20_poly1305" => Ok(EncryptionMode::XSalsa20Poly1305), 289 | _ => Err(custom_error("unknown encryption mode")) 290 | } 291 | } 292 | } 293 | 294 | impl Ready { 295 | pub fn get_encryption_mode(&self) -> Result { 296 | self.modes.iter() 297 | .map(|s| s.parse::()) 298 | .filter_map(Result::ok) 299 | .max() 300 | .ok_or(custom_error("No best supported encryption mode found")) 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /native_voice/src/player.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ProtocolError; 2 | use crate::payloads::{EncryptionMode, SpeakingFlags}; 3 | use crate::protocol::DiscordVoiceProtocol; 4 | use crate::state::PlayingState; 5 | 6 | use parking_lot::Mutex; 7 | use std::io::ErrorKind; 8 | use std::io::Read; 9 | use std::net::UdpSocket; 10 | use std::sync::Arc; 11 | use std::thread; 12 | use std::time::{Duration, Instant}; 13 | 14 | use std::process::{Child, Command, Stdio}; 15 | 16 | use rand::RngCore; 17 | use xsalsa20poly1305::aead::Buffer; 18 | use xsalsa20poly1305::aead::{generic_array::GenericArray, AeadInPlace, NewAead}; 19 | use xsalsa20poly1305::XSalsa20Poly1305; 20 | 21 | pub const SAMPLING_RATE: u16 = 48000; 22 | pub const CHANNELS: u16 = 2; 23 | pub const FRAME_LENGTH: u16 = 20; 24 | pub const SAMPLE_SIZE: u16 = 4; // 16-bits / 8 * channels 25 | pub const SAMPLES_PER_FRAME: u32 = ((SAMPLING_RATE / 1000) * FRAME_LENGTH) as u32; 26 | pub const FRAME_SIZE: u32 = SAMPLES_PER_FRAME * SAMPLE_SIZE as u32; 27 | 28 | pub enum AudioType { 29 | Opus, 30 | Pcm, 31 | } 32 | 33 | pub trait AudioSource: Send { 34 | /// The audio type of this source 35 | /// If AudioType is Opus then the data will be passed as-is to discord 36 | fn get_type(&self) -> AudioType { 37 | AudioType::Pcm 38 | } 39 | 40 | /// Reads a frame of audio (20ms 16-bit stereo 48000Hz) 41 | /// Returns Some(num) where num is number of frames written to the buffer 42 | /// Returns None if the audio source has terminated 43 | /// This is only called if the AudioType is PCM. 44 | fn read_pcm_frame(&mut self, _buffer: &mut [i16]) -> Option { 45 | unimplemented!() 46 | } 47 | 48 | /// Same as read_pcm_frame except for opus encoded audio 49 | fn read_opus_frame(&mut self, _buffer: &mut [u8]) -> Option { 50 | unimplemented!() 51 | } 52 | } 53 | 54 | pub struct FFmpegPCMAudio { 55 | process: Child, 56 | } 57 | 58 | impl FFmpegPCMAudio { 59 | pub fn new(input: &str) -> Result { 60 | let process = Command::new("ffmpeg") 61 | .args(&[ 62 | "-reconnect", 63 | "1", 64 | "-reconnect_streamed", 65 | "1", 66 | "-reconnect_delay_max", 67 | "5", 68 | ]) 69 | .arg("-i") 70 | .arg(&input) 71 | .args(&[ 72 | "-f", 73 | "s16le", 74 | "-ar", 75 | "48000", 76 | "-ac", 77 | "2", 78 | "-loglevel", 79 | "panic", 80 | "pipe:1", 81 | ]) 82 | .stdout(Stdio::piped()) 83 | .stderr(Stdio::null()) // no output lol 84 | .spawn()?; 85 | Ok(Self { process }) 86 | } 87 | } 88 | 89 | impl AudioSource for FFmpegPCMAudio { 90 | fn read_pcm_frame(&mut self, buffer: &mut [i16]) -> Option { 91 | let stdout = self.process.stdout.as_mut().unwrap(); 92 | let bytes = unsafe { 93 | std::slice::from_raw_parts_mut(buffer.as_mut_ptr() as *mut u8, buffer.len() * 2) 94 | }; 95 | stdout.read_exact(bytes).map(|_| buffer.len()).ok() 96 | } 97 | } 98 | 99 | impl Drop for FFmpegPCMAudio { 100 | fn drop(&mut self) { 101 | if let Err(e) = self.process.kill() { 102 | println!("Could not kill ffmpeg process: {:?}", e); 103 | } 104 | } 105 | } 106 | 107 | /// In order to efficiently manage a buffer we need to prepend some bytes during 108 | /// packet creation, so a specific offset of that buffer has to modified 109 | /// This type is a wrapper that allows me to do that. 110 | pub struct InPlaceBuffer<'a> { 111 | slice: &'a mut [u8], 112 | length: usize, 113 | capacity: usize, 114 | } 115 | 116 | impl InPlaceBuffer<'_> { 117 | pub fn new<'a>(slice: &'a mut [u8], length: usize) -> InPlaceBuffer<'a> { 118 | InPlaceBuffer { 119 | capacity: slice.len(), 120 | slice, 121 | length, 122 | } 123 | } 124 | } 125 | 126 | impl<'a> AsRef<[u8]> for InPlaceBuffer<'a> { 127 | fn as_ref(&self) -> &[u8] { 128 | &self.slice[..self.length] 129 | } 130 | } 131 | 132 | impl<'a> AsMut<[u8]> for InPlaceBuffer<'a> { 133 | fn as_mut(&mut self) -> &mut [u8] { 134 | &mut self.slice[..self.length] 135 | } 136 | } 137 | 138 | impl Buffer for InPlaceBuffer<'_> { 139 | fn extend_from_slice(&mut self, other: &[u8]) -> Result<(), xsalsa20poly1305::aead::Error> { 140 | if self.length + other.len() > self.capacity { 141 | Err(xsalsa20poly1305::aead::Error) 142 | } else { 143 | self.slice[self.length..self.length + other.len()].copy_from_slice(&other); 144 | self.length += other.len(); 145 | Ok(()) 146 | } 147 | } 148 | 149 | fn truncate(&mut self, len: usize) { 150 | // No need to drop since u8 are basic types 151 | if len < self.length { 152 | for i in self.slice[len..].iter_mut() { 153 | *i = 0; 154 | } 155 | self.length = len; 156 | } 157 | } 158 | 159 | fn len(&self) -> usize { 160 | self.length 161 | } 162 | 163 | fn is_empty(&self) -> bool { 164 | self.slice.is_empty() 165 | } 166 | } 167 | 168 | /// The maximum buffer size. 1275 is the maximum size of an ideal Opus frame packet. 169 | /// 24 bytes is for the nonce when constructing the audio packet 170 | /// 12 bytes is for the header. 171 | /// 24 bytes for the xsalsa20poly1305 nonce (again) 172 | /// 16 bytes for the xsalsa20poly1305 tag 173 | /// 12 extra bytes of space 174 | pub const MAX_BUFFER_SIZE: usize = 1275 + 24 + 12 + 24 + 16 + 12; 175 | pub const BUFFER_OFFSET: usize = 12; 176 | type PacketBuffer = [u8; MAX_BUFFER_SIZE]; 177 | 178 | struct AudioEncoder { 179 | opus: audiopus::coder::Encoder, 180 | cipher: XSalsa20Poly1305, 181 | sequence: u16, 182 | timestamp: u32, 183 | lite_nonce: u32, 184 | ssrc: u32, 185 | pcm_buffer: [i16; 1920], 186 | // It's a re-used buffer that is used for multiple things 187 | // 1) The opus encoding result goes here 188 | // 2) The cipher is done in-place 189 | // 3) The final packet to send is through this buffer as well 190 | buffer: PacketBuffer, 191 | encrypter: fn( 192 | &XSalsa20Poly1305, 193 | u32, 194 | &[u8], 195 | &mut dyn Buffer, 196 | ) -> Result<(), xsalsa20poly1305::aead::Error>, 197 | } 198 | 199 | fn encrypt_xsalsa20_poly1305( 200 | cipher: &XSalsa20Poly1305, 201 | _lite: u32, 202 | header: &[u8], 203 | data: &mut dyn Buffer, 204 | ) -> Result<(), xsalsa20poly1305::aead::Error> { 205 | let mut nonce: [u8; 24] = [0; 24]; 206 | nonce[0..12].copy_from_slice(&header); 207 | 208 | cipher.encrypt_in_place(GenericArray::from_slice(&nonce), b"", data)?; 209 | data.extend_from_slice(&nonce)?; 210 | Ok(()) 211 | } 212 | 213 | fn encrypt_xsalsa20_poly1305_suffix( 214 | cipher: &XSalsa20Poly1305, 215 | _lite: u32, 216 | _header: &[u8], 217 | data: &mut dyn Buffer, 218 | ) -> Result<(), xsalsa20poly1305::aead::Error> { 219 | let mut nonce: [u8; 24] = [0; 24]; 220 | rand::thread_rng().fill_bytes(&mut nonce); 221 | 222 | cipher.encrypt_in_place(GenericArray::from_slice(&nonce), b"", data)?; 223 | data.extend_from_slice(&nonce)?; 224 | Ok(()) 225 | } 226 | 227 | fn encrypt_xsalsa20_poly1305_lite( 228 | cipher: &XSalsa20Poly1305, 229 | lite: u32, 230 | _header: &[u8], 231 | data: &mut dyn Buffer, 232 | ) -> Result<(), xsalsa20poly1305::aead::Error> { 233 | let mut nonce: [u8; 24] = [0; 24]; 234 | nonce[0..4].copy_from_slice(&lite.to_be_bytes()); 235 | 236 | cipher.encrypt_in_place(GenericArray::from_slice(&nonce), b"", data)?; 237 | data.extend_from_slice(&nonce[0..4])?; 238 | Ok(()) 239 | } 240 | 241 | impl AudioEncoder { 242 | fn from_protocol(protocol: &DiscordVoiceProtocol) -> Result { 243 | let mut encoder = audiopus::coder::Encoder::new( 244 | audiopus::SampleRate::Hz48000, 245 | audiopus::Channels::Stereo, 246 | audiopus::Application::Audio, 247 | )?; 248 | 249 | encoder.set_bitrate(audiopus::Bitrate::BitsPerSecond(128000))?; 250 | encoder.enable_inband_fec()?; 251 | encoder.set_packet_loss_perc(15)?; 252 | encoder.set_bandwidth(audiopus::Bandwidth::Fullband)?; 253 | encoder.set_signal(audiopus::Signal::Auto)?; 254 | 255 | let key = GenericArray::clone_from_slice(&protocol.secret_key); 256 | let cipher = XSalsa20Poly1305::new(&key); 257 | 258 | let encrypter = match &protocol.encryption { 259 | EncryptionMode::XSalsa20Poly1305 => encrypt_xsalsa20_poly1305, 260 | EncryptionMode::XSalsa20Poly1305Suffix => encrypt_xsalsa20_poly1305_suffix, 261 | EncryptionMode::XSalsa20Poly1305Lite => encrypt_xsalsa20_poly1305_lite, 262 | }; 263 | 264 | Ok(Self { 265 | opus: encoder, 266 | cipher, 267 | encrypter, 268 | sequence: 0, 269 | timestamp: 0, 270 | lite_nonce: 0, 271 | ssrc: protocol.ssrc, 272 | pcm_buffer: [0i16; 1920], 273 | buffer: [0; MAX_BUFFER_SIZE], 274 | }) 275 | } 276 | 277 | /// Formulates the audio packet. 278 | /// By the time this function is called, the buffer should have the opus data 279 | /// already loaded at buffer[BUFFER_OFFSET..] 280 | /// Takes everything after BUFFER_OFFSET + `size` and encrypts it 281 | fn prepare_packet(&mut self, size: usize) -> Result { 282 | let mut header = [0u8; BUFFER_OFFSET]; 283 | header[0] = 0x80; 284 | header[1] = 0x78; 285 | header[2..4].copy_from_slice(&self.sequence.to_be_bytes()); 286 | header[4..8].copy_from_slice(&self.timestamp.to_be_bytes()); 287 | header[8..BUFFER_OFFSET].copy_from_slice(&self.ssrc.to_be_bytes()); 288 | self.buffer[0..BUFFER_OFFSET].copy_from_slice(&header); 289 | 290 | let mut buffer = InPlaceBuffer::new(&mut self.buffer[BUFFER_OFFSET..], size); 291 | (self.encrypter)(&self.cipher, self.lite_nonce, &header, &mut buffer)?; 292 | self.lite_nonce = self.lite_nonce.wrapping_add(1); 293 | Ok(buffer.len()) 294 | } 295 | 296 | fn encode_pcm_buffer(&mut self) -> Result { 297 | self.opus.encode(&self.pcm_buffer, &mut self.buffer[BUFFER_OFFSET..]) 298 | } 299 | 300 | /// Sends already opus encoded data over the wire 301 | fn send_opus_packet( 302 | &mut self, 303 | socket: &UdpSocket, 304 | addr: &std::net::SocketAddr, 305 | size: usize, 306 | ) -> Result<(), ProtocolError> { 307 | self.sequence = self.sequence.wrapping_add(1); 308 | let size = self.prepare_packet(size)?; 309 | // println!("Sending buffer: {:?}", &self.buffer[0..size]); 310 | match socket.send_to(&self.buffer[0..BUFFER_OFFSET+size], addr) { 311 | Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => { 312 | println!( 313 | "A packet has been dropped (seq: {}, timestamp: {})", 314 | &self.sequence, &self.timestamp 315 | ); 316 | return Ok(()); 317 | } 318 | Err(e) => return Err(ProtocolError::from(e)), 319 | _ => {} 320 | }; 321 | 322 | self.timestamp = self.timestamp.wrapping_add(SAMPLES_PER_FRAME); 323 | Ok(()) 324 | } 325 | } 326 | 327 | type Protocol = Arc>; 328 | type Source = Arc>>; 329 | 330 | #[allow(dead_code)] 331 | pub struct AudioPlayer { 332 | thread: thread::JoinHandle<()>, 333 | protocol: Protocol, 334 | state: Arc, 335 | source: Source, 336 | } 337 | 338 | fn audio_play_loop( 339 | protocol: &Protocol, 340 | state: &Arc, 341 | source: &Source, 342 | ) -> Result<(), ProtocolError> { 343 | let mut next_iteration = Instant::now(); 344 | 345 | let (mut encoder, mut socket) = { 346 | let mut proto = protocol.lock(); 347 | proto.speaking(SpeakingFlags::microphone())?; 348 | (AudioEncoder::from_protocol(&*proto)?, proto.clone_socket()?) 349 | }; 350 | 351 | let addr = socket.peer_addr()?; 352 | 353 | loop { 354 | if state.is_finished() { 355 | break; 356 | } 357 | 358 | if state.is_paused() { 359 | // Wait until we're no longer paused 360 | state.wait_until_not_paused(); 361 | continue; 362 | } 363 | 364 | if state.is_disconnected() { 365 | // Wait until we're connected again to reset our state 366 | state.wait_until_connected(); 367 | next_iteration = Instant::now(); 368 | 369 | let proto = protocol.lock(); 370 | encoder = AudioEncoder::from_protocol(&*proto)?; 371 | socket = proto.clone_socket()?; 372 | } 373 | 374 | next_iteration += Duration::from_millis(20); 375 | let buffer_size = { 376 | let mut aud = source.lock(); 377 | match aud.get_type() { 378 | AudioType::Opus => aud.read_opus_frame(&mut encoder.buffer[BUFFER_OFFSET..]), 379 | AudioType::Pcm => { 380 | if let Some(_) = aud.read_pcm_frame(&mut encoder.pcm_buffer) { 381 | // println!("Read {} bytes", &num); 382 | match encoder.encode_pcm_buffer() { 383 | Ok(bytes) => { 384 | // println!("Encoded {} bytes", &bytes); 385 | Some(bytes) 386 | } 387 | Err(e) => { 388 | println!("Error encoding bytes: {:?}", &e); 389 | return Err(e.into()); 390 | } 391 | } 392 | } else { 393 | None 394 | } 395 | } 396 | } 397 | }; 398 | 399 | if let Some(size) = buffer_size { 400 | if size != 0 { 401 | encoder.send_opus_packet(&socket, &addr, size)?; 402 | let now = Instant::now(); 403 | next_iteration = next_iteration.max(now); 404 | thread::sleep(next_iteration - now); 405 | } 406 | } else { 407 | state.finished(); 408 | } 409 | } 410 | 411 | Ok(()) 412 | } 413 | 414 | impl AudioPlayer { 415 | pub fn new(after: After, protocol: Protocol, source: Source) -> Self 416 | where 417 | After: FnOnce(Option) -> (), 418 | After: Send + 'static, 419 | { 420 | let state = { 421 | let guard = protocol.lock(); 422 | guard.clone_state() 423 | }; 424 | state.connected(); 425 | 426 | Self { 427 | protocol: Arc::clone(&protocol), 428 | state: Arc::clone(&state), 429 | source: Arc::clone(&source), 430 | thread: thread::spawn(move || { 431 | let mut current_error = None; 432 | if let Err(e) = audio_play_loop(&protocol, &state, &source) { 433 | current_error = Some(e); 434 | } 435 | { 436 | let mut proto = protocol.lock(); 437 | // ignore the error 438 | let _ = proto.speaking(SpeakingFlags::off()); 439 | } 440 | after(current_error); 441 | }), 442 | } 443 | } 444 | 445 | pub fn pause(&self) { 446 | self.state.paused(); 447 | } 448 | 449 | pub fn resume(&self) { 450 | self.state.playing(); 451 | } 452 | 453 | pub fn stop(&self) { 454 | self.state.finished() 455 | } 456 | 457 | pub fn is_paused(&self) -> bool { 458 | self.state.is_paused() 459 | } 460 | 461 | pub fn is_playing(&self) -> bool { 462 | self.state.is_playing() 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /native_voice/src/protocol.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use tungstenite::error::Error as TungError; 4 | use tungstenite::protocol::{frame::coding::CloseCode, frame::CloseFrame, WebSocket}; 5 | use tungstenite::Message; 6 | 7 | use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream, UdpSocket}; 8 | use std::str::FromStr; 9 | use std::sync::Arc; 10 | use std::time::Instant; 11 | 12 | use std::io::ErrorKind; 13 | 14 | use native_tls::{TlsConnector, TlsStream}; 15 | 16 | use crate::error::*; 17 | use crate::payloads::*; 18 | use crate::state::PlayingState; 19 | 20 | pub struct DiscordVoiceProtocol { 21 | pub endpoint: String, 22 | pub endpoint_ip: String, 23 | user_id: String, 24 | server_id: String, 25 | pub session_id: String, 26 | pub token: String, 27 | pub recent_acks: std::collections::VecDeque, 28 | ws: WebSocket>, 29 | close_code: u16, 30 | state: Arc, 31 | socket: Option, 32 | pub port: u16, 33 | heartbeat_interval: u64, 34 | pub last_heartbeat: Instant, 35 | pub ssrc: u32, 36 | pub encryption: EncryptionMode, 37 | pub secret_key: [u8; 32], 38 | } 39 | 40 | pub struct ProtocolBuilder { 41 | endpoint: String, 42 | user_id: String, 43 | server_id: String, 44 | session_id: String, 45 | token: String, 46 | } 47 | 48 | impl ProtocolBuilder { 49 | pub fn new(endpoint: String) -> Self { 50 | Self { 51 | endpoint, 52 | user_id: String::new(), 53 | server_id: String::new(), 54 | session_id: String::new(), 55 | token: String::new(), 56 | } 57 | } 58 | 59 | pub fn user(&mut self, user_id: String) -> &mut Self { 60 | self.user_id = user_id; 61 | self 62 | } 63 | 64 | pub fn server(&mut self, server_id: String) -> &mut Self { 65 | self.server_id = server_id; 66 | self 67 | } 68 | 69 | pub fn session(&mut self, session_id: String) -> &mut Self { 70 | self.session_id = session_id; 71 | self 72 | } 73 | 74 | pub fn auth(&mut self, token: String) -> &mut Self { 75 | self.token = token; 76 | self 77 | } 78 | 79 | pub fn connect(self) -> Result { 80 | let ws = { 81 | let connector = TlsConnector::new()?; 82 | let stream = TcpStream::connect((self.endpoint.as_str(), 443))?; 83 | let stream = connector.connect(&self.endpoint, stream)?; 84 | let mut url = String::from("wss://"); 85 | url.push_str(self.endpoint.as_str()); 86 | url.push_str("/?v=4"); 87 | match tungstenite::client::client(&url, stream) { 88 | Ok((ws, _)) => ws, 89 | Err(e) => return Err(custom_error(e.to_string().as_str())), 90 | } 91 | }; 92 | 93 | Ok(DiscordVoiceProtocol { 94 | endpoint: self.endpoint, 95 | user_id: self.user_id, 96 | server_id: self.server_id, 97 | session_id: self.session_id, 98 | token: self.token, 99 | recent_acks: std::collections::VecDeque::with_capacity(20), 100 | close_code: 0, 101 | ws, 102 | socket: None, 103 | heartbeat_interval: std::u64::MAX, 104 | port: 0, 105 | ssrc: 0, 106 | endpoint_ip: String::default(), 107 | encryption: EncryptionMode::default(), 108 | last_heartbeat: Instant::now(), 109 | secret_key: [0; 32], 110 | state: Arc::new(PlayingState::default()), 111 | }) 112 | } 113 | } 114 | 115 | impl DiscordVoiceProtocol { 116 | pub fn clone_socket(&self) -> Result { 117 | match &self.socket { 118 | Some(ref socket) => Ok(socket.try_clone()?), 119 | None => Err(custom_error("No socket found")), 120 | } 121 | } 122 | 123 | pub fn clone_state(&self) -> Arc { 124 | Arc::clone(&self.state) 125 | } 126 | 127 | pub fn finish_flow(&mut self, resume: bool) -> Result<(), ProtocolError> { 128 | // get the op HELLO 129 | self.poll()?; 130 | if resume { 131 | self.resume()?; 132 | } else { 133 | self.identify()?; 134 | } 135 | 136 | while self.secret_key.iter().all(|&c| c == 0) { 137 | self.poll()?; 138 | } 139 | Ok(()) 140 | } 141 | 142 | pub fn close(&mut self, code: u16) -> Result<(), ProtocolError> { 143 | self.state.disconnected(); 144 | self.close_code = code; 145 | self.ws.close(Some(CloseFrame { 146 | code: CloseCode::from(code), 147 | reason: std::borrow::Cow::Owned("closing connection".to_string()), 148 | }))?; 149 | Ok(()) 150 | } 151 | 152 | pub fn poll(&mut self) -> Result<(), ProtocolError> { 153 | if self.last_heartbeat.elapsed().as_millis() as u64 >= self.heartbeat_interval { 154 | self.heartbeat()?; 155 | } 156 | 157 | let msg = { 158 | match self.ws.read_message() { 159 | Err(TungError::Io(ref e)) 160 | if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => 161 | { 162 | // We'll just continue reading since we timed out? 163 | return Ok(()); 164 | } 165 | Err(e) => return Err(ProtocolError::from(e)), 166 | Ok(msg) => msg, 167 | } 168 | }; 169 | 170 | match msg { 171 | Message::Text(string) => { 172 | let payload: RawReceivedPayload = serde_json::from_str(string.as_str())?; 173 | 174 | match payload.op { 175 | Opcode::HELLO => { 176 | let payload: Hello = serde_json::from_str(payload.d.get())?; 177 | let interval = payload.heartbeat_interval as u64; 178 | self.heartbeat_interval = interval.min(5000); 179 | // Get the original stream 180 | let socket = self.ws.get_ref().get_ref(); 181 | socket.set_read_timeout(Some(std::time::Duration::from_millis(1000)))?; 182 | self.last_heartbeat = Instant::now(); 183 | } 184 | Opcode::READY => { 185 | let payload: Ready = serde_json::from_str(payload.d.get())?; 186 | self.handle_ready(payload)?; 187 | } 188 | Opcode::HEARTBEAT => { 189 | self.heartbeat()?; 190 | } 191 | Opcode::HEARTBEAT_ACK => { 192 | let now = Instant::now(); 193 | let delta = now.duration_since(self.last_heartbeat); 194 | if self.recent_acks.len() == 20 { 195 | self.recent_acks.pop_front(); 196 | } 197 | self.recent_acks.push_back(delta.as_secs_f64()); 198 | } 199 | Opcode::SESSION_DESCRIPTION => { 200 | let payload: SessionDescription = serde_json::from_str(payload.d.get())?; 201 | self.encryption = EncryptionMode::from_str(payload.mode.as_str())?; 202 | self.secret_key = payload.secret_key; 203 | self.state.connected(); 204 | } 205 | // The rest are unhandled for now 206 | _ => {} 207 | } 208 | } 209 | Message::Close(msg) => { 210 | if let Some(frame) = msg { 211 | self.close_code = u16::from(frame.code); 212 | } 213 | self.state.disconnected(); 214 | return Err(ProtocolError::Closed(self.close_code)); 215 | } 216 | _ => {} 217 | } 218 | 219 | Ok(()) 220 | } 221 | 222 | fn get_latency(&self) -> f64 { 223 | *self.recent_acks.back().unwrap_or(&f64::NAN) 224 | } 225 | 226 | fn get_average_latency(&self) -> f64 { 227 | if self.recent_acks.len() == 0 { 228 | f64::NAN 229 | } else { 230 | self.recent_acks.iter().sum::() / self.recent_acks.len() as f64 231 | } 232 | } 233 | 234 | fn heartbeat(&mut self) -> Result<(), ProtocolError> { 235 | let msg = Heartbeat::now(); 236 | self.ws 237 | .write_message(Message::text(serde_json::to_string(&msg)?))?; 238 | self.last_heartbeat = Instant::now(); 239 | Ok(()) 240 | } 241 | 242 | fn identify(&mut self) -> Result<(), ProtocolError> { 243 | let msg = Identify::new(IdentifyInfo { 244 | server_id: self.server_id.clone(), 245 | user_id: self.user_id.clone(), 246 | session_id: self.session_id.clone(), 247 | token: self.token.clone(), 248 | }); 249 | self.ws 250 | .write_message(Message::text(serde_json::to_string(&msg)?))?; 251 | Ok(()) 252 | } 253 | 254 | fn resume(&mut self) -> Result<(), ProtocolError> { 255 | let msg = Resume::new(ResumeInfo { 256 | token: self.token.clone(), 257 | server_id: self.server_id.clone(), 258 | session_id: self.session_id.clone(), 259 | }); 260 | self.ws 261 | .write_message(Message::text(serde_json::to_string(&msg)?))?; 262 | Ok(()) 263 | } 264 | 265 | fn handle_ready(&mut self, payload: Ready) -> Result<(), ProtocolError> { 266 | self.ssrc = payload.ssrc; 267 | self.port = payload.port; 268 | self.encryption = payload.get_encryption_mode()?; 269 | self.endpoint_ip = payload.ip; 270 | let addr = SocketAddr::new( 271 | IpAddr::V4(self.endpoint_ip.as_str().parse::()?), 272 | self.port, 273 | ); 274 | // I'm unsure why I have to explicitly bind with Rust 275 | let socket = UdpSocket::bind("0.0.0.0:0")?; 276 | socket.connect(&addr)?; 277 | self.socket = Some(socket); 278 | 279 | // attempt to do this up to 5 times 280 | let (ip, port) = { 281 | let mut retries = 0; 282 | loop { 283 | match self.udp_discovery() { 284 | Ok(x) => break x, 285 | Err(e) => { 286 | if retries < 5 { 287 | retries += 1; 288 | continue; 289 | } 290 | return Err(e); 291 | } 292 | } 293 | } 294 | }; 295 | 296 | // select protocol 297 | let to_send = SelectProtocol::from_addr(ip, port, self.encryption); 298 | self.ws 299 | .write_message(Message::text(serde_json::to_string(&to_send)?))?; 300 | Ok(()) 301 | } 302 | 303 | fn get_socket<'a>(&'a self) -> Result<&'a UdpSocket, ProtocolError> { 304 | match &self.socket { 305 | Some(s) => Ok(s), 306 | None => Err(custom_error("no socket found")), 307 | } 308 | } 309 | 310 | fn udp_discovery(&mut self) -> Result<(String, u16), ProtocolError> { 311 | let socket = self.get_socket()?; 312 | // Generate a packet 313 | let mut buffer: [u8; 70] = [0; 70]; 314 | buffer[0..2].copy_from_slice(&1u16.to_be_bytes()); // 1 = send 315 | buffer[2..4].copy_from_slice(&70u16.to_be_bytes()); // 70 = length 316 | buffer[4..8].copy_from_slice(&self.ssrc.to_be_bytes()); // the SSRC 317 | 318 | // rest of this is unused 319 | // let's send the packet 320 | socket.send(&buffer)?; 321 | 322 | // receive the new buffer 323 | let mut buffer: [u8; 70] = [0; 70]; 324 | socket.recv(&mut buffer)?; 325 | 326 | // The IP is surrounded by 4 leading bytes and ends on the first encounter of a null byte 327 | let ip_end = &buffer[4..] 328 | .iter() 329 | .position(|&b| b == 0) 330 | .ok_or_else(|| custom_error("could not find end of IP"))?; 331 | let ip: String = { 332 | let ip_slice = &buffer[4..4 + ip_end]; 333 | let as_str = std::str::from_utf8(ip_slice) 334 | .map_err(|_| custom_error("invalid IP found (not UTF-8"))?; 335 | String::from(as_str) 336 | }; 337 | // The port is the last 2 bytes in big endian 338 | // can't use regular slices with this API 339 | let port = u16::from_be_bytes([buffer[68], buffer[69]]); 340 | Ok((ip, port)) 341 | } 342 | 343 | pub fn speaking(&mut self, flags: SpeakingFlags) -> Result<(), ProtocolError> { 344 | let msg: Speaking = Speaking::new(flags); 345 | self.ws 346 | .write_message(Message::text(serde_json::to_string(&msg)?))?; 347 | Ok(()) 348 | } 349 | 350 | fn start_handshaking(&mut self) -> Result<(), ProtocolError> { 351 | Ok(()) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /native_voice/src/state.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use parking_lot::{Condvar, Mutex}; 3 | // use crossbeam_channel::{bounded, Sender, Receiver}; 4 | 5 | const DISCONNECTED: u8 = 0; 6 | const CONNECTED: u8 = 1; 7 | const PLAYING: u8 = 2; 8 | const PAUSED: u8 = 3; 9 | const FINISHED: u8 = 4; 10 | 11 | pub struct PlayingState { 12 | state: Mutex, 13 | cond: Condvar, 14 | } 15 | 16 | impl Default for PlayingState { 17 | fn default() -> Self { 18 | Self { 19 | state: Mutex::new(DISCONNECTED), 20 | cond: Condvar::new(), 21 | } 22 | } 23 | } 24 | 25 | impl PlayingState { 26 | pub fn is_disconnected(&self) -> bool { 27 | let value = self.state.lock(); 28 | *value == DISCONNECTED 29 | } 30 | 31 | pub fn is_connected(&self) -> bool { 32 | let value = self.state.lock(); 33 | *value == CONNECTED 34 | } 35 | 36 | pub fn is_playing(&self) -> bool { 37 | let value = self.state.lock(); 38 | *value == PLAYING 39 | } 40 | 41 | pub fn is_paused(&self) -> bool { 42 | let value = self.state.lock(); 43 | *value == PAUSED 44 | } 45 | 46 | pub fn is_finished(&self) -> bool { 47 | let value = self.state.lock(); 48 | *value == FINISHED 49 | } 50 | 51 | pub fn disconnected(&self) { 52 | let mut guard = self.state.lock(); 53 | *guard = DISCONNECTED; 54 | self.cond.notify_all(); 55 | } 56 | 57 | pub fn connected(&self) { 58 | let mut guard = self.state.lock(); 59 | *guard = CONNECTED; 60 | self.cond.notify_all(); 61 | } 62 | 63 | pub fn playing(&self) { 64 | let mut guard = self.state.lock(); 65 | *guard = PLAYING; 66 | self.cond.notify_all(); 67 | } 68 | 69 | pub fn paused(&self) { 70 | let mut guard = self.state.lock(); 71 | *guard = PAUSED; 72 | self.cond.notify_all(); 73 | } 74 | 75 | pub fn finished(&self) { 76 | let mut guard = self.state.lock(); 77 | *guard = FINISHED; 78 | self.cond.notify_all(); 79 | } 80 | 81 | fn wait_until_state(&self, state: u8) { 82 | let mut guard = self.state.lock(); 83 | while *guard != state { 84 | self.cond.wait(&mut guard); 85 | } 86 | } 87 | 88 | pub fn wait_until_not_paused(&self) { 89 | let mut guard = self.state.lock(); 90 | while *guard == PAUSED { 91 | self.cond.wait(&mut guard); 92 | } 93 | } 94 | 95 | pub fn wait_until_disconnected(&self) { 96 | self.wait_until_state(DISCONNECTED); 97 | } 98 | 99 | pub fn wait_until_connected(&self) { 100 | self.wait_until_state(CONNECTED); 101 | } 102 | 103 | pub fn wait_until_playing(&self) { 104 | self.wait_until_state(PLAYING); 105 | } 106 | 107 | pub fn wait_until_paused(&self) { 108 | self.wait_until_state(PAUSED); 109 | } 110 | 111 | pub fn wait_until_finished(&self) { 112 | self.wait_until_state(FINISHED); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['poetry-core>=1.0.0'] 3 | build-backend = 'poetry.core.masonry.api' 4 | 5 | 6 | [tool.poetry] 7 | name = 'Swish' 8 | version = '0.0.1' 9 | description = '' 10 | authors = [] 11 | 12 | 13 | [tool.poetry.dependencies] 14 | python = '^3.10' 15 | aiohttp = '~3.8.0' 16 | colorama = '~0.4.0' 17 | toml = '~0.10.0' 18 | typing_extensions = '~4.3.0' 19 | yt-dlp = '~2022.7.0' 20 | dacite = '~1.6.0' 21 | 'discord.py' = { git = 'https://github.com/Rapptz/discord.py' } 22 | 23 | # 'build' extras 24 | pyinstaller = { version = '*', optional = true } 25 | 26 | # 'dev' extras 27 | jishaku = { version = '*', optional = true } 28 | 29 | 30 | [tool.poetry.extras] 31 | build = ['pyinstaller'] 32 | dev = ['jishaku'] 33 | 34 | 35 | [tool.pyright] 36 | include = ['swish'] 37 | pythonVersion = '3.10' 38 | typeCheckingMode = 'strict' 39 | useLibraryCodeForTypes = true 40 | 41 | reportUnknownMemberType = false 42 | reportPrivateUsage = false 43 | reportImportCycles = false 44 | reportMissingTypeStubs = false 45 | reportUnknownArgumentType = false 46 | reportConstantRedefinition = false 47 | reportPrivateImportUsage = false 48 | -------------------------------------------------------------------------------- /swish.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | host = "127.0.0.1" 3 | port = 8000 4 | password = "helloworld!" 5 | 6 | [rotation] 7 | enabled = false 8 | method = "nanosecond-rotator" 9 | blocks = [] 10 | 11 | [search] 12 | max_results = 10 13 | 14 | [logging] 15 | path = "logs/" 16 | backup_count = 5 17 | max_bytes = 5242880 18 | 19 | [logging.levels] 20 | swish = "DEBUG" 21 | aiohttp = "NOTSET" 22 | -------------------------------------------------------------------------------- /swish/__init__.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | -------------------------------------------------------------------------------- /swish/app.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | 19 | from __future__ import annotations 20 | 21 | import base64 22 | import contextlib 23 | import functools 24 | import json 25 | import logging 26 | import os 27 | from typing import Any 28 | 29 | import aiohttp 30 | import aiohttp.web 31 | import yarl 32 | import yt_dlp 33 | 34 | from .config import CONFIG 35 | from .player import Player 36 | from .rotator import BanRotator, NanosecondRotator 37 | from .types.payloads import ReceivedPayload 38 | 39 | 40 | __all__ = ( 41 | 'App', 42 | ) 43 | 44 | 45 | LOG: logging.Logger = logging.getLogger('swish.app') 46 | 47 | 48 | class App(aiohttp.web.Application): 49 | 50 | def __init__(self) -> None: 51 | super().__init__() 52 | 53 | self._connections: list[aiohttp.web.WebSocketResponse] = [] 54 | 55 | self.add_routes( 56 | [ 57 | aiohttp.web.get('/', self.websocket_handler), 58 | aiohttp.web.get('/search', self.search_tracks), 59 | ] 60 | ) 61 | 62 | async def run(self) -> None: 63 | 64 | runner = aiohttp.web.AppRunner( 65 | app=self 66 | ) 67 | await runner.setup() 68 | 69 | host = CONFIG.server.host 70 | port = CONFIG.server.port 71 | 72 | site = aiohttp.web.TCPSite( 73 | runner=runner, 74 | host=host, 75 | port=port 76 | ) 77 | await site.start() 78 | 79 | LOG.info(f'Swish server started on {host}:{port}') 80 | 81 | # websocket handling 82 | 83 | async def websocket_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse: 84 | 85 | LOG.info(f'<{request.remote}> - Incoming websocket connection request.') 86 | 87 | websocket = aiohttp.web.WebSocketResponse() 88 | await websocket.prepare(request) 89 | 90 | user_agent: str | None = request.headers.get('User-Agent') 91 | if not user_agent: 92 | LOG.error(f'<{request.remote}> - Websocket connection failed due to missing \'User-Agent\' header.') 93 | await websocket.close(code=4000, message=b'Missing \'User-Agent\' header.') 94 | return websocket 95 | 96 | client_name = f'<{user_agent} ({request.remote})>' 97 | 98 | user_id: str | None = request.headers.get('User-Id') 99 | if not user_id: 100 | LOG.error(f'{client_name} - Websocket connection failed due to missing \'User-Id\' header.') 101 | await websocket.close(code=4000, message=b'Missing \'User-Id\' header.') 102 | return websocket 103 | 104 | password: str = CONFIG.server.password 105 | authorization: str | None = request.headers.get('Authorization') 106 | if password != authorization: 107 | LOG.error(f'{client_name} - Websocket connection failed due to mismatched \'Authorization\' header.') 108 | await websocket.close(code=4001, message=b'Authorization failed.') 109 | return websocket 110 | 111 | websocket['client_name'] = client_name 112 | websocket['user_agent'] = user_agent 113 | websocket['user_id'] = user_id 114 | websocket['app'] = self 115 | websocket['players'] = {} 116 | self._connections.append(websocket) 117 | 118 | LOG.info(f'{client_name} - Websocket connection established.') 119 | 120 | message: aiohttp.WSMessage 121 | async for message in websocket: 122 | 123 | try: 124 | payload: ReceivedPayload = message.json() 125 | except json.JSONDecodeError: 126 | LOG.error(f'{client_name} - Received payload with invalid JSON format.\nPayload: {message.data}') 127 | continue 128 | 129 | if 'op' not in payload: 130 | LOG.error(f'{client_name} - Received payload with missing \'op\' key.\nPayload: {payload}') 131 | continue 132 | if 'd' not in payload: 133 | LOG.error(f'{client_name} - Received payload with missing \'d\' key.\nPayload: {payload}') 134 | continue 135 | 136 | # op codes that don't require player should be handled here. 137 | # TODO: handle debug op 138 | 139 | guild_id: str | None = payload['d'].get('guild_id') 140 | if not guild_id: 141 | LOG.error(f'{client_name} - Received payload with missing \'guild_id\' data key. Payload: {payload}') 142 | continue 143 | 144 | player: Player | None = websocket['players'].get(guild_id) 145 | if not player: 146 | player = Player(websocket, guild_id) 147 | websocket['players'][guild_id] = player 148 | 149 | await player.handle_payload(payload) 150 | 151 | LOG.info(f'{client_name} - Websocket connection closed.') 152 | 153 | # TODO: destroy/disconnect all players 154 | self._connections.remove(websocket) 155 | return websocket 156 | 157 | # search handling 158 | 159 | @staticmethod 160 | def _encode_track_info(info: dict[str, Any], /) -> str: 161 | return base64.b64encode(json.dumps(info).encode()).decode() 162 | 163 | @staticmethod 164 | def _decode_track_id(_id: str, /) -> dict[str, Any]: 165 | return json.loads(base64.b64decode(_id).decode()) 166 | 167 | _SEARCH_OPTIONS: dict[str, Any] = { 168 | 'quiet': True, 169 | 'no_warnings': True, 170 | 'format': 'bestaudio[ext=webm][acodec=opus]/' 171 | 'bestaudio[ext=mp4][acodec=aac]/' 172 | 'bestvideo[ext=mp4][acodec=aac]/' 173 | 'best', 174 | 'restrictfilenames': False, 175 | 'ignoreerrors': True, 176 | 'logtostderr': False, 177 | 'noplaylist': False, 178 | 'nocheckcertificate': True, 179 | 'default_search': 'auto', 180 | 'source_address': '0.0.0.0', 181 | } 182 | 183 | _SOURCE_MAPPING: dict[str, str] = { 184 | 'youtube': f'ytsearch{CONFIG.search.max_results}:', 185 | 'soundcloud': f'scsearch{CONFIG.search.max_results}:', 186 | 'niconico': f'nicosearch{CONFIG.search.max_results}:', 187 | 'bilibili': f'bilisearch{CONFIG.search.max_results}:', 188 | 'none': '' 189 | } 190 | 191 | _ROTATOR_MAPPING: dict[str, type[NanosecondRotator] | type[BanRotator]] = { 192 | 'nanosecond-rotator': NanosecondRotator, 193 | 'ban-rotator': BanRotator 194 | } 195 | 196 | async def _ytdl_search(self, query: str, internal: bool) -> Any: 197 | 198 | self._SEARCH_OPTIONS['extract_flat'] = not internal 199 | if CONFIG.rotation.enabled: 200 | self._SEARCH_OPTIONS['source_address'] = self._ROTATOR_MAPPING[CONFIG.rotation.method].rotate() 201 | 202 | with yt_dlp.YoutubeDL(self._SEARCH_OPTIONS) as YTDL: 203 | with contextlib.redirect_stdout(open(os.devnull, 'w')): 204 | assert self._loop is not None 205 | _search: Any = await self._loop.run_in_executor( 206 | None, 207 | functools.partial(YTDL.extract_info, query, download=False) 208 | ) 209 | 210 | return YTDL.sanitize_info(_search) # type: ignore 211 | 212 | async def _get_playback_url(self, url: str) -> str: 213 | 214 | search = await self._ytdl_search(url, internal=True) 215 | return search['url'] 216 | 217 | async def _get_tracks(self, query: str) -> list[dict[str, Any]]: 218 | 219 | search = await self._ytdl_search(query, internal=False) 220 | 221 | entries = search.get('entries', [search]) 222 | tracks: list[dict[str, Any]] = [] 223 | 224 | for entry in entries: 225 | info: dict[str, Any] = { 226 | 'title': entry['title'], 227 | 'identifier': entry['id'], 228 | 'url': entry['url'], 229 | 'length': int(entry.get('duration') or 0 * 1000), 230 | 'author': entry.get('uploader', 'Unknown'), 231 | 'author_id': entry.get('channel_id', None), 232 | 'thumbnail': entry.get('thumbnails', [None])[0], 233 | 'is_live': entry.get('live_status', False), 234 | } 235 | tracks.append( 236 | { 237 | 'id': self._encode_track_info(info), 238 | 'info': info 239 | } 240 | ) 241 | 242 | return tracks 243 | 244 | async def search_tracks(self, request: aiohttp.web.Request) -> aiohttp.web.Response: 245 | 246 | query = request.query.get('query') 247 | if not query: 248 | return aiohttp.web.json_response({'error': 'Missing \'query\' query parameter.'}, status=400) 249 | 250 | source = request.query.get('source', 'youtube') 251 | if (url := yarl.URL(query)) and url.host and url.scheme: 252 | source = 'none' 253 | 254 | prefix = self._SOURCE_MAPPING.get(source) 255 | if prefix is None: 256 | return aiohttp.web.json_response({'error': 'Invalid \'source\' query parameter.'}, status=400) 257 | 258 | tracks = await self._get_tracks(f'{prefix}{query}') 259 | 260 | return aiohttp.web.json_response(tracks) 261 | -------------------------------------------------------------------------------- /swish/config.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | 19 | import dataclasses 20 | import sys 21 | from typing import Any, Literal 22 | 23 | import dacite 24 | import toml 25 | 26 | 27 | __all__ = ( 28 | 'CONFIG', 29 | ) 30 | 31 | 32 | DEFAULT_CONFIG: dict[str, Any] = { 33 | 'server': { 34 | 'host': '127.0.0.1', 35 | 'port': 8000, 36 | 'password': 'helloworld!' 37 | }, 38 | 'rotation': { 39 | 'enabled': False, 40 | 'method': 'nanosecond-rotator', 41 | 'blocks': [] 42 | }, 43 | 'search': { 44 | 'max_results': 10 45 | }, 46 | 'logging': { 47 | 'path': 'logs/', 48 | 'backup_count': 5, 49 | 'max_bytes': (2 ** 20) * 5, 50 | 'levels': { 51 | 'swish': 'DEBUG', 52 | 'aiohttp': 'NOTSET' 53 | } 54 | } 55 | } 56 | 57 | 58 | @dataclasses.dataclass 59 | class Server: 60 | host: str 61 | port: int 62 | password: str 63 | 64 | 65 | @dataclasses.dataclass 66 | class Rotation: 67 | enabled: bool 68 | method: Literal['nanosecond-rotator', 'ban-rotator'] 69 | blocks: list[str] 70 | 71 | 72 | @dataclasses.dataclass 73 | class Search: 74 | max_results: int 75 | 76 | 77 | @dataclasses.dataclass 78 | class LoggingLevels: 79 | swish: Literal['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'] 80 | aiohttp: Literal['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'] 81 | 82 | 83 | @dataclasses.dataclass 84 | class Logging: 85 | path: str 86 | backup_count: int 87 | max_bytes: int 88 | levels: LoggingLevels 89 | 90 | 91 | @dataclasses.dataclass 92 | class Config: 93 | server: Server 94 | rotation: Rotation 95 | search: Search 96 | logging: Logging 97 | 98 | 99 | def load_config() -> Config: 100 | 101 | try: 102 | return dacite.from_dict(Config, toml.load('swish.toml')) 103 | 104 | except (toml.TomlDecodeError, FileNotFoundError): 105 | 106 | with open('swish.toml', 'w') as fp: 107 | toml.dump(DEFAULT_CONFIG, fp) 108 | 109 | print('Could not find or parse swish.toml, using default configuration values.') 110 | return dacite.from_dict(Config, DEFAULT_CONFIG) 111 | 112 | except dacite.DaciteError as error: 113 | sys.exit(f'Your swish.toml configuration file is invalid: {str(error).capitalize()}.') 114 | 115 | 116 | CONFIG: Config = load_config() 117 | -------------------------------------------------------------------------------- /swish/logging.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | 19 | from __future__ import annotations 20 | 21 | import logging 22 | import logging.handlers 23 | import os 24 | 25 | import colorama 26 | 27 | from .config import CONFIG 28 | 29 | 30 | __all__ = ( 31 | 'setup_logging', 32 | ) 33 | 34 | 35 | class ColourFormatter(logging.Formatter): 36 | 37 | COLOURS: dict[int, str] = { 38 | logging.DEBUG: colorama.Fore.MAGENTA, 39 | logging.INFO: colorama.Fore.GREEN, 40 | logging.WARNING: colorama.Fore.YELLOW, 41 | logging.ERROR: colorama.Fore.RED, 42 | } 43 | 44 | def __init__(self, enabled: bool) -> None: 45 | 46 | self.enabled: bool = enabled 47 | 48 | if self.enabled: 49 | fmt = f'{colorama.Fore.CYAN}[%(asctime)s] {colorama.Style.RESET_ALL}' \ 50 | f'{colorama.Fore.LIGHTCYAN_EX}[%(name) 16s] {colorama.Style.RESET_ALL}' \ 51 | f'%(colour)s[%(levelname) 8s] {colorama.Style.RESET_ALL}' \ 52 | f'%(message)s' 53 | else: 54 | fmt = '[%(asctime)s] [%(name) 16s] [%(levelname) 8s] %(message)s' 55 | 56 | super().__init__( 57 | fmt=fmt, 58 | datefmt='%I:%M:%S %Y/%m/%d' 59 | ) 60 | 61 | def format(self, record: logging.LogRecord) -> str: 62 | record.colour = self.COLOURS[record.levelno] # type: ignore 63 | return super().format(record) 64 | 65 | 66 | def setup_logging() -> None: 67 | 68 | colorama.init(autoreset=True) 69 | 70 | loggers: dict[str, logging.Logger] = { 71 | 'swish': logging.getLogger('swish'), 72 | 'aiohttp': logging.getLogger('aiohttp'), 73 | } 74 | loggers['swish'].setLevel(CONFIG.logging.levels.swish) 75 | loggers['aiohttp'].setLevel(CONFIG.logging.levels.aiohttp) 76 | 77 | for name, logger in loggers.items(): 78 | 79 | path = CONFIG.logging.path 80 | 81 | if not os.path.exists(path): 82 | os.makedirs(path) 83 | 84 | # file handler 85 | file_handler = logging.handlers.RotatingFileHandler( 86 | filename=f'{path}{name}.log', 87 | mode='w', 88 | maxBytes=CONFIG.logging.max_bytes, 89 | backupCount=CONFIG.logging.backup_count, 90 | encoding='utf-8', 91 | ) 92 | file_handler.setFormatter(ColourFormatter(enabled=False)) 93 | logger.addHandler(file_handler) 94 | 95 | # stdout handler 96 | stream_handler = logging.StreamHandler() 97 | stream_handler.setFormatter(ColourFormatter(enabled=True)) 98 | logger.addHandler(stream_handler) 99 | -------------------------------------------------------------------------------- /swish/player.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | 19 | from __future__ import annotations 20 | 21 | import asyncio 22 | import logging 23 | from collections.abc import Callable 24 | from typing import Any, TYPE_CHECKING 25 | 26 | import aiohttp 27 | import aiohttp.web 28 | import discord.backoff 29 | from discord.ext.native_voice import native_voice # type: ignore 30 | 31 | from .types.payloads import ( 32 | PayloadHandlers, 33 | ReceivedPayload, 34 | SentPayloadOp, 35 | VoiceUpdateData, 36 | PlayData, 37 | SetPauseStateData, 38 | SetPositionData, 39 | SetFilterData, 40 | ) 41 | 42 | if TYPE_CHECKING: 43 | from .app import App 44 | 45 | 46 | __all__ = ( 47 | 'Player', 48 | ) 49 | 50 | 51 | LOG: logging.Logger = logging.getLogger('swish.player') 52 | 53 | 54 | class Player: 55 | 56 | def __init__( 57 | self, 58 | websocket: aiohttp.web.WebSocketResponse, 59 | guild_id: str, 60 | ) -> None: 61 | 62 | self._app: App = websocket['app'] 63 | self._websocket: aiohttp.web.WebSocketResponse = websocket 64 | self._guild_id: str = guild_id 65 | 66 | self._connector: native_voice.VoiceConnector = native_voice.VoiceConnector() 67 | self._connector.user_id = int(websocket['user_id']) 68 | 69 | self._connection: native_voice.VoiceConnection | None = None 70 | self._runner: asyncio.Task[None] | None = None 71 | 72 | self._PAYLOAD_HANDLERS: PayloadHandlers = { 73 | 'voice_update': self._voice_update, 74 | 'destroy': self._destroy, 75 | 'play': self._play, 76 | 'stop': self._stop, 77 | 'set_pause_state': self._set_pause_state, 78 | 'set_position': self._set_position, 79 | 'set_filter': self._set_filter, 80 | } 81 | 82 | self._LOG_PREFIX: str = f'{self._websocket["client_name"]} - Player \'{self._guild_id}\'' 83 | 84 | self._NO_CONNECTION_MESSAGE: Callable[[str], str] = ( 85 | lambda op: f'{self._LOG_PREFIX} attempted \'{op}\' op while internal connection is down.' 86 | ) 87 | self._MISSING_KEY_MESSAGE: Callable[[str, str], str] = ( 88 | lambda op, key: f'{self._LOG_PREFIX} received \'{op}\' op with missing \'{key}\' key.' 89 | ) 90 | 91 | # websocket handlers 92 | 93 | async def handle_payload(self, payload: ReceivedPayload) -> None: 94 | 95 | op = payload['op'] 96 | 97 | if op not in self._PAYLOAD_HANDLERS: 98 | LOG.error(f'{self._LOG_PREFIX} received payload with unknown \'op\' key.\nPayload: {payload}') 99 | return 100 | 101 | LOG.debug(f'{self._LOG_PREFIX} received payload with \'{op}\' op.\nPayload: {payload}') 102 | await self._PAYLOAD_HANDLERS[op](payload['d']) 103 | 104 | async def send_payload(self, op: SentPayloadOp, data: Any) -> None: 105 | await self._websocket.send_json({'op': op, 'd': data}) 106 | 107 | # connection handlers 108 | 109 | async def _connect(self) -> None: 110 | 111 | loop = asyncio.get_running_loop() 112 | self._connection = await self._connector.connect(loop) 113 | 114 | if self._runner is not None: 115 | self._runner.cancel() 116 | self._runner = loop.create_task(self._reconnect_handler()) 117 | 118 | async def _reconnect_handler(self) -> None: 119 | 120 | loop = asyncio.get_running_loop() 121 | backoff = discord.backoff.ExponentialBackoff() 122 | 123 | while True: 124 | 125 | try: 126 | assert self._connection is not None 127 | await self._connection.run(loop) 128 | 129 | except native_voice.ConnectionClosed: 130 | await self._disconnect() 131 | return 132 | 133 | except native_voice.ConnectionError: 134 | await self._disconnect() 135 | return 136 | 137 | except native_voice.ReconnectError: 138 | 139 | retry = backoff.delay() 140 | await asyncio.sleep(retry) 141 | 142 | try: 143 | await self._connect() 144 | except asyncio.TimeoutError: 145 | continue 146 | 147 | else: 148 | await self._disconnect() 149 | return 150 | 151 | async def _disconnect(self) -> None: 152 | 153 | if self._connection is None: 154 | return 155 | 156 | self._connection.disconnect() 157 | self._connection = None 158 | 159 | # payload handlers 160 | 161 | async def _voice_update(self, data: VoiceUpdateData) -> None: 162 | 163 | if not (session_id := data.get('session_id')): 164 | LOG.error(self._MISSING_KEY_MESSAGE('voice_update', 'session_id')) 165 | return 166 | if not (token := data.get('token')): 167 | LOG.error(self._MISSING_KEY_MESSAGE('voice_update', 'token')) 168 | return 169 | if not (endpoint := data.get('endpoint')): 170 | LOG.error(self._MISSING_KEY_MESSAGE('voice_update', 'endpoint')) 171 | return 172 | 173 | self._connector.session_id = session_id 174 | 175 | endpoint, _, _ = endpoint.rpartition(':') 176 | endpoint = endpoint.removeprefix('wss://') 177 | 178 | self._connector.update_socket( 179 | token, 180 | data['guild_id'], 181 | endpoint 182 | ) 183 | await self._connect() 184 | LOG.info(f'{self._LOG_PREFIX} connected to internal voice server \'{endpoint}\'.') 185 | 186 | async def _destroy(self) -> None: 187 | 188 | await self._disconnect() 189 | LOG.info(f'{self._LOG_PREFIX} has been disconnected.') 190 | 191 | del self._websocket['players'][self._guild_id] 192 | 193 | async def _play(self, data: PlayData) -> None: 194 | 195 | if not self._connection: 196 | LOG.error(self._NO_CONNECTION_MESSAGE('play')) 197 | return 198 | 199 | if not (track_id := data.get('track_id')): 200 | LOG.error(self._MISSING_KEY_MESSAGE('play', 'track_id')) 201 | return 202 | 203 | # TODO: handle start_time 204 | # TODO: handle end_time 205 | # TODO: handle replace 206 | 207 | track_info = self._app._decode_track_id(track_id) 208 | url = await self._app._get_playback_url(track_info['url']) 209 | 210 | self._connection.play(url) 211 | LOG.info(f'{self._LOG_PREFIX} started playing track \'{track_info["title"]}\' by \'{track_info["author"]}\'.') 212 | 213 | async def _stop(self) -> None: 214 | 215 | if not self._connection: 216 | LOG.error(self._NO_CONNECTION_MESSAGE('stop')) 217 | return 218 | if not self._connection.is_playing(): 219 | LOG.error(f'{self._LOG_PREFIX} attempted \'stop\' op while no tracks are playing.') 220 | return 221 | 222 | self._connection.stop() 223 | LOG.info(f'{self._LOG_PREFIX} stopped the current track.') 224 | 225 | async def _set_pause_state(self, data: SetPauseStateData) -> None: 226 | 227 | if not self._connection: 228 | LOG.error(self._NO_CONNECTION_MESSAGE('set_pause_state')) 229 | return 230 | if not (state := data.get('state')): 231 | LOG.error(self._MISSING_KEY_MESSAGE('set_pause_state', 'state')) 232 | return 233 | 234 | if state != self._connection.is_paused(): 235 | self._connection.pause() if state else self._connection.resume() 236 | 237 | LOG.info(f'{self._LOG_PREFIX} set its paused state to \'{state}\'.') 238 | 239 | async def _set_position(self, data: SetPositionData) -> None: 240 | 241 | if not self._connection: 242 | LOG.error(self._NO_CONNECTION_MESSAGE('set_position')) 243 | return 244 | if not self._connection.is_playing(): 245 | LOG.error(f'{self._LOG_PREFIX} attempted \'set_position\' op while no tracks are playing.') 246 | return 247 | 248 | if not (position := data.get('position')): 249 | LOG.error(self._MISSING_KEY_MESSAGE('set_position', 'position')) 250 | return 251 | 252 | # TODO: implement 253 | LOG.info(f'{self._LOG_PREFIX} set its position to \'{position}\'.') 254 | 255 | async def _set_filter(self, data: SetFilterData) -> None: 256 | LOG.error(f'{self._LOG_PREFIX} received \'set_filter\' op which is not yet implemented.') 257 | -------------------------------------------------------------------------------- /swish/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonistaGuild/Swish/3666bfd644cea28264703ae4820054e53ba89879/swish/py.typed -------------------------------------------------------------------------------- /swish/rotator.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | 19 | from __future__ import annotations 20 | 21 | import ipaddress 22 | import itertools 23 | import logging 24 | import time 25 | 26 | import discord.utils 27 | from collections.abc import Iterator 28 | 29 | from .config import CONFIG 30 | from .utilities import plural 31 | 32 | 33 | __all__ = ( 34 | 'BaseRotator', 35 | 'NanosecondRotator', 36 | 'BanRotator', 37 | ) 38 | 39 | 40 | LOG: logging.Logger = logging.getLogger('swish.rotator') 41 | 42 | 43 | Network = ipaddress.IPv4Network | ipaddress.IPv6Network 44 | 45 | 46 | class BaseRotator: 47 | 48 | _enabled: bool 49 | _networks: list[Network] 50 | _address_count: int 51 | 52 | _cycle: Iterator[Network] 53 | _current_network: Network 54 | 55 | if CONFIG.rotation.blocks: 56 | _enabled = True 57 | _networks = [ipaddress.ip_network(block) for block in CONFIG.rotation.blocks] 58 | _address_count = sum(network.num_addresses for network in _networks) 59 | LOG.info( 60 | f'IP rotation enabled using {plural(_address_count, "IP")} from {plural(len(_networks), "network block")}.' 61 | ) 62 | _cycle = itertools.cycle(_networks) 63 | _current_network = next(_cycle) 64 | 65 | else: 66 | _enabled = False 67 | _networks = [] 68 | _address_count = 0 69 | _cycle = discord.utils.MISSING 70 | _current_network = discord.utils.MISSING 71 | 72 | LOG.warning('No network blocks configured, increased risk of ratelimiting.') 73 | 74 | @classmethod 75 | def rotate(cls) -> ...: 76 | raise NotImplementedError 77 | 78 | 79 | class BanRotator(BaseRotator): 80 | 81 | _offset: int = 0 82 | 83 | @classmethod 84 | def rotate(cls) -> str: 85 | 86 | if not cls._enabled: 87 | return '0.0.0.0' 88 | 89 | if cls._offset >= cls._current_network.num_addresses: 90 | cls._current_network = next(cls._cycle) 91 | cls._offset = 0 92 | 93 | address = cls._current_network[cls._offset] 94 | cls._offset += 1 95 | 96 | return str(address) 97 | 98 | 99 | class NanosecondRotator(BaseRotator): 100 | 101 | _ns: int = time.time_ns() 102 | 103 | @classmethod 104 | def rotate(cls) -> str: 105 | 106 | if not cls._enabled or cls._address_count < 2 ** 64: 107 | return '0.0.0.0' 108 | 109 | while True: 110 | 111 | offset = time.time_ns() - cls._ns 112 | 113 | if offset > cls._address_count: 114 | cls._ns = time.time_ns() 115 | continue 116 | elif offset >= cls._current_network.num_addresses: 117 | cls._current_network = next(cls._cycle) 118 | offset -= cls._current_network.num_addresses 119 | else: 120 | break 121 | 122 | return str(cls._current_network[offset]) 123 | -------------------------------------------------------------------------------- /swish/types/payloads.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | 19 | from __future__ import annotations 20 | 21 | from collections.abc import Awaitable, Callable 22 | from typing import Any, Literal, TypedDict, Union 23 | 24 | from typing_extensions import NotRequired 25 | 26 | 27 | __all__ = ( 28 | # Received 29 | 'VoiceUpdateData', 30 | 'PlayData', 31 | 'SetPauseStateData', 32 | 'SetPositionData', 33 | 'SetFilterData', 34 | 35 | 'ReceivedPayloadOp', 36 | 'ReceivedPayload', 37 | 38 | # Sent 39 | 'EventData', 40 | 41 | 'SentPayloadOp', 42 | 'SentPayload', 43 | 44 | # Final 45 | 'PayloadHandlers', 46 | 'Payload', 47 | ) 48 | 49 | 50 | ############ 51 | # Received # 52 | ############ 53 | 54 | class VoiceUpdateData(TypedDict): 55 | guild_id: str 56 | session_id: str 57 | token: str 58 | endpoint: str 59 | 60 | 61 | class PlayData(TypedDict): 62 | guild_id: str 63 | track_id: str 64 | start_time: NotRequired[int] 65 | end_time: NotRequired[int] 66 | replace: NotRequired[bool] 67 | 68 | 69 | class SetPauseStateData(TypedDict): 70 | guild_id: str 71 | state: bool 72 | 73 | 74 | class SetPositionData(TypedDict): 75 | guild_id: str 76 | position: int 77 | 78 | 79 | class SetFilterData(TypedDict): 80 | guild_id: str 81 | 82 | 83 | ReceivedPayloadOp = Literal[ 84 | 'voice_update', 85 | 'destroy', 86 | 'play', 87 | 'stop', 88 | 'set_pause_state', 89 | 'set_position', 90 | 'set_filter', 91 | ] 92 | 93 | 94 | class ReceivedPayload(TypedDict): 95 | op: ReceivedPayloadOp 96 | d: Any 97 | 98 | 99 | ######## 100 | # Sent # 101 | ######## 102 | 103 | class EventData(TypedDict): 104 | guild_id: str 105 | type: Literal['track_start', 'track_end', 'track_error', 'track_update', 'player_debug'] 106 | 107 | 108 | SentPayloadOp = Literal['event'] 109 | 110 | 111 | class SentPayload(TypedDict): 112 | op: SentPayloadOp 113 | d: Any 114 | 115 | 116 | ######### 117 | # Final # 118 | ######### 119 | 120 | 121 | class PayloadHandlers(TypedDict): 122 | voice_update: Callable[[VoiceUpdateData], Awaitable[None]] 123 | destroy: Callable[..., Awaitable[None]] 124 | play: Callable[[PlayData], Awaitable[None]] 125 | stop: Callable[..., Awaitable[None]] 126 | set_pause_state: Callable[[SetPauseStateData], Awaitable[None]] 127 | set_position: Callable[[SetPositionData], Awaitable[None]] 128 | set_filter: Callable[[SetFilterData], Awaitable[None]] 129 | 130 | 131 | Payload = Union[ReceivedPayload, SentPayload] 132 | -------------------------------------------------------------------------------- /swish/utilities.py: -------------------------------------------------------------------------------- 1 | """Swish. A standalone audio player and server for bots on Discord. 2 | 3 | Copyright (C) 2022 PythonistaGuild 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | """ 18 | 19 | from __future__ import annotations 20 | 21 | from collections.abc import Callable 22 | 23 | 24 | __all__ = ( 25 | 'plural', 26 | ) 27 | 28 | 29 | plural: Callable[[int, str], str] = lambda count, thing: f'{count} {thing}s' if count > 1 else f'{count} {thing}' 30 | -------------------------------------------------------------------------------- /tests/bot.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from typing import Any 5 | 6 | import aiohttp 7 | import discord 8 | import discord.types.voice 9 | from discord.ext import commands 10 | 11 | 12 | from swish.config import CONFIG 13 | 14 | 15 | class Bot(commands.Bot): 16 | 17 | def __init__(self) -> None: 18 | super().__init__( 19 | command_prefix=commands.when_mentioned_or('cb '), 20 | intents=discord.Intents.all(), 21 | case_insensitive=True, 22 | ) 23 | 24 | self.first_ready: bool = True 25 | 26 | self.session: aiohttp.ClientSession | None = None 27 | self.websocket: aiohttp.ClientWebSocketResponse | None = None 28 | self.task: asyncio.Task[None] | None = None 29 | 30 | async def on_ready(self) -> None: 31 | 32 | if not self.first_ready: 33 | return 34 | 35 | self.first_ready = False 36 | 37 | self.session = aiohttp.ClientSession() 38 | self.websocket = await self.session.ws_connect( 39 | url=f'ws://{CONFIG.server.host}:{CONFIG.server.port}', 40 | headers={ 41 | 'Authorization': CONFIG.server.password, 42 | 'User-Agent': 'Python/v3.10.1,swish.py/v0.0.1a', 43 | 'User-Id': str(self.user.id), 44 | }, 45 | ) 46 | self.task = asyncio.create_task(self._listen()) 47 | 48 | print('Bot is ready!') 49 | 50 | async def _listen(self) -> None: 51 | 52 | while True: 53 | message = await self.websocket.receive() 54 | payload = message.json() 55 | 56 | asyncio.create_task(self._receive_payload(payload['op'], data=payload['d'])) 57 | 58 | async def _receive_payload(self, op: str, /, *, data: dict[str, Any]) -> None: 59 | raise NotImplementedError 60 | 61 | async def _send_payload(self, op: str, data: dict[str, Any]) -> None: 62 | 63 | await self.websocket.send_json( 64 | data={ 65 | 'op': op, 66 | 'd': data, 67 | } 68 | ) 69 | 70 | 71 | class Player(discord.VoiceProtocol): 72 | 73 | def __init__(self, client: Bot, channel: discord.VoiceChannel) -> None: 74 | super().__init__(client, channel) 75 | 76 | self.bot: Bot = client 77 | self.voice_channel: discord.VoiceChannel = channel 78 | 79 | self._voice_server_update_data: discord.types.voice.VoiceServerUpdate | None = None 80 | self._session_id: str | None = None 81 | 82 | async def on_voice_server_update( 83 | self, 84 | data: discord.types.voice.VoiceServerUpdate 85 | ) -> None: 86 | 87 | self._voice_server_update_data = data 88 | await self._dispatch_voice_update() 89 | 90 | async def on_voice_state_update( 91 | self, 92 | data: discord.types.voice.GuildVoiceState 93 | ) -> None: 94 | 95 | self._session_id = data.get('session_id') 96 | await self._dispatch_voice_update() 97 | 98 | async def _dispatch_voice_update(self) -> None: 99 | 100 | if not self._session_id or not self._voice_server_update_data: 101 | return 102 | 103 | await self.bot._send_payload( 104 | 'voice_update', 105 | data={'session_id': self._session_id, **self._voice_server_update_data}, 106 | ) 107 | 108 | async def connect( 109 | self, 110 | *, 111 | timeout: float | None = None, 112 | reconnect: bool | None = None, 113 | self_mute: bool = False, 114 | self_deaf: bool = True, 115 | ) -> None: 116 | await self.voice_channel.guild.change_voice_state( 117 | channel=self.voice_channel, 118 | self_mute=self_mute, 119 | self_deaf=self_deaf 120 | ) 121 | 122 | async def disconnect( 123 | self, 124 | *, 125 | force: bool = False 126 | ) -> None: 127 | await self.voice_channel.guild.change_voice_state(channel=None) 128 | self.cleanup() 129 | 130 | 131 | class Music(commands.Cog): 132 | 133 | def __init__(self, bot: Bot) -> None: 134 | self.bot: Bot = bot 135 | 136 | @commands.command() 137 | async def play(self, ctx: commands.Context, *, query: str) -> None: 138 | 139 | if not ctx.guild.me.voice: 140 | await ctx.author.voice.channel.connect(cls=Player) 141 | 142 | async with self.bot.session.get( 143 | url='http://127.0.0.1:8000/search', 144 | params={'query': query}, 145 | ) as response: 146 | 147 | data = await response.json() 148 | await self.bot._send_payload( 149 | 'play', 150 | data={'guild_id': str(ctx.guild.id), 'track_id': data[0]['id']}, 151 | ) 152 | 153 | 154 | bot: Bot = Bot() 155 | 156 | 157 | async def main() -> None: 158 | 159 | async with bot: 160 | 161 | await bot.load_extension('jishaku') 162 | await bot.add_cog(Music(bot)) 163 | 164 | await bot.start(token='') 165 | 166 | 167 | asyncio.run(main()) 168 | --------------------------------------------------------------------------------