initial commit
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
This commit is contained in:
		
						commit
						0ac377da90
					
				
					 37 changed files with 2848 additions and 0 deletions
				
			
		
							
								
								
									
										15
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| # ---> Go | ||||
| # Binaries for programs and plugins | ||||
| *.exe | ||||
| *.exe~ | ||||
| *.dll | ||||
| *.so | ||||
| *.dylib | ||||
| 
 | ||||
| # Test binary, build with `go test -c` | ||||
| *.test | ||||
| 
 | ||||
| # Output of the go coverage tool, specifically when used with LiteIDE | ||||
| *.out | ||||
| 
 | ||||
| /twitchsingstools | ||||
							
								
								
									
										319
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,319 @@ | |||
| GNU GENERAL PUBLIC LICENSE | ||||
| 
 | ||||
| Version 2, June 1991 | ||||
| 
 | ||||
| Copyright (C) 1989, 1991 Free Software Foundation, Inc.  | ||||
| 
 | ||||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 , USA | ||||
| 
 | ||||
| Everyone is permitted to copy and distribute verbatim copies of this license | ||||
| document, but changing it is not allowed. | ||||
| 
 | ||||
| Preamble | ||||
| 
 | ||||
| The licenses for most software are designed to take away your freedom to share | ||||
| and change it. By contrast, the GNU General Public License is intended to | ||||
| guarantee your freedom to share and change free software--to make sure the | ||||
| software is free for all its users. This General Public License applies to | ||||
| most of the Free Software Foundation's software and to any other program whose | ||||
| authors commit to using it. (Some other Free Software Foundation software | ||||
| is covered by the GNU Lesser General Public License instead.) You can apply | ||||
| it to your programs, too. | ||||
| 
 | ||||
| When we speak of free software, we are referring to freedom, not price. Our | ||||
| General Public Licenses are designed to make sure that you have the freedom | ||||
| to distribute copies of free software (and charge for this service if you | ||||
| wish), that you receive source code or can get it if you want it, that you | ||||
| can change the software or use pieces of it in new free programs; and that | ||||
| you know you can do these things. | ||||
| 
 | ||||
| To protect your rights, we need to make restrictions that forbid anyone to | ||||
| deny you these rights or to ask you to surrender the rights. These restrictions | ||||
| translate to certain responsibilities for you if you distribute copies of | ||||
| the software, or if you modify it. | ||||
| 
 | ||||
| For example, if you distribute copies of such a program, whether gratis or | ||||
| for a fee, you must give the recipients all the rights that you have. You | ||||
| must make sure that they, too, receive or can get the source code. And you | ||||
| must show them these terms so they know their rights. | ||||
| 
 | ||||
| We protect your rights with two steps: (1) copyright the software, and (2) | ||||
| offer you this license which gives you legal permission to copy, distribute | ||||
| and/or modify the software. | ||||
| 
 | ||||
| Also, for each author's protection and ours, we want to make certain that | ||||
| everyone understands that there is no warranty for this free software. If | ||||
| the software is modified by someone else and passed on, we want its recipients | ||||
| to know that what they have is not the original, so that any problems introduced | ||||
| by others will not reflect on the original authors' reputations. | ||||
| 
 | ||||
| Finally, any free program is threatened constantly by software patents. We | ||||
| wish to avoid the danger that redistributors of a free program will individually | ||||
| obtain patent licenses, in effect making the program proprietary. To prevent | ||||
| this, we have made it clear that any patent must be licensed for everyone's | ||||
| free use or not licensed at all. | ||||
| 
 | ||||
| The precise terms and conditions for copying, distribution and modification | ||||
| follow. | ||||
| 
 | ||||
| TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION | ||||
| 
 | ||||
| 0. This License applies to any program or other work which contains a notice | ||||
| placed by the copyright holder saying it may be distributed under the terms | ||||
| of this General Public License. The "Program", below, refers to any such program | ||||
| or work, and a "work based on the Program" means either the Program or any | ||||
| derivative work under copyright law: that is to say, a work containing the | ||||
| Program or a portion of it, either verbatim or with modifications and/or translated | ||||
| into another language. (Hereinafter, translation is included without limitation | ||||
| in the term "modification".) Each licensee is addressed as "you". | ||||
| 
 | ||||
| Activities other than copying, distribution and modification are not covered | ||||
| by this License; they are outside its scope. The act of running the Program | ||||
| is not restricted, and the output from the Program is covered only if its | ||||
| contents constitute a work based on the Program (independent of having been | ||||
| made by running the Program). Whether that is true depends on what the Program | ||||
| does. | ||||
| 
 | ||||
| 1. You may copy and distribute verbatim copies of the Program's source code | ||||
| as you receive it, in any medium, provided that you conspicuously and appropriately | ||||
| publish on each copy an appropriate copyright notice and disclaimer of warranty; | ||||
| keep intact all the notices that refer to this License and to the absence | ||||
| of any warranty; and give any other recipients of the Program a copy of this | ||||
| License along with the Program. | ||||
| 
 | ||||
| You may charge a fee for the physical act of transferring a copy, and you | ||||
| may at your option offer warranty protection in exchange for a fee. | ||||
| 
 | ||||
| 2. You may modify your copy or copies of the Program or any portion of it, | ||||
| thus forming a work based on the Program, and copy and distribute such modifications | ||||
| or work under the terms of Section 1 above, provided that you also meet all | ||||
| of these conditions: | ||||
| 
 | ||||
| a) You must cause the modified files to carry prominent notices stating that | ||||
| you changed the files and the date of any change. | ||||
| 
 | ||||
| b) You must cause any work that you distribute or publish, that in whole or | ||||
| in part contains or is derived from the Program or any part thereof, to be | ||||
| licensed as a whole at no charge to all third parties under the terms of this | ||||
| License. | ||||
| 
 | ||||
| c) If the modified program normally reads commands interactively when run, | ||||
| you must cause it, when started running for such interactive use in the most | ||||
| ordinary way, to print or display an announcement including an appropriate | ||||
| copyright notice and a notice that there is no warranty (or else, saying that | ||||
| you provide a warranty) and that users may redistribute the program under | ||||
| these conditions, and telling the user how to view a copy of this License. | ||||
| (Exception: if the Program itself is interactive but does not normally print | ||||
| such an announcement, your work based on the Program is not required to print | ||||
| an announcement.) | ||||
| 
 | ||||
| These requirements apply to the modified work as a whole. If identifiable | ||||
| sections of that work are not derived from the Program, and can be reasonably | ||||
| considered independent and separate works in themselves, then this License, | ||||
| and its terms, do not apply to those sections when you distribute them as | ||||
| separate works. But when you distribute the same sections as part of a whole | ||||
| which is a work based on the Program, the distribution of the whole must be | ||||
| on the terms of this License, whose permissions for other licensees extend | ||||
| to the entire whole, and thus to each and every part regardless of who wrote | ||||
| it. | ||||
| 
 | ||||
| Thus, it is not the intent of this section to claim rights or contest your | ||||
| rights to work written entirely by you; rather, the intent is to exercise | ||||
| the right to control the distribution of derivative or collective works based | ||||
| on the Program. | ||||
| 
 | ||||
| In addition, mere aggregation of another work not based on the Program with | ||||
| the Program (or with a work based on the Program) on a volume of a storage | ||||
| or distribution medium does not bring the other work under the scope of this | ||||
| License. | ||||
| 
 | ||||
| 3. You may copy and distribute the Program (or a work based on it, under Section | ||||
| 2) in object code or executable form under the terms of Sections 1 and 2 above | ||||
| provided that you also do one of the following: | ||||
| 
 | ||||
| a) Accompany it with the complete corresponding machine-readable source code, | ||||
| which must be distributed under the terms of Sections 1 and 2 above on a medium | ||||
| customarily used for software interchange; or, | ||||
| 
 | ||||
| b) Accompany it with a written offer, valid for at least three years, to give | ||||
| any third party, for a charge no more than your cost of physically performing | ||||
| source distribution, a complete machine-readable copy of the corresponding | ||||
| source code, to be distributed under the terms of Sections 1 and 2 above on | ||||
| a medium customarily used for software interchange; or, | ||||
| 
 | ||||
| c) Accompany it with the information you received as to the offer to distribute | ||||
| corresponding source code. (This alternative is allowed only for noncommercial | ||||
| distribution and only if you received the program in object code or executable | ||||
| form with such an offer, in accord with Subsection b above.) | ||||
| 
 | ||||
| The source code for a work means the preferred form of the work for making | ||||
| modifications to it. For an executable work, complete source code means all | ||||
| the source code for all modules it contains, plus any associated interface | ||||
| definition files, plus the scripts used to control compilation and installation | ||||
| of the executable. However, as a special exception, the source code distributed | ||||
| need not include anything that is normally distributed (in either source or | ||||
| binary form) with the major components (compiler, kernel, and so on) of the | ||||
| operating system on which the executable runs, unless that component itself | ||||
| accompanies the executable. | ||||
| 
 | ||||
| If distribution of executable or object code is made by offering access to | ||||
| copy from a designated place, then offering equivalent access to copy the | ||||
| source code from the same place counts as distribution of the source code, | ||||
| even though third parties are not compelled to copy the source along with | ||||
| the object code. | ||||
| 
 | ||||
| 4. You may not copy, modify, sublicense, or distribute the Program except | ||||
| as expressly provided under this License. Any attempt otherwise to copy, modify, | ||||
| sublicense or distribute the Program is void, and will automatically terminate | ||||
| your rights under this License. However, parties who have received copies, | ||||
| or rights, from you under this License will not have their licenses terminated | ||||
| so long as such parties remain in full compliance. | ||||
| 
 | ||||
| 5. You are not required to accept this License, since you have not signed | ||||
| it. However, nothing else grants you permission to modify or distribute the | ||||
| Program or its derivative works. These actions are prohibited by law if you | ||||
| do not accept this License. Therefore, by modifying or distributing the Program | ||||
| (or any work based on the Program), you indicate your acceptance of this License | ||||
| to do so, and all its terms and conditions for copying, distributing or modifying | ||||
| the Program or works based on it. | ||||
| 
 | ||||
| 6. Each time you redistribute the Program (or any work based on the Program), | ||||
| the recipient automatically receives a license from the original licensor | ||||
| to copy, distribute or modify the Program subject to these terms and conditions. | ||||
| You may not impose any further restrictions on the recipients' exercise of | ||||
| the rights granted herein. You are not responsible for enforcing compliance | ||||
| by third parties to this License. | ||||
| 
 | ||||
| 7. If, as a consequence of a court judgment or allegation of patent infringement | ||||
| or for any other reason (not limited to patent issues), conditions are imposed | ||||
| on you (whether by court order, agreement or otherwise) that contradict the | ||||
| conditions of this License, they do not excuse you from the conditions of | ||||
| this License. If you cannot distribute so as to satisfy simultaneously your | ||||
| obligations under this License and any other pertinent obligations, then as | ||||
| a consequence you may not distribute the Program at all. For example, if a | ||||
| patent license would not permit royalty-free redistribution of the Program | ||||
| by all those who receive copies directly or indirectly through you, then the | ||||
| only way you could satisfy both it and this License would be to refrain entirely | ||||
| from distribution of the Program. | ||||
| 
 | ||||
| If any portion of this section is held invalid or unenforceable under any | ||||
| particular circumstance, the balance of the section is intended to apply and | ||||
| the section as a whole is intended to apply in other circumstances. | ||||
| 
 | ||||
| It is not the purpose of this section to induce you to infringe any patents | ||||
| or other property right claims or to contest validity of any such claims; | ||||
| this section has the sole purpose of protecting the integrity of the free | ||||
| software distribution system, which is implemented by public license practices. | ||||
| Many people have made generous contributions to the wide range of software | ||||
| distributed through that system in reliance on consistent application of that | ||||
| system; it is up to the author/donor to decide if he or she is willing to | ||||
| distribute software through any other system and a licensee cannot impose | ||||
| that choice. | ||||
| 
 | ||||
| This section is intended to make thoroughly clear what is believed to be a | ||||
| consequence of the rest of this License. | ||||
| 
 | ||||
| 8. If the distribution and/or use of the Program is restricted in certain | ||||
| countries either by patents or by copyrighted interfaces, the original copyright | ||||
| holder who places the Program under this License may add an explicit geographical | ||||
| distribution limitation excluding those countries, so that distribution is | ||||
| permitted only in or among countries not thus excluded. In such case, this | ||||
| License incorporates the limitation as if written in the body of this License. | ||||
| 
 | ||||
| 9. The Free Software Foundation may publish revised and/or new versions of | ||||
| the General Public License from time to time. Such new versions will be similar | ||||
| in spirit to the present version, but may differ in detail to address new | ||||
| problems or concerns. | ||||
| 
 | ||||
| Each version is given a distinguishing version number. If the Program specifies | ||||
| a version number of this License which applies to it and "any later version", | ||||
| you have the option of following the terms and conditions either of that version | ||||
| or of any later version published by the Free Software Foundation. If the | ||||
| Program does not specify a version number of this License, you may choose | ||||
| any version ever published by the Free Software Foundation. | ||||
| 
 | ||||
| 10. If you wish to incorporate parts of the Program into other free programs | ||||
| whose distribution conditions are different, write to the author to ask for | ||||
| permission. For software which is copyrighted by the Free Software Foundation, | ||||
| write to the Free Software Foundation; we sometimes make exceptions for this. | ||||
| Our decision will be guided by the two goals of preserving the free status | ||||
| of all derivatives of our free software and of promoting the sharing and reuse | ||||
| of software generally. | ||||
| 
 | ||||
|    NO WARRANTY | ||||
| 
 | ||||
| 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR | ||||
| THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE | ||||
| STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM | ||||
| "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, | ||||
| BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | ||||
| FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE | ||||
| OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME | ||||
| THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||||
| 
 | ||||
| 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE | ||||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE | ||||
| OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA | ||||
| OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES | ||||
| OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH | ||||
| HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. | ||||
| END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
| How to Apply These Terms to Your New Programs | ||||
| 
 | ||||
| If you develop a new program, and you want it to be of the greatest possible | ||||
| use to the public, the best way to achieve this is to make it free software | ||||
| which everyone can redistribute and change under these terms. | ||||
| 
 | ||||
| To do so, attach the following notices to the program. It is safest to attach | ||||
| them to the start of each source file to most effectively convey the exclusion | ||||
| of warranty; and each file should have at least the "copyright" line and a | ||||
| pointer to where the full notice is found. | ||||
| 
 | ||||
| < one line to give the program's name and an idea of what it does. > | ||||
| 
 | ||||
| Copyright (C) < yyyy > < name of author > | ||||
| 
 | ||||
| This program is free software; you can redistribute it and/or modify it under | ||||
| the terms of the GNU General Public License as published by the Free Software | ||||
| Foundation; either version 2 of the License, or (at your option) any later | ||||
| version. | ||||
| 
 | ||||
| This program is distributed in the hope that it will be useful, but WITHOUT | ||||
| ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | ||||
| FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. | ||||
| 
 | ||||
| You should have received a copy of the GNU General Public License along with | ||||
| this program; if not, write to the Free Software Foundation, Inc., 51 Franklin | ||||
| Street, Fifth Floor, Boston, MA 02110-1301 , USA. | ||||
| 
 | ||||
| Also add information on how to contact you by electronic and paper mail. | ||||
| 
 | ||||
| If the program is interactive, make it output a short notice like this when | ||||
| it starts in an interactive mode: | ||||
| 
 | ||||
| Gnomovision version 69, Copyright (C) year name of author Gnomovision comes | ||||
| with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, | ||||
| and you are welcome to redistribute it under certain conditions; type `show | ||||
| c' for details. | ||||
| 
 | ||||
| The hypothetical commands `show w' and `show c' should show the appropriate | ||||
| parts of the General Public License. Of course, the commands you use may be | ||||
| called something other than `show w' and `show c'; they could even be mouse-clicks | ||||
| or menu items--whatever suits your program. | ||||
| 
 | ||||
| You should also get your employer (if you work as a programmer) or your school, | ||||
| if any, to sign a "copyright disclaimer" for the program, if necessary. Here | ||||
| is a sample; alter the names: | ||||
| 
 | ||||
| Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' | ||||
| (which makes passes at compilers) written by James Hacker. | ||||
| 
 | ||||
