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