├── .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 | [](https://goreportcard.com/report/github.com/Strum355/2Bot-Discord-Bot)
5 | [](https://discord.gg/9T34Y6u)
6 | [](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 |
--------------------------------------------------------------------------------