new init without secrets
Some checks failed
Deploy Matrix Bot / deploy (push) Waiting to run
Docker Build / build (push) Failing after 20m2s

This commit is contained in:
shinya 2026-03-04 22:00:42 +01:00
commit 64b6e71b0e
15 changed files with 268900 additions and 0 deletions

13
.dockerignore Normal file
View File

@ -0,0 +1,13 @@
.git
.gitignore
.DS_Store
.env
**/*.db
**/*.db-shm
**/*.db-wal
**/*.log
**/.idea
**/.vscode
**/node_modules
**/dist
**/build

View File

@ -0,0 +1,49 @@
name: Deploy Matrix Bot
on:
push:
branches: ["main"]
workflow_dispatch:
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build image
run: docker build -t matrix-bot:latest .
- name: Stop and remove old container
run: |
docker stop matrix-bot || true
docker rm matrix-bot || true
- name: Run container
env:
MATRIX_HOMESERVER: ${{ secrets.MATRIX_HOMESERVER }}
MATRIX_USER_ID: ${{ secrets.MATRIX_USER_ID }}
MATRIX_ACCESS_TOKEN: ${{ secrets.MATRIX_ACCESS_TOKEN }}
MATRIX_DEVICE_ID: ${{ secrets.MATRIX_DEVICE_ID }}
MATRIX_PICKLE_KEY: ${{ secrets.MATRIX_PICKLE_KEY }}
MATRIX_RECOVERY_KEY: ${{ secrets.MATRIX_RECOVERY_KEY }}
MATRIX_USERNAME: ${{ secrets.MATRIX_USERNAME }}
MATRIX_PASSWORD: ${{ secrets.MATRIX_PASSWORD }}
MATRIX_ROOM_ID: ${{ secrets.MATRIX_ROOM_ID }}
run: |
docker run -d --name matrix-bot \
--restart unless-stopped \
-e MATRIX_HOMESERVER \
-e MATRIX_USER_ID \
-e MATRIX_ACCESS_TOKEN \
-e MATRIX_DEVICE_ID \
-e MATRIX_PICKLE_KEY \
-e MATRIX_RECOVERY_KEY \
-e MATRIX_USERNAME \
-e MATRIX_PASSWORD \
-e MATRIX_ROOM_ID \
-e MATRIX_CRYPTO_DB=/data/crypto.db \
-e MATRIX_WORDLIST=/data/sowpods.csv \
-v /DATA/AppData/matrix-bot:/data \
matrix-bot:latest

View File

@ -0,0 +1,53 @@
name: Docker Build
on:
push:
branches: ["main"]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: matrix-bot:latest
- name: Run container
env:
MATRIX_HOMESERVER: ${{ secrets.MATRIX_HOMESERVER }}
MATRIX_USER_ID: ${{ secrets.MATRIX_USER_ID }}
MATRIX_ACCESS_TOKEN: ${{ secrets.MATRIX_ACCESS_TOKEN }}
MATRIX_DEVICE_ID: ${{ secrets.MATRIX_DEVICE_ID }}
MATRIX_PICKLE_KEY: ${{ secrets.MATRIX_PICKLE_KEY }}
MATRIX_RECOVERY_KEY: ${{ secrets.MATRIX_RECOVERY_KEY }}
MATRIX_USERNAME: ${{ secrets.MATRIX_USERNAME }}
MATRIX_PASSWORD: ${{ secrets.MATRIX_PASSWORD }}
MATRIX_ROOM_ID: ${{ secrets.MATRIX_ROOM_ID }}
run: |
docker run --rm \
-e MATRIX_HOMESERVER \
-e MATRIX_USER_ID \
-e MATRIX_ACCESS_TOKEN \
-e MATRIX_DEVICE_ID \
-e MATRIX_PICKLE_KEY \
-e MATRIX_RECOVERY_KEY \
-e MATRIX_USERNAME \
-e MATRIX_PASSWORD \
-e MATRIX_ROOM_ID \
-e MATRIX_WORDLIST=/data/sowpods.csv \
-e MATRIX_CRYPTO_DB=/data/crypto.db \
-v matrix-bot-data:/data \
matrix-bot:latest

29
Dockerfile Normal file
View File