| < signature of Ty Coon > , 1 April 1989 Ty Coon, President of Vice This General | ||||
| Public License does not permit incorporating your program into proprietary | ||||
| programs. If your program is a subroutine library, you may consider it more | ||||
| useful to permit linking proprietary applications with the library. If this | ||||
| is what you want to do, use the GNU Lesser General Public License instead | ||||
| of this License. | ||||
							
								
								
									
										14
									
								
								Makefile
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										14
									
								
								Makefile
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| BUILD=`date +%FT%T%z` | ||||
| 
 | ||||
| LDFLAGS=-ldflags "-X main.buildDate=${BUILD}" | ||||
| 
 | ||||
| .PHONY: build deps static | ||||
| 
 | ||||
| build: | ||||
| 	go build ${LDFLAGS} | ||||
| 
 | ||||
| deps: | ||||
| 	go get | ||||
| 
 | ||||
| static: | ||||
| 	CGO_ENABLED=0 GOOS=linux go build ${LDFLAGS} -a -installsuffix cgo -o twitchsingstools . | ||||
							
								
								
									
										9
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| # karaokards | ||||
| 
 | ||||
| Silly little twitch bot that gives prompts for songs to sing. | ||||
| 
 | ||||
| Almost a hackday style project.  Apologies for the mess. | ||||
| 
 | ||||
| Auto-built from my Drone CI and pushed to dockerhub.  | ||||
| [](https://ci.martyn.berlin/martyn/karaokards) | ||||
| 
 | ||||
							
								
								
									
										63
									
								
								build/ci/drone.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								build/ci/drone.yml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| kind: pipeline | ||||
| type: docker | ||||
| name: linux-amd64-taggedver | ||||
| 
 | ||||
| platform: | ||||
|   arch: amd64 | ||||
|   os: linux | ||||
| 
 | ||||
| steps: | ||||
| - name: build | ||||
|   image: golang | ||||
|   commands: | ||||
|   - pwd | ||||
|   - mkdir -p /go/src/git.martyn.berlin/martyn | ||||
|   - ln -s /drone/src /go/src/git.martyn.berlin/martyn/twitchsingstools | ||||
|   - cd /go/src/git.martyn.berlin/martyn/twitchsingstools | ||||
|   - go get | ||||
|   - go build | ||||
| 
 | ||||
| - name: publish | ||||
|   image: plugins/docker:18 | ||||
|   settings: | ||||
|     auto_tag: true | ||||
|     auto_tag_suffix: linux-amd64 | ||||
|     dockerfile: build/package/Dockerfile | ||||
|     repo: imartyn/karaokardbot | ||||
|     username: | ||||
|       from_secret: docker_username | ||||
|     password: | ||||
|       from_secret: docker_password | ||||
|     when: | ||||
|       event: | ||||
|       - push | ||||
|       - tag | ||||
| 
 | ||||
| trigger: | ||||
|   ref: | ||||
|   - refs/tags/v* | ||||
| 
 | ||||
| --- | ||||
| kind: pipeline | ||||
| type: docker | ||||
| name: linux-amd64-devel-master | ||||
| 
 | ||||
| platform: | ||||
|   arch: amd64 | ||||
|   os: linux | ||||
| 
 | ||||
| steps: | ||||
| - name: build | ||||
|   image: golang | ||||
|   commands: | ||||
|   - pwd | ||||
|   - mkdir -p /go/src/git.martyn.berlin/martyn | ||||
|   - ln -s /drone/src /go/src/git.martyn.berlin/martyn/twitchsingstools | ||||
|   - cd /go/src/git.martyn.berlin/martyn/twitchsingstools | ||||
|   - go get | ||||
|   - go build | ||||
| 
 | ||||
| trigger: | ||||
|   ref: | ||||
|   - refs/heads/devel | ||||
|   - refs/heads/master | ||||
							
								
								
									
										14
									
								
								build/package/Dockerfile
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										14
									
								
								build/package/Dockerfile
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| FROM golang@sha256:cee6f4b901543e8e3f20da3a4f7caac6ea643fd5a46201c3c2387183a332d989 AS builder | ||||
| RUN apk update && apk add --no-cache git make ca-certificates && update-ca-certificates | ||||
| COPY main.go /go/src/git.martyn.berlin/martyn/twitchsingstools/ | ||||
| COPY internal/ /go/src/git.martyn.berlin/martyn/twitchsingstools/internal/ | ||||
| COPY Makefile /go/src/git.martyn.berlin/martyn/twitchsingstools/ | ||||
| RUN cd /go/src/git.martyn.berlin/martyn/twitchsingstools/; make deps ; make static | ||||
| 
 | ||||
| FROM scratch | ||||
| COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ | ||||
| COPY --from=builder /go/src/git.martyn.berlin/martyn/twitchsingstools /app/ | ||||
| COPY strings.json /app/strings.json | ||||
| COPY web/ /app/web/ | ||||
| WORKDIR /app | ||||
| CMD ["/app/twitchsingstools"] | ||||
							
								
								
									
										4
									
								
								configs/config.json
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								configs/config.json
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| { | ||||
|     "channels": ["karaokards"], | ||||
|     "externalUrl": "karaokards.ing.martyn.berlin" | ||||
| } | ||||
							
								
								
									
										6
									
								
								data/channelData/imartynonbot.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								data/channelData/imartynonbot.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| { | ||||
| 	"name": "imartynonbot", | ||||
| 	"customcommand": "card", | ||||
| 	"jointime": "2020-02-22T14:20:44.372767247+01:00", | ||||
| 	"ControlChannel": true | ||||
| } | ||||
							
								
								
									
										7
									
								
								data/channelData/imartynontwitch.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								data/channelData/imartynontwitch.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| { | ||||
| 	"name": "imartynontwitch", | ||||
| 	"value": "MzdmY2MwYzEtN2FlMC00ZTQ0LThkZDUtZDFhZDljZWUwMjZj", | ||||
| 	"customcommand": "card", | ||||
| 	"extrastrings": "Make Pineboy Choose!", | ||||
| 	"jointime": "2020-02-22T13:54:06.31158195+01:00" | ||||
| } | ||||
							
								
								
									
										7
									
								
								data/channelData/karaokards.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								data/channelData/karaokards.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| { | ||||
| 	"name": "karaokards", | ||||
| 	"customcommand": "card", | ||||
| 	"jointime": "2020-05-16T13:25:55.339717754+02:00", | ||||
| 	"ControlChannel": false, | ||||
| 	"hasleft": false | ||||
| } | ||||
							
								
								
									
										1
									
								
								data/prompts/common.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								data/prompts/common.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| null | ||||
							
								
								
									
										23
									
								
								deployments/helm/twitchsingstools/.helmignore
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								deployments/helm/twitchsingstools/.helmignore
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| # Patterns to ignore when building packages. | ||||
| # This supports shell glob matching, relative path matching, and | ||||
| # negation (prefixed with !). Only one pattern per line. | ||||
| .DS_Store | ||||
| # Common VCS dirs | ||||
| .git/ | ||||
| .gitignore | ||||
| .bzr/ | ||||
| .bzrignore | ||||
| .hg/ | ||||
| .hgignore | ||||
| .svn/ | ||||
| # Common backup files | ||||
| *.swp | ||||
| *.bak | ||||
| *.tmp | ||||
| *.orig | ||||
| *~ | ||||
| # Various IDEs | ||||
| .project | ||||
| .idea/ | ||||
| *.tmproj | ||||
| .vscode/ | ||||
							
								
								
									
										21
									
								
								deployments/helm/twitchsingstools/Chart.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								deployments/helm/twitchsingstools/Chart.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| apiVersion: v2 | ||||
| name: twitchsingstools | ||||
| description: A Helm chart for Kubernetes | ||||
| 
 | ||||
| # A chart can be either an 'application' or a 'library' chart. | ||||
| # | ||||
| # Application charts are a collection of templates that can be packaged into versioned archives | ||||
| # to be deployed. | ||||
| # | ||||
| # Library charts provide useful utilities or functions for the chart developer. They're included as | ||||
| # a dependency of application charts to inject those utilities and functions into the rendering | ||||
| # pipeline. Library charts do not define any templates and therefore cannot be deployed. | ||||
| type: application | ||||
| 
 | ||||
| # This is the chart version. This version number should be incremented each time you make changes | ||||
| # to the chart and its templates, including the app version. | ||||
| version: 0.1.0 | ||||
| 
 | ||||
| # This is the version number of the application being deployed. This version number should be | ||||
| # incremented each time you make changes to the application. | ||||
| appVersion: 1.16.0 | ||||
							
								
								
									
										21
									
								
								deployments/helm/twitchsingstools/templates/NOTES.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								deployments/helm/twitchsingstools/templates/NOTES.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| 1. Get the application URL by running these commands: | ||||
| {{- if .Values.ingress.enabled }} | ||||
| {{- range $host := .Values.ingress.hosts }} | ||||
|   {{- range .paths }} | ||||
|   http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} | ||||
|   {{- end }} | ||||
| {{- end }} | ||||
| {{- else if contains "NodePort" .Values.service.type }} | ||||
|   export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "twitchsingstools.fullname" . }}) | ||||
|   export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") | ||||
|   echo http://$NODE_IP:$NODE_PORT | ||||
| {{- else if contains "LoadBalancer" .Values.service.type }} | ||||
|      NOTE: It may take a few minutes for the LoadBalancer IP to be available. | ||||
|            You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "twitchsingstools.fullname" . }}' | ||||
|   export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "twitchsingstools.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") | ||||
|   echo http://$SERVICE_IP:{{ .Values.service.port }} | ||||
| {{- else if contains "ClusterIP" .Values.service.type }} | ||||
|   export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "twitchsingstools.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") | ||||
|   echo "Visit http://127.0.0.1:8080 to use your application" | ||||
|   kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 | ||||
| {{- end }} | ||||
							
								
								
									
										63
									
								
								deployments/helm/twitchsingstools/templates/_helpers.tpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								deployments/helm/twitchsingstools/templates/_helpers.tpl
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| {{/* vim: set filetype=mustache: */}} | ||||
| {{/* | ||||
| Expand the name of the chart. | ||||
| */}} | ||||
| {{- define "twitchsingstools.name" -}} | ||||
| {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} | ||||
| {{- end -}} | ||||
| 
 | ||||
| {{/* | ||||
| Create a default fully qualified app name. | ||||
| We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). | ||||
| If release name contains chart name it will be used as a full name. | ||||
| */}} | ||||
| {{- define "twitchsingstools.fullname" -}} | ||||
| {{- if .Values.fullnameOverride -}} | ||||
| {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} | ||||
| {{- else -}} | ||||
| {{- $name := default .Chart.Name .Values.nameOverride -}} | ||||
| {{- if contains $name .Release.Name -}} | ||||
| {{- .Release.Name | trunc 63 | trimSuffix "-" -}} | ||||
| {{- else -}} | ||||
| {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} | ||||
| {{- end -}} | ||||
| {{- end -}} | ||||
| {{- end -}} | ||||
| 
 | ||||
| {{/* | ||||
| Create chart name and version as used by the chart label. | ||||
| */}} | ||||
| {{- define "twitchsingstools.chart" -}} | ||||
| {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} | ||||
| {{- end -}} | ||||
| 
 | ||||
| {{/* | ||||
| Common labels | ||||
| */}} | ||||
| {{- define "twitchsingstools.labels" -}} | ||||
| helm.sh/chart: {{ include "twitchsingstools.chart" . }} | ||||
| {{ include "twitchsingstools.selectorLabels" . }} | ||||
| {{- if .Chart.AppVersion }} | ||||
| app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} | ||||
| {{- end }} | ||||
| app.kubernetes.io/managed-by: {{ .Release.Service }} | ||||
| {{- end -}} | ||||
| 
 | ||||
| {{/* | ||||
| Selector labels | ||||
| */}} | ||||
| {{- define "twitchsingstools.selectorLabels" -}} | ||||
| app.kubernetes.io/name: {{ include "twitchsingstools.name" . }} | ||||
| app.kubernetes.io/instance: {{ .Release.Name }} | ||||
| {{- end -}} | ||||
| 
 | ||||
| {{/* | ||||
| Create the name of the service account to use | ||||
| */}} | ||||
| {{- define "twitchsingstools.serviceAccountName" -}} | ||||
| {{- if .Values.serviceAccount.create -}} | ||||
|     {{ default (include "twitchsingstools.fullname" .) .Values.serviceAccount.name }} | ||||
| {{- else -}} | ||||
|     {{ default "default" .Values.serviceAccount.name }} | ||||
| {{- end -}} | ||||
| {{- end -}} | ||||
|  | @ -0,0 +1,8 @@ | |||
| apiVersion: v1 | ||||
| kind: ConfigMap | ||||
| metadata: | ||||
|   name: tstoolsconfig | ||||
|   labels: | ||||
|     {{- include "twitchsingstools.labels" . | nindent 4 }} | ||||
| data: | ||||
|   config.json: {{ printf "{ \"externalUrl\": \"%s\" }" .Values.externalHostname | quote }} | ||||
							
								
								
									
										78
									
								
								deployments/helm/twitchsingstools/templates/deployment.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								deployments/helm/twitchsingstools/templates/deployment.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | |||
| apiVersion: apps/v1 | ||||
| kind: Deployment | ||||
| metadata: | ||||
|   name: {{ include "twitchsingstools.fullname" . }} | ||||
|   labels: | ||||
|     {{- include "twitchsingstools.labels" . | nindent 4 }} | ||||
| spec: | ||||
|   replicas: {{ .Values.replicaCount }} | ||||
|   selector: | ||||
|     matchLabels: | ||||
|       {{- include "twitchsingstools.selectorLabels" . | nindent 6 }} | ||||
|   template: | ||||
|     metadata: | ||||
|       labels: | ||||
|         {{- include "twitchsingstools.selectorLabels" . | nindent 8 }} | ||||
|     spec: | ||||
|     {{- with .Values.imagePullSecrets }} | ||||
|       imagePullSecrets: | ||||
|         {{- toYaml . | nindent 8 }} | ||||
|     {{- end }} | ||||
|       serviceAccountName: {{ include "twitchsingstools.serviceAccountName" . }} | ||||
|       securityContext: | ||||
|         {{- toYaml .Values.podSecurityContext | nindent 8 }} | ||||
|       containers: | ||||
|         - name: {{ .Chart.Name }} | ||||
|           securityContext: | ||||
|             {{- toYaml .Values.securityContext | nindent 12 }} | ||||
|           image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" | ||||
|           imagePullPolicy: {{ .Values.image.pullPolicy }} | ||||
|           ports: | ||||
|             - name: http | ||||
|               containerPort: 80 | ||||
|               protocol: TCP | ||||
|           livenessProbe: | ||||
|             httpGet: | ||||
|               path: / | ||||
|               port: http | ||||
|           readinessProbe: | ||||
|             httpGet: | ||||
|               path: / | ||||
|               port: http | ||||
|           resources: | ||||
|             {{- toYaml .Values.resources | nindent 12 }} | ||||
|           volumeMounts: | ||||
|           - mountPath: /etc/tstools/ | ||||
|             name: oauth | ||||
|           - mountPath: /app/config.json | ||||
|             name: config | ||||
|             subPath: config.json | ||||
|           - mountPath: /data | ||||
|             name: data | ||||
|       volumes: | ||||
|       - name: oauth | ||||
|         secret: | ||||
|           defaultMode: 420 | ||||
|           secretName: twitchoauth | ||||
|       - name: config | ||||
|         configMap: | ||||
|           defaultMode: 420 | ||||
|           items: | ||||
|           - key: config.json | ||||
|             path: config.json | ||||
|           name: tstoolsconfig | ||||
|       - name: data | ||||
|         persistentVolumeClaim:  | ||||
|           claimName: tstools-data | ||||
|       {{- with .Values.nodeSelector }} | ||||
|       nodeSelector: | ||||
|         {{- toYaml . | nindent 8 }} | ||||
|       {{- end }} | ||||
|     {{- with .Values.affinity }} | ||||
|       affinity: | ||||
|         {{- toYaml . | nindent 8 }} | ||||
|     {{- end }} | ||||
|     {{- with .Values.tolerations }} | ||||
|       tolerations: | ||||
|         {{- toYaml . | nindent 8 }} | ||||
|     {{- end }} | ||||
							
								
								
									
										41
									
								
								deployments/helm/twitchsingstools/templates/ingress.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								deployments/helm/twitchsingstools/templates/ingress.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | |||
