initial commit

This commit is contained in:
2026-01-29 21:35:36 +00:00
commit b0f9a7048a
17 changed files with 923 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
*.dll
lambda.toml
lambda-im
lambda-im.exe
*.exe

1
README.md Normal file
View File

@@ -0,0 +1 @@
# gay

101
cache.go Normal file
View File

@@ -0,0 +1,101 @@
package main
// This file caches images in memory so we are not loading them from disk every time we need them
// It also does the same for images that need to be grabbed from HTTP.
import (
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"crypto/sha256"
"fmt"
"net/http"
"io"
"os"
)
// global or app-level map/cache
var textureCache = make(map[string]gdk.Paintabler)
func getTexture(path string) gdk.Paintabler {
if tex, exists := textureCache[path]; exists {
return tex
}
tex, err := gdk.NewTextureFromFilename(path) // load once
if err != nil {
panic(err)
}
textureCache[path] = tex
return tex
}
func newPictureFromPath(path string) *gtk.Picture {
tex := getTexture(path)
img := gtk.NewPictureForPaintable(tex)
return img
}
func newImageFromPath(path string) *gtk.Image {
tex := getTexture(path)
img := gtk.NewImageFromPaintable(tex)
return img
}
func newPictureFromWeb(url string) *gtk.Picture {
// step 1: get a sha256 sum of the URL
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
p, ok := textureCache[sum]
if ok {
return gtk.NewPictureForPaintable(p)
}
// step 2: download it
resp, err := http.Get(url)
if err != nil {
return nil
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
// step 3: save it
err = os.WriteFile(sum, b, 0644)
if err != nil {
return nil
}
return newPictureFromPath(sum)
}
func newImageFromWeb(url string) *gtk.Image {
// step 1: get a sha256 sum of the URL
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
p, ok := textureCache[sum]
if ok {
return gtk.NewImageFromPaintable(p)
}
// step 2: download it
resp, err := http.Get(url)
if err != nil {
return nil
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
// step 3: save it
err = os.WriteFile(sum, b, 0644)
if err != nil {
return nil
}
return newImageFromPath(sum)
}

BIN
debug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

26
go.mod Normal file
View File

@@ -0,0 +1,26 @@
module lambda-im
go 1.25.6
require (
github.com/BurntSushi/toml v1.6.0
github.com/diamondburned/gotk4/pkg v0.3.1
github.com/google/uuid v1.1.1
github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030
github.com/sqweek/dialog v0.0.0-20260123140253-64c163d53aac
gosrc.io/xmpp v0.5.1
mellium.im/xmpp v0.22.0
)
require (
github.com/KarpelesLab/weak v0.1.1 // indirect
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
mellium.im/reader v0.1.0 // indirect
mellium.im/xmlstream v0.15.4 // indirect
nhooyr.io/websocket v1.6.5 // indirect
)

142
go.sum Normal file
View File

@@ -0,0 +1,142 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/KarpelesLab/weak v0.1.1 h1:fNnlPo3aypS9tBzoEQluY13XyUfd/eWaSE/vMvo9s4g=
github.com/KarpelesLab/weak v0.1.1/go.mod h1:pzXsWs5f2bf+fpgHayTlBE1qJpO3MpJKo5sRaLu1XNw=
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I=
github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I=
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diamondburned/gotk4/pkg v0.3.1 h1:uhkXSUPUsCyz3yujdvl7DSN8jiLS2BgNTQE95hk6ygg=
github.com/diamondburned/gotk4/pkg v0.3.1/go.mod h1:DqeOW+MxSZFg9OO+esk4JgQk0TiUJJUBfMltKhG+ub4=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030 h1:NFCJG3BerP/5ZLXwu08x9xDs+9p7AYFMeo5IXjGANxw=
github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030/go.mod h1:+YdGDBjXJho3QTsEntqzdm0YaiALOsz3sL6b67QLC8M=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/sqweek/dialog v0.0.0-20260123140253-64c163d53aac h1:/QqP+ajFMma4hNWQyBDVaQQhz9Z1kDyXScNWMO3owx0=
github.com/sqweek/dialog v0.0.0-20260123140253-64c163d53aac/go.mod h1:/qNPSY91qTz/8TgHEMioAUc6q7+3SOybeKczHMXFcXw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4=
gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
mellium.im/reader v0.1.0 h1:UUEMev16gdvaxxZC7fC08j7IzuDKh310nB6BlwnxTww=
mellium.im/reader v0.1.0/go.mod h1:F+X5HXpkIfJ9EE1zHQG9lM/hO946iYAmU7xjg5dsQHI=
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY=
mellium.im/xmlstream v0.15.4 h1:gLKxcWl4rLMUpKgtzrTBvr4OexPeO/edYus+uK3F6ZI=
mellium.im/xmlstream v0.15.4/go.mod h1:yXaCW2++fmVO4L9piKVkyLDqnCmictVYF7FDQW8prb4=
mellium.im/xmpp v0.22.0 h1:UthQVSwEAr7SNrmyc90c2ykGpVHxjn/3yw8Ey4+Im8s=
mellium.im/xmpp v0.22.0/go.mod h1:WSjq12nhREFD88Vy/0WD6Q8inE8t6a8w7QjzwivWitw=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=

38
gtk-helpers.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/sqweek/dialog"
)
func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) {
glib.IdleAdd(func() bool {
vAdj := scrolledWindow.VAdjustment()
vAdj.SetValue(vAdj.Upper() - vAdj.PageSize())
return false // Return false to run only once
})
}
func createTab(jid string, isMuc bool) {
_, ok := tabs[jid]
if !ok {
newTab := new(chatTab)
newTab.isMuc = isMuc
newTab.msgs = gtk.NewListBox()
newTab.msgs.SetVExpand(true)
newTab.msgs.SetShowSeparators(true)
newTab.msgs.Append(gtk.NewButtonWithLabel("Get past messages..."))
tabs[jid] = newTab
}
}
func switchToTab(jid string) {
current = jid
scroller.SetChild(tabs[current].msgs)
}
func showErrorDialog(err error) {
dialog.Message("%s", err.Error()).Title("Error").Error()
}