@ -0,0 +1,29 @@
FROM golang:1.25.5-alpine AS build
WORKDIR /app
RUN apk add --no-cache ca-certificates olm-dev build-base
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o matrix-bot ./
FROM alpine:3.19
WORKDIR /app
RUN apk add --no-cache ca-certificates olm
RUN mkdir -p /data
COPY --from=build /app/matrix-bot /app/matrix-bot
COPY --from=build /app/sowpods.csv /data/sowpods.csv
COPY --from=build /app/words.csv /app/words.csv
COPY --from=build /app/words-bad.csv /app/words-bad.csv
COPY --from=build /app/words-def.csv /app/words-def.csv
VOLUME ["/data"]
ENV MATRIX_CRYPTO_DB=/data/crypto.db
ENV MATRIX_WORDLIST=/data/sowpods.csv
CMD ["/app/matrix-bot"]

209
bot/bot.go Normal file
View File

@ -0,0 +1,209 @@
package bot
import (
"bytes"
"context"
"log"
"matrix-bot/game"
"matrix-bot/wordleimg"
"matrix-bot/words"
"os"
"strconv"
"strings"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
)
var dict *words.Dictionary
type roomUserPair struct {
room string
user string
}
var wordleGames = make(map[roomUserPair]game.Wordle)
var userSettings = make(map[string]map[string]string)
var defaultSettings = map[string]string{
"wordletheme": "dark",
}
func Load() {
var err error
wordListPath := os.Getenv("MATRIX_WORDLIST")
if wordListPath == "" {
wordListPath = "sowpods.csv"
}
dict, err = words.New(wordListPath)
if err != nil {
log.Fatal(err)
}
}
func HandleCommand(body string, sender string, roomID string,
ctx context.Context, client *mautrix.Client, evt *event.Event, replyTo *event.RelatesTo) []interface{} {
pair := roomUserPair{roomID, sender}
part := strings.Fields(strings.ToLower(body))
_, e := userSettings[sender]
if !e {
userSettings[sender] = make(map[string]string)
for key, value := range defaultSettings {
userSettings[sender][key] = value
}
}
us := userSettings[sender]
if equals(part[0], "!settings", "!setts", "!stns", "!?") {
if len(part) < 2 {
} else if equals(part[1], "set", "add", "s", "a") {
if len(part) == 4 {
userSettings[sender][part[2]] = part[3]
us = userSettings[sender]
}
} else if equals(part[1], "delete", "del", "remove", "rem", "d", "r") {
if len(part) == 3 {
delete(userSettings[sender], part[2])
}
}
b := strings.Builder{}
b.WriteString("Your settings: ")
for k, s := range us {
b.WriteString("\n" + k + " : " + s)
}
return jia(txt(b.String(), replyTo))
} else if equals(part[0], "!wordle", "!wrdl", "!w") {
wg, wgEx := wordleGames[pair]
wordleState := func() event.MessageEventContent {
state := wg.GetState()
if setting(us, "wordletheme") == "ascii" {
return txt(state, nil)
} else {
theme, e := wordleimg.Themes[setting(us, "wordletheme")]
if !e {
theme = wordleimg.Themes["dark"]
}
imgData, w, h, err := wordleimg.RenderHistoryImage(state, theme, len(wg.GetSecret()), 6)
if err != nil {
log.Println("Image render error:", err)
}
return img(imgData, w, h, state, ctx, client)
}
}
if len(part) < 2 {
part = append(part, "daily")
}
if equals(part[1], "daily", "day", "d") {
if wgEx {
return jia(txt("Wordle:\nAlready exist", replyTo))
}
word := dict.DailyWord(5)
wordleGames[pair] = game.NewWordle(word, 6)
return jia(txt("Wordle:\nDaily started ("+roomID+" "+sender+")\nLength: "+
strconv.Itoa(len(word))+"\n[CAN BE PLAYED ONCE A DAY, DO NOT END!]", replyTo))
} else if equals(part[1], "normal", "play", "p") {
if wgEx {
return jia(txt("Wordle:\nAlready exist", replyTo))
}
length := 5
if len(part) > 2 {
if l, err := strconv.Atoi(part[2]); err == nil {
length = l
}
}
word, err := dict.RandomWord(length)
if err != nil {
return jia(txt("Wordle:\nNo words of that length", replyTo))
}
wordleGames[pair] = game.NewWordle(word, 6)
return jia(txt("Wordle:\nInitilized ("+roomID+" "+sender+")\nLength: "+strconv.Itoa(length), replyTo))
} else if equals(part[1], "guess", "gus", "g") && len(part) > 2 {
if !wgEx {
return jia(txt("Wordle:\nNot found (use !wordle play)", replyTo))
} else if !wg.ValidLength(part[2]) {
return jia(txt("Wordle:\nWord guess length is bad", replyTo))
} else if !dict.Contains(part[2]) {
return jia(txt("Wordle:\nSorry but idk that word", replyTo))
}
sig := wg.Guess(part[2])
if sig == 0 {
return jia(txt("Wordle:", replyTo), wordleState())
}
delete(wordleGames, pair)
switch sig {
case -1:
return jia(txt("Wordle:\nEnded in lost, word was "+wg.GetSecret(), replyTo), wordleState())
case 1:
return jia(txt("Wordle:\nYou guessed right!", replyTo), wordleState())
}
} else if equals(part[1], "status", "stat", "s") {
if !wgEx {
return jia(txt("Wordle:\nNone found", replyTo))
}
return jia(txt("Wordle:", replyTo), wordleState())
} else if equals(part[1], "forfit", "end", "e") {
if !wgEx {
return jia(txt("Wordle:\nNone to end", replyTo))
}
delete(wordleGames, pair)
return jia(txt("Wordle:\nEnded", replyTo))
}
}
return nil
}
func jia(i ...interface{}) []interface{} {
return i
}
func txt(body string, relates *event.RelatesTo) event.MessageEventContent {
return event.MessageEventContent{
MsgType: event.MsgText,
Body: body,
RelatesTo: relates,
}
}
func img(data []byte, width int, height int, body string, ctx context.Context, client *mautrix.Client) event.MessageEventContent {
uploadResp, err := client.UploadMedia(
ctx,
mautrix.ReqUploadMedia{
FileName: "temp.png",
Content: bytes.NewReader(data),
ContentType: "image/png",
},
)
if err != nil {
log.Println("Upload error:", err)
return event.MessageEventContent{}
}
return event.MessageEventContent{
MsgType: event.MsgImage,
Body: body,
URL: uploadResp.ContentURI.CUString(),
Info: &event.FileInfo{
MimeType: "image/png",
Size: len(data),
Height: height,
Width: width,
},
}
}
func setting(us map[string]string, key string) string {
if v, e := us[key]; e {
return v
}
return ""
}
func equals(body string, prefixes ...string) bool {
for _, prefix := range prefixes {
if strings.Compare(body, prefix) == 0 {
return true
}
}
return false
}

