├── .dockerignore ├── .drone.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── commands.go ├── events.go ├── go.mod ├── go.sum ├── logging.go ├── main.go ├── msgAvatar.go ├── msgEmoji.go ├── msgEncode.go ├── msgIbsearch.go ├── msgImageRecall.go ├── msgLogging.go ├── msgPlaylist.go ├── msgPrefix.go ├── msgPurge.go ├── msgRule34.go ├── msgUserStats.go ├── msgUtils.go ├── msgYoutube.go ├── server.go ├── storage.go ├── structs.go └── utilities.go /.dockerignore: -------------------------------------------------------------------------------- 1 | json/*.json 2 | emoji/* 3 | LICENSE 4 | README 5 | .gitignore 6 | .dockerignore 7 | .drone.yml -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | name: default 5 | 6 | platform: 7 | os: linux 8 | arch: amd64 9 | 10 | steps: 11 | - name: lint 12 | image: golang 13 | commands: 14 | - bash -c "if [[ \$(gofmt -l *.go) ]]; then gofmt -l *.go; exit 1; fi" 15 | when: 16 | event: 17 | - push 18 | - pull_request 19 | 20 | - name: docker-image 21 | image: plugins/docker 22 | settings: 23 | dockerfile: Dockerfile 24 | repo: strum355/2bot 25 | tags: latest 26 | username: 27 | from_secret: docker_username 28 | password: 29 | from_secret: docker_password 30 | when: 31 | branch: 32 | - master 33 | status: 34 | - success 35 | 36 | - name: discord-notif-success 37 | image: appleboy/drone-discord 38 | settings: 39 | avatar_url: https://raw.githubusercontent.com/drone/brand/3051b0d85318a2a20b62927ba19fc07e24c0d751/logos/png/white/drone-logo-png-white-256.png 40 | color: "#e04414" 41 | message: 2Bot successfully built and pushed. Build num {{build.number}}. {{build.link}} 42 | username: 2Bot CI 43 | environment: 44 | WEBHOOK_ID: 45 | from_secret: discord_webhook_id 46 | WEBHOOK_TOKEN: 47 | from_secret: discord_webhook_token 48 | when: 49 | branch: 50 | - master 51 | event: 52 | - push 53 | status: 54 | - success 55 | 56 | - name: discord-notif-failure 57 | image: appleboy/drone-discord 58 | settings: 59 | avatar_url: https://raw.githubusercontent.com/drone/brand/3051b0d85318a2a20b62927ba19fc07e24c0d751/logos/png/white/drone-logo-png-white-256.png 60 | color: "#e04414" 61 | message: 2Bot failed to build. Build num {{build.number}}. {{build.link}} 62 | username: 2Bot CI 63 | environment: 64 | WEBHOOK_ID: 65 | from_secret: discord_webhook_id 66 | WEBHOOK_TOKEN: 67 | from_secret: discord_webhook_token 68 | when: 69 | branch: 70 | - master 71 | event: 72 | - push 73 | status: 74 | - failure 75 | 76 | ... 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | emoji 2 | *.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15-alpine AS builder 2 | LABEL maintainer="Noah Santschi-Cooney (noah@santschi-cooney.ch)" 3 | 4 | WORKDIR /go/src/github.com/Strum355/2Bot-Discord-Bot 5 | 6 | ENV GOBIN=/go/2Bot 7 | ENV GOPATH=/go 8 | 9 | COPY . . 10 | 11 | RUN apk update && apk add --no-cache git 12 | 13 | RUN go mod download && \ 14 | go install -v ./... 15 | 16 | FROM alpine:3.11 17 | 18 | COPY --from=builder /go/2Bot /go/2Bot 19 | 20 | ENV PATH=/go/2Bot:/go/2Bot/ffmpeg:${PATH} 21 | 22 | RUN mkdir -p /go/2Bot/images/ && \ 23 | mkdir -p /go/2Bot/json/ && \ 24 | mkdir -p /go/2Bot/emoji/ && \ 25 | mkdir -p /go/2Bot/ffmpeg 26 | 27 | RUN apk --no-cache add ca-certificates && \ 28 | update-ca-certificates && \ 29 | apk update && \ 30 | apk add --no-cache opus ffmpeg 31 | 32 | WORKDIR /go/2Bot 33 | 34 | VOLUME ["/go/2Bot/images", "/go/2Bot/json"] 35 | 36 | EXPOSE 8080 37 | 38 | CMD ["2Bot-Discord-Bot"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 2Bot Discord Bot 633 | Copyright (C) 2017 Noah Santschi-Cooney 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2Bot-Discord-Bot 2 | ## Powered by Docker and DiscordGo 3 | 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/Strum355/2Bot-Discord-Bot)](https://goreportcard.com/report/github.com/Strum355/2Bot-Discord-Bot) 5 | [![Website](https://img.shields.io/badge/discord-2Bot%20Server-blue.svg)](https://discord.gg/9T34Y6u) 6 | [![Build Status](https://ci.netsoc.co/api/badges/UCCNetworkingSociety/Netsoc-Discord-Bot/status.svg?branch=master)](https://ci.netsoc.co/Strum355/2Bot-Discord-Bot/) 7 | 8 | 9 | 10 | ## Preface 11 | 12 | First to get a few things out of the way: 13 | 14 | - [Here](https://discordapp.com/api/oauth2/authorize?client_id=301819949683572738&scope=bot&permissions=11264) is the link to invite 2Bot to your server. 15 | - [Here](https://discord.gg/9T34Y6u) is a link to the support server, for support amongst other things. 16 | - [Read the wiki](https://github.com/Strum355/2Bot-Discord-Bot/wiki) to learn more about 2Bot! Updated sometimes. 17 | - If you ever run into problems with the bot, or have any questions, open an issue [here](https://github.com/Strum355/2Bot-Discord-Bot/issues) or join the support server. 18 | 19 | A command to automatically submit issues coming soon. 20 | 21 | ## Roadmap 22 | 23 | See [issues](https://github.com/Strum355/2Bot-Discord-Bot/issues). 24 | 25 | ### [Download the complementary SelfBot, 2Bot2Go, here!](https://github.com/Strum355/2Bot2Go) 26 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | ) 9 | 10 | var ( 11 | activeCommands = make(map[string]command) 12 | disabledCommands = make(map[string]command) 13 | ) 14 | 15 | type command struct { 16 | Name string 17 | Help string 18 | 19 | OwnerOnly bool 20 | RequiresPerms bool 21 | 22 | PermsRequired int 23 | 24 | Exec func(*discordgo.Session, *discordgo.MessageCreate, []string) 25 | } 26 | 27 | /* 28 | go-chi style command adding 29 | - aliases 30 | - nicer help text 31 | - uh...MORE 32 | also more here! 33 | */ 34 | 35 | func parseCommand(s *discordgo.Session, m *discordgo.MessageCreate, guildDetails *discordgo.Guild, message string) { 36 | msglist := strings.Fields(message) 37 | if len(msglist) == 0 { 38 | return 39 | } 40 | 41 | log.Trace(fmt.Sprintf("%s %s#%s, %s %s: %s", m.Author.ID, m.Author.Username, m.Author.Discriminator, guildDetails.ID, guildDetails.Name, m.Content)) 42 | 43 | commandName := strings.ToLower(func() string { 44 | if strings.HasPrefix(message, " ") { 45 | return " " + msglist[0] 46 | } 47 | return msglist[0] 48 | }()) 49 | 50 | if command, ok := activeCommands[commandName]; ok && commandName == strings.ToLower(command.Name) { 51 | userPerms, err := permissionDetails(m.Author.ID, m.ChannelID, s) 52 | if err != nil { 53 | s.ChannelMessageSend(m.ChannelID, "Error verifying permissions :(") 54 | return 55 | } 56 | 57 | isOwner := m.Author.ID == conf.OwnerID 58 | hasPerms := userPerms&command.PermsRequired > 0 59 | if (!command.OwnerOnly && !command.RequiresPerms) || (command.RequiresPerms && hasPerms) || isOwner { 60 | command.Exec(s, m, msglist) 61 | return 62 | } 63 | s.ChannelMessageSend(m.ChannelID, "You don't have the correct permissions to run this!") 64 | return 65 | } 66 | 67 | activeCommands["bigmoji"].Exec(s, m, msglist) 68 | } 69 | 70 | func (c command) add() command { 71 | activeCommands[strings.ToLower(c.Name)] = c 72 | return c 73 | } 74 | 75 | func newCommand(name string, permissions int, needsPerms bool, f func(*discordgo.Session, *discordgo.MessageCreate, []string)) command { 76 | return command{ 77 | Name: name, 78 | PermsRequired: permissions, 79 | RequiresPerms: needsPerms, 80 | Exec: f, 81 | } 82 | } 83 | 84 | func (c command) alias(a string) command { 85 | activeCommands[strings.ToLower(a)] = c 86 | return c 87 | } 88 | 89 | func (c command) setHelp(help string) command { 90 | c.Help = help 91 | return c 92 | } 93 | 94 | func (c command) ownerOnly() command { 95 | c.OwnerOnly = true 96 | return c 97 | } 98 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | ) 10 | 11 | func messageCreateEvent(s *discordgo.Session, m *discordgo.MessageCreate) { 12 | if m.Author.Bot { 13 | return 14 | } 15 | 16 | guildDetails, err := guildDetails(m.ChannelID, "", s) 17 | if err != nil { 18 | return 19 | } 20 | 21 | prefix, err := activePrefix(m.ChannelID, s) 22 | if err != nil { 23 | return 24 | } 25 | 26 | if !strings.HasPrefix(m.Content, conf.Prefix) && !strings.HasPrefix(m.Content, prefix) { 27 | return 28 | } 29 | 30 | parseCommand(s, m, guildDetails, func() string { 31 | if strings.HasPrefix(m.Content, conf.Prefix) { 32 | return strings.TrimPrefix(m.Content, conf.Prefix) 33 | } 34 | return strings.TrimPrefix(m.Content, prefix) 35 | }()) 36 | } 37 | 38 | func readyEvent(s *discordgo.Session, m *discordgo.Ready) { 39 | log.Trace("received ready event") 40 | /* s.ChannelMessageSendEmbed(logChan, &discordgo.MessageEmbed{ 41 | Fields: []*discordgo.MessageEmbedField{ 42 | {Name: "Info:", Value: "Received ready payload"}, 43 | }, 44 | }) */ 45 | setBotGame(s) 46 | } 47 | 48 | func guildJoinEvent(s *discordgo.Session, m *discordgo.GuildCreate) { 49 | if m.Unavailable { 50 | log.Info("joined unavailable guild", m.Guild.ID) 51 | s.ChannelMessageSendEmbed(logChan, &discordgo.MessageEmbed{ 52 | Fields: []*discordgo.MessageEmbedField{ 53 | {"Info", "Joined unavailable guild", true}, 54 | }, 55 | Color: 0x00ff00, 56 | }) 57 | return 58 | } 59 | 60 | user, err := userDetails(m.Guild.OwnerID, s) 61 | if err != nil { 62 | user = &discordgo.User{ 63 | Username: "error", 64 | Discriminator: "error", 65 | } 66 | } 67 | 68 | embed := &discordgo.MessageEmbed{ 69 | Image: &discordgo.MessageEmbedImage{ 70 | URL: discordgo.EndpointGuildIcon(m.Guild.ID, m.Guild.Icon), 71 | }, 72 | 73 | Footer: footer, 74 | 75 | Fields: []*discordgo.MessageEmbedField{ 76 | {"Name:", m.Guild.Name, true}, 77 | {"User Count:", strconv.Itoa(m.Guild.MemberCount), true}, 78 | {"Region:", m.Guild.Region, true}, 79 | {"Channel Count:", strconv.Itoa(len(m.Guild.Channels)), true}, 80 | {"ID:", m.Guild.ID, true}, 81 | {"Owner:", user.Username + "#" + user.Discriminator, true}, 82 | }, 83 | } 84 | 85 | if _, ok := sMap.server(m.Guild.ID); !ok { 86 | //if newly joined 87 | embed.Color = 0x00ff00 88 | s.ChannelMessageSendEmbed(logChan, embed) 89 | log.Info("joined server", m.Guild.ID, m.Guild.Name) 90 | 91 | sMap.setServer(m.Guild.ID, server{ 92 | LogChannel: m.Guild.ID, 93 | Log: false, 94 | Nsfw: false, 95 | JoinMessage: [3]string{"false", "", ""}, 96 | }) 97 | } else if val, _ := sMap.server(m.Guild.ID); val.Kicked { 98 | //If previously kicked and then readded 99 | embed.Color = 0xff9a00 100 | s.ChannelMessageSendEmbed(logChan, embed) 101 | log.Info("rejoined server", m.Guild.ID, m.Guild.Name) 102 | val.Kicked = false 103 | } 104 | 105 | saveServers() 106 | } 107 | 108 | func guildKickedEvent(s *discordgo.Session, m *discordgo.GuildDelete) { 109 | if m.Unavailable { 110 | guild, err := guildDetails("", m.Guild.ID, s) 111 | if err != nil { 112 | log.Trace("unavailable guild", m.Guild.ID) 113 | return 114 | } 115 | log.Trace("unavailable guild", m.Guild.ID, guild.Name) 116 | return 117 | } 118 | 119 | s.ChannelMessageSendEmbed(logChan, &discordgo.MessageEmbed{ 120 | Color: 0xff0000, 121 | Footer: footer, 122 | Fields: []*discordgo.MessageEmbedField{ 123 | {"Name:", m.Name, true}, 124 | {"ID:", m.Guild.ID, true}, 125 | }, 126 | }) 127 | 128 | log.Info("kicked from", m.Guild.ID, m.Name) 129 | 130 | if guild, ok := sMap.server(m.Guild.ID); ok { 131 | guild.Kicked = true 132 | } 133 | 134 | saveServers() 135 | } 136 | 137 | func presenceChangeEvent(s *discordgo.Session, m *discordgo.PresenceUpdate) { 138 | guild, ok := sMap.server(m.GuildID) 139 | if !ok || guild.Kicked || !guild.Log { 140 | return 141 | } 142 | 143 | memberStruct, err := memberDetails(m.GuildID, m.User.ID, s) 144 | if err != nil { 145 | return 146 | } 147 | 148 | s.ChannelMessageSend(guild.LogChannel, fmt.Sprintf("`%s is now %s`", memberStruct.User, status[m.Status])) 149 | } 150 | 151 | func memberJoinEvent(s *discordgo.Session, m *discordgo.GuildMemberAdd) { 152 | guild, ok := sMap.server(m.GuildID) 153 | if !ok || guild.Kicked || len(guild.JoinMessage) != 3 { 154 | return 155 | } 156 | 157 | isBool, err := strconv.ParseBool(guild.JoinMessage[0]) 158 | if err != nil { 159 | log.Error("couldnt parse bool", err) 160 | return 161 | } 162 | 163 | if !isBool || guild.JoinMessage[1] == "" { 164 | return 165 | } 166 | 167 | membStruct, err := userDetails(m.User.ID, s) 168 | if err != nil { 169 | return 170 | } 171 | 172 | s.ChannelMessageSend(guild.JoinMessage[2], strings.Replace(guild.JoinMessage[1], "%s", membStruct.Mention(), -1)) 173 | } 174 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/2Bot/2Bot-Discord-Bot 2 | 3 | go 1.12 4 | 5 | replace github.com/Sirupsen/logrus => github.com/sirupsen/logrus v1.4.2 6 | 7 | require ( 8 | github.com/Necroforger/dgwidgets v0.0.0-20190131052008-56c8c1ca33e0 9 | github.com/PuerkitoBio/goquery v1.5.0 // indirect 10 | github.com/Sirupsen/logrus v0.0.0-00010101000000-000000000000 // indirect 11 | github.com/Strum355/go-queue v1.0.0 12 | github.com/bwmarrin/discordgo v0.19.0 13 | github.com/go-chi/chi v4.0.2+incompatible 14 | github.com/jonas747/dca v0.0.0-20190317094138-10e959e9d3e8 15 | github.com/jonas747/ogg v0.0.0-20161220051205-b4f6f4cf3757 // indirect 16 | github.com/rylio/ytdl v0.5.1 17 | github.com/sirupsen/logrus v1.8.1 // indirect 18 | golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Necroforger/dgwidgets v0.0.0-20190131052008-56c8c1ca33e0 h1:x09GwpB1wwKc1o2TBcDP3uR5qPiOVo6Lh79dv2WdX3g= 2 | github.com/Necroforger/dgwidgets v0.0.0-20190131052008-56c8c1ca33e0/go.mod h1:3aFNgGdU2RT4mNS7ExVL2BGPsKa14bkOPOjrT9s9oR8= 3 | github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk= 4 | github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= 5 | github.com/Strum355/go-queue v1.0.0 h1:TB3EJHZPQiZtd/8FYutVexR1I5v49+nBwgzJ8O/BkA4= 6 | github.com/Strum355/go-queue v1.0.0/go.mod h1:I6UW0pAuWGJ7hc2l+r6MoICbdORnTcWYUpgCZtKCtBg= 7 | github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= 8 | github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 9 | github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY= 10 | github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= 14 | github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 15 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 16 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 17 | github.com/jonas747/dca v0.0.0-20190317094138-10e959e9d3e8 h1:k/3mvr7ImDZ8Ig/qcLVnvNSW99wlkbVyPDv4erwSQPQ= 18 | github.com/jonas747/dca v0.0.0-20190317094138-10e959e9d3e8/go.mod h1:rxjYX9OJU81unMxQDHChU/lAiOhlY9MV+faPX/NmwLk= 19 | github.com/jonas747/ogg v0.0.0-20161220051205-b4f6f4cf3757 h1:Kyv+zTfWIGRNaz/4+lS+CxvuKVZSKFz/6G8E3BKKBRs= 20 | github.com/jonas747/ogg v0.0.0-20161220051205-b4f6f4cf3757/go.mod h1:cZnNmdLiLpihzgIVqiaQppi9Ts3D4qF/M45//yW35nI= 21 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 22 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/rylio/ytdl v0.5.1 h1:79MZWKZUbT56m+2/wcAemfky15rJINV+AOoZZyUA3es= 26 | github.com/rylio/ytdl v0.5.1/go.mod h1:95YUr8z28/4SbAtOMw027cd07GG2yt2cONPpSE7rUH4= 27 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 28 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 29 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 30 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 31 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 33 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 34 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM= 37 | golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 38 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 39 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 40 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 45 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 46 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 48 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | l "log" 6 | "os" 7 | ) 8 | 9 | type logging struct { 10 | i *l.Logger 11 | e *l.Logger 12 | t *l.Logger 13 | } 14 | 15 | func newLog() logging { 16 | return logging{ 17 | l.New(os.Stdout, "INFO - ", l.Ldate|l.Ltime), 18 | l.New(os.Stdout, "ERROR - ", l.Ldate|l.Ltime), 19 | l.New(os.Stdout, "TRACE - ", l.Ldate|l.Ltime), 20 | } 21 | } 22 | 23 | func (l logging) Error(f string, s ...interface{}) { 24 | l.e.Print(f, " ", fmt.Sprintln(s...)) 25 | } 26 | 27 | func (l logging) Info(f string, s ...interface{}) { 28 | l.i.Print(f, " ", fmt.Sprintln(s...)) 29 | } 30 | 31 | func (l logging) Trace(f string, s ...interface{}) { 32 | l.t.Print(f, " ", fmt.Sprintln(s...)) 33 | } 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2017 Noah Santschi-Cooney 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published 6 | by the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "net/http" 23 | "os" 24 | "os/signal" 25 | "regexp" 26 | "runtime" 27 | "strconv" 28 | "syscall" 29 | "time" 30 | 31 | "github.com/bwmarrin/discordgo" 32 | "github.com/go-chi/chi" 33 | ) 34 | 35 | const ( 36 | happyEmoji string = "https://cdn.discordapp.com/emojis/332968429210435585.png" 37 | thinkEmoji string = "https://cdn.discordapp.com/emojis/333694872802426880.png" 38 | reviewChan string = "334092230845267988" 39 | logChan string = "312352242504040448" 40 | serverID string = "312292616089894924" 41 | xmark string = "<:xmark:314349398824058880>" 42 | zerowidth string = "​" 43 | ) 44 | 45 | var ( 46 | conf = new(config) 47 | dg *discordgo.Session 48 | lastReboot string 49 | log = newLog() 50 | emojiRegex = regexp.MustCompile("<(a)?:.*?:(.*?)>") 51 | userIDRegex = regexp.MustCompile("<@!?([0-9]+)>") 52 | channelRegex = regexp.MustCompile("<#([0-9]+)>") 53 | status = map[discordgo.Status]string{"dnd": "busy", "online": "online", "idle": "idle", "offline": "offline"} 54 | footer = new(discordgo.MessageEmbedFooter) 55 | ) 56 | 57 | func init() { 58 | footer.Text = "Created with ❤ by Strum355\nLast Bot reboot: " + time.Now().Format("Mon, 02-Jan-06 15:04:05 MST") 59 | } 60 | 61 | func dailyJobs() { 62 | for { 63 | postServerCount() 64 | time.Sleep(time.Hour * 24) 65 | } 66 | } 67 | 68 | func postServerCount() { 69 | url := "https://bots.discord.pw/api/bots/301819949683572738/stats" 70 | 71 | count := len(dg.State.Guilds) 72 | 73 | jsonStr := []byte(`{"server_count":` + strconv.Itoa(count) + `}`) 74 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr)) 75 | if err != nil { 76 | log.Error("error making bots.discord.pw request", err) 77 | return 78 | } 79 | 80 | req.Header.Set("Authorization", conf.DiscordPWKey) 81 | req.Header.Set("Content-Type", "application/json") 82 | 83 | resp, err := new(http.Client).Do(req) 84 | if err != nil { 85 | log.Error("bots.discord.pw error", err) 86 | return 87 | } 88 | defer resp.Body.Close() 89 | 90 | log.Info("POSTed " + strconv.Itoa(count) + " to bots.discord.pw") 91 | 92 | if resp.StatusCode != http.StatusNoContent { 93 | log.Error("received " + strconv.Itoa(resp.StatusCode) + " from bots.discord.pw") 94 | } 95 | 96 | } 97 | 98 | func setBotGame(s *discordgo.Session) { 99 | if err := s.UpdateStatus(0, conf.Game); err != nil { 100 | log.Error("Update status err:", err) 101 | return 102 | } 103 | log.Info("set initial game to", conf.Game) 104 | } 105 | 106 | // Set all handlers for queued images, in case the bot crashes with images still in queue 107 | func setQueuedImageHandlers() { 108 | for imgNum := range imageQueue { 109 | imgNumInt, err := strconv.Atoi(imgNum) 110 | if err != nil { 111 | log.Error("Error converting string to num for queue:", err) 112 | continue 113 | } 114 | go fimageReview(dg, imgNumInt) 115 | } 116 | } 117 | 118 | func main() { 119 | runtime.GOMAXPROCS(conf.MaxProc) 120 | 121 | log.Info("/*********BOT RESTARTING*********\\") 122 | 123 | names := []string{"config", "users", "servers", "queue"} 124 | for i, f := range []func() error{loadConfig, loadUsers, loadServers, loadQueue} { 125 | if err := f(); err != nil { 126 | switch i { 127 | case 0: 128 | os.Exit(404) 129 | default: 130 | } 131 | continue 132 | } 133 | log.Trace("loaded", names[i]) 134 | } 135 | defer cleanup() 136 | 137 | log.Info("files loaded") 138 | 139 | var err error 140 | dg, err = discordgo.New("Bot " + conf.Token) 141 | if err != nil { 142 | log.Error("Error creating Discord session,", err) 143 | return 144 | } 145 | 146 | log.Trace("session created") 147 | 148 | dg.AddHandler(messageCreateEvent) 149 | dg.AddHandler(presenceChangeEvent) 150 | dg.AddHandler(guildKickedEvent) 151 | dg.AddHandler(memberJoinEvent) 152 | dg.AddHandler(readyEvent) 153 | dg.AddHandler(guildJoinEvent) 154 | 155 | if err := dg.Open(); err != nil { 156 | log.Error("Error opening connection,", err) 157 | return 158 | } 159 | defer dg.Close() 160 | 161 | log.Trace("connection opened") 162 | 163 | sMap.Count = len(sMap.serverMap) 164 | 165 | go setQueuedImageHandlers() 166 | 167 | if !conf.InDev { 168 | go dailyJobs() 169 | } 170 | 171 | // Setup http server for selfbots 172 | router := chi.NewRouter() 173 | router.Get("/image/{id:[0-9]{18}}/recall/{img:[0-9a-z]{64}}", httpImageRecall) 174 | router.Get("/inServer/{id:[0-9]{18}}", isInServer) 175 | 176 | go func() { log.Error("error starting http server", http.ListenAndServe("0.0.0.0:8080", router)) }() 177 | 178 | log.Info("Bot is now running. Press CTRL-C to exit.") 179 | 180 | sc := make(chan os.Signal, 1) 181 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, syscall.SIGSEGV, syscall.SIGHUP) 182 | <-sc 183 | } 184 | -------------------------------------------------------------------------------- /msgAvatar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bwmarrin/discordgo" 5 | ) 6 | 7 | func init() { 8 | newCommand("avatar", 0, false, msgAvatar).setHelp("Args: [@user]\n\nReturns the given users avatar.\nIf no user ID is given, your own avatar is sent.\n\nExample:\n`!owo avatar @Strum355#2298`").add() 9 | } 10 | 11 | func msgAvatar(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 12 | if len(msglist) == 1 { 13 | getAvatar(m.Author.ID, m, s) 14 | return 15 | } 16 | 17 | if len(m.Mentions) != 0 { 18 | getAvatar(m.Mentions[0].ID, m, s) 19 | return 20 | } 21 | 22 | s.ChannelMessageSend(m.ChannelID, "User not found :(") 23 | } 24 | 25 | func getAvatar(userID string, m *discordgo.MessageCreate, s *discordgo.Session) { 26 | /* guild, err := guildDetails(m.ChannelID, "", s) 27 | if err != nil { 28 | s.ChannelMessageSend(m.ChannelID, "There was an error finding the user :( Please try again") 29 | return 30 | } */ 31 | 32 | // slow warmup or fast but limited to guild :( shame 33 | // or maybe not??? will monitor 34 | user, err := userDetails(userID, s) 35 | if err != nil { 36 | s.ChannelMessageSend(m.ChannelID, "There was an error finding the user :( Please try again") 37 | return 38 | } 39 | 40 | s.ChannelMessageSendEmbed(m.ChannelID, &discordgo.MessageEmbed{ 41 | Description: user.Username + "'s Avatar", 42 | 43 | Color: 0x000000, 44 | 45 | Image: &discordgo.MessageEmbedImage{ 46 | URL: user.AvatarURL("2048"), 47 | }, 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /msgEmoji.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | ) 13 | 14 | var ( 15 | errNotEmoji = errors.New("not an emoji") 16 | ) 17 | 18 | func init() { 19 | newCommand("bigMoji", 0, false, msgEmoji).setHelp("Args: [emoji]\n\nSends a large image of the given emoji.\n" + 20 | "Command 'bigMoji' can be excluded for shorthand.\n\nExample:\n`!owo :smile:`\nor\n`!owo bigMoji :smile:`").add() 21 | } 22 | 23 | // Thanks to iopred 24 | func emojiFile(s string) string { 25 | var found, filename string 26 | 27 | for _, r := range s { 28 | if filename != "" { 29 | filename = fmt.Sprintf("%s-%x", filename, r) 30 | } else { 31 | filename = fmt.Sprintf("%x", r) 32 | } 33 | 34 | if _, err := os.Stat(fmt.Sprintf("emoji/%s.png", filename)); err == nil { 35 | found = filename 36 | } else if found != "" { 37 | return found 38 | } 39 | } 40 | return found 41 | } 42 | 43 | func sendEmojiFromFile(s *discordgo.Session, m *discordgo.MessageCreate, e string) (file io.ReadCloser, err error) { 44 | emoji := emojiFile(e) 45 | if emoji == "" { 46 | return nil, errNotEmoji 47 | } 48 | 49 | return os.Open(fmt.Sprintf("emoji/%s.png", emoji)) 50 | } 51 | 52 | func msgEmoji(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 53 | if len(msglist) < 1 { 54 | return 55 | } 56 | 57 | var emoji string 58 | var emojiReader io.ReadCloser 59 | var err error 60 | 61 | filename := "emoji" 62 | 63 | if strings.ToLower(msglist[0]) == "bigmoji" { 64 | if len(msglist) < 2 { 65 | return 66 | } 67 | emoji = msglist[1] 68 | } else { 69 | emoji = msglist[0] 70 | } 71 | 72 | submatch := emojiRegex.FindStringSubmatch(emoji) 73 | 74 | if len(submatch) == 0 { 75 | filename += ".png" 76 | emojiReader, err = sendEmojiFromFile(s, m, emoji) 77 | if err != nil { 78 | if err != errNotEmoji { 79 | log.Error("error getting emoji from file", err) 80 | } 81 | goto errored 82 | } 83 | } else { 84 | var url string 85 | 86 | switch submatch[1] { 87 | case "": 88 | url = fmt.Sprintf("https://cdn.discordapp.com/emojis/%s.png", submatch[2]) 89 | filename += ".png" 90 | case "a": 91 | url = fmt.Sprintf("https://cdn.discordapp.com/emojis/%s.gif", submatch[2]) 92 | filename += ".gif" 93 | } 94 | 95 | resp, err := http.Get(url) 96 | if err != nil { 97 | log.Error("error getting emoji from URL", err) 98 | goto errored 99 | } 100 | 101 | emojiReader = resp.Body 102 | } 103 | defer emojiReader.Close() 104 | 105 | errored: 106 | if err != nil { 107 | if err == errNotEmoji { 108 | return 109 | } 110 | s.ChannelMessageSend(m.ChannelID, "There was an error getting the emoji :(") 111 | return 112 | } 113 | 114 | s.ChannelFileSend(m.ChannelID, filename, emojiReader) 115 | deleteMessage(m.Message, s) 116 | } 117 | -------------------------------------------------------------------------------- /msgEncode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "strings" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | "golang.org/x/crypto/bcrypt" 12 | ) 13 | 14 | func init() { 15 | newCommand("encode", 0, false, msgEncode).setHelp("Args: [base] [text]\n\nBases: `base64`, `bcrypt`, `md5`, `sh256`\nEncodes the given text in the given base.\n\nExample:\n`!owo encode md5 some text`").add() 16 | } 17 | 18 | func msgEncode(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 19 | if len(msglist) < 3 { 20 | return 21 | } 22 | 23 | base := strings.ToLower(msglist[1]) 24 | text := strings.Join(msglist[2:], " ") 25 | var output []byte 26 | 27 | s.ChannelTyping(m.ChannelID) 28 | 29 | switch base { 30 | case "base64": 31 | output = []byte(base64.StdEncoding.EncodeToString([]byte(text))) 32 | case "bcrypt": 33 | var err error 34 | if output, err = bcrypt.GenerateFromPassword([]byte(text), 14); err != nil { 35 | log.Error("bcrypt err", err) 36 | return 37 | } 38 | case "md5": 39 | hash := md5.Sum([]byte(text)) 40 | output = make([]byte, hex.EncodedLen(len(hash))) 41 | hex.Encode(output, hash[:]) 42 | case "sha256": 43 | hash := sha256.Sum256([]byte(text)) 44 | output = make([]byte, hex.EncodedLen(len(hash))) 45 | hex.Encode(output, hash[:]) 46 | default: 47 | s.ChannelMessageSend(m.ChannelID, "Base not supported") 48 | return 49 | } 50 | 51 | s.ChannelMessageSend(m.ChannelID, string(output)) 52 | } 53 | -------------------------------------------------------------------------------- /msgIbsearch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "time" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | ) 13 | 14 | type ibStruct struct { 15 | Path string `json:"path"` 16 | Server string `json:"server"` 17 | } 18 | 19 | func init() { 20 | newCommand("ibsearch", 0, false, msgIbsearch).setHelp("Args: [search] | rating=[e,s,q] | format=[gif,png,jpg]\n\n" + 21 | "Returns a random image from ibsearch for the given search term with the given filters applied.\n\n" + 22 | "Example:\n`!owo ibsearch lewds | rating=e | format=gif`") 23 | } 24 | 25 | func msgIbsearch(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 26 | if len(msglist) < 2 { 27 | return 28 | } 29 | 30 | channel, err := channelDetails(m.ChannelID, s) 31 | if err != nil { 32 | return 33 | } 34 | 35 | guild, err := guildDetails("", channel.GuildID, s) 36 | if err != nil { 37 | return 38 | } 39 | 40 | if val, ok := sMap.server(guild.ID); ok && !val.Nsfw && !strings.Contains(channel.Name, "nsfw") && !channel.NSFW { 41 | s.ChannelMessageSend(m.ChannelID, "NSFW is disabled on this server~") 42 | return 43 | } 44 | 45 | s.ChannelTyping(m.ChannelID) 46 | 47 | URL, err := url.Parse("https://ibsearch.xxx") 48 | if err != nil { 49 | log.Error("IBSearch query error", err) 50 | return 51 | } 52 | 53 | var queries []string 54 | 55 | queryList := strings.Split(strings.Join(remove(msglist, 0), " "), "|") 56 | 57 | for i, item := range queryList { 58 | if strings.Contains(item, "=") { 59 | queries = append(queries, strings.TrimSpace(strings.Split(queryList[i], "=")[0])) 60 | } 61 | } 62 | 63 | var finalQuery string 64 | 65 | filters := []string{"rating", "format"} 66 | 67 | for _, item1 := range filters { 68 | for i, item2 := range queries { 69 | if strings.Contains(item1, item2) { 70 | finalQuery += strings.Replace(queryList[i+1], " ", "", -1) + " " 71 | } 72 | } 73 | } 74 | 75 | //Assemble the URL 76 | var par url.Values 77 | URL.Path += "/api/v1/images.json" 78 | par.Add("q", strings.TrimSpace(queryList[0])+" "+finalQuery+"random:") 79 | par.Add("limit", "1") 80 | 81 | //Public key that is for free, worst that can happen is that 82 | //i hit the ratelimit, but please dont do that to me 83 | par.Add("key", "2480CFA681A7A882CB33C0E4BA00A812C6F906A6") 84 | URL.RawQuery = par.Encode() 85 | 86 | client := http.Client{Timeout: time.Second * 2} 87 | page, err := client.Get(URL.String()) 88 | if err != nil { 89 | log.Error("Ibsearch http error", err) 90 | s.ChannelMessageSend(m.ChannelID, "Error getting results from ibsearch") 91 | return 92 | } 93 | defer page.Body.Close() 94 | 95 | if page.StatusCode != http.StatusOK { 96 | s.ChannelMessageSend(m.ChannelID, "IBSearch didn't respond :(") 97 | return 98 | } 99 | 100 | var ibsearchStruct [1]ibStruct 101 | if err := json.NewDecoder(page.Body).Decode(&ibsearchStruct); err != nil { 102 | log.Error("IBSearch json unmarshal err", err) 103 | s.ChannelMessageSend(m.ChannelID, "No results ¯\\_(ツ)_/¯") 104 | return 105 | } 106 | 107 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("%s searched for `%s` \nhttps://%s.ibsearch.xxx/%s", m.Author.Username, queryList[0], ibsearchStruct[0].Server, ibsearchStruct[0].Path)) 108 | } 109 | -------------------------------------------------------------------------------- /msgImageRecall.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | 8 | "github.com/Necroforger/dgwidgets" 9 | "github.com/bwmarrin/discordgo" 10 | "github.com/go-chi/chi" 11 | 12 | "encoding/hex" 13 | "fmt" 14 | "io/ioutil" 15 | "net/url" 16 | "path" 17 | "strconv" 18 | "time" 19 | 20 | "golang.org/x/crypto/blake2b" 21 | ) 22 | 23 | var imageQueue = make(map[string]*queuedImage) 24 | 25 | func init() { 26 | newCommand("image", 0, false, msgImageRecall).setHelp("Args: [save,recall,delete,list,status] [name]\n\nSave images and recall them at anytime! Everyone gets 8MB of image storage. Any name counts so long theres no `/` in it." + 27 | "Only you can 'recall' your saved images. There's a review process to make sure nothing illegal is being uploaded but we're fairly relaxed for the most part\n\n" + 28 | "Example:\n`!owo image save 2B Happy`\n2Bot downloads the image and sends it off for reviewing\n\n" + 29 | "`!owo image recall 2B Happy`\nIf your image was confirmed, 2Bot will send the image named `2B Happy`\n\n" + 30 | "`!owo image delete 2B Happy`\nThis will delete the image you saved called `2B Happy`\n\n" + 31 | "`!owo image list`\nThis will list your saved images along with a preview!\n\n" + 32 | "`!owo image status`\nShows some details on your saved images and quota").add() 33 | } 34 | 35 | func msgImageRecall(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 36 | if len(msglist) < 2 { 37 | prefix, err := activePrefix(m.ChannelID, s) 38 | if err != nil { 39 | return 40 | } 41 | 42 | s.ChannelMessageSend(m.ChannelID, 43 | "Available sub-commands for `image`:\n`save`, `delete`, `recall`, `list`, `status`\n"+ 44 | "Type `"+prefix+"help image` to see more info about this command") 45 | return 46 | } 47 | 48 | switch msglist[1] { 49 | case "recall": 50 | fimageRecall(s, m, msglist[2:]) 51 | case "save": 52 | fimageSave(s, m, msglist[2:]) 53 | case "delete": 54 | fimageDelete(s, m, msglist[2:]) 55 | case "list": 56 | fimageList(s, m, nil) 57 | case "status": 58 | fimageInfo(s, m, nil) 59 | } 60 | } 61 | 62 | func httpImageRecall(w http.ResponseWriter, r *http.Request) { 63 | // 404 for user not found, 410 for image not found 64 | defer r.Body.Close() 65 | 66 | id := chi.URLParam(r, "id") 67 | img := chi.URLParam(r, "img") 68 | 69 | log.Info(fmt.Sprintf("image request from %s for %s", id, img)) 70 | 71 | if val, ok := u[id]; ok { 72 | for _, val := range val.Images { 73 | if strings.HasPrefix(val, img) { 74 | w.WriteHeader(http.StatusOK) 75 | log.Trace(fmt.Sprintf("user %s has image %s", id, img)) 76 | fmt.Fprint(w, conf.URL+val) 77 | return 78 | } 79 | } 80 | w.WriteHeader(http.StatusGone) 81 | log.Trace(fmt.Sprintf("user %s doesn't have image %s", id, img)) 82 | return 83 | } 84 | 85 | log.Trace(fmt.Sprintf("user %s not in map", id)) 86 | w.WriteHeader(http.StatusNotFound) 87 | } 88 | 89 | func fimageRecall(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 90 | var filename string 91 | if val, ok := u[m.Author.ID]; ok { 92 | if val, ok := val.Images[strings.Join(msglist, " ")]; ok { 93 | filename = val 94 | } else { 95 | s.ChannelMessageSend(m.ChannelID, "You dont have an image under that name saved with me <:2BThink:333694872802426880>") 96 | return 97 | } 98 | } else { 99 | s.ChannelMessageSend(m.ChannelID, "You've no saved images! Get storin'!") 100 | return 101 | } 102 | 103 | imgURL := conf.URL + url.PathEscape(filename) 104 | 105 | resp, err := http.Head(imgURL) 106 | if err != nil { 107 | log.Error("error recalling image", err) 108 | s.ChannelMessageSend(m.ChannelID, "Error getting the image :( Please pester my creator about this") 109 | return 110 | } else if resp.StatusCode != http.StatusOK { 111 | log.Error("non 200 status code " + imgURL) 112 | s.ChannelMessageSend(m.ChannelID, "Error getting the image :( Please pester my creator about this") 113 | return 114 | } 115 | 116 | s.ChannelMessageSendEmbed(m.ChannelID, &discordgo.MessageEmbed{ 117 | Description: strings.Join(msglist, " "), 118 | 119 | Color: 0x000000, 120 | 121 | Image: &discordgo.MessageEmbedImage{ 122 | URL: imgURL, 123 | }, 124 | }) 125 | 126 | return 127 | } 128 | 129 | func fimageSave(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 130 | conf.CurrImg++ 131 | saveConfig() 132 | 133 | currentImageNumber := conf.CurrImg 134 | 135 | if len(m.Attachments) == 0 { 136 | s.ChannelMessageSend(m.ChannelID, "No image sent. Please send me an image to save for you!") 137 | return 138 | } 139 | 140 | if len(msglist) < 1 { 141 | s.ChannelMessageSend(m.ChannelID, "Gotta name your image!") 142 | return 143 | } 144 | 145 | if m.Attachments[0].Height == 0 { 146 | s.ChannelMessageSend(m.ChannelID, "Either your image is corrupted or you didn't send me an image <:2BThink:333694872802426880> I can only save images for you~") 147 | return 148 | } 149 | 150 | imgName := strings.Join(msglist, " ") 151 | prefixedImgName := m.Author.ID + "_" + imgName 152 | 153 | fileExtension := strings.ToLower(path.Ext(m.Attachments[0].URL)) 154 | hash := blake2b.Sum256([]byte(prefixedImgName)) 155 | imgFileName := hex.EncodeToString(hash[:]) + fileExtension 156 | 157 | currUser, ok := u[m.Author.ID] 158 | if !ok { 159 | u[m.Author.ID] = &user{ 160 | Images: map[string]string{}, 161 | TempImages: []string{}, 162 | DiskQuota: 8000000, 163 | QueueSize: 0, 164 | } 165 | currUser = u[m.Author.ID] 166 | } 167 | 168 | _, ok = currUser.Images[imgName] 169 | //if named image is in queue or already saved, abort 170 | if isIn(imgName, currUser.TempImages) || ok { 171 | s.ChannelMessageSend(m.ChannelID, "You've already saved an image under that name! Delete it first~") 172 | return 173 | } 174 | 175 | fileSize := m.Attachments[0].Size 176 | 177 | //if the image + current used space > quota 178 | if fileSize+currUser.CurrDiskUsed > currUser.DiskQuota { 179 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("The image file size is too big by %.2fMB :(", 180 | float32(fileSize+currUser.CurrDiskUsed-currUser.DiskQuota)/1000/1000)) 181 | return 182 | } 183 | 184 | //if when the image is added to the queue, the queue size + current used space > quota 185 | if fileSize+currUser.QueueSize+currUser.CurrDiskUsed > currUser.DiskQuota { 186 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("The image file size is too big by %.2fMB :(\n"+ 187 | "Note, this only takes your queued (aka unconfirmed) images into account, so if one of them gets rejected, you can try adding this image again!", 188 | float32(fileSize+currUser.QueueSize+currUser.CurrDiskUsed-currUser.DiskQuota)/1000/1000)) 189 | return 190 | } 191 | 192 | dlMsg, _ := s.ChannelMessageSend(m.ChannelID, "<:update:264184209617321984> Downloading your image~") 193 | 194 | resp, err := http.Get(m.Attachments[0].URL) 195 | if err != nil || resp.StatusCode != http.StatusOK { 196 | s.ChannelMessageSend(m.ChannelID, "Error downloading the image :( Please pester creator about this") 197 | log.Error("error downloading image ", err) 198 | return 199 | } 200 | defer resp.Body.Close() 201 | 202 | guild, err := guildDetails(m.ChannelID, "", s) 203 | if err != nil { 204 | guild = &discordgo.Guild{ 205 | Name: "error", 206 | ID: "error", 207 | } 208 | } 209 | 210 | tempFilepath := "images/temp/" + imgFileName 211 | 212 | //create temp file in temp path 213 | tempFile, err := os.Create(tempFilepath) 214 | if err != nil { 215 | log.Error("error creating temp file", err) 216 | s.ChannelMessageSend(m.ChannelID, "There was an error saving the image :( Please pester my creator about this") 217 | return 218 | } 219 | defer tempFile.Close() 220 | 221 | bodyImg, err := ioutil.ReadAll(resp.Body) 222 | if err != nil { 223 | log.Error("error parsing body", err) 224 | return 225 | } 226 | 227 | if _, err = tempFile.Write(bodyImg); err != nil { 228 | log.Error("error writing image to file", err) 229 | return 230 | } 231 | 232 | _, err = s.ChannelMessageEdit(m.ChannelID, dlMsg.ID, m.Author.Mention()+" Thanks for the submission! "+ 233 | "Your image is being reviewed by our ~~lazy~~ hard-working review team! You'll get a PM from either my master himself or from me once its been confirmed or rejected :) Sit tight!") 234 | if err != nil { 235 | s.ChannelMessageSend(m.ChannelID, m.Author.Mention()+" Thanks for the submission! "+ 236 | "Your image is being reviewed by our ~~lazy~~ hard-working review team! You'll get a PM from either my master himself or from me once its been confirmed or rejected :) Sit tight!") 237 | deleteMessage(dlMsg, s) 238 | } 239 | 240 | reviewMsg, _ := s.ChannelMessageSendEmbed(reviewChan, &discordgo.MessageEmbed{ 241 | Description: fmt.Sprintf("Image ID: %d\nNew image from:\n`%s#%s` ID: %s\nfrom server `%s` `%s`\nnamed `%s`", 242 | currentImageNumber, 243 | m.Author.Username, 244 | m.Author.Discriminator, 245 | m.Author.ID, 246 | guild.Name, 247 | guild.ID, 248 | imgName), 249 | 250 | Color: 0x000000, 251 | 252 | Image: &discordgo.MessageEmbedImage{ 253 | URL: m.Attachments[0].URL, 254 | }, 255 | }) 256 | 257 | err = s.MessageReactionAdd(reviewMsg.ChannelID, reviewMsg.ID, "✅") 258 | if err != nil { 259 | s.ChannelMessageSend(reviewChan, "Couldn't add ✅ to message") 260 | log.Error("error attaching reaction", err) 261 | } 262 | err = s.MessageReactionAdd(reviewMsg.ChannelID, reviewMsg.ID, "❌") 263 | if err != nil { 264 | s.ChannelMessageSend(reviewChan, "Couldn't add ❌ to message") 265 | log.Error("error attaching reaction", err) 266 | } 267 | 268 | currUser.TempImages = append(currUser.TempImages, imgName) 269 | currUser.QueueSize += fileSize 270 | 271 | imageQueue[strconv.Itoa(currentImageNumber)] = &queuedImage{ 272 | ReviewMsgID: reviewMsg.ID, 273 | AuthorID: m.Author.ID, 274 | AuthorDiscrim: m.Author.Discriminator, 275 | AuthorName: m.Author.Username, 276 | ImageName: imgName, 277 | ImageURL: m.Attachments[0].ProxyURL, 278 | FileSize: fileSize, 279 | } 280 | 281 | saveQueue() 282 | saveUsers() 283 | 284 | fimageReview(s, currentImageNumber) 285 | } 286 | 287 | func fimageReview(s *discordgo.Session, currentImageNumber int) { 288 | imgInQueue := imageQueue[strconv.Itoa(currentImageNumber)] 289 | 290 | fileSize := imgInQueue.FileSize 291 | prefixedImgName := imgInQueue.AuthorID + "_" + imgInQueue.ImageName 292 | 293 | fileExtension := strings.ToLower(path.Ext(imgInQueue.ImageURL)) 294 | hash := blake2b.Sum256([]byte(prefixedImgName)) 295 | imgFileName := hex.EncodeToString(hash[:]) + fileExtension 296 | tempFilepath := "images/temp/" + imgFileName 297 | currUser := u[imgInQueue.AuthorID] 298 | 299 | //Wait here for a relevant reaction to the confirmation message 300 | for { 301 | confirm := <-nextReactionAdd(s) 302 | if confirm.UserID == s.State.User.ID || confirm.MessageID != imgInQueue.ReviewMsgID { 303 | continue 304 | } 305 | 306 | user, err := userDetails(confirm.UserID, s) 307 | if err != nil { 308 | s.ChannelMessageSend(reviewChan, "Error getting user for image confirming") 309 | continue 310 | } 311 | 312 | if confirm.MessageReaction.Emoji.Name == "✅" { 313 | //IF CONFIRMED 314 | s.ChannelMessageSend(reviewChan, fmt.Sprintf("%s confirmed image `%s` from `%s#%s` ID: `%s`", 315 | func() string { 316 | if user != nil { 317 | return user.Username 318 | } 319 | return confirm.UserID 320 | }(), 321 | imgInQueue.ImageName, 322 | imgInQueue.AuthorName, 323 | imgInQueue.AuthorDiscrim, 324 | imgInQueue.AuthorID)) 325 | 326 | break 327 | } else if confirm.MessageReaction.Emoji.Name == "❌" { 328 | //IF REJECTED 329 | s.ChannelMessageSend(reviewChan, fmt.Sprintf("%s rejected image `%s` from `%s#%s` ID: `%s`\nGive a reason next! Enter `None` to give no reason", 330 | func() string { 331 | if user != nil { 332 | return user.Username 333 | } 334 | return confirm.UserID 335 | }(), 336 | imgInQueue.ImageName, 337 | imgInQueue.AuthorName, 338 | imgInQueue.AuthorDiscrim, 339 | imgInQueue.AuthorID)) 340 | 341 | var reason string 342 | for { 343 | rejectMsg := <-nextMessageCreate(s) 344 | if rejectMsg.Author.ID == confirm.UserID { 345 | rejectMsgList := strings.Fields(rejectMsg.Content) 346 | if len(rejectMsgList) < 1 || rejectMsgList[0] != strconv.Itoa(currentImageNumber) { 347 | continue 348 | } 349 | 350 | if strings.Join(rejectMsgList[1:], " ") != "None" { 351 | reason = "Reason: " + strings.Join(rejectMsgList[1:], " ") 352 | } 353 | 354 | currUser.TempImages = remove(currUser.TempImages, findIndex(currUser.TempImages, imgInQueue.ImageName)) 355 | currUser.QueueSize -= fileSize 356 | delete(imageQueue, strconv.Itoa(currentImageNumber)) 357 | 358 | saveUsers() 359 | saveQueue() 360 | 361 | if err := os.Remove(tempFilepath); err != nil { 362 | log.Error("error deleting temp image", err) 363 | s.ChannelMessageSend(reviewChan, "Error deleting temp image") 364 | } 365 | 366 | //Make PM channel to inform user that image was rejected 367 | channel, err := s.UserChannelCreate(imgInQueue.AuthorID) 368 | //Couldnt make PM channel 369 | if err != nil { 370 | s.ChannelMessageSend(reviewChan, fmt.Sprintf("Couldn't inform %s#%s ID: %s about rejection\n%s", imgInQueue.AuthorName, imgInQueue.AuthorDiscrim, imgInQueue.AuthorID, err)) 371 | return 372 | } 373 | 374 | //Try PMing 375 | if _, err = s.ChannelMessageSend(channel.ID, "Your image got rejected :( Sorry\n"+reason); err != nil { 376 | s.ChannelMessageSend(reviewChan, fmt.Sprintf("Couldn't inform %s#%s ID: %s about rejection\n%s", imgInQueue.AuthorName, imgInQueue.AuthorDiscrim, imgInQueue.AuthorID, err)) 377 | } 378 | 379 | s.ChannelMessageSend(reviewChan, fmt.Sprintf("Reason for image `%s` from `%s#%s` ID: `%s`\n%s", 380 | imgInQueue.ImageName, 381 | imgInQueue.AuthorName, 382 | imgInQueue.AuthorDiscrim, 383 | imgInQueue.AuthorID, 384 | reason)) 385 | return 386 | } 387 | } 388 | } 389 | } 390 | 391 | //If image has been reviewed and confirmed 392 | channel, err := s.UserChannelCreate(imgInQueue.AuthorID) 393 | if err != nil { 394 | s.ChannelMessageSend(reviewChan, fmt.Sprintf("Couldn't inform %s#%s ID: %s about confirmation\n%s", imgInQueue.AuthorName, imgInQueue.AuthorDiscrim, imgInQueue.AuthorID, err)) 395 | } 396 | 397 | filepath := "images/" + imgFileName 398 | 399 | if err := os.Rename(tempFilepath, filepath); err != nil { 400 | s.ChannelMessageSend(reviewChan, "Error moving file from temp dir") 401 | log.Error("error moving file from temp dir", err) 402 | } else { 403 | if err := os.Chmod(filepath, 0755); err != nil { 404 | s.ChannelMessageSend(reviewChan, "Can't chmod "+err.Error()) 405 | log.Error("cant chmod", err) 406 | } 407 | } 408 | 409 | delete(imageQueue, strconv.Itoa(currentImageNumber)) 410 | currUser.TempImages = remove(currUser.TempImages, findIndex(currUser.TempImages, imgInQueue.ImageName)) 411 | currUser.CurrDiskUsed += fileSize 412 | currUser.QueueSize -= fileSize 413 | currUser.Images[imgInQueue.ImageName] = imgFileName 414 | 415 | saveQueue() 416 | saveUsers() 417 | 418 | s.ChannelMessageSend(channel.ID, "Your image was confirmed and is now saved :D To \"recall\" it, type `[prefix] image recall "+imgInQueue.ImageName+"`") 419 | } 420 | 421 | func fimageDelete(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 422 | var filename string 423 | val, ok := u[m.Author.ID] 424 | if ok { 425 | if val, ok := val.Images[strings.Join(msglist, " ")]; ok { 426 | filename = val 427 | } else { 428 | s.ChannelMessageSend(m.ChannelID, "You dont have an image under that name saved with me <:2BThink:333694872802426880>") 429 | return 430 | } 431 | } else { 432 | s.ChannelMessageSend(m.ChannelID, "You've no saved images! Get storin'!") 433 | return 434 | } 435 | 436 | f, err := os.Open("images/" + filename) 437 | if err != nil { 438 | s.ChannelMessageSend(m.ChannelID, "Image couldnt be deleted :( Please pester my creator for me") 439 | log.Error("error opening image for file size", err) 440 | return 441 | } 442 | 443 | stats, err := f.Stat() 444 | if err != nil { 445 | s.ChannelMessageSend(m.ChannelID, "Image couldnt be deleted :( Please pester my creator for me") 446 | log.Error("error getting file stats", err) 447 | return 448 | } 449 | 450 | if err := os.Remove("images/" + filename); err != nil { 451 | s.ChannelMessageSend(m.ChannelID, "Image couldnt be deleted :( Please pester my creator for me") 452 | log.Error("error deleting image", err) 453 | return 454 | } 455 | 456 | val.CurrDiskUsed -= int(stats.Size()) 457 | 458 | delete(val.Images, strings.Join(msglist, " ")) 459 | 460 | saveUsers() 461 | 462 | s.ChannelMessageSend(m.ChannelID, "Image deleted~") 463 | } 464 | 465 | func fimageList(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 466 | val, ok := u[m.Author.ID] 467 | if (ok && len(u[m.Author.ID].Images) == 0) || !ok { 468 | s.ChannelMessageSend(m.ChannelID, "You've no saved images! Get storin'!") 469 | return 470 | } 471 | 472 | var out []string 473 | var files []string 474 | for key, value := range val.Images { 475 | out = append(out, key) 476 | files = append(files, value) 477 | } 478 | 479 | msg, err := s.ChannelMessageSend(m.ChannelID, "Assemblin' a preview your images!") 480 | 481 | p := dgwidgets.NewPaginator(s, m.ChannelID) 482 | 483 | success := true 484 | for i, img := range files { 485 | imgURL, err := url.Parse(conf.URL + url.PathEscape(img)) 486 | if err != nil { 487 | log.Error("error parsing img url", err) 488 | success = false 489 | continue 490 | } 491 | p.Add(&discordgo.MessageEmbed{ 492 | Description: out[i], 493 | Image: &discordgo.MessageEmbedImage{ 494 | URL: imgURL.String(), 495 | }, 496 | }) 497 | } 498 | 499 | p.SetPageFooters() 500 | p.Loop = true 501 | p.ColourWhenDone = 0xff0000 502 | p.DeleteReactionsWhenDone = true 503 | p.Widget.Timeout = time.Minute * 2 504 | 505 | if err != nil && msg != nil { 506 | s.ChannelMessageEdit(m.ChannelID, msg.ID, "Your saved images are: `"+strings.Join(out, ", ")) 507 | } 508 | 509 | if err := p.Spawn(); err != nil { 510 | log.Error("error creating image list", err) 511 | s.ChannelMessageSend(m.ChannelID, "Couldn't make the list :( Go pester Strum355#1180 about this") 512 | return 513 | } 514 | 515 | if !success { 516 | s.ChannelMessageSend(m.ChannelID, "I couldn't assemble all of your images, but here are the ones i could get!") 517 | } 518 | } 519 | 520 | func fimageInfo(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 521 | if val, ok := u[m.Author.ID]; ok { 522 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("```autohotkey\nTotal Images:%21d```"+ 523 | "```autohotkey\nTotal Space Used:%20.2f/%.2fMB (%.2f/%.2fKB)```"+ 524 | "```autohotkey\nQueued Images:%20d```"+ 525 | "```autohotkey\nQueued Disk Space:%19.2f/%.2fMB (%.2f/%.2fKB)```"+ 526 | "```autohotkey\nFree Space:%26.2fMB (%.2fKB)```", 527 | len(val.Images), 528 | float32(val.CurrDiskUsed)/1000/1000, 529 | float32(val.DiskQuota)/1000/1000, 530 | float32(val.CurrDiskUsed)/1000, 531 | float32(val.DiskQuota)/1000, 532 | len(val.TempImages), 533 | float32(val.QueueSize)/1000/1000, 534 | float32(val.DiskQuota)/1000/1000, 535 | float32(val.QueueSize)/1000, 536 | float32(val.DiskQuota)/1000, 537 | float32(val.DiskQuota-(val.QueueSize+val.CurrDiskUsed))/1000/1000, 538 | float32(val.DiskQuota-(val.QueueSize+val.CurrDiskUsed))/1000)) 539 | 540 | return 541 | } 542 | 543 | u[m.Author.ID] = &user{ 544 | Images: map[string]string{}, 545 | TempImages: []string{}, 546 | DiskQuota: 8000000, 547 | QueueSize: 0, 548 | } 549 | 550 | saveUsers() 551 | 552 | fimageInfo(s, m, msglist) 553 | } 554 | -------------------------------------------------------------------------------- /msgLogging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bwmarrin/discordgo" 7 | ) 8 | 9 | func init() { 10 | newCommand("logging", 11 | discordgo.PermissionAdministrator|discordgo.PermissionManageServer, 12 | true, msgLogging).setHelp("Args: none\n\nToggles user presence logging.\n\nExample:\n`!owo logging`").add() 13 | newCommand("logChannel", 14 | discordgo.PermissionAdministrator|discordgo.PermissionManageServer, 15 | true, msgLogChannel).setHelp("Args: [channelID,channel tag]\n\nSets the log channel to the given channel.\nAdmin only.\n\nExample:\n`!owo logChannel 312292616089894924`\n`!owo logChannel #bot-channel`").add() 16 | } 17 | 18 | func msgLogChannel(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 19 | guild, err := guildDetails(m.ChannelID, "", s) 20 | if err != nil { 21 | s.ChannelMessageSend(m.ChannelID, "There was a problem setting the details :( Try again please~") 22 | return 23 | } 24 | 25 | if len(msglist) < 2 { 26 | return 27 | } 28 | 29 | var channelID string 30 | channelIDMatch := channelRegex.FindStringSubmatch(msglist[1]) 31 | if len(channelIDMatch) != 2 { 32 | s.ChannelMessageSend(m.ChannelID, "Not a valid channel!") 33 | return 34 | } 35 | channelID = channelIDMatch[1] 36 | 37 | var chanList []string 38 | for _, channel := range guild.Channels { 39 | chanList = append(chanList, channel.ID) 40 | } 41 | 42 | if !isIn(channelID, chanList) { 43 | s.ChannelMessageSend(m.ChannelID, "That channel isn't in this server <:2BThink:333694872802426880>") 44 | return 45 | } 46 | 47 | if guild, ok := sMap.server(guild.ID); ok && !guild.Kicked { 48 | guild.LogChannel = channelID 49 | saveServers() 50 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Log channel changed to <#%s>", channelID)) 51 | } 52 | return 53 | } 54 | 55 | func msgLogging(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 56 | guild, err := guildDetails(m.ChannelID, "", s) 57 | if err != nil { 58 | s.ChannelMessageSend(m.ChannelID, "There was a problem toggling logging :( Try again please~") 59 | return 60 | } 61 | 62 | if guild, ok := sMap.server(guild.ID); ok && !guild.Kicked { 63 | guild.Log = !guild.Log 64 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Logging %t", guild.Log)) 65 | saveServers() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /msgPlaylist.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | "github.com/rylio/ytdl" 9 | ) 10 | 11 | func init() { 12 | newCommand("playlist", 0, false, msgPlaylist).setHelp("dab on em").add() //TODO 13 | } 14 | 15 | func msgPlaylist(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 16 | if len(msglist) < 2 { 17 | return 18 | } 19 | 20 | guild, err := guildDetails(m.ChannelID, "", s) 21 | if err != nil { 22 | return 23 | } 24 | 25 | server, ok := sMap.server(guild.ID) 26 | if !ok { 27 | return 28 | } 29 | 30 | if server.Playlists == nil { 31 | server.Playlists = make(map[string][]song) 32 | } 33 | 34 | switch msglist[1] { 35 | case "create": 36 | createPlaylist(s, m, msglist, server) 37 | case "delete": 38 | deletePlaylist(s, m, msglist, server) 39 | case "add": 40 | addToPlaylist(s, m, msglist, server) 41 | case "remove": 42 | removeFromPlaylist(s, m, msglist, server) 43 | } 44 | 45 | saveServers() 46 | } 47 | 48 | func createPlaylist(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string, server *server) { 49 | playlist := strings.Join(msglist[2:], " ") 50 | if _, ok := server.Playlists[playlist]; ok { 51 | s.ChannelMessageSend(m.ChannelID, "Playlist `"+playlist+"` already exists!") 52 | return 53 | } 54 | 55 | server.Playlists[playlist] = []song{} 56 | s.ChannelMessageSend(m.ChannelID, "Created playlist `"+playlist+"`") 57 | return 58 | } 59 | 60 | func deletePlaylist(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string, server *server) { 61 | playlist := strings.Join(msglist[2:], " ") 62 | if _, ok := server.Playlists[playlist]; !ok { 63 | s.ChannelMessageSend(m.ChannelID, "Playlist `"+playlist+"` doesn't exist!") 64 | return 65 | } 66 | delete(server.Playlists, playlist) 67 | s.ChannelMessageSend(m.ChannelID, "Playlist `"+playlist+"` was deleted") 68 | } 69 | 70 | func addToPlaylist(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string, server *server) { 71 | if len(msglist) < 3 { 72 | return 73 | } 74 | 75 | playlist := strings.Join(msglist[4:], " ") 76 | url := msglist[3] 77 | if !strings.HasPrefix(url, stdURL) && !strings.HasPrefix(url, shortURL) && !strings.HasPrefix(url, embedURL) { 78 | s.ChannelMessageSend(m.ChannelID, "Please make sure the URL is a valid YouTube URL. If I got this wrong, please let my creator know~") 79 | return 80 | } 81 | 82 | if _, ok := server.Playlists[playlist]; !ok { 83 | s.ChannelMessageSend(m.ChannelID, "Playlist `"+playlist+"` doesn't exist!") 84 | return 85 | } 86 | 87 | for _, song := range server.Playlists[playlist] { 88 | if song.URL == url { 89 | s.ChannelMessageSend(m.ChannelID, "That song is already in the playlist!") 90 | return 91 | } 92 | } 93 | 94 | vid, err := ytdl.GetVideoInfo(url) 95 | if err != nil { 96 | log.Error("error getting YouTube video info", err) 97 | s.ChannelMessageSend(m.ChannelID, "There was an error adding the song to the playlist :( Check the command and try again") 98 | return 99 | } 100 | 101 | format := vid.Formats.Extremes(ytdl.FormatAudioBitrateKey, true)[0] 102 | if _, err = vid.GetDownloadURL(format); err != nil { 103 | log.Error("error getting download URL", err) 104 | s.ChannelMessageSend(m.ChannelID, "There was an error adding the song to the playlist :( Check the command and try again") 105 | return 106 | } 107 | 108 | server.Playlists[playlist] = append(server.Playlists[playlist], song{ 109 | URL: url, 110 | Name: vid.Title, 111 | Duration: vid.Duration, 112 | }) 113 | 114 | s.ChannelMessageSend(m.ChannelID, vid.Title+" added to playlist `"+playlist+"`") 115 | } 116 | 117 | func removeFromPlaylist(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string, server *server) { 118 | index, err := strconv.Atoi(msglist[2]) 119 | if err != nil { 120 | s.ChannelMessageSend(m.ChannelID, "Please give the index of the song to delete~") 121 | return 122 | } 123 | 124 | server.Playlists[msglist[1]] = append(server.Playlists[msglist[1]][:index], server.Playlists[msglist[1]][index+1:]...) 125 | s.ChannelMessageSend(m.ChannelID, "Song removed from `"+msglist[1]+"`") 126 | } 127 | -------------------------------------------------------------------------------- /msgPrefix.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | ) 9 | 10 | func init() { 11 | newCommand("setGlobalPrefix", 0, false, msgGlobalPrefix).ownerOnly().add() 12 | newCommand("setPrefix", 13 | discordgo.PermissionAdministrator|discordgo.PermissionManageServer, 14 | true, msgPrefix).setHelp("Args: [prefix]\n\nSets the servers prefix to 'prefix'\nAdmin only.\n\nExample:\n`!owo setPrefix .`\nNew Example command:\n`.help`").add() 15 | } 16 | 17 | func prefixWorker(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) (prefix string, ok bool) { 18 | prefix = strings.Join(msglist[1:], " ") 19 | 20 | for { 21 | next := <-nextMessageCreate(s) 22 | if next.ChannelID != m.ChannelID || next.Author.ID != m.Author.ID { 23 | continue 24 | } 25 | 26 | response := strings.ToLower(next.Content) 27 | if response != "yes" && response != "no" { 28 | s.ChannelMessageSend(m.ChannelID, "Invalid response. Command cancelled.") 29 | return 30 | } 31 | 32 | f := func() string { 33 | if response == "yes" { 34 | return " " 35 | } 36 | return "" 37 | } 38 | 39 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Prefix changed to %s %s a trailing space", codeSeg(prefix), func() string { 40 | if f() == "" { 41 | return "without" 42 | } 43 | return "with" 44 | }())) 45 | 46 | return prefix + f(), true 47 | } 48 | } 49 | 50 | func msgPrefix(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 51 | if len(msglist) < 2 { 52 | s.ChannelMessageSend(m.ChannelID, "No prefix given :/") 53 | return 54 | } 55 | 56 | guildDetails, err := guildDetails(m.ChannelID, "", s) 57 | if err != nil { 58 | s.ChannelMessageSend(m.ChannelID, "There was a problem changing the prefix :( Try again please~") 59 | return 60 | } 61 | 62 | guild, ok := sMap.server(guildDetails.ID) 63 | if !ok || guild.Kicked { 64 | return 65 | } 66 | 67 | prefix := strings.Join(msglist[1:], " ") 68 | 69 | s.ChannelMessageSend(m.ChannelID, "Do you want trailing space? (yes/no)"+ 70 | "```"+ 71 | prefix+" help -> with trailing space\n"+ 72 | prefix+"help -> without trailing space```") 73 | 74 | if prefix, ok = prefixWorker(s, m, msglist); ok { 75 | guild.Prefix = prefix 76 | saveServers() 77 | } 78 | } 79 | 80 | func msgGlobalPrefix(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 81 | if len(msglist) < 2 { 82 | return 83 | } 84 | 85 | if prefix, ok := prefixWorker(s, m, msglist); ok { 86 | conf.Prefix = prefix 87 | saveConfig() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /msgPurge.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/bwmarrin/discordgo" 10 | ) 11 | 12 | func init() { 13 | newCommand("purge", 14 | discordgo.PermissionAdministrator|discordgo.PermissionManageMessages|discordgo.PermissionManageServer, 15 | true, msgPurge).setHelp("Args: [number] [@user]\n\nPurges 'number' amount of messages. Optionally, purge only the messages from a given user!\nAdmin only\n\nExample:\n`!owo purge 300`\n" + 16 | "Example 2:\n`!owo purge 300 @Strum355#1180`").add() 17 | } 18 | 19 | func msgPurge(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 20 | if len(msglist) < 2 { 21 | s.ChannelMessageSend(m.ChannelID, "Gotta specify a number of messages to delete~") 22 | return 23 | } 24 | 25 | purgeAmount, err := strconv.Atoi(msglist[1]) 26 | if err != nil { 27 | if strings.HasPrefix(msglist[1], "@") { 28 | msglist[1] = "@" + zerowidth + strings.TrimPrefix(msglist[1], "@") 29 | } 30 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("How do i delete %s messages? Please only give numbers!", msglist[1])) 31 | return 32 | } 33 | 34 | var userToPurge string 35 | if len(msglist) == 3 { 36 | submatch := userIDRegex.FindStringSubmatch(msglist[2]) 37 | if len(submatch) == 0 { 38 | s.ChannelMessageSend(m.ChannelID, "Couldn't find that user :(") 39 | return 40 | } 41 | userToPurge = submatch[1] 42 | } 43 | 44 | deleteMessage(m.Message, s) 45 | 46 | if userToPurge == "" { 47 | err = standardPurge(purgeAmount, s, m) 48 | } else { 49 | err = userPurge(purgeAmount, s, m, userToPurge) 50 | } 51 | 52 | if err == nil { 53 | msg, _ := s.ChannelMessageSend(m.ChannelID, "Successfully deleted :ok_hand:") 54 | time.Sleep(time.Second * 5) 55 | deleteMessage(msg, s) 56 | } 57 | } 58 | 59 | func getMessages(amount int, id string, s *discordgo.Session) (list []*discordgo.Message, err error) { 60 | list, err = s.ChannelMessages(id, amount, "", "", "") 61 | if err != nil { 62 | log.Error("error getting messages to delete", err) 63 | } 64 | return 65 | } 66 | 67 | func standardPurge(purgeAmount int, s *discordgo.Session, m *discordgo.MessageCreate) error { 68 | var outOfDate bool 69 | for purgeAmount > 0 { 70 | list, err := getMessages(purgeAmount%100, m.ChannelID, s) 71 | if err != nil { 72 | s.ChannelMessageSend(m.ChannelID, "There was an issue deleting messages :(") 73 | return err 74 | } 75 | 76 | //if more was requested to be deleted than exists 77 | if len(list) == 0 { 78 | break 79 | } 80 | 81 | var purgeList []string 82 | for _, msg := range list { 83 | timeSince, err := getMessageAge(msg, s, m) 84 | if err != nil { 85 | //if the time is malformed for whatever reason, we'll try the next message 86 | continue 87 | } 88 | 89 | if timeSince.Hours()/24 >= 14 { 90 | outOfDate = true 91 | break 92 | } 93 | 94 | purgeList = append(purgeList, msg.ID) 95 | } 96 | 97 | if err := massDelete(purgeList, s, m); err != nil { 98 | return err 99 | } 100 | 101 | if outOfDate { 102 | break 103 | } 104 | 105 | purgeAmount -= len(purgeList) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func userPurge(purgeAmount int, s *discordgo.Session, m *discordgo.MessageCreate, userToPurge string) error { 112 | var outOfDate bool 113 | for purgeAmount > 0 { 114 | del := purgeAmount % 100 115 | var purgeList []string 116 | 117 | for len(purgeList) < del { 118 | list, err := getMessages(100, m.ChannelID, s) 119 | if err != nil { 120 | s.ChannelMessageSend(m.ChannelID, "There was an issue deleting messages :(") 121 | return err 122 | } 123 | 124 | //if more was requested to be deleted than exists 125 | if len(list) == 0 { 126 | break 127 | } 128 | 129 | for _, msg := range list { 130 | if len(purgeList) == del { 131 | break 132 | } 133 | 134 | if msg.Author.ID != userToPurge { 135 | continue 136 | } 137 | 138 | timeSince, err := getMessageAge(msg, s, m) 139 | if err != nil { 140 | //if the time is malformed for whatever reason, we'll try the next message 141 | continue 142 | } 143 | 144 | if timeSince.Hours()/24 >= 14 { 145 | outOfDate = true 146 | break 147 | } 148 | 149 | purgeList = append(purgeList, msg.ID) 150 | } 151 | 152 | if outOfDate { 153 | break 154 | } 155 | } 156 | 157 | if err := massDelete(purgeList, s, m); err != nil { 158 | return err 159 | } 160 | 161 | if outOfDate { 162 | break 163 | } 164 | 165 | purgeAmount -= len(purgeList) 166 | } 167 | 168 | return nil 169 | } 170 | 171 | func massDelete(list []string, s *discordgo.Session, m *discordgo.MessageCreate) (err error) { 172 | if err = s.ChannelMessagesBulkDelete(m.ChannelID, list); err != nil { 173 | s.ChannelMessageSend(m.ChannelID, "There was an issue deleting messages :(") 174 | log.Error("error purging", err) 175 | } 176 | return 177 | } 178 | 179 | func getMessageAge(msg *discordgo.Message, s *discordgo.Session, m *discordgo.MessageCreate) (time.Duration, error) { 180 | then, err := msg.Timestamp.Parse() 181 | if err != nil { 182 | log.Error("error parsing time", err) 183 | return time.Duration(0), err 184 | } 185 | return time.Since(then), nil 186 | } 187 | -------------------------------------------------------------------------------- /msgRule34.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/bwmarrin/discordgo" 10 | ) 11 | 12 | type rule34 struct { 13 | PostCount int `xml:"count,attr"` 14 | 15 | Posts []struct { 16 | URL string `xml:"file_url,attr"` 17 | } `xml:"post"` 18 | } 19 | 20 | func init() { 21 | newCommand("r34", 0, false, msgRule34).setHelp("Args: [search]\n\nReturns a random image from rule34 for the given search term.\n\nExample:\n`!owo r34 lewds`").add() 22 | } 23 | 24 | func msgRule34(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 25 | if len(msglist) < 2 { 26 | return 27 | } 28 | 29 | channel, err := channelDetails(m.ChannelID, s) 30 | if err != nil { 31 | s.ChannelMessageSend(m.ChannelID, "There was a problem getting some details :( Please try again!") 32 | return 33 | } 34 | 35 | guild, err := guildDetails("", channel.GuildID, s) 36 | if err != nil { 37 | s.ChannelMessageSend(m.ChannelID, "There was a problem getting some details :( Please try again!") 38 | return 39 | } 40 | 41 | if val, ok := sMap.server(guild.ID); ok && !val.Nsfw && (!strings.HasPrefix(channel.Name, "nsfw") && !channel.NSFW) { 42 | s.ChannelMessageSend(m.ChannelID, "NSFW is disabled on this server~") 43 | return 44 | } 45 | 46 | var r34 rule34 47 | var query string 48 | 49 | s.ChannelTyping(m.ChannelID) 50 | 51 | for _, word := range msglist[1:] { 52 | query += "+" + word 53 | } 54 | 55 | url := fmt.Sprintf("https://rule34.xxx/index.php?page=dapi&s=post&q=index&tags=%s", query) 56 | page, err := http.Get(url) 57 | if err != nil { 58 | s.ChannelMessageSend(m.ChannelID, "error getting data from Rule34 :(") 59 | log.Error("error from r34", err) 60 | return 61 | } 62 | defer page.Body.Close() 63 | 64 | if page.StatusCode != http.StatusOK { 65 | s.ChannelMessageSend(m.ChannelID, "Rule34 didn't respond :(") 66 | log.Error("non 200 status code", url) 67 | return 68 | } 69 | 70 | if err = xml.NewDecoder(page.Body).Decode(&r34); err != nil { 71 | log.Error("error unmarshalling xml", err) 72 | return 73 | } 74 | 75 | if r34.PostCount == 0 { 76 | s.ChannelMessageSend(m.ChannelID, "No results ¯\\_(ツ)_/¯") 77 | return 78 | } 79 | 80 | url = r34.Posts[randRange(0, len(r34.Posts)-1)].URL 81 | 82 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("%s searched for `%s` \n%s", m.Author.Username, strings.Replace(query, "+", " ", -1), url)) 83 | } 84 | -------------------------------------------------------------------------------- /msgUserStats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | ) 10 | 11 | func init() { 12 | newCommand("whois", 0, false, msgUserStats).setHelp("Args: [@user]\n\nSome info about the given user.\n\nExample:\n`!owo whois @Strum355#2298`").add() 13 | } 14 | 15 | func msgUserStats(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 16 | channel, err := channelDetails(m.ChannelID, s) 17 | if err != nil { 18 | s.ChannelMessageSend(m.ChannelID, "There was an error getting the data :(") 19 | return 20 | } 21 | 22 | guild, err := guildDetails("", channel.GuildID, s) 23 | if err != nil { 24 | s.ChannelMessageSend(m.ChannelID, "There was an error getting the data :(") 25 | return 26 | } 27 | 28 | var userID string 29 | var nick string 30 | 31 | if len(msglist) > 1 { 32 | submatch := userIDRegex.FindStringSubmatch(msglist[1]) 33 | if len(submatch) != 0 { 34 | userID = submatch[1] 35 | } 36 | } else { 37 | userID = m.Author.ID 38 | } 39 | 40 | user, err := userDetails(userID, s) 41 | if err != nil { 42 | s.ChannelMessageSend(m.ChannelID, "There was an error getting the data :(") 43 | return 44 | } 45 | 46 | memberStruct, err := memberDetails(guild.ID, userID, s) 47 | if err != nil { 48 | s.ChannelMessageSend(m.ChannelID, "There was an error getting the data :(") 49 | return 50 | } 51 | 52 | var roleNames []string 53 | 54 | for _, role := range memberStruct.Roles { 55 | for _, guildRole := range guild.Roles { 56 | if guildRole.ID == role { 57 | roleNames = append(roleNames, guildRole.Name) 58 | } 59 | } 60 | } 61 | 62 | if len(roleNames) == 0 { 63 | roleNames = append(roleNames, "None") 64 | } 65 | 66 | if memberStruct.Nick == "" { 67 | nick = "None" 68 | } else { 69 | nick = memberStruct.Nick 70 | } 71 | 72 | var joinString string 73 | joinDateParsed, _ := memberStruct.JoinedAt.Parse() 74 | joinDate, err := time.Parse("2006-01-02T15:04:05.999999-07:00", joinDateParsed.String()) 75 | if err != nil { 76 | log.Error("error parsing time", err) 77 | joinString = "???" 78 | } else { 79 | joinString = joinDate.Format("02 Jan 06 15:04") 80 | } 81 | 82 | s.ChannelMessageSendEmbed(m.ChannelID, &discordgo.MessageEmbed{ 83 | Color: s.State.UserColor(userID, m.ChannelID), 84 | Description: fmt.Sprintf("%s is a loyal member of %s", user.Username, guild.Name), 85 | Author: &discordgo.MessageEmbedAuthor{ 86 | Name: user.Username, 87 | IconURL: discordgo.EndpointUserAvatar(userID, user.Avatar), 88 | }, 89 | Footer: footer, 90 | 91 | Fields: []*discordgo.MessageEmbedField{ 92 | {Name: "Username:", Value: user.Username, Inline: true}, 93 | {Name: "Nickname:", Value: nick, Inline: true}, 94 | {Name: "Joined Server:", Value: joinString, Inline: false}, 95 | {Name: "Roles:", Value: strings.Join(roleNames, ", "), Inline: true}, 96 | {Name: "ID Number:", Value: user.ID, Inline: true}, 97 | }, 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /msgUtils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | ) 12 | 13 | var mem runtime.MemStats 14 | 15 | func init() { 16 | newCommand("setGame", 0, false, msgSetGame).ownerOnly().add() 17 | newCommand("listUsers", 0, false, msgListUsers).ownerOnly().add() 18 | newCommand("reloadConfig", 0, false, msgReloadConfig).ownerOnly() 19 | newCommand("command", 0, false, msgCommand).ownerOnly().add() 20 | newCommand("help", 0, false, msgHelp).setHelp("ok").add() 21 | newCommand("info", 0, false, msgInfo).setHelp("Args: none\n\nSome info about 2Bot.\n\nExample:\n`!owo info`").add() 22 | newCommand("invite", 0, false, msgInvite).setHelp("Args: none\n\nSends an invite link for 2Bot!\n\nExample:\n`!owo invite`").add() 23 | newCommand("git", 0, false, msgGit).setHelp("Args: none\n\nLinks 2Bots github page.\n\nExample:\n`!owo git`").add() 24 | 25 | newCommand("setNSFW", 26 | discordgo.PermissionAdministrator|discordgo.PermissionManageServer, 27 | true, msgNSFW).setHelp("Args: none\n\nToggles NSFW commands in NSFW channels.\nAdmin only.\n\nExample:\n`!owo setNSFW`").add() 28 | 29 | newCommand("joinMessage", 30 | discordgo.PermissionAdministrator|discordgo.PermissionManageServer, 31 | true, msgJoinMessage).setHelp("Args: [true,false] | [message] | [channelID]\n\nEnables or disables join messages.\nthe message and channel that the bot welcomes new people in.\n" + 32 | "To mention the user in the message, put `%s` where you want the user to be mentioned in the message.\nLeave message \n\nExample to set message:\n" + 33 | "`!owo joinMessage true | Hey there %s! | 312294858582654978`\n>On member join\n`Hey there [@new member]`\n\n" + 34 | "Example to disable:\n`!owo joinMessage false`").add() 35 | 36 | } 37 | 38 | /* 39 | These are usually short commands that dont warrant their own file 40 | or are only for me, the creator..usually 41 | */ 42 | 43 | func msgCommand(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 44 | if len(msglist) != 3 { 45 | return 46 | } 47 | 48 | command := msglist[2] 49 | switch msglist[1] { 50 | case "enable": 51 | if comm, ok := disabledCommands[command]; ok { 52 | activeCommands[command] = comm 53 | delete(disabledCommands, command) 54 | s.ChannelMessageSend(m.ChannelID, "Enabled "+command) 55 | } 56 | case "disable": 57 | if comm, ok := activeCommands[command]; ok { 58 | disabledCommands[command] = comm 59 | delete(activeCommands, command) 60 | s.ChannelMessageSend(m.ChannelID, "Disabled "+command) 61 | } 62 | } 63 | } 64 | 65 | func msgSetGame(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 66 | if len(msglist) < 2 { 67 | return 68 | } 69 | 70 | game := strings.Join(msglist[1:], " ") 71 | 72 | if err := s.UpdateStatus(0, game); err != nil { 73 | log.Error("error changing game", err) 74 | return 75 | } 76 | 77 | conf.Game = game 78 | saveConfig() 79 | 80 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Game changed to %s!", game)) 81 | return 82 | } 83 | 84 | func msgHelp(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 85 | if len(msglist) == 2 { 86 | if val, ok := activeCommands[strings.ToLower(msglist[1])]; ok { 87 | val.helpCommand(s, m) 88 | return 89 | } 90 | } 91 | 92 | var commands []string 93 | for _, val := range activeCommands { 94 | if m.Author.ID == conf.OwnerID || !val.OwnerOnly { 95 | commands = append(commands, "`"+val.Name+"`") 96 | } 97 | } 98 | 99 | prefix := conf.Prefix 100 | if guild, err := guildDetails(m.ChannelID, "", s); err != nil { 101 | if val, ok := sMap.server(guild.ID); ok { 102 | prefix = val.Prefix 103 | } 104 | } 105 | 106 | s.ChannelMessageSendEmbed(m.ChannelID, &discordgo.MessageEmbed{ 107 | Color: 0, 108 | 109 | Fields: []*discordgo.MessageEmbedField{ 110 | {Name: "2Bot help", Value: strings.Join(commands, ", ") + "\n\nUse `" + prefix + "help [command]` for detailed info about a command."}, 111 | }, 112 | }) 113 | } 114 | 115 | func (c command) helpCommand(s *discordgo.Session, m *discordgo.MessageCreate) { 116 | s.ChannelMessageSendEmbed(m.ChannelID, &discordgo.MessageEmbed{ 117 | Color: 0, 118 | 119 | Fields: []*discordgo.MessageEmbedField{ 120 | { 121 | Name: c.Name, 122 | Value: c.Help, 123 | }, 124 | }, 125 | 126 | Footer: footer, 127 | }) 128 | } 129 | 130 | func msgInfo(s *discordgo.Session, m *discordgo.MessageCreate, _ []string) { 131 | ct1, err := getCreationTime(s.State.User.ID) 132 | if err != nil { 133 | s.ChannelMessageSend(m.ChannelID, "There was an error getting Bot info :(") 134 | return 135 | } 136 | 137 | creationTime := ct1.Format(time.UnixDate)[:10] 138 | 139 | runtime.ReadMemStats(&mem) 140 | 141 | var prefix string 142 | guild, err := guildDetails(m.ChannelID, "", s) 143 | if err == nil { 144 | if val, ok := sMap.server(guild.ID); ok { 145 | prefix = val.Prefix 146 | } 147 | } 148 | if prefix == "" { 149 | prefix = "None" 150 | } 151 | 152 | s.ChannelMessageSendEmbed(m.ChannelID, &discordgo.MessageEmbed{ 153 | Color: 0, 154 | Author: &discordgo.MessageEmbedAuthor{ 155 | Name: s.State.User.Username, 156 | IconURL: discordgo.EndpointUserAvatar(s.State.User.ID, s.State.User.Avatar), 157 | }, 158 | Footer: footer, 159 | 160 | Fields: []*discordgo.MessageEmbedField{ 161 | {Name: "Bot Name:", Value: codeBlock(s.State.User.Username), Inline: true}, 162 | {Name: "Creator:", Value: codeBlock("Strum355#0554"), Inline: true}, 163 | {Name: "Creation Date:", Value: codeBlock(creationTime), Inline: true}, 164 | {Name: "Global Prefix:", Value: codeBlock(conf.Prefix), Inline: true}, 165 | {Name: "Local Prefix", Value: codeBlock(prefix), Inline: true}, 166 | {Name: "Programming Language:", Value: codeBlock("Go"), Inline: true}, 167 | {Name: "Library:", Value: codeBlock("Discordgo"), Inline: true}, 168 | {Name: "Server Count:", Value: codeBlock(strconv.Itoa(len(s.State.Guilds))), Inline: true}, 169 | {Name: "Memory Usage:", Value: codeBlock(strconv.Itoa(int(mem.Alloc/1024/1024)) + "MB"), Inline: true}, 170 | {Name: "My Server:", Value: "https://discord.gg/9T34Y6u\nJoin here for support amongst other things!", Inline: false}, 171 | }, 172 | }) 173 | } 174 | 175 | func msgListUsers(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 176 | if len(msglist) < 2 { 177 | return 178 | } 179 | 180 | if guild, ok := sMap.server(msglist[1]); !ok || guild.Kicked { 181 | s.ChannelMessageSend(m.ChannelID, "2Bot isn't in that server") 182 | return 183 | } 184 | 185 | s.ChannelTyping(m.ChannelID) 186 | 187 | guild, err := guildDetails(msglist[1], "", s) 188 | if err != nil { 189 | return 190 | } 191 | 192 | var out []string 193 | 194 | for _, user := range guild.Members { 195 | //TODO limit check 196 | out = append(out, user.User.Username) 197 | } 198 | 199 | s.ChannelMessageSend(m.ChannelID, "Users in: "+guild.Name+"\n`"+strings.Join(out, ", ")+"`") 200 | } 201 | 202 | func msgGit(s *discordgo.Session, m *discordgo.MessageCreate, _ []string) { 203 | s.ChannelMessageSend(m.ChannelID, "Check me out here https://github.com/Strum355/2Bot-Discord-Bot\nGive it star to make my creators day! ⭐") 204 | } 205 | 206 | func msgNSFW(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 207 | guild, err := guildDetails(m.ChannelID, "", s) 208 | if err != nil { 209 | s.ChannelMessageSend(m.ChannelID, "There was an error toggling NSFW :( Try again please~") 210 | return 211 | } 212 | 213 | onOrOff := map[bool]string{true: "enabled", false: "disabled"} 214 | 215 | if guild, ok := sMap.server(guild.ID); ok { 216 | guild.Nsfw = !guild.Nsfw 217 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("NSFW %s", onOrOff[guild.Nsfw])) 218 | saveServers() 219 | } 220 | } 221 | 222 | func msgJoinMessage(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 223 | guild, err := guildDetails(m.ChannelID, "", s) 224 | if err != nil { 225 | s.ChannelMessageSend(m.ChannelID, "There was an error with discord :( Try again please~") 226 | return 227 | } 228 | 229 | split := trimSlice(strings.Split(strings.Join(msglist[1:], " "), "|")) 230 | 231 | if len(split) == 0 { 232 | split = append(split, msglist[1]) 233 | } 234 | 235 | if len(split) > 0 { 236 | if guild, ok := sMap.server(guild.ID); ok { 237 | if split[0] != "false" && split[0] != "true" { 238 | s.ChannelMessageSend(m.ChannelID, "Please say either `true` or `false` for enabling or disabling join messages~") 239 | return 240 | } 241 | 242 | if split[0] == "false" { 243 | guild.JoinMessage = [3]string{split[0]} 244 | saveServers() 245 | s.ChannelMessageSend(m.ChannelID, "Join messages disabled! ") 246 | return 247 | } 248 | 249 | if len(split) != 3 { 250 | s.ChannelMessageSend(m.ChannelID, "Not enough info given! :/\nMake sure the command only has two `|` in it.") 251 | return 252 | } 253 | 254 | channelStruct, err := channelDetails(split[2], s) 255 | if err != nil { 256 | s.ChannelMessageSend(m.ChannelID, "Please give me a proper channel ID :(") 257 | return 258 | } 259 | 260 | if split[1] == "" { 261 | s.ChannelMessageSend(m.ChannelID, "No message given :/") 262 | return 263 | } 264 | 265 | guild.JoinMessage = [3]string{split[0], split[1], split[2]} 266 | saveServers() 267 | 268 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Join message set to:\n%s\nin %s", split[1], channelStruct.Name)) 269 | } 270 | } 271 | } 272 | 273 | func msgReloadConfig(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 274 | if len(msglist) < 2 { 275 | return 276 | } 277 | 278 | var reloaded string 279 | switch msglist[1] { 280 | case "c": 281 | conf = new(config) 282 | if err := loadConfig(); err != nil { 283 | log.Error("error reloading config", err) 284 | s.ChannelMessageSend(m.ChannelID, "Error reloading config") 285 | return 286 | } 287 | reloaded = "config" 288 | case "u": 289 | u = make(users) 290 | if err := loadUsers(); err != nil { 291 | log.Error("error reloading config", err) 292 | s.ChannelMessageSend(m.ChannelID, "Error reloading config") 293 | return 294 | } 295 | reloaded = "users" 296 | } 297 | 298 | s.ChannelMessageSend(m.ChannelID, "Reloaded "+reloaded) 299 | } 300 | 301 | func msgInvite(s *discordgo.Session, m *discordgo.MessageCreate, _ []string) { 302 | s.ChannelMessageSendEmbed(m.ChannelID, &discordgo.MessageEmbed{ 303 | Color: 0, 304 | Image: &discordgo.MessageEmbedImage{ 305 | URL: happyEmoji, 306 | }, 307 | Fields: []*discordgo.MessageEmbedField{ 308 | {Name: "Invite me with this link!", Value: "https://discordapp.com/oauth2/authorize?client_id=301819949683572738&scope=bot&permissions=3533824", Inline: true}, 309 | }, 310 | }) 311 | } 312 | -------------------------------------------------------------------------------- /msgYoutube.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/Strum355/go-queue/queue" 12 | 13 | "github.com/Necroforger/dgwidgets" 14 | "github.com/bwmarrin/discordgo" 15 | "github.com/jonas747/dca" 16 | "github.com/rylio/ytdl" 17 | ) 18 | 19 | const ( 20 | stdURL = "https://www.youtube.com/watch" 21 | shortURL = "https://youtu.be/" 22 | embedURL = "https://www.youtube.com/embed/" 23 | ) 24 | 25 | type voiceInst struct { 26 | ChannelID string 27 | 28 | Playing bool 29 | 30 | Done chan error 31 | 32 | *sync.RWMutex 33 | 34 | Queue *queue.Queue 35 | VoiceCon *discordgo.VoiceConnection 36 | StreamingSession *dca.StreamingSession 37 | } 38 | 39 | type song struct { 40 | URL string `json:"url,omitempty"` 41 | Name string `json:"name,omitempty"` 42 | Image string `json:"image,omitempty"` 43 | 44 | Duration time.Duration `json:"duration"` 45 | } 46 | 47 | func init() { 48 | newCommand("yt", 0, false, msgYoutube).setHelp("Args: [play,stop] [url]\n\nWork In Progress!!! Play music from Youtube straight to your Discord Server!\n\n" + 49 | "Example 1: `!owo yt play https://www.youtube.com/watch?v=MvLdxtICOIY`\n" + 50 | "Example 2: `!owo yt stop`\n\n" + 51 | "SubCommands:\nplay\nstop\nlist, queue, songs\npause\nresume, unpause\nskip, next").add() 52 | } 53 | 54 | func msgYoutube(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 55 | if len(msglist) == 1 { 56 | return 57 | } 58 | 59 | switch msglist[1] { 60 | case "play": 61 | addToQueue(s, m, msglist[2:]) 62 | case "stop": 63 | stopQueue(s, m) 64 | case "list", "queue", "songs": 65 | listQueue(s, m) 66 | case "pause": 67 | pauseQueue(s, m) 68 | case "resume", "unpause": 69 | unpauseQueue(s, m) 70 | case "skip", "next": 71 | skipSong(s, m) 72 | default: 73 | s.ChannelMessageSend(m.ChannelID, activeCommands["youtube"].Help) 74 | } 75 | } 76 | 77 | func addToQueue(s *discordgo.Session, m *discordgo.MessageCreate, msglist []string) { 78 | if len(msglist) == 0 { 79 | return 80 | } 81 | 82 | guild, err := guildDetails(m.ChannelID, "", s) 83 | if err != nil { 84 | s.ChannelMessageSend(m.ChannelID, "There was a problem adding to queue :( please try again") 85 | return 86 | } 87 | 88 | srvr, ok := sMap.server(guild.ID) 89 | if !ok { 90 | s.ChannelMessageSend(m.ChannelID, "An error occurred that really shouldn't have happened...") 91 | log.Error("not in server map?", guild.ID) 92 | return 93 | } 94 | 95 | if srvr.VoiceInst == nil { 96 | srvr.newVoiceInstance() 97 | } 98 | 99 | srvr.VoiceInst.Lock() 100 | defer srvr.VoiceInst.Unlock() 101 | 102 | url := msglist[0] 103 | 104 | if !strings.HasPrefix(url, stdURL) && !strings.HasPrefix(url, shortURL) && !strings.HasPrefix(url, embedURL) { 105 | s.ChannelMessageSend(m.ChannelID, "Please make sure the URL is a valid YouTube URL. If I got this wrong, please let my creator know~") 106 | return 107 | } 108 | 109 | vid, err := getVideoInfo(url, s, m) 110 | if err != nil { 111 | return 112 | } 113 | 114 | vc, err := createVoiceConnection(s, m, guild, srvr) 115 | if err != nil { 116 | return 117 | } 118 | 119 | srvr.addSong(song{ 120 | URL: url, 121 | Name: vid.Title, 122 | Duration: vid.Duration, 123 | Image: vid.GetThumbnailURL(ytdl.ThumbnailQualityMedium).String(), 124 | }) 125 | 126 | s.ChannelMessageSend(m.ChannelID, "Added "+vid.Title+" to the queue!") 127 | 128 | if !srvr.VoiceInst.Playing { 129 | srvr.VoiceInst.VoiceCon = vc 130 | srvr.VoiceInst.Playing = true 131 | srvr.VoiceInst.ChannelID = vc.ChannelID 132 | go play(s, m, srvr, vc) 133 | } 134 | 135 | s.ChannelMessageSend(m.ChannelID, "Need to be in a voice channel!") 136 | } 137 | 138 | func createVoiceConnection(s *discordgo.Session, m *discordgo.MessageCreate, guild *discordgo.Guild, srvr *server) (*discordgo.VoiceConnection, error) { 139 | for _, vs := range guild.VoiceStates { 140 | if vs.UserID == m.Author.ID && (vs.ChannelID == srvr.VoiceInst.ChannelID || !srvr.VoiceInst.Playing) { 141 | vc, err := s.ChannelVoiceJoin(guild.ID, vs.ChannelID, false, true) 142 | if err != nil { 143 | s.ChannelMessageSend(m.ChannelID, "Error joining voice channel") 144 | log.Error("error joining voice channel", err) 145 | return nil, err 146 | } 147 | return vc, nil 148 | } 149 | } 150 | return nil, errors.New("not in voice channel") 151 | } 152 | 153 | func getVideoInfo(url string, s *discordgo.Session, m *discordgo.MessageCreate) (*ytdl.VideoInfo, error) { 154 | vid, err := ytdl.GetVideoInfo(url) 155 | if err != nil { 156 | s.ChannelMessageSend(m.ChannelID, "Error getting video info") 157 | log.Error("error getting video info", err) 158 | return nil, err 159 | } 160 | return vid, nil 161 | } 162 | 163 | func play(s *discordgo.Session, m *discordgo.MessageCreate, srvr *server, vc *discordgo.VoiceConnection) { 164 | if srvr.queueLength() == 0 { 165 | srvr.youtubeCleanup() 166 | s.ChannelMessageSend(m.ChannelID, "🔇 Done queue!") 167 | return 168 | } 169 | 170 | srvr.VoiceInst.Lock() 171 | vid, err := getVideoInfo(srvr.nextSong().URL, s, m) 172 | if err != nil { 173 | srvr.VoiceInst.Unlock() 174 | return 175 | } 176 | 177 | reader, writer := io.Pipe() 178 | defer reader.Close() 179 | 180 | formats := vid.Formats.Best(ytdl.FormatAudioBitrateKey) 181 | if len(formats) > 0 { 182 | go func() { 183 | defer writer.Close() 184 | if err := vid.Download(formats[0], writer); err != nil && err != io.ErrClosedPipe { 185 | s.ChannelMessageSend(m.ChannelID, xmark+" Error downloading the music") 186 | log.Error("error downloading YouTube video", err) 187 | srvr.VoiceInst.Done <- err 188 | return 189 | } 190 | }() 191 | } 192 | 193 | encSesh, err := dca.EncodeMem(reader, dca.StdEncodeOptions) 194 | if err != nil { 195 | s.ChannelMessageSend(m.ChannelID, xmark+" Error starting the stream") 196 | srvr.youtubeCleanup() 197 | srvr.VoiceInst.Unlock() 198 | // will only return non nill error if options arent valid 199 | log.Error("error validating options", err) 200 | return 201 | } 202 | defer encSesh.Cleanup() 203 | 204 | srvr.VoiceInst.StreamingSession = dca.NewStream(encSesh, vc, srvr.VoiceInst.Done) 205 | 206 | s.ChannelMessageSend(m.ChannelID, "🔊 Playing: "+vid.Title) 207 | 208 | srvr.VoiceInst.Unlock() 209 | 210 | Outer: 211 | for { 212 | err = <-srvr.VoiceInst.Done 213 | 214 | done, _ := srvr.VoiceInst.StreamingSession.Finished() 215 | 216 | switch { 217 | case err.Error() == "stop": 218 | srvr.youtubeCleanup() 219 | s.ChannelMessageSend(m.ChannelID, "🔇 Stopped") 220 | return 221 | case err.Error() == "skip": 222 | s.ChannelMessageSend(m.ChannelID, "⏩ Skipping") 223 | break Outer 224 | case !done && err != io.EOF: 225 | srvr.youtubeCleanup() 226 | s.ChannelMessageSend(m.ChannelID, "There was an error streaming music :(") 227 | log.Error("error streaming music", err) 228 | return 229 | case done && err == io.EOF: 230 | // Remove the currently playing song from the queue and then start the next one 231 | srvr.finishedSong() 232 | break Outer 233 | } 234 | } 235 | 236 | go play(s, m, srvr, vc) 237 | } 238 | 239 | func listQueue(s *discordgo.Session, m *discordgo.MessageCreate) { 240 | guild, err := guildDetails(m.ChannelID, "", s) 241 | if err != nil { 242 | s.ChannelMessageSend(m.ChannelID, "There was an issue loading the list :( please try again") 243 | return 244 | } 245 | 246 | srvr, ok := sMap.server(guild.ID) 247 | if !ok { 248 | s.ChannelMessageSend(m.ChannelID, "An error occurred that really shouldn't have happened...") 249 | log.Error("not in server map?", guild.ID) 250 | return 251 | } 252 | 253 | if srvr.queueLength() == 0 { 254 | s.ChannelMessageSend(m.ChannelID, "No songs in queue!") 255 | return 256 | } 257 | 258 | p := dgwidgets.NewPaginator(s, m.ChannelID) 259 | p.Add(&discordgo.MessageEmbed{ 260 | Title: guild.Name + "'s queue", 261 | 262 | Fields: func() (out []*discordgo.MessageEmbedField) { 263 | for i, song := range srvr.iterateQueue() { 264 | out = append(out, &discordgo.MessageEmbedField{ 265 | Name: fmt.Sprintf("%d - %s", i, song.Name), 266 | Value: song.Duration.String(), 267 | }) 268 | } 269 | return 270 | }(), 271 | }) 272 | 273 | for _, song := range srvr.iterateQueue() { 274 | p.Add(&discordgo.MessageEmbed{ 275 | Title: fmt.Sprintf("Title: %s\nDuration: %s\nURL: %s", song.Name, song.Duration, song.URL), 276 | 277 | Image: &discordgo.MessageEmbedImage{ 278 | URL: song.Image, 279 | }, 280 | }) 281 | } 282 | 283 | p.SetPageFooters() 284 | p.Loop = true 285 | p.ColourWhenDone = 0xff0000 286 | p.DeleteReactionsWhenDone = true 287 | p.Widget.Timeout = time.Minute * 2 288 | p.Spawn() 289 | } 290 | 291 | func stopQueue(s *discordgo.Session, m *discordgo.MessageCreate) { 292 | guild, err := guildDetails(m.ChannelID, "", s) 293 | if err != nil { 294 | s.ChannelMessageSend(m.ChannelID, "There was an error stopping the queue :( Please try again.") 295 | return 296 | } 297 | 298 | if srvr, ok := sMap.server(guild.ID); ok { 299 | srvr.VoiceInst.Done <- errors.New("stop") 300 | } 301 | } 302 | 303 | func pauseQueue(s *discordgo.Session, m *discordgo.MessageCreate) { 304 | guild, err := guildDetails(m.ChannelID, "", s) 305 | if err != nil { 306 | s.ChannelMessageSend(m.ChannelID, "There was an error pausing the video :( Please try again.") 307 | return 308 | } 309 | 310 | srvr, ok := sMap.server(guild.ID) 311 | if !ok { 312 | return 313 | } 314 | 315 | srvr.VoiceInst.Lock() 316 | defer srvr.VoiceInst.Unlock() 317 | 318 | s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("⏸ Paused. To unpause, use the command `%syt unpause`", func() string { 319 | if srvr.Prefix == "" { 320 | return conf.Prefix 321 | } 322 | return srvr.Prefix 323 | }())) 324 | 325 | srvr.VoiceInst.StreamingSession.SetPaused(true) 326 | } 327 | 328 | func unpauseQueue(s *discordgo.Session, m *discordgo.MessageCreate) { 329 | guild, err := guildDetails(m.ChannelID, "", s) 330 | if err != nil { 331 | s.ChannelMessageSend(m.ChannelID, "There was an error unpausing the song :( please try again") 332 | return 333 | } 334 | 335 | if srvr, ok := sMap.server(guild.ID); ok { 336 | srvr.VoiceInst.Lock() 337 | defer srvr.VoiceInst.Unlock() 338 | srvr.VoiceInst.StreamingSession.SetPaused(false) 339 | } 340 | } 341 | 342 | func skipSong(s *discordgo.Session, m *discordgo.MessageCreate) { 343 | guild, err := guildDetails(m.ChannelID, "", s) 344 | if err != nil { 345 | s.ChannelMessageSend(m.ChannelID, "There was an error skipping the song :( please try again") 346 | return 347 | } 348 | 349 | if srvr, ok := sMap.server(guild.ID); ok { 350 | srvr.VoiceInst.Lock() 351 | defer srvr.VoiceInst.Unlock() 352 | srvr.VoiceInst.Done <- errors.New("skip") 353 | } 354 | } 355 | 356 | func (s *server) youtubeCleanup() { 357 | s.VoiceInst.Lock() 358 | defer s.VoiceInst.Unlock() 359 | s.VoiceInst.VoiceCon.Disconnect() 360 | s.newVoiceInstance() 361 | //sMap.VoiceInsts-- 362 | } 363 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/Strum355/go-queue/queue" 7 | ) 8 | 9 | type servers struct { 10 | Count int `json:"-"` 11 | VoiceInsts int `json:"-"` 12 | 13 | Mutex sync.RWMutex `json:"-"` 14 | 15 | serverMap map[string]*server 16 | } 17 | 18 | func (s *servers) server(id string) (val *server, ok bool) { 19 | val, ok = s.serverMap[id] 20 | return 21 | } 22 | 23 | func (s *servers) setServer(id string, serv server) { 24 | s.serverMap[id] = &serv 25 | } 26 | 27 | type server struct { 28 | LogChannel string `json:"log_channel"` 29 | Prefix string `json:"server_prefix,omitempty"` 30 | 31 | Log bool `json:"log_active"` 32 | Kicked bool `json:"kicked"` 33 | Nsfw bool `json:"nsfw"` 34 | 35 | //Enabled, Message, Channel 36 | JoinMessage [3]string `json:"join"` 37 | 38 | VoiceInst *voiceInst `json:"-"` 39 | 40 | Playlists map[string][]song `json:"playlists"` 41 | } 42 | 43 | func (s *servers) getCount() int { 44 | return s.Count 45 | } 46 | 47 | /* 48 | func (s *servers) validate() { 49 | for _, guild := range s.Server { 50 | details := guildDetails(guild.ID) 51 | } 52 | } */ 53 | 54 | func (s *server) newVoiceInstance() { 55 | s.VoiceInst = &voiceInst{ 56 | Queue: queue.New(), 57 | Done: make(chan error), 58 | RWMutex: new(sync.RWMutex), 59 | } 60 | } 61 | 62 | func (s server) nextSong() song { 63 | return s.VoiceInst.Queue.Front().(song) 64 | } 65 | 66 | func (s server) finishedSong() { 67 | s.VoiceInst.Queue.PopFront() 68 | } 69 | 70 | func (s server) addSong(song song) { 71 | s.VoiceInst.Queue.PushBack(song) 72 | } 73 | 74 | func (s server) queueLength() int { 75 | s.VoiceInst.RLock() 76 | defer s.VoiceInst.RUnlock() 77 | return s.VoiceInst.Queue.Len() 78 | } 79 | 80 | func (s server) iterateQueue() []song { 81 | s.VoiceInst.RLock() 82 | defer s.VoiceInst.RUnlock() 83 | ret := make([]song, s.VoiceInst.Queue.Len()) 84 | for i, val := range s.VoiceInst.Queue.List() { 85 | ret[i] = val.(song) 86 | } 87 | return ret 88 | } 89 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | var ( 9 | u = make(users) 10 | sMap = servers{serverMap: make(map[string]*server)} 11 | ) 12 | 13 | func saveJSON(path string, data interface{}) error { 14 | f, err := os.OpenFile("json/"+path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 15 | if err != nil { 16 | log.Error("error saving", path, err) 17 | return err 18 | } 19 | 20 | if err = json.NewEncoder(f).Encode(data); err != nil { 21 | log.Error("error saving", path, err) 22 | return err 23 | } 24 | return nil 25 | } 26 | 27 | func loadJSON(path string, v interface{}) error { 28 | f, err := os.OpenFile("json/"+path, os.O_RDONLY, 0600) 29 | if err != nil { 30 | log.Error("error loading", path, err) 31 | return err 32 | } 33 | 34 | if err := json.NewDecoder(f).Decode(v); err != nil { 35 | log.Error("error loading", path, err) 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | func cleanup() { 42 | for _, f := range []func() error{saveConfig, saveQueue, saveServers, saveUsers} { 43 | if err := f(); err != nil { 44 | log.Error("error cleaning up files", err) 45 | } 46 | } 47 | log.Info("Done cleanup. Exiting.") 48 | } 49 | 50 | func loadConfig() error { 51 | return loadJSON("config.json", conf) 52 | } 53 | 54 | func saveConfig() error { 55 | return saveJSON("config.json", conf) 56 | } 57 | 58 | func loadServers() error { 59 | sMap = servers{serverMap: make(map[string]*server)} 60 | return loadJSON("servers.json", &sMap) 61 | } 62 | 63 | func saveServers() error { 64 | return saveJSON("servers.json", sMap) 65 | } 66 | 67 | func loadUsers() error { 68 | u = make(map[string]*user) 69 | return loadJSON("users.json", &u) 70 | } 71 | 72 | func saveUsers() error { 73 | return saveJSON("users.json", u) 74 | } 75 | 76 | func loadQueue() error { 77 | return loadJSON("queue.json", &imageQueue) 78 | } 79 | 80 | func saveQueue() error { 81 | return saveJSON("queue.json", imageQueue) 82 | } 83 | -------------------------------------------------------------------------------- /structs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type config struct { 4 | Game string `json:"game"` 5 | Prefix string `json:"prefix"` 6 | Token string `json:"token"` 7 | OwnerID string `json:"owner_id"` 8 | URL string `json:"url"` 9 | 10 | InDev bool `json:"indev"` 11 | 12 | DiscordPWKey string `json:"discord.pw_key"` 13 | 14 | CurrImg int `json:"curr_img_id"` 15 | MaxProc int `json:"maxproc"` 16 | 17 | Blacklist []string `json:"blacklist"` 18 | } 19 | 20 | type queuedImage struct { 21 | ReviewMsgID string `json:"reviewMsgID"` 22 | AuthorID string `json:"author_id"` 23 | AuthorDiscrim string `json:"author_discrim"` 24 | AuthorName string `json:"author_name"` 25 | ImageName string `json:"image_name"` 26 | ImageURL string `json:"image_url"` 27 | 28 | FileSize int `json:"file_size"` 29 | } 30 | 31 | type users map[string]*user 32 | 33 | type user struct { 34 | Images map[string]string `json:"images"` 35 | 36 | DiskQuota int `json:"quota"` 37 | CurrDiskUsed int `json:"curr_used"` 38 | QueueSize int `json:"queue_size"` 39 | 40 | TempImages []string `json:"temp_images"` 41 | } 42 | -------------------------------------------------------------------------------- /utilities.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/bwmarrin/discordgo" 11 | "github.com/go-chi/chi" 12 | ) 13 | 14 | //From Necroforger's dgwidgets 15 | func nextReactionAdd(s *discordgo.Session) chan *discordgo.MessageReactionAdd { 16 | out := make(chan *discordgo.MessageReactionAdd) 17 | s.AddHandlerOnce(func(_ *discordgo.Session, e *discordgo.MessageReactionAdd) { 18 | out <- e 19 | }) 20 | return out 21 | } 22 | 23 | func nextMessageCreate(s *discordgo.Session) chan *discordgo.MessageCreate { 24 | out := make(chan *discordgo.MessageCreate) 25 | s.AddHandlerOnce(func(_ *discordgo.Session, e *discordgo.MessageCreate) { 26 | out <- e 27 | }) 28 | return out 29 | } 30 | 31 | func randRange(min, max int) int { 32 | rand.Seed(time.Now().Unix()) 33 | if max == 0 { 34 | return 0 35 | } 36 | return rand.Intn(max-min) + min 37 | } 38 | 39 | func findIndex(s []string, f string) int { 40 | for i, j := range s { 41 | if j == f { 42 | return i 43 | } 44 | } 45 | return -1 46 | } 47 | 48 | func remove(s []string, i int) []string { 49 | s[i] = s[len(s)-1] 50 | return s[:len(s)-1] 51 | } 52 | 53 | func min(a, b int) int { 54 | if a < b { 55 | return a 56 | } 57 | return b 58 | } 59 | 60 | func getCreationTime(ID string) (t time.Time, err error) { 61 | i, err := strconv.ParseInt(ID, 10, 64) 62 | if err != nil { 63 | return 64 | } 65 | 66 | timestamp := (i >> 22) + 1420070400000 67 | t = time.Unix(timestamp/1000, 0) 68 | return 69 | } 70 | 71 | func codeSeg(s ...string) string { 72 | return "`" + strings.Join(s, " ") + "`" 73 | } 74 | 75 | func codeBlock(s ...string) string { 76 | return "```" + strings.Join(s, " ") + "```" 77 | } 78 | 79 | func isIn(a string, list []string) bool { 80 | for _, b := range list { 81 | if b == a { 82 | return true 83 | } 84 | } 85 | return false 86 | } 87 | 88 | func trimSlice(s []string) (ret []string) { 89 | for _, i := range s { 90 | ret = append(ret, strings.TrimSpace(i)) 91 | } 92 | return 93 | } 94 | 95 | func deleteMessage(m *discordgo.Message, s *discordgo.Session) { 96 | if m != nil { 97 | s.ChannelMessageDelete(m.ChannelID, m.ID) 98 | } 99 | } 100 | 101 | func channelDetails(channelID string, s *discordgo.Session) (channelDetails *discordgo.Channel, err error) { 102 | channelDetails, err = s.State.Channel(channelID) 103 | if err != nil { 104 | if err == discordgo.ErrStateNotFound { 105 | channelDetails, err = s.Channel(channelID) 106 | if err != nil { 107 | log.Error("error getting channel details", channelID, err) 108 | } 109 | } 110 | } 111 | return 112 | } 113 | 114 | func permissionDetails(authorID, channelID string, s *discordgo.Session) (perms int, err error) { 115 | perms, err = s.State.UserChannelPermissions(authorID, channelID) 116 | if err != nil { 117 | if err == discordgo.ErrStateNotFound { 118 | perms, err = s.UserChannelPermissions(authorID, channelID) 119 | if err != nil { 120 | log.Error("error getting perm details", err) 121 | } 122 | } 123 | } 124 | return 125 | } 126 | 127 | func userDetails(memberID string, s *discordgo.Session) (user *discordgo.User, err error) { 128 | user, err = s.User(memberID) 129 | if err != nil { 130 | log.Error("error getting user details", err) 131 | } 132 | return 133 | } 134 | 135 | func activePrefix(channelID string, s *discordgo.Session) (prefix string, err error) { 136 | prefix = conf.Prefix 137 | guild, err := guildDetails(channelID, "", s) 138 | if err != nil { 139 | s.ChannelMessageSend(channelID, "There was an issue executing the command :( Try again please~") 140 | return 141 | } else if val, ok := sMap.server(guild.ID); ok && val.Prefix != "" { 142 | prefix = val.Prefix 143 | } 144 | return prefix, nil 145 | } 146 | 147 | func memberDetails(guildID, memberID string, s *discordgo.Session) (member *discordgo.Member, err error) { 148 | member, err = s.State.Member(guildID, memberID) 149 | if err != nil { 150 | if err == discordgo.ErrStateNotFound { 151 | member, err = s.GuildMember(guildID, memberID) 152 | if err != nil { 153 | log.Error("error getting member details", err) 154 | } 155 | } 156 | } 157 | return 158 | } 159 | 160 | func guildDetails(channelID, guildID string, s *discordgo.Session) (guildDetails *discordgo.Guild, err error) { 161 | if guildID == "" { 162 | var channel *discordgo.Channel 163 | channel, err = channelDetails(channelID, s) 164 | if err != nil { 165 | return 166 | } 167 | 168 | guildID = channel.GuildID 169 | } 170 | 171 | guildDetails, err = s.State.Guild(guildID) 172 | if err != nil { 173 | if err == discordgo.ErrStateNotFound { 174 | guildDetails, err = s.Guild(guildID) 175 | if err != nil { 176 | log.Error("error getting guild details", guildID, err) 177 | } 178 | } 179 | } 180 | return 181 | } 182 | 183 | func isInServer(w http.ResponseWriter, r *http.Request) { 184 | defer r.Body.Close() 185 | 186 | id := chi.URLParam(r, "id") 187 | guild, err := guildDetails(serverID, "", dg) 188 | if err != nil { 189 | w.WriteHeader(http.StatusInternalServerError) 190 | return 191 | } 192 | 193 | for _, member := range guild.Members { 194 | if member.User.ID == id { 195 | w.WriteHeader(http.StatusOK) 196 | return 197 | } 198 | } 199 | 200 | w.WriteHeader(http.StatusNotFound) 201 | } 202 | --------------------------------------------------------------------------------