112
gtk-message.go Normal file
View File

@@ -0,0 +1,112 @@
package main
// This file contains the function that generates message widgets depending on message stanza
import (
"fmt"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"gosrc.io/xmpp/stanza"
"mellium.im/xmpp/jid"
"github.com/google/uuid"
"github.com/jasonlovesdoggo/gopen"
)
func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
m, ok := p.(stanza.Message)
if !ok {
return gtk.NewLabel("Unsupported message.")
}
sid := StanzaID{}
m.Get(&sid)
mainBox := gtk.NewBox(gtk.OrientationVertical, 0)
gesture := gtk.NewGestureClick()
gesture.SetButton(3) // Right click
vis := false
reactions := gtk.NewBox(gtk.OrientationHorizontal, 0)
reactions.SetVisible(false)
reaction := []string{"👍", "👎", "♥️", "🤣", "😭",}
for _, v := range reaction {
like := gtk.NewButton()
like.SetLabel(v)
like.SetHExpand(true)
like.ConnectClicked(func() {
fmt.Println("licked")
client.SendRaw(fmt.Sprintf(`
<message from='%s' to='%s' id='%s' type='%s'>
<reactions id='%s' xmlns='urn:xmpp:reactions:0'>
<reaction>%s</reaction>
</reactions>
</message>
`, m.To, jid.MustParse(m.From).Bare().String(), uuid.New().String(), m.Type, sid.ID, v))
})
reactions.Append(like)
}
gesture.Connect("pressed", func(n_press, x, y int) {
if !vis {
vis = true
reactions.SetVisible(true)
} else {
vis = false
reactions.SetVisible(false)
}
})
mainBox.AddController(gesture)
reply := Reply{}
ok = m.Get(&reply)
if ok {
replyBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
replyBox.Append(gtk.NewLabel("↱ " + jid.MustParse(reply.To).Resourcepart()))
mainBox.Append(replyBox)
}
authorBox := gtk.NewBox(gtk.OrientationHorizontal, 10)
contentBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
im := newImageFromPath("debug.png")
im.SetPixelSize(40)
authorBox.Append(im)
al := gtk.NewLabel(jid.MustParse(m.From).Resourcepart())
al.AddCSSClass("author")
authorBox.Append(al)
mlabel := gtk.NewLabel(m.Body)
// mlabel.SetMarkup(convertXEPToPango(m.Body))
mlabel.SetWrap(true)
mlabel.SetSelectable(true)
mlabel.SetHAlign(gtk.AlignFill)
contentBox.Append(mlabel)
mainBox.Append(authorBox)
mainBox.Append(contentBox)
mainBox.Append(reactions)
oob := stanza.OOB{}
ok = m.Get(&oob)
if ok {
// media := newPictureFromWeb(oob.URL)
// media.SetCanShrink(false)
// mainBox.Append(media)
// media.AddCSSClass("chat_image")
mbtn := gtk.NewButtonWithLabel("🖼️")
mbtn.ConnectClicked(func(){
gopen.Open(oob.URL)
})
authorBox.Append(mbtn)
}
return mainBox
}
func getVAdjustment(scrolledWindow *gtk.ScrolledWindow) *gtk.Adjustment {
val := scrolledWindow.ObjectProperty("vadjustment").(*gtk.Adjustment)
return val
}

