initial commit
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
This commit is contained in:
commit
0ac377da90
|
@ -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
|
|
@ -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.
|
|
@ -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 .
|
|
@ -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.
|
||||||
|
[![Build Status](https://ci.martyn.berlin/api/badges/martyn/karaokards/status.svg)](https://ci.martyn.berlin/martyn/karaokards)
|
||||||
|
|
|
@ -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
|
|
@ -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"]
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"channels": ["karaokards"],
|
||||||
|
"externalUrl": "karaokards.ing.martyn.berlin"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "imartynonbot",
|
||||||
|
"customcommand": "card",
|
||||||
|
"jointime": "2020-02-22T14:20:44.372767247+01:00",
|
||||||
|
"ControlChannel": true
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "imartynontwitch",
|
||||||
|
"value": "MzdmY2MwYzEtN2FlMC00ZTQ0LThkZDUtZDFhZDljZWUwMjZj",
|
||||||
|
"customcommand": "card",
|
||||||
|
"extrastrings": "Make Pineboy Choose!",
|
||||||
|
"jointime": "2020-02-22T13:54:06.31158195+01:00"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "karaokards",
|
||||||
|
"customcommand": "card",
|
||||||
|
"jointime": "2020-05-16T13:25:55.339717754+02:00",
|
||||||
|
"ControlChannel": false,
|
||||||
|
"hasleft": false
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
null
|
|
@ -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/
|
|
@ -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
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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,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 }}
|
|
@ -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
|
|
@ -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
|
|
@ -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: {}
|
|
@ -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"}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}()
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
Binary file not shown.
After Width: | Height: | Size: 263 KiB |
Loading…
Reference in New Issue