21
docker-compose.yml Normal file
View File

@ -0,0 +1,21 @@
services:
matrix-bot:
build: .
image: matrix-bot:latest
container_name: matrix-bot
restart: unless-stopped
environment:
MATRIX_HOMESERVER: ${MATRIX_HOMESERVER}
MATRIX_USER_ID: ${MATRIX_USER_ID}
MATRIX_ACCESS_TOKEN: ${MATRIX_ACCESS_TOKEN}
MATRIX_DEVICE_ID: ${MATRIX_DEVICE_ID}
MATRIX_PICKLE_KEY: ${MATRIX_PICKLE_KEY}
MATRIX_RECOVERY_KEY: ${MATRIX_RECOVERY_KEY}
MATRIX_CRYPTO_DB: /data/crypto.db
MATRIX_WORDLIST: /data/sowpods.csv
MATRIX_USERNAME: ${MATRIX_USERNAME}
MATRIX_PASSWORD: ${MATRIX_PASSWORD}
MATRIX_ROOM_ID: ${MATRIX_ROOM_ID}
volumes:
- /DATA/AppData/matrix-bot:/data
- /DATA/AppData/matrix-bot/sowpods.csv:/data/sowpods.csv:ro

30
flake.nix Normal file
View File

@ -0,0 +1,30 @@
{
description = "Go project with mautrix and libolm";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
in
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go
pkg-config
olm
];
# Needed so CGO can find olm/olm.h and libolm.so
shellHook = ''
export CGO_ENABLED=1
'';
};
});
}

100
game/wordle.go Normal file
View File