19
gtk-xmpp-markup.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
_ "regexp"
)
// This file implements XEP-0393 partially by providing a function to get a Pango string.
// https://xmpp.org/extensions/xep-0393.html
func convertXEPToPango(text string) string {
/* FIXME this RegEx causes certain strings to appear invisible
text = regexp.MustCompile(`\*([^*]+)\*`).ReplaceAllString(text, "<b>$1</b>")
text = regexp.MustCompile(`_([^_]+)_`).ReplaceAllString(text, "<i>$1</i>")
text = regexp.MustCompile(`~([^~]+)~`).ReplaceAllString(text, "<s>$1</s>")
text = regexp.MustCompile("`([^`]+)`").ReplaceAllString(text, "<tt>$1</tt>")
*/
return text
}

317
main.go Normal file
View File

@@ -0,0 +1,317 @@
package main
import (
"os"
"context"
"fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/BurntSushi/toml"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"mellium.im/xmpp/jid"
"time"
_ "embed"
"encoding/xml"
"errors"
)
var loadedConfig lambdaConfig
var empty_dialog *gtk.Label
// var msgs *gtk.ListBox
var content *gtk.Widgetter
var tabs map[string]*chatTab = make(map[string]*chatTab)
var current string
var scroller *gtk.ScrolledWindow
//go:embed style.css
var styleCSS string
var client xmpp.Sender
var clientroot *xmpp.Client
var uiQueue = make(chan func(), 100)
func init() {
go func() {
for fn := range uiQueue {
glib.IdleAdd(func() bool {
fn()
return false
})
time.Sleep(10 * time.Millisecond) // Small delay between updates
}
}()
}
func main() {
b, err := os.ReadFile("lambda.toml")
if err != nil {
showErrorDialog(err)
panic(err)
}
_, err = toml.Decode(string(b), &loadedConfig)
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server,
},
Jid: loadedConfig.Username,
Credential: xmpp.Password(loadedConfig.Password),
Insecure: loadedConfig.Insecure,
StreamLogger: os.Stdout,
}
router := xmpp.NewRouter()
router.NewRoute().IQNamespaces(stanza.NSDiscoInfo).HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
iq, ok := p.(*stanza.IQ)
if !ok || iq.Type != stanza.IQTypeGet {
return
}
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
if err != nil {
panic(err)
}
identity := stanza.Identity{
Name: "Lambda",
Category: "client", // TODO: Allow spoofing on user request
Type: "pc",
}
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
{Var: "http://jabber.org/protocol/muc"},
{Var: "urn:xmpp:reply:0"},
},
}
iqResp.Payload = &payload
s.Send(iqResp)
})
router.NewRoute().IQNamespaces("jabber:iq:version").HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
iq, ok := p.(*stanza.IQ)
if !ok || iq.Type != stanza.IQTypeGet {
return
}
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
if err != nil {
panic(err)
}
v := &stanza.Version{}
v = v.SetInfo("Lambda", "1.0", "Windows") // TODO: Allow spoofing and report correct information
iqResp.Payload = v
s.Send(iqResp)
})
router.HandleFunc("message", func(s xmpp.Sender, p stanza.Packet) {
m, ok := p.(stanza.Message)
if !ok {
return
}
if m.Body == "" {
return
}
originator := jid.MustParse(m.From).Bare().String()
glib.IdleAdd(func() {
uiQueue <- func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generateMessageWidget(p).(*gtk.Box)
if ok {
b = ba
}
_, ok = tabs[originator]
if ok {
fmt.Println("valid")
tabs[originator].msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
}
}
})
})
c, err := xmpp.NewClient(&config, router, func(err error) {
showErrorDialog(err)
panic(err)
})
if err != nil {
showErrorDialog(err)
panic(err)
}
client = c
clientroot = c
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
fmt.Println("XMPP client connected")
/*
*/
})
go func() {
err = cm.Run()
if err != nil {
showErrorDialog(err)
panic(err)
}
}()
app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
app.ConnectActivate(func() { activate(app) })
if code := app.Run(os.Args); code > 0 {
os.Exit(code)
}
}
func activate(app *gtk.Application) {
// Load the CSS and apply it globally.
gtk.StyleContextAddProviderForDisplay(
gdk.DisplayGetDefault(), loadCSS(styleCSS),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
window := gtk.NewApplicationWindow(app)
app.SetMenubar(gio.NewMenu())
window.SetTitle("Lambda")
menu := gtk.NewBox(gtk.OrientationHorizontal, 0)
/*
f_menu := gtk.NewMenuButton()
f_menu.SetLabel("File")
f_menu.SetAlwaysShowArrow(false)
e_menu := gtk.NewMenuButton()
e_menu.SetLabel("Edit")
e_menu.SetAlwaysShowArrow(false)
v_menu := gtk.NewMenuButton()
v_menu.SetLabel("View")
v_menu.SetAlwaysShowArrow(false)
b_menu := gtk.NewMenuButton()
b_menu.SetLabel("Bookmarks")
b_menu.SetAlwaysShowArrow(false)
h_menu := gtk.NewMenuButton()
h_menu.SetLabel("Help")
h_menu.SetAlwaysShowArrow(false)
menu.Append(f_menu)
menu.Append(e_menu)
menu.Append(v_menu)
menu.Append(b_menu)
menu.Append(h_menu)
*/
empty_dialog = gtk.NewLabel("You are not focused on any chats.")
empty_dialog.SetVExpand(true)
scroller = gtk.NewScrolledWindow()
box := gtk.NewBox(gtk.OrientationVertical, 0)
// scroller.SetChild(empty_dialog)
scroller.SetChild(empty_dialog)
menu_scroll := gtk.NewScrolledWindow()
menu_scroll.SetChild(menu)
box.Append(menu_scroll)
box.Append(scroller)
entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
en := gtk.NewEntry()
en.SetPlaceholderText("Say something, what else are you gonna do here?")
b := gtk.NewButtonWithLabel("Send")
sendtxt := func() {
t := en.Text()
if t == "" {
dialog := &gtk.AlertDialog{}
dialog.SetDetail("detail")
dialog.SetMessage("message")
dialog.SetButtons([]string{"yes, no"})
dialog.Choose(context.TODO(), &window.Window, nil)
}
err := sendMessage(client, current, stanza.MessageTypeGroupchat, t, "", "")
if err != nil {
panic(err) // TODO: Show error message via GTK
}
en.SetText("")
scrollToBottomAfterUpdate(scroller)
}
en.Connect("activate", sendtxt)
b.ConnectClicked(sendtxt)
en.SetHExpand(true)
entry_box.Append(en)
entry_box.Append(b)
debug_btn := gtk.NewButtonWithLabel("Join muc")
debug_btn.ConnectClicked(func() {
showErrorDialog(errors.New("test error"))
t := en.Text()
_, ok := tabs[t]
if !ok {
err := joinMuc(client, clientroot.Session.BindJid, t, loadedConfig.Nick)
if err != nil {
panic(err)
}
createTab(t, true)
b := gtk.NewButtonWithLabel(t)
b.ConnectClicked(func() {
b.AddCSSClass("accent")
switchToTab(t)
})
menu.Append(b)
}
})
entry_box.Append(debug_btn)
box.Append(entry_box)
window.SetChild(box)
window.SetVisible(true)
}
func loadCSS(content string) *gtk.CSSProvider {
prov := gtk.NewCSSProvider()
prov.LoadFromString(content)
return prov
}

8
style.css Normal file
View File

@@ -0,0 +1,8 @@
.author {
font-size: 150%;
}
.chat_img {
max-width: 50px;
max-height: 50px;
}

19
types.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"github.com/diamondburned/gotk4/pkg/gtk/v4"
)
type chatTab struct {
isMuc bool
msgs *gtk.ListBox
}
type lambdaConfig struct {
Server string
Username string
Password string
Insecure bool
Nick string
}

