Add timer command with .env support

Adds a `timer` command that immediately confirms the timer and posts a
notification back to Campfire when it fires, using CAMPFIRE_URL and
CAMPFIRE_TOKEN. Adds godotenv so the bot loads a .env file automatically
on startup. Includes a .env.example documenting all env vars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Waldo 2026-03-07 07:10:36 -07:00
parent 0b2e83ba0c
commit 38635243f7
6 changed files with 80 additions and 1 deletions

10
.env.example Normal file
View File

@ -0,0 +1,10 @@
# Sox environment variables
# Port to listen on (default: 4567)
PORT=4567
# Base URL of your Campfire instance (used by the timer command to post notifications)
CAMPFIRE_URL=https://your-campfire-instance.example.com
# Token used in the notification POST URL path
CAMPFIRE_TOKEN=your_token_here

61
cmd_timer.go Normal file
View File

@ -0,0 +1,61 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
type TimerPlugin struct{}
func (p TimerPlugin) Name() string { return "timer" }
func (p TimerPlugin) ShortHelp() string { return "set a timer" }
func (p TimerPlugin) DetailedHelp() string {
return "Usage: timer <duration>\nSets a timer. When it goes off, posts a message to the room.\nExamples: timer 5m, timer 1h30m, timer 90s"
}
func (p TimerPlugin) Execute(msg BotMessage, args string) string {
d, err := time.ParseDuration(strings.TrimSpace(args))
if err != nil || d <= 0 {
return "Usage: timer &lt;duration&gt; (e.g. timer 5m, timer 1h30m)"
}
if os.Getenv("CAMPFIRE_URL") == "" || os.Getenv("CAMPFIRE_TOKEN") == "" {
return "Cannot set timer: CAMPFIRE_URL and CAMPFIRE_TOKEN must be configured."
}
firesAt := time.Now().Add(d)
go func() {
time.Sleep(d)
sendTimerNotification(msg, d)
}()
return fmt.Sprintf("Timer set for %s. Will go off at %s.", d, firesAt.Format("15:04:05"))
}
func sendTimerNotification(msg BotMessage, d time.Duration) {
baseURL := os.Getenv("CAMPFIRE_URL")
token := os.Getenv("CAMPFIRE_TOKEN")
if baseURL == "" || token == "" {
log.Println("timer: CAMPFIRE_URL or CAMPFIRE_TOKEN not set, cannot send notification")
return
}
url := fmt.Sprintf("%s/rooms/%d/%s/messages", baseURL, msg.Room.ID, token)
body := fmt.Sprintf("%s your %s timer is done!", msg.User.Name, d)
resp, err := http.Post(url, "text/plain", strings.NewReader(body))
if err != nil {
log.Printf("timer: failed to send notification: %v", err)
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
}

View File

@ -61,7 +61,7 @@
in
{
default = pkgs.mkShell {
buildInputs = with pkgs; [ go gopls gotools go-tools ];
buildInputs = with pkgs; [ go gopls gotools go-tools libqalculate ];
};
});

2
go.mod
View File

@ -1,3 +1,5 @@
module sox
go 1.22.0
require github.com/joho/godotenv v1.5.1 // indirect

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=

View File

@ -5,6 +5,8 @@ import (
"fmt"
"net/http"
"os"
"github.com/joho/godotenv"
)
type BotMessage struct {
@ -35,9 +37,11 @@ func init() {
registerPlugin(HiPlugin{})
registerPlugin(TracePlugin{})
registerPlugin(HelpPlugin{})
registerPlugin(TimerPlugin{})
}
func main() {
godotenv.Load()
fmt.Println("Sox started")
http.HandleFunc("/", commandHandler)