├── .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 | 
2 |
3 | [](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 |
--------------------------------------------------------------------------------