49
xmpp-helpers.go Normal file
View File

@@ -0,0 +1,49 @@
package main
import (
"fmt"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
// This file has small functions that can be used to do XMPP stuff without writing tons of boilerplate
// Basic message sender. Anything more complex should be written by hand
func sendMessage(c xmpp.Sender, sendTo string, msgType stanza.StanzaType, body string, subject string, thread string) error {
m := stanza.Message{
Attrs: stanza.Attrs{
To: sendTo,
Type: msgType,
},
Body: body,
Subject: subject,
Thread: thread,
}
err := c.Send(m)
if err != nil {
return err
}
return nil
}
// Joins a MUC
func joinMuc(c xmpp.Sender, jid string, muc string, nick string) error{
addr := muc + "/" + nick
fmt.Println(addr)
joinPresence := stanza.Presence{
Attrs: stanza.Attrs{
From: jid,
To: addr,
},
Extensions: []stanza.PresExtension{
&stanza.MucPresence{
},
},
}
err := client.Send(joinPresence)
if err != nil {
return err
}
return nil
}

26
xmpp-mucuser.go Normal file
View File

@@ -0,0 +1,26 @@
package main
// Implementation of XEP-0045: Multi User Chat 7.2.2
// https://xmpp.org/extensions/xep-0045.html#enter-pres
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
type MucUser struct {
stanza.PresExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"`
MucUserItem MucUserItem `xml:"item,omitempty"`
}
type MucUserItem struct {
XMLName xml.Name
Affiliation string `xml:"affiliation,attr,omitempty"` // TODO: Use enum
Role string `xml:"role,attr,omitempty"` // TODO: Use enum
JID string `xml:"jid,attr,omitempty"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc#user", Local: "x"}, MucUser{})
}

21
xmpp-occupantid.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Implementation of XEP-0421: Occupant identifiers for semi-anonymous MUCs
// https://xmpp.org/extensions/xep-0421.html
type OccupantID struct {
stanza.PresExtension
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:occupant-id:0 occupant-id"`
ID string `xml:"id,attr"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:occupant-id:0", Local: "occupant-id"}, OccupantID{})
stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{Space: "urn:xmpp:occupant-id:0", Local: "occupant-id"}, OccupantID{})
}

20
xmpp-reply.go Normal file
View File

@@ -0,0 +1,20 @@
package main
// Implementation of XEP-0461
// https://xmpp.org/extensions/xep-0461.html#business-id
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
type Reply struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:reply:0 reply"`
To string `xml:"to,attr"`
ID string `xml:"id,attr"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:reply:0", Local: "reply"}, Reply{})
}

19
xmpp-sid.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Implementation of XEP-0359: Unique and Stable Stanza IDs
type StanzaID struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:sid:0 stanza-id"`
By string `xml:"by,attr"`
ID string `xml:"id,attr"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:sid:0", Local: "stanza-id"}, StanzaID{})
}