| {{- if .Values.ingress.enabled -}} | ||||
| {{- $fullName := include "twitchsingstools.fullname" . -}} | ||||
| {{- $svcPort := .Values.service.port -}} | ||||
| {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} | ||||
| apiVersion: networking.k8s.io/v1beta1 | ||||
| {{- else -}} | ||||
| apiVersion: extensions/v1beta1 | ||||
| {{- end }} | ||||
| kind: Ingress | ||||
| metadata: | ||||
|   name: {{ $fullName }} | ||||
|   labels: | ||||
|     {{- include "twitchsingstools.labels" . | nindent 4 }} | ||||
|   {{- with .Values.ingress.annotations }} | ||||
|   annotations: | ||||
|     {{- toYaml . | nindent 4 }} | ||||
|   {{- end }} | ||||
| spec: | ||||
| {{- if .Values.ingress.tls }} | ||||
|   tls: | ||||
|   {{- range .Values.ingress.tls }} | ||||
|     - hosts: | ||||
|       {{- range .hosts }} | ||||
|         - {{ . | quote }} | ||||
|       {{- end }} | ||||
|       secretName: {{ .secretName }} | ||||
|   {{- end }} | ||||
| {{- end }} | ||||
|   rules: | ||||
|   {{- range .Values.ingress.hosts }} | ||||
|     - host: {{ .host | quote }} | ||||
|       http: | ||||
|         paths: | ||||
|         {{- range .paths }} | ||||
|           - path: {{ . }} | ||||
|             backend: | ||||
|               serviceName: {{ $fullName }} | ||||
|               servicePort: {{ $svcPort }} | ||||
|         {{- end }} | ||||
|   {{- end }} | ||||
| {{- end }} | ||||
							
								
								
									
										12
									
								
								deployments/helm/twitchsingstools/templates/pvc.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								deployments/helm/twitchsingstools/templates/pvc.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| {{- if .Values.serviceAccount.create -}} | ||||
| apiVersion: v1 | ||||
| kind: ServiceAccount | ||||
| metadata: | ||||
|   name: {{ include "twitchsingstools.serviceAccountName" . }} | ||||
|   labels: | ||||
|     {{- include "twitchsingstools.labels" . | nindent 4 }} | ||||
|   {{- with .Values.serviceAccount.annotations }} | ||||
|   annotations: | ||||
|     {{- toYaml . | nindent 4 }} | ||||
|   {{- end }} | ||||
| {{- end -}} | ||||
							
								
								
									
										13
									
								
								deployments/helm/twitchsingstools/templates/secret.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								deployments/helm/twitchsingstools/templates/secret.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| apiVersion: v1 | ||||
| kind: Secret | ||||
| metadata: | ||||
|   name: twitchoauth | ||||
|   labels: | ||||
|     {{- include "twitchsingstools.labels" . | nindent 4 }} | ||||
| type: Opaque | ||||
| data: | ||||
|   ircoauth.json: {{ printf "{ \"nick\": \"%s\", \"password\": \"%s\" }" .Values.irc.nick .Values.irc.password | b64enc | quote }}  | ||||
|   appoauth.json: {{ printf "{ \"client_secret\": \"%s\", \"client_id\": \"%s\" }" .Values.twitchapp.client_secret .Values.twitchapp.client_id | b64enc | quote }}  | ||||
|   {{- range $key, $value := .Values.secretFiles }} | ||||
|   {{ $key }} : {{ $value | b64enc | quote }} | ||||
|   {{- end }} | ||||
							
								
								
									
										15
									
								
								deployments/helm/twitchsingstools/templates/service.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								deployments/helm/twitchsingstools/templates/service.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| apiVersion: v1 | ||||
| kind: Service | ||||
| metadata: | ||||
|   name: {{ include "twitchsingstools.fullname" . }} | ||||
|   labels: | ||||
|     {{- include "twitchsingstools.labels" . | nindent 4 }} | ||||
| spec: | ||||
|   type: {{ .Values.service.type }} | ||||
|   ports: | ||||
|     - port: {{ .Values.service.port }} | ||||
|       targetPort: http | ||||
|       protocol: TCP | ||||
|       name: http | ||||
|   selector: | ||||
|     {{- include "twitchsingstools.selectorLabels" . | nindent 4 }} | ||||
|  | @ -0,0 +1,12 @@ | |||
| {{- if .Values.serviceAccount.create -}} | ||||
| apiVersion: v1 | ||||
| kind: ServiceAccount | ||||
| metadata: | ||||
|   name: {{ include "twitchsingstools.serviceAccountName" . }} | ||||
|   labels: | ||||
|     {{- include "twitchsingstools.labels" . | nindent 4 }} | ||||
|   {{- with .Values.serviceAccount.annotations }} | ||||
|   annotations: | ||||
|     {{- toYaml . | nindent 4 }} | ||||
|   {{- end }} | ||||
| {{- end -}} | ||||
|  | @ -0,0 +1,15 @@ | |||
| apiVersion: v1 | ||||
| kind: Pod | ||||
| metadata: | ||||
|   name: "{{ include "twitchsingstools.fullname" . }}-test-connection" | ||||
|   labels: | ||||
|     {{- include "twitchsingstools.labels" . | nindent 4 }} | ||||
|   annotations: | ||||
|     "helm.sh/hook": test-success | ||||
| spec: | ||||
|   containers: | ||||
|     - name: wget | ||||
|       image: busybox | ||||
|       command: ['wget'] | ||||
|       args: ['{{ include "twitchsingstools.fullname" . }}:{{ .Values.service.port }}'] | ||||
|   restartPolicy: Never | ||||
							
								
								
									
										32
									
								
								deployments/helm/twitchsingstools/values.secret.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								deployments/helm/twitchsingstools/values.secret.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| irc: | ||||
|     password: ENC[AES256_GCM,data:bmVyAcxlCKaL8MkttQWipktXkoU637pSRuEZHETvkFBdRX4p,iv:NSZ6TtlXcxy7cUtKZ1MrB4hAX4FwNV5LuPNViFd0jh0=,tag:4Ex7p59w2FFkFkp4yEhZ0w==,type:str] | ||||
| twitchapp: | ||||
|     client_id: ENC[AES256_GCM,data:Iy1Wb5WevXQtDpDPTKKkpYez+WWOGOUDre89qf1Y,iv:z/ZbcgADO5vDQOjX/A6xU99lGiD2k3qxor31KnVFE4o=,tag:pgf67HdxOjUSWEktgPfolw==,type:str] | ||||
|     client_secret: ENC[AES256_GCM,data:5fY77ccyfqZMh30hMdhXy1h7fio4763NLgYu9tgP,iv:kLyEULWcgKHwpPxoBX69IcHAaeAob9AN8gD5/K5nIyg=,tag:nxtZMuCZdGyVhBBvgRz2SA==,type:str] | ||||
| sops: | ||||
|     kms: [] | ||||
|     gcp_kms: [] | ||||
|     azure_kv: [] | ||||
|     lastmodified: '2020-07-14T18:03:02Z' | ||||
|     mac: ENC[AES256_GCM,data:quUrWcjviC08daW0k+Y0a6oB+ZXT8NkF2z1cni2DeFUuSXjrxDQcMY3POj45DbzHrojXMkJKOk6JLOcj9oV1h8uCqyZ/rGWhgfly1YNLJEP8LcJbCQt2jUAhctxaQkvPtqIak7e/ABKHhMuKmBbNWzhTLBbyl2/YR2+pPsLxKQA=,iv:PbRPKBaLtns4+M0ydrVTecVHgtR3q7TkhKWN01cEhVE=,tag:nCK7gSQ+F97lJJhCOol7zw==,type:str] | ||||
|     pgp: | ||||
|     -   created_at: '2020-07-14T18:02:04Z' | ||||
|         enc: | | ||||
|             -----BEGIN PGP MESSAGE----- | ||||
| 
 | ||||
|             hQGMA/w0sxTgx97RAQv+IVeVm2yGUv4++uJ5BEy7fZQLHsi/k6C8XxVX9rm9WjSa | ||||
|             pPPovDHJUJ0BDigtWRvKsQLh8ahIv3I5hQzFl2oQNjOSQmMyK6yr5XGWhyFtKRUu | ||||
|             SSQrnPh5NJgZXD3cnG1/jMTf5Hbdj4CFM/TZuc0tW9oot5iNpL5u8jXQhfQ+ckj1 | ||||
|             CtHB65H/TEjlEe/I8MB+ijqbnmSRyaDZKrdx97xbJZbk0HLqwCXIFOhSd182TjXd | ||||
|             v+qrXtHB790pUuJNcZtyfwOfJldhwBXmHW4As4EllZ4d2WW5g13undMyE1qg4dBj | ||||
|             EguI9Vze/hAWf48X4IRmJTNXECQkz3+MutINQIPBpCgZtdz8AJghwVPSHvS2G0de | ||||
|             esBULkV1C48nj2ndISk4OsNtCwU7PfNdQzySckBtBKhd+av3EvFJqPDTJGnkGwD6 | ||||
|             9dlhVD5nL3ZwbzM8Y7LwAg5l0ID1ITvUW/T9Ngqnxz8cirJDDawyS/TQll5gZIrV | ||||
|             lsEE65wHrf5J3bm4CY2D0l4B3tySeDQ0daZQWMUKiloTlNpffDcmAdNUBLH+IPYa | ||||
|             LRgRzqCZWJgojwrub2lCT03Ksw7SLFn1b77rxqMnqjP4w0sPq04iGz5U6L06oJ32 | ||||
|             clkhMGk6hdaCW+fk+VBa | ||||
|             =OesV | ||||
|             -----END PGP MESSAGE----- | ||||
|         fp: 96D854EEBF3057E6A059C2D3091E073506551185 | ||||
|     unencrypted_suffix: _unencrypted | ||||
|     version: 3.5.0 | ||||
							
								
								
									
										76
									
								
								deployments/helm/twitchsingstools/values.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								deployments/helm/twitchsingstools/values.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,76 @@ | |||
| # Default values for twitchsingstools. | ||||
| # This is a YAML-formatted file. | ||||
| # Declare variables to be passed into your templates. | ||||
| 
 | ||||
| replicaCount: 1 | ||||
| 
 | ||||
| image: | ||||
|   repository: twitchsingstools | ||||
|   pullPolicy: IfNotPresent | ||||
| 
 | ||||
| imagePullSecrets: [] | ||||
| nameOverride: "" | ||||
| fullnameOverride: "" | ||||
| 
 | ||||
| serviceAccount: | ||||
|   # Specifies whether a service account should be created | ||||
|   create: true | ||||
|   # Annotations to add to the service account | ||||
|   annotations: {} | ||||
|   # The name of the service account to use. | ||||
|   # If not set and create is true, a name is generated using the fullname template | ||||
|   name: | ||||
| 
 | ||||
| podSecurityContext: {} | ||||
|   # fsGroup: 2000 | ||||
| 
 | ||||
| securityContext: {} | ||||
|   # capabilities: | ||||
|   #   drop: | ||||
|   #   - ALL | ||||
|   # readOnlyRootFilesystem: true | ||||
|   # runAsNonRoot: true | ||||
|   # runAsUser: 1000 | ||||
| 
 | ||||
| secretFiles: {} | ||||
| 
 | ||||
| irc: | ||||
|   nick: twitchsingstools | ||||
| 
 | ||||
| twitchapp: {} | ||||
| 
 | ||||
| service: | ||||
|   type: ClusterIP | ||||
|   port: 80 | ||||
| 
 | ||||
| externalHostname: twitchsingstools.ing.martyn.berlin | ||||
| 
 | ||||
| ingress: | ||||
|   enabled: true | ||||
|   annotations:  | ||||
|     kubernetes.io/ingress.class: nginx | ||||
|   hosts: | ||||
|     - host: twitchsingstools.ing.martyn.berlin | ||||
|       paths: [] | ||||
|   tls:  | ||||
|     - secretName: tstools-tls | ||||
|       hosts: | ||||
|         - twitchsingstools.ing.martyn.berlin | ||||
| 
 | ||||
| resources: {} | ||||
|   # We usually recommend not to specify default resources and to leave this as a conscious | ||||
|   # choice for the user. This also increases chances charts run on environments with little | ||||
|   # resources, such as Minikube. If you do want to specify resources, uncomment the following | ||||
|   # lines, adjust them as necessary, and remove the curly braces after 'resources:'. | ||||
|   # limits: | ||||
|   #   cpu: 100m | ||||
|   #   memory: 128Mi | ||||
|   # requests: | ||||
|   #   cpu: 100m | ||||
|   #   memory: 128Mi | ||||
| 
 | ||||
| nodeSelector: {} | ||||
| 
 | ||||
| tolerations: [] | ||||
| 
 | ||||
| affinity: {} | ||||
							
								
								
									
										181
									
								
								internal/builtins/kards.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								internal/builtins/kards.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,181 @@ | |||
| package builtins | ||||
| 
 | ||||
| var Karaokards = [...]string{ | ||||
| 	"Chorus contains a name", | ||||
| 	"Refers to an animal", | ||||
| 	"Aggressive", | ||||
| 	"Contains instructions", | ||||
| 	"Refers to relationships", | ||||
| 	"Folk", | ||||
| 	"Novelty", | ||||
| 	"Pre-sixties", | ||||
| 	"1960s", | ||||
| 	"Ballad", | ||||
| 	"Musical", | ||||
| 	"Heartbroken", | ||||
| 	"Contains made-up words", | ||||
| 	"Refers to a colour", | ||||
| 	"Chorus contains me, mine or my", | ||||
| 	"Chorus contains want, need or have", | ||||
| 	"Chorus contains how, when or why", | ||||
| 	"Refers to money", | ||||
| 	"Chorus contains love, like or hate", | ||||
| 	"Chorus contains man, woman or everybody", | ||||
| 	"Anthemic", | ||||
| 	"Refers to explosives", | ||||
| 	"Chorus contains a number", | ||||
| 	"1970s", | ||||
| 	"2000s", | ||||
| 	"Love Song", | ||||
| 	"Country-pop", | ||||
| 	"Grunge", | ||||
| 	"Indie", | ||||
| 	"This is a morality tale", | ||||
| 	"I've never sung this before", | ||||
| 	"They'd beat me in a fight", | ||||
| 	"My mortal enemy", | ||||
| 	"Their name starts with the same letter as mine", | ||||
| 	"This is NOT my era", | ||||
| 	"Chorus contains oh, ooh or baby", | ||||
| 	"Samples another song", | ||||
| 	"Chorus contains don't, won't or can't", | ||||
| 	"Euphoric", | ||||
| 	"Title contains day, night or tomorrow", | ||||
| 	"Chorus contains boy, girl or child", | ||||
| 	"Refers to space", | ||||
| 	"Soul", | ||||
| 	"R&B", | ||||
| 	"Dance", | ||||
| 	"Electronie", | ||||
| 	"Pop", | ||||
| 	"1990s", | ||||
| 	"Rock", | ||||
| 	"Title contains brackets", | ||||
| 	"Refers to religion", | ||||
| 	"Refers to music", | ||||
| 	"Chorus contains this, that or there", | ||||
| 	"Chorus contains up, down or over", | ||||
| 	"Beautiful", | ||||
| 	"Mean about someone", | ||||
| 	"Refers to weather", | ||||
| 	"Chorus contains you, your or you're", | ||||
| 	"Gloomy", | ||||
| 	"Contains questions", | ||||
| 	"Refers to death", | ||||
| 	"Refers to sleep", | ||||
| 	"Chorus contains heart, head or soul", | ||||
| 	"Rock", | ||||
| 	"Pop", | ||||
| 	"Country", | ||||
| 	"Punk", | ||||
| 	"Rap", | ||||
| 	"Christmas", | ||||
| 	"Motown", | ||||
| 	"This is their Second-best song", | ||||
| 	"I shouldn't know this song. But I do!", | ||||
| 	"I don't need the screen!", | ||||
| 	"I'm too old for this song", | ||||
| 	"The story of my life", | ||||
| 	"I love this song SO much", | ||||
| 	"They're twice my age!", | ||||
| 	"Chorus contains I, I’m or I’ve", | ||||
| 	"Title is one word long", | ||||
| 	"Soundtrack", | ||||
| 	"Pop", | ||||
| 	"Is a metaphor", | ||||
| 	"Title is at least five words long", | ||||
| 	"Chorus contains move, stay or go", | ||||
| 	"Refers to a place", | ||||
| 	"R&B", | ||||
| 	"Metal", | ||||
| 	"Good workout music", | ||||
| 	"Requires audience participation", | ||||
| 	"Power Ballad", | ||||
| 	"1980s", | ||||
| 	"Rock", | ||||
| 	"Rap", | ||||
| 	"Pop-punk", | ||||
| 	"Alternative", | ||||
| 	"Soul", | ||||
| 	"My secret shame", | ||||
| 	"This gets me a bit emotional", | ||||
| 	"Out of my range", | ||||
| 	"This person is bad at their job", | ||||
| 	"They look like someone here", | ||||
| 	"I don't know the verse", | ||||
| 	"They're half my age!", | ||||
| 	"Play this at my funeral", | ||||
| 	"This is NOT my genre", | ||||
| 	"I hate this song so much", | ||||
| 	"Girl band", | ||||
| 	"Male solo", | ||||
| 	"Artist begins with A", | ||||
| 	"Just one word", | ||||
| 	"Mixed gender band", | ||||
| 	"Artist begins with C", | ||||
| 	"Artist begins with G", | ||||
| 	"Two people", | ||||
| 	"TV contestant", | ||||
| 	"European", | ||||
| 	"Artist begins with P", | ||||
| 	"Artist begins with M", | ||||
| 	"In a famous family", | ||||
| 	"Male-fronted band", | ||||
| 	"Australian", | ||||
| 	"One-hit wonder", | ||||
| 	"Female-fronted band", | ||||
| 	"Artist begins with T", | ||||
| 	"Rock", | ||||
| 	"Indie-rock", | ||||
| 	"R&B", | ||||
| 	"Latin", | ||||
| 	"Disco", | ||||
| 	"Britpop", | ||||
| 	"2010s", | ||||
| 	"I was a teenager!", | ||||
| 	"The music video is so good", | ||||
| 	"I’m younger than this song", | ||||
| 	"First dance at my imaginary wedding", | ||||
| 	"I'm worried about them", | ||||
| 	"This person is the best dancer", | ||||
| 	"I know the dance", | ||||
| 	"Mostly shouting", | ||||
| 	"They're not my gender", | ||||
| 	"I wish we were married", | ||||
| 	"I played this song too often", | ||||
| 	"They are so influential", | ||||
| 	"My favourite song of theirs", | ||||
| 	"Perfect montage music", | ||||
| 	"Artist uses their surname", | ||||
| 	"Famous partner", | ||||
| 	"Artist begins with E", | ||||
| 	"Troubled artist", | ||||
| 	"Actor", | ||||
| 	"Artist begins with F", | ||||
| 	"Solo artist", | ||||
| 	"Artist begins with R", | ||||
| 	"Hellraiser", | ||||
| 	"Artist begins with ‘The’", | ||||
| 	"10+ year career", | ||||
| 	"Boy band", | ||||
| 	"Female solo", | ||||
| 	"British", | ||||
| 	"Inappropriately clothed", | ||||
| 	"Award winners", | ||||
| 	"Artist begins with S", | ||||
| 	"Artist begins with W", | ||||
| 	"Asian", | ||||
| 	"They split up :(", | ||||
| 	"North American", | ||||
| 	"Pop", | ||||
| 	"Goth or Emo", | ||||
| 	"This is a bit creepy, frankly", | ||||
| 	"I’ve seen them in real life", | ||||
| 	"A beautiful love story", | ||||
| 	"OK, this is just ridiculous", | ||||
| 	"Artist begins with B", | ||||
| 	"Artist begins with D", | ||||
| 	"They’re dead :(", | ||||
| 	"Artist begins with L", | ||||
| 	"Amazing hair", | ||||
| 	"Artist begins with J"} | ||||
							
								
								
									
										492
									
								
								internal/irc/irc.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										492
									
								
								internal/irc/irc.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,492 @@ | |||
| package irc | ||||
| 
 | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"math/rand" | ||||
| 	"net" | ||||
| 	"net/textproto" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	rgb "github.com/foresthoffman/rgblog" | ||||
| 	uuid "github.com/google/uuid" | ||||
| 	scribble "github.com/nanobox-io/golang-scribble" | ||||
| ) | ||||
| 
 | ||||
| const UTCFormat = "Jan 2 15:04:05 UTC" | ||||
| 
 | ||||
| // Regex for parsing connection messages | ||||
| // | ||||
| // First matched group is our real username - twitch doesn't complain at using NICK command but doesn't honor it. | ||||
| var ConnectRegex *regexp.Regexp = regexp.MustCompile(`^:tmi.twitch.tv 001 ([^ ]+) .*`) | ||||
| 
 | ||||
| // Regex for parsing PRIVMSG strings. | ||||
| // | ||||
| // First matched group is the user's name, second is the channel? and the third matched group is the content of the | ||||
| // user's message. | ||||
| var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #(\w+)(?: :(.*))?$`) | ||||
| var DirectMsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) (\w+)(?: :(.*))?$`) | ||||
| 
 | ||||
| // Regex for parsing user commands, from already parsed PRIVMSG strings. | ||||
| // | ||||
| // First matched group is the command name and the second matched group is the argument for the | ||||
| // command. | ||||
| var CmdRegex *regexp.Regexp = regexp.MustCompile(`^!(\w+)\s?(\w+)?`) | ||||
| 
 | ||||
| type OAuthCred struct { | ||||
| 
 | ||||
| 	// The bot account's OAuth password. | ||||
| 	Password string `json:"password,omitempty"` | ||||
| 
 | ||||
| 	// The developer application client ID. Used for API calls to Twitch. | ||||
| 	ClientID string `json:"client_id,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type ConfigStruct struct { | ||||
| 	InitialChannels []string `json:"channels"` | ||||
| 	IrcOAuthPath    string   `json:"ircoauthpath,omitempty"` | ||||
| 	StringPath      string   `json:"authpath,omitempty"` | ||||
| 	DataPath        string   `json:"datapath,omitempty"` | ||||
| 	ExternalUrl     string   `json:"externalurl,omitempty"` | ||||
| 	AppOAuthPath    string   `json:"appoauthpath,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type KardBot struct { | ||||
| 	Channel     string | ||||
| 	conn        net.Conn | ||||
| 	IrcCredentials *OAuthCred | ||||
| 	AppCredentials *OAuthCred | ||||
| 	MsgRate     time.Duration | ||||
| 	Name        string | ||||
| 	Port        string | ||||
| 	IrcPrivatePath string | ||||
| 	AppPrivatePath string | ||||
| 	Server      string | ||||
| 	startTime   time.Time | ||||
| 	Prompts     []string | ||||
| 	Database    scribble.Driver | ||||
| 	ChannelData map[string]ChannelData | ||||
| 	Config      ConfigStruct | ||||
| } | ||||
| 
 | ||||
| type ChannelData struct { | ||||
| 	Name           string    `json:"name"` | ||||
| 	AdminKey       string    `json:"value,omitempty"` | ||||
| 	Command        string    `json:"customcommand,omitempty"` | ||||
| 	ExtraStrings   string    `json:"extrastrings,omitempty"` | ||||
| 	JoinTime       time.Time `json:"jointime"` | ||||
| 	ControlChannel bool | ||||
| 	HasLeft        bool      `json:"hasleft"` | ||||
| } | ||||
| 
 | ||||
| // Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it | ||||
| // succeeds or is forcefully shutdown. | ||||
| func (bb *KardBot) Connect() { | ||||
| 	var err error | ||||
| 	rgb.YPrintf("[%s] Connecting to %s...\n", TimeStamp(), bb.Server) | ||||
| 
 | ||||
| 	// makes connection to Twitch IRC server | ||||
| 	bb.conn, err = net.Dial("tcp", bb.Server+":"+bb.Port) | ||||
| 	if nil != err { | ||||
| 		rgb.YPrintf("[%s] Cannot connect to %s, retrying.\n", TimeStamp(), bb.Server) | ||||
| 		bb.Connect() | ||||
| 		return | ||||
| 	} | ||||
| 	rgb.YPrintf("[%s] Connected to %s!\n", TimeStamp(), bb.Server) | ||||
| 	bb.startTime = time.Now() | ||||
| } | ||||
| 
 | ||||
| // Officially disconnects the bot from the Twitch IRC server. | ||||
| func (bb *KardBot) Disconnect() { | ||||
| 	bb.conn.Close() | ||||
| 	upTime := time.Now().Sub(bb.startTime).Seconds() | ||||
| 	rgb.YPrintf("[%s] Closed connection from %s! | Live for: %fs\n", TimeStamp(), bb.Server, upTime) | ||||
| } | ||||
| 
 | ||||
| // Look at the channels I'm actually in | ||||
| func (bb *KardBot) ActiveChannels() int { | ||||
| 	count := 0 | ||||
| 	for _, channel := range bb.ChannelData { | ||||
| 		if !channel.HasLeft { | ||||
| 			count = count + 1 | ||||
| 		} | ||||
| 	} | ||||
| 	return count | ||||
| } | ||||
| 
 | ||||
| // Listens for and logs messages from chat. Responds to commands from the channel owner. The bot | ||||
| // continues until it gets disconnected, told to shutdown, or forcefully shutdown. | ||||
| func (bb *KardBot) HandleChat() error { | ||||
| 	rgb.YPrintf("[%s] Watching #%s...\n", TimeStamp(), bb.Channel) | ||||
| 
 | ||||
| 	// reads from connection | ||||
| 	tp := textproto.NewReader(bufio.NewReader(bb.conn)) | ||||
| 
 | ||||
| 	// listens for chat messages | ||||
| 	for { | ||||
| 		line, err := tp.ReadLine() | ||||
| 		if nil != err { | ||||
| 
 | ||||
| 			// officially disconnects the bot from the server | ||||
| 			bb.Disconnect() | ||||
| 
 | ||||
| 			return errors.New("bb.Bot.HandleChat: Failed to read line from channel. Disconnected.") | ||||
| 		} | ||||
| 
 | ||||
| 		// logs the response from the IRC server | ||||
| 		rgb.YPrintf("[%s] %s\n", TimeStamp(), line) | ||||
| 
 | ||||
| 		if "PING :tmi.twitch.tv" == line { | ||||
| 
 | ||||
| 			// respond to PING message with a PONG message, to maintain the connection | ||||
| 			bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n")) | ||||
| 			continue | ||||
| 		} else { | ||||
| 			matches := ConnectRegex.FindStringSubmatch(line) | ||||
| 			if nil != matches { | ||||
| 				realUserName := matches[1] | ||||
| 				if bb.ChannelData[realUserName].Name == "" { | ||||
| 					record := ChannelData{Name: realUserName, JoinTime: time.Now(), Command: "card", ControlChannel: true} | ||||
| 					bb.Database.Write("channelData", realUserName, record) | ||||
| 					bb.ChannelData[realUserName] = record | ||||
| 				} | ||||
| 				bb.JoinChannel(realUserName) | ||||
| 			} | ||||
| 
 | ||||
| 			matches = DirectMsgRegex.FindStringSubmatch(line) | ||||
| 			if nil != matches { | ||||
| 				userName := matches[1] | ||||
| //				msgType := matches[2] | ||||
| //				channel := matches[3] | ||||
| 				msg := matches[4] | ||||
| 				rgb.GPrintf("[%s] Direct message %s: %s\n", TimeStamp(), userName, msg) | ||||
| 
 | ||||
| 			} | ||||
| 
 | ||||
| 			// handle a PRIVMSG message | ||||
| 			matches = MsgRegex.FindStringSubmatch(line) | ||||
| 			if nil != matches { | ||||
| 				userName := matches[1] | ||||
| 				msgType := matches[2] | ||||
| 				channel := matches[3] | ||||
| 
 | ||||
| 				switch msgType { | ||||
| 				case "PRIVMSG": | ||||
| 					msg := matches[4] | ||||
| 					rgb.GPrintf("[%s] %s: %s\n", TimeStamp(), userName, msg) | ||||
| 					rgb.GPrintf("[%s] raw line: %s\n", TimeStamp(), line) | ||||
| 
 | ||||
| 					// parse commands from user message | ||||
| 					cmdMatches := CmdRegex.FindStringSubmatch(msg) | ||||
| 					if nil != cmdMatches { | ||||
| 						cmd := cmdMatches[1] | ||||
| 
 | ||||
| 						rgb.YPrintf("[%s] Checking cmd %s against %s\n", TimeStamp(), cmd, bb.ChannelData[channel].Command) | ||||
| 						switch cmd { | ||||
| 						case bb.ChannelData[channel].Command: | ||||
| 							rgb.CPrintf("[%s] Card asked for by %s on %s' channel!\n", TimeStamp(), userName, channel) | ||||
| 
 | ||||
| 							bb.Say("Your prompt is : "+bb.Prompts[rand.Intn(len(bb.Prompts))], channel) | ||||
| 						case "join": | ||||
| 							if bb.ChannelData[channel].ControlChannel { | ||||
| 								rgb.CPrintf("[%s] Join asked for by %s on %s' channel!\n", TimeStamp(), userName, channel) | ||||
| 								if bb.ChannelData[userName].Name == "" { | ||||
| 									record := ChannelData{Name: userName, JoinTime: time.Now(), Command: "card", ControlChannel: true} | ||||
| 									bb.Database.Write("channelData", userName, record) | ||||
| 									bb.ChannelData[userName] = record | ||||
| 								} | ||||
| 								bb.JoinChannel(userName) | ||||
| 							} | ||||
| 						} | ||||
| 
 | ||||
| 						// channel-owner specific commands | ||||
| 						if userName == channel { | ||||
| 							switch cmd { | ||||
| 							case "tbdown": | ||||
| 								rgb.CPrintf( | ||||
| 									"[%s] Shutdown command received. Shutting down now...\n", | ||||
| 									TimeStamp(), | ||||
| 								) | ||||
| 
 | ||||
| 								bb.Disconnect() | ||||
| 								return nil | ||||
| 							case "kcardadmin": | ||||
| 								magicCode := bb.ReadOrCreateChannelKey(channel) | ||||
| 								rgb.CPrintf( | ||||
| 									"[%s] Magic code is %s - https://karaokards.ing.martyn.berlin/admin/%s/%s\n", | ||||
| 									TimeStamp(), | ||||
| 									magicCode, userName, magicCode, | ||||
| 								) | ||||
| 								err := bb.Msg("Welcome to Karaokards, your admin panel is https://karaokards.ing.martyn.berlin/admin/"+userName+"/"+magicCode, userName) | ||||
| 								if err != nil { | ||||
| 									rgb.RPrintf("[%s] ERROR %s\n",err) | ||||
| 								} | ||||
| 								bb.Say("Ack.") | ||||
| 							default: | ||||
| 								// do nothing | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				default: | ||||
| 					// do nothing | ||||
| 					rgb.YPrintf("[%s] unknown IRC message : %s\n", TimeStamp(), line) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		time.Sleep(bb.MsgRate) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Login to the IRC server | ||||
| func (bb *KardBot) Login() { | ||||
| 	rgb.YPrintf("[%s] Logging into #%s...\n", TimeStamp(), bb.Channel) | ||||
| 	bb.conn.Write([]byte("PASS " + bb.IrcCredentials.Password + "\r\n")) | ||||
| 	bb.conn.Write([]byte("NICK " + bb.Name + "\r\n")) | ||||
| } | ||||
| 
 | ||||
| func (bb *KardBot) LeaveChannel(channels ...string) { | ||||
| 	for _, channel := range channels { | ||||
| 		rgb.YPrintf("[%s] Leaving #%s...\n", TimeStamp(), channel) | ||||
| 		bb.conn.Write([]byte("PART #" + channel + "\r\n")) | ||||
| 		rgb.YPrintf("[%s] Left #%s as @%s!\n", TimeStamp(), channel, bb.Name) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Makes the bot join its pre-specified channel. | ||||
| func (bb *KardBot) JoinChannel(channels ...string) { | ||||
| 	if len(channels) == 0 { | ||||
| 		channels = append(channels, bb.Channel) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, channel := range channels { | ||||
| 		rgb.YPrintf("[%s] Joining #%s...\n", TimeStamp(), channel) | ||||
| 		bb.conn.Write([]byte("JOIN #" + channel + "\r\n")) | ||||
| 		rgb.YPrintf("[%s] Joined #%s as @%s!\n", TimeStamp(), channel, bb.Name) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Reads from the private credentials file and stores the data in the bot's appropriate Credentials field. | ||||
| func (bb *KardBot) ReadCredentials(credType string) error { | ||||
| 
 | ||||
| 	var err error | ||||
|     var credFile []byte | ||||
| 	// reads from the file | ||||
| 	if credType == "IRC" { | ||||
| 		credFile, err = ioutil.ReadFile(bb.IrcPrivatePath) | ||||
| 	} else { | ||||
| 		credFile, err = ioutil.ReadFile(bb.AppPrivatePath) | ||||
| 	} | ||||
| 	if nil != err { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// parses the file contents | ||||
| 	var creds OAuthCred | ||||
| 	dec := json.NewDecoder(strings.NewReader(string(credFile))) | ||||
| 	if err = dec.Decode(&creds); nil != err && io.EOF != err { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if credType == "IRC" { | ||||
| 		bb.IrcCredentials = &creds | ||||
| 	} else { | ||||
| 		bb.AppCredentials = &creds | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (bb *KardBot) Msg(msg string, users ...string) error { | ||||
| 	if "" == msg { | ||||
| 		return errors.New("BasicBot.Say: msg was empty.") | ||||
| 	} | ||||
| 
 | ||||
| 	// check if message is too large for IRC | ||||
| 	if len(msg) > 512 { | ||||
| 		return errors.New("BasicBot.Say: msg exceeded 512 bytes") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(users) == 0 { | ||||
| 		return errors.New("BasicBot.Say: users was empty.") | ||||
| 	} | ||||
| 
 | ||||
| 	rgb.YPrintf("[%s] sending %s to users %v as @%s!\n", TimeStamp(), msg, users, bb.Name) | ||||
| 	for _, channel := range users { | ||||
| 		_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG %s :%s\r\n", channel, msg))) | ||||
| 		rgb.YPrintf("[%s] PRIVMSG %s :%s\r\n", TimeStamp(), channel, msg) | ||||
| 		if nil != err { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Makes the bot send a message to the chat channel. | ||||
| func (bb *KardBot) Say(msg string, channels ...string) error { | ||||
| 	if "" == msg { | ||||
| 		return errors.New("BasicBot.Say: msg was empty.") | ||||
| 	} | ||||
| 
 | ||||
| 	// check if message is too large for IRC | ||||
| 	if len(msg) > 512 { | ||||
| 		return errors.New("BasicBot.Say: msg exceeded 512 bytes") | ||||
| 	} | ||||
| 
 | ||||
| 	if len(channels) == 0 { | ||||
| 		channels = append(channels, bb.Channel) | ||||
| 	} | ||||
| 
 | ||||
| 	rgb.YPrintf("[%s] sending %s to channels %v as @%s!\n", TimeStamp(), msg, channels, bb.Name) | ||||
| 	for _, channel := range channels { | ||||
| 		_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG #%s :%s\r\n", channel, msg))) | ||||
| 		rgb.YPrintf("[%s] PRIVMSG #%s :%s\r\n", TimeStamp(), channel, msg) | ||||
| 		if nil != err { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Starts a loop where the bot will attempt to connect to the Twitch IRC server, then connect to the | ||||
| // pre-specified channel, and then handle the chat. It will attempt to reconnect until it is told to | ||||
| // shut down, or is forcefully shutdown. | ||||
| func (bb *KardBot) Start() { | ||||
| 	err := bb.ReadCredentials("IRC") | ||||
| 	if nil != err { | ||||
| 		fmt.Println(err) | ||||
| 		fmt.Println("Aborting!") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	err = bb.ReadCredentials("App") | ||||
| 	if nil != err { | ||||
| 		fmt.Println(err) | ||||
| 		fmt.Println("Aborting!") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	err = bb.readChannelData() | ||||
| 	if nil != err { | ||||
| 		fmt.Println(err) | ||||
| 		fmt.Println("Aborting!") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	for { | ||||
| 		bb.Connect() | ||||
| 		bb.Login() | ||||
| 		if len(bb.ChannelData) > 0 { | ||||
| 			for channelName,channelData := range bb.ChannelData { | ||||
| 				if !channelData.HasLeft { | ||||
| 					bb.JoinChannel(channelName) | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			bb.JoinChannel() | ||||
| 		} | ||||
| 		err = bb.HandleChat() | ||||
| 		if nil != err { | ||||
| 
 | ||||
| 			// attempts to reconnect upon unexpected chat error | ||||
| 			time.Sleep(1000 * time.Millisecond) | ||||
| 			fmt.Println(err) | ||||
| 			fmt.Println("Starting bot again...") | ||||
| 		} else { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (bb *KardBot) readChannelData() error { | ||||
| 	records, err := bb.Database.ReadAll("channelData") | ||||
| 	if err != nil { | ||||
| 		// no db? initialise one? | ||||
| 		record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Command: "card"} | ||||
| 		rgb.YPrintf("[%s] No channel table for #%s exists, creating...\n", TimeStamp(), bb.Channel) | ||||
| 		if err := bb.Database.Write("channelData", bb.Channel, record); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		bb.ChannelData = make(map[string]ChannelData) | ||||
| 		bb.ChannelData[bb.Channel] = record | ||||
| 	} else { | ||||
| 		bb.ChannelData = make(map[string]ChannelData) | ||||
| 	} | ||||
| 	for _, data := range records { | ||||
| 		record := ChannelData{} | ||||
| 		err := json.Unmarshal([]byte(data), &record) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if record.Name != "" { | ||||
| 			if record.Command == "" { | ||||
| 				record.Command = "card" | ||||
| 
 | ||||
| 				rgb.YPrintf("[%s] Rewriting data for #%s...\n", TimeStamp(), bb.Channel) | ||||
| 				if err := bb.Database.Write("channelData", record.Name, record); err != nil { | ||||
| 					return err | ||||
| 				} | ||||
| 			} | ||||
| 			bb.ChannelData[record.Name] = record | ||||
| 		} | ||||
| 	} | ||||
| 	// Managed to leave the main channel!? | ||||
| 	if bb.ChannelData[bb.Channel].Name == "" { | ||||
| 		rgb.YPrintf("[%s] No channel data for #%s exists, creating...\n", TimeStamp(), bb.Channel) | ||||
| 		record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Command: "card"} | ||||
| 		bb.ChannelData[bb.Channel] = record | ||||
| 		if err := bb.Database.Write("channelData", bb.Channel, record); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		records, err = bb.Database.ReadAll("channelData") | ||||
| 	} | ||||
| 	rgb.YPrintf("[%s] Read channel data for %d channels\n", TimeStamp(), len(bb.ChannelData)) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (bb *KardBot) ReadOrCreateChannelKey(channel string) string { | ||||
| 	magicCode := "" | ||||
| 	var err error | ||||
| 	var record ChannelData | ||||
| 	if record, ok := bb.ChannelData[channel]; !ok { | ||||
| 		rgb.YPrintf("[%s] No channel data for #%s exists, creating\n", TimeStamp(), channel) | ||||
| 		err = bb.Database.Read("channelData", channel, &record) | ||||
| 		if err == nil { | ||||
| 			bb.ChannelData[channel] = record | ||||
| 		} | ||||
| 	} | ||||
| 	record = bb.ChannelData[channel] | ||||
| 	if err != nil || record.AdminKey == "" { | ||||
| 		rgb.YPrintf("[%s] No channel key for #%s exists, creating one\n", TimeStamp(), channel) | ||||
| 		newuu, _ := uuid.NewRandom() | ||||
| 		magicCode = base64.StdEncoding.EncodeToString([]byte(newuu.String())) | ||||
| 		record.HasLeft = true | ||||
| 		record.AdminKey = magicCode | ||||
| 		if record.Name == "" { | ||||
| 			record.Name = channel | ||||
| 		} | ||||
| 		if err := bb.Database.Write("channelData", channel, record); err != nil { | ||||
| 			rgb.RPrintf("[%s] Error writing channel data for #%s\n", TimeStamp(), channel) | ||||
| 		} | ||||
| 		bb.ChannelData[record.Name] = record | ||||
| 		rgb.YPrintf("[%s] Cached channel key for #%s\n", TimeStamp(), record.Name) | ||||
| 	} else { | ||||
| 		magicCode = record.AdminKey | ||||
| 		rgb.YPrintf("[%s] Loaded data for #%s\n", TimeStamp(), channel) | ||||
| 	} | ||||
| 	return magicCode | ||||
| } | ||||
| 
 | ||||
| func TimeStamp() string { | ||||
| 	return TimeStampFmt(UTCFormat) | ||||
| } | ||||
| 
 | ||||
| func TimeStampFmt(format string) string { | ||||
| 	return time.Now().Format(format) | ||||
| } | ||||
							
								
								
									
										354
									
								
								internal/webserver/webserver.go
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										354
									
								
								internal/webserver/webserver.go
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,354 @@ | |||
| package webserver | ||||
| 
 | ||||
| import ( | ||||
| 	"math/rand" | ||||
| 
 | ||||
| 	irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc" | ||||
| 	"github.com/gorilla/handlers" | ||||
| 	"github.com/gorilla/mux" | ||||
| 
 | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| //var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY")) | ||||
| 
 | ||||
| type twitchauthresponse struct { | ||||
| 	Access_token  string   `json: "access_token"` | ||||
| 	Expires_in    int      `json: "expires_in"` | ||||
| 	Refresh_token string   `json: "refresh_token"` | ||||
| 	Scope         []string `json: "scope"` | ||||
| 	Token_type    string   `json: "token_type"` | ||||
| } | ||||
| 
 | ||||
| type twitchUser struct { | ||||
| 	Id                string `json: "id"` | ||||
| 	Login             string `json: "login"` | ||||
| 	Display_name      string `json: "display_name"` | ||||
| 	Type              string `json: "type"` | ||||
| 	Broadcaster_type  string `json: "affiliate"` | ||||
| 	Description       string `json: "description"` | ||||
| 	Profile_image_url string `json: "profile_image_url"` | ||||
| 	Offline_image_url string `json: "offline_image_url"` | ||||
| 	View_count        int    `json: "view_count"` | ||||
| } | ||||
| 
 | ||||
| type twitchUsersBigResponse struct { | ||||
| 	Data []twitchUser `json:"data"` | ||||
| } | ||||
| 
 | ||||
| var ircBot *irc.KardBot | ||||
| 
 | ||||
| func HealthHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	response.Header().Add("Content-type", "text/plain") | ||||
| 	fmt.Fprint(response, "I'm okay jack!") | ||||
| } | ||||
| 
 | ||||
| func NotFoundHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	response.Header().Add("X-Template-File", "html"+request.URL.Path) | ||||
| 	response.WriteHeader(404) | ||||
| 	tmpl := template.Must(template.ParseFiles("web/404.html")) | ||||
| 	tmpl.Execute(response, nil) | ||||
| } | ||||
| 
 | ||||
| func CSSHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	response.Header().Add("Content-type", "text/css") | ||||
| 	tmpl := template.Must(template.ParseFiles("web/cover.css")) | ||||
| 	tmpl.Execute(response, nil) | ||||
| } | ||||
| 
 | ||||
| func RootHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	request.URL.Path = "/index.html" | ||||
| 	TemplateHandler(response, request) | ||||
| } | ||||
| 
 | ||||
| func TemplateHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	response.Header().Add("X-Template-File", "web"+request.URL.Path) | ||||
| 	type TemplateData struct { | ||||
| 		Prompt       string | ||||
| 		AvailCount   int | ||||
| 		ChannelCount int | ||||
| 		MessageCount int | ||||
| 		ClientID     string | ||||
| 		BaseURI      string | ||||
| 	} | ||||
| 	// tmpl, err := template.New("html"+request.URL.Path).Funcs(template.FuncMap{ | ||||
| 	// 	"ToUpper": strings.ToUpper, | ||||
| 	// 	"ToLower": strings.ToLower, | ||||
| 	// }).ParseFiles("html"+request.URL.Path) | ||||
| 	_ = strings.ToLower("Hello") | ||||
| 	if strings.Index(request.URL.Path, "/") < 0 { | ||||
| 		http.Error(response, "No slashes wat - "+request.URL.Path, http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	basenameSlice := strings.Split(request.URL.Path, "/") | ||||
| 	basename := basenameSlice[len(basenameSlice)-1] | ||||
| 	//fmt.Fprintf(response, "%q", basenameSlice) | ||||
| 	tmpl, err := template.New(basename).Funcs(template.FuncMap{ | ||||
| 		"ToUpper": strings.ToUpper, | ||||
| 		"ToLower": strings.ToLower, | ||||
| 	}).ParseFiles("web" + request.URL.Path) | ||||
| 	if err != nil { | ||||
| 		http.Error(response, err.Error(), http.StatusInternalServerError) | ||||
| 		return | ||||
| 		// NotFoundHandler(response, request) | ||||
| 		// return | ||||
| 	} | ||||
| 	var td = TemplateData{ircBot.Prompts[rand.Intn(len(ircBot.Prompts))], len(ircBot.Prompts), ircBot.ActiveChannels(), 0, ircBot.AppCredentials.ClientID, "https://" + ircBot.Config.ExternalUrl} | ||||
| 	err = tmpl.Execute(response, td) | ||||
| 	if err != nil { | ||||
| 		http.Error(response, err.Error(), http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func LeaveHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	request.URL.Path = "/bye.html" | ||||
| 	TemplateHandler(response, request) | ||||
| } | ||||
| 
 | ||||
| func AdminHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	vars := mux.Vars(request) | ||||
| 	if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { | ||||
| 		UnauthorizedHandler(response, request) | ||||
| 		return | ||||
| 	} | ||||
| 	type TemplateData struct { | ||||
| 		Channel      string | ||||
| 		Command      string | ||||
| 		ExtraStrings string | ||||
| 		SinceTime    time.Time | ||||
| 		SinceTimeUTC string | ||||
| 		Leaving      bool | ||||
| 		HasLeft      bool | ||||
| 	} | ||||
| 	channelData := ircBot.ChannelData[vars["channel"]] | ||||
| 	var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft} | ||||
| 
 | ||||
| 	if request.Method == "POST" { | ||||
| 		request.ParseForm() | ||||
| 		if strings.Join(request.PostForm["leave"], ",") == "Leave twitch channel" { | ||||
| 			td.Leaving = true | ||||
| 		} else if strings.Join(request.PostForm["reallyleave"], ",") == "Really leave twitch channel" { | ||||
| 			record := ircBot.ChannelData[vars["channel"]] | ||||
| 			record.HasLeft = true | ||||
| 			ircBot.ChannelData[vars["channel"]] = record | ||||
| 			ircBot.LeaveChannel(vars["channel"]) | ||||
| 			ircBot.Database.Write("channelData", vars["channel"], record) | ||||
| 			LeaveHandler(response, request) | ||||
| 			return | ||||
| 		} | ||||
| 		if strings.Join(request.PostForm["join"], ",") == "Come on in" { | ||||
| 			record := ircBot.ChannelData[vars["channel"]] | ||||
| 			td.HasLeft = false | ||||
| 			record.Name = vars["channel"] | ||||
| 			record.JoinTime = time.Now() | ||||
| 			record.HasLeft = false | ||||
| 			if record.Command == "" { | ||||
| 				record.Command = "card" | ||||
| 			} | ||||
| 			ircBot.Database.Write("channelData", vars["channel"], record) | ||||
| 			ircBot.ChannelData[vars["channel"]] = record | ||||
| 			td = TemplateData{record.Name, record.Command, record.ExtraStrings, record.JoinTime, record.JoinTime.Format(irc.UTCFormat), false, record.HasLeft} | ||||
| 			ircBot.JoinChannel(record.Name) | ||||
| 		} | ||||
| 		sourceData := ircBot.ChannelData[vars["channel"]] | ||||
| 		if strings.Join(request.PostForm["Command"], ",") != "" { | ||||
| 			sourceData.Command = strings.Join(request.PostForm["Command"], ",") | ||||
| 			td.Command = sourceData.Command | ||||
| 			ircBot.ChannelData[vars["channel"]] = sourceData | ||||
| 		} | ||||
| 		if strings.Join(request.PostForm["ExtraStrings"], ",") != sourceData.ExtraStrings { | ||||
| 			sourceData.ExtraStrings = strings.Join(request.PostForm["ExtraStrings"], ",") | ||||
| 			td.ExtraStrings = sourceData.ExtraStrings | ||||
| 			ircBot.ChannelData[vars["channel"]] = sourceData | ||||
| 		} | ||||
| 		ircBot.Database.Write("channelData", vars["channel"], sourceData) | ||||
| 	} | ||||
| 	tmpl := template.Must(template.ParseFiles("web/admin.html")) | ||||
| 	tmpl.Execute(response, td) | ||||
| } | ||||
| 
 | ||||
| func UnauthorizedHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	response.Header().Add("X-Template-File", "html"+request.URL.Path) | ||||
| 	response.WriteHeader(401) | ||||
| 	tmpl := template.Must(template.ParseFiles("web/401.html")) | ||||
| 	tmpl.Execute(response, nil) | ||||
| } | ||||
| 
 | ||||
| func twitchHTTPClient(call string, bearer string) (string, error) { | ||||
| 	url := "https://api.twitch.tv/helix/" + call | ||||
| 	var bearerHeader = "Bearer " + bearer | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", url, nil) | ||||
| 	req.Header.Add("Client-ID", ircBot.AppCredentials.ClientID) | ||||
| 	req.Header.Add("Authorization", bearerHeader) | ||||
| 
 | ||||
| 	client := &http.Client{} | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	body, _ := ioutil.ReadAll(resp.Body) | ||||
| 	return string([]byte(body)), nil | ||||
| } | ||||
| 
 | ||||
| func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 
 | ||||
| 	vars := mux.Vars(request) | ||||
| 	if vars["code"] != "" { | ||||
| 		response.Header().Add("Content-type", "text/plain") | ||||
| 		resp, err := http.PostForm( | ||||
| 			"https://id.twitch.tv/oauth2/token", | ||||
| 			url.Values{ | ||||
| 				"client_id":     {ircBot.AppCredentials.ClientID}, | ||||
| 				"client_secret": {ircBot.AppCredentials.Password}, | ||||
| 				"code":          {vars["code"]}, | ||||
| 				"grant_type":    {"authorization_code"}, | ||||
| 				"redirect_uri":  {"https://" + ircBot.Config.ExternalUrl + "/twitchadmin"}}) | ||||
| 		if err != nil { | ||||
| 			response.WriteHeader(500) | ||||
| 			response.Header().Add("Content-type", "text/plain") | ||||
| 			fmt.Fprint(response, "ERROR: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		defer resp.Body.Close() | ||||
| 		body, err := ioutil.ReadAll(resp.Body) | ||||
| 		if err != nil { | ||||
| 			response.WriteHeader(500) | ||||
| 			response.Header().Add("Content-type", "text/plain") | ||||
| 			fmt.Fprint(response, "ERROR: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		var oauthResponse twitchauthresponse | ||||
| 		err = json.Unmarshal(body, &oauthResponse) | ||||
| 		if err != nil { | ||||
| 			response.WriteHeader(500) | ||||
| 			response.Header().Add("Content-type", "text/plain") | ||||
| 			fmt.Fprint(response, "ERROR: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		usersResponse, err := twitchHTTPClient("users", oauthResponse.Access_token) | ||||
| 		if err != nil { | ||||
| 			response.WriteHeader(500) | ||||
| 			response.Header().Add("Content-type", "text/plain") | ||||
| 			fmt.Fprint(response, "ERROR: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		var usersObject twitchUsersBigResponse | ||||
| 		err = json.Unmarshal([]byte(usersResponse), &usersObject) | ||||
| 		if err != nil { | ||||
| 			response.WriteHeader(500) | ||||
| 			response.Header().Add("Content-type", "text/plain") | ||||
| 			fmt.Fprint(response, "ERROR: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if len(usersObject.Data) != 1 { | ||||
| 			response.WriteHeader(500) | ||||
| 			response.Header().Add("Content-type", "text/plain") | ||||
| 			fmt.Fprint(response, "ERROR: Twitch returned not 1 user for the request!\n---\n") | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		user := usersObject.Data[0] | ||||
| 
 | ||||
| 		magicCode := ircBot.ReadOrCreateChannelKey(user.Login) | ||||
| 		url := "https://" + ircBot.Config.ExternalUrl + "/admin/" + user.Login + "/" + magicCode | ||||
| 		http.Redirect(response, request, url, http.StatusFound) | ||||
| 
 | ||||
| 	} else { | ||||
| 
 | ||||
| 		fmt.Fprintf(response, "I'm not okay jack! %v \n", vars) | ||||
| 		for key, val := range vars { | ||||
| 			fmt.Fprint(response, "%s = %s\n", key, val) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TwitchBackendHandler(response http.ResponseWriter, request *http.Request) { | ||||
| 	response.Header().Add("Content-type", "text/plain") | ||||
| 	vars := mux.Vars(request) | ||||
| 	// fmt.Fprintf(response, "I'm okay jack! %v \n", vars) | ||||
| 	// for key, val := range(vars) { | ||||
| 	// 	fmt.Fprint(response, "%s = %s\n", key, val) | ||||
| 	// } | ||||
| 
 | ||||
| 	if vars["code"] != "" { | ||||
| 		// https://id.twitch.tv/oauth2/token | ||||
| 		// ?client_id=<your client ID> | ||||
| 		// &client_secret=<your client secret> | ||||
| 		// &code=<authorization code received above> | ||||
| 		// &grant_type=authorization_code | ||||
| 		// &redirect_uri=<your registered redirect URI> | ||||
| 
 | ||||
| 		// ircBot.AppCredentials.ClientID | ||||
| 		// ircBot.AppCredentials.Password | ||||
| 		// vars["oauthtoken"] | ||||
| 		// authorization_code | ||||
| 		// "https://"+ircBot.Config.ExternalUrl+/twitchadmin | ||||
| 		fmt.Println("Asking twitch for more...") | ||||
| 		resp, err := http.PostForm( | ||||
| 			"https://id.twitch.tv/oauth2/token", | ||||
| 			url.Values{ | ||||
| 				"client_id":     {ircBot.AppCredentials.ClientID}, | ||||
| 				"client_secret": {ircBot.AppCredentials.Password}, | ||||
| 				"code":          {vars["code"]}, | ||||
| 				"grant_type":    {"authorization_code"}, | ||||
| 				"redirect_uri":  {"https://" + ircBot.Config.ExternalUrl + "/twitchadmin"}}) | ||||
| 		if err != nil { | ||||
| 			response.Header().Add("Content-type", "text/plain") | ||||
| 			fmt.Fprint(response, "ERROR: "+err.Error()) | ||||
| 		} | ||||
| 		defer resp.Body.Close() | ||||
| 		body, err := ioutil.ReadAll(resp.Body) | ||||
| 		if err != nil { | ||||
| 			response.Header().Add("Content-type", "text/plain") | ||||
| 			fmt.Fprint(response, "ERROR: "+err.Error()) | ||||
| 		} | ||||
| 		response.Header().Add("Content-type", "text/plain") | ||||
| 		fmt.Fprint(response, string(body)) | ||||
| 	} else { | ||||
| 		UnauthorizedHandler(response, request) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func HandleHTTP(passedIrcBot *irc.KardBot) { | ||||
| 	ircBot = passedIrcBot | ||||
| 	r := mux.NewRouter() | ||||
| 	loggedRouter := handlers.LoggingHandler(os.Stdout, r) | ||||
| 	r.NotFoundHandler = http.HandlerFunc(NotFoundHandler) | ||||
| 	r.HandleFunc("/", RootHandler) | ||||
| 	r.HandleFunc("/healthz", HealthHandler) | ||||
| 	r.HandleFunc("/web/{.*}", TemplateHandler) | ||||
| 	r.PathPrefix("/static/").Handler(http.FileServer(http.Dir("./web/"))) | ||||
| 	r.HandleFunc("/cover.css", CSSHandler) | ||||
| 	r.HandleFunc("/admin/{channel}/{key}", AdminHandler) | ||||
| 	//r.HandleFunc("/twitchadmin", TwitchAdminHandler) | ||||
| 	//r.HandleFunc("/twitchtobackend", TwitchBackendHandler) | ||||
| 	r.Path("/twitchtobackend").Queries("access_token", "{access_token}", "scope", "{scope}", "token_type", "{token_type}").HandlerFunc(TwitchBackendHandler) | ||||
| 	r.Path("/twitchadmin").Queries("code", "{code}", "scope", "{scope}").HandlerFunc(TwitchAdminHandler) | ||||
| 	http.Handle("/", r) | ||||
| 	srv := &http.Server{ | ||||
| 		Handler:      loggedRouter, | ||||
| 		Addr:         "0.0.0.0:5353", | ||||
| 		WriteTimeout: 15 * time.Second, | ||||
| 		ReadTimeout:  15 * time.Second, | ||||
| 	} | ||||
| 	fmt.Println("Listening on 0.0.0.0:5353") | ||||
| 	srv.ListenAndServe() | ||||
| } | ||||
							
								
								
									
										205
									
								
								main.go
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										205
									
								
								main.go
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,205 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io/ioutil" | ||||
| 	"math/rand" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| 
 | ||||
| 	builtins "git.martyn.berlin/martyn/twitchsingstools/internal/builtins" | ||||
| 	irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc" | ||||
| 	webserver "git.martyn.berlin/martyn/twitchsingstools/internal/webserver" | ||||
| 	rgb "github.com/foresthoffman/rgblog" | ||||
| 	scribble "github.com/nanobox-io/golang-scribble" | ||||
| ) | ||||
| 
 | ||||
| type customStringsStruct struct { | ||||
| 	Strings []string `json:"strings,omitempty"` | ||||
| } | ||||
| 
 | ||||
| var selectablePrompts []string | ||||
| 
 | ||||
| var customStrings customStringsStruct | ||||
| 
 | ||||
| var config irc.ConfigStruct | ||||
| 
 | ||||
| func readConfig() { | ||||
| 	var data []byte | ||||
| 	var err error | ||||
| 	configFile := "" | ||||
| 	if os.Getenv("TSTOOLS_CONFIGFILE") != "" { | ||||
| 		if _, err := os.Stat(os.Getenv("TSTOOLS_CONFIGFILE")); os.IsNotExist(err) { | ||||
| 			rgb.RPrintf("[%s] Error, TSTOOLS_CONFIGFILE env var set and '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("TSTOOLS_CONFIGFILE")) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		configFile = os.Getenv("TSTOOLS_CONFIGFILE") | ||||
| 	} else { | ||||
| 		ex, err := os.Executable() | ||||
| 		if err != nil { | ||||
| 			rgb.YPrintf("[%s] Warning, TSTOOLS_CONFIGFILE env var unset and cannot find executable!\n", irc.TimeStamp()) | ||||
| 		} | ||||
| 		exPath := filepath.Dir(ex) | ||||
| 		if _, err := os.Stat(exPath + "/config.json"); os.IsNotExist(err) { | ||||
| 			rgb.YPrintf("[%s] Warning, TSTOOLS_CONFIGFILE env var unset and `config.json` not alongside executable!\n", irc.TimeStamp()) | ||||
| 			if _, err := os.Stat("/etc/tstools/config.json"); os.IsNotExist(err) { | ||||
| 				rgb.RPrintf("[%s] Error, TSTOOLS_CONFIGFILE env var unset and neither '%s' nor '%s' exist!\n", irc.TimeStamp(), exPath+"/config.json", "/etc/tstools/config.json") | ||||
| 				os.Exit(1) | ||||
| 			} else { | ||||
| 				configFile = "/etc/tstools/config.json" | ||||
| 			} | ||||
| 		} else { | ||||
| 			configFile = exPath + "/config.json" | ||||
| 		} | ||||
| 	} | ||||
| 	data, err = ioutil.ReadFile(configFile) | ||||
| 	if err != nil { | ||||
| 		rgb.RPrintf("[%s] Could not read `%s`. File reading error: %s\n", irc.TimeStamp(), configFile, err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	err = json.Unmarshal(data, &config) | ||||
| 	if err != nil { | ||||
| 		rgb.RPrintf("[%s] Could not unmarshal `%s`. Unmarshal error: %s\n", irc.TimeStamp(), configFile, err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	rgb.YPrintf("[%s] Read config file from `%s`\n", irc.TimeStamp(), configFile) | ||||
| 	rgb.YPrintf("[%s] config %v\n", irc.TimeStamp(), config) | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| //openDatabase "database" in this sense being a scribble db | ||||
| func openDatabase() *scribble.Driver { | ||||
| 	dataPath := "" | ||||
| 	if config.DataPath == "" { | ||||
| 		if os.Getenv("TSTOOLS_DATA_FOLDER") != "" { | ||||
| 			if _, err := os.Stat(os.Getenv("TSTOOLS_DATA_FOLDER")); os.IsNotExist(err) { | ||||
| 				rgb.RPrintf("[%s] Error, TSTOOLS_DATA_FOLDER env var set and '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("TSTOOLS_DATA_FOLDER")) | ||||
| 				os.Exit(1) | ||||
| 			} | ||||
| 			dataPath = os.Getenv("TSTOOLS_DATA_FOLDER") | ||||
| 		} else { | ||||
| 			ex, err := os.Executable() | ||||
| 			if err != nil { | ||||
| 				rgb.RPrintf("[%s] Error, TSTOOLS_DATA_FOLDER env var unset and cannot find executable!\n", irc.TimeStamp()) | ||||
| 				os.Exit(1) | ||||
| 			} | ||||
| 			exPath := filepath.Dir(ex) | ||||
| 			if _, err := os.Stat(exPath + "/data"); os.IsNotExist(err) { | ||||
| 				rgb.YPrintf("[%s] Warning %s doesn't exist, trying to create it.\n", irc.TimeStamp(), exPath+"/data") | ||||
| 				err = os.Mkdir(exPath+"/data", 0770) | ||||
| 				if err != nil { | ||||
| 					rgb.RPrintf("[%s] Error cannot create %s: %s!\n", irc.TimeStamp(), exPath+"/data", err) | ||||
| 					os.Exit(1) | ||||
| 				} | ||||
| 			} | ||||
| 			dataPath = exPath + "/data" | ||||
| 		} | ||||
| 	} else { | ||||
| 		if _, err := os.Stat(config.DataPath); os.IsNotExist(err) { | ||||
| 			rgb.RPrintf("[%s] Error, config-specified path '%s' doesn't exist!\n", irc.TimeStamp(), config.DataPath) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		dataPath = config.DataPath | ||||
| 	} | ||||
| 	db, err := scribble.New(dataPath, nil) | ||||
| 	if err != nil { | ||||
| 		rgb.RPrintf("[%s] Error opening database in '%s' : %s\n", irc.TimeStamp(), dataPath, err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	return db | ||||
| } | ||||
| 
 | ||||
| var buildDate string | ||||
| 
 | ||||
| func main() { | ||||
| 	rgb.YPrintf("[%s] starting twitchsingstools bot build %s\n", irc.TimeStamp(), buildDate) | ||||
| 	readConfig() | ||||
| 	rand.Seed(time.Now().UnixNano()) | ||||
| 	for _, val := range builtins.Karaokards { | ||||
| 		selectablePrompts = append(selectablePrompts, val) | ||||
| 	} | ||||
| 	persistentData := openDatabase() | ||||
| 	var dbGlobalPrompts []string | ||||
| 	if err := persistentData.Read("prompts", "global", &dbGlobalPrompts); err != nil { | ||||
| 		persistentData.Write("prompts", "common", dbGlobalPrompts) | ||||
| 	} | ||||
| 	selectablePrompts := append(selectablePrompts, dbGlobalPrompts...) | ||||
| 
 | ||||
| 	rgb.YPrintf("[%s] %d prompts available.\n", irc.TimeStamp(), len(selectablePrompts)) | ||||
| 	ircOauthPath := "" | ||||
| 	if config.IrcOAuthPath == "" { | ||||
| 		if os.Getenv("TSTOOLS_OAUTH_JSON") != "" { | ||||
| 			if _, err := os.Stat(os.Getenv("TSTOOLS_OAUTH_JSON")); os.IsNotExist(err) { | ||||
| 				os.Exit(1) | ||||
| 			} | ||||
| 			ircOauthPath = os.Getenv("TSTOOLS_OAUTH_JSON") | ||||
| 		} else { | ||||
| 			if _, err := os.Stat(os.Getenv("HOME") + "/.tstools/ircoauth.json"); os.IsNotExist(err) { | ||||
| 				rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", irc.TimeStamp(), os.Getenv("HOME")+"/.tstools/ircoauth.json", "/etc/tstools/ircoauth.json") | ||||
| 				if _, err := os.Stat("/etc/tstools/ircoauth.json"); os.IsNotExist(err) { | ||||
| 					rgb.YPrintf("[%s] Error %s doesn't exist either, cannot connect to irc!\n", irc.TimeStamp(), "/etc/tstools/ircoauth.json") | ||||
| 				} else { | ||||
| 					ircOauthPath = "/etc/tstools/ircoauth.json" | ||||
| 				} | ||||
| 			} else { | ||||
| 				ircOauthPath = os.Getenv("HOME") + "/.tstools/ircoauth.json" | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		if _, err := os.Stat(config.IrcOAuthPath); os.IsNotExist(err) { | ||||
| 			rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, cannot connect to irc!\n", irc.TimeStamp(), config.IrcOAuthPath) | ||||
| 		} else { | ||||
| 			ircOauthPath = config.IrcOAuthPath | ||||
| 		} | ||||
| 	} | ||||
| 	appOauthPath := "" | ||||
| 	if config.AppOAuthPath == "" { | ||||
| 		if os.Getenv("TSTOOLS_OAUTH_JSON") != "" { | ||||
| 			if _, err := os.Stat(os.Getenv("TSTOOLS_OAUTH_JSON")); os.IsNotExist(err) { | ||||
| 				os.Exit(1) | ||||
| 			} | ||||
| 			appOauthPath = os.Getenv("TSTOOLS_OAUTH_JSON") | ||||
| 		} else { | ||||
| 			if _, err := os.Stat(os.Getenv("HOME") + "/.tstools/appoauth.json"); os.IsNotExist(err) { | ||||
| 				rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", irc.TimeStamp(), os.Getenv("HOME")+"/.tstools/appoauth.json", "/etc/tstools/appoauth.json") | ||||
| 				if _, err := os.Stat("/etc/tstools/appoauth.json"); os.IsNotExist(err) { | ||||
| 					rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/tstools/appoauth.json") | ||||
| 					os.Exit(1) | ||||
| 				} | ||||
| 				appOauthPath = "/etc/tstools/appoauth.json" | ||||
| 			} else { | ||||
| 				appOauthPath = os.Getenv("HOME") + "/.tstools/appoauth.json" | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		if _, err := os.Stat(config.AppOAuthPath); os.IsNotExist(err) { | ||||
| 			rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.AppOAuthPath) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		appOauthPath = config.AppOAuthPath | ||||
| 	} | ||||
| 
 | ||||
| 	// Replace the channel name, bot name, and the path to the private directory with your respective | ||||
| 	// values. | ||||
| 	var myBot irc.KardBot | ||||
| 	if ircOauthPath != "" { | ||||
| 		myBot = irc.KardBot{ | ||||
| 			Channel:        "twitchsingstools", | ||||
| 			MsgRate:        time.Duration(20/30) * time.Millisecond, | ||||
| 			Name:           "twitchsingstools", | ||||
| 			Port:           "6667", | ||||
| 			IrcPrivatePath: ircOauthPath, | ||||
| 			AppPrivatePath: appOauthPath, | ||||
| 			Server:         "irc.chat.twitch.tv", | ||||
| 			Prompts:        selectablePrompts, | ||||
| 			Database:       *persistentData, | ||||
| 			Config:         config, | ||||
| 		} | ||||
| 		myBot.Start() | ||||
| 	} | ||||
| 	go func() { | ||||
| 		rgb.YPrintf("[%s] Starting webserver on port %s\n", irc.TimeStamp(), "5353") | ||||
| 		webserver.HandleHTTP(&myBot) | ||||
| 	}() | ||||
| } | ||||
							
								
								
									
										107
									
								
								web/401.html
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										107
									
								
								web/401.html
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,107 @@ | |||
|       <html> | ||||
|         <!doctype html> | ||||
|         <html lang="en"> | ||||
|           <head> | ||||
|             <meta charset="utf-8"> | ||||
|             <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|             <meta name="description" content="Karaokards"> | ||||
|             <meta name="author" content="Martyn Ranyard"> | ||||
|             <title>The great unknown!</title> | ||||
|          | ||||
|             <!-- Bootstrap core CSS --> | ||||
|             <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> | ||||
|             <!-- Custom styles for this template --> | ||||
|             <link href="/cover.css" rel="stylesheet"> | ||||
|          | ||||
|             <style> | ||||
|               .bd-placeholder-img { | ||||
|                 font-size: 1.125rem; | ||||
|                 text-anchor: middle; | ||||
|                 -webkit-user-select: none; | ||||
|                 -moz-user-select: none; | ||||
|                 -ms-user-select: none; | ||||
|                 user-select: none; | ||||
|               } | ||||
|          | ||||
|               @media (min-width: 768px) { | ||||
|                 .bd-placeholder-img-lg { | ||||
|                   font-size: 3.5rem; | ||||
|                 } | ||||
|                     } | ||||
|                      | ||||
|                     li.match-nomatch{ | ||||
|                         background-color: #1e2122; | ||||
|                     } | ||||
|                     li.match-matchtrack{ | ||||
|                         background-color: #E9B000; | ||||
|                     } | ||||
|                     li.match-fullmatch{ | ||||
|                         background-color: #008F95; | ||||
|                     } | ||||
|                     li.match-matchtrackfuzzt{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     li.match-fullmatchfuzzy{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     a{ | ||||
|                         text-decoration-line: underline; | ||||
|                     } | ||||
|             </style> | ||||
|           </head> | ||||
|           <body class="text-center"> | ||||
|                 <div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> | ||||
|                 <header class="masthead mb-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <h3 class="masthead-brand">k8s-zoo</h3> | ||||
|                   <nav class="nav nav-masthead justify-content-center"> | ||||
|                     <a class="nav-link active" href="/">Home</a> | ||||
|                   </nav> | ||||
|                 </div> | ||||
|                 </header> | ||||
|                 <main role="main" class="inner cover"> | ||||
|                         <h1 class="cover-heading">Scary user alert!</h1> | ||||
|                         <img src="https://http.cat/401" alt="Cat sat outside a door that has a sign depictinging no cats allowed" /> | ||||
|                         <p>It seems you've gone somewhere you shouldn't!  401 NOT AUTHORIZED!</p> | ||||
|                         <p/> | ||||
|                         <p>I'm not quite sure how you got here to be honest, if it was via a link on the site, let me know via twitch DM, if it was from someone else, let them know.</p> | ||||
|                         <p>Shameless self-promotion : Follow me on twitch - <a href="https://www.twitch.tv/iMartynOnTwitch">iMartynOnTwitch</a>, oddly enough, I do a lot of twitchsings!</p> | ||||
|                       </main> | ||||
|         <footer class="mastfoot mt-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> | ||||
|                 </div> | ||||
|                 </footer> | ||||
|                 </div> | ||||
|                 <script> | ||||
|                 function directToResults() { | ||||
|                     var url = document.createElement('a'); | ||||
|                     url.setAttribute("href", window.location.href); | ||||
|                     if ((url.port != 80) && (url.port != 443)) { | ||||
|                         customPort = ":"+url.port | ||||
|                     } else { | ||||
|                         customPort = "" | ||||
|                     } | ||||
|                     var destination = url.protocol + "//" + url.hostname + customPort + "/" + document.getElementById("mode").value + "/" + document.getElementById("spotifyid").value | ||||
|                     window.location.href = destination | ||||
|                 } | ||||
|                  | ||||
|                 function toggleUnfound() { | ||||
|                     var unmatched = document.getElementsByClassName('match-nomatch'), i; | ||||
|                     if (document.getElementById("showhidebutton").getAttribute("tracksHidden") != "true") { | ||||
|                         document.getElementById("showhidebutton").setAttribute("tracksHidden","true") | ||||
|                         for (i = 0; i < unmatched.length; i += 1) { | ||||
|                                 unmatched[i].style.display = 'none'; | ||||
|                         } | ||||
|                     } else { | ||||
|                         document.getElementById("showhidebutton").setAttribute("tracksHidden","false") | ||||
|                         for (i = 0; i < unmatched.length; i += 1) { | ||||
|                                 unmatched[i].style.display = 'list-item'; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 </script> | ||||
|                 </body> | ||||
|                 </html> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										106
									
								
								web/404.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								web/404.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | |||
|       <html> | ||||
|         <!doctype html> | ||||
|         <html lang="en"> | ||||
|           <head> | ||||
|             <meta charset="utf-8"> | ||||
|             <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|             <meta name="description" content="Karaokards"> | ||||
|             <meta name="author" content="Martyn Ranyard"> | ||||
|             <title>The great unknown!</title> | ||||
|          | ||||
|             <!-- Bootstrap core CSS --> | ||||
|             <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> | ||||
|             <!-- Custom styles for this template --> | ||||
|             <link href="/cover.css" rel="stylesheet"> | ||||
|          | ||||
|             <style> | ||||
|               .bd-placeholder-img { | ||||
|                 font-size: 1.125rem; | ||||
|                 text-anchor: middle; | ||||
|                 -webkit-user-select: none; | ||||
|                 -moz-user-select: none; | ||||
|                 -ms-user-select: none; | ||||
|                 user-select: none; | ||||
|               } | ||||
|          | ||||
|               @media (min-width: 768px) { | ||||
|                 .bd-placeholder-img-lg { | ||||
|                   font-size: 3.5rem; | ||||
|                 } | ||||
|                     } | ||||
|                      | ||||
|                     li.match-nomatch{ | ||||
|                         background-color: #1e2122; | ||||
|                     } | ||||
|                     li.match-matchtrack{ | ||||
|                         background-color: #E9B000; | ||||
|                     } | ||||
|                     li.match-fullmatch{ | ||||
|                         background-color: #008F95; | ||||
|                     } | ||||
|                     li.match-matchtrackfuzzt{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     li.match-fullmatchfuzzy{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     a{ | ||||
|                         text-decoration-line: underline; | ||||
|                     } | ||||
|             </style> | ||||
|           </head> | ||||
|           <body class="text-center"> | ||||
|                 <div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> | ||||
|                 <header class="masthead mb-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <h3 class="masthead-brand">k8s-zoo</h3> | ||||
|                   <nav class="nav nav-masthead justify-content-center"> | ||||
|                     <a class="nav-link active" href="/">Home</a> | ||||
|                   </nav> | ||||
|                 </div> | ||||
|                 </header> | ||||
|                 <main role="main" class="inner cover"> | ||||
|                         <h1 class="cover-heading">Ooops!</h1> | ||||
|                         <p>It seems you've gone somewhere you shouldn't!  404 NOT FOUND!</p> | ||||
|                         <p/> | ||||
|                         <p>I'm not quite sure how you got here to be honest, if it was via a link on the site, let me know via twitch DM, if it was from someone else, let them know.</p> | ||||
|                             <p>Shameless self-promotion : Follow me on twitch - <a href="https://www.twitch.tv/iMartynOnTwitch">iMartynOnTwitch</a>, oddly enough, I do a lot of twitchsings!</p> | ||||
|                       </main> | ||||
|         <footer class="mastfoot mt-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> | ||||
|                 </div> | ||||
|                 </footer> | ||||
|                 </div> | ||||
|                 <script> | ||||
|                 function directToResults() { | ||||
|                     var url = document.createElement('a'); | ||||
|                     url.setAttribute("href", window.location.href); | ||||
|                     if ((url.port != 80) && (url.port != 443)) { | ||||
|                         customPort = ":"+url.port | ||||
|                     } else { | ||||
|                         customPort = "" | ||||
|                     } | ||||
|                     var destination = url.protocol + "//" + url.hostname + customPort + "/" + document.getElementById("mode").value + "/" + document.getElementById("spotifyid").value | ||||
|                     window.location.href = destination | ||||
|                 } | ||||
|                  | ||||
|                 function toggleUnfound() { | ||||
|                     var unmatched = document.getElementsByClassName('match-nomatch'), i; | ||||
|                     if (document.getElementById("showhidebutton").getAttribute("tracksHidden") != "true") { | ||||
|                         document.getElementById("showhidebutton").setAttribute("tracksHidden","true") | ||||
|                         for (i = 0; i < unmatched.length; i += 1) { | ||||
|                                 unmatched[i].style.display = 'none'; | ||||
|                         } | ||||
|                     } else { | ||||
|                         document.getElementById("showhidebutton").setAttribute("tracksHidden","false") | ||||
|                         for (i = 0; i < unmatched.length; i += 1) { | ||||
|                                 unmatched[i].style.display = 'list-item'; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 </script> | ||||
|                 </body> | ||||
|                 </html> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										143
									
								
								web/admin.html
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										143
									
								
								web/admin.html
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,143 @@ | |||
| <html> | ||||
|         <!doctype html> | ||||
|         <html lang="en"> | ||||
|           <head> | ||||
|             <meta charset="utf-8"> | ||||
|             <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|             <meta name="description" content="Karaokards bot for twitch chat"> | ||||
|             <meta name="author" content="Martyn Ranyard"> | ||||
|             <title>Karaokards</title> | ||||
|          | ||||
|             <!-- Bootstrap core CSS --> | ||||
|             <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> | ||||
|             <!-- Custom styles for this template --> | ||||
|             <link href="/cover.css" rel="stylesheet"> | ||||
|          | ||||
|             <style> | ||||
|               .bd-placeholder-img { | ||||
|                 font-size: 1.125rem; | ||||
|                 text-anchor: middle; | ||||
|                 -webkit-user-select: none; | ||||
|                 -moz-user-select: none; | ||||
|                 -ms-user-select: none; | ||||
|                 user-select: none; | ||||
|               } | ||||
|          | ||||
|               @media (min-width: 768px) { | ||||
|                 .bd-placeholder-img-lg { | ||||
|                   font-size: 3.5rem; | ||||
|                 } | ||||
|                     } | ||||
|                      | ||||
|                     li.match-nomatch{ | ||||
|                         background-color: #1e2122; | ||||
|                     } | ||||
|                     li.match-matchtrack{ | ||||
|                         background-color: #E9B000; | ||||
|                     } | ||||
|                     li.match-fullmatch{ | ||||
|                         background-color: #008F95; | ||||
|                     } | ||||
|                     li.match-matchtrackfuzzt{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     li.match-fullmatchfuzzy{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     a{ | ||||
|                         text-decoration-line: underline; | ||||
|                     } | ||||
|                     .displaySetting{ | ||||
|                       display: inline | ||||
|                     } | ||||
|                     .hiddenDisplaySetting{ | ||||
|                       display: none; | ||||
|                     } | ||||
|                     .hiddenSave { | ||||
|                       display: none; | ||||
|                     } | ||||
|                     .editSetting{ | ||||
|                       display: none; | ||||
|                     } | ||||
|                     .visibleEditSetting{ | ||||
|                       display: inline; | ||||
|                     } | ||||
|                     .visibleSave { | ||||
|                       display: inline; | ||||
|                     } | ||||
|          | ||||
|             </style> | ||||
|           </head> | ||||
|           <body class="text-center"> | ||||
|                 <div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> | ||||
|                 <header class="masthead mb-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <h3 class="masthead-brand">Karaokards</h3> | ||||
|                   <nav class="nav nav-masthead justify-content-center"> | ||||
|                     <a class="nav-link active" href="/">Home</a> | ||||
|                   </nav> | ||||
|                 </div> | ||||
|                 </header> | ||||
|                 <main role="main" class="inner cover"> | ||||
|                    | ||||
|                 <script> | ||||
|                     function editMode() { | ||||
|                         var alldisps = document.getElementsByClassName("displaySetting"); | ||||
|                         for (item of alldisps) { | ||||
|                           item.classList.add("hiddenDisplaySetting") | ||||
|                         } | ||||
|                         for (item of alldisps) { | ||||
|                           item.classList.remove("displaySetting") | ||||
|                         } | ||||
|                         var alldisps = document.getElementsByClassName("editSetting"); | ||||
|                         for (item of alldisps) { | ||||
|                           item.classList.add("visibleEditSetting") | ||||
|                         } | ||||
|                         for (item of alldisps) { | ||||
|                           item.classList.remove("editSetting") | ||||
|                         } | ||||
|                         document.getElementById("saveButton").classList.remove("hiddenSave") | ||||
|                         document.getElementById("saveButton").classList.add("visibleSave") | ||||
|                         document.getElementById("leaveButton").classList.remove("hiddenSave") | ||||
|                         document.getElementById("leaveButton").classList.add("visibleSave") | ||||
|                         document.getElementById("yuhateme").classList.remove("hiddenSave") | ||||
|                         document.getElementById("yuhateme").classList.add("visibleSave") | ||||
|                     } | ||||
|                     </script> | ||||
|                         <h1 class="cover-heading">Karaokards admin panel for {{.Channel}}!!!</h1> | ||||
|                         <form method="POST"> | ||||
|                         {{ if .HasLeft }} | ||||
|                         <h2>Not in your channel at the moment!</h2> | ||||
|                         <p>The bot is not currently in your channel, chances are you've not ever asked it to join, you asked it to leave, or something went horribly wrong.</p> | ||||
|                         <p>You can invite the bot to your channel by clicking here : <input id="joinButton" type="submit" name="join" value="Come on in"></p> | ||||
|                         {{ else }} | ||||
|                           {{ if .Leaving }} | ||||
|                           <h2>Do you really want this bot to leave your channel?</h2> | ||||
|                           <p><input id="leaveButton" type="submit" name="reallyleave" value="Really leave twitch channel"></p> | ||||
|                           {{ else }} | ||||
|                           <h2>Note you can give your moderators the url you are on right now to control this bot.  They don't have to be logged into twitch to do so.</h2> | ||||
|                           <table> | ||||
|                             <thead><tr><td>Channel Data :</td><td><input type="button" value="Edit" onclick="javascript:editMode();"></td></tr></thead> | ||||
|                             <tbody> | ||||
|                             <tr><td>Member of channel since {{.SinceTimeUTC}}</td></tr> | ||||
|                             <tr><td>Command for prompt:</td><td class="displaySetting"></tdclass>{{.Command}}</td><td class="editSetting"><input type="text" name="Command" value="{{.Command}}"></td></tr> | ||||
|                             <tr><td>Extra prompts (one per line):</td><td class="displaySetting">{{.ExtraStrings}}</td><td class="editSetting"><textarea name="ExtraStrings" >{{.ExtraStrings}}</textarea></td></tr> | ||||
|                             <tr><td> </td><td><input id="saveButton" type="submit" class="hiddenSave" name="save" value="Save changes"></td></tr> | ||||
|                             <tr id="yuhateme" class="hiddenSave"><td>Or... please don't go but...</td></tr> | ||||
|                             <tr><td><input id="leaveButton" type="submit" class="hiddenSave" name="leave" value="Leave twitch channel"></td></tr> | ||||
|                           </tbody> | ||||
|                           </table> | ||||
|                           {{ end }} | ||||
|                         {{ end }} | ||||
|                         </form> | ||||
|                 </main> | ||||
|         <footer class="mastfoot mt-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> | ||||
|                 </div> | ||||
|                 </footer> | ||||
|                 </div> | ||||
|                 </body> | ||||
|                 </html> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										75
									
								
								web/bye.html
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										75
									
								
								web/bye.html
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,75 @@ | |||
| <html> | ||||
|         <!doctype html> | ||||
|         <html lang="en"> | ||||
|           <head> | ||||
|             <meta charset="utf-8"> | ||||
|             <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|             <meta name="description" content="Karaokards bot for twitch chat"> | ||||
|             <meta name="author" content="Martyn Ranyard"> | ||||
|             <title>Karaokards</title> | ||||
|          | ||||
|             <!-- Bootstrap core CSS --> | ||||
|             <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> | ||||
|             <!-- Custom styles for this template --> | ||||
|             <link href="/cover.css" rel="stylesheet"> | ||||
|          | ||||
|             <style> | ||||
|               .bd-placeholder-img { | ||||
|                 font-size: 1.125rem; | ||||
|                 text-anchor: middle; | ||||
|                 -webkit-user-select: none; | ||||
|                 -moz-user-select: none; | ||||
|                 -ms-user-select: none; | ||||
|                 user-select: none; | ||||
|               } | ||||
|          | ||||
|               @media (min-width: 768px) { | ||||
|                 .bd-placeholder-img-lg { | ||||
|                   font-size: 3.5rem; | ||||
|                 } | ||||
|                     } | ||||
|                      | ||||
|                     li.match-nomatch{ | ||||
|                         background-color: #1e2122; | ||||
|                     } | ||||
|                     li.match-matchtrack{ | ||||
|                         background-color: #E9B000; | ||||
|                     } | ||||
|                     li.match-fullmatch{ | ||||
|                         background-color: #008F95; | ||||
|                     } | ||||
|                     li.match-matchtrackfuzzt{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     li.match-fullmatchfuzzy{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     a{ | ||||
|                         text-decoration-line: underline; | ||||
|                     }         | ||||
|             </style> | ||||
|           </head> | ||||
|           <body class="text-center"> | ||||
|                 <div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> | ||||
|                 <header class="masthead mb-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <h3 class="masthead-brand">Karaokards</h3> | ||||
|                   <nav class="nav nav-masthead justify-content-center"> | ||||
|                     <a class="nav-link active" href="/">Home</a> | ||||
|                   </nav> | ||||
|                 </div> | ||||
|                 </header> | ||||
|                 <main role="main" class="inner cover"> | ||||
|                         <h1 class="cover-heading">Left channel!!!</h1> | ||||
|                         <p><img src="/static/okaybye.gif" alt="animation of dissappointed sister from Frozen saying Okay, bye..."/></p> | ||||
|                 </main> | ||||
|         <footer class="mastfoot mt-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> | ||||
|                 </div> | ||||
|                 </footer> | ||||
|                 </div> | ||||
|                 </body> | ||||
|                 </html> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										106
									
								
								web/cover.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								web/cover.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | |||
| /* | ||||
| * Globals | ||||
| */ | ||||
| 
 | ||||
| /* Links */ | ||||
| a, | ||||
| a:focus, | ||||
| a:hover { | ||||
|  color: #fff; | ||||
| } | ||||
| 
 | ||||
| /* Custom default button */ | ||||
| .btn-secondary, | ||||
| .btn-secondary:hover, | ||||
| .btn-secondary:focus { | ||||
|  color: #333; | ||||
|  text-shadow: none; /* Prevent inheritance from body */ | ||||
|  background-color: #fff; | ||||
|  border: .05rem solid #fff; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| * Base structure | ||||
| */ | ||||
| 
 | ||||
| html, | ||||
| body { | ||||
|  height: 100%; | ||||
|  background-color: #333; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|  display: -ms-flexbox; | ||||
|  display: flex; | ||||
|  color: #fff; | ||||
|  text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); | ||||
|  box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); | ||||
| } | ||||
| 
 | ||||
| .cover-container { | ||||
|  max-width: 142em; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| * Header | ||||
| */ | ||||
| .masthead { | ||||
|  margin-bottom: 2rem; | ||||
| } | ||||
| 
 | ||||
| .masthead-brand { | ||||
|  margin-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .nav-masthead .nav-link { | ||||
|  padding: .25rem 0; | ||||
|  font-weight: 700; | ||||
|  color: rgba(255, 255, 255, .5); | ||||
|  background-color: transparent; | ||||
|  border-bottom: .25rem solid transparent; | ||||
| } | ||||
| 
 | ||||
| .nav-masthead .nav-link:hover, | ||||
| .nav-masthead .nav-link:focus { | ||||
|  border-bottom-color: rgba(255, 255, 255, .25); | ||||
| } | ||||
| 
 | ||||
| .nav-masthead .nav-link + .nav-link { | ||||
|  margin-left: 1rem; | ||||
| } | ||||
| 
 | ||||
| .nav-masthead .active { | ||||
|  color: #fff; | ||||
|  border-bottom-color: #fff; | ||||
| } | ||||
| 
 | ||||
| @media (min-width: 48em) { | ||||
|  .masthead-brand { | ||||
|    float: left; | ||||
|  } | ||||
|  .nav-masthead { | ||||
|    float: right; | ||||
|  } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| * Cover | ||||
| */ | ||||
| .cover { | ||||
|  padding: 0 1.5rem; | ||||
| } | ||||
| .cover .btn-lg { | ||||
|  padding: .75rem 1.25rem; | ||||
|  font-weight: 700; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| * Footer | ||||
| */ | ||||
| .mastfoot { | ||||
|  color: rgba(255, 255, 255, .5); | ||||
| } | ||||
							
								
								
									
										79
									
								
								web/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								web/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | |||
| <html> | ||||
|         <!doctype html> | ||||
|         <html lang="en"> | ||||
|           <head> | ||||
|             <meta charset="utf-8"> | ||||
|             <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|             <meta name="description" content="Twitch Sings Tools bot"> | ||||
|             <meta name="author" content="Martyn Ranyard"> | ||||
|             <title>Twitch Sings Tools</title> | ||||
|          | ||||
|             <!-- Bootstrap core CSS --> | ||||
|             <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> | ||||
|             <!-- Custom styles for this template --> | ||||
|             <link href="/cover.css" rel="stylesheet"> | ||||
|          | ||||
|             <style> | ||||
|               .bd-placeholder-img { | ||||
|                 font-size: 1.125rem; | ||||
|                 text-anchor: middle; | ||||
|                 -webkit-user-select: none; | ||||
|                 -moz-user-select: none; | ||||
|                 -ms-user-select: none; | ||||
|                 user-select: none; | ||||
|               } | ||||
|          | ||||
|               @media (min-width: 768px) { | ||||
|                 .bd-placeholder-img-lg { | ||||
|                   font-size: 3.5rem; | ||||
|                 } | ||||
|                     } | ||||
|                      | ||||
|                     li.match-nomatch{ | ||||
|                         background-color: #1e2122; | ||||
|                     } | ||||
|                     li.match-matchtrack{ | ||||
|                         background-color: #E9B000; | ||||
|                     } | ||||
|                     li.match-fullmatch{ | ||||
|                         background-color: #008F95; | ||||
|                     } | ||||
|                     li.match-matchtrackfuzzt{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     li.match-fullmatchfuzzy{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     a{ | ||||
|                         text-decoration-line: underline; | ||||
|                     } | ||||
|          | ||||
|             </style> | ||||
|           </head> | ||||
|           <body class="text-center"> | ||||
|                 <div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> | ||||
|                 <header class="masthead mb-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <h3 class="masthead-brand">Twitch Sings Tools</h3> | ||||
|                   <p>(Not endorsed by Twitch, the app is called "Twitch Sings" and these are tools for it, whaddya want it to be called?!)</p> | ||||
|                   <nav class="nav nav-masthead justify-content-center"> | ||||
|                     <a class="nav-link active" href="/">Home</a> | ||||
|                     <a class="nav-link active" href="https://id.twitch.tv/oauth2/authorize?client_id={{.ClientID}}&redirect_uri={{.BaseURI}}/twitchadmin&response_type=code&scope=user:read:broadcast">Admin - log in with twitch</a> | ||||
|                   </nav> | ||||
|                 </div> | ||||
|                 </header> | ||||
|                 <main role="main" class="inner cover"> | ||||
|                         <h1 class="cover-heading">Karaokards!!!</h1> | ||||
|                         <p>Random prompt for you : {{.Prompt}}</p> | ||||
|                         <p>There are a total of {{.AvailCount}} prompts available.  This bot is hanging out in {{.ChannelCount}} channels and has served {{.MessageCount}} prompts via twitch chat!</p> | ||||
|                 </main> | ||||
|                 <footer class="mastfoot mt-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> | ||||
|                 </div> | ||||
|                 </footer> | ||||
|                 </div> | ||||
|                 </body> | ||||
|                 </html> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										111
									
								
								web/standby.html
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										111
									
								
								web/standby.html
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,111 @@ | |||
|       <html> | ||||
|         <!doctype html> | ||||
|         <html lang="en"> | ||||
|           <head> | ||||
|             <meta charset="utf-8"> | ||||
|             <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|             <meta name="description" content="Karaokards"> | ||||
|             <meta name="author" content="Martyn Ranyard"> | ||||
|             <title>Please Stand By!</title> | ||||
|          | ||||
|             <!-- Bootstrap core CSS --> | ||||
|             <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> | ||||
|             <!-- Custom styles for this template --> | ||||
|             <link href="/cover.css" rel="stylesheet"> | ||||
|          | ||||
|             <style> | ||||
|               .bd-placeholder-img { | ||||
|                 font-size: 1.125rem; | ||||
|                 text-anchor: middle; | ||||
|                 -webkit-user-select: none; | ||||
|                 -moz-user-select: none; | ||||
|                 -ms-user-select: none; | ||||
|                 user-select: none; | ||||
|               } | ||||
|          | ||||
|               @media (min-width: 768px) { | ||||
|                 .bd-placeholder-img-lg { | ||||
|                   font-size: 3.5rem; | ||||
|                 } | ||||
|                     } | ||||
|                      | ||||
|                     li.match-nomatch{ | ||||
|                         background-color: #1e2122; | ||||
|                     } | ||||
|                     li.match-matchtrack{ | ||||
|                         background-color: #E9B000; | ||||
|                     } | ||||
|                     li.match-fullmatch{ | ||||
|                         background-color: #008F95; | ||||
|                     } | ||||
|                     li.match-matchtrackfuzzt{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     li.match-fullmatchfuzzy{ | ||||
|                         background-color: darkgray; | ||||
|                     } | ||||
|                     a{ | ||||
|                         text-decoration-line: underline; | ||||
|                     } | ||||
|             </style> | ||||
|           </head> | ||||
|           <body class="text-center" onload=directToResults()> | ||||
|                 <div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column"> | ||||
|                 <header class="masthead mb-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <h3 class="masthead-brand">Karaokards</h3> | ||||
|                   <nav class="nav nav-masthead justify-content-center"> | ||||
|                     <a class="nav-link active" href="/">Home</a> | ||||
|                   </nav> | ||||
|                 </div> | ||||
|                 </header> | ||||
|                 <main role="main" class="inner cover"> | ||||
|                         <h1 class="cover-heading">Please stand by, twitch gives us stuff that needs to be sent to the server!</h1> | ||||
|                         <img src="https://media.giphy.com/media/ule4vhcY1xEKQ/source.gif" alt="Cats typing furiously" /> | ||||
|                         <p/> | ||||
|                         <p>Just hold on, the javascript is doing it's stuff. Don't blame me, twitch really forces us to use javascript here.</p> | ||||
|                         <p>Shameless self-promotion : Follow me on twitch - <a href="https://www.twitch.tv/iMartynOnTwitch">iMartynOnTwitch</a>, oddly enough, I do a lot of twitchsings!</p> | ||||
|                       </main> | ||||
|         <footer class="mastfoot mt-auto"> | ||||
|                 <div class="inner"> | ||||
|                   <p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p> | ||||
|                 </div> | ||||
|                 </footer> | ||||
|                 </div> | ||||
|                 <script> | ||||
|                   // #access_token=hkq5diaiopu23tzyo5oik7jl7g2w0n&scope=user%3Aread%3Abroadcast&token_type=bearer | ||||
| 
 | ||||
|                 function directToResults() { | ||||
|                     var url = document.createElement('a'); | ||||
|                     url.setAttribute("href", window.location.href); | ||||
|                     if ((url.port != 80) && (url.port != 443)) { | ||||
|                         customPort = ":"+url.port | ||||
|                     } else { | ||||
|                         customPort = "" | ||||
|                     } | ||||
|                  | ||||
|                   u = new URLSearchParams(document.location.hash.substr(1)) | ||||
|                   var destination = new URL(url.protocol + "//" + url.hostname + customPort + "/twitchtobackend?" + u.toString()) | ||||
|                   console.log(destination) | ||||
|                   window.location.href = destination | ||||
|                 } | ||||
|                  | ||||
|                 function toggleUnfound() { | ||||
|                     var unmatched = document.getElementsByClassName('match-nomatch'), i; | ||||
|                     if (document.getElementById("showhidebutton").getAttribute("tracksHidden") != "true") { | ||||
|                         document.getElementById("showhidebutton").setAttribute("tracksHidden","true") | ||||
|                         for (i = 0; i < unmatched.length; i += 1) { | ||||
|                                 unmatched[i].style.display = 'none'; | ||||
|                         } | ||||
|                     } else { | ||||
|                         document.getElementById("showhidebutton").setAttribute("tracksHidden","false") | ||||
|                         for (i = 0; i < unmatched.length; i += 1) { | ||||
|                                 unmatched[i].style.display = 'list-item'; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 </script> | ||||
|                 </body> | ||||
|                 </html> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										
											BIN
										
									
								
								web/static/okaybye.gif
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/static/okaybye.gif
									
										
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 263 KiB | 
		Loading…
	
	Add table
		
		Reference in a new issue