new init without secrets
This commit is contained in:
commit
64b6e71b0e
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
**/*.db
|
||||||
|
**/*.db-shm
|
||||||
|
**/*.db-wal
|
||||||
|
**/*.log
|
||||||
|
**/.idea
|
||||||
|
**/.vscode
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/build
|
||||||
49
.gitea/workflows/deploy.yml
Normal file
49
.gitea/workflows/deploy.yml
Normal 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
|
||||||
53
.gitea/workflows/docker.yml
Normal file
53
.gitea/workflows/docker.yml
Normal 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
29
Dockerfile
Normal 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
209
bot/bot.go
Normal 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
21
docker-compose.yml
Normal 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
30
flake.nix
Normal 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
100
game/wordle.go
Normal 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
29
go.mod
Normal 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
31
krkrkr/krkrkr.go
Normal 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
184
main.go
Normal 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
267751
sowpods.csv
Normal file
File diff suppressed because it is too large
Load Diff
139
wordleimg/skibidi/skibidi.go
Normal file
139
wordleimg/skibidi/skibidi.go
Normal 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
189
wordleimg/wordleimg.go
Normal 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
73
words/words.go
Normal 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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user