initial commit

This commit is contained in:
Waldo 2026-01-17 11:22:16 -07:00
commit 5e84ebf0b7
7 changed files with 265 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/bin

3
Makefile Normal file
View File

@ -0,0 +1,3 @@
build:
go build -o bin/ .

14
README.md Normal file
View File

@ -0,0 +1,14 @@
# Go bot example
This directory contains an example of a simple Campfire bot written in Go.
This bot implments a single endpoint, `/trace`. You can message this endpoint
with a URL. The bot will make a `GET` request to that URL, and respond with some
timings about how long parts of that request took: DNS lookup, time to first
byte, and so on.
The functionality of the bot is basic. But this example shows how you can:
- Start an HTTP service to listen on a bot endpoint
- Parse the JSON from the message request
- Respond to that request with some HTML-formatted text

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module sox
go 1.22.0

149
main.go Normal file
View File

@ -0,0 +1,149 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"os/exec"
"bytes"
// "log"
"time"
"strings"
)
type BotMessage struct {
User struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"user"`
Room struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"room"`
Message struct {
ID int `json:"id"`
Body struct {
HTML string `json:"html"`
Plain string `json:"plain"`
} `json:"body"`
} `json:"message"`
}
func main() {
http.HandleFunc("/", commandHandler)
port := os.Getenv("PORT")
if port == "" {
port = "8096"
}
panic(http.ListenAndServe(":"+port, nil))
}
func SplitFirstWord(input string) (firstWord string, restOfString string) {
words := strings.Fields(input)
if len(words) == 0 {
return "", ""
}
firstWord = words[0]
restOfString = strings.Join(words[1:], " ")
return strings.ToLower(firstWord), restOfString
}
func commandHandler(w http.ResponseWriter, r *http.Request) {
var msg BotMessage
err := json.NewDecoder(r.Body).Decode(&msg)
if err != nil {
errorResponse(w, http.StatusBadRequest, "invalid request body")
return
}
command, arguments := SplitFirstWord(msg.Message.Body.Plain)
switch command {
case "ping":
fmt.Println("ping command received")
fmt.Fprintln(w, "Pong!")
case "trace":
fmt.Println("trace command received")
var j = traceHandler(w, r, arguments, msg.User.Name)
fmt.Fprintln(w, j)
case "math":
fmt.Fprintln(w, runMath(arguments))
case "hi":
fmt.Fprintln(w, "Hello " + msg.User.Name + "! How are you doing today?")
default:
fmt.Fprintln(w, "Command is: " + command + " arguments are: " + arguments + ". Not sure what to do...")
}
return
}
func runMath(s string)(string){
cmd := exec.Command("qalc", s)
// cmd.Stdin = strings.NewReader("and old falcon")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
// log.Warn(err)
fmt.Println(err)
}
return out.String()
// fmt.Printf("translated phrase: %q\n", out.String())
}
func traceHandler(w http.ResponseWriter, r *http.Request, urlstring string, username string)(string) {
var msg BotMessage
err := json.NewDecoder(r.Body).Decode(&msg)
uri, err := url.Parse(urlstring)
if err != nil || (uri.Scheme != "http" && uri.Scheme != "https") {
return "That doesn't look like a valid URL for to me to call"
}
response, err := timeRequest(username, uri)
if err != nil {
// fmt.Fprintf(w, "Failed to time the request (%s)", err)
// return
return "Failed to time the request " //+ err
}
// fmt.Fprintln(w, response)
return response
}
func timeRequest(username string, uri *url.URL) (string, error) {
trace, err := TraceRequest(uri)
if err != nil {
return "", err
}
result := fmt.Sprintf("Hi %s! I've checked %s for you, and here's what I found:<br><br>\n", username, uri)
result += formatDuration("DNS lookup", trace.DNSStart, trace.DNSDone)
result += formatDuration("Connect", trace.ConnectStart, trace.ConnectDone)
result += formatDuration("TLS negotiation", trace.TLSStart, trace.TLSDone)
result += formatDuration("Sending headers", trace.ConnectionReady(), trace.WroteHeaders)
result += formatDuration("Time to first byte", trace.WroteHeaders, trace.FirstByte)
result += formatDuration("Time to last byte", trace.WroteHeaders, trace.AllDone)
return result, nil
}
func errorResponse(w http.ResponseWriter, status int, msg string) {
w.WriteHeader(status)
fmt.Fprintf(w, "Error: %s", msg)
}
func formatDuration(label string, start, end time.Time) string {
dur := end.Sub(start).String()
if start.IsZero() || end.IsZero() {
dur = "n/a"
}
return fmt.Sprintf("%s: <strong>%s</strong><br>\n", label, dur)
}

23
package.nix Normal file
View File

@ -0,0 +1,23 @@
{ pkgs, lib }:
pkgs.stdenv.mkDerivation {
pname = "sox";
version = "1.0";
src = ./.;
buildInputs = [ pkgs.go pkgs.libqalculate ];
buildPhase = ''
# Nothing to build for a static site
# cp -r $src/* $out/var/www/my-static-site/
mkdir -p $out/bin
go build -o $out/bin/ $src/
'';
installPhase = ''
chmod +x $out/bin/sox
'';
}

72
request_trace.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"context"
"crypto/tls"
"io"
"net/http"
"net/http/httptrace"
"net/url"
"time"
)
type RequestTrace struct {
Started time.Time
DNSStart time.Time
DNSDone time.Time
ConnectStart time.Time
ConnectDone time.Time
TLSStart time.Time
TLSDone time.Time
WroteHeaders time.Time
FirstByte time.Time
AllDone time.Time
}
func (t RequestTrace) ConnectionReady() time.Time {
if t.TLSDone.After(t.ConnectDone) {
return t.TLSDone
}
return t.ConnectDone
}
func TraceRequest(uri *url.URL) (RequestTrace, error) {
var trace RequestTrace
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) { trace.Started = time.Now() },
DNSStart: func(info httptrace.DNSStartInfo) { trace.DNSStart = time.Now() },
DNSDone: func(info httptrace.DNSDoneInfo) { trace.DNSDone = time.Now() },
ConnectStart: func(network, addr string) { trace.ConnectStart = time.Now() },
ConnectDone: func(network, addr string, err error) { trace.ConnectDone = time.Now() },
TLSHandshakeStart: func() { trace.TLSStart = time.Now() },
TLSHandshakeDone: func(tls.ConnectionState, error) { trace.TLSDone = time.Now() },
WroteHeaders: func() { trace.WroteHeaders = time.Now() },
GotFirstResponseByte: func() { trace.FirstByte = time.Now() },
})
req, err := http.NewRequest("GET", uri.String(), nil)
req = req.WithContext(ctx)
if err != nil {
return trace, err
}
t := http.DefaultTransport.(*http.Transport).Clone()
t.DisableKeepAlives = true
c := &http.Client{Transport: t}
resp, err := c.Do(req)
if err != nil {
return trace, err
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
trace.AllDone = time.Now()
return trace, nil
}