@ -0,0 +1,100 @@
package game
import (
"strings"
)
type Wordle interface {
Guess(string) int
GetState() string
GetSecret() string
ValidLength(string) bool
}
type wordle struct {
secret string
maxAttempts int
history []string
}
func NewWordle(secret string, maxAttempts int) Wordle {
return &wordle{
secret: secret,
maxAttempts: maxAttempts,
history: make([]string, 0),
}
}
func (w *wordle) Guess(guess string) int {
guess = strings.ToUpper(guess)
line, yes := wordleCompare([]rune(guess), []rune(w.secret))
w.history = append(w.history, line)
st := 0
if yes {
st = 1
} else if len(w.history) >= w.maxAttempts {
st = -1
}
return st
}
func wordleCompare(guess, secret []rune) (string, bool) {
yes := true
leng := len(secret)
state := make([]int, leng)
result := strings.Builder{}
for n := 0; n < leng; n++ {
if guess[n] == secret[n] {
state[n] = 2
secret[n] = ' '
} else {
yes = false
}
}
for n := 0; n < leng; n++ {
if state[n] == 0 {
for n2 := 0; n2 < leng; n2++ {
if n == n2 {
continue
} else if guess[n] == secret[n2] {
state[n] = 1
secret[n2] = ' '
break
}
}
}
start := '.'
end := '.'
if state[n] == 1 {
start = '('
end = ')'
}
if state[n] == 2 {
start = '['
end = ']'
}
result.WriteRune(start)
result.WriteRune(guess[n])
result.WriteRune(end)
}
return result.String(), yes
}
func (w *wordle) GetState() string {
state := strings.Builder{}
for n, h := range w.history {
state.WriteString(h)
if n != len(w.history)-1 {
state.WriteRune('\n')
}
}
return state.String()
}
func (w *wordle) GetSecret() string {
return w.secret
}
func (w *wordle) ValidLength(s string) bool {
return len(s) == len(w.secret)
}

29
go.mod Normal file
View File

@ -0,0 +1,29 @@
module matrix-bot
go 1.25.5
require (
github.com/fogleman/gg v1.3.0
maunium.net/go/mautrix v0.26.2
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.33 // indirect
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
go.mau.fi/util v0.9.5 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/image v0.36.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
)

31
krkrkr/krkrkr.go Normal file
View File

@ -0,0 +1,31 @@
package krkrkr
import "time"
func Today() int {
return hashRounds(100, int(time.Now().Unix()/86400))
}
func hashRounds(rounds int, start int) int {
for i := 0; i < rounds; i++ {
start = hash(start)
}
return start
}
func hash(x int) int {
for i := 0; i < 5; i++ {
o := x
o ^= 0xDADADADA
o *= 0xBAADFACC
o ^= 0xFEEDFACE
o += 0xC0FFEE
o = (o << 13) | (o >> (32 - 13))
o *= o
o ^= (o >> 17)
o *= 0x5DEECE66D
o ^= 0xB16B00B5
x = o % 0x7FFFFFFF
}
return x & 0x7FFFFFFF
}

184
main.go Normal file
View File

