├── .gitignore ├── .travis.yml ├── CrossCompile.sh ├── LICENSE ├── PermCalc ├── README.md ├── data.go ├── logic.go ├── permcalc.go └── permcalc_test.go ├── README.md ├── bookmarks.go ├── commands.go ├── commands_navigate.go ├── commands_query.go ├── commands_roles.go ├── commands_say.go ├── commands_usermod.go ├── completer.go ├── emojify.go ├── errorfilter.go ├── events.go ├── filepaths.go ├── help.go ├── intercept.go ├── justfile ├── languages.go ├── location.go ├── lua.go ├── main.go ├── main_!windows.go ├── main_test.go ├── main_windows.go ├── msgutil.go ├── music.go ├── parser.go ├── types.go └── update.go /.gitignore: -------------------------------------------------------------------------------- 1 | wiki 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - master 5 | 6 | install: 7 | - curl https://raw.githubusercontent.com/jD91mZM2/my-scripts/master/gogetbranch.sh > gogetbranch.sh 8 | - bash gogetbranch.sh github.com/bwmarrin/discordgo develop 9 | - go get github.com/bwmarrin/discordgo 10 | - cd PermCalc 11 | - go get 12 | - go install 13 | - cd - 14 | - go get 15 | - go get github.com/golang/lint/golint 16 | 17 | script: 18 | - golint 19 | - go vet 20 | - go install 21 | - go test -race 22 | -------------------------------------------------------------------------------- /CrossCompile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "Usage: ./CrossCompile.sh [author]" 5 | exit 6 | fi 7 | 8 | author="$2" || "discordconsole-team" 9 | 10 | for i in {1..2}; do 11 | if [ $i == 1 ] 12 | then arch=386 13 | else arch=amd64 14 | fi 15 | folder="github.com/$author/$1" 16 | GOOS=linux GOARCH=$arch go install "$folder" 17 | GOOS=windows GOARCH=$arch go install "$folder" 18 | GOOS=darwin GOARCH=$arch go install "$folder" 19 | done 20 | 21 | dir="$HOME/Downloads/$1" 22 | rm -r "$dir" 23 | mkdir "$dir" 24 | 25 | for i in {1..2}; do 26 | archnum=$((i * 32)) 27 | mkdir -p "$dir/Linux/$archnum-bit" 28 | mkdir -p "$dir/macOS/$archnum-bit" 29 | mkdir -p "$dir/Windows/$archnum-bit" 30 | done 31 | 32 | for i in {1..2}; do 33 | archnum=$((i * 32)) 34 | if [ $i == 1 ]; then 35 | arch=386 36 | cp "linux_$arch/$1" "$dir/Linux/$archnum-bit/$1" 37 | else 38 | arch=amd64 39 | cp "$1" "$dir/Linux/$archnum-bit/$1" 40 | fi 41 | 42 | cp "darwin_$arch/$1" "$dir/macOS/$archnum-bit/$1" 43 | cp "windows_$arch/$1.exe" "$dir/Windows/$archnum-bit/$1.exe" 44 | done 45 | 46 | cd "$dir/.." 47 | tar -czf "$1.tar.gz" "$1" 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 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 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 635 | Copyright (C) 2017 Mnpn 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | DiscordConsole Copyright (C) 2017 Mnpn 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /PermCalc/README.md: -------------------------------------------------------------------------------- 1 | # PermCalc 2 | 3 | This is a sub-package of DiscordConsole you can use in your own applications! 4 | It's a simple Discord permission calculator. 5 | 6 | It's simple to use: 7 | ```Go 8 | perm, err := permcalc.Show() 9 | ``` 10 | 11 | `perm` is the permission as an integer. For example 8 for admin. 12 | `err` is any error encountered during the process. 13 | 14 | You can also pre-set permissions... 15 | ```Go 16 | pm := permcalc.PermCalc{ 17 | Perm: 8, 18 | } 19 | err := pm.Show() 20 | perm := pm.Perm // Updated by Show() 21 | ``` 22 | 23 | ... or even make it read-only! 24 | ```Go 25 | pm := permcalc.PermCalc{ 26 | ReadOnly: true, 27 | } 28 | err := pm.Show() 29 | ``` 30 | -------------------------------------------------------------------------------- /PermCalc/data.go: -------------------------------------------------------------------------------- 1 | package permcalc 2 | 3 | // First few permission constants 4 | // (All permission constants are from discordgo) 5 | const ( 6 | PermCreateInstantInvite = 1 << iota 7 | PermKickMembers 8 | PermBanMembers 9 | PermAdministrator 10 | PermManageChannels 11 | PermManageServer 12 | PermAddReactions 13 | PermViewAuditLogs 14 | ) 15 | 16 | // Second batch of permission constants 17 | const ( 18 | PermChangeNickname = 1 << (iota + 26) 19 | PermManageNicknames 20 | PermManageRoles 21 | PermManageWebhooks 22 | PermManageEmojis 23 | ) 24 | 25 | // Third batch of permission constants 26 | const ( 27 | PermReadMessages = 1 << (iota + 10) 28 | PermSendMessages 29 | PermSendTTSMessages 30 | PermManageMessages 31 | PermEmbedLinks 32 | PermAttachFiles 33 | PermReadMessageHistory 34 | PermMentionEveryone 35 | PermUseExternalEmojis 36 | ) 37 | 38 | // Last few permission constants 39 | const ( 40 | PermConnect = 1 << (iota + 20) 41 | PermSpeak 42 | PermMuteMembers 43 | PermDeafenMembers 44 | PermMoveMembers 45 | PermUseVoiceActivity 46 | ) 47 | 48 | // PermOrder is all permissions, sorted by order. 49 | var PermOrder = []int{ 50 | // General Permissions 51 | PermAdministrator, 52 | PermViewAuditLogs, 53 | PermManageRoles, 54 | PermKickMembers, 55 | PermCreateInstantInvite, 56 | PermManageNicknames, 57 | PermManageWebhooks, 58 | 59 | PermManageServer, 60 | PermManageChannels, 61 | PermBanMembers, 62 | PermChangeNickname, 63 | PermManageEmojis, 64 | 65 | // Text Permissions 66 | PermReadMessages, 67 | PermSendTTSMessages, 68 | PermEmbedLinks, 69 | PermReadMessageHistory, 70 | PermUseExternalEmojis, 71 | 72 | PermSendMessages, 73 | PermManageMessages, 74 | PermAttachFiles, 75 | PermMentionEveryone, 76 | PermAddReactions, 77 | 78 | // Voice Permissions 79 | PermConnect, 80 | PermMuteMembers, 81 | PermMoveMembers, 82 | 83 | PermSpeak, 84 | PermDeafenMembers, 85 | PermUseVoiceActivity, 86 | } 87 | 88 | // PermStrings the name for all permissions. 89 | var PermStrings = map[int]string{ 90 | // General Permissions 91 | PermAdministrator: "Administrator", 92 | PermViewAuditLogs: "View Audit Logs", 93 | PermManageRoles: "Manage Roles", 94 | PermKickMembers: "Kick Members", 95 | PermCreateInstantInvite: "Create Instant Invite", 96 | PermManageNicknames: "Manage Nicknames", 97 | PermManageWebhooks: "Manage Webhooks", 98 | 99 | PermManageServer: "Manage Server", 100 | PermManageChannels: "Manage Channels", 101 | PermBanMembers: "Ban Members", 102 | PermChangeNickname: "Change Nickname", 103 | PermManageEmojis: "Manage Emojis", 104 | 105 | // Text Permissions 106 | PermReadMessages: "Read Messages", 107 | PermSendTTSMessages: "Send TTS Messages", 108 | PermEmbedLinks: "Embed Links", 109 | PermReadMessageHistory: "Read Message History", 110 | PermUseExternalEmojis: "Use External Emojis", 111 | 112 | PermSendMessages: "Send Messages", 113 | PermManageMessages: "Manage Messages", 114 | PermAttachFiles: "Attach Files", 115 | PermMentionEveryone: "Mention Everyone", 116 | PermAddReactions: "Add Reactions", 117 | 118 | // Voice Permissions 119 | PermConnect: "Connect", 120 | PermMuteMembers: "Mute Members", 121 | PermMoveMembers: "Move Members", 122 | 123 | PermSpeak: "Speak", 124 | PermDeafenMembers: "Deafen Members", 125 | PermUseVoiceActivity: "Use Voice Activity", 126 | } 127 | -------------------------------------------------------------------------------- /PermCalc/logic.go: -------------------------------------------------------------------------------- 1 | package permcalc 2 | 3 | import "github.com/nsf/termbox-go" 4 | 5 | type data struct { 6 | running bool 7 | x, y int 8 | perm int 9 | readonly bool 10 | } 11 | 12 | var d data 13 | 14 | func handleKey(key termbox.Key, char rune) { 15 | switch key { 16 | case termbox.KeyArrowUp: 17 | moveY(true) 18 | case termbox.KeyArrowLeft: 19 | moveX(false) 20 | case termbox.KeyArrowRight: 21 | moveX(true) 22 | case termbox.KeyArrowDown: 23 | moveY(false) 24 | 25 | case termbox.KeySpace: 26 | if d.readonly { 27 | break 28 | } 29 | d.perm = d.perm ^ PermOrder[pos2perm(d.x, d.y)] 30 | case termbox.KeyEsc: 31 | d.running = false 32 | } 33 | 34 | switch char { 35 | case 'q': 36 | d.running = false 37 | } 38 | } 39 | 40 | func moveX(right bool) { 41 | if right { 42 | if d.x == optionX1+1 { 43 | d.x = optionX2 + 1 44 | } 45 | } else { 46 | if d.x == optionX2+1 { 47 | d.x = optionX1 + 1 48 | } 49 | } 50 | } 51 | func moveY(up bool) { 52 | y := d.y 53 | switch { 54 | case up && y == optionY1: 55 | case !up && y == optionY3+optionH3-1: 56 | 57 | case !up && y == optionY1+optionH1-1: 58 | d.y = optionY2 59 | case up && y == optionY2: 60 | d.y = optionY1 + optionH1 - 1 61 | 62 | case !up && y == optionY2+optionH2-1: 63 | d.y = optionY3 64 | case up && y == optionY3: 65 | d.y = optionY2 + optionH2 - 1 66 | 67 | default: 68 | if up { 69 | d.y-- 70 | } else { 71 | d.y++ 72 | } 73 | } 74 | } 75 | 76 | const optionX1 = 1 77 | const optionY1 = 2 78 | 79 | const optionH1 = 6 80 | const optionH2 = 5 81 | const optionH3 = 3 82 | 83 | const offset = 2 84 | 85 | const optionX2 = 30 86 | const optionY2 = optionY1 + optionH1 + offset 87 | const optionY3 = optionY2 + optionH2 + offset 88 | const optionY4 = optionY3 + optionH3 + offset 89 | 90 | func drawScreen() error { 91 | err := termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | drawString(optionX1-1, optionY1-1, "General Permissions") 97 | total := 0 98 | drawOptions(optionX1, optionY1, optionH1, &total) 99 | drawOptions(optionX2, optionY1, optionH1, &total) 100 | 101 | drawString(optionX1-1, optionY2-1, "Text Permissions") 102 | drawOptions(optionX1, optionY2, optionH2, &total) 103 | drawOptions(optionX2, optionY2, optionH2, &total) 104 | 105 | drawString(optionX1-1, optionY3-1, "Voice Permissions") 106 | drawOptions(optionX1, optionY3, optionH3, &total) 107 | drawOptions(optionX2, optionY3, optionH3, &total) 108 | 109 | y := optionY4 110 | if !d.readonly { 111 | drawString(optionX1-1, y, "Press space to toggle permissions.") 112 | y++ 113 | } 114 | drawString(optionX1-1, y, "Press Esc or Q to exit.") 115 | 116 | return termbox.Flush() 117 | } 118 | 119 | func pos2perm(x, y int) int { 120 | index := 0 121 | section2 := x == optionX2+1 122 | 123 | if y >= optionY3 { 124 | index = optionH1*2 + optionH2*2 + y - optionY3 125 | if section2 { 126 | index += optionH3 127 | } 128 | } else if y >= optionY2 { 129 | index = optionH1*2 + y - optionY2 130 | if section2 { 131 | index += optionH2 132 | } 133 | } else { 134 | index = y - optionY1 135 | if section2 { 136 | index += optionH1 137 | } 138 | } 139 | 140 | return index 141 | } 142 | 143 | func drawOptions(x int, y int, amount int, total *int) { 144 | for i := 0; i < amount; i++ { 145 | drawOption(x, y+i, *total+i) 146 | } 147 | *total += amount 148 | } 149 | func drawOption(x int, y int, index int) { 150 | perm := PermOrder[index] 151 | 152 | char := " " 153 | if d.perm&perm == perm { 154 | char = "*" 155 | } 156 | drawString(x, y, "["+char+"] "+PermStrings[perm]) 157 | } 158 | func drawString(x int, y int, str string) { 159 | for i, c := range str { 160 | drawCell(x+i, y, c, termbox.ColorDefault) 161 | } 162 | } 163 | 164 | func drawCell(x int, y int, c rune, fg termbox.Attribute) { 165 | bg := termbox.ColorDefault 166 | if x == d.x && y == d.y { 167 | bg = termbox.ColorWhite 168 | } 169 | termbox.SetCell(x, y, c, fg, bg) 170 | } 171 | -------------------------------------------------------------------------------- /PermCalc/permcalc.go: -------------------------------------------------------------------------------- 1 | package permcalc 2 | 3 | import "errors" 4 | import "github.com/nsf/termbox-go" 5 | 6 | // ErrAlreadyRunning is thrown when you attempt to show 7 | // a prompt when there already is one displayed. 8 | var ErrAlreadyRunning = errors.New("permcalc prompt already running") 9 | 10 | // PromptPerm shows the permission calculator 11 | // and returns whatever the user checked. 12 | func PromptPerm() (int, error) { 13 | pm := PermCalc{} 14 | 15 | err := pm.Show() 16 | if err != nil { 17 | return 0, err 18 | } 19 | 20 | return pm.Perm, nil 21 | } 22 | 23 | // PermCalc is a permission calculator object. 24 | // It stores data and similar 25 | type PermCalc struct { 26 | Perm int 27 | ReadOnly bool 28 | } 29 | 30 | // Show shows the permission calculator 31 | // according to the PermCalc object. 32 | func (pm *PermCalc) Show() error { 33 | if d.running { 34 | return ErrAlreadyRunning 35 | } 36 | 37 | d = data{ 38 | running: true, 39 | x: optionX1 + 1, 40 | y: optionY1, 41 | perm: pm.Perm, 42 | readonly: pm.ReadOnly, 43 | } 44 | 45 | err := termbox.Init() 46 | if err != nil { 47 | return err 48 | } 49 | defer termbox.Close() 50 | 51 | for d.running { 52 | err := drawScreen() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | event := termbox.PollEvent() 58 | handleKey(event.Key, event.Ch) 59 | } 60 | 61 | pm.Perm = d.perm 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /PermCalc/permcalc_test.go: -------------------------------------------------------------------------------- 1 | package permcalc_test 2 | 3 | /* 4 | func TestPermCalc(t *testing.T) { 5 | fmt.Println(permcalc.PromptPerm()) 6 | } 7 | */ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DiscordConsole [![License](https://img.shields.io/badge/license-GPL-blue.svg?style=flat-square)](https://github.com/discordconsole-team/DiscordConsole/blob/master/LICENSE) 2 | 3 | > DiscordConsole is software aiming to give you full control over 4 | > accounts, bots and webhooks! 5 | 6 | ### Install 7 | 8 | To get started, simply [download the console from 9 | releases](https://github.com/discordconsole-team/DiscordConsole/releases), or compile it yourself: `go get 10 | github.com/discordconsole-team/DiscordConsole` 11 | 12 | ### Usage 13 | 14 | It is recommended to run DiscordConsole in a terminal. To do that, 15 | just `cd` to the folder you extracted it and run it by typing 16 | `DiscordConsole`. There are a few launch options. You can display them 17 | by running DiscordConsole with `-h`. 18 | 19 | When starting the DiscordConsole in Windows for the first time, you 20 | might get a screen saying that Windows SmartScreen prevented an 21 | unrecognized app from starting. Just click **More Info** and then 22 | **Run anyway**. 23 | 24 | Once started, you'll get asked to fill in your token. If it's a bot 25 | token, just type `` in the terminal. If it's a user token, 26 | enter `user `. If it's a webhook, enter `webhook .` 27 | 28 | If you have copied your token to your clipboard and are having troubles 29 | pasting it on Windows, use the substitute `${paste}` in place of your token. 30 | 31 | Then, just press enter! You'll get the following screen: 32 | ![Picture](https://i.imgur.com/tnurMPA.png) 33 | 34 | Try to type something like `help` and let the magic happen! For a 35 | detailed review of each command, type `help command`. 36 | 37 | DiscordConsole also supports the use of quotes `" "` to use more than one word as an argument. 38 | 39 | ### Credits 40 | 41 | DiscordConsole was founded and written by 42 | [LEGOlord208/jD91mZM2](https://github.com/jD91mZM2) and has seen 43 | continued development by maintainer [Mnpn](https://github.com/Mnpn03) 44 | since mid-September of 2017. You can find a full list of contributors 45 | [here](https://github.com/discordconsole-team/DiscordConsole/graphs/contributors). 46 | 47 | 48 | DiscordConsole is licensed under the GNU G.P.L. © Mnpn - [Click here for more 49 | information.](https://github.com/discordconsole-team/DiscordConsole/blob/master/LICENSE) 50 | -------------------------------------------------------------------------------- /bookmarks.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "encoding/json" 22 | "os" 23 | ) 24 | 25 | const fileBookmarks = ".bookmarks" 26 | 27 | var bookmarks = make(map[string]string) 28 | var bookmarksCache = make(map[string]*location) 29 | 30 | func loadBookmarks() error { 31 | reader, err := os.Open(fileBookmarks) 32 | if err != nil { 33 | if os.IsNotExist(err) { 34 | return nil 35 | } 36 | return err 37 | } 38 | defer reader.Close() 39 | 40 | return json.NewDecoder(reader).Decode(&bookmarks) 41 | } 42 | 43 | func saveBookmarks() error { 44 | writer, err := os.Create(fileBookmarks) 45 | if err != nil { 46 | return err 47 | } 48 | defer writer.Close() 49 | 50 | encoder := json.NewEncoder(writer) 51 | encoder.SetIndent("", "\t") 52 | return encoder.Encode(bookmarks) 53 | } 54 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "io" 24 | "os" 25 | "strconv" 26 | "strings" 27 | "sync" 28 | 29 | "github.com/atotto/clipboard" 30 | "github.com/bwmarrin/discordgo" 31 | "github.com/discordconsole-team/DiscordConsole/PermCalc" 32 | "github.com/fatih/color" 33 | "github.com/jD91mZM2/gtable" 34 | "github.com/jD91mZM2/stdutil" 35 | ) 36 | 37 | var mutexCommand sync.Mutex 38 | 39 | var lastUsedMsg string 40 | var lastUsedRole string 41 | 42 | var cacheRead *discordgo.Message 43 | var cacheUser []*keyval 44 | var cacheInvite []*keyval 45 | 46 | var messages = messagesNone 47 | var intercept = true 48 | var output = false 49 | 50 | var aliases map[string]string 51 | var clear map[string]func() 52 | 53 | var webhookCommands = []string{"help", "big", "say", "sayfile", "embed", "name", "avatar", "exit", "exec", "run", "lang"} 54 | 55 | func command(session *discordgo.Session, source commandSource, cmd string, w io.Writer) (returnVal string) { 56 | cmd = strings.TrimSpace(cmd) 57 | if cmd == "" { 58 | return 59 | } 60 | 61 | parts, err := parse(substitute, cmd) 62 | 63 | if err != nil { 64 | stdutil.PrintErr(err.Error(), nil) 65 | return 66 | } 67 | 68 | cmd = parts[0] 69 | args := parts[1:] 70 | 71 | returnVal = commandRaw(session, source, cmd, args, w) 72 | return 73 | } 74 | 75 | func commandRaw(session *discordgo.Session, source commandSource, cmd string, args []string, w io.Writer) (returnVal string) { 76 | defer handleCrash() 77 | nargs := len(args) 78 | 79 | if !source.NoMutex { 80 | mutexCommand.Lock() 81 | defer mutexCommand.Unlock() 82 | } 83 | 84 | if aliascmd, ok := aliases[cmd]; ok && !source.Alias && cmd != "alias" { 85 | if nargs >= 1 { 86 | aliascmd += " " + strings.Join(args, " ") 87 | } 88 | colors := w == color.Output 89 | if colors { 90 | colorAutomated.Set() 91 | } 92 | writeln(w, aliascmd) 93 | if colors { 94 | color.Unset() 95 | } 96 | 97 | // Won't use source anywhere else. 98 | // No reason to copy the variable. 99 | source.Alias = true 100 | source.NoMutex = true 101 | return command(session, source, aliascmd, w) 102 | } 103 | 104 | if userType == typeWebhook { 105 | allowed := false 106 | for _, allow := range webhookCommands { 107 | if cmd == allow { 108 | allowed = true 109 | } 110 | } 111 | 112 | if !allowed { 113 | stdutil.PrintErr(tl("invalid.webhook.command"), nil) 114 | return 115 | } 116 | } 117 | 118 | switch cmd { 119 | case "help": 120 | search := strings.Join(args, " ") 121 | printHelp(search) 122 | case "exit": 123 | closing = true 124 | case "exec": 125 | if nargs < 1 { 126 | stdutil.PrintErr("exec ", nil) 127 | return 128 | } 129 | 130 | cmd := strings.Join(args, " ") 131 | 132 | err := execute(sh, c, cmd) 133 | if err != nil { 134 | stdutil.PrintErr(tl("failed.exec"), err) 135 | } 136 | case "run": 137 | if nargs < 1 { 138 | stdutil.PrintErr("run ", nil) 139 | return 140 | } 141 | var script string 142 | var scriptArgs []string 143 | 144 | scriptName := true 145 | for i, arg := range args { 146 | if scriptName { 147 | if i != 0 { 148 | script += " " 149 | } 150 | if strings.HasSuffix(arg, ":") { 151 | scriptName = false 152 | arg = arg[:len(arg)-1] 153 | } 154 | script += arg 155 | } else { 156 | scriptArgs = append(scriptArgs, arg) 157 | } 158 | } 159 | 160 | err := fixPath(&script) 161 | if err != nil { 162 | stdutil.PrintErr(tl("failed.fixpath"), err) 163 | } 164 | 165 | mutexCommand.Unlock() 166 | err = runLua(session, script, scriptArgs...) 167 | mutexCommand.Lock() 168 | if err != nil { 169 | stdutil.PrintErr(tl("failed.lua.run"), err) 170 | } 171 | case "lang": 172 | if nargs < 1 { 173 | stdutil.PrintErr("lang ", nil) 174 | return 175 | } 176 | 177 | loadLangAuto(args[0]) 178 | case "guilds", "guild", "servers", "server", "channels", "pchannels", "vchannels", 179 | "channel", "dm", "bookmarks", "bookmark", "go": 180 | returnVal = commandsNavigate(session, cmd, args, nargs, w) 181 | case "say", "tts", "embed", "quote", "big", "file", "edit", 182 | "editembed", "sayfile", "del", "delall": 183 | returnVal = commandsSay(session, source, cmd, args, nargs, w) 184 | case "log": 185 | if nargs < 2 { 186 | stdutil.PrintErr("log ", nil) 187 | return 188 | } 189 | 190 | if loc.channel == nil { 191 | stdutil.PrintErr(tl("invalid.channel"), nil) 192 | return 193 | } 194 | 195 | var file io.Writer 196 | directly := false 197 | limit := 100 // 100 is Discord's limit. 198 | 199 | switch strings.ToLower(args[0]) { 200 | case "directly": 201 | directly = true 202 | file = w 203 | var err error 204 | limit, err = strconv.Atoi(args[1]) 205 | if err != nil { 206 | stdutil.PrintErr(tl("invalid.number"), nil) 207 | return 208 | } 209 | case "file": 210 | name := strings.Join(args[1:], " ") 211 | err := fixPath(&name) 212 | if err != nil { 213 | stdutil.PrintErr(tl("failed.fixpath"), err) 214 | } 215 | 216 | file2, err := os.Create(name) 217 | if err != nil { 218 | stdutil.PrintErr(tl("failed.file.open"), err) 219 | return 220 | } 221 | defer file2.Close() 222 | 223 | file = file2 224 | default: 225 | stdutil.PrintErr("log ", nil) 226 | return 227 | } 228 | 229 | msgs, err := session.ChannelMessages(loc.channel.ID, limit, "", "", "") 230 | if err != nil { 231 | stdutil.PrintErr(tl("failed.msg.query"), err) 232 | return 233 | } 234 | 235 | for i := len(msgs) - 1; i >= 0; i-- { 236 | msg := msgs[i] 237 | if msg.Author == nil { 238 | return 239 | } 240 | s := "" 241 | if directly { 242 | s = "(ID " + msg.ID + ") " 243 | } 244 | 245 | err = writeln(file, s+msg.Author.Username+"#"+msg.Author.Discriminator+": "+msgToString(msg)) 246 | if err != nil && !directly { 247 | stdutil.PrintErr(tl("failed.msg.write"), err) 248 | return 249 | } 250 | } 251 | case "members": 252 | if loc.guild == nil { 253 | stdutil.PrintErr(tl("invalid.guild"), nil) 254 | return 255 | } 256 | 257 | members, err := session.GuildMembers(loc.guild.ID, "", 100) 258 | if err != nil { 259 | stdutil.PrintErr(tl("failed.members"), err) 260 | return 261 | } 262 | 263 | table := gtable.NewStringTable() 264 | table.AddStrings("ID", "Name", "Nick") 265 | 266 | for _, member := range members { 267 | table.AddRow() 268 | table.AddStrings(member.User.ID, member.User.String(), member.Nick) 269 | } 270 | writeln(w, table.String()) 271 | case "invite": 272 | if nargs < 1 { 273 | stdutil.PrintErr("invite\taccept \n\tcreate [expire] [max uses] ['temp']\n\tlist\n\trevoke ", nil) 274 | return 275 | } 276 | switch strings.ToLower(args[0]) { 277 | case "see", "read": 278 | if nargs < 2 { 279 | stdutil.PrintErr("invite see [property]", nil) 280 | return 281 | } 282 | 283 | var keyvals []*keyval 284 | if strings.EqualFold(args[1], "cache") { 285 | if cacheInvite == nil { 286 | stdutil.PrintErr(tl("invalid.cache"), nil) 287 | return 288 | } 289 | 290 | keyvals = cacheInvite 291 | } else { 292 | invite, err := session.Invite(args[1]) 293 | if err != nil { 294 | stdutil.PrintErr(tl("failed.invite"), err) 295 | return 296 | } 297 | 298 | keyvals = invite2array(invite) 299 | cacheInvite = keyvals 300 | } 301 | 302 | if nargs < 3 { 303 | for _, keyval := range keyvals { 304 | writeln(w, keyval.String()) 305 | } 306 | } else { 307 | var ok bool 308 | returnVal, ok = findValByKey(keyvals, args[2]) 309 | if !ok { 310 | stdutil.PrintErr(tl("invalid.value"), nil) 311 | return 312 | } 313 | 314 | writeln(w, returnVal) 315 | } 316 | case "accept": 317 | if nargs < 2 { 318 | stdutil.PrintErr("invite accept ", nil) 319 | return 320 | } 321 | if userType != typeUser { 322 | stdutil.PrintErr(tl("invalid.onlyfor.users"), nil) 323 | return 324 | } 325 | 326 | invite, err := session.InviteAccept(args[1]) 327 | if err != nil { 328 | stdutil.PrintErr(tl("failed.invite.accept"), err) 329 | return 330 | } 331 | writeln(w, tl("status.invite.accept")) 332 | 333 | loc.push(invite.Guild, invite.Channel) 334 | case "create": 335 | if loc.channel == nil { 336 | stdutil.PrintErr(tl("failed.channel"), nil) 337 | return 338 | } 339 | 340 | // Restricting this to bots. Discord resends a verification email 341 | // and it errors with "json unmarshal" when attempted from a user account. 342 | // I'd rather not have that. 343 | if userType != typeBot { 344 | stdutil.PrintErr(tl("invalid.onlyfor.bots"), nil) 345 | return 346 | } 347 | 348 | inviteObj := discordgo.Invite{} 349 | if nargs >= 2 { 350 | min, err := strconv.Atoi(args[1]) 351 | if err != nil { 352 | stdutil.PrintErr(tl("invalid.number"), nil) 353 | return 354 | } 355 | inviteObj.MaxAge = 60 * min 356 | if nargs >= 3 { 357 | num, err := strconv.Atoi(args[2]) 358 | if err != nil { 359 | stdutil.PrintErr(tl("invalid.number"), nil) 360 | return 361 | } 362 | inviteObj.MaxUses = num 363 | 364 | if nargs >= 4 && strings.EqualFold(args[3], "temp") { 365 | inviteObj.Temporary = true 366 | } 367 | } 368 | } 369 | 370 | invite, err := session.ChannelInviteCreate(loc.channel.ID, inviteObj) 371 | if err != nil { 372 | stdutil.PrintErr(tl("failed.invite.create"), err) 373 | return 374 | } 375 | writeln(w, tl("status.invite.create")+" "+invite.Code) 376 | returnVal = invite.Code 377 | case "list": 378 | if loc.guild == nil { 379 | stdutil.PrintErr(tl("invalid.guild"), nil) 380 | return 381 | } 382 | 383 | invites, err := session.GuildInvites(loc.guild.ID) 384 | if err != nil { 385 | stdutil.PrintErr(tl("failed.invite"), err) 386 | return 387 | } 388 | 389 | table := gtable.NewStringTable() 390 | table.AddStrings("Invites") 391 | 392 | for _, invite := range invites { 393 | table.AddRow() 394 | table.AddStrings(invite.Code) 395 | } 396 | 397 | writeln(w, table.String()) 398 | case "revoke": 399 | if nargs < 2 { 400 | stdutil.PrintErr("invite revoke ", nil) 401 | return 402 | } 403 | 404 | invite, err := session.InviteDelete(args[1]) 405 | if err != nil { 406 | stdutil.PrintErr(tl("failed.revoke"), err) 407 | return 408 | } 409 | writeln(w, tl("information.revoked.successfully")+invite.Code) 410 | default: 411 | stdutil.PrintErr(tl("invalid.value"), nil) 412 | } 413 | case "messages": 414 | if len(args) < 1 { 415 | messages = messagesCurrent 416 | return 417 | } 418 | 419 | val, ok := typeMessages[strings.ToLower(args[0])] 420 | if !ok { 421 | stdutil.PrintErr(tl("invalid.value"), nil) 422 | return 423 | } 424 | messages = val 425 | case "intercept": 426 | if len(args) < 1 { 427 | intercept = !intercept 428 | returnVal = strconv.FormatBool(intercept) 429 | writeln(w, returnVal) 430 | return 431 | } 432 | 433 | state, err := parseBool(args[0]) 434 | if err != nil { 435 | stdutil.PrintErr("", err) 436 | return 437 | } 438 | intercept = state 439 | case "output": 440 | if len(args) < 1 { 441 | output = !output 442 | returnVal = strconv.FormatBool(output) 443 | writeln(w, returnVal) 444 | return 445 | } 446 | 447 | state, err := parseBool(args[0]) 448 | if err != nil { 449 | stdutil.PrintErr("", err) 450 | return 451 | } 452 | output = state 453 | case "back": 454 | loc.push(lastLoc.guild, lastLoc.channel) 455 | case "ban": 456 | if nargs < 1 { 457 | stdutil.PrintErr("ban [description]", nil) 458 | return 459 | } 460 | if loc.guild == nil { 461 | stdutil.PrintErr(tl("invalid.guild"), nil) 462 | return 463 | } 464 | 465 | var err error 466 | if nargs < 2 { 467 | err = session.GuildBanCreate(loc.guild.ID, args[0], 0) 468 | } else { 469 | err = session.GuildBanCreateWithReason(loc.guild.ID, args[0], strings.Join(args[1:], " "), 0) 470 | } 471 | if err != nil { 472 | stdutil.PrintErr(tl("failed.ban.create"), err) 473 | } 474 | case "unban": 475 | if nargs < 1 { 476 | stdutil.PrintErr("unban ", nil) 477 | return 478 | } 479 | if loc.guild == nil { 480 | stdutil.PrintErr(tl("invalid.guild"), nil) 481 | return 482 | } 483 | 484 | err := session.GuildBanDelete(loc.guild.ID, args[0]) 485 | if err != nil { 486 | stdutil.PrintErr(tl("failed.ban.delete"), err) 487 | } 488 | case "kick": 489 | if nargs < 1 { 490 | stdutil.PrintErr("kick [reason]", nil) 491 | return 492 | } 493 | if loc.guild == nil { 494 | stdutil.PrintErr(tl("invalid.guild"), nil) 495 | return 496 | } 497 | 498 | var err error 499 | if nargs < 2 { 500 | err = session.GuildMemberDelete(loc.guild.ID, args[0]) 501 | } else { 502 | err = session.GuildMemberDeleteWithReason(loc.guild.ID, args[0], strings.Join(args[1:], " ")) 503 | } 504 | if err != nil { 505 | stdutil.PrintErr(tl("failed.kick"), err) 506 | } 507 | case "leave": 508 | var err error 509 | if nargs < 1 { 510 | if loc.guild == nil { 511 | stdutil.PrintErr(tl("invalid.guild"), nil) 512 | return 513 | } 514 | err = session.GuildLeave(loc.guild.ID) 515 | } else { 516 | err = session.GuildLeave(args[0]) 517 | } 518 | 519 | if err != nil { 520 | stdutil.PrintErr(tl("failed.leave"), err) 521 | return 522 | } 523 | 524 | loc.push(nil, nil) 525 | case "bans": 526 | if loc.guild == nil { 527 | stdutil.PrintErr(tl("invalid.guild"), nil) 528 | return 529 | } 530 | arg := "" 531 | if nargs > 0 { 532 | arg = strings.ToLower(args[0]) 533 | if arg != "text" { 534 | stdutil.PrintErr("bans [text]", nil) 535 | return 536 | } 537 | } 538 | 539 | bans, err := session.GuildBans(loc.guild.ID) 540 | if err != nil { 541 | stdutil.PrintErr(tl("failed.ban.list"), err) 542 | return 543 | } 544 | 545 | if nargs < 1 { 546 | lengthNotify := false 547 | table := gtable.NewStringTable() 548 | table.AddStrings("User ID", "Username", "Reason") 549 | 550 | for _, ban := range bans { 551 | reason := ban.Reason 552 | if len(reason) > 100 { 553 | reason = reason[0:100] + "..." 554 | lengthNotify = true 555 | } 556 | table.AddRow() 557 | table.AddStrings(ban.User.ID, ban.User.Username, reason) 558 | } 559 | 560 | writeln(w, table.String()) 561 | if lengthNotify { 562 | fmt.Println(tl("information.length") + " \"bans text\".") 563 | } 564 | } else if arg == "text" { // only reason that this exists is because gtable can break if the reason is too long 565 | for _, ban := range bans { 566 | reason := "" 567 | if ban.Reason != "" { 568 | reason = "): " + ban.Reason 569 | } else { 570 | reason = ")" 571 | } 572 | writeln(w, ban.User.Username + "#" + ban.User.Discriminator + " (" + ban.User.ID + reason + "\n") 573 | } 574 | } 575 | case "nickall": 576 | if loc.guild == nil { 577 | stdutil.PrintErr(tl("invalid.guild"), nil) 578 | return 579 | } 580 | 581 | members, err := session.GuildMembers(loc.guild.ID, "", 100) 582 | if err != nil { 583 | stdutil.PrintErr(tl("failed.members"), err) 584 | return 585 | } 586 | 587 | nick := strings.Join(args, " ") 588 | 589 | for _, member := range members { 590 | err := session.GuildMemberNickname(loc.guild.ID, member.User.ID, nick) 591 | if err != nil { 592 | stdutil.PrintErr(tl("failed.nick"), err) 593 | } 594 | } 595 | case "play": 596 | if userType != typeBot { 597 | stdutil.PrintErr(tl("invalid.onlyfor.bots"), nil) 598 | return 599 | } 600 | if nargs < 1 { 601 | stdutil.PrintErr("play ", nil) 602 | return 603 | } 604 | if vc == nil { 605 | stdutil.PrintErr(tl("invalid.channel.voice"), nil) 606 | return 607 | } 608 | if playing != "" { 609 | stdutil.PrintErr(tl("invalid.music.playing"), nil) 610 | return 611 | } 612 | 613 | file := strings.Join(args, " ") 614 | err := fixPath(&file) 615 | if err != nil { 616 | stdutil.PrintErr(tl("failed.fixpath"), err) 617 | } 618 | 619 | playing = file 620 | 621 | writeln(w, tl("status.loading")) 622 | 623 | var buffer [][]byte 624 | err = loadAudio(file, &buffer) 625 | if err != nil { 626 | stdutil.PrintErr(tl("failed.file.load"), err) 627 | playing = "" 628 | return 629 | } 630 | 631 | writeln(w, "Loaded!") 632 | writeln(w, "Playing!") 633 | 634 | go func(buffer [][]byte, session *discordgo.Session, guild, channel string) { 635 | play(buffer, session, guild, channel) 636 | playing = "" 637 | }(buffer, session, loc.guild.ID, loc.channel.ID) 638 | case "stop": 639 | if userType != typeBot { 640 | stdutil.PrintErr(tl("invalid.onlyfor.bots"), nil) 641 | return 642 | } 643 | playing = "" 644 | case "react": 645 | if nargs < 2 { 646 | stdutil.PrintErr("react\tadd/del \n\tdelall \n\tbig ", nil) 647 | return 648 | } 649 | if loc.channel == nil { 650 | stdutil.PrintErr(tl("invalid.channel"), nil) 651 | return 652 | } 653 | switch strings.ToLower(args[0]) { 654 | case "add": 655 | fallthrough 656 | case "del": 657 | if nargs < 3 { 658 | stdutil.PrintErr("react add/del ", nil) 659 | return 660 | } 661 | if args[0] == "add" { 662 | err := session.MessageReactionAdd(loc.channel.ID, args[1], args[2]) 663 | if err != nil { 664 | stdutil.PrintErr(tl("failed.react"), err) 665 | return 666 | } 667 | } else { 668 | err := session.MessageReactionRemove(loc.channel.ID, args[1], args[2], "@me") 669 | if err != nil { 670 | stdutil.PrintErr(tl("failed.react.del"), err) 671 | return 672 | } 673 | } 674 | case "delall": 675 | err := session.MessageReactionsRemoveAll(loc.channel.ID, args[1]) 676 | if err != nil { 677 | stdutil.PrintErr(tl("failed.react.delall"), err) 678 | return 679 | } 680 | case "big": 681 | used := "" 682 | 683 | for _, c := range strings.Join(args[2:], " ") { 684 | str := string(toEmoji(c)) 685 | 686 | if strings.Contains(used, str) { 687 | writeln(w, tl("failed.react.used")) 688 | continue 689 | } 690 | used += str 691 | 692 | err := session.MessageReactionAdd(loc.channel.ID, args[1], str) 693 | if err != nil { 694 | stdutil.PrintErr(tl("failed.react"), err) 695 | } 696 | } 697 | default: 698 | stdutil.PrintErr("react\tadd/del \n\tdelall \n\tbig ", nil) 699 | } 700 | case "block": 701 | if nargs < 1 { 702 | stdutil.PrintErr("block ", nil) 703 | return 704 | } 705 | if userType != typeUser { 706 | stdutil.PrintErr(tl("invalid.onlyfor.users"), nil) 707 | return 708 | } 709 | err := session.RelationshipUserBlock(args[0]) 710 | if err != nil { 711 | stdutil.PrintErr(tl("failed.block"), err) 712 | return 713 | } 714 | case "friend": 715 | if userType != typeUser { 716 | stdutil.PrintErr(tl("invalid.onlyfor.users"), nil) 717 | return 718 | } 719 | if nargs < 1 { 720 | stdutil.PrintErr("friend ", nil) 721 | return 722 | } 723 | switch strings.ToLower(args[0]) { 724 | case "list": 725 | relations, err := session.RelationshipsGet() 726 | if err != nil { 727 | stdutil.PrintErr(tl("failed.friend.list"), err) 728 | return 729 | } 730 | 731 | table := gtable.NewStringTable() 732 | table.AddStrings("ID", "Type", "Name") 733 | 734 | for _, relation := range relations { 735 | table.AddRow() 736 | table.AddStrings(relation.ID, typeRelationships[relation.Type], relation.User.Username) 737 | } 738 | 739 | writeln(w, table.String()) 740 | case "add": 741 | if nargs < 2 { 742 | stdutil.PrintErr("friend add ", nil) 743 | return 744 | } 745 | err := session.RelationshipFriendRequestSend(args[1]) 746 | if err != nil { 747 | stdutil.PrintErr(tl("failed.friend.add"), err) 748 | return 749 | } 750 | case "accept": 751 | if nargs < 2 { 752 | stdutil.PrintErr("friend accept ", nil) 753 | return 754 | } 755 | err := session.RelationshipFriendRequestAccept(args[1]) 756 | if err != nil { 757 | stdutil.PrintErr(tl("failed.friend.add"), err) 758 | return 759 | } 760 | case "remove": 761 | if nargs < 2 { 762 | stdutil.PrintErr("friend remove ", nil) 763 | return 764 | } 765 | err := session.RelationshipDelete(args[1]) 766 | if err != nil { 767 | stdutil.PrintErr(tl("failed.friend.remove"), err) 768 | return 769 | } 770 | default: 771 | stdutil.PrintErr("friend ", nil) 772 | } 773 | case "rl": 774 | full := nargs >= 1 && strings.EqualFold(args[0], "full") 775 | 776 | var err error 777 | if full { 778 | writeln(w, tl("rl.session")) 779 | session.Close() 780 | err = session.Open() 781 | if err != nil { 782 | stdutil.PrintErr(tl("failed.session.start"), err) 783 | } 784 | } 785 | 786 | writeln(w, tl("rl.cache.loc")) 787 | var guild *discordgo.Guild 788 | var channel *discordgo.Channel 789 | 790 | if loc.guild != nil { 791 | guild, err = session.Guild(loc.guild.ID) 792 | 793 | if err != nil { 794 | stdutil.PrintErr(tl("failed.guild"), err) 795 | return 796 | } 797 | } 798 | 799 | if loc.channel != nil { 800 | channel, err = session.Channel(loc.channel.ID) 801 | 802 | if err != nil { 803 | stdutil.PrintErr(tl("failed.channel"), err) 804 | return 805 | } 806 | } 807 | 808 | loc.guild = guild 809 | loc.channel = channel 810 | pointerCache = "" 811 | 812 | writeln(w, tl("rl.cache.vars")) 813 | cacheGuilds = nil 814 | cacheChannels = nil 815 | cacheAudio = make(map[string][][]byte) 816 | bookmarksCache = make(map[string]*location) 817 | 818 | lastLoc = &location{} 819 | lastUsedMsg = "" 820 | lastUsedRole = "" 821 | 822 | cacheRead = nil 823 | cacheUser = nil 824 | case "avatar", "name", "playing", "streaming", "typing", "nick", "status", "game": 825 | returnVal = commandsUserMod(session, cmd, args, nargs, w) 826 | case "read", "info": 827 | returnVal = commandsQuery(session, cmd, args, nargs, w) 828 | case "role": 829 | returnVal = commandsRoles(session, cmd, args, nargs, w) 830 | case "region": 831 | if nargs < 1 { 832 | stdutil.PrintErr("region list OR region set ", nil) 833 | return 834 | } 835 | switch strings.ToLower(args[0]) { 836 | case "list": 837 | regions, err := session.VoiceRegions() 838 | if err != nil { 839 | stdutil.PrintErr(tl("failed.voice.regions"), err) 840 | return 841 | } 842 | 843 | table := gtable.NewStringTable() 844 | table.AddStrings("ID", "Name", "Port") 845 | 846 | for _, region := range regions { 847 | table.AddRow() 848 | table.AddStrings(region.ID, region.Name, strconv.Itoa(region.Port)) 849 | } 850 | 851 | writeln(w, table.String()) 852 | case "set": 853 | if nargs < 2 { 854 | stdutil.PrintErr("region set ", nil) 855 | return 856 | } 857 | if loc.guild == nil { 858 | stdutil.PrintErr(tl("invalid.guild"), nil) 859 | return 860 | } 861 | 862 | _, err := session.GuildEdit(loc.guild.ID, discordgo.GuildParams{ 863 | Region: args[1], 864 | }) 865 | if err != nil { 866 | stdutil.PrintErr(tl("failed.guild.edit"), err) 867 | } 868 | } 869 | case "alias": 870 | if nargs <= 0 { 871 | for alias, aliascmd := range aliases { 872 | writeln(w, alias+"=`"+aliascmd+"`") 873 | } 874 | } else if nargs == 1 { 875 | delete(aliases, strings.ToLower(args[0])) 876 | } else { 877 | if aliases == nil { 878 | aliases = make(map[string]string) 879 | } 880 | 881 | aliases[strings.ToLower(args[0])] = strings.Join(args[1:], " ") 882 | } 883 | case "ownership": 884 | if loc.guild == nil { 885 | stdutil.PrintErr(tl("invalid.guild"), nil) 886 | return 887 | } 888 | 889 | if nargs < 1 { 890 | stdutil.PrintErr("ownership ", nil) 891 | return 892 | } 893 | id := args[0] 894 | 895 | if loc.guild.OwnerID != userObj.ID { 896 | stdutil.PrintErr(tl("invalid.not.owner"), nil) 897 | return 898 | } 899 | 900 | member, err := session.State.Member(loc.guild.ID, id) 901 | if err != nil { 902 | stdutil.PrintErr(tl("failed.user"), err) 903 | return 904 | } 905 | 906 | if userType == typeBot { 907 | stdutil.PrintErr(tl("invalid.onlyfor.users"), nil) 908 | return 909 | } 910 | 911 | execerr := execute("clear") 912 | if execerr != nil { 913 | stdutil.PrintErr("", err) 914 | } 915 | // We're Microsoft, and we're special! 916 | // We need our own damn part, because fuck you! 917 | // Windows is being a donkey. We'll only be clearing on Unix. 918 | 919 | c := color.New(color.FgHiRed) 920 | c.Println(tl("information.wait")) 921 | fmt.Println(tl("information.give.ownership") + member.User.Username + "#" + member.User.Discriminator + ". " + tl("information.irreversible")) 922 | fmt.Println(tl("information.confirmation") + " (y/n)") 923 | 924 | var response string 925 | _, err1 := fmt.Scanln(&response) 926 | if err1 != nil { 927 | stdutil.PrintErr("", err1) 928 | } 929 | 930 | state, err := parseBool(response) 931 | if err != nil { 932 | stdutil.PrintErr("", err) 933 | return 934 | } 935 | 936 | if state == true { 937 | _, oerr := session.GuildEdit(loc.guild.ID, discordgo.GuildParams{OwnerID: id}) 938 | if oerr != nil { 939 | stdutil.PrintErr(tl("failed.transfer"), oerr) 940 | return 941 | } 942 | } else { 943 | fmt.Println(tl("information.aborted")) 944 | return 945 | } 946 | case "permcalc": 947 | if !source.Terminal { 948 | stdutil.PrintErr(tl("invalid.source.terminal"), nil) 949 | return 950 | } 951 | pm := permcalc.PermCalc{} 952 | 953 | if nargs >= 1 { 954 | i, err := strconv.Atoi(args[0]) 955 | if err != nil { 956 | stdutil.PrintErr(tl("invalid.number"), nil) 957 | return 958 | } 959 | pm.Perm = i 960 | } 961 | 962 | err := pm.Show() 963 | if err != nil { 964 | stdutil.PrintErr("failed.permcalc", err) 965 | return 966 | } 967 | writeln(w, strconv.Itoa(pm.Perm)) 968 | case "crash": 969 | if nargs >= 1 && strings.EqualFold(args[0], "die") { 970 | // Make error async for more "damage" 971 | go func() { 972 | panic("die") 973 | }() 974 | return 975 | } 976 | panic("triggered crash") 977 | case "pin": 978 | if loc.channel == nil { 979 | stdutil.PrintErr(tl("invalid.channel"), nil) 980 | return 981 | } 982 | if nargs < 1 { 983 | stdutil.PrintErr("pin ", nil) 984 | return 985 | } 986 | 987 | err := session.ChannelMessagePin(loc.channel.ID, args[0]) 988 | if err != nil { 989 | stdutil.PrintErr(tl("failed.pin"), err) 990 | return 991 | } 992 | case "unpin": 993 | if loc.channel == nil { 994 | stdutil.PrintErr(tl("invalid.channel"), nil) 995 | return 996 | } 997 | if nargs < 1 { 998 | stdutil.PrintErr("unpin ", nil) 999 | return 1000 | } 1001 | 1002 | err := session.ChannelMessageUnpin(loc.channel.ID, args[0]) 1003 | if err != nil { 1004 | stdutil.PrintErr(tl("failed.unpin"), err) 1005 | return 1006 | } 1007 | case "new": 1008 | if nargs < 2 { 1009 | stdutil.PrintErr("new ", nil) 1010 | return 1011 | } 1012 | switch strings.ToLower(args[0]) { 1013 | case "guild": 1014 | g, err := session.GuildCreate(args[1]) 1015 | if err != nil { 1016 | stdutil.PrintErr(tl("failed.guild.create"), err) 1017 | return 1018 | } 1019 | fmt.Println(tl("information.guild") + args[1] + tl("information.created.successfully") + g.ID + ".") 1020 | case "channel": 1021 | if loc.guild == nil { 1022 | stdutil.PrintErr(tl("invalid.guild"), nil) 1023 | return 1024 | } 1025 | c, err := session.GuildChannelCreate(loc.guild.ID, args[1], discordgo.ChannelTypeGuildText) 1026 | if err != nil { 1027 | stdutil.PrintErr(tl("failed.channel.create"), err) 1028 | return 1029 | } 1030 | fmt.Println(tl("information.channel") + args[1] + tl("information.created.successfully") + c.ID + ".") 1031 | case "vchannel": 1032 | if loc.guild == nil { 1033 | stdutil.PrintErr(tl("invalid.guild"), nil) 1034 | return 1035 | } 1036 | vc, err := session.GuildChannelCreate(loc.guild.ID, args[1], discordgo.ChannelTypeGuildVoice) 1037 | if err != nil { 1038 | stdutil.PrintErr(tl("failed.channel.create"), err) 1039 | return 1040 | } 1041 | fmt.Println(tl("information.channel") + args[1] + tl("information.created.successfully") + vc.ID + ".") 1042 | case "category": 1043 | if loc.guild == nil { 1044 | stdutil.PrintErr(tl("invalid.guild"), nil) 1045 | return 1046 | } 1047 | c, err := session.GuildChannelCreate(loc.guild.ID, args[1], discordgo.ChannelTypeGuildCategory) 1048 | if err != nil { 1049 | stdutil.PrintErr(tl("failed.category.create"), err) 1050 | return 1051 | } 1052 | fmt.Println(tl("information.category") + args[1] + tl("information.created.successfully") + c.ID + ".") 1053 | default: 1054 | stdutil.PrintErr("new ", nil) 1055 | } 1056 | case "delete": 1057 | if nargs < 2 { 1058 | stdutil.PrintErr("delete ", nil) 1059 | return 1060 | } 1061 | switch strings.ToLower(args[0]) { 1062 | case "channel", "category": 1063 | _, err := session.ChannelDelete(args[1]) 1064 | if err != nil { 1065 | stdutil.PrintErr(tl("failed.channel.delete"), err) 1066 | return 1067 | } 1068 | fmt.Println(tl("information.channel") + args[1] + tl("information.deleted.successfully")) 1069 | case "guild": 1070 | _, err := session.GuildDelete(args[1]) 1071 | if err != nil { 1072 | stdutil.PrintErr(tl("failed.guild.delete"), err) 1073 | return 1074 | } 1075 | fmt.Println(tl("information.guild") + args[1] + tl("information.deleted.successfully")) 1076 | } 1077 | case "move": 1078 | if nargs < 2 { 1079 | stdutil.PrintErr("move ", nil) 1080 | return 1081 | } 1082 | if loc.guild == nil { 1083 | stdutil.PrintErr(tl("invalid.guild"), nil) 1084 | return 1085 | } 1086 | err := session.GuildMemberMove(loc.guild.ID, args[0], args[1]) 1087 | if err != nil { 1088 | stdutil.PrintErr(tl("failed.move"), err) 1089 | return 1090 | } 1091 | writeln(w, tl("information.moved")) 1092 | case "note": 1093 | if nargs < 2 { 1094 | stdutil.PrintErr("note ", nil) 1095 | return 1096 | } 1097 | err := session.UserNoteSet(args[0], strings.Join(args[1:], " ")) 1098 | if err != nil { 1099 | stdutil.PrintErr(tl("failed.note"), err) 1100 | return 1101 | } 1102 | fmt.Println(tl("information.note")) 1103 | case "latency": 1104 | fmt.Println(session.HeartbeatLatency()) 1105 | default: 1106 | stdutil.PrintErr(tl("invalid.command")+" '"+cmd+"'. "+tl("invalid.command2"), nil) 1107 | } 1108 | return 1109 | } 1110 | 1111 | func parseBool(str string) (bool, error) { 1112 | str = strings.ToLower(str) 1113 | if str == "yes" || str == "true" || str == "y" { 1114 | return true, nil 1115 | } else if str == "no" || str == "false" || str == "n" { 1116 | return false, nil 1117 | } 1118 | return false, errors.New(tl("invalid.yn")) 1119 | } 1120 | 1121 | func msgToString(msg *discordgo.Message) string { 1122 | msgc := msg.Content 1123 | for _, attachment := range msg.Attachments { 1124 | if len(msgc) > 0 { 1125 | msgc += " " 1126 | } 1127 | msgc += attachment.URL 1128 | } 1129 | return msgc 1130 | } 1131 | 1132 | func substitute(args string) (string, bool) { 1133 | switch args { 1134 | case "paste": 1135 | clipboardcontent, err := clipboard.ReadAll() 1136 | if err != nil { 1137 | stdutil.PrintErr((tl("failed.paste") + err.Error()), nil) 1138 | return "", true 1139 | } 1140 | return clipboardcontent, true 1141 | case "s.id": 1142 | if loc.guild != nil { 1143 | return loc.guild.ID, true 1144 | } 1145 | return "nil", true 1146 | case "s.owner.id": 1147 | if loc.guild != nil { 1148 | return loc.guild.OwnerID, true 1149 | } 1150 | return "nil", true 1151 | case "s.owner.mention": 1152 | if loc.guild != nil { 1153 | return "<@" + loc.guild.OwnerID + ">", true 1154 | } 1155 | return "nil", true 1156 | case "c.id": 1157 | if loc.channel != nil { 1158 | return loc.channel.ID, true 1159 | } 1160 | return "nil", true 1161 | // Webhooks don't have access to this info and would crash the program. 1162 | case "u.name": 1163 | if userType != typeWebhook { 1164 | return userObj.Username, true 1165 | } 1166 | case "u.discrim": 1167 | if userType != typeWebhook { 1168 | return userObj.Discriminator, true 1169 | } 1170 | case "u.id": 1171 | if userType != typeWebhook { 1172 | return userObj.ID, true 1173 | } 1174 | case "u.mention": 1175 | if userType != typeWebhook { 1176 | return "<@" + userObj.ID + ">", true 1177 | } 1178 | default: 1179 | return "", false 1180 | } 1181 | return "", false 1182 | } 1183 | -------------------------------------------------------------------------------- /commands_navigate.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "io" 22 | "sort" 23 | "strings" 24 | 25 | "github.com/bwmarrin/discordgo" 26 | "github.com/jD91mZM2/gtable" 27 | "github.com/jD91mZM2/stdutil" 28 | ) 29 | 30 | func commandsNavigate(session *discordgo.Session, cmd string, args []string, nargs int, w io.Writer) (returnVal string) { 31 | switch cmd { 32 | case "guilds", "servers": 33 | cache, ok := <-chanReady 34 | if !ok && cacheGuilds != nil { 35 | mutexCacheGuilds.RLock() 36 | defer mutexCacheGuilds.RUnlock() 37 | 38 | cache = cacheGuilds 39 | } 40 | 41 | var guilds []*discordgo.UserGuild 42 | if cache != nil { 43 | guilds = cache 44 | } else { 45 | var err error 46 | guilds, err = session.UserGuilds(100, "", "") 47 | if err != nil { 48 | stdutil.PrintErr(tl("failed.guild"), err) 49 | return 50 | } 51 | 52 | if userType == typeUser { 53 | settings, err := session.UserSettings() 54 | if err != nil { 55 | stdutil.PrintErr(tl("failed.settings"), err) 56 | } else { 57 | guilds = sortGuilds(guilds, settings) 58 | } 59 | } 60 | 61 | mutexCacheGuilds.Lock() 62 | cacheGuilds = guilds 63 | mutexCacheGuilds.Unlock() 64 | } 65 | 66 | table := gtable.NewStringTable() 67 | table.AddStrings("ID", "Name") 68 | 69 | for _, guild := range guilds { 70 | table.AddRow() 71 | table.AddStrings(guild.ID, guild.Name) 72 | } 73 | 74 | writeln(w, table.String()) 75 | case "guild", "server": 76 | if nargs < 1 { 77 | stdutil.PrintErr(cmd+" ", nil) 78 | return 79 | } 80 | 81 | guildID := strings.Join(args, " ") 82 | for _, g := range cacheGuilds { 83 | if strings.EqualFold(guildID, g.Name) { 84 | guildID = g.ID 85 | break 86 | } 87 | } 88 | 89 | guild, err := session.Guild(guildID) 90 | if err != nil { 91 | stdutil.PrintErr(tl("failed.guild"), err) 92 | return 93 | } 94 | 95 | channels, err := session.GuildChannels(guildID) 96 | if err != nil { 97 | stdutil.PrintErr(tl("failed.channel"), err) 98 | return 99 | } 100 | 101 | var channel *discordgo.Channel 102 | for _, channel2 := range channels { 103 | if channel2.Position == 0 { 104 | channel = channel2 105 | } 106 | } 107 | if channel == nil { 108 | stdutil.PrintErr(tl("failed.nochannel"), err) 109 | return 110 | } 111 | 112 | loc.push(guild, channel) 113 | case "channels": 114 | channels(session, discordgo.ChannelTypeGuildText, w) 115 | case "vchannels": 116 | channels(session, discordgo.ChannelTypeGuildVoice, w) 117 | case "pchannels": 118 | channels, err := session.UserChannels() 119 | if err != nil { 120 | stdutil.PrintErr(tl("failed.channel"), err) 121 | return 122 | } 123 | 124 | table := gtable.NewStringTable() 125 | table.AddStrings("ID", "Type", "Recipient(s)") 126 | 127 | for _, channel := range channels { 128 | table.AddRow() 129 | recipient := "" 130 | if len(channel.Recipients) > 0 { 131 | if len(channel.Recipients[0].Username) > 1 { 132 | recipient = channel.Recipients[0].Username 133 | if len(channel.Recipients) > 1 { 134 | for _, user := range channel.Recipients[1:] { 135 | recipient += ", " + user.Username 136 | } 137 | } 138 | } 139 | } 140 | kind := "DM" 141 | if channel.Type == discordgo.ChannelTypeGroupDM { 142 | if len(channel.Recipients) == 0 { 143 | kind = "Empty Group" 144 | } else { 145 | kind = "Group" 146 | } 147 | } 148 | table.AddStrings(channel.ID, kind, recipient) 149 | } 150 | writeln(w, table.String()) 151 | case "channel": 152 | if nargs < 1 { 153 | stdutil.PrintErr("channel ", nil) 154 | return 155 | } 156 | 157 | arg := strings.Join(args, " ") 158 | 159 | var channel *discordgo.Channel 160 | for _, c := range cacheChannels { 161 | if strings.EqualFold(arg, c.Name) { 162 | channel = c 163 | } 164 | } 165 | if channel == nil { 166 | var err error 167 | channel, err = session.Channel(arg) 168 | if err != nil { 169 | stdutil.PrintErr(tl("failed.channel"), err) 170 | return 171 | } 172 | } 173 | 174 | if isPrivate(channel) { 175 | loc.push(nil, channel) 176 | } else { 177 | if loc.guild == nil || channel.GuildID != loc.guild.ID { 178 | guild, err := session.Guild(channel.GuildID) 179 | 180 | if err != nil { 181 | stdutil.PrintErr(tl("failed.guild"), err) 182 | return 183 | } 184 | 185 | loc.push(guild, channel) 186 | } else { 187 | loc.push(loc.guild, channel) 188 | } 189 | } 190 | case "dm": 191 | if nargs < 1 { 192 | stdutil.PrintErr("dm ", nil) 193 | return 194 | } 195 | channel, err := session.UserChannelCreate(args[0]) 196 | if err != nil { 197 | stdutil.PrintErr(tl("failed.channel.create"), err) 198 | return 199 | } 200 | loc.push(nil, channel) 201 | 202 | writeln(w, tl("status.channel")+" "+channel.ID) 203 | case "bookmarks": 204 | for key := range bookmarks { 205 | writeln(w, key) 206 | } 207 | case "bookmark": 208 | if nargs < 1 { 209 | stdutil.PrintErr("bookmark ", nil) 210 | return 211 | } 212 | 213 | name := strings.ToLower(strings.Join(args, " ")) 214 | if strings.HasPrefix(name, "-") { 215 | name = name[1:] 216 | delete(bookmarks, name) 217 | delete(bookmarksCache, name) 218 | } else { 219 | bookmarks[name] = loc.channel.ID 220 | bookmarksCache[name] = loc 221 | } 222 | err := saveBookmarks() 223 | if err != nil { 224 | stdutil.PrintErr(tl("failed.file.save"), err) 225 | } 226 | case "go": 227 | if nargs < 1 { 228 | stdutil.PrintErr("go ", nil) 229 | return 230 | } 231 | name := strings.ToLower(strings.Join(args, " ")) 232 | if cache, ok := bookmarksCache[name]; ok { 233 | loc.push(cache.guild, cache.channel) 234 | return 235 | } 236 | 237 | bookmark, ok := bookmarks[name] 238 | if !ok { 239 | stdutil.PrintErr(tl("invalid.bookmark"), nil) 240 | return 241 | } 242 | 243 | var guild *discordgo.Guild 244 | var channel *discordgo.Channel 245 | var err error 246 | 247 | if bookmark != "" { 248 | channel, err = session.Channel(bookmark) 249 | if err != nil { 250 | stdutil.PrintErr(tl("failed.channel"), err) 251 | return 252 | } 253 | } 254 | 255 | if channel != nil && !isPrivate(channel) { 256 | guild, err = session.Guild(channel.GuildID) 257 | if err != nil { 258 | stdutil.PrintErr(tl("failed.guild"), err) 259 | return 260 | } 261 | } 262 | 263 | bookmarksCache[name] = &location{ 264 | guild: guild, 265 | channel: channel, 266 | } 267 | 268 | loc.push(guild, channel) 269 | } 270 | return 271 | } 272 | 273 | func channels(session *discordgo.Session, kind discordgo.ChannelType, w io.Writer) { 274 | var channels []*discordgo.Channel 275 | if cacheChannels != nil && cachedChannelType == kind { 276 | channels = cacheChannels 277 | } else { 278 | if loc.guild == nil { 279 | stdutil.PrintErr(tl("invalid.guild"), nil) 280 | return 281 | } 282 | channels2, err := session.GuildChannels(loc.guild.ID) 283 | if err != nil { 284 | stdutil.PrintErr(tl("failed.channel"), nil) 285 | return 286 | } 287 | 288 | cacheChannels = channels 289 | cachedChannelType = kind 290 | 291 | channels = make([]*discordgo.Channel, 0) 292 | for _, c := range channels2 { 293 | if c.Type != kind { 294 | continue 295 | } 296 | channels = append(channels, c) 297 | } 298 | 299 | sort.Slice(channels, func(i int, j int) bool { 300 | return channels[i].Position < channels[j].Position 301 | }) 302 | 303 | cacheChannels = channels 304 | cachedChannelType = kind 305 | } 306 | 307 | table := gtable.NewStringTable() 308 | table.AddStrings("ID", "Name") 309 | 310 | for _, channel := range channels { 311 | table.AddRow() 312 | table.AddStrings(channel.ID, channel.Name) 313 | } 314 | 315 | writeln(w, table.String()) 316 | } 317 | 318 | func sortGuilds(guilds []*discordgo.UserGuild, settings *discordgo.Settings) []*discordgo.UserGuild { 319 | // Endpoints aren't always synced when deleted, can't pre-allocate 320 | guilds2 := make([]*discordgo.UserGuild, 0) 321 | for _, g := range settings.GuildPositions { 322 | for _, g2 := range guilds { 323 | if g == g2.ID { 324 | guilds2 = append(guilds2, g2) 325 | } 326 | } 327 | } 328 | 329 | // Remove intercepting 330 | guilds3 := make([]*discordgo.UserGuild, 0) 331 | for _, g := range guilds { 332 | contains := false 333 | for _, g2 := range guilds2 { 334 | if g.ID == g2.ID { 335 | contains = true 336 | } 337 | } 338 | 339 | if !contains { 340 | guilds3 = append(guilds3, g) 341 | } 342 | } 343 | return append(guilds3, guilds2...) 344 | } 345 | -------------------------------------------------------------------------------- /commands_query.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "encoding/json" 22 | "io" 23 | "strconv" 24 | "strings" 25 | "time" 26 | 27 | "github.com/bwmarrin/discordgo" 28 | "github.com/jD91mZM2/stdutil" 29 | ) 30 | 31 | func commandsQuery(session *discordgo.Session, cmd string, args []string, nargs int, w io.Writer) (returnVal string) { 32 | switch cmd { 33 | case "read": 34 | if nargs < 1 { 35 | stdutil.PrintErr("read [property]", nil) 36 | return 37 | } 38 | if loc.channel == nil { 39 | stdutil.PrintErr(tl("invalid.channel"), nil) 40 | return 41 | } 42 | msgID := args[0] 43 | 44 | var msg *discordgo.Message 45 | var err error 46 | if strings.EqualFold(msgID, "cache") { 47 | if cacheRead == nil { 48 | stdutil.PrintErr(tl("invalid.cache"), nil) 49 | return 50 | } 51 | 52 | msg = cacheRead 53 | } else { 54 | msg, err = getMessage(session, loc.channel.ID, msgID) 55 | if err != nil { 56 | stdutil.PrintErr(tl("failed.msg.query"), err) 57 | return 58 | } 59 | 60 | cacheRead = msg 61 | } 62 | 63 | property := "" 64 | if len(args) >= 2 { 65 | property = strings.ToLower(args[1]) 66 | } 67 | switch property { 68 | case "": 69 | printMessage(session, msg, false, loc.guild, loc.channel, w) 70 | case "text": 71 | returnVal = msg.Content 72 | case "channel": 73 | returnVal = msg.ChannelID 74 | case "timestamp": 75 | t, err := timestamp(msg) 76 | if err != nil { 77 | stdutil.PrintErr(tl("failed.timestamp"), err) 78 | return 79 | } 80 | returnVal = t 81 | case "author": 82 | returnVal = msg.Author.ID 83 | case "author_email": 84 | returnVal = msg.Author.Email 85 | case "author_name": 86 | returnVal = msg.Author.Username 87 | case "author_avatar": 88 | returnVal = msg.Author.Avatar 89 | case "author_bot": 90 | returnVal = strconv.FormatBool(msg.Author.Bot) 91 | case "embed": 92 | embed := "{}" 93 | if len(msg.Embeds) >= 1 { 94 | embedBytes, err := json.MarshalIndent(msg.Embeds[0], "", "\t") 95 | if err != nil { 96 | stdutil.PrintErr("Failed to make embed into JSON", err) 97 | return 98 | } 99 | embed = string(embedBytes) 100 | } 101 | returnVal = embed 102 | default: 103 | stdutil.PrintErr(tl("invalid.value"), nil) 104 | } 105 | 106 | lastUsedMsg = msg.ID 107 | if returnVal != "" { 108 | writeln(w, returnVal) 109 | } 110 | case "info": 111 | if nargs < 1 { 112 | stdutil.PrintErr("info (for user: ) [property] (or info u/g/c/s)", nil) 113 | return 114 | } 115 | switch strings.ToLower(args[0]) { 116 | case "channel", "c": 117 | if loc.channel == nil { 118 | stdutil.PrintErr(tl("invalid.channel"), nil) 119 | return 120 | } 121 | 122 | values := chan2array(loc.channel) 123 | 124 | if nargs < 2 { 125 | for _, keyval := range values { 126 | writeln(w, keyval.String()) 127 | } 128 | } else { 129 | var ok bool 130 | returnVal, ok = findValByKey(values, args[1]) 131 | if !ok { 132 | stdutil.PrintErr(tl("invalid.value"), nil) 133 | return 134 | } 135 | 136 | writeln(w, returnVal) 137 | } 138 | case "guild", "g": 139 | if loc.guild == nil { 140 | stdutil.PrintErr(tl("invalid.guild"), nil) 141 | return 142 | } 143 | 144 | values := guild2array(loc.guild) 145 | 146 | if nargs < 2 { 147 | for _, keyval := range values { 148 | writeln(w, keyval.String()) 149 | } 150 | } else { 151 | var ok bool 152 | returnVal, ok = findValByKey(values, args[1]) 153 | if !ok { 154 | stdutil.PrintErr(tl("invalid.value"), nil) 155 | return 156 | } 157 | 158 | writeln(w, returnVal) 159 | } 160 | case "user", "u": 161 | if nargs < 2 { 162 | stdutil.PrintErr("info user/u [property]", nil) 163 | return 164 | } 165 | id := args[1] 166 | var keyvals []*keyval 167 | 168 | if strings.EqualFold(id, "cache") { 169 | if cacheUser == nil { 170 | stdutil.PrintErr(tl("invalid.cache"), nil) 171 | return 172 | } 173 | 174 | keyvals = cacheUser 175 | } else { 176 | 177 | user, err := session.User(id) 178 | if err != nil { 179 | stdutil.PrintErr(tl("failed.user"), err) 180 | return 181 | } 182 | 183 | keyvals = user2array(user) 184 | cacheUser = keyvals 185 | } 186 | 187 | if nargs < 3 { 188 | for _, keyval := range keyvals { 189 | writeln(w, keyval.String()) 190 | } 191 | } else { 192 | var ok bool 193 | returnVal, ok = findValByKey(keyvals, args[2]) 194 | if !ok { 195 | stdutil.PrintErr(tl("invalid.value"), nil) 196 | return 197 | } 198 | 199 | writeln(w, returnVal) 200 | } 201 | case "settings", "s": 202 | if userType == typeBot { 203 | stdutil.PrintErr(tl("invalid.onlyfor.users"), nil) 204 | return 205 | } 206 | settings, err := session.UserSettings() 207 | if err != nil { 208 | stdutil.PrintErr(tl("failed.settings"), err) 209 | return 210 | } 211 | values := settings2array(settings) 212 | 213 | if nargs < 2 { 214 | for _, keyval := range values { 215 | writeln(w, keyval.String()) 216 | } 217 | } else { 218 | var ok bool 219 | returnVal, ok = findValByKey(values, args[1]) 220 | if !ok { 221 | stdutil.PrintErr(tl("invalid.value"), nil) 222 | return 223 | } 224 | 225 | writeln(w, returnVal) 226 | } 227 | default: 228 | stdutil.PrintErr("info (for user: ) [property] (or info u/g/c/s)", nil) 229 | } 230 | } 231 | return 232 | } 233 | 234 | func guild2array(guild *discordgo.Guild) []*keyval { 235 | return []*keyval{ 236 | &keyval{"ID", guild.ID}, 237 | &keyval{"Name", guild.Name}, 238 | &keyval{"Icon", guild.Icon}, 239 | &keyval{"Region", guild.Region}, 240 | &keyval{"Owner", guild.OwnerID}, 241 | &keyval{"Join messages", guild.SystemChannelID}, 242 | &keyval{"Widget channel", guild.WidgetChannelID}, 243 | &keyval{"AFK channel", guild.AfkChannelID}, 244 | &keyval{"AFK timeout", strconv.Itoa(guild.AfkTimeout)}, 245 | &keyval{"Members", strconv.Itoa(guild.MemberCount)}, 246 | &keyval{"Verification", typeVerifications[guild.VerificationLevel]}, 247 | &keyval{"Admin MFA", typeMfa[guild.MfaLevel]}, 248 | &keyval{"Explicit Content Filter", typeContentFilter[guild.ExplicitContentFilter]}, 249 | &keyval{"Unavailable", strconv.FormatBool(guild.Unavailable)}, 250 | } 251 | } 252 | 253 | func chan2array(channel *discordgo.Channel) []*keyval { 254 | return []*keyval{ 255 | &keyval{"ID", channel.ID}, 256 | &keyval{"Guild", channel.GuildID}, 257 | &keyval{"Name", channel.Name}, 258 | &keyval{"Topic", channel.Topic}, 259 | &keyval{"Type", typeChannel[channel.Type]}, 260 | &keyval{"NSFW", strconv.FormatBool(channel.NSFW)}, 261 | &keyval{"Parent category", channel.ParentID}, 262 | &keyval{"Last message", channel.LastMessageID}, 263 | &keyval{"Bitrate", strconv.Itoa(channel.Bitrate)}, 264 | &keyval{"User limit", strconv.Itoa(channel.UserLimit)}, 265 | } 266 | } 267 | 268 | func user2array(user *discordgo.User) []*keyval { 269 | return []*keyval{ 270 | &keyval{"ID", user.ID}, 271 | &keyval{"Email", user.Email}, 272 | &keyval{"Name", user.Username}, 273 | &keyval{"Discrim", user.Discriminator}, 274 | &keyval{"Avatar", user.Avatar}, 275 | &keyval{"Avatar URL", user.AvatarURL("1024")}, 276 | &keyval{"Verified", strconv.FormatBool(user.Verified)}, 277 | &keyval{"MFA Enabled", strconv.FormatBool(user.MFAEnabled)}, 278 | &keyval{"Bot", strconv.FormatBool(user.Bot)}, 279 | } 280 | } 281 | 282 | func settings2array(settings *discordgo.Settings) []*keyval { 283 | return []*keyval{ 284 | &keyval{"Theme", settings.Theme}, 285 | &keyval{"Compact", strconv.FormatBool(settings.MessageDisplayCompact)}, 286 | &keyval{"Locale", settings.Locale}, 287 | &keyval{"TTS", strconv.FormatBool(settings.EnableTtsCommand)}, 288 | &keyval{"Convert emotes", strconv.FormatBool(settings.ConvertEmoticons)}, 289 | &keyval{"Attachments", strconv.FormatBool(settings.InlineAttachmentMedia)}, 290 | &keyval{"Media embeds", strconv.FormatBool(settings.InlineEmbedMedia)}, 291 | &keyval{"Show embeds", strconv.FormatBool(settings.RenderEmbeds)}, 292 | &keyval{"Show current game", strconv.FormatBool(settings.ShowCurrentGame)}, 293 | &keyval{"Dev mode", strconv.FormatBool(settings.DeveloperMode)}, 294 | &keyval{"Platform accounts", strconv.FormatBool(settings.DetectPlatformAccounts)}, 295 | } 296 | } 297 | 298 | func invite2array(invite *discordgo.Invite) []*keyval { 299 | values := make([]*keyval, 0) 300 | if invite.Inviter != nil { 301 | for _, keyval := range user2array(invite.Inviter) { 302 | keyval.Key = "Inviter_" + keyval.Key 303 | values = append(values, keyval) 304 | } 305 | } 306 | for _, keyval := range guild2array(invite.Guild) { 307 | keyval.Key = "Guild_" + keyval.Key 308 | values = append(values, keyval) 309 | } 310 | for _, keyval := range chan2array(invite.Channel) { 311 | keyval.Key = "Channel_" + keyval.Key 312 | values = append(values, keyval) 313 | } 314 | values = append(values, 315 | &keyval{"Created_at", string(invite.CreatedAt)}, 316 | &keyval{"Max_age", (time.Duration(invite.MaxAge) * time.Second).String()}, 317 | &keyval{"Max_uses", strconv.Itoa(invite.MaxUses)}, 318 | &keyval{"Uses", strconv.Itoa(invite.Uses)}, 319 | &keyval{"Revoked", strconv.FormatBool(invite.Revoked)}, 320 | &keyval{"Temporary", strconv.FormatBool(invite.Temporary)}, 321 | ) 322 | return values 323 | } 324 | -------------------------------------------------------------------------------- /commands_roles.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "io" 22 | "sort" 23 | "strconv" 24 | "strings" 25 | 26 | "github.com/bwmarrin/discordgo" 27 | "github.com/jD91mZM2/gtable" 28 | "github.com/jD91mZM2/stdutil" 29 | ) 30 | 31 | func commandsRoles(session *discordgo.Session, cmd string, args []string, nargs int, w io.Writer) (returnVal string) { 32 | switch cmd { 33 | case "role": 34 | if nargs < 1 { 35 | stdutil.PrintErr("role list/add/rem/create/edit/delete", nil) 36 | return 37 | } 38 | switch strings.ToLower(args[0]) { 39 | case "list": 40 | if loc.guild == nil { 41 | stdutil.PrintErr(tl("invalid.guild"), nil) 42 | return 43 | } 44 | 45 | roles, err := session.GuildRoles(loc.guild.ID) 46 | if err != nil { 47 | stdutil.PrintErr(tl("failed.roles"), err) 48 | return 49 | } 50 | sort.Slice(roles, func(i, j int) bool { 51 | return roles[i].Position > roles[j].Position 52 | }) 53 | 54 | table := gtable.NewStringTable() 55 | table.AddStrings("ID", "Name", "Permissions", "Color") 56 | 57 | for _, role := range roles { 58 | table.AddRow() 59 | table.AddStrings(role.ID, role.Name, strconv.Itoa(role.Permissions), strconv.Itoa(role.Color)) 60 | } 61 | 62 | writeln(w, table.String()) 63 | case "add": 64 | fallthrough 65 | case "rem": 66 | if nargs < 3 { 67 | stdutil.PrintErr("role add/rem ", nil) 68 | return 69 | } 70 | if loc.guild == nil { 71 | stdutil.PrintErr(tl("invalid.guild"), nil) 72 | return 73 | } 74 | 75 | var err error 76 | if args[0] == "add" { 77 | err = session.GuildMemberRoleAdd(loc.guild.ID, args[1], args[2]) 78 | } else { 79 | err = session.GuildMemberRoleRemove(loc.guild.ID, args[1], args[2]) 80 | } 81 | 82 | if err != nil { 83 | stdutil.PrintErr(tl("failed.role.change"), err) 84 | } 85 | case "create": 86 | if loc.guild == nil { 87 | stdutil.PrintErr(tl("invalid.guild"), nil) 88 | return 89 | } 90 | 91 | role, err := session.GuildRoleCreate(loc.guild.ID) 92 | if err != nil { 93 | stdutil.PrintErr(tl("failed.role.create"), err) 94 | return 95 | } 96 | writeln(w, "Created role with ID "+role.ID) 97 | lastUsedRole = role.ID 98 | returnVal = role.ID 99 | case "edit": 100 | if nargs < 4 { 101 | stdutil.PrintErr("role edit ", nil) 102 | return 103 | } 104 | if loc.guild == nil { 105 | stdutil.PrintErr(tl("invalid.guild"), nil) 106 | return 107 | } 108 | 109 | value := strings.Join(args[3:], " ") 110 | 111 | roles, err := session.GuildRoles(loc.guild.ID) 112 | if err != nil { 113 | stdutil.PrintErr(tl("failed.roles"), err) 114 | return 115 | } 116 | 117 | var role *discordgo.Role 118 | for _, r := range roles { 119 | if r.ID == args[1] { 120 | role = r 121 | break 122 | } 123 | } 124 | if role == nil { 125 | stdutil.PrintErr(tl("invalid.role"), nil) 126 | return 127 | } 128 | 129 | name := role.Name 130 | color := int64(role.Color) 131 | hoist := role.Hoist 132 | perms := role.Permissions 133 | mention := role.Mentionable 134 | 135 | switch strings.ToLower(args[2]) { 136 | case "name": 137 | name = value 138 | case "color": 139 | value = strings.TrimPrefix(value, "#") 140 | color, err = strconv.ParseInt(value, 16, 0) 141 | if err != nil { 142 | stdutil.PrintErr(tl("invalid.number"), nil) 143 | return 144 | } 145 | case "separate": 146 | hoist, err = parseBool(value) 147 | if err != nil { 148 | stdutil.PrintErr(err.Error(), nil) 149 | return 150 | } 151 | case "perms": 152 | perms, err = strconv.Atoi(value) 153 | if err != nil { 154 | stdutil.PrintErr(tl("invalid.number"), nil) 155 | return 156 | } 157 | case "mention": 158 | mention, err = parseBool(value) 159 | if err != nil { 160 | stdutil.PrintErr(err.Error(), nil) 161 | return 162 | } 163 | default: 164 | stdutil.PrintErr(tl("invalid.value")+": "+args[2], nil) 165 | return 166 | } 167 | 168 | role, err = session.GuildRoleEdit(loc.guild.ID, args[1], name, int(color), hoist, perms, mention) 169 | if err != nil { 170 | stdutil.PrintErr(tl("failed.role.edit"), err) 171 | return 172 | } 173 | lastUsedRole = role.ID 174 | writeln(w, "Edited role "+role.ID) 175 | case "delete": 176 | if nargs < 2 { 177 | stdutil.PrintErr("role delete ", nil) 178 | return 179 | } 180 | if loc.guild == nil { 181 | stdutil.PrintErr(tl("invalid.guild"), nil) 182 | return 183 | } 184 | 185 | err := session.GuildRoleDelete(loc.guild.ID, args[1]) 186 | if err != nil { 187 | stdutil.PrintErr(tl("failed.role.delete"), err) 188 | } 189 | default: 190 | stdutil.PrintErr("role list/add/rem/create/edit/delete", nil) 191 | return 192 | } 193 | } 194 | return 195 | } 196 | -------------------------------------------------------------------------------- /commands_say.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "bufio" 22 | "encoding/json" 23 | "io" 24 | "os" 25 | "path/filepath" 26 | "strconv" 27 | "strings" 28 | 29 | "github.com/bwmarrin/discordgo" 30 | "github.com/chzyer/readline" 31 | "github.com/fatih/color" 32 | "github.com/jD91mZM2/stdutil" 33 | ) 34 | 35 | func commandsSay(session *discordgo.Session, source commandSource, cmd string, args []string, nargs int, w io.Writer) (returnVal string) { 36 | switch cmd { 37 | case "tts": 38 | fallthrough 39 | case "say": 40 | if nargs < 1 { 41 | stdutil.PrintErr("say/tts ", nil) 42 | return 43 | } 44 | if loc.channel == nil && userType != typeWebhook { 45 | stdutil.PrintErr(tl("invalid.channel"), nil) 46 | return 47 | } 48 | toggle := false 49 | tts := cmd == "tts" 50 | parts := args 51 | 52 | sendbuf := func(buffer string) (ok bool) { 53 | if userType == typeWebhook { 54 | _, err := session.WebhookExecute(userID, userToken, false, &discordgo.WebhookParams{ 55 | Content: buffer, 56 | TTS: tts, 57 | }) 58 | if err != nil { 59 | stdutil.PrintErr(tl("failed.msg.send"), err) 60 | return 61 | } 62 | ok = true 63 | return 64 | } 65 | 66 | msg, err := session.ChannelMessageSendComplex(loc.channel.ID, &discordgo.MessageSend{ 67 | Content: buffer, 68 | Tts: tts, 69 | }) 70 | if err != nil { 71 | stdutil.PrintErr(tl("failed.msg.send"), err) 72 | return 73 | } 74 | writeln(w, tl("status.msg.create")+" "+msg.ID) 75 | lastUsedMsg = msg.ID 76 | returnVal = msg.ID 77 | ok = true 78 | return 79 | } 80 | 81 | outer: 82 | for { 83 | msgStr := strings.Join(parts, " ") 84 | if source.Terminal && msgStr == "toggle" { 85 | toggle = !toggle 86 | } else { 87 | /* 88 | if len(msgStr) > msgLimit { 89 | stdutil.PrintErr(tl("invalid.limit.message"), nil) 90 | return 91 | } 92 | */ 93 | 94 | for len(msgStr) > msgLimit { 95 | buffer := msgStr[:msgLimit] 96 | msgStr = msgStr[msgLimit:] 97 | 98 | if !sendbuf(buffer) { 99 | return 100 | } 101 | } 102 | sendbuf(msgStr) 103 | } 104 | 105 | if !toggle { 106 | break 107 | } 108 | 109 | for { 110 | color.Unset() 111 | colorChatMode.Set() 112 | 113 | text, err := rl.Readline() 114 | if err != nil { 115 | if err != readline.ErrInterrupt && err != io.EOF { 116 | stdutil.PrintErr(tl("failed.readline.read"), err) 117 | } 118 | break outer 119 | } 120 | 121 | color.Unset() 122 | 123 | parts, err = parse(substitute, text) 124 | if len(parts) >= 1 { 125 | break 126 | } 127 | } 128 | } 129 | case "embed": 130 | if nargs < 1 { 131 | stdutil.PrintErr("embed ", nil) 132 | return 133 | } 134 | if loc.channel == nil && userType != typeWebhook { 135 | stdutil.PrintErr(tl("invalid.channel"), nil) 136 | return 137 | } 138 | 139 | jsonstr := strings.Join(args, " ") 140 | var embed = &discordgo.MessageEmbed{} 141 | 142 | err := json.Unmarshal([]byte(jsonstr), embed) 143 | if err != nil { 144 | stdutil.PrintErr(tl("failed.json"), err) 145 | return 146 | } 147 | 148 | if userType == typeWebhook { 149 | _, err = session.WebhookExecute(userID, userToken, false, &discordgo.WebhookParams{ 150 | Embeds: []*discordgo.MessageEmbed{embed}, 151 | }) 152 | if err != nil { 153 | stdutil.PrintErr(tl("failed.msg.send"), err) 154 | return 155 | } 156 | } else { 157 | msg, err := session.ChannelMessageSendEmbed(loc.channel.ID, embed) 158 | if err != nil { 159 | stdutil.PrintErr(tl("failed.msg.send"), err) 160 | return 161 | } 162 | writeln(w, tl("status.msg.create")+" "+msg.ID) 163 | lastUsedMsg = msg.ID 164 | returnVal = msg.ID 165 | } 166 | case "big": 167 | if nargs < 1 { 168 | stdutil.PrintErr("big ", nil) 169 | return 170 | } 171 | if loc.channel == nil && userType != typeWebhook { 172 | stdutil.PrintErr(tl("invalid.channel"), nil) 173 | return 174 | } 175 | 176 | buffer := "" 177 | for _, c := range strings.Join(args, " ") { 178 | str := toEmojiString(c) 179 | if len(buffer)+len(str) > msgLimit { 180 | var msg *discordgo.Message 181 | if userType == typeWebhook { // Webhook checking. 182 | msg, _ = say(session, w, "", buffer) 183 | } else { 184 | msg, _ = say(session, w, loc.channel.ID, buffer) 185 | } 186 | 187 | if msg != nil { 188 | lastUsedMsg = msg.ID 189 | returnVal = msg.ID 190 | } 191 | 192 | buffer = "" 193 | } 194 | buffer += str 195 | } 196 | if buffer != "" { 197 | var msg *discordgo.Message 198 | if userType == typeWebhook { // Webhook checking. 199 | msg, _ = say(session, w, "", buffer) 200 | } else { 201 | msg, _ = say(session, w, loc.channel.ID, buffer) 202 | } 203 | 204 | if msg != nil { 205 | lastUsedMsg = msg.ID 206 | returnVal = msg.ID 207 | } 208 | } 209 | case "sayfile": 210 | if nargs < 1 { 211 | stdutil.PrintErr("sayfile ", nil) 212 | return 213 | } 214 | if loc.channel == nil && userType != typeWebhook { 215 | stdutil.PrintErr(tl("invalid.channel"), nil) 216 | return 217 | } 218 | 219 | channel := "" 220 | if loc.channel != nil { 221 | channel = loc.channel.ID 222 | } 223 | 224 | path := args[0] 225 | err := fixPath(&path) 226 | if err != nil { 227 | stdutil.PrintErr(tl("failed.fixpath"), err) 228 | return 229 | } 230 | 231 | reader, err := os.Open(path) 232 | if err != nil { 233 | stdutil.PrintErr(tl("failed.file.open"), err) 234 | return 235 | } 236 | defer reader.Close() 237 | 238 | scanner := bufio.NewScanner(reader) 239 | buffer := "" 240 | 241 | for i := 1; scanner.Scan(); i++ { 242 | text := scanner.Text() 243 | if len(text) > msgLimit { 244 | stdutil.PrintErr("Line "+strconv.Itoa(i)+" exceeded "+strconv.Itoa(msgLimit)+" characters.", nil) 245 | return 246 | } else if len(buffer)+len(text) > msgLimit { 247 | msg, ok := say(session, w, channel, buffer) 248 | if !ok { 249 | return 250 | } 251 | if msg != nil { 252 | returnVal = msg.ID 253 | lastUsedMsg = msg.ID 254 | } 255 | 256 | buffer = "" 257 | } 258 | buffer += text + "\n" 259 | } 260 | 261 | err = scanner.Err() 262 | if err != nil { 263 | stdutil.PrintErr(tl("failed.file.read"), err) 264 | return 265 | } 266 | if buffer != "" { 267 | msg, _ := say(session, w, channel, buffer) 268 | if msg != nil { 269 | returnVal = msg.ID 270 | lastUsedMsg = msg.ID 271 | } 272 | } 273 | case "file": 274 | if nargs < 1 { 275 | stdutil.PrintErr("file ", nil) 276 | return 277 | } 278 | if loc.channel == nil { 279 | stdutil.PrintErr(tl("invalid.channel"), nil) 280 | return 281 | } 282 | name := strings.Join(args, " ") 283 | err := fixPath(&name) 284 | if err != nil { 285 | stdutil.PrintErr(tl("failed.fixpath"), err) 286 | } 287 | 288 | file, err := os.Open(name) 289 | if err != nil { 290 | stdutil.PrintErr(tl("failed.file.open"), nil) 291 | return 292 | } 293 | msg, err := session.ChannelFileSend(loc.channel.ID, filepath.Base(name), file) 294 | if err != nil { 295 | stdutil.PrintErr(tl("failed.msg.send"), err) 296 | return 297 | } 298 | file.Close() 299 | writeln(w, tl("status.msg.create")+" "+msg.ID) 300 | returnVal = msg.ID 301 | case "quote": 302 | if nargs < 1 { 303 | stdutil.PrintErr("quote ", nil) 304 | return 305 | } 306 | if loc.channel == nil { 307 | stdutil.PrintErr(tl("invalid.channel"), nil) 308 | return 309 | } 310 | 311 | msg, err := getMessage(session, loc.channel.ID, args[0]) 312 | if err != nil { 313 | stdutil.PrintErr(tl("failed.msg.query"), err) 314 | return 315 | } 316 | 317 | t, err := timestamp(msg) 318 | if err != nil { 319 | stdutil.PrintErr(tl("failed.timestamp"), err) 320 | return 321 | } 322 | 323 | msg, err = session.ChannelMessageSendEmbed(loc.channel.ID, &discordgo.MessageEmbed{ 324 | Author: &discordgo.MessageEmbedAuthor{ 325 | Name: msg.Author.Username, 326 | IconURL: discordgo.EndpointUserAvatar(msg.Author.ID, msg.Author.Avatar), 327 | }, 328 | Description: msg.Content, 329 | Footer: &discordgo.MessageEmbedFooter{ 330 | Text: "Sent " + t, 331 | }, 332 | }) 333 | if err != nil { 334 | stdutil.PrintErr(tl("failed.msg.send"), err) 335 | return 336 | } 337 | writeln(w, tl("status.msg.create")+" "+msg.ID) 338 | lastUsedMsg = msg.ID 339 | returnVal = msg.ID 340 | case "editembed": 341 | fallthrough 342 | case "edit": 343 | if nargs < 2 { 344 | stdutil.PrintErr("edit ", nil) 345 | return 346 | } 347 | if loc.channel == nil { 348 | stdutil.PrintErr(tl("invalid.channel"), nil) 349 | return 350 | } 351 | 352 | id := args[0] 353 | contents := strings.Join(args[1:], " ") 354 | 355 | var msg *discordgo.Message 356 | var err error 357 | if cmd == "editembed" { 358 | var embed = &discordgo.MessageEmbed{} 359 | err := json.Unmarshal([]byte(contents), embed) 360 | if err != nil { 361 | stdutil.PrintErr(tl("failed.json"), err) 362 | return 363 | } 364 | 365 | msg, err = session.ChannelMessageEditEmbed(loc.channel.ID, id, embed) 366 | } else { 367 | msg, err = session.ChannelMessageEdit(loc.channel.ID, id, contents) 368 | } 369 | if err != nil { 370 | stdutil.PrintErr(tl("failed.msg.edit"), err) 371 | return 372 | } 373 | lastUsedMsg = msg.ID 374 | case "del": 375 | if nargs < 1 { 376 | stdutil.PrintErr("del ", nil) 377 | return 378 | } 379 | if loc.channel == nil { 380 | stdutil.PrintErr(tl("invalid.channel"), nil) 381 | return 382 | } 383 | 384 | err := session.ChannelMessageDelete(loc.channel.ID, args[0]) 385 | if err != nil { 386 | stdutil.PrintErr(tl("failed.msg.delete"), err) 387 | return 388 | } 389 | case "delall": 390 | if loc.channel == nil { 391 | stdutil.PrintErr(tl("invalid.channel"), nil) 392 | return 393 | } 394 | if isPrivate(loc.channel) { 395 | stdutil.PrintErr(tl("invalid.dm"), nil) 396 | return 397 | } 398 | since := "" 399 | if nargs >= 1 { 400 | since = args[0] 401 | } 402 | messages, err := session.ChannelMessages(loc.channel.ID, 100, "", since, "") 403 | if err != nil { 404 | stdutil.PrintErr(tl("failed.msg.query"), err) 405 | return 406 | } 407 | 408 | ids := make([]string, len(messages)) 409 | for i, msg := range messages { 410 | ids[i] = msg.ID 411 | } 412 | 413 | err = session.ChannelMessagesBulkDelete(loc.channel.ID, ids) 414 | if err != nil { 415 | stdutil.PrintErr(tl("failed.msg.query"), err) 416 | return 417 | } 418 | returnVal := strconv.Itoa(len(ids)) 419 | writeln(w, strings.Replace(tl("status.msg.delall"), "#", returnVal, -1)) 420 | } 421 | return 422 | } 423 | -------------------------------------------------------------------------------- /commands_usermod.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "encoding/base64" 23 | "io" 24 | "net/http" 25 | "os" 26 | "strings" 27 | "time" 28 | 29 | "github.com/bwmarrin/discordgo" 30 | "github.com/jD91mZM2/stdutil" 31 | ) 32 | 33 | func commandsUserMod(session *discordgo.Session, cmd string, args []string, nargs int, w io.Writer) (returnVal string) { 34 | switch cmd { 35 | case "avatar": 36 | if nargs < 1 { 37 | stdutil.PrintErr("avatar ", nil) 38 | return 39 | } 40 | 41 | var reader io.Reader 42 | resource := strings.Join(args, " ") 43 | 44 | if strings.HasPrefix(resource, "https://") || strings.HasPrefix(resource, "http://") { 45 | res, err := http.Get(resource) 46 | if err != nil { 47 | stdutil.PrintErr(tl("failed.webrequest"), err) 48 | return 49 | } 50 | defer res.Body.Close() 51 | 52 | reader = res.Body 53 | } else { 54 | err := fixPath(&resource) 55 | if err != nil { 56 | stdutil.PrintErr(tl("failed.fixpath"), err) 57 | return 58 | } 59 | 60 | r, err := os.Open(resource) 61 | defer r.Close() 62 | if err != nil { 63 | stdutil.PrintErr(tl("failed.file.open"), err) 64 | return 65 | } 66 | 67 | reader = r 68 | } 69 | 70 | writer := bytes.NewBuffer([]byte{}) 71 | b64 := base64.NewEncoder(base64.StdEncoding, writer) 72 | 73 | _, err := io.Copy(b64, reader) 74 | if err != nil { 75 | stdutil.PrintErr(tl("failed.base64"), err) 76 | return 77 | } 78 | b64.Close() 79 | 80 | // Too lazy to detect image type. Seems to work anyway ¯\_(ツ)_/¯ 81 | str := "data:image/png;base64," + writer.String() 82 | 83 | if userType == typeWebhook { 84 | _, err = session.WebhookEditWithToken(userID, userToken, "", str) 85 | if err != nil { 86 | stdutil.PrintErr(tl("failed.avatar"), err) 87 | return 88 | } 89 | return 90 | } 91 | 92 | user, err := session.User("@me") 93 | if err != nil { 94 | stdutil.PrintErr(tl("failed.user"), err) 95 | return 96 | } 97 | 98 | _, err = session.UserUpdate("", "", user.Username, str, "") 99 | if err != nil { 100 | stdutil.PrintErr(tl("failed.avatar"), err) 101 | return 102 | } 103 | writeln(w, tl("status.avatar")) 104 | case "name": 105 | if nargs < 1 { 106 | stdutil.PrintErr("name ", nil) 107 | return 108 | } 109 | 110 | if userType == typeWebhook { 111 | _, err := session.WebhookEditWithToken(userID, userToken, strings.Join(args, " "), "") 112 | if err != nil { 113 | stdutil.PrintErr(tl("failed.user.edit"), err) 114 | } 115 | return 116 | } 117 | 118 | user, err := session.User("@me") 119 | if err != nil { 120 | stdutil.PrintErr(tl("failed.user"), err) 121 | return 122 | } 123 | 124 | user, err = session.UserUpdate("", "", strings.Join(args, " "), user.Avatar, "") 125 | if err != nil { 126 | stdutil.PrintErr(tl("failed.user.edit"), err) 127 | return 128 | } 129 | writeln(w, tl("status.name")) 130 | case "playing": 131 | err := session.UpdateStatus(0, strings.Join(args, " ")) 132 | if err != nil { 133 | stdutil.PrintErr(tl("failed.status"), err) 134 | } 135 | case "streaming": 136 | var err error 137 | if nargs <= 0 { 138 | err = session.UpdateStreamingStatus(0, "", "") 139 | } else if nargs < 2 { 140 | err = session.UpdateStreamingStatus(0, strings.Join(args[1:], " "), "") 141 | } else { 142 | err = session.UpdateStreamingStatus(0, strings.Join(args[1:], " "), args[0]) 143 | } 144 | if err != nil { 145 | stdutil.PrintErr(tl("failed.status"), err) 146 | } 147 | case "typing": 148 | if loc.channel == nil { 149 | stdutil.PrintErr(tl("failed.channel"), nil) 150 | return 151 | } 152 | err := session.ChannelTyping(loc.channel.ID) 153 | if err != nil { 154 | stdutil.PrintErr(tl("failed.typing"), err) 155 | } 156 | case "nick": 157 | if loc.guild == nil { 158 | stdutil.PrintErr(tl("invalid.guild"), nil) 159 | return 160 | } 161 | if nargs < 1 { 162 | stdutil.PrintErr("nick [nickname]", nil) 163 | return 164 | } 165 | 166 | who := args[0] 167 | if strings.EqualFold(who, "@me") { 168 | who = "@me" 169 | } 170 | 171 | err := session.GuildMemberNickname(loc.guild.ID, who, strings.Join(args[1:], " ")) 172 | if err != nil { 173 | stdutil.PrintErr(tl("failed.nick"), err) 174 | } 175 | case "status": 176 | if nargs < 1 { 177 | stdutil.PrintErr("status ", nil) 178 | return 179 | } 180 | if userType != typeUser { 181 | stdutil.PrintErr(tl("invalid.onlyfor.users"), nil) 182 | return 183 | } 184 | 185 | status, ok := typeStatuses[strings.ToLower(args[0])] 186 | if !ok { 187 | stdutil.PrintErr(tl("invalid.value"), nil) 188 | return 189 | } 190 | 191 | if status == discordgo.StatusOffline { 192 | stdutil.PrintErr(tl("invalid.status.offline"), nil) 193 | return 194 | } 195 | 196 | _, err := session.UserUpdateStatus(status) 197 | if err != nil { 198 | stdutil.PrintErr(tl("failed.status"), err) 199 | return 200 | } 201 | writeln(w, tl("status.status")) 202 | case "game": // if anyone has a better name for this then @ me 203 | if nargs < 2 { 204 | stdutil.PrintErr("game [details] [extra text]", nil) 205 | return 206 | } 207 | status, ok := typeGames[strings.ToLower(args[0])] 208 | if !ok { 209 | stdutil.PrintErr(tl("invalid.value"), nil) 210 | return 211 | } 212 | details := "" 213 | lt := "" 214 | if nargs > 2 { 215 | details = args[2] 216 | } 217 | if nargs > 3 { 218 | lt = args[3] 219 | } 220 | game := &discordgo.Game{ 221 | Name: args[1], 222 | Details: details, 223 | Type: status, 224 | TimeStamps: discordgo.TimeStamps{StartTimestamp: time.Now().Unix()}, 225 | Assets: discordgo.Assets{LargeText: lt}, 226 | } 227 | statusData := discordgo.UpdateStatusData{new(int), game, false, ""} 228 | err := session.UpdateStatusComplex(statusData) 229 | if err != nil { 230 | stdutil.PrintErr(tl("failed.status"), err) 231 | } 232 | } 233 | return 234 | } 235 | -------------------------------------------------------------------------------- /completer.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "github.com/chzyer/readline" 22 | ) 23 | 24 | func setCompleter(rl *readline.Instance) { 25 | rl.Config.AutoComplete = readline.NewPrefixCompleter( 26 | readline.PcItem("guild", readline.PcItemDynamic(func(name string) []string { 27 | names := make([]string, len(cacheGuilds)) 28 | for i, g := range cacheGuilds { 29 | names[i] = g.Name 30 | } 31 | return names 32 | })), 33 | // please let me know if I can skip repeating this twice 34 | readline.PcItem("server", readline.PcItemDynamic(func(name string) []string { 35 | names := make([]string, len(cacheGuilds)) 36 | for i, g := range cacheGuilds { 37 | names[i] = g.Name 38 | } 39 | return names 40 | })), 41 | readline.PcItem("channel", readline.PcItemDynamic(func(name string) []string { 42 | names := make([]string, len(cacheChannels)) 43 | for i, c := range cacheChannels { 44 | names[i] = c.Name 45 | } 46 | return names 47 | })), 48 | 49 | readline.PcItem("edit", readline.PcItemDynamic(singleValue(&lastUsedMsg))), 50 | readline.PcItem("del", readline.PcItemDynamic(singleValue(&lastUsedMsg))), 51 | readline.PcItem("quote", readline.PcItemDynamic(singleValue(&lastUsedMsg))), 52 | readline.PcItem("react", 53 | readline.PcItem("add", readline.PcItemDynamic(singleValue(&lastUsedMsg))), 54 | readline.PcItem("del", readline.PcItemDynamic(singleValue(&lastUsedMsg))), 55 | readline.PcItem("big", readline.PcItemDynamic(singleValue(&lastUsedMsg))), 56 | readline.PcItem("delall", readline.PcItemDynamic(singleValue(&lastUsedMsg))), 57 | ), 58 | 59 | readline.PcItem("role", 60 | readline.PcItem("add", readline.PcItem(userID, readline.PcItemDynamic(singleValue(&lastUsedRole)))), 61 | readline.PcItem("rem", readline.PcItem(userID, readline.PcItemDynamic(singleValue(&lastUsedRole)))), 62 | readline.PcItem("edit", readline.PcItemDynamic(singleValue(&lastUsedRole))), 63 | readline.PcItem("delete", readline.PcItemDynamic(singleValue(&lastUsedRole))), 64 | ), 65 | 66 | readline.PcItem("bookmark", readline.PcItemDynamic(bookmarkTab)), 67 | readline.PcItem("go", readline.PcItemDynamic(bookmarkTab)), 68 | 69 | readline.PcItem("read", readline.PcItemDynamic(singleValue(&lastUsedMsg))), 70 | readline.PcItem("info", 71 | readline.PcItem("channel", 72 | readline.PcItem("id"), 73 | readline.PcItem("guild"), 74 | readline.PcItem("name"), 75 | readline.PcItem("topic"), 76 | readline.PcItem("type"), 77 | readline.PcItem("nsfw"), 78 | readline.PcItem("\"parent category\""), 79 | readline.PcItem("\"last message\""), 80 | readline.PcItem("bitrate"), 81 | readline.PcItem("\"user limit\""), 82 | ), 83 | readline.PcItem("guild", 84 | readline.PcItem("id"), 85 | readline.PcItem("name"), 86 | readline.PcItem("icon"), 87 | readline.PcItem("region"), 88 | readline.PcItem("owner"), 89 | readline.PcItem("\"join messages\""), 90 | readline.PcItem("\"widget channel\""), 91 | readline.PcItem("\"afk channel\""), 92 | readline.PcItem("\"afk timeout\""), 93 | readline.PcItem("members"), 94 | readline.PcItem("verification"), 95 | readline.PcItem("\"admin mfa\""), 96 | readline.PcItem("\"explicit content filter\""), 97 | readline.PcItem("unavailable"), 98 | ), 99 | // only @me gets autocomplete for the rest, 100 | // idk how i can make it efficiently work for both. 101 | readline.PcItem("user", 102 | readline.PcItem("@me", 103 | readline.PcItem("id"), 104 | readline.PcItem("email"), 105 | readline.PcItem("name"), 106 | readline.PcItem("discrim"), 107 | readline.PcItem("locale"), 108 | readline.PcItem("avatar"), 109 | readline.PcItem("\"avatar url\""), 110 | readline.PcItem("verified"), 111 | readline.PcItem("\"mfa enabled\""), 112 | readline.PcItem("bot"), 113 | ), 114 | readline.PcItem(""), 115 | ), 116 | readline.PcItem("settings", 117 | readline.PcItem("theme"), 118 | readline.PcItem("compact"), 119 | readline.PcItem("locale"), 120 | readline.PcItem("tts"), 121 | readline.PcItem("\"convert emotes\""), 122 | readline.PcItem("attachments"), 123 | readline.PcItem("\"media embeds\""), 124 | readline.PcItem("\"show embeds\""), 125 | readline.PcItem("\"show current game\""), 126 | readline.PcItem("\"dev mode\""), 127 | readline.PcItem("\"platform accounts\""), 128 | ), 129 | ), 130 | readline.PcItem("messages", 131 | readline.PcItem("all"), 132 | readline.PcItem("mentions"), 133 | readline.PcItem("private"), 134 | readline.PcItem("current"), 135 | readline.PcItem("none"), 136 | ), 137 | readline.PcItem("intercept", 138 | readline.PcItem("true"), 139 | readline.PcItem("false"), 140 | ), 141 | readline.PcItem("output", 142 | readline.PcItem("true"), 143 | readline.PcItem("false"), 144 | ), 145 | readline.PcItem("avatar", 146 | readline.PcItem("link"), 147 | readline.PcItem("file"), 148 | ), 149 | readline.PcItem("log", 150 | readline.PcItem("file"), 151 | readline.PcItem("directly"), 152 | ), 153 | readline.PcItem("game", 154 | readline.PcItem("streaming"), 155 | readline.PcItem("watching"), 156 | readline.PcItem("listening"), 157 | ), 158 | readline.PcItem("friend", 159 | readline.PcItem("add"), 160 | readline.PcItem("accept"), 161 | readline.PcItem("remove"), 162 | readline.PcItem("list"), 163 | ), 164 | readline.PcItem("new", 165 | readline.PcItem("channel"), 166 | readline.PcItem("vchannel"), 167 | readline.PcItem("guild"), 168 | ), 169 | readline.PcItem("delete", 170 | readline.PcItem("guild"), 171 | readline.PcItem("channel"), 172 | readline.PcItem("category"), 173 | ), 174 | readline.PcItem("invite", 175 | readline.PcItem("create"), 176 | readline.PcItem("accept"), 177 | readline.PcItem("read"), 178 | readline.PcItem("list"), 179 | readline.PcItem("revoke"), 180 | ), 181 | readline.PcItem("region", 182 | readline.PcItem("list"), 183 | readline.PcItem("set"), 184 | ), 185 | readline.PcItem("note", 186 | readline.PcItem("@me"), 187 | ), 188 | ) 189 | } 190 | func bookmarkTab(line string) []string { 191 | items := make([]string, len(bookmarks)) 192 | 193 | i := 0 194 | for key := range bookmarks { 195 | items[i] = key 196 | i++ 197 | } 198 | return items 199 | } 200 | func singleValue(val *string) func(string) []string { 201 | return func(line string) []string { 202 | return []string{*val} 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /emojify.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import "unicode" 21 | 22 | func toEmojiString(c rune) string { 23 | if c >= 'a' && c <= 'z' { 24 | return regionalIndicator(c) 25 | } else if c >= 'A' && c <= 'Z' { 26 | return regionalIndicator(unicode.ToLower(c)) 27 | } else { 28 | switch c { 29 | case '-': 30 | return ":heavy_minus_sign:" 31 | case '+': 32 | return ":heavy_plus_sign:" 33 | case '$': 34 | return ":heavy_dollar_sign:" 35 | case '*': 36 | return ":asterisk:" 37 | case '!': 38 | return ":exclamation:" 39 | case '?': 40 | return ":question:" 41 | case ' ': 42 | return "\t" 43 | 44 | case '0': 45 | return ":zero:" 46 | case '1': 47 | return ":one:" 48 | case '2': 49 | return ":two:" 50 | case '3': 51 | return ":three:" 52 | case '4': 53 | return ":four:" 54 | case '5': 55 | return ":five:" 56 | case '6': 57 | return ":six:" 58 | case '7': 59 | return ":seven:" 60 | case '8': 61 | return ":eight:" 62 | case '9': 63 | return ":nine:" 64 | 65 | default: 66 | return string(c) 67 | } 68 | } 69 | } 70 | func toEmoji(c rune) string { 71 | if c >= 'A' && c <= 'Z' { 72 | return string(c - 'A' + '🇦') 73 | } 74 | if c >= 'a' && c <= 'z' { 75 | return string(c - 'a' + '🇦') 76 | } 77 | if c >= '0' && c <= '9' || c == '*' { 78 | return string(c) + "\u20E3" 79 | } 80 | switch c { 81 | case '-': 82 | return "➖" 83 | case '+': 84 | return "➕" 85 | case '$': 86 | return "💲" 87 | case '!': 88 | return "❗" 89 | case '?': 90 | return "❓" 91 | default: 92 | return string(c) 93 | } 94 | } 95 | 96 | func regionalIndicator(c rune) string { 97 | return ":regional_indicator_" + string(c) + ":" 98 | } 99 | -------------------------------------------------------------------------------- /errorfilter.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "os" 22 | "strings" 23 | 24 | "github.com/fatih/color" 25 | "github.com/jD91mZM2/stdutil" 26 | "github.com/mattn/go-colorable" 27 | ) 28 | 29 | func doErrorHook() { 30 | stdutil.EventPrePrintError = append(stdutil.EventPrePrintError, func(full string, msg string, err error) bool { 31 | if err != nil && isPermission(err) { 32 | colorError.Fprintln(colorable.NewColorable(os.Stderr), tl("failed.perms")) 33 | return true 34 | } 35 | color.Unset() 36 | colorError.Set() 37 | return false 38 | }) 39 | stdutil.EventPostPrintError = append(stdutil.EventPostPrintError, func(text string, msg string, err error) { 40 | color.Unset() 41 | }) 42 | } 43 | 44 | func isPermission(err error) bool { 45 | return strings.Contains(err.Error(), "Missing Permission") 46 | } 47 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "sync" 22 | 23 | "github.com/bwmarrin/discordgo" 24 | ) 25 | 26 | var mutexCacheGuilds sync.RWMutex 27 | var cacheGuilds []*discordgo.UserGuild 28 | var cacheChannels []*discordgo.Channel 29 | var cachedChannelType discordgo.ChannelType 30 | 31 | var chanReady = make(chan []*discordgo.UserGuild) 32 | 33 | func ready(session *discordgo.Session, e *discordgo.Ready) { 34 | select { 35 | case _, ok := <-chanReady: 36 | if !ok { 37 | return 38 | } 39 | default: 40 | } 41 | 42 | guilds := make([]*discordgo.UserGuild, len(e.Guilds)) 43 | for i, guild := range e.Guilds { 44 | guilds[i] = toUserGuild(guild) 45 | } 46 | if userType == typeUser { 47 | guilds = sortGuilds(guilds, e.Settings) 48 | } 49 | 50 | mutexCacheGuilds.Lock() 51 | cacheGuilds = guilds 52 | mutexCacheGuilds.Unlock() 53 | 54 | select { 55 | case chanReady <- guilds: 56 | default: 57 | } 58 | close(chanReady) 59 | } 60 | 61 | func guildCreate(session *discordgo.Session, e *discordgo.GuildCreate) { 62 | mutexCacheGuilds.Lock() 63 | defer mutexCacheGuilds.Unlock() 64 | 65 | for _, guild := range cacheGuilds { 66 | if guild.ID == e.ID { 67 | guild.Name = e.Name 68 | // For bots, the ready event does not send it's name 69 | return 70 | } 71 | } 72 | 73 | cacheGuilds = append(cacheGuilds, toUserGuild(e.Guild)) 74 | } 75 | func guildDelete(session *discordgo.Session, e *discordgo.GuildDelete) { 76 | mutexCacheGuilds.Lock() 77 | defer mutexCacheGuilds.Unlock() 78 | 79 | index := -1 80 | for i, guild := range cacheGuilds { 81 | if guild.ID == e.Guild.ID { 82 | index = i 83 | break 84 | } 85 | } 86 | if index >= 0 { 87 | // UGH! 88 | // append(cacheGuilds, cacheGuilds[:index], cacheGuilds[index+1:]) 89 | // would give a pointer leak because Go's garbage collector is ~~stupid~~ 90 | // Garbage* //Mnpn ( ͡° ͜ʖ ͡°) 91 | copy(cacheGuilds[index:], cacheGuilds[index+1:]) 92 | cacheGuilds[len(cacheGuilds)-1] = nil 93 | cacheGuilds = cacheGuilds[:len(cacheGuilds)-1] 94 | } 95 | } 96 | 97 | func toUserGuild(guild *discordgo.Guild) *discordgo.UserGuild { 98 | return &discordgo.UserGuild{ 99 | ID: guild.ID, 100 | Name: guild.Name, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /filepaths.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "errors" 22 | "os" 23 | "os/user" 24 | "path/filepath" 25 | "strings" 26 | ) 27 | 28 | func fixPath(path *string) error { 29 | s := *path 30 | 31 | for { 32 | i := strings.Index(s, "~") 33 | if i < 0 { 34 | break 35 | } 36 | 37 | current, err := user.Current() 38 | if err != nil { 39 | return errors.New(tl("failed.path.home") + ", " + err.Error()) 40 | } 41 | 42 | s = filepath.Join(s[:i], current.HomeDir, s[i+1:]) 43 | } 44 | for _, env := range os.Environ() { 45 | keyval := strings.SplitN(env, "=", -1) 46 | 47 | s = strings.Replace(s, "%"+keyval[0]+"%", keyval[1], -1) 48 | s = strings.Replace(s, "$"+keyval[0], keyval[1], -1) 49 | } 50 | 51 | *path = s 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "strings" 23 | ) 24 | 25 | func printHelp(search string) { 26 | search = strings.ToLower(search) 27 | help := make([]string, 0) 28 | 29 | help = append(help, "help [search]\tShow help menu. Optionally search.") 30 | help = append(help, "exit\tExit DiscordConsole") 31 | help = append(help, "exec\tExecute a shell command") 32 | help = append(help, "run\tRun a LUA file with DiscordConsole's special functions") 33 | help = append(help, "alias \tAdd a new alias for a command.") 34 | help = append(help, "lang \tSame as starting with --lang") 35 | help = append(help, "latency\tShows the last heartbeat's ping.") 36 | help = append(help, "permcalc [preset]\tOpen the permission calculator, and optionally with pre-set values.") 37 | help = append(help, "") 38 | if userType != typeWebhook { 39 | help = append(help, "guilds\t\tList guilds/servers you're in.") 40 | help = append(help, "guild \tSelect a guild to use for further commands.") 41 | help = append(help, "channels\tList channels in your selected guild.") 42 | help = append(help, "channel \tSelect a channel to use for further commands.") 43 | help = append(help, "pchannels\tList private channels a.k.a. 'DMs'.") 44 | help = append(help, "vchannels\tList voice channels in your selected guild.") 45 | help = append(help, "dm \tCreate a DM with specific user.") 46 | help = append(help, "") 47 | help = append(help, "region ()\tSet current guild region.") 48 | help = append(help, "") 49 | help = append(help, "info (for user: ) [property] (or info u/g/c/s)\tGet information about a user, server, channel or your set Discord settings!") 50 | help = append(help, "read [property]\tRead or get info from a message. Properties: (empty), text, channel, timestamp, author, "+ 51 | "author_email, author_name, author_avatar, author_bot, embed; 'cache' may be used as message ID.") 52 | help = append(help, "pin \tPin a message to the current channel.") 53 | help = append(help, "unpin \tUnpin a message from the current channel.") 54 | help = append(help, "") 55 | help = append(help, "name \tChange username completely.") 56 | help = append(help, "avatar \tChange avatar to a link or file.") 57 | help = append(help, "") 58 | } 59 | help = append(help, "say \tSend a message in your selected channel. `say toggle` starts chat-mode, and `toggle` ends it.") 60 | if userType != typeWebhook { 61 | help = append(help, "${Placeholders}:\tReplaces e.g. ${u.name} with "+userObj.Username+".") 62 | } 63 | help = append(help, "sayfile \tSend the contents of a file (auto-splitted).") 64 | help = append(help, "big \tSend a message, but attempt to make it using emojis!") 65 | help = append(help, "embed \tSend an embed! (ADVANCED!) See https://discordapp.com/developers/docs/resources/channel#embed-object") 66 | if userType != typeWebhook { 67 | help = append(help, "tts \tSend a TTS message in your selected channel.") 68 | help = append(help, "file \tUpload file to selected channel.") 69 | help = append(help, "") 70 | help = append(help, "edit \tEdit a message in your selected channel.") 71 | help = append(help, "editembed \tEdit a message embed in your selected channel.") 72 | help = append(help, "del \tDelete a message in the selected channel.") 73 | help = append(help, "delall [since message id]\tBulk delete messages since a specific message") 74 | help = append(help, "log \tLog the last few messages in console or to a file.") 75 | help = append(help, "react add/del \tReact to a message") 76 | help = append(help, "react big \tLike the 'big' command, but in reactions!") 77 | help = append(help, "react delall \tDelete all reactions") 78 | help = append(help, "") 79 | help = append(help, "playing [game]\tSet your playing status. Run without an argument to clear.") 80 | help = append(help, "streaming [url] [game]\tSet your streaming status.") 81 | help = append(help, "game [details] [extra text]\tSet a custom status.") 82 | help = append(help, "typing\tSimulate typing in selected channel.") 83 | help = append(help, "") 84 | help = append(help, "members\tList (max 100) members in selected guild") 85 | help = append(help, "invite create [expires] [max uses] ['temp'] OR invite accept OR invite read OR invite list OR invite revoke \tCreate an invite, accept an existing one, see invite information, list all invites or revoke an invite.") 86 | help = append(help, "") 87 | help = append(help, "role list\tList all roles in selected guild.") 88 | help = append(help, "role add \tAdd role to user") 89 | help = append(help, "role rem \tRemove role from user") 90 | help = append(help, "role create\tCreate new role") 91 | help = append(help, "role edit \tEdit a role. Flags are: name, color, separate, perms, mention") 92 | help = append(help, "role delete \tDelete a role.") 93 | help = append(help, "") 94 | help = append(help, "nick [nickname]\tChange somebody's nickname") 95 | help = append(help, "nickall [nickname]\tChange ALL nicknames!") 96 | help = append(help, "") 97 | help = append(help, "messages [scope]\tIntercepting messages. Optionally, scope can have a filter on it: all, mentions, private, "+ 98 | "current (default), none") 99 | help = append(help, "intercept [yes/no]\tToggle intercepting 'console.' commands in Discord.") 100 | help = append(help, "output [yes/no]\tToggle showing 'console.' outputs directly in Discord.") 101 | help = append(help, "back\tJump to previous guild and/or channel.") 102 | help = append(help, "") 103 | help = append(help, "new \tCreate a new guild or channel") 104 | help = append(help, "bans [text]\tList all bans, can also be shown in text form to show full ban reasons.") 105 | help = append(help, "ban \tBan user") 106 | help = append(help, "unban \tUnban user") 107 | help = append(help, "kick \tKick user") 108 | help = append(help, "leave [id]\tLeave a guild! Run with no arguments to leave the selected one.") 109 | help = append(help, "ownership \tTransfer ownership.") 110 | help = append(help, "") 111 | // Deleting a category also works when selecting channel, but this is less confusing, I hope. 112 | help = append(help, "delete \tDelete a channel, guild or category.") 113 | help = append(help, "") 114 | help = append(help, "play \tPlays a song in the selected voice channel") 115 | help = append(help, "stop\tStops playing any song.") 116 | help = append(help, "move \tMove a user to another voice channel.") 117 | help = append(help, "") 118 | help = append(help, "status \tSet the user status. Possible values are: online, idle, dnd and invisible.") 119 | help = append(help, "") 120 | help = append(help, "friend ()\tManage your friends. Add, accept, remove and list them.") 121 | help = append(help, "block \tBlock a user.") 122 | help = append(help, "note \tSet a note. Remove with `note \"\"`.") 123 | help = append(help, "") 124 | help = append(help, "bookmarks\tList all bookmarks in the console.") 125 | help = append(help, "bookmark \tCreate new bookmark out of current location. If the name starts with -, it removes the bookmark.") 126 | help = append(help, "go \tJump to the specified bookmark.") 127 | help = append(help, "") 128 | help = append(help, "rl [full]\tReload cache. If 'full' is set, it also restarts the session.") 129 | } 130 | 131 | if search != "" { 132 | help2 := make([]string, 0) 133 | for _, line := range help { 134 | if strings.Contains(strings.ToLower(line), search) { 135 | help2 = append(help2, line) 136 | } 137 | } 138 | help = help2 139 | } 140 | 141 | fmt.Println(strings.Join(help, "\n")) 142 | } 143 | -------------------------------------------------------------------------------- /intercept.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "fmt" 23 | "io" 24 | "strings" 25 | "time" 26 | 27 | "github.com/bwmarrin/discordgo" 28 | "github.com/fatih/color" 29 | "github.com/jD91mZM2/stdutil" 30 | ) 31 | 32 | func messageCreate(session *discordgo.Session, e *discordgo.MessageCreate) { 33 | defer handleCrash() 34 | 35 | if e.Author == nil { 36 | return 37 | } 38 | 39 | var channel *discordgo.Channel 40 | var err error 41 | for _, c := range cacheChannels { 42 | if c.ID == e.ChannelID { 43 | channel = c 44 | break 45 | } 46 | } 47 | 48 | if channel == nil { 49 | channel, err = session.Channel(e.ChannelID) 50 | if err != nil { 51 | stdutil.PrintErr(tl("failed.channel"), err) 52 | return 53 | } 54 | } 55 | 56 | var guild *discordgo.Guild 57 | if !isPrivate(channel) { 58 | // Can't use cache. It's of user guild 59 | guild, err = session.Guild(channel.GuildID) 60 | if err != nil { 61 | stdutil.PrintErr(tl("failed.guild"), err) 62 | return 63 | } 64 | } 65 | 66 | if messageCommand(session, e.Message, guild, channel) { 67 | return 68 | } 69 | 70 | hasOutput := false 71 | 72 | print := false 73 | outer: 74 | switch messages { 75 | case messagesAll: 76 | print = true 77 | case messagesPrivate: 78 | if isPrivate(channel) { 79 | print = true 80 | } 81 | case messagesMentions: 82 | if isPrivate(channel) || e.MentionEveryone { 83 | print = true 84 | break 85 | } 86 | 87 | for _, u := range e.Mentions { 88 | if u.ID == userID { 89 | print = true 90 | break outer 91 | } 92 | } 93 | 94 | user, err := session.GuildMember(guild.ID, userID) 95 | if err != nil { 96 | stdutil.PrintErr(tl("failed.user"), err) 97 | break 98 | } 99 | 100 | for _, role := range user.Roles { 101 | for _, role2 := range e.MentionRoles { 102 | if role == role2 { 103 | print = true 104 | break outer 105 | } 106 | } 107 | } 108 | case messagesCurrent: 109 | if (guild == nil || loc.guild == nil) && loc.channel != nil && channel.ID != loc.channel.ID { 110 | break 111 | } 112 | if guild != nil && loc.guild != nil && guild.ID != loc.guild.ID { 113 | break 114 | } 115 | 116 | print = true 117 | } 118 | if print { 119 | printMessage(session, e.Message, true, guild, channel, color.Output) 120 | hasOutput = true 121 | } 122 | 123 | if len(luaMessageEvents) > 0 { 124 | hasOutput = true 125 | 126 | color.Unset() 127 | colorAutomated.Set() 128 | 129 | fmt.Print("\r" + strings.Repeat(" ", 20) + "\r") 130 | luaMessageEvent(session, e.Message) 131 | 132 | color.Unset() 133 | colorDefault.Set() 134 | } 135 | if hasOutput { 136 | printPointer(session) 137 | } 138 | } 139 | 140 | func messageCommand(session *discordgo.Session, e *discordgo.Message, guild *discordgo.Guild, channel *discordgo.Channel) (isCmd bool) { 141 | if e.Author.ID != userID { 142 | return 143 | } else if !intercept { 144 | return 145 | } 146 | 147 | prefix := tl("console.") 148 | 149 | contents := strings.TrimSpace(e.Content) 150 | if !strings.HasPrefix(contents, prefix) { 151 | return 152 | } 153 | cmd := contents[len(prefix):] 154 | 155 | isCmd = true 156 | 157 | if strings.EqualFold(cmd, "ping") { 158 | first := time.Now().UTC() 159 | 160 | timestamp, err := e.Timestamp.Parse() 161 | if err != nil { 162 | stdutil.PrintErr(tl("failed.timestamp"), err) 163 | return 164 | } 165 | 166 | in := first.Sub(timestamp) 167 | 168 | // Discord 'bug' makes us receive the message before the timestamp, sometimes. 169 | text := "" 170 | if in.Nanoseconds() >= 0 { 171 | text += "Incoming: `" + in.String() + "`" 172 | } else { 173 | text += "The message was recieved earlier than the timestamp. This is a Discord bug." 174 | } 175 | 176 | middle := time.Now().UTC() 177 | 178 | _, err = session.ChannelMessageEditComplex(discordgo.NewMessageEdit(e.ChannelID, e.ID). 179 | SetContent("Ping! 1/2"). 180 | SetEmbed(&discordgo.MessageEmbed{ 181 | Description: text + "\nCalculating outgoing..", 182 | })) 183 | 184 | last := time.Now().UTC() 185 | 186 | text += "\nOutgoing: `" + last.Sub(middle).String() + "`" 187 | text += "\n\n\nIncoming is the time it takes for the message to reach DiscordConsole." 188 | text += "\nOutgoing is the time it takes for DiscordConsole to reach Discord." 189 | 190 | _, err = session.ChannelMessageEditComplex(discordgo.NewMessageEdit(e.ChannelID, e.ID). 191 | SetContent("Pong! 2/2"). 192 | SetEmbed(&discordgo.MessageEmbed{ 193 | Description: text, 194 | })) 195 | if err != nil { 196 | stdutil.PrintErr(tl("failed.msg.edit"), err) 197 | } 198 | return 199 | } 200 | 201 | loc.push(guild, channel) 202 | 203 | capture := output 204 | 205 | var w io.Writer 206 | var str *bytes.Buffer 207 | if capture { 208 | str = bytes.NewBuffer(nil) 209 | w = str 210 | } else { 211 | go func() { 212 | err := session.ChannelMessageDelete(e.ChannelID, e.ID) 213 | if err != nil { 214 | stdutil.PrintErr(tl("failed.msg.delete"), err) 215 | } 216 | }() 217 | color.Unset() 218 | colorAutomated.Set() 219 | 220 | fmt.Println(cmd) 221 | w = color.Output 222 | } 223 | command(session, commandSource{}, cmd, w) 224 | 225 | if !capture { 226 | color.Unset() 227 | colorDefault.Set() 228 | printPointer(session) 229 | } else { 230 | first := true 231 | send := func(buf string) { 232 | if buf == "" { 233 | return 234 | } 235 | 236 | // Zero width space 237 | buf = "```\n" + strings.Replace(buf, "`", "​`", -1) + "\n```" 238 | if first { 239 | first = false 240 | _, err := session.ChannelMessageEdit(e.ChannelID, e.ID, buf) 241 | if err != nil { 242 | stdutil.PrintErr(tl("failed.msg.edit"), err) 243 | return 244 | } 245 | } else { 246 | _, err := session.ChannelMessageSend(e.ChannelID, buf) 247 | if err != nil { 248 | stdutil.PrintErr(tl("failed.msg.send"), err) 249 | return 250 | } 251 | } 252 | } 253 | 254 | buf := "" 255 | for { 256 | line, err := str.ReadString('\n') 257 | if err != nil { 258 | break 259 | } 260 | 261 | if len(line)+len(buf)+8 < msgLimit { 262 | buf += line 263 | } else { 264 | send(buf) 265 | buf = "" 266 | } 267 | } 268 | send(buf) 269 | } 270 | 271 | color.Unset() 272 | colorDefault.Set() 273 | return 274 | } 275 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # These are a few scripts for your convenience. To run them, use Just: 2 | # https://github.com/casey/just 3 | 4 | # -*- mode: make-gmake-mode; -*- 5 | 6 | debug: 7 | go install --race 8 | 9 | release: 10 | go install 11 | 12 | fix-dgo: 13 | #!/usr/bin/env sh 14 | go get github.com/bwmarrin/discordgo 15 | cd "$GOPATH/src/github.com/bwmarrin/discordgo" 16 | git checkout develop 17 | go install 18 | 19 | cross-compile: 20 | #!/usr/bin/env sh 21 | 22 | cd "$GOPATH/bin" 23 | ./CrossCompile.sh DiscordConsole discordconsole-team 24 | -------------------------------------------------------------------------------- /languages.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | // TRANSLATORS: 19 | // - English, jD91mZM2 & Mnpn 20 | // - Swedish, Mnpn 21 | // - Spanish, ArceCreeper 22 | package main 23 | 24 | import ( 25 | "bufio" 26 | "errors" 27 | "fmt" 28 | "io" 29 | "os" 30 | "strings" 31 | 32 | "github.com/jD91mZM2/stdutil" 33 | ) 34 | 35 | var errLangCorrupt = errors.New("corrupt language file") 36 | var lang map[string]string 37 | 38 | // TL stands for TransLate kek 39 | func tl(name string) string { 40 | str, ok := lang[name] 41 | if ok { 42 | return str 43 | } 44 | 45 | return name 46 | } 47 | 48 | func loadLangAuto(langfile string) { 49 | fmt.Println("Loading language...") 50 | switch langfile { 51 | case "en": 52 | loadLangDefault() 53 | case "sv": 54 | loadLangString(langSv) 55 | case "es": 56 | loadLangString(langEs) 57 | default: 58 | reader, err := os.Open(langfile) 59 | if err != nil { 60 | stdutil.PrintErr("Could not read language file", err) 61 | return 62 | } 63 | defer reader.Close() 64 | 65 | err = loadLang(reader) 66 | if err != nil { 67 | stdutil.PrintErr("Could not load language file", err) 68 | loadLangDefault() 69 | } 70 | } 71 | } 72 | func loadLang(reader io.Reader) error { 73 | lang = make(map[string]string) 74 | scanner := bufio.NewScanner(reader) 75 | 76 | for scanner.Scan() { 77 | text := scanner.Text() 78 | if text == "" { 79 | continue 80 | } 81 | parts := strings.SplitN(text, "=", 2) 82 | if len(parts) != 2 { 83 | return errLangCorrupt 84 | } 85 | key := parts[0] 86 | val := parts[1] 87 | 88 | if strings.HasSuffix(key, ".dev") && devVersion { 89 | key = key[:len(key)-len(".dev")] 90 | } 91 | 92 | lang[key] = val 93 | } 94 | 95 | return scanner.Err() 96 | } 97 | func loadLangString(lang string) error { 98 | return loadLang(strings.NewReader(lang)) 99 | } 100 | func loadLangDefault() { 101 | loadLangString(langEn) 102 | } 103 | 104 | // Here is just some long data. 105 | // This comment is a separator, btw. 106 | 107 | // English by jD91mZM2 & Mnpn 108 | var langEn = ` 109 | update.checking=Checking for updates... 110 | update.error=Error checking for updates 111 | update.available=Update available! Version 112 | update.available.dev=Latest stable release: 113 | update.download=Download from 114 | update.none=No updates found. 115 | 116 | loading.bookmarks=Reading bookmarks... 117 | 118 | failed.auth=Couldn't authenticate 119 | failed.avatar=Couldn't set avatar 120 | failed.ban.create=Could not ban user 121 | failed.ban.delete=Could not unban user 122 | failed.ban.list=Could not list bans 123 | failed.base64=Couldn't convert to Base64 124 | failed.block=Couldn't block user 125 | failed.category.create=Could not create category 126 | failed.channel.create=Could not create channel 127 | failed.channel.delete=Could not delete channel 128 | failed.channel=Could not query channel 129 | failed.exec=Could not execute 130 | failed.file.delete=Could not delete file 131 | failed.file.load=Could not load file 132 | failed.file.open=Couldn't open file 133 | failed.file.read=Could not read file 134 | failed.file.save=Could not save file 135 | failed.file.write=Could not write file 136 | failed.fixpath=Could not 'fix' filepath 137 | failed.friend.add=Couldn't add friend 138 | failed.friend.list=Couldn't get friends 139 | failed.friend.remove=Couldn't remove friend 140 | failed.generic=Failed 141 | failed.guild.create=Could not create guild 142 | failed.guild.delete=Could not delete guild 143 | failed.guild.edit=Could not edit guild 144 | failed.guild=Could not query guild 145 | failed.invite.accept=Could not accept invite 146 | failed.invite.create=Invite could not be created 147 | failed.invite=Could not query invite 148 | failed.json=Could not parse json 149 | failed.kick=Could not kick user 150 | failed.leave=Could not leave 151 | failed.lua.event=Recovered from LUA error 152 | failed.lua.run=Could not run lua 153 | failed.members=Could not list members 154 | failed.move=Could not move the user 155 | failed.msg.delete=Couldn't delete message 156 | failed.msg.edit=Couldn't edit message 157 | failed.msg.query=Could not get message 158 | failed.msg.send=Could not send message 159 | failed.nick=Could not set nickname 160 | failed.note=Could not set note 161 | failed.nochannel=Server does not have a channel 162 | failed.paste=Failed to paste clipboard: 163 | failed.path.home=Could not determine value of ~ 164 | failed.permcalc=Could not open PermCalc (permission calculator) 165 | failed.perms=No permissions to perform this action. 166 | failed.pin=Couldn't pin the message 167 | failed.react.del=Could not remove reaction 168 | failed.react.delall=Could not delete all reactions 169 | failed.react.used=Emoji used already, skipping 170 | failed.react=Could not react to message 171 | failed.reading=Could not read 172 | failed.readline.read=Could not read line 173 | failed.readline.start=Could not start readline library 174 | failed.revoke=Could not revoke 175 | failed.role.change=Could not add/remove role 176 | failed.role.create=Could not create role 177 | failed.role.delete=Could not delete role 178 | failed.role.edit=Could not edit role 179 | failed.roles=Could not get roles 180 | failed.session.start=Could not open session 181 | failed.settings=Could not query user settings 182 | failed.status=Could not set status 183 | failed.timestamp=Couldn't parse timestamp 184 | failed.transfer=Couldn't transfer ownership 185 | failed.typing=Couldn't start typing 186 | failed.unpin=Couldn't unpin the message 187 | failed.user.edit=Couldn't edit user data 188 | failed.user=Couldn't query user 189 | failed.voice.connect=Could not connect to voice channel 190 | failed.voice.disconnect=Could not disconnect 191 | failed.voice.regions=Could not get regions 192 | failed.voice.speak=Could not start speaking 193 | failed.webrequest=Could not make web request 194 | 195 | information.aborted=Aborted. 196 | information.category=Category 197 | information.created.successfully= was created successfully with ID 198 | information.confirmation=Are you really sure you want to do this? 199 | information.deleted.successfully= was deleted successfully. 200 | information.give.ownership=This will give server ownership to 201 | information.guild=Guild 202 | information.channel=Channel 203 | information.irreversible=This action is irreversible! 204 | information.length=Some text was cut short. To see the full text, run 205 | information.moved=Moved the user. 206 | information.note=Note set! 207 | information.revoked.successfully=Revoked 208 | information.wait=Wait a second! 209 | information.warning=Warning! 210 | 211 | intro.exit=Press Ctrl+D or type 'exit' to exit. 212 | intro.help=Write 'help' for help. 213 | 214 | invalid.bookmark=Bookmark doesn't exist 215 | invalid.cache=No cache available! 216 | invalid.channel.voice=No voice channel selected! 217 | invalid.channel=No channel selected! 218 | invalid.command=Unknown command: 219 | invalid.command2=Do 'help' for help. 220 | invalid.dm=This command doesn't work in a DM. 221 | invalid.guild=No guild selected! 222 | invalid.id=You need to select something to get its ID! 223 | invalid.limit.message=Message exceeds character limit 224 | invalid.music.playing=Already playing something 225 | invalid.not.owner=You're not the server owner! 226 | invalid.number=Not a number 227 | invalid.onlyfor.bots=This command only works for bot users. 228 | invalid.onlyfor.users=This only works for users. 229 | invalid.role=No role with that ID 230 | invalid.source.terminal=You must be in terminal to do this. 231 | invalid.status.offline=The offline status exists, but cannot be set through the API 232 | invalid.substitute=Invalid substitute 233 | invalid.unmatched.quote=Unmatched quote in input string 234 | invalid.value=No such value 235 | invalid.webhook.command=Not an allowed webhook command 236 | invalid.webhook=Webhook format invalid. Format: id/token 237 | invalid.yn=Please type either 'y' or 'n'. 238 | 239 | login.finish=Logged in with user ID 240 | login.hidden=[Hidden] 241 | login.starting=Authenticating... 242 | login.token.user=User tokens are prefixed with 'user ' 243 | login.token.webhook=Webhook tokens are prefixed with 'webhook ', and their URL or id/token 244 | login.token=Please paste your 'token' here. 245 | 246 | pointer.private=Private 247 | pointer.unknown=Unknown 248 | 249 | status.avatar=Avatar set! 250 | status.cache=Message cached! 251 | status.channel=Selected channel with ID 252 | status.invite.accept=Accepted invite. 253 | status.invite.create=Created invite with code: 254 | status.loading=Loading... 255 | status.msg.create=Created message with ID 256 | status.msg.delall=Deleted # messages 257 | status.name=Name set! 258 | status.status=Status set! 259 | 260 | rl.session=Restarting session... 261 | rl.cache.loc=Reloading location cache... 262 | rl.cache.vars=Deleting cache variables... 263 | ` 264 | 265 | // Swedish by Mnpn 266 | var langSv = ` 267 | update.checking=Letar efter uppdateringar... 268 | update.error=Ett fel inträffade under letandet efter nya uppdateringar. 269 | update.available=En uppdatering finns tillgänglig! Version 270 | update.available.dev=Senast stabila version: 271 | update.download=Ladda ner från 272 | update.none=Hittade inga uppdateringar. 273 | 274 | loading.bookmarks=Läser bokmärken... 275 | 276 | failed.auth=Kunde inte autentisera 277 | failed.avatar=Kunde inte sätta avatar 278 | failed.ban.create=Kunde inte bannlysa användaren 279 | failed.ban.delete=Kunde inte avbannlysa användaren 280 | failed.ban.list=Kunde inte lista bannlysningar 281 | failed.base64=Kunde inte konvertera till Bas64 282 | failed.block=Kunde inte blockera användaren 283 | failed.category.create=Kunde inte skapa kategori 284 | failed.channel.create=Kunde inte skapa kanal 285 | failed.channel.delete=Kunde inte ta bort kanal 286 | failed.channel=Kunde inte fråga efter kanal 287 | failed.exec=Kunde inte köra 288 | failed.file.delete=Kunde inte ta bort fil 289 | failed.file.load=Kunde inte ladda fil 290 | failed.file.open=Kunde inte öppna fil 291 | failed.file.read=Kunde inte läsa fil 292 | failed.file.save=Kunde inte spara fil 293 | failed.file.write=Kunde inte skriva till 294 | failed.fixpath=Kunde inte 'fixa' sökväg 295 | failed.friend.add=Kunde inte acceptera vän 296 | failed.friend.list=Kunde inte få vänner :( 297 | failed.friend.remove=Kunde inte ta bort vän 298 | failed.generic=Misslyckades 299 | failed.guild.create=Kunde inte skapa server 300 | failed.guild.delete=Kunde inte ta bort server 301 | failed.guild.edit=Kunde inte skapa servern 302 | failed.guild=Kunde inte fråga efter server 303 | failed.invite.accept=Kunde inte acceptera inbjudningen 304 | failed.invite.create=Inbjudningen kunde inte skapas 305 | failed.json=Kunde inte tolka JSON 306 | failed.kick=Kunde inte sparka användaren 307 | failed.leave=Kunde inte lämna 308 | failed.lua.event=Återhämtade från LUA-fel 309 | failed.lua.run=Kunde inte köra lua 310 | failed.members=Kunde inte visa medlemmarna 311 | failed.move=Kunde inte flytta användaren 312 | failed.msg.delete=Kunde inte ta bort meddelande 313 | failed.msg.edit=Kunde inte ändra meddelande 314 | failed.msg.query=Kunde inte ta emot meddelande 315 | failed.msg.send=Kunde inte skicka meddelande 316 | failed.nick=Kunde inte sätta smeknamn 317 | failed.note=Kunde inte sätta anteckning 318 | failed.nochannel=Servern har ingen kanal 319 | failed.paste=Kunde inte klista in: 320 | failed.path.home=Det gick inte att bedöma värdet av ~ 321 | failed.permcalc=Kunde inte öppna PermCalc 322 | failed.perms=Inga behörigheter för att utföra den här åtgärden. 323 | failed.pin=Kunde inte fästa meddelandet 324 | failed.react.del=Kunde inte ta bort reaktionen 325 | failed.react.delall=Kunde inte ta bort alla reaktioner 326 | failed.react.used=Emoji redan använd, hoppar 327 | failed.react=Kunde inte reagera 328 | failed.reading=Kunde inte läsa 329 | failed.readline.read=Kunde inte läsa linje 330 | failed.readline.start=Kunde inte starta "readline"-biblioteket 331 | failed.revoke=Kunde inte återkalla 332 | failed.role.change=Kunde inte lägga till/ta bort roll 333 | failed.role.create=Kunde inte skapa roll 334 | failed.role.delete=Kunde inte ta bort roll 335 | failed.role.edit=Kunde inte ändra roll 336 | failed.roles=Kunde inte ta emot roller 337 | failed.session.start=Kunde inte öppna sessionen 338 | failed.settings=Kunde inte ta emot användarinställningar 339 | failed.status=Kunde inte sätta status 340 | failed.timestamp=Kunde inte tolka tidsstämplar 341 | failed.transfer=Kunde inte överföra ägarskap 342 | failed.typing=Kunde inte börja skriva 343 | failed.unpin=Kunde inte ta bort det fästa meddelandet 344 | failed.user.edit=Kunde inte ändra användarinformationen 345 | failed.user=Kunde inte förfråga efter användarinformation 346 | failed.voice.connect=Kunde inte ansluta till röstkanal 347 | failed.voice.disconnect=Kunde inte koppla ifrån 348 | failed.voice.regions=Kunde inte läsa regioner 349 | failed.voice.speak=Kunde inte börja prata 350 | failed.webrequest=Det gick inte att göra webbegäran 351 | 352 | information.aborted=Avbrutet. 353 | information.category=Kategorin 354 | information.created.successfully= skapades med ID 355 | information.confirmation=Vill du verkligen göra detta? 356 | information.deleted.successfully= togs bort. 357 | information.give.ownership=Detta kommer att ge server-ägarskap till 358 | information.guild=Servern 359 | information.channel=Kanalen 360 | information.irreversible=Denna åtgärd kan inte ångras! 361 | information.length=Viss text var klippt. För att se hela texten, kör 362 | information.moved=Flyttade användaren. 363 | information.note=Anteckning satt! 364 | information.revoked.successfully=Återkallade 365 | information.wait=Vänta en sekund! 366 | information.warning=Varning! 367 | 368 | intro.exit=Tryck Ctrl+D eller kör 'exit' för att avsluta. 369 | intro.help=Kör 'help' för hjälp. 370 | 371 | invalid.bookmark=Bokmärket finns inte 372 | invalid.cache=Ingen cache tillgänglig! 373 | invalid.channel.voice=Ingen röstkanal vald! 374 | invalid.channel=Ingen kanal vald! 375 | invalid.command=Okänt kommando: 376 | invalid.command2=Kör 'help' för att få hjälp. 377 | invalid.dm=Detta kommandot fungerar inte i DMs. 378 | invalid.guild=Ingen server vald! 379 | invalid.id=Du måste välja något för att kunna få dess ID! 380 | invalid.limit.message=Meddelande överskrider teckenbegränsningen 381 | invalid.music.playing=Spelar redan något 382 | invalid.not.owner=Du är inte server-ägaren! 383 | invalid.number=Inte ett nummer 384 | invalid.onlyfor.bots=Detta kommandot fungerar endast för bot-användare. 385 | invalid.onlyfor.users=Detta kommandot fungerar endast för användare 386 | invalid.role=Ingen roll med det ID:t 387 | invalid.source.terminal=Du måste vara i en terminal för att göra detta. 388 | invalid.status.offline=Offline-statusen finns men kan inte ställas in via API:n 389 | invalid.substitute=Ogiltigt utbyte 390 | invalid.unmatched.quote=Omatchat citattecken i inmatningssträngen 391 | invalid.value=Inget sådant värde 392 | invalid.webhook.command=Inte ett tillåtet Webhook-commando 393 | invalid.webhook=Webhook-formatet är ogiltit. Format: id/token 394 | invalid.yn=Vänligen skriv antigen 'y' eller 'n'. 395 | 396 | login.finish=Loggade in med användar-ID:t 397 | login.hidden=[Dold] 398 | login.starting=Autentiserar... 399 | login.token.user=Användar-'tokens' har prefixet 'user ' 400 | login.token.webhook=Webhook-'tokens' har prefixet 'webhook ', och deras URL eller id/token 401 | login.token=Vänligen klistra in en 'token' här. 402 | 403 | pointer.private=Privat 404 | pointer.unknown=Okänd 405 | 406 | status.avatar=Avatar satt! 407 | status.cache=Meddelande cache-at! 408 | status.channel=Valde kanal med ID 409 | status.invite.accept=Accepterade inbjudan. 410 | status.invite.create=Skapade en inbjudan med kod: 411 | status.loading=Laddar... 412 | status.msg.create=Skapade meddelande med ID 413 | status.msg.delall=Tor bort # meddelanden 414 | status.name=Namn satt! 415 | status.status=Status satt! 416 | 417 | rl.cache.loc=Laddar om plats-cache... 418 | rl.cache.vars=Tar bort cache-variablar... 419 | rl.session=Startar om session... 420 | 421 | console.=konsoll. 422 | ` 423 | 424 | // Spanish by ArceCreeper 425 | var langEs = ` 426 | update.checking=Buscando actualizaciones... 427 | update.error=Error al buscar actualizaciones. 428 | update.available=¡Actualización disponible! Versión 429 | update.available.dev=Última release estable: 430 | update.download=Descarga de 431 | update.none=No se han encontrado actualizaciones. 432 | 433 | loading.bookmarks=Leyendo marcadores... 434 | 435 | failed.auth=No se ha podido autenticar 436 | failed.avatar=No se ha podido establecer el avatar. 437 | failed.ban.create=No se ha podido banear al usuario 438 | failed.ban.delete=No se ha podido desbanear al usuario 439 | failed.ban.list=No se han podido listar los baneos 440 | failed.base64=No se ha podido convertir a Base64 441 | failed.block=No se ha podido bloquear al usuario 442 | failed.category.create=No se pudo crear la categoría 443 | failed.channel.create=No se ha podido crear el canal 444 | failed.channel.delete=No se pudo borrar el canal 445 | failed.channel=No se ha podido consultar el canal 446 | failed.exec=No se ha podido ejecutar 447 | failed.file.delete=No se ha podido borrar el archivo 448 | failed.file.load=No se ha podido cargar el archivo 449 | failed.file.open=No se ha podido abrir el archivo 450 | failed.file.read=No se ha podido leer el archivo 451 | failed.file.save=No se ha podido guardar el archivo 452 | failed.file.write=No se ha podido escribir el archivo 453 | failed.fixpath=No se ha podido 'arreglar' la ruta del archivo 454 | failed.friend.list=No se han podido conseguir amigos 455 | failed.generic=Fallido 456 | failed.guild.create=No se pudo crear el servidor 457 | failed.guild.delete=No se pudo borrar el servidor 458 | failed.guild.edit=No se ha podido editar el servidor 459 | failed.guild=No se ha podido consultar el servidor 460 | failed.invite.accept=No se ha podido aceptar la invitación. 461 | failed.invite.create=No se ha podido crear la invitación. 462 | failed.invite=No se ha podido consultar la invitación 463 | failed.json=No se ha podido analizar json 464 | failed.kick=No se ha podido kickear al usuario 465 | failed.leave=No se ha podido dejar el servidor 466 | failed.lua.event=Recuperado de error de LUA 467 | failed.lua.run=No se ha podido ejecutar lua 468 | failed.members=No se ha podido listar a los miembros 469 | failed.move=No se pudo mover el usuario 470 | failed.msg.delete=No se ha podido borrar el mensaje 471 | failed.msg.edit=No se ha podido editar el mensaje 472 | failed.msg.query=No se ha podido recibir el mensaje 473 | failed.msg.send=No se ha podido enviar el mensaje 474 | failed.nick=No se ha podido establecer el apodo 475 | failed.note=No se pudo cambiar la nota. 476 | failed.nochannel=El servidor no tiene un canal. 477 | failed.paste=Fallo al copiar el contenido del portapapeles: 478 | failed.path.home=No se ha podido determinar el valor de ~ 479 | failed.permcalc=No se ha podido abrir PermCalc (calculador de permisos) 480 | failed.perms=No hay permisos para ejecutar esta acción. 481 | failed.pin=No se pudo fijar el mensaje 482 | failed.react.used=Emoji ya usado, omitiendo 483 | failed.react=No se ha podido reaccionar al mensaje 484 | failed.reading=No se ha podido leer 485 | failed.readline.read=No se ha podido leer la línea 486 | failed.readline.start=No se ha podido iniciar la librería de readline 487 | failed.revoke=No se pudo anular 488 | failed.role.change=No se ha podido añadir/quitar el rol 489 | failed.role.create=No se ha podido crear el rol 490 | failed.role.delete=¡No se ha podido borrar el rol! 491 | failed.role.edit=No se ha podido editar el rol 492 | failed.roles=No se han podido conseguir los roles 493 | failed.session.start=No se ha podido abrir sesión. 494 | failed.settings=No se ha podido consultar las opciones del usuario 495 | failed.status=No se ha podido establecer el estatus. 496 | failed.timestamp=No se ha podido analizar marca de tiempo 497 | failed.transfer=No se ha podido transferir la propiedad. 498 | failed.typing=No se ha podido empezar a escribir 499 | failed.unpin=No se pudo retirar el mensaje 500 | failed.user.edit=No se ha podido editar los datos del usuario 501 | failed.user=No se ha podido consultar el usuario 502 | failed.voice.connect=No se ha podido conectar al canal de voz. 503 | failed.voice.disconnect=No se ha podido desconectar 504 | failed.voice.regions=No se han podido obtener las regiones 505 | failed.voice.speak=No se ha podido empezar a hablar 506 | failed.webrequest=No se ha podido hacer la solicitud de web 507 | 508 | information.aborted=Abortado. 509 | information.category=Categoría 510 | information.created.successfully= se creó correctamente con ID 511 | information.confirmation=¿Estás realmente seguro de que quieres hacer esto? 512 | information.deleted.successfully=" se borró correctamente. 513 | information.give.ownership=Esto le dará la propiedad del servidor a 514 | information.guild=Servidor 515 | information.channel=Canal 516 | information.irreversible=¡Esta acción es irrreversible! 517 | information.moved=Usuario movido. 518 | information.note=¡Nota cambiada! 519 | information.revoked.successfully=Anulado. 520 | information.wait=¡Espera un segundo! 521 | information.warning=¡Advertencia! 522 | 523 | intro.exit=Pulsa Ctrl+D o escribe 'exit' para salir. 524 | intro.help=Escribe 'help' para obtener ayuda 525 | 526 | invalid.bookmark=No existe el marcador 527 | invalid.cache=Caché no disponible! 528 | invalid.channel.voice=¡No se ha seleccionado ningún chat de voz! 529 | invalid.channel=¡Ningún canal seleccionado! 530 | invalid.command2=Escribe 'help' para obtener ayuda. 531 | invalid.command=Comando desconocido: 532 | invalid.guild=¡No se ha seleccionado ningún servidor! 533 | invalid.id=¡Necesitas seleccionar algo para obtener su ID! 534 | invalid.limit.message=El mensaje excede el límite de caracteres. 535 | invalid.music.playing=Ya se está reproduciendo algo. 536 | invalid.not.owner=¡No eres el propietario del servidor! 537 | invalid.number=No es un número. 538 | invalid.onlyfor.bots=Este comando solo funciona con usuarios de bots. 539 | invalid.onlyfor.users=Esto solo funciona para usuarios. 540 | invalid.role=No hay un rol con esa ID 541 | invalid.source.terminal=Debes estar en la terminal para hacer esto. 542 | invalid.status.offline=El estatus desconectado existe, pero no se puede enviar a través de la API 543 | invalid.substitute=Sustituto inválido 544 | invalid.unmatched.quote=Comillas sin cerrar en la cadena de caracteres entrante 545 | invalid.value=No existe esa variable 546 | invalid.webhook.command=No es un comando webhook permitido 547 | invalid.webhook=Formato de webhook inválido. Formato: id/token 548 | invalid.yn=Por favor escriba 'y'(sí) o 'n'(no). 549 | 550 | login.finish=Sesión iniciado con ID de usuario 551 | login.hidden=[Oculto] 552 | login.starting=Autenticando... 553 | login.token.user=Los tokens de usuario se prefijan con 'user ' 554 | login.token.webhook=Los tokens de webhook se prefijan con 'webhook ', y su URL o id/token 555 | login.token=Por favor pega tu 'token' aquí. 556 | 557 | pointer.private=Privado 558 | pointer.unknown=Desconocido 559 | 560 | status.avatar=¡Avatar establecido! 561 | status.cache=¡Mensage mandado al caché! 562 | status.channel=Canal seleccionado con ID 563 | status.invite.accept=Invitación aceptada. 564 | status.invite.create=Invitación creada con código: 565 | status.loading=Cargando... 566 | status.msg.create=Creado mensaje con ID 567 | status.msg.delall=# mensajes borrados. 568 | status.name=¡Nombre establecido! 569 | status.status=¡Estatus establecido! 570 | 571 | rl.cache.loc=Recargando el caché del servidor actual... 572 | rl.cache.vars=Borrando variables del caché... 573 | rl.session=Reiniciando sesión... 574 | ` 575 | -------------------------------------------------------------------------------- /location.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "github.com/bwmarrin/discordgo" 22 | "github.com/jD91mZM2/stdutil" 23 | ) 24 | 25 | type location struct { 26 | guild *discordgo.Guild 27 | channel *discordgo.Channel 28 | } 29 | 30 | func (loc *location) push(guild *discordgo.Guild, channel *discordgo.Channel) { 31 | sameGuild := guild == loc.guild || (loc.guild != nil && guild != nil && loc.guild.ID == guild.ID) 32 | sameChannel := channel == loc.channel || (loc.channel != nil && channel != nil && loc.channel.ID == channel.ID) 33 | 34 | if sameGuild && sameChannel { 35 | return 36 | } 37 | 38 | lastLoc.guild = loc.guild 39 | lastLoc.channel = loc.channel 40 | 41 | loc.guild = guild 42 | loc.channel = channel 43 | pointerCache = "" 44 | 45 | if !sameGuild { 46 | cacheChannels = nil 47 | } 48 | 49 | var err error 50 | if vc != nil { 51 | playing = "" 52 | if channel != nil && channel.Type == discordgo.ChannelTypeGuildVoice { 53 | err = vc.ChangeChannel(channel.ID, false, false) 54 | } else { 55 | err = vc.Disconnect() 56 | vc = nil 57 | } 58 | if err != nil { 59 | stdutil.PrintErr(tl("failed.voice.disconnect"), err) 60 | } 61 | } else if guild != nil && channel != nil && channel.Type == discordgo.ChannelTypeGuildVoice { 62 | vc, err = session.ChannelVoiceJoin(guild.ID, channel.ID, false, false) 63 | if err != nil { 64 | stdutil.PrintErr(tl("failed.voice.connect"), err) 65 | } 66 | } 67 | } 68 | 69 | var loc = &location{} 70 | var lastLoc = &location{} 71 | -------------------------------------------------------------------------------- /lua.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "strconv" 22 | "strings" 23 | "time" 24 | 25 | "github.com/Shopify/go-lua" 26 | "github.com/bwmarrin/discordgo" 27 | "github.com/fatih/color" 28 | "github.com/jD91mZM2/stdutil" 29 | ) 30 | 31 | type luaEventData struct { 32 | state *lua.State 33 | function string 34 | } 35 | 36 | var luaMessageEvents = make(map[string]*luaEventData) 37 | 38 | func runLua(session *discordgo.Session, file string, args ...string) error { 39 | l := lua.NewState() 40 | 41 | l.Register("exec", luaExec) 42 | l.Register("replace", luaReplace) 43 | l.Register("sleep", luaSleep) 44 | l.Register("registerEvent", luaRegister) 45 | 46 | l.NewTable() 47 | for i, val := range args { 48 | l.PushInteger(i + 1) 49 | l.PushString(val) 50 | l.SetTable(-3) 51 | } 52 | l.SetGlobal("arg") 53 | 54 | lua.OpenLibraries(l) 55 | 56 | err := lua.DoFile(l, file) 57 | return err 58 | } 59 | 60 | func luaExec(l *lua.State) int { 61 | colorAutomated.Set() 62 | returnVal := command(session, commandSource{}, lua.CheckString(l, 1), color.Output) 63 | color.Unset() 64 | 65 | l.PushString(returnVal) 66 | return 1 67 | } 68 | func luaReplace(l *lua.State) int { 69 | replaced := strings.Replace(lua.CheckString(l, 1), lua.CheckString(l, 2), lua.CheckString(l, 3), -1) 70 | l.PushString(replaced) 71 | return 1 72 | } 73 | func luaSleep(l *lua.State) int { 74 | num := lua.CheckInteger(l, 1) 75 | time.Sleep(time.Duration(num) * time.Second) 76 | return 0 77 | } 78 | func luaRegister(l *lua.State) int { 79 | id := lua.CheckString(l, 1) 80 | name := lua.CheckString(l, 2) 81 | luaMessageEvents[id] = &luaEventData{ 82 | state: l, 83 | function: name, 84 | } 85 | return 0 86 | } 87 | 88 | func luaMessageEvent(session *discordgo.Session, e *discordgo.Message) { 89 | timestamp, err := timestamp(e) 90 | if err != nil { 91 | stdutil.PrintErr(tl("failed.timestamp"), err) 92 | } 93 | 94 | defer func() { 95 | r := recover() 96 | if r != nil { 97 | stdutil.PrintErr(tl("failed.lua.event"), nil) 98 | } 99 | }() 100 | 101 | params := map[string]string{ 102 | "ID": e.ID, 103 | "Content": e.Content, 104 | "ChannelID": e.ChannelID, 105 | "Timestamp": timestamp, 106 | 107 | "AuthorID": e.Author.ID, 108 | "AuthorBot": strconv.FormatBool(e.Author.Bot), 109 | "AuthorAvatar": e.Author.Avatar, 110 | "AuthorName": e.Author.Username, 111 | } 112 | 113 | for _, event := range luaMessageEvents { 114 | l := event.state 115 | l.Global(event.function) 116 | 117 | l.NewTable() 118 | for key, val := range params { 119 | l.PushString(key) 120 | l.PushString(val) 121 | l.SetTable(-3) 122 | } 123 | 124 | l.Call(1, 0) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "flag" 22 | "fmt" 23 | "io" 24 | "io/ioutil" 25 | "os" 26 | "os/exec" 27 | "os/signal" 28 | "strings" 29 | "syscall" 30 | "time" 31 | 32 | "github.com/atotto/clipboard" 33 | "github.com/bwmarrin/discordgo" 34 | "github.com/chzyer/readline" 35 | "github.com/fatih/color" 36 | "github.com/jD91mZM2/stdutil" 37 | ) 38 | 39 | const autoRunFile = ".autorun" 40 | const version = "3.0.2" 41 | 42 | var devVersion = strings.Contains(version, "dev") 43 | 44 | const ( 45 | typeUser = iota 46 | typeBot 47 | typeWebhook 48 | ) 49 | 50 | var closing bool 51 | var closed = make(chan bool) 52 | 53 | var userID string 54 | var userToken string 55 | var userType int 56 | var session *discordgo.Session 57 | var userObj *discordgo.User 58 | 59 | var rl *readline.Instance 60 | var colorDefault = color.New(color.Bold) 61 | var colorAutomated = color.New(color.Italic) 62 | var colorMsg = color.New(color.FgYellow) 63 | var colorChatMode = color.New(color.FgBlue) 64 | var colorError = color.New(color.FgRed, color.Bold) 65 | 66 | const msgLimit = 2000 67 | 68 | func main() { 69 | defer handleCrash() 70 | defer func() { 71 | closing = true 72 | 73 | playing = "" 74 | if vc != nil { 75 | vc.Disconnect() 76 | } 77 | 78 | if session != nil { 79 | session.Close() 80 | } 81 | color.Unset() 82 | 83 | close(closed) 84 | }() 85 | 86 | go func() { 87 | c := make(chan os.Signal, 2) 88 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 89 | 90 | <-c 91 | closing = true 92 | after := time.After(time.Second * 2) 93 | 94 | select { 95 | case <-closed: 96 | case <-after: 97 | // Took too long. 98 | // Malicious LUA script? 99 | stdutil.PrintErr("Timed out", nil) 100 | os.Exit(1) 101 | } 102 | }() 103 | 104 | var token string 105 | var langfile string 106 | var help string 107 | var commands stringArr 108 | 109 | var noupdate bool 110 | var noautorun bool 111 | 112 | flag.StringVar(&token, "t", "", "Set token.") 113 | flag.StringVar(&langfile, "lang", "en", "Set language. Either a file path, or any of the following: en, sv, es") 114 | flag.StringVar(&help, "lookup", "", "Search in `help` without starting the console") 115 | flag.Var(&commands, "x", "Pre-execute command. Can use flag multiple times.") 116 | 117 | flag.BoolVar(&noupdate, "noupdate", false, "Disable update checking.") 118 | flag.BoolVar(&noautorun, "noautorun", false, "Disable running commands in "+autoRunFile+" file.") 119 | flag.Parse() 120 | 121 | if help != "" { 122 | printHelp(help) 123 | return 124 | } 125 | 126 | doErrorHook() 127 | fmt.Println("DiscordConsole " + version) 128 | if devVersion { 129 | fmt.Println("This is a pre-release version of DiscordConsole. Please report any bugs on GitHub.") 130 | } 131 | 132 | fmt.Println(` 133 | Copyright (C) 2020 Mnpn 134 | This program comes with absolutely no warranty. 135 | This is free software, and you are welcome to redistribute it under certain conditions.`) 136 | 137 | loadLangAuto(langfile) 138 | 139 | if !noupdate { 140 | fmt.Print(tl("update.checking") + " ") 141 | update, err := checkUpdate() 142 | if err != nil { 143 | stdutil.PrintErr(tl("update.error"), err) 144 | } else { 145 | if update.UpdateAvailable { 146 | fmt.Println() 147 | color.Cyan(tl("update.available") + " " + update.Version) 148 | color.Cyan(tl("update.download") + " " + update.URL) 149 | } else { 150 | fmt.Println(tl("update.none")) 151 | } 152 | } 153 | } 154 | 155 | fmt.Println(tl("loading.bookmarks")) 156 | err := loadBookmarks() 157 | if err != nil { 158 | stdutil.PrintErr(tl("failed.file.read"), err) 159 | } 160 | 161 | var arLines []string 162 | if !noautorun { 163 | ar, err := ioutil.ReadFile(autoRunFile) 164 | if err != nil { 165 | if !os.IsNotExist(err) { 166 | stdutil.PrintErr(tl("failed.file.read")+autoRunFile, err) 167 | } 168 | } else if err == nil { 169 | arLines = strings.Split(string(ar), "\n") 170 | 171 | if len(arLines) > 0 { 172 | firstLine := arLines[0] 173 | if strings.HasPrefix(firstLine, ":") { 174 | token = firstLine[1:] 175 | arLines = arLines[1:] 176 | } 177 | } 178 | } 179 | } 180 | 181 | rl, err = readline.New(pointerEmpty) 182 | if err != nil { 183 | stdutil.PrintErr(tl("failed.readline.start"), err) 184 | return 185 | } 186 | 187 | fmt.Println() 188 | fmt.Println(tl("login.token")) 189 | fmt.Println(tl("login.token.user")) 190 | fmt.Println(tl("login.token.webhook")) 191 | fmt.Print("> ") 192 | if token == "" { 193 | token, err = rl.Readline() 194 | if err != nil { 195 | if err != io.EOF && err != readline.ErrInterrupt { 196 | stdutil.PrintErr(tl("failed.readline.read"), err) 197 | } 198 | return 199 | } 200 | } else { 201 | fmt.Println(tl("login.hidden")) 202 | } 203 | 204 | // Very unlikely someone's token is ever going to ever be ${paste}. 205 | if strings.Contains(token, "${paste}") { 206 | clipboardcontent, err := clipboard.ReadAll() 207 | if err != nil { 208 | stdutil.PrintErr((tl("failed.paste") + err.Error()), nil) 209 | return 210 | } 211 | replacer := strings.NewReplacer("${paste}", clipboardcontent) 212 | token = replacer.Replace(token) 213 | } 214 | 215 | fmt.Println(tl("login.starting")) 216 | 217 | token = strings.ReplaceAll(token, "\"", "") 218 | lower := strings.ToLower(token) 219 | 220 | if strings.HasPrefix(lower, "webhook ") { 221 | token = token[len("webhook "):] 222 | 223 | parts := strings.Split(token, "/") 224 | 225 | len := len(parts) 226 | if len >= 2 { 227 | userID = parts[len-2] 228 | userToken = parts[len-1] 229 | } else { 230 | stdutil.PrintErr(tl("invalid.webhook"), nil) 231 | return 232 | } 233 | 234 | userType = typeWebhook 235 | session, _ = discordgo.New(userToken) 236 | } else { 237 | if strings.HasPrefix(lower, "user ") { 238 | token = token[len("user "):] 239 | userType = typeUser 240 | } else { 241 | if !strings.HasPrefix(token, "Bot ") { 242 | token = "Bot " + token 243 | } 244 | userType = typeBot 245 | intercept = false 246 | } 247 | session, _ = discordgo.New(token) 248 | } 249 | 250 | if userType != typeWebhook { 251 | if err != nil { 252 | stdutil.PrintErr(tl("failed.auth"), err) 253 | return 254 | } 255 | 256 | userToken = session.Token 257 | 258 | user, err := session.User("@me") 259 | if err != nil { 260 | stdutil.PrintErr(tl("failed.user"), err) 261 | return 262 | } 263 | 264 | userID = user.ID 265 | userObj = user 266 | 267 | session.AddHandler(ready) 268 | session.AddHandler(guildCreate) 269 | session.AddHandler(guildDelete) 270 | session.AddHandler(messageCreate) 271 | err = session.Open() 272 | if err != nil { 273 | stdutil.PrintErr(tl("failed.session.open"), err) 274 | return 275 | } 276 | 277 | fmt.Println(tl("login.finish") + " " + userID + " (" + user.Username + "#" + user.Discriminator + ").") 278 | } 279 | fmt.Println(tl("intro.help")) 280 | fmt.Println(tl("intro.exit")) 281 | 282 | for i := 0; i < 3; i++ { 283 | fmt.Println() 284 | } 285 | 286 | colorAutomated.Set() 287 | 288 | for _, cmd := range arLines { 289 | if cmd == "" { 290 | continue 291 | } 292 | printPointer(session) 293 | fmt.Println(cmd) 294 | 295 | command(session, commandSource{Terminal: true}, cmd, color.Output) 296 | if closing { 297 | return 298 | } 299 | } 300 | for _, cmd := range commands { 301 | printPointer(session) 302 | fmt.Println(cmd) 303 | 304 | command(session, commandSource{Terminal: true}, cmd, color.Output) 305 | if closing { 306 | return 307 | } 308 | } 309 | 310 | color.Unset() 311 | setCompleter(rl) 312 | 313 | for { 314 | colorDefault.Set() 315 | 316 | rl.SetPrompt(pointer(session)) 317 | cmd, err := rl.Readline() 318 | 319 | color.Unset() 320 | 321 | if err != nil { 322 | if err != io.EOF && err != readline.ErrInterrupt { 323 | stdutil.PrintErr(tl("failed.readline.read"), err) 324 | } else { 325 | fmt.Println("exit") 326 | } 327 | closing = true 328 | return 329 | } 330 | 331 | command(session, commandSource{Terminal: true}, cmd, color.Output) 332 | if closing { 333 | break 334 | } 335 | } 336 | } 337 | 338 | func execute(command string, args ...string) error { 339 | cmd := exec.Command(command, args...) 340 | cmd.Stdin = os.Stdin 341 | cmd.Stdout = os.Stdout 342 | cmd.Stderr = os.Stderr 343 | return cmd.Run() 344 | } 345 | 346 | func printMessage(session *discordgo.Session, msg *discordgo.Message, prefixR bool, guild *discordgo.Guild, channel *discordgo.Channel, w io.Writer) { 347 | var s string 348 | if prefixR { 349 | s += "\r" 350 | } 351 | t, err := timestamp(msg) 352 | if err == nil { 353 | s += t + " - " 354 | } 355 | s += "(" 356 | 357 | if isPrivate(channel) { 358 | s += "Private" 359 | } else { 360 | s += guild.Name + " #" + channel.Name 361 | } 362 | 363 | s += ") " + msg.Author.Username + "#" + msg.Author.Discriminator + ": " + msgToString(msg) 364 | 365 | color.Unset() 366 | colorMsg.Set() 367 | writeln(w, s) 368 | color.Unset() 369 | colorDefault.Set() 370 | } 371 | 372 | func writeln(w io.Writer, line string) error { 373 | // No error catching for now. 374 | // Because... if printing out fails, 375 | // chances are printing the error also fails 376 | _, err := w.Write([]byte(line + "\n")) 377 | return err 378 | } 379 | 380 | func handleCrash() { 381 | if val := recover(); val != nil { 382 | if val == "die" { 383 | panic(val) 384 | } 385 | 386 | // No translations here. We want to be as safe as possible. 387 | stdutil.PrintErr("DiscordConsole " + version + " has crashed.", nil) 388 | stdutil.PrintErr("Please tell the DiscordConsole team what you did to cause this.", nil) 389 | stdutil.PrintErr("https://discord.gg/xvQV8bT,", nil) 390 | stdutil.PrintErr("https://github.com/discordconsole-team/discordconsole/issues", nil) 391 | stdutil.PrintErr("Error Details: "+fmt.Sprint(val), nil) 392 | } 393 | } 394 | 395 | const pointerEmpty = "> " 396 | 397 | var pointerCache string 398 | 399 | func printPointer(session *discordgo.Session) { 400 | fmt.Print(pointer(session)) 401 | } 402 | func pointer(session *discordgo.Session) string { 403 | if pointerCache != "" { 404 | return pointerCache 405 | } 406 | 407 | if loc.channel == nil { 408 | return pointerEmpty 409 | } 410 | 411 | s := "" 412 | 413 | if isPrivate(loc.channel) { 414 | recipient := tl("pointer.unknown") 415 | if len(loc.channel.Recipients) > 0 { 416 | recipient = loc.channel.Recipients[0].Username 417 | } 418 | s += tl("pointer.private") + " (" + recipient + ")" 419 | } else { 420 | guild := "" 421 | if loc.guild != nil { 422 | guild = loc.guild.Name 423 | } 424 | s += guild + " (#" + loc.channel.Name + ")" 425 | } 426 | 427 | s += pointerEmpty 428 | pointerCache = s 429 | return s 430 | } 431 | 432 | func say(session *discordgo.Session, w io.Writer, channel, str string) (*discordgo.Message, bool) { 433 | if userType == typeWebhook { 434 | _, err := session.WebhookExecute(userID, userToken, false, &discordgo.WebhookParams{ 435 | Content: str, 436 | }) 437 | if err != nil { 438 | stdutil.PrintErr(tl("failed.msg.send"), err) 439 | return nil, false 440 | } 441 | return nil, true 442 | } 443 | 444 | msg, err := session.ChannelMessageSend(loc.channel.ID, str) 445 | if err != nil { 446 | stdutil.PrintErr(tl("failed.msg.send"), err) 447 | return nil, false 448 | } 449 | writeln(w, tl("status.msg.create")+" "+msg.ID) 450 | 451 | return msg, true 452 | } 453 | 454 | func isPrivate(channel *discordgo.Channel) bool { 455 | return channel.Type == discordgo.ChannelTypeDM || channel.Type == discordgo.ChannelTypeGroupDM 456 | } 457 | -------------------------------------------------------------------------------- /main_!windows.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | /* 4 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 5 | Copyright (C) 2020 Mnpn 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | package main 21 | 22 | const sh = "sh" 23 | const c = "-c" 24 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fatih/color" 7 | ) 8 | 9 | func TestAlias(t *testing.T) { 10 | // Alias known to use map 11 | go func() { 12 | command(nil, commandSource{ 13 | // NoMutex: true, 14 | }, "alias test exec echo hi", color.Output) 15 | }() 16 | go func() { 17 | command(nil, commandSource{ 18 | // NoMutex: true, 19 | }, "test", color.Output) 20 | }() 21 | } 22 | -------------------------------------------------------------------------------- /main_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | const sh = "cmd" 21 | const c = "/c" 22 | -------------------------------------------------------------------------------- /msgutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "errors" 22 | "time" 23 | 24 | "github.com/bwmarrin/discordgo" 25 | ) 26 | 27 | var errMsgNotFound = errors.New("message not found") 28 | 29 | func timestamp(e *discordgo.Message) (string, error) { 30 | t, err := e.Timestamp.Parse() 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | s := t.Format(time.ANSIC) 36 | 37 | if e.EditedTimestamp != "" { 38 | s += "*" 39 | } 40 | 41 | return s, nil 42 | } 43 | func getMessage(session *discordgo.Session, channel, msgID string) (*discordgo.Message, error) { 44 | if userType == typeUser { 45 | msgs, err := session.ChannelMessages(channel, 3, "", "", msgID) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // This comment is a gravestone as memory 51 | // from when I did a web request manually 52 | // because I didn't wanna use the develop branch. 53 | // lol. 54 | 55 | for _, m := range msgs { 56 | if m.ID == msgID { 57 | return m, nil 58 | } 59 | } 60 | return nil, errMsgNotFound 61 | } 62 | return session.ChannelMessage(channel, msgID) 63 | } 64 | -------------------------------------------------------------------------------- /music.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "encoding/binary" 22 | "errors" 23 | "io" 24 | "os" 25 | 26 | "github.com/bwmarrin/discordgo" 27 | "github.com/jD91mZM2/stdutil" 28 | ) 29 | 30 | var vc *discordgo.VoiceConnection 31 | 32 | var playing string 33 | var cacheAudio = make(map[string][][]byte, 0) 34 | 35 | var errDcaNegaitve = errors.New("negative number in DCA file") 36 | 37 | func loadAudio(file string, buffer *[][]byte) error { 38 | cache, ok := cacheAudio[file] 39 | if ok { 40 | *buffer = cache 41 | return nil 42 | } 43 | 44 | reader, err := os.Open(file) 45 | if err != nil { 46 | return err 47 | } 48 | defer reader.Close() 49 | 50 | var length int16 51 | for { 52 | err := binary.Read(reader, binary.LittleEndian, &length) 53 | if err == io.EOF || err == io.ErrUnexpectedEOF { 54 | break 55 | } else if err != nil { 56 | return err 57 | } 58 | 59 | if length <= 0 { 60 | return errDcaNegaitve 61 | } 62 | 63 | buf := make([]byte, length) 64 | err = binary.Read(reader, binary.LittleEndian, buf) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | *buffer = append(*buffer, buf) 70 | } 71 | 72 | cacheAudio[file] = *buffer 73 | return nil 74 | } 75 | 76 | func play(buffer [][]byte, session *discordgo.Session, guild, channel string) { 77 | err := vc.Speaking(true) 78 | if err != nil { 79 | stdutil.PrintErr(tl("failed.voice.speak"), err) 80 | return 81 | } 82 | defer func() { 83 | err = vc.Speaking(false) 84 | if err != nil { 85 | stdutil.PrintErr(tl("failed.voice.speak"), err) 86 | return 87 | } 88 | }() 89 | 90 | for _, buf := range buffer { 91 | if playing == "" { 92 | break 93 | } 94 | vc.OpusSend <- buf 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type tokenKind int 10 | 11 | const ( 12 | tokenSeparator tokenKind = iota 13 | tokenString 14 | tokenSubstitute 15 | ) 16 | 17 | type token struct { 18 | kind tokenKind 19 | text string 20 | } 21 | 22 | type tokenizer struct { 23 | quote bool 24 | } 25 | 26 | func (tokenizer *tokenizer) nextToken(r *strings.Reader) (t token, err error) { 27 | text := "" 28 | for { 29 | var c rune 30 | c, _, err = r.ReadRune() 31 | if err == io.EOF && len(text) > 0 { 32 | t = token{ 33 | kind: tokenString, 34 | text: text, 35 | } 36 | err = nil 37 | return 38 | } 39 | if err != nil { 40 | return 41 | } 42 | 43 | if c == '"' { 44 | tokenizer.quote = !tokenizer.quote 45 | } else if c == '$' { 46 | if len(text) > 0 { 47 | r.UnreadRune() 48 | t = token{ 49 | kind: tokenString, 50 | text: text, 51 | } 52 | return 53 | } 54 | 55 | c, _, err = r.ReadRune() 56 | if err != nil { 57 | return 58 | } 59 | 60 | text += string(c) 61 | 62 | if c == '{' { 63 | text = "" 64 | for c != '}' { 65 | c, _, err = r.ReadRune() 66 | if err != nil { 67 | return 68 | } 69 | text += string(c) 70 | } 71 | text = text[:len(text)-1] 72 | t = token{ 73 | kind: tokenSubstitute, 74 | text: text, 75 | } 76 | return 77 | } 78 | 79 | text += string(c) 80 | } else if c == '\\' { 81 | c, _, err = r.ReadRune() 82 | if err != nil { 83 | return 84 | } 85 | 86 | if c == 'n' { 87 | c = '\n' 88 | } 89 | 90 | text += string(c) 91 | } else if (c == ' ' || c == '\t') && !tokenizer.quote { 92 | if len(text) > 0 { 93 | r.UnreadByte() 94 | t = token{ 95 | kind: tokenString, 96 | text: text, 97 | } 98 | return 99 | } 100 | 101 | t = token{ 102 | kind: tokenSeparator, 103 | text: string(c), 104 | } 105 | return 106 | } else { 107 | text += string(c) 108 | } 109 | } 110 | } 111 | 112 | func parse(subst func(string) (string, bool), text string) ([]string, error) { 113 | reader := strings.NewReader(text) 114 | 115 | var previous []string 116 | current := "" 117 | 118 | var t tokenizer 119 | 120 | for { 121 | token, err := t.nextToken(reader) 122 | if err == io.EOF { 123 | break 124 | } 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | switch token.kind { 130 | case tokenSeparator: 131 | previous = append(previous, current) 132 | current = "" 133 | case tokenSubstitute: 134 | val, ok := subst(token.text) 135 | if !ok { 136 | return nil, errors.New(tl("invalid.substitute") + ": " + token.text) 137 | } 138 | current += val 139 | case tokenString: 140 | current += token.text 141 | default: 142 | panic("Unreachable code: All tokens should be checked") 143 | } 144 | } 145 | 146 | if t.quote { 147 | return nil, errors.New(tl("invalid.unmatched.quote")) 148 | } 149 | previous = append(previous, current) 150 | 151 | return previous, nil 152 | } 153 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "strings" 22 | 23 | "github.com/bwmarrin/discordgo" 24 | ) 25 | 26 | type keyval struct { 27 | Key string 28 | Val string 29 | } 30 | 31 | func (data *keyval) String() string { 32 | return data.Key + ": " + data.Val 33 | } 34 | 35 | func findValByKey(keyvals []*keyval, key string) (string, bool) { 36 | for _, keyval := range keyvals { 37 | if strings.EqualFold(key, keyval.Key) { 38 | return keyval.Val, true 39 | } 40 | } 41 | return "", false 42 | } 43 | 44 | type commandSource struct { 45 | Terminal bool 46 | NoMutex bool 47 | Alias bool 48 | } 49 | 50 | var typeRelationships = map[int]string{ 51 | 1: "Friend", 52 | 2: "Blocked", 53 | 3: "Incoming request", 54 | 4: "Sent request", 55 | } 56 | var typeVerifications = map[discordgo.VerificationLevel]string{ 57 | discordgo.VerificationLevelNone: "None", 58 | discordgo.VerificationLevelLow: "Low", 59 | discordgo.VerificationLevelMedium: "Medium", 60 | discordgo.VerificationLevelHigh: "High", 61 | discordgo.VerificationLevelVeryHigh: "Very High", 62 | } 63 | var typeContentFilter = map[discordgo.ExplicitContentFilterLevel]string{ 64 | discordgo.ExplicitContentFilterDisabled: "Off", 65 | discordgo.ExplicitContentFilterMembersWithoutRoles: "Members without roles", 66 | discordgo.ExplicitContentFilterAllMembers: "All", 67 | } 68 | var typeMfa = map[discordgo.MfaLevel]string{ 69 | discordgo.MfaLevelNone: "Off", 70 | discordgo.MfaLevelElevated: "On", 71 | } 72 | var typeMessages = map[string]int{ 73 | "all": messagesAll, 74 | "mentions": messagesMentions, 75 | "private": messagesPrivate, 76 | "current": messagesCurrent, 77 | "none": messagesNone, 78 | } 79 | var typeStatuses = map[string]discordgo.Status{ 80 | "online": discordgo.StatusOnline, 81 | "idle": discordgo.StatusIdle, 82 | "dnd": discordgo.StatusDoNotDisturb, 83 | "invisible": discordgo.StatusInvisible, 84 | } 85 | var typeGames = map[string]discordgo.GameType{ 86 | "streaming": discordgo.GameTypeStreaming, 87 | "listening": discordgo.GameTypeListening, 88 | "watching": discordgo.GameTypeWatching, 89 | } 90 | var typeChannel = map[discordgo.ChannelType]string{ 91 | discordgo.ChannelTypeDM: "DM", 92 | discordgo.ChannelTypeGroupDM: "Group", 93 | discordgo.ChannelTypeGuildCategory: "Category", 94 | discordgo.ChannelTypeGuildText: "Text", 95 | discordgo.ChannelTypeGuildVoice: "Voice", 96 | } 97 | 98 | const ( 99 | messagesNone = iota 100 | messagesCurrent 101 | messagesPrivate 102 | messagesMentions 103 | messagesAll 104 | ) 105 | 106 | type stringArr []string 107 | 108 | func (arr *stringArr) Set(val string) error { 109 | *arr = append(*arr, val) 110 | return nil 111 | } 112 | 113 | func (arr *stringArr) String() string { 114 | return "[" + strings.Join(*arr, " ") + "]" 115 | } 116 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | /* 2 | DiscordConsole is a software aiming to give you full control over accounts, bots and webhooks! 3 | Copyright (C) 2020 Mnpn 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU 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 General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | package main 19 | 20 | import ( 21 | "encoding/json" 22 | "errors" 23 | "io/ioutil" 24 | "net/http" 25 | ) 26 | 27 | const updateURL = "https://api.github.com/repos/discordconsole-team/DiscordConsole/releases" 28 | 29 | var errNoRelease = errors.New("no release available") 30 | 31 | type updateObj struct { 32 | Version string `json:"tag_name"` 33 | URL string `json:"html_url"` 34 | UpdateAvailable bool 35 | } 36 | 37 | func checkUpdate() (*updateObj, error) { 38 | res, err := http.Get(updateURL) 39 | if err != nil { 40 | return nil, err 41 | } 42 | defer res.Body.Close() 43 | 44 | content, err := ioutil.ReadAll(res.Body) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | var updates []updateObj 50 | err = json.Unmarshal(content, &updates) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if len(updates) < 1 { 56 | return nil, errNoRelease 57 | } 58 | 59 | update := updates[0] 60 | 61 | update.UpdateAvailable = update.Version != version 62 | return &update, nil 63 | } 64 | --------------------------------------------------------------------------------