Compare commits

..

2 Commits

Author SHA1 Message Date
Martyn a9dc6db0a4 Multi-channel support and deployment for webserver.
continuous-integration/drone/tag Build was killed Details
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
2020-02-14 15:56:10 +01:00
Martyn Ranyard deb12318b6 Add web server that is not useful 2020-02-14 11:42:38 +01:00
8 changed files with 457 additions and 16 deletions

View File

@ -10,4 +10,6 @@ FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /go/src/git.martyn.berlin/martyn/karaokards /app/ COPY --from=builder /go/src/git.martyn.berlin/martyn/karaokards /app/
COPY strings.json /app/strings.json COPY strings.json /app/strings.json
COPY web/ /app/web/
WORKDIR /app
CMD ["/app/karaokards"] CMD ["/app/karaokards"]

View File

@ -34,6 +34,9 @@ spec:
- image: imartyn/karaokardbot:0.0.1 - image: imartyn/karaokardbot:0.0.1
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
name: kardbot name: kardbot
ports:
- name: web
containerPort: 5353
resources: {} resources: {}
terminationMessagePath: /dev/termination-log terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File terminationMessagePolicy: File

View File

@ -21,9 +21,9 @@ const PSTFormat = "Jan 2 15:04:05 PST"
// Regex for parsing PRIVMSG strings. // Regex for parsing PRIVMSG strings.
// //
// First matched group is the user's name and the second matched group is the content of the // 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. // user's message.
var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #\w+(?: :(.*))?$`) var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #(\w+)(?: :(.*))?$`)
// Regex for parsing user commands, from already parsed PRIVMSG strings. // Regex for parsing user commands, from already parsed PRIVMSG strings.
// //
@ -38,6 +38,9 @@ type OAuthCred struct {
// The developer application client ID. Used for API calls to Twitch. // The developer application client ID. Used for API calls to Twitch.
ClientID string `json:"client_id,omitempty"` ClientID string `json:"client_id,omitempty"`
// List of Channels to join
Channels []string `json:"channels,omitempty"`
} }
type KardBot struct { type KardBot struct {
@ -111,10 +114,11 @@ func (bb *KardBot) HandleChat() error {
if nil != matches { if nil != matches {
userName := matches[1] userName := matches[1]
msgType := matches[2] msgType := matches[2]
channel := matches[3]
switch msgType { switch msgType {
case "PRIVMSG": case "PRIVMSG":
msg := matches[3] msg := matches[4]
rgb.GPrintf("[%s] %s: %s\n", TimeStamp(), userName, msg) rgb.GPrintf("[%s] %s: %s\n", TimeStamp(), userName, msg)
// parse commands from user message // parse commands from user message
@ -124,9 +128,9 @@ func (bb *KardBot) HandleChat() error {
switch cmd { switch cmd {
case "card": case "card":
rgb.CPrintf("[%s] Card asked for!\n", TimeStamp()) 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))]) bb.Say("Your prompt is : "+bb.Prompts[rand.Intn(len(bb.Prompts))], channel)
} }
// channel-owner specific commands // channel-owner specific commands
@ -147,6 +151,7 @@ func (bb *KardBot) HandleChat() error {
} }
default: default:
// do nothing // do nothing
rgb.YPrintf("[%s] unknown IRC message : %s\n", TimeStamp(), line)
} }
} }
} }
@ -154,14 +159,24 @@ func (bb *KardBot) HandleChat() error {
} }
} }
// Makes the bot join its pre-specified channel. // Login to the IRC server
func (bb *KardBot) JoinChannel() { func (bb *KardBot) Login() {
rgb.YPrintf("[%s] Joining #%s...\n", TimeStamp(), bb.Channel) rgb.YPrintf("[%s] Logging into #%s...\n", TimeStamp(), bb.Channel)
bb.conn.Write([]byte("PASS " + bb.Credentials.Password + "\r\n")) bb.conn.Write([]byte("PASS " + bb.Credentials.Password + "\r\n"))
bb.conn.Write([]byte("NICK " + bb.Name + "\r\n")) bb.conn.Write([]byte("NICK " + bb.Name + "\r\n"))
bb.conn.Write([]byte("JOIN #" + bb.Channel + "\r\n")) }
rgb.YPrintf("[%s] Joined #%s as @%s!\n", TimeStamp(), bb.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 Credentials field. // Reads from the private credentials file and stores the data in the bot's Credentials field.
@ -185,7 +200,7 @@ func (bb *KardBot) ReadCredentials() error {
} }
// Makes the bot send a message to the chat channel. // Makes the bot send a message to the chat channel.
func (bb *KardBot) Say(msg string) error { func (bb *KardBot) Say(msg string, channels ...string) error {
if "" == msg { if "" == msg {
return errors.New("BasicBot.Say: msg was empty.") return errors.New("BasicBot.Say: msg was empty.")
} }
@ -195,10 +210,18 @@ func (bb *KardBot) Say(msg string) error {
return errors.New("BasicBot.Say: msg exceeded 512 bytes") return errors.New("BasicBot.Say: msg exceeded 512 bytes")
} }
_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG #%s :%s\r\n", bb.Channel, msg))) 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 { if nil != err {
return err return err
} }
}
return nil return nil
} }
@ -215,7 +238,12 @@ func (bb *KardBot) Start() {
for { for {
bb.Connect() bb.Connect()
bb.Login()
if len(bb.Credentials.Channels) > 0 {
bb.JoinChannel(bb.Credentials.Channels...)
} else {
bb.JoinChannel() bb.JoinChannel()
}
err = bb.HandleChat() err = bb.HandleChat()
if nil != err { if nil != err {

101
internal/webserver/webserver.go Executable file
View File

@ -0,0 +1,101 @@
package webserver
import (
"math/rand"
irc "git.martyn.berlin/martyn/karaokards/internal/irc"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"fmt"
"html/template"
"net/http"
"os"
"strings"
"time"
)
//var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY"))
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)
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
}
// 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), len(ircBot.Credentials.Channels), 0}
err = tmpl.Execute(response, td)
if err != nil {
http.Error(response, err.Error(), http.StatusInternalServerError)
return
}
}
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("/example/{.*}", TemplateHandler)
r.HandleFunc("/cover.css", CSSHandler)
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()
}

View File

@ -11,6 +11,7 @@ import (
builtins "git.martyn.berlin/martyn/karaokards/internal/builtins" builtins "git.martyn.berlin/martyn/karaokards/internal/builtins"
irc "git.martyn.berlin/martyn/karaokards/internal/irc" irc "git.martyn.berlin/martyn/karaokards/internal/irc"
webserver "git.martyn.berlin/martyn/karaokards/internal/webserver"
rgb "github.com/foresthoffman/rgblog" rgb "github.com/foresthoffman/rgblog"
) )
@ -26,8 +27,8 @@ func readBonusStrings() []string {
ex, err := os.Executable() ex, err := os.Executable()
if err != nil { if err != nil {
return []string{}
fmt.Println("Could not read `strings.json`, will only have builtin prompts. File reading error", err) fmt.Println("Could not read `strings.json`, will only have builtin prompts. File reading error", err)
return []string{}
} }
exPath := filepath.Dir(ex) exPath := filepath.Dir(ex)
data, err := ioutil.ReadFile(exPath + "/strings.json") data, err := ioutil.ReadFile(exPath + "/strings.json")
@ -83,5 +84,9 @@ func main() {
Server: "irc.chat.twitch.tv", Server: "irc.chat.twitch.tv",
Prompts: selectablePrompts, Prompts: selectablePrompts,
} }
go func() {
rgb.YPrintf("[%s] Starting webserver on port %s\n", irc.TimeStamp(), "5353")
webserver.HandleHTTP(myBot)
}()
myBot.Start() myBot.Start()
} }

106
web/404.html Normal file
View File

@ -0,0 +1,106 @@
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Karaokards">
<meta name="author" content="Martyn Ranyard">
<title>The great unknown!</title>
<!-- Bootstrap core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/cover.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
li.match-nomatch{
background-color: #1e2122;
}
li.match-matchtrack{
background-color: #E9B000;
}
li.match-fullmatch{
background-color: #008F95;
}
li.match-matchtrackfuzzt{
background-color: darkgray;
}
li.match-fullmatchfuzzy{
background-color: darkgray;
}
a{
text-decoration-line: underline;
}
</style>
</head>
<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">k8s-zoo</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="/">Home</a>
</nav>
</div>
</header>
<main role="main" class="inner cover">
<h1 class="cover-heading">Ooops!</h1>
<p>It seems you've gone somewhere you shouldn't! 404 NOT FOUND!</p>
<p/>
<p>I'm not quite sure how you got here to be honest, if it was via a link on the site, let me know via twitch DM, if it was from someone else, let them know.</p>
<p>Shameless self-promotion : Follow me on twitch - <a href="https://www.twitch.tv/iMartynOnTwitch">iMartynOnTwitch</a>, oddly enough, I do a lot of twitchsings!</p>
</main>
<footer class="mastfoot mt-auto">
<div class="inner">
<p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p>
</div>
</footer>
</div>
<script>
function directToResults() {
var url = document.createElement('a');
url.setAttribute("href", window.location.href);
if ((url.port != 80) && (url.port != 443)) {
customPort = ":"+url.port
} else {
customPort = ""
}
var destination = url.protocol + "//" + url.hostname + customPort + "/" + document.getElementById("mode").value + "/" + document.getElementById("spotifyid").value
window.location.href = destination
}
function toggleUnfound() {
var unmatched = document.getElementsByClassName('match-nomatch'), i;
if (document.getElementById("showhidebutton").getAttribute("tracksHidden") != "true") {
document.getElementById("showhidebutton").setAttribute("tracksHidden","true")
for (i = 0; i < unmatched.length; i += 1) {
unmatched[i].style.display = 'none';
}
} else {
document.getElementById("showhidebutton").setAttribute("tracksHidden","false")
for (i = 0; i < unmatched.length; i += 1) {
unmatched[i].style.display = 'list-item';
}
}
}
</script>
</body>
</html>
</body>
</html>

106
web/cover.css Normal file
View File

@ -0,0 +1,106 @@
/*
* Globals
*/
/* Links */
a,
a:focus,
a:hover {
color: #fff;
}
/* Custom default button */
.btn-secondary,
.btn-secondary:hover,
.btn-secondary:focus {
color: #333;
text-shadow: none; /* Prevent inheritance from body */
background-color: #fff;
border: .05rem solid #fff;
}
/*
* Base structure
*/
html,
body {
height: 100%;
background-color: #333;
}
body {
display: -ms-flexbox;
display: flex;
color: #fff;
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
}
.cover-container {
max-width: 142em;
}
/*
* Header
*/
.masthead {
margin-bottom: 2rem;
}
.masthead-brand {
margin-bottom: 0;
}
.nav-masthead .nav-link {
padding: .25rem 0;
font-weight: 700;
color: rgba(255, 255, 255, .5);
background-color: transparent;
border-bottom: .25rem solid transparent;
}
.nav-masthead .nav-link:hover,
.nav-masthead .nav-link:focus {
border-bottom-color: rgba(255, 255, 255, .25);
}
.nav-masthead .nav-link + .nav-link {
margin-left: 1rem;
}
.nav-masthead .active {
color: #fff;
border-bottom-color: #fff;
}
@media (min-width: 48em) {
.masthead-brand {
float: left;
}
.nav-masthead {
float: right;
}
}
/*
* Cover
*/
.cover {
padding: 0 1.5rem;
}
.cover .btn-lg {
padding: .75rem 1.25rem;
font-weight: 700;
}
/*
* Footer
*/
.mastfoot {
color: rgba(255, 255, 255, .5);
}

90
web/index.html Normal file
View File

@ -0,0 +1,90 @@
<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">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>
<script>
function releaseAnimal() {
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 + "/release"
window.location.href = destination
}
</script>
</body>
</html>
</body>
</html>