@ -0,0 +1,184 @@
package main
import (
"context"
"log"
"matrix-bot/bot"
"os"
"path/filepath"
"sync"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var homeserver string
var username string
var password string
var roomID string
var userId string
var accessToken string
var deviceId string
var pickleKeyString string
var recoveryKey string
var cryptoDBPath string
func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) {
// remember to use a secure key for the pickle key in production
pickleKey := []byte(pickleKeyString)
if cryptoDBPath != "" {
dir := filepath.Dir(cryptoDBPath)
if dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
}
}
helper, err := cryptohelper.NewCryptoHelper(cli, pickleKey, cryptoDBPath)
if err != nil {
return nil, err
}
err = helper.Init(context.Background())
if err != nil {
return nil, err
}
return helper, nil
}
func verifyWithRecoveryKey(machine *crypto.OlmMachine) (err error) {
ctx := context.Background()
keyId, keyData, err := machine.SSSS.GetDefaultKeyData(ctx)
if err != nil {
return
}
key, err := keyData.VerifyRecoveryKey(keyId, recoveryKey)
if err != nil {
return
}
err = machine.FetchCrossSigningKeysFromSSSS(ctx, key)
if err != nil {
return
}
err = machine.SignOwnDevice(ctx, machine.OwnIdentity())
if err != nil {
return
}
err = machine.SignOwnMasterKey(ctx)
return
}
func envOrFatal(key string) string {
val := os.Getenv(key)
if val == "" {
log.Fatalf("missing required env var: %s", key)
}
return val
}
func envOrDefault(key string, def string) string {
val := os.Getenv(key)
if val == "" {
return def
}
return val
}
func loadConfig() {
homeserver = envOrFatal("MATRIX_HOMESERVER")
username = envOrDefault("MATRIX_USERNAME", "")
password = envOrDefault("MATRIX_PASSWORD", "")
roomID = envOrDefault("MATRIX_ROOM_ID", "")
userId = envOrFatal("MATRIX_USER_ID")
accessToken = envOrFatal("MATRIX_ACCESS_TOKEN")
deviceId = envOrFatal("MATRIX_DEVICE_ID")
pickleKeyString = envOrFatal("MATRIX_PICKLE_KEY")
recoveryKey = envOrFatal("MATRIX_RECOVERY_KEY")
cryptoDBPath = envOrDefault("MATRIX_CRYPTO_DB", "crypto.db")
}
func main() {
var err error
loadConfig()
bot.Load()
client, err := mautrix.NewClient(homeserver, id.UserID(userId), accessToken)
if err != nil {
log.Fatal(err)
}
client.DeviceID = id.DeviceID(deviceId)
syncer := mautrix.NewDefaultSyncer()
client.Syncer = syncer
cryptoHelper, err := setupCryptoHelper(client)
if err != nil {
log.Fatal(err)
}
client.Crypto = cryptoHelper
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
// Ignore our own messages
if evt.Sender.String() == userId {
return
}
content := evt.Content.AsMessage()
if content.MsgType != event.MsgText {
return
}
log.Printf("Message from %s: %s\n", evt.Sender, content.Body)
response := bot.HandleCommand(content.Body, evt.Sender.String(), evt.RoomID.String(),
ctx, client, evt, &event.RelatesTo{
InReplyTo: &event.InReplyTo{EventID: evt.ID},
})
if response == nil {
return
}
for _, resp := range response {
switch r := resp.(type) {
case event.MessageEventContent:
_, err := client.SendMessageEvent(ctx, evt.RoomID, event.EventMessage, r)
if err != nil {
log.Println("Send error:", err)
}
default:
log.Println("Unknown response type")
}
}
})
ready := make(chan struct{})
var once sync.Once
syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
once.Do(func() { close(ready) })
return true
})
go func() {
if err := client.Sync(); err != nil {
log.Fatal(err)
}
}()
log.Println("Waiting for initial sync...")
<-ready
log.Println("Sync complete")
if err := verifyWithRecoveryKey(cryptoHelper.Machine()); err != nil {
log.Fatal(err)
}
log.Println("Bot is running...")
select {}
}

267751
sowpods.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,139 @@
package skibidi
import (
"bytes"
"strings"
"github.com/fogleman/gg"
)
type Theme struct {
BG string
KB_Any string
KB_Not string
KB_Maybe string
KB_Good string
KB_Txt string
Any string
Not string
Maybe string
Good string
Txt string
}
type SubTheme struct {
Round float64
Size float64
Empty_Filled bool
KB_Round float64
KB_Size float64
}
var keyboard = [][]rune{
[]rune("QWERTYUIOP"),
[]rune("ASDFGHJKL"),
[]rune("ZXCVBNM"),
}
func kb_MaxLen() int {
max := 0
for _, row := range keyboard {
if len(row) > max {
max = len(row)
}
}
return max
}
const (
edge = 20
boxB = 40
boxMarginB = 4
fontB = 30
kb_boxB = 25
kb_boxMarginB = 2
kb_fontB = 15
)
// history format has to be like [X] (X) .X.
func RenderWordle(history string, wordLen int, maxGuesses int, theme Theme, subTheme SubTheme) (img []byte, width int, height int, err error) {
lines := strings.Split(history, "\n")
wordLenF := float64(wordLen)
//maxGuessesF := float64(maxGuesses)
box := subTheme.Size * boxB
boxMargin := subTheme.Size * boxMarginB
font := subTheme.Size * fontB
kb_box := subTheme.Size * kb_boxB
kb_boxMargin := subTheme.Size * kb_boxMarginB
//kb_font := subTheme.Size * kb_fontB
wordSize := wordLenF*(box+boxMargin) - boxMargin
widthF := 2*edge + max(wordSize, float64(kb_MaxLen())*(kb_box+kb_boxMargin)-kb_boxMargin)
//heightF := 2*edge + maxGuessesF*(box+boxMargin) + float64(len(keyboard))*(kb_box+kb_boxMargin)
width = int(widthF)
height = int(height)
wordPrePad := (widthF - wordSize) / 2.0
gc := gg.NewContext(width, height)
gc.SetLineWidth(boxMargin / 2)
if err := gc.LoadFontFace("./wordleimg/static/Roboto_Condensed-Regular.ttf", font); err != nil {
return nil, 0, 0, err
}
for L := range maxGuesses {
l := float64(L)
line := ""
if len(lines) >= L {
line = lines[L]
}
boxY := edge + l*(box+boxMargin)
for N := range wordLen {
N3 := N * 3
n := float64(N)
rn := []rune{' ', ' '}
if len(line) >= N3+1 {
rn = []rune(line)[N3 : N3+1]
}
boxX := wordPrePad + n*(box+boxMargin)
gc.DrawRoundedRectangle(boxX, boxY, box, box, subTheme.Round)
if rn[0] == ' ' {
gc.SetHexColor(theme.Any)
if subTheme.Empty_Filled {
gc.Fill()
}
gc.Stroke()
continue
}
switch rn[0] {
case '.':
gc.SetHexColor(theme.Not)
case '(':
gc.SetHexColor(theme.Maybe)
case '[':
gc.SetHexColor(theme.Good)
}
gc.Fill()
gc.DrawString(string(rn[1]), boxX+box/2, boxY+box/2)
}
}
var buf bytes.Buffer
err = gc.EncodePNG(&buf)
if err != nil {
return nil, 0, 0, err
}
img = buf.Bytes()
return
}

189
wordleimg/wordleimg.go Normal file
View File

@ -0,0 +1,189 @@
package wordleimg
import (
"bytes"
"strings"
"github.com/fogleman/gg"
)
type Theme struct {
BG string
Nah string
Not string
Nearly string
Good string
Txt string
}
var Themes = map[string]Theme{
"light": {
BG: "#d7dadc",
Nah: "#161618",
Not: "#3a3a3c",
Nearly: "#b59f3b",
Good: "#538d4e",
Txt: "#d7dadc",
},
"dark": {
BG: "#121213",
Nah: "#161618",
Not: "#3a3a3c",
Nearly: "#b59f3b",
Good: "#538d4e",
Txt: "#d7dadc",
},
"nixos": {
BG: "#121213",
Nah: "#161618",
Not: "#4C6eb5",
Nearly: "#f8ac0d",
Good: "#029839",
Txt: "#d7dadc",
},
"catppuccin": {
BG: "#1e1e2e",
Nah: "#313244",
Not: "#b4befe",
Nearly: "#cba6f7",
Good: "#a6e3a1",
Txt: "#6c7086",
},
"grayscale": {
BG: "#121213",
Nah: "#161618",
Not: "#202020",
Nearly: "#505050",
Good: "#a0a0a0",
Txt: "#f0f0f0",
},
"bloody": {
BG: "#121213",
Nah: "#161618",
Not: "#8B0000",
Nearly: "#ff4500",
Good: "#92ad32",
Txt: "#d7dadc",
},
}
const box float64 = 40 * 2
const boxEdge float64 = 4 * 2
const smallBox float64 = 25 * 2
const smallBoxEdge float64 = 2 * 2
const frameEdge float64 = 20 * 2
const round float64 = 4 * 2
var keyboard = []rune("QWERTYUIOPASDFGHJKLZXCVBNM")
var keyboardRows = []float64{0, 10, 19, 26}
var krMax float64 = 10
func RenderHistoryImage(history string, theme Theme, wordSize int, maxGuesses int) ([]byte, int, int, error) {
keyboardValidity := make([]rune, 128)
lines := strings.Split(history, "\n")
sus := max(float64(wordSize), krMax/(box/smallBox))
gc := gg.NewContext(
int((box*sus)+
(2*frameEdge)+((sus-1)*boxEdge)),
int(((box+boxEdge)*float64(maxGuesses))+
(2*frameEdge)+(3*(smallBox+smallBoxEdge))))
edging := ((box * sus) + ((sus - 1) * boxEdge) - (float64(wordSize)*box + (float64(wordSize)-1)*boxEdge)) / 2
gc.SetHexColor(theme.BG)
gc.Clear()
if err := gc.LoadFontFace("./wordleimg/static/Roboto_Condensed-Regular.ttf", 24*2); err != nil {
return nil, 0, 0, err
}
for i := 0.0; i < float64(maxGuesses); i++ {
if i < float64(len(lines)) && len(lines[int(i)]) >= wordSize*3 {
runes := []rune(lines[int(i)])
for n := 0.0; n < float64(wordSize); n++ {
switch runes[int(3*n)] {
case '[':
gc.SetHexColor(theme.Good)
keyboardValidity[runes[int(3*n+1)]] = 2
case '(':
gc.SetHexColor(theme.Nearly)
if keyboardValidity[runes[int(3*n+1)]] != 2 {
keyboardValidity[runes[int(3*n+1)]] = 1
}
case '.':
gc.SetHexColor(theme.Not)
keyboardValidity[runes[int(3*n+1)]] = -1
default:
gc.SetHexColor(theme.Nah)
}
gc.DrawRoundedRectangle(
edging+frameEdge+n*(box+boxEdge),
frameEdge+i*(box+boxEdge),
box, box, round)
gc.Fill()
gc.SetHexColor(theme.Txt)
gc.DrawStringAnchored(string(runes[int(3*n+1)]), edging+
frameEdge+n*(box+boxEdge)+(box/2),
frameEdge+i*(box+boxEdge)+(box/2),
0.5, 0.5)
}
} else {
for n := 0.0; n < float64(wordSize); n++ {
gc.SetHexColor(theme.Not)
gc.DrawRoundedRectangle(
edging+frameEdge+n*(box+boxEdge),
frameEdge+i*(box+boxEdge),
box, box, round)
gc.Fill()
gc.SetHexColor(theme.BG)
gc.DrawRoundedRectangle(
edging+frameEdge+n*(box+boxEdge)+boxEdge/2,
frameEdge+i*(box+boxEdge)+boxEdge/2,
box-boxEdge, box-boxEdge, round)
gc.Fill()
}
}
}
row := 1
for n := 0.0; n < float64(len(keyboard)); n++ {
switch keyboardValidity[keyboard[int(n)]] {
case 2:
gc.SetHexColor(theme.Good)
case 1:
gc.SetHexColor(theme.Nearly)
case 0:
gc.SetHexColor(theme.Not)
case -1:
gc.SetHexColor(theme.Nah)
}
corrector := (krMax - keyboardRows[row] + keyboardRows[row-1]) / 2.0
x := frameEdge + (n-keyboardRows[row-1]+corrector)*(smallBox+smallBoxEdge)
y := frameEdge + (box+boxEdge)*float64(maxGuesses) + smallBoxEdge + float64(row-1)*(smallBox+smallBoxEdge)
gc.DrawRoundedRectangle(x, y,
smallBox, smallBox, round)
gc.Fill()
gc.SetHexColor(theme.Txt)
gc.DrawStringAnchored(string(keyboard[int(n)]),
x+(smallBox/2),
y+(smallBox/2),
0.5, 0.5)
if int(n) == int(keyboardRows[row])-1 {
row++
}
}
var buf bytes.Buffer
err := gc.EncodePNG(&buf)
if err != nil {
return nil, 0, 0, err
}
return buf.Bytes(), gc.Width(), gc.Height(), nil
}

73
words/words.go Normal file
View File

@ -0,0 +1,73 @@
package words
import (
"encoding/csv"
"fmt"
"io"
"math/rand"
"matrix-bot/krkrkr"
"os"
"slices"
"strings"
"time"
)
type Dictionary struct {
words map[int][]string // key: word length, value: slice of words of that length
rng *rand.Rand
}
func New(path string) (*Dictionary, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
reader := csv.NewReader(file)
words := make(map[int][]string)
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if len(record) > 0 {
word := strings.ToUpper(strings.TrimSpace(record[0]))
l := len(word)
if l > 0 {
words[l] = append(words[l], word)
}
}
}
return &Dictionary{
words: words,
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
}, nil
}
func (d *Dictionary) RandomWord(length int) (string, error) {
words := d.words[length]
if len(words) == 0 {
return "", fmt.Errorf("no words of length %d", length)
}
return words[d.rng.Intn(len(words))], nil
}
func (d *Dictionary) DailyWord(length int) string {
words := d.words[length]
if len(words) == 0 {
return ""
}
return words[krkrkr.Today()%len(words)]
}
func (d *Dictionary) Contains(word string) bool {
word = strings.ToUpper(strings.TrimSpace(word))
l := len(word)
return slices.Contains(d.words[l], word)
}