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