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