Compare commits

...

43 Commits

Author SHA1 Message Date
359e8ed63e 'I like small icons, the big ones weird me out' 2026-03-17 19:04:43 +00:00
3b9df94cf6 add CONTRIBUTING.md 2026-03-17 19:02:52 +00:00
634f451595 Merge branch 'master' of https://forge.sunglocto.net/sunglocto/lambda 2026-03-17 18:56:02 +00:00
00d7945f70 add kick png (not initialised yet) 2026-03-17 18:53:08 +00:00
60d6a287e5 oops 2026-03-15 16:14:27 +00:00
d5981b7475 icl i forgot i had this file 2026-03-15 09:58:27 +00:00
523722bae6 Format code 2026-03-15 09:57:08 +00:00
71e6a58fbd ban, kick and change role&affil menu, status icons in muc, blocking avatars that are invalid so we dont fetch them over and over again 2026-03-15 09:55:26 +00:00
a7e90e4ae5 fix avatars in rc menu 2026-03-12 16:17:29 +00:00
654ab8b618 add some icons and only store members by resource 2026-03-12 16:09:22 +00:00
7a201808e3 format code & add additional assets 2026-03-10 16:48:10 +00:00
77e4e444d4 Attention and experimental mentions impl 2026-03-10 16:35:56 +00:00
bf1685a382 MODS 2026-02-28 16:24:37 +00:00
0ac43946b1 this commit was sponsored by RAID SHADOW LEGENDS 2026-02-22 08:16:14 +00:00
15ff7a20db prevent loading svg and webp if on netbsd (thanks gtk) 2026-02-20 09:01:40 +00:00
1dd3f09fed ping graph 2026-02-20 06:28:31 +00:00
87bdbc440a i'll do as much, for my true love, as any young girl may 2026-02-19 14:30:37 +00:00
62b5a9db72 cold blows the wind over my true love, cold blows the drops of rain 2026-02-19 14:30:04 +00:00
39156af48a format code, add confirmation to destroy muc, implement some disconnect logic 2026-02-19 11:28:18 +00:00
3f40d3da29 stat bar 2026-02-17 09:27:46 +00:00
a7a49f7441 format code 2026-02-16 09:54:37 +00:00
e026e777f6 BOOKMARKS! BOOKMARKS! WE GOT BOOKMARKS PEOPLE 2026-02-16 09:52:25 +00:00
09a809c102 add Destroy room button, show error notification on presence error and show disabled lambda icon when not focused on any chat 2026-02-15 13:08:52 +00:00
29ef37e237 Format code, remove use of Mellium JID parser, and add indicator for messages originating from WebXDC apps 2026-02-14 22:38:32 +00:00
713cb24508 Add hat icon, change some CSS and remove all reply support 2026-02-08 09:44:45 +00:00
589101c292 add info to readme 2026-02-04 19:29:56 +00:00
f807565cb2 Add nickname fallback for rooms that don't have XEP-0421 2026-02-04 17:57:24 +00:00
6626d35920 fix some crashes and debug smtn 2026-02-04 10:12:49 +00:00
5c76729a6b Attempt to fix duplicated user tabs 2026-02-03 10:38:41 +00:00
c260b8b231 add more support for other message types 2026-02-03 10:07:33 +00:00
971147dcb8 add icon 2026-02-02 18:57:11 +00:00
777df725b6 Add affiliation medals 2026-02-02 13:23:08 +00:00
6cb8771994 Sensible room joining flow 2026-02-01 19:22:43 +00:00
ac013e7969 right-click menus alongside other changes 2026-02-01 18:16:55 +00:00
067a74e157 add forgotten file 2026-02-01 14:03:31 +00:00
1dcd55d5ff remove some occurences of mellium jid lib and try to add experimental affiliation displays 2026-02-01 13:49:30 +00:00
e87369912d sensible size defaults 2026-02-01 09:17:57 +00:00
63bb323ac3 Merge branch 'master' of https://forge.sunglocto.net/sunglocto/lambda 2026-02-01 09:05:30 +00:00
e7054741a0 seperate signin logic into its own file and have error handling 2026-02-01 09:05:20 +00:00
70d51aef47 Update README.md -> manually creating configuration is no longer rquired 2026-02-01 09:03:50 +00:00
519e7bcf25 Merge branch 'master' of https://forge.sunglocto.net/sunglocto/lambda 2026-01-31 23:32:52 +00:00
4d69d95c88 custom resource 2026-01-31 23:32:26 +00:00
c0e330ea22 Merge pull request 'One character patch to allow building with Go 1.25.5' (#1) from Hydrogen/lambda:master into master
Reviewed-on: sunglocto/lambda#1
2026-01-31 23:32:04 +00:00
59 changed files with 1780 additions and 322 deletions

15
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,15 @@
# Contributing
As you might have gathered, this Gitea instance does not allow registrations. If you want to have an account on this instance in order to contribute to this project, you must contact me for an account. I am available on:
- XMPP: `x@sunglocto.net`
- Matrix: `@x:sunglocto.net`
- E-Mail: sunglocto <img width=16px height=16px src="https://sunglocto.net/icons/email.gif"/> protonmail.com
For actually submitting your patches, you can just fork this repo and submit a pull request. I currently have no contribution policy that you have to follow however if you just use common sense you should be good.
## AI contributions
Obviously vibe-coded or fully AI-generated contributions will be immediately rejected. This is in order to ensure the stability of the project and decrease the chance of significant bugs or vulnerabilities.
AI contributions that you have reviewed, edited and **ensured that they compile, run and work successfully** are acceptable. AI is a tool, and like any other tools, can be used for good if used in moderation.

View File

@@ -1,12 +1,7 @@
# lambda # lambda
In order to run this program you must have a file named `lambda.toml` that specifies your config. an XMPP client
Here is an example:
```toml icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im))
Server = "example.com:5222"
Username = "user@example.com" additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons)
Password = "123456789"
Insecure = false
Nick = "User"
```

View File

@@ -1,7 +1,6 @@
# TODO # TODO
- XEP-0393: Message Styling 0% - XEP-0393: Message Styling 0%
- XEP-0402: PEP Native Bookmarks 0% - XEP-0402: PEP Native Bookmarks 50% (often lags out client if enabled)
- XEP-0066: Out of Band Data partial - XEP-0461: Message Replies 0%
- XEP-0461: Message Replies partial
- XEP-0444: Message Reactions partial - XEP-0444: Message Reactions partial

334
assets.go Normal file
View File

@@ -0,0 +1,334 @@
package main
import (
_ "embed"
"encoding/base64"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
)
//go:embed debug.png
var defaultAvatarBytes []byte
var defaultAvatarB64 string = base64.StdEncoding.EncodeToString(defaultAvatarBytes)
//go:embed failed_load.png
var failedBytes []byte
var failedB64 string = base64.StdEncoding.EncodeToString(failedBytes)
//go:embed assets/owner.png
var ownerMedalBytes []byte
var ownerMedalB64 string = base64.StdEncoding.EncodeToString(ownerMedalBytes)
//go:embed assets/admin.png
var adminMedalBytes []byte
var adminMedalB64 string = base64.StdEncoding.EncodeToString(adminMedalBytes)
//go:embed assets/member.png
var memberMedalBytes []byte
var memberMedalB64 string = base64.StdEncoding.EncodeToString(memberMedalBytes)
//go:embed assets/noaffiliation.png
var noneMedalBytes []byte
var noneMedalB64 string = base64.StdEncoding.EncodeToString(noneMedalBytes)
//go:embed assets/outcast.png
var outcastMedalBytes []byte
var outcastMedalB64 string = base64.StdEncoding.EncodeToString(outcastMedalBytes)
//go:embed assets/cancel.png
var cancelBytes []byte
var cancelB64 string = base64.StdEncoding.EncodeToString(cancelBytes)
//go:embed assets/status_away.png
var sABytes []byte
var sAB64 string = base64.StdEncoding.EncodeToString(sABytes)
//go:embed assets/status_busy.png
var sBBytes []byte
var sBB64 string = base64.StdEncoding.EncodeToString(sBBytes)
//go:embed assets/status_chatty.png
var sCBytes []byte
var sCB64 string = base64.StdEncoding.EncodeToString(sCBytes)
//go:embed assets/status_online.png
var sOBytes []byte
var sOB64 string = base64.StdEncoding.EncodeToString(sOBytes)
//go:embed assets/status_xa.png
var xaBytes []byte
var xaB64 string = base64.StdEncoding.EncodeToString(xaBytes)
//go:embed assets/tag.png
var tagBytes []byte
var tagB64 string = base64.StdEncoding.EncodeToString(tagBytes)
//go:embed assets/lambda-disabled.png
var logoDisabledBytes []byte
var logoDisabledB64 string = base64.StdEncoding.EncodeToString(logoDisabledBytes)
//go:embed assets/group.png
var groupBytes []byte
var groupB64 string = base64.StdEncoding.EncodeToString(groupBytes)
//go:embed assets/door_in.png
var doorInBytes []byte
var doorInB64 string = base64.StdEncoding.EncodeToString(doorInBytes)
//go:embed assets/door_out.png
var doorOutBytes []byte
var doorOutB64 string = base64.StdEncoding.EncodeToString(doorOutBytes)
//go:embed assets/large_group.png
var largeGroupBytes []byte
var largeGroupB64 string = base64.StdEncoding.EncodeToString(largeGroupBytes)
//go:embed assets/world.png
var worldBytes []byte
var worldB64 string = base64.StdEncoding.EncodeToString(worldBytes)
//go:embed assets/disconnect.png
var disconnectBytes []byte
var disconnectB64 string = base64.StdEncoding.EncodeToString(disconnectBytes)
//go:embed assets/chart_bar.png
var barBytes []byte
var barB64 string = base64.StdEncoding.EncodeToString(barBytes)
//go:embed assets/ok.png
var okBytes []byte
var okB64 string = base64.StdEncoding.EncodeToString(okBytes)
//go:embed assets/hourglass.png
var hourglassBytes []byte
var hourglassB64 string = base64.StdEncoding.EncodeToString(hourglassBytes)
//go:embed assets/connect_tls.png
var connectBytes []byte
var connectB64 string = base64.StdEncoding.EncodeToString(connectBytes)
//go:embed assets/comment.png
var commentBytes []byte
var commentB64 string = base64.StdEncoding.EncodeToString(commentBytes)
//go:embed assets/information.png
var informationBytes []byte
var informationB64 string = base64.StdEncoding.EncodeToString(informationBytes)
func init() {
loader := gdkpixbuf.NewPixbufLoader()
defaultAvatarData, _ := base64.StdEncoding.DecodeString(defaultAvatarB64)
loader.Write(defaultAvatarData)
loader.Close()
clientAssets["DefaultAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
failedData, _ := base64.StdEncoding.DecodeString(failedB64)
loader.Write(failedData)
loader.Close()
clientAssets["FailedAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
ownerMedalData, _ := base64.StdEncoding.DecodeString(ownerMedalB64)
loader.Write(ownerMedalData)
loader.Close()
clientAssets["owner"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
cancelData, _ := base64.StdEncoding.DecodeString(cancelB64)
loader.Write(cancelData)
loader.Close()
clientAssets["cancel"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
tagData, _ := base64.StdEncoding.DecodeString(tagB64)
loader.Write(tagData)
loader.Close()
clientAssets["tag"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
adminMedalData, _ := base64.StdEncoding.DecodeString(adminMedalB64)
loader.Write(adminMedalData)
loader.Close()
clientAssets["admin"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
memberMedalData, _ := base64.StdEncoding.DecodeString(memberMedalB64)
loader.Write(memberMedalData)
loader.Close()
clientAssets["member"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
noneMedalData, _ := base64.StdEncoding.DecodeString(noneMedalB64)
loader.Write(noneMedalData)
loader.Close()
clientAssets["none"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
outcastMedalData, _ := base64.StdEncoding.DecodeString(outcastMedalB64)
loader.Write(outcastMedalData)
loader.Close()
clientAssets["outcast"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
disabledLogoData, _ := base64.StdEncoding.DecodeString(logoDisabledB64)
loader.Write(disabledLogoData)
loader.Close()
clientAssets["disabled_logo"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
groupData, _ := base64.StdEncoding.DecodeString(groupB64)
loader.Write(groupData)
loader.Close()
clientAssets["group"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
doorInData, _ := base64.StdEncoding.DecodeString(doorInB64)
loader.Write(doorInData)
loader.Close()
clientAssets["door_in"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
doorOutData, _ := base64.StdEncoding.DecodeString(doorOutB64)
loader.Write(doorOutData)
loader.Close()
clientAssets["door_out"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
largeGroupData, _ := base64.StdEncoding.DecodeString(largeGroupB64)
loader.Write(largeGroupData)
loader.Close()
clientAssets["large_group"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
worldData, _ := base64.StdEncoding.DecodeString(worldB64)
loader.Write(worldData)
loader.Close()
clientAssets["world"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
disconnectData, _ := base64.StdEncoding.DecodeString(disconnectB64)
loader.Write(disconnectData)
loader.Close()
clientAssets["disconnect"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
barData, _ := base64.StdEncoding.DecodeString(barB64)
loader.Write(barData)
loader.Close()
clientAssets["chart_bar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
okData, _ := base64.StdEncoding.DecodeString(okB64)
loader.Write(okData)
loader.Close()
clientAssets["ok"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
hourglassData, _ := base64.StdEncoding.DecodeString(hourglassB64)
loader.Write(hourglassData)
loader.Close()
clientAssets["hourglass"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
connectData, _ := base64.StdEncoding.DecodeString(connectB64)
loader.Write(connectData)
loader.Close()
clientAssets["connect"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
commentData, _ := base64.StdEncoding.DecodeString(commentB64)
loader.Write(commentData)
loader.Close()
clientAssets["comment"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
informationData, _ := base64.StdEncoding.DecodeString(informationB64)
loader.Write(informationData)
loader.Close()
clientAssets["information"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sAData, _ := base64.StdEncoding.DecodeString(sAB64)
loader.Write(sAData)
loader.Close()
clientAssets["status_away"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sBData, _ := base64.StdEncoding.DecodeString(sBB64)
loader.Write(sBData)
loader.Close()
clientAssets["status_dnd"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sCData, _ := base64.StdEncoding.DecodeString(sCB64)
loader.Write(sCData)
loader.Close()
clientAssets["status_chat"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
xaData, _ := base64.StdEncoding.DecodeString(xaB64)
loader.Write(xaData)
loader.Close()
clientAssets["status_xa"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sOData, _ := base64.StdEncoding.DecodeString(sOB64)
loader.Write(sOData)
loader.Close()
clientAssets["status_"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
}

BIN
assets/admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

BIN
assets/ban.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

BIN
assets/cancel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

BIN
assets/chart_bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

View File

@@ -0,0 +1 @@
All client assets are owned by their respective owners

BIN
assets/comment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

BIN
assets/connect_tls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

BIN
assets/disconnect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

BIN
assets/door_in.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

BIN
assets/door_out.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

BIN
assets/group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

BIN
assets/hourglass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/information.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

BIN
assets/jabber.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

BIN
assets/kick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/lambda-disabled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/large_group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

BIN
assets/member.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

BIN
assets/noaffiliation.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

BIN
assets/ok.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

BIN
assets/outcast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

BIN
assets/owner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

BIN
assets/status_away.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

BIN
assets/status_busy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

BIN
assets/status_chatty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/status_online.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

BIN
assets/status_xa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/tag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

BIN
assets/vcard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

BIN
assets/world.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

View File

@@ -18,6 +18,9 @@ import (
// global or app-level map/cache // global or app-level map/cache
var textureCache = make(map[string]gdk.Paintabler) var textureCache = make(map[string]gdk.Paintabler)
// Invalid images, if an image/avatar cannot be loaded on the system (e.g: incompatible format) it's put here
var invalidImages = make(map[string]bool)
func ensureCache() (string, error) { func ensureCache() (string, error) {
cachePath := configdir.LocalCache("lambda-im") cachePath := configdir.LocalCache("lambda-im")
err := configdir.MakePath(cachePath) // Ensure it exists. err := configdir.MakePath(cachePath) // Ensure it exists.

BIN
failed_load.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

24
go.mod
View File

@@ -5,22 +5,38 @@ go 1.25.5
require ( require (
github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/toml v1.6.0
github.com/diamondburned/gotk4/pkg v0.3.1 github.com/diamondburned/gotk4/pkg v0.3.1
github.com/gen2brain/beeep v0.11.2
github.com/go-analyze/charts v0.5.24
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/jacoblockett/sanitizefilename v1.0.1 github.com/jacoblockett/sanitizefilename v1.0.1
github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030 github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/kr/pretty v0.1.0 github.com/kr/pretty v0.2.0
golang.org/x/net v0.29.0
gosrc.io/xmpp v0.5.1 gosrc.io/xmpp v0.5.1
mellium.im/xmpp v0.22.0 mellium.im/xmpp v0.22.0
) )
require ( require (
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
github.com/KarpelesLab/weak v0.1.1 // indirect github.com/KarpelesLab/weak v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/esiqveland/notify v0.13.3 // indirect
github.com/go-analyze/bulk v0.1.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
github.com/kr/text v0.1.0 // indirect github.com/kr/text v0.1.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // 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/image v0.24.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/text v0.18.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
mellium.im/reader v0.1.0 // indirect mellium.im/reader v0.1.0 // indirect
mellium.im/xmlstream v0.15.4 // indirect mellium.im/xmlstream v0.15.4 // indirect

56
go.sum
View File

@@ -1,3 +1,5 @@
git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 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 h1:fNnlPo3aypS9tBzoEQluY13XyUfd/eWaSE/vMvo9s4g=
@@ -10,18 +12,35 @@ github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVz
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= 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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/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 h1:uhkXSUPUsCyz3yujdvl7DSN8jiLS2BgNTQE95hk6ygg=
github.com/diamondburned/gotk4/pkg v0.3.1/go.mod h1:DqeOW+MxSZFg9OO+esk4JgQk0TiUJJUBfMltKhG+ub4= github.com/diamondburned/gotk4/pkg v0.3.1/go.mod h1:DqeOW+MxSZFg9OO+esk4JgQk0TiUJJUBfMltKhG+ub4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=
github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWDA=
github.com/gen2brain/beeep v0.11.2/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
github.com/go-analyze/bulk v0.1.3 h1:pzRdBqzHDAT9PyROt0SlWE0YqPtdmTcEpIJY0C3vF0c=
github.com/go-analyze/bulk v0.1.3/go.mod h1:afon/KtFJYnekIyN20H/+XUvcLFjE8sKR1CfpqfClgM=
github.com/go-analyze/charts v0.5.24 h1:HamvYWEgbANbLb/P6uijygtG9qeSYev34ki1RelA1Lw=
github.com/go-analyze/charts v0.5.24/go.mod h1:s1YvQhjiSwtLx1f2dOKfiV9x2TT49nVSL6v2rlRpTbY=
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= 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/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 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/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -33,6 +52,8 @@ github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OI
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 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/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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
github.com/jacoblockett/sanitizefilename v1.0.1 h1:HhPMoPp05U5aKjhht+d7lsqhyF4trKSBme0FE6S/1C4= github.com/jacoblockett/sanitizefilename v1.0.1 h1:HhPMoPp05U5aKjhht+d7lsqhyF4trKSBme0FE6S/1C4=
github.com/jacoblockett/sanitizefilename v1.0.1/go.mod h1:/cQHSz2fHeR1iDKpHTSW/MIyONa+Uqj0eszbvPuIxNs= github.com/jacoblockett/sanitizefilename v1.0.1/go.mod h1:/cQHSz2fHeR1iDKpHTSW/MIyONa+Uqj0eszbvPuIxNs=
github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030 h1:NFCJG3BerP/5ZLXwu08x9xDs+9p7AYFMeo5IXjGANxw= github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030 h1:NFCJG3BerP/5ZLXwu08x9xDs+9p7AYFMeo5IXjGANxw=
@@ -43,8 +64,9 @@ github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDS
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= 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.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/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -58,21 +80,37 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 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/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/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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 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/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.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= 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.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 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
@@ -85,6 +123,8 @@ golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
@@ -97,8 +137,8 @@ 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-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-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.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.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-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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -108,9 +148,12 @@ golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/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-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 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/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -127,6 +170,9 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4= gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4=
gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

View File

@@ -3,10 +3,11 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/diamondburned/gotk4/pkg/pango"
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
Jid "mellium.im/xmpp/jid"
) )
func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) { func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) {
@@ -17,10 +18,12 @@ func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) {
}) })
} }
func createTab(jid string, isMuc bool) { func createTab(jid string, isMuc bool) bool {
fmt.Println("Creating tab", jid, "isMuc:", isMuc) fmt.Println("Creating tab", jid, "isMuc:", isMuc)
_, ok := tabs.Load(jid) _, ok := tabs.Load(jid)
if !ok { _, uok := userdevices.Load(jid)
_, mok := mucmembers.Load(jid)
if !ok && !uok && !mok {
newTab := new(chatTab) newTab := new(chatTab)
newTab.isMuc = isMuc newTab.isMuc = isMuc
newTab.msgs = gtk.NewListBox() newTab.msgs = gtk.NewListBox()
@@ -29,7 +32,10 @@ func createTab(jid string, isMuc bool) {
newTab.msgs.Append(gtk.NewButtonWithLabel("Get past messages...")) newTab.msgs.Append(gtk.NewButtonWithLabel("Get past messages..."))
tabs.Store(jid, newTab) tabs.Store(jid, newTab)
return true
} }
return false
} }
func switchToTab(jid string, w *gtk.Window) { func switchToTab(jid string, w *gtk.Window) {
@@ -43,12 +49,21 @@ func switchToTab(jid string, w *gtk.Window) {
scroller.SetChild(typed_tab.msgs) scroller.SetChild(typed_tab.msgs)
if typed_tab.isMuc { if typed_tab.isMuc {
m, _ := mucmembers.Load(jid)
ma := m.(mucUnit) m, ok := mucmembers.Load(jid)
if !ok {
return
}
ma, ok := m.(mucUnit)
if !ok {
return
}
mm := ma.Members mm := ma.Members
gen := gtk.NewBox(gtk.OrientationVertical, 0) gen := gtk.NewBox(gtk.OrientationVertical, 0)
i := 0
mm.Range(func(k, v any) bool { mm.Range(func(k, v any) bool {
i++
userbox := gtk.NewBox(gtk.OrientationHorizontal, 0) userbox := gtk.NewBox(gtk.OrientationHorizontal, 0)
u := v.(stanza.Presence) u := v.(stanza.Presence)
@@ -56,22 +71,203 @@ func switchToTab(jid string, w *gtk.Window) {
var ocu OccupantID var ocu OccupantID
u.Get(&mu) u.Get(&mu)
u.Get(&ocu) u.Get(&ocu)
//id := ocu.ID
//if id == "" {
id := JidMustParse(u.From).Resource
//}
nick_label := gtk.NewLabel(Jid.MustParse(u.From).Resourcepart()) nick_label := gtk.NewLabel(JidMustParse(u.From).Resource)
nick_label.SetEllipsize(pango.EllipsizeEnd)
nick_label.AddCSSClass(mu.MucUserItem.Role)
if mu.MucUserItem.Role == "visitor" {
nick_label.SetOpacity(0.5)
}
userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\nClick for more information", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation))
userbox.Append(nick_label) userbox.Append(nick_label)
var hats Hats
ok := u.Get(&hats)
if ok {
for _, hat := range hats.Hats {
tag := gtk.NewImageFromPaintable(clientAssets["tag"])
tag.SetTooltipText(hat.Title)
userbox.Prepend(tag)
}
}
status := gtk.NewImageFromPaintable(clientAssets["status_"+string(u.Show)])
status.SetTooltipText(string(u.Show))
status.SetHAlign(gtk.AlignEnd)
// medal.SetHExpand(true)
userbox.Prepend(status)
medal := gtk.NewImageFromPaintable(clientAssets[mu.MucUserItem.Affiliation])
medal.SetTooltipText(mu.MucUserItem.Affiliation)
medal.SetHAlign(gtk.AlignEnd)
medal.SetHExpand(true)
userbox.Append(medal)
gesture := gtk.NewGestureClick() gesture := gtk.NewGestureClick()
gesture.SetButton(3) // Right click gesture.SetButton(1)
mod_gesture := gtk.NewGestureClick()
mod_gesture.SetButton(3)
popover := gtk.NewPopover()
popover.SetHasArrow(false)
popover.SetParent(userbox)
rc_box := gtk.NewBox(gtk.OrientationVertical, 0)
bb := gtk.NewButtonWithLabel("Ban")
kb := gtk.NewButtonWithLabel("Kick")
ab := gtk.NewButtonWithLabel("Set affil")
rb := gtk.NewButtonWithLabel("Set role")
kb.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='kick1'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item nick='%s' role='none'>
</item>
</query>
</iq>
`, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource))
})
bb.ConnectClicked(func() {
var mu MucUser
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='ban1'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item affiliation='outcast' jid='%s'/>
</query>
</iq>
`, clientroot.Session.BindJid, jid, JidMustParse(mu.MucUserItem.JID).Bare()))
}
}
})
ab.ConnectClicked(func() {
var mu MucUser
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
win := gtk.NewWindow()
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel("Set " + JidMustParse(u.From).Resource + "'s affiliation"))
the_entry := gtk.NewEntry()
the_entry.SetText(mu.MucUserItem.Affiliation)
submit := gtk.NewButtonWithLabel("Submit")
submit.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='ba1'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item affiliation='%s' jid='%s'/>
</query>
</iq>
`, clientroot.Session.BindJid, jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare()))
win.SetVisible(false)
})
box.Append(the_entry)
box.Append(submit)
win.SetChild(box)
win.SetVisible(true)
}
}
})
rb.ConnectClicked(func() {
var mu MucUser
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
win := gtk.NewWindow()
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel("Set " + JidMustParse(u.From).Resource + "'s role"))
box.Append(gtk.NewLabel("Important: if you want this to be permanent, set their affiliation instead"))
the_entry := gtk.NewEntry()
the_entry.SetText(mu.MucUserItem.Role)
submit := gtk.NewButtonWithLabel("Submit")
submit.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='kick1'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item nick='%s' role='%s'>
</item>
</query>
</iq>
`, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource, the_entry.Text()))
})
box.Append(the_entry)
box.Append(submit)
win.SetChild(box)
win.SetVisible(true)
}
}
})
rc_box.Append(bb)
rc_box.Append(kb)
rc_box.Append(ab)
rc_box.Append(rb)
popover.SetChild(rc_box)
mod_gesture.Connect("pressed", func(n_press, x, y int) {
rect := gdk.NewRectangle(x, y, 1, 1)
popover.SetPointingTo(&rect)
popover.Popup()
})
gesture.Connect("pressed", func(n_press, x, y int) { gesture.Connect("pressed", func(n_press, x, y int) {
win := gtk.NewWindow() win := gtk.NewWindow()
win.SetDefaultSize(400, 400) win.SetDefaultSize(400, 400)
profile_box := gtk.NewBox(gtk.OrientationVertical, 0) profile_box := gtk.NewBox(gtk.OrientationVertical, 0)
nick := gtk.NewLabel(Jid.MustParse(u.From).Resourcepart()) nick := gtk.NewLabel(JidMustParse(u.From).Resource)
ver_text := gtk.NewLabel("Getting version...")
ver_text.AddCSSClass("visitor")
win.SetTitle(JidMustParse(u.From).Resource)
nick.AddCSSClass("author") nick.AddCSSClass("author")
profile_box.Append(nick) profile_box.Append(nick)
profile_box.Append(gtk.NewLabel(u.From)) profile_box.Append(ver_text)
fr := gtk.NewLabel(u.From)
fr.AddCSSClass("jid")
profile_box.Append(fr)
profile_box.Append(ver_text)
iqResp, err := stanza.NewIQ(stanza.Attrs{ iqResp, err := stanza.NewIQ(stanza.Attrs{
Type: "get", Type: "get",
@@ -92,10 +288,71 @@ func switchToTab(jid string, w *gtk.Window) {
ok := u.Get(&hats) ok := u.Get(&hats)
if ok { if ok {
for _, hat := range hats.Hats { for _, hat := range hats.Hats {
profile_box.Append(gtk.NewLabel(hat.Title)) l := gtk.NewLabel(hat.Title)
l.AddCSSClass("hat")
profile_box.Append(l)
} }
} }
var mu MucUser
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
ji := (gtk.NewLabel(mu.MucUserItem.JID))
ji.AddCSSClass("jid")
profile_box.Append(ji)
}
profile_box.Append(gtk.NewLabel("Connected with role " + mu.MucUserItem.Role))
profile_box.Append(gtk.NewLabel("Affiliated as " + mu.MucUserItem.Affiliation))
}
go func() {
fmt.Println("Attempting to get Disco info")
myIQ, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
From: clientroot.Session.BindJid,
To: u.From,
Id: "dicks",
Lang: "en",
})
if err != nil {
panic(err)
}
myIQ.Payload = &stanza.DiscoInfo{}
ctx := context.TODO()
mychan, err := client.SendIQ(ctx, myIQ)
if err == nil {
result := <-mychan
res, ok := result.Payload.(*stanza.DiscoInfo)
if ok {
idents := res.Identity
for i, ident := range idents {
profile_box.Append(gtk.NewLabel(fmt.Sprintf("%d: Name: %s, Category: %s, Type: %s", i+1, ident.Name, ident.Category, ident.Type)))
}
/*
s := fmt.Sprintf("%v", res.Features)
h := sha1.New()
h.Write([]byte(s))
sha1_hash := hex.EncodeToString(h.Sum(nil))
*/
sw := gtk.NewScrolledWindow()
s := ""
for _, feature := range res.Features {
s = s + feature.Var + "\n"
}
sw.SetChild(gtk.NewLabel(s))
profile_box.Append(sw)
}
}
}()
go func() { go func() {
ctx := context.TODO() ctx := context.TODO()
mychan, err := client.SendIQ(ctx, iqResp) mychan, err := client.SendIQ(ctx, iqResp)
@@ -108,18 +365,27 @@ func switchToTab(jid string, w *gtk.Window) {
version := ver.Version version := ver.Version
os := ver.OS os := ver.OS
profile_box.Append(gtk.NewLabel(name)) vr := fmt.Sprintf("%s %s %s", name, version, os)
profile_box.Append(gtk.NewLabel(version)) if name == "" && version == "" && os == "" {
profile_box.Append(gtk.NewLabel(os)) ver_text.SetText("Client responded with empty version")
} else {
ver_text.SetText(vr)
ver_text.RemoveCSSClass("visitor")
}
} else if result.Error != nil && result.Error.Type != "" {
ver_text.SetText("Got error trying to get version")
ver_text.SetTooltipText(result.Error.Reason + ": " + result.Error.Text)
ver_text.RemoveCSSClass("visitor")
ver_text.AddCSSClass("error")
} }
} }
}() }()
go func() { go func() {
mo, _ := mucmembers.Load(Jid.MustParse(u.From).Bare().String()) mo, _ := mucmembers.Load(JidMustParse(u.From).Bare())
mm := mo.(mucUnit) mm := mo.(mucUnit)
mmm := mm.Members mmm := mm.Members
mmmm, ok := mmm.Load(ocu.ID) mmmm, ok := mmm.Load(id)
if ok { if ok {
pres := mmmm.(stanza.Presence) pres := mmmm.(stanza.Presence)
@@ -129,18 +395,18 @@ func switchToTab(jid string, w *gtk.Window) {
im := getAvatar(u.From, vu.Photo) im := getAvatar(u.From, vu.Photo)
im.SetPixelSize(80) im.SetPixelSize(80)
im.AddCSSClass("author_img") im.AddCSSClass("author_img")
profile_box.Append(im) profile_box.Prepend(im)
} else { } else {
im := newImageFromPath("debug.png") im := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
im.SetPixelSize(80) im.SetPixelSize(80)
im.AddCSSClass("author_img") im.AddCSSClass("author_img")
profile_box.Append(im) profile_box.Prepend(im)
} }
} else { } else {
im := newImageFromPath("debug.png") im := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
im.SetPixelSize(80) im.SetPixelSize(80)
im.AddCSSClass("author_img") im.AddCSSClass("author_img")
profile_box.Append(im) profile_box.Prepend(im)
} }
}() }()
@@ -148,12 +414,32 @@ func switchToTab(jid string, w *gtk.Window) {
win.SetTransientFor(win) win.SetTransientFor(win)
win.Present() win.Present()
}) })
userbox.AddController(gesture)
userbox.AddController(gesture)
userbox.AddController(mod_gesture)
if mu.MucUserItem.Role == "moderator" {
gen.Prepend(userbox)
} else {
gen.Append(userbox) gen.Append(userbox)
}
return true return true
}) })
headerBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
if i >= 500 {
headerBox.Append(gtk.NewImageFromPaintable(clientAssets["world"]))
} else if i >= 50 {
headerBox.Append(gtk.NewImageFromPaintable(clientAssets["large_group"]))
} else {
headerBox.Append(gtk.NewImageFromPaintable(clientAssets["group"]))
}
headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d participant(s)", i)))
gen.Prepend(headerBox)
muci := getAvatar(jid, jid)
muci.SetPixelSize(80)
gen.Prepend(muci)
memberList.SetChild(gen) memberList.SetChild(gen)
} else { } else {
memberList.SetChild(gtk.NewLabel(jid)) memberList.SetChild(gtk.NewLabel(jid))

View File

@@ -6,6 +6,7 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jacoblockett/sanitizefilename" "github.com/jacoblockett/sanitizefilename"
@@ -18,6 +19,7 @@ import (
) )
func generatePresenceWidget(p stanza.Packet) gtk.Widgetter { func generatePresenceWidget(p stanza.Packet) gtk.Widgetter {
b := gtk.NewBox(gtk.OrientationHorizontal, 0)
presence, ok := p.(stanza.Presence) presence, ok := p.(stanza.Presence)
if !ok { if !ok {
return gtk.NewLabel("Unsupported message.") return gtk.NewLabel("Unsupported message.")
@@ -28,14 +30,21 @@ func generatePresenceWidget(p stanza.Packet) gtk.Widgetter {
ok := presence.Get(&mu) ok := presence.Get(&mu)
if ok { if ok {
if mu.MucUserItem.Affiliation == "outcast" { if mu.MucUserItem.Affiliation == "outcast" {
return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " has been banned!") b.Append(gtk.NewImageFromPaintable(clientAssets["outcast"]))
b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource + " has been banned by " + mu.MucUserItem.Actor.Nick + "!"))
return b
} }
} }
return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " left the room") b.Append(gtk.NewImageFromPaintable(clientAssets["door_out"]))
b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource))
} else { } else {
return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " joined the room") b.Append(gtk.NewImageFromPaintable(clientAssets["door_in"]))
b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource))
} }
b.SetTooltipText(presence.Status)
return b
} }
func generateMessageWidget(p stanza.Packet) gtk.Widgetter { func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
@@ -43,6 +52,35 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
if !ok { if !ok {
return gtk.NewLabel("Unsupported message.") return gtk.NewLabel("Unsupported message.")
} }
fmt.Println(m.Body)
readmarker := Marker{}
ok = m.Get(&readmarker)
if ok {
b := gtk.NewBox(gtk.OrientationHorizontal, 0)
b.Append(gtk.NewLabel(fmt.Sprintf("%s has read to this point", JidMustParse(m.From).Resource)))
return b
}
composing := stanza.StateComposing{}
ok = m.Get(&composing)
if ok {
b := gtk.NewBox(gtk.OrientationHorizontal, 0)
b.Append(gtk.NewLabel(fmt.Sprintf("%s is typing...", JidMustParse(m.From).Resource)))
return b
}
if m.Error.Type != "" {
error_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
cancel_img := gtk.NewImageFromPaintable(clientAssets["cancel"])
error_label := gtk.NewLabel(m.Error.Text + ": ")
error_box.Append(cancel_img)
error_box.Append(error_label)
error_box.Append(gtk.NewLabel(m.Error.Reason))
return error_box
}
sid := StanzaID{} sid := StanzaID{}
m.Get(&sid) m.Get(&sid)
@@ -50,17 +88,20 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
gesture := gtk.NewGestureClick() gesture := gtk.NewGestureClick()
gesture.SetButton(3) // Right click gesture.SetButton(3) // Right click
vis := false popover := gtk.NewPopover()
reactions := gtk.NewBox(gtk.OrientationHorizontal, 0) popover.SetParent(mainBox)
reactions.SetVisible(false) popover.SetHasArrow(false)
rc_box := gtk.NewBox(gtk.OrientationVertical, 0)
reactions := gtk.NewBox(gtk.OrientationHorizontal, 0)
reaction := []string{"👍", "👎", "♥️", "🤣", "😭"} reaction := []string{"👍", "👎", "♥️", "🤣", "😭"}
for _, v := range reaction { for _, v := range reaction {
like := gtk.NewButton() like := gtk.NewButton()
like.SetLabel(v) like.SetLabel(v)
like.SetHExpand(true) like.SetHExpand(true)
like.ConnectClicked(func() { like.ConnectClicked(func() {
fmt.Println("licked") fmt.Println("licked") // TODO: Implement proper support for reactions via extension
client.SendRaw(fmt.Sprintf(` client.SendRaw(fmt.Sprintf(`
<message from='%s' to='%s' id='%s' type='%s'> <message from='%s' to='%s' id='%s' type='%s'>
<reactions id='%s' xmlns='urn:xmpp:reactions:0'> <reactions id='%s' xmlns='urn:xmpp:reactions:0'>
@@ -72,41 +113,52 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
reactions.Append(like) reactions.Append(like)
} }
rc_box.Append(reactions)
quote := gtk.NewButtonWithLabel("Quote")
quote.ConnectClicked(func() {
message_en.SetText("> " + m.Body + "\n")
})
rc_box.Append(quote)
popover.SetChild(rc_box)
gesture.Connect("pressed", func(n_press, x, y int) { gesture.Connect("pressed", func(n_press, x, y int) {
if !vis { rect := gdk.NewRectangle(x, y, 1, 1)
vis = true popover.SetPointingTo(&rect)
reactions.SetVisible(true) popover.Popup()
} else {
vis = false
reactions.SetVisible(false)
}
}) })
mainBox.AddController(gesture) 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)
}
ocu := OccupantID{} ocu := OccupantID{}
m.Get(&ocu) m.Get(&ocu)
//id := ocu.ID
// if id == "" {
id := JidMustParse(m.From).Resource
// }
authorBox := gtk.NewBox(gtk.OrientationHorizontal, 10) authorBox := gtk.NewBox(gtk.OrientationHorizontal, 10)
contentBox := gtk.NewBox(gtk.OrientationHorizontal, 0) contentBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
// im := newImageFromPath("debug.png") // im := newImageFromPath("debug.png")
// authorBox.Append(im) // authorBox.Append(im)
n := jid.MustParse(m.From).Resourcepart()
if n == "" {
n = jid.MustParse(m.From).String()
}
al := gtk.NewLabel(n)
al.AddCSSClass("author")
al.SetSelectable(true)
if m.Type == stanza.MessageTypeGroupchat { if m.Type == stanza.MessageTypeGroupchat {
mo, _ := mucmembers.Load(jid.MustParse(m.From).Bare().String()) mo, _ := mucmembers.Load(jid.MustParse(m.From).Bare().String())
mm := mo.(mucUnit) mm := mo.(mucUnit)
mmm := mm.Members mmm := mm.Members
mmmm, ok := mmm.Load(ocu.ID) mmmm, ok := mmm.Load(id)
if ok { if ok {
pres := mmmm.(stanza.Presence) pres := mmmm.(stanza.Presence)
@@ -118,29 +170,44 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
im.AddCSSClass("author_img") im.AddCSSClass("author_img")
authorBox.Append(im) authorBox.Append(im)
} else { } else {
im := newImageFromPath("debug.png") im := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
im.SetPixelSize(40) im.SetPixelSize(40)
im.AddCSSClass("author_img") im.AddCSSClass("author_img")
authorBox.Append(im) authorBox.Append(im)
} }
} else { } else {
im := newImageFromPath("debug.png") im := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
im.SetPixelSize(40) im.SetPixelSize(40)
im.AddCSSClass("author_img") im.AddCSSClass("author_img")
authorBox.Append(im) authorBox.Append(im)
} }
} else if m.Type == stanza.MessageTypeChat {
al.SetText(al.Text() + " whispers")
} }
al := gtk.NewLabel(jid.MustParse(m.From).Resourcepart())
al.AddCSSClass("author")
authorBox.Append(al) authorBox.Append(al)
wxdc := XDCEl{}
ok = m.Get(&wxdc)
if ok {
authorBox.Append(gtk.NewLabel("🎮"))
}
mlabel := gtk.NewLabel(m.Body) mlabel := gtk.NewLabel(m.Body)
// mlabel.SetMarkup(convertXEPToPango(m.Body)) if m.Body == "" {
mlabel.SetText("No body set")
mlabel.AddCSSClass("visitor")
}
mlabel.SetWrap(true) mlabel.SetWrap(true)
mlabel.SetSelectable(true) mlabel.SetSelectable(true)
mlabel.SetHAlign(gtk.AlignFill) mlabel.SetHAlign(gtk.AlignFill)
mum := MucUser{}
ok = m.Get(&mum)
if ok {
mlabel.SetText(fmt.Sprintf("%s's affiliation has been changed to %s", mum.MucUserItem.JID, mum.MucUserItem.Affiliation))
}
contentBox.Append(mlabel) contentBox.Append(mlabel)
mainBox.Append(authorBox) mainBox.Append(authorBox)
@@ -163,6 +230,15 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
authorBox.Append(mbtn) authorBox.Append(mbtn)
} }
if m.Subject != "" {
subjectlabel := gtk.NewLabel(m.Subject)
subjectlabel.SetWrap(true)
subjectlabel.SetSelectable(true)
subjectlabel.SetHAlign(gtk.AlignFill)
subjectlabel.AddCSSClass("subject")
mainBox.Append(subjectlabel)
}
return mainBox return mainBox
} }
@@ -172,14 +248,21 @@ func getVAdjustment(scrolledWindow *gtk.ScrolledWindow) *gtk.Adjustment {
} }
func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shouldn't be here, and should probably be in xmpp-helpers or somewhere similar. func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shouldn't be here, and should probably be in xmpp-helpers or somewhere similar.
oghash := hash
p, err := ensureCache() p, err := ensureCache()
if err != nil { if err != nil {
return newImageFromPath("debug.png") return gtk.NewImageFromPaintable(clientAssets["FailedAvatar"])
} }
if hash == "" { if hash == "" {
fmt.Println("Hash is nil!") fmt.Println("Hash is nil!")
return newImageFromPath("debug.png") return gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
}
_, ok := invalidImages[hash]
if ok {
fmt.Println("Image is invalid")
return gtk.NewImageFromPaintable(clientAssets["FailedAvatar"])
} }
hash = filepath.Join(p, sanitizefilename.Sanitize(hash)) hash = filepath.Join(p, sanitizefilename.Sanitize(hash))
@@ -211,12 +294,14 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou
result := <-mychan result := <-mychan
card, ok := result.Payload.(*VCard) card, ok := result.Payload.(*VCard)
if !ok { if !ok {
return newImageFromPath("debug.png") return gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
} }
base64_data := card.Photo.Binval base64_data := card.Photo.Binval
if card.Photo.Binval == "" || (card.Photo.Type == "image/svg+xml" && runtime.GOOS == "windows") { if card.Photo.Binval == "" || ((card.Photo.Type == "image/svg+xml" || card.Photo.Type == "image/webp") && (runtime.GOOS == "windows" || runtime.GOOS == "netbsd")) {
return newImageFromPath("debug.png") fmt.Println("Blocking image")
invalidImages[oghash] = true
return gtk.NewImageFromPaintable(clientAssets["FailedAvatar"])
} }
data, err := base64.StdEncoding.DecodeString(base64_data) data, err := base64.StdEncoding.DecodeString(base64_data)

123
gtk-signin.go Normal file
View File

@@ -0,0 +1,123 @@
package main
import (
"bytes"
"os"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"path/filepath"
"github.com/BurntSushi/toml"
"math/rand/v2"
_ "embed"
)
func randomClientResource() string {
chars := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZλ"
str := ""
for range 4 {
str = str + string(chars[rand.IntN(len(chars))])
}
return "lambda." + str
}
func dropToSignInPage(err error) {
app := gtk.NewApplication("net.sunglocto.lambda.login", gio.ApplicationFlagsNone)
app.ConnectActivate(func() {
form_box := gtk.NewBox(gtk.OrientationVertical, 0)
server_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
username_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
password_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
nickname_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
insecure_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
server_label := gtk.NewLabel("Server: ")
username_label := gtk.NewLabel("JID: ")
password_label := gtk.NewLabel("Password: ")
nickname_label := gtk.NewLabel("Nickname: ")
insecure_label := gtk.NewLabel("Insecure: (?)")
insecure_label.SetTooltipText("Tick this if you need to connect without TLS, usually for connecting to Tor XMPP servers")
server_entry := gtk.NewEntry()
server_entry.SetHAlign(gtk.AlignEnd)
server_entry.SetHExpand(true)
username_entry := gtk.NewEntry()
username_entry.SetHAlign(gtk.AlignEnd)
username_entry.SetHExpand(true)
password_entry := gtk.NewPasswordEntry()
password_entry.SetHAlign(gtk.AlignEnd)
password_entry.SetHExpand(true)
nickname_entry := gtk.NewEntry()
nickname_entry.SetHAlign(gtk.AlignEnd)
nickname_entry.SetHExpand(true)
insecure_check := gtk.NewCheckButton()
insecure_check.SetHAlign(gtk.AlignEnd)
insecure_check.SetHExpand(true)
server_box.Append(server_label)
server_box.Append(server_entry)
username_box.Append(username_label)
username_box.Append(username_entry)
password_box.Append(password_label)
password_box.Append(password_entry)
nickname_box.Append(nickname_label)
nickname_box.Append(nickname_entry)
insecure_box.Append(insecure_label)
insecure_box.Append(insecure_check)
form_box.Append(server_box)
form_box.Append(username_box)
form_box.Append(password_box)
form_box.Append(nickname_box)
form_box.Append(insecure_box)
sumbit_btn := gtk.NewButtonWithLabel("Submit")
sumbit_btn.ConnectClicked(func() {
conf := new(lambdaConfig)
conf.Server = server_entry.Text()
conf.Username = username_entry.Text()
conf.Password = password_entry.Text()
conf.Nick = nickname_entry.Text()
conf.Insecure = insecure_check.Active()
conf.JoinBookmarks = true
conf.Resource = randomClientResource()
var b bytes.Buffer
e := toml.NewEncoder(&b)
e.Encode(conf)
p, err := ensureConfig()
if err != nil {
panic(err)
}
os.WriteFile(filepath.Join(p, "lambda.toml"), b.Bytes(), 0644)
window.SetVisible(false)
main()
os.Exit(0)
})
form_box.Append(sumbit_btn)
window = gtk.NewApplicationWindow(app)
window.SetChild(form_box)
window.SetResizable(false)
window.SetVisible(true)
})
if code := app.Run(os.Args); code == 0 {
os.Exit(code)
}
}

748
main.go
View File

@@ -2,16 +2,19 @@ package main
import ( import (
"os" "os"
"strings"
"sync" "sync"
"bytes"
"context" "context"
"fmt" "fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4" "github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
"github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/kr/pretty" "github.com/gen2brain/beeep"
"github.com/go-analyze/charts"
"golang.org/x/net/html/charset"
"path/filepath" "path/filepath"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
@@ -22,12 +25,22 @@ import (
_ "embed" _ "embed"
"encoding/xml" "encoding/xml"
"github.com/kr/pretty"
"io"
"runtime" "runtime"
) )
var loadedConfig lambdaConfig var loadedConfig lambdaConfig
var empty_dialog *gtk.Label var empty_dialog *gtk.Image
var connectionStatus *gtk.Label
var connectionIcon *gtk.Image
var mStatus *gtk.Label
var mIcon *gtk.Image
var pingStatus *gtk.Label
// var msgs *gtk.ListBox // var msgs *gtk.ListBox
var content *gtk.Widgetter var content *gtk.Widgetter
@@ -39,6 +52,7 @@ var current string
var scroller *gtk.ScrolledWindow var scroller *gtk.ScrolledWindow
var memberList *gtk.ScrolledWindow var memberList *gtk.ScrolledWindow
var menu *gtk.Box var menu *gtk.Box
var message_en *gtk.Entry
//go:embed style.css //go:embed style.css
var styleCSS string var styleCSS string
@@ -55,7 +69,13 @@ var mucmembers sync.Map
// stores devices of users // stores devices of users
var userdevices sync.Map var userdevices sync.Map
var pingTimes = [][]float64{}
var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler)
func init() { func init() {
beeep.AppName = "Lambda"
go func() { go func() {
for fn := range uiQueue { for fn := range uiQueue {
glib.IdleAdd(func() bool { glib.IdleAdd(func() bool {
@@ -65,97 +85,16 @@ func init() {
time.Sleep(10 * time.Millisecond) // Small delay between updates time.Sleep(10 * time.Millisecond) // Small delay between updates
} }
}() }()
}
func dropToSignInPage(err error) {
app := gtk.NewApplication("net.sunglocto.lambda.login", gio.ApplicationFlagsNone)
app.ConnectActivate(func() {
form_box := gtk.NewBox(gtk.OrientationVertical, 0)
server_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
username_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
password_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
nickname_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
insecure_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
server_label := gtk.NewLabel("Server: ")
username_label := gtk.NewLabel("Username: ")
password_label := gtk.NewLabel("Password: ")
nickname_label := gtk.NewLabel("Nickname: ")
insecure_label := gtk.NewLabel("Insecure: ")
server_entry := gtk.NewEntry()
server_entry.SetHAlign(gtk.AlignEnd)
username_entry := gtk.NewEntry()
username_entry.SetHAlign(gtk.AlignEnd)
password_entry := gtk.NewPasswordEntry()
password_entry.SetHAlign(gtk.AlignEnd)
nickname_entry := gtk.NewEntry()
nickname_entry.SetHAlign(gtk.AlignEnd)
insecure_check := gtk.NewCheckButton()
insecure_check.SetHAlign(gtk.AlignEnd)
server_box.Append(server_label)
server_box.Append(server_entry)
username_box.Append(username_label)
username_box.Append(username_entry)
password_box.Append(password_label)
password_box.Append(password_entry)
nickname_box.Append(nickname_label)
nickname_box.Append(nickname_entry)
insecure_box.Append(insecure_label)
insecure_box.Append(insecure_check)
form_box.Append(server_box)
form_box.Append(username_box)
form_box.Append(password_box)
form_box.Append(nickname_box)
form_box.Append(insecure_box)
sumbit_btn := gtk.NewButtonWithLabel("Submit")
sumbit_btn.ConnectClicked(func() {
conf := new(lambdaConfig)
conf.Server = server_entry.Text()
conf.Username = username_entry.Text()
conf.Password = password_entry.Text()
conf.Nick = nickname_entry.Text()
conf.Insecure = insecure_check.Active()
var b bytes.Buffer
e := toml.NewEncoder(&b)
e.Encode(conf)
p, _ := ensureConfig()
os.WriteFile(filepath.Join(p, "lambda.toml"), b.Bytes(), 0644)
window.SetVisible(false)
main()
os.Exit(0)
})
form_box.Append(sumbit_btn)
window = gtk.NewApplicationWindow(app)
window.SetChild(form_box)
window.SetResizable(false)
window.SetVisible(true)
})
if code := app.Run(os.Args); code == 0 {
os.Exit(code)
}
} }
func main() { func main() {
p, _ := ensureConfig() pingTimes = append(pingTimes, []float64{})
p, err := ensureConfig()
if err != nil {
panic(err)
}
b, err := os.ReadFile(filepath.Join(p, "lambda.toml")) b, err := os.ReadFile(filepath.Join(p, "lambda.toml"))
if err != nil { if err != nil {
dropToSignInPage(err) dropToSignInPage(err)
@@ -168,14 +107,23 @@ func main() {
panic(err) panic(err)
} }
if loadedConfig.Resource == "" {
fmt.Println("Config resource is empty! Generating a random one")
loadedConfig.Resource = randomClientResource()
}
config := xmpp.Config{ config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{ TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server, Address: loadedConfig.Server,
CharsetReader: func(c string, input io.Reader) (io.Reader, error) {
return charset.NewReaderLabel(c, input)
}, },
Jid: loadedConfig.Username, },
Jid: loadedConfig.Username + "/" + loadedConfig.Resource,
Credential: xmpp.Password(loadedConfig.Password), Credential: xmpp.Password(loadedConfig.Password),
Insecure: loadedConfig.Insecure, Insecure: loadedConfig.Insecure,
// StreamLogger: os.Stdout, // StreamLogger: os.Stdout,
StreamManagementEnable: true,
} }
router := xmpp.NewRouter() router := xmpp.NewRouter()
@@ -207,8 +155,8 @@ func main() {
{Var: "jabber:iq:version"}, {Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"}, {Var: "urn:xmpp:delegation:1"},
{Var: "http://jabber.org/protocol/muc"}, {Var: "http://jabber.org/protocol/muc"},
{Var: "urn:xmpp:reply:0"},
{Var: "λ"}, {Var: "λ"},
{Var: "urn:xmpp:attention:0"},
}, },
} }
iqResp.Payload = &payload iqResp.Payload = &payload
@@ -240,21 +188,73 @@ func main() {
return return
} }
e := stanza.PubSubEvent{}
ok = m.Get(&e)
if ok {
fmt.Println(e)
}
/*
if m.Body == "" { if m.Body == "" {
return return
} }
*/
originator := jid.MustParse(m.From).Bare().String() originator := JidMustParse(m.From).Bare()
mStatus.SetText(originator)
glib.IdleAdd(func() { at := new(Attention)
uiQueue <- func() { ok = m.Get(at)
b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generateMessageWidget(p).(*gtk.Box)
if ok { if ok {
b = ba beeep.Notify("Attention", fmt.Sprintf("%s: %s", JidMustParse(m.From).Resource, m.Body), commentBytes) // TODO: Use localpart if DM
} }
// Handle mentions
for _, ext := range m.Extensions {
mention, ok := ext.(*Mention)
if ok {
pretty.Println(mention)
}
}
sc := new(SentCarbon)
ok = m.Get(sc)
if ok {
fm, ok := sc.Forwarded.Stanza.(stanza.Message)
if ok {
if JidMustParse(fm.From).Bare() == JidMustParse(m.From).Bare() {
p = sc.Forwarded.Stanza
m = sc.Forwarded.Stanza.(stanza.Message)
} else {
panic(fmt.Sprintln("Impersonation: ", fm.From, m.From))
}
}
}
rc := new(SentCarbon)
ok = m.Get(rc)
if ok {
fm, ok := rc.Forwarded.Stanza.(stanza.Message)
if ok {
if JidMustParse(fm.From).Bare() == JidMustParse(m.From).Bare() {
p = rc.Forwarded.Stanza
m = rc.Forwarded.Stanza.(stanza.Message)
} else {
panic(fmt.Sprintln("Impersonation: ", fm.From, m.From))
}
}
}
glib.IdleAdd(func() {
//uiQueue <- func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
tab, ok := tabs.Load(originator) tab, ok := tabs.Load(originator)
if !ok {
return
}
typed_tab := tab.(*chatTab) typed_tab := tab.(*chatTab)
if ok { if ok {
@@ -263,7 +263,12 @@ func main() {
} else { } else {
fmt.Println("Got message when the tab does not exist!") fmt.Println("Got message when the tab does not exist!")
} }
ba, ok := generateMessageWidget(p).(*gtk.Box)
if ok {
b.Append(ba)
} }
//}
}) })
}) })
@@ -273,9 +278,8 @@ func main() {
return return
} }
pretty.Println(presence) if presence.Error.Reason != "" {
beeep.Notify(fmt.Sprintf("%s : %s", presence.From, presence.Error.Reason), presence.Error.Text, cancelBytes)
if presence.Error != *new(stanza.Err) {
return return
} }
@@ -286,7 +290,12 @@ func main() {
if ok { // This is a presence stanza from a user in a MUC if ok { // This is a presence stanza from a user in a MUC
presence.Get(&ocu) presence.Get(&ocu)
muc := jid.MustParse(presence.From).Bare().String() // id := ocu.ID
// if id == "" {
id := JidMustParse(presence.From).Resource
// }
from, _ := stanza.NewJid(presence.From)
muc := from.Bare()
_, ok = mucmembers.Load(muc) _, ok = mucmembers.Load(muc)
if !ok { if !ok {
mucmembers.Store(muc, mucUnit{}) mucmembers.Store(muc, mucUnit{})
@@ -300,13 +309,11 @@ func main() {
typed_unit := unit.(mucUnit) typed_unit := unit.(mucUnit)
if presence.Type != "unavailable" { if presence.Type != "unavailable" {
typed_unit.Members.Store(ocu.ID, presence) _, ok := typed_unit.Members.Load(id)
} else { if !ok {
typed_unit.Members.Delete(ocu.ID)
glib.IdleAdd(func() { glib.IdleAdd(func() {
uiQueue <- func() { b := gtk.NewBox(gtk.OrientationVertical, 0)
b := gtk.NewLabel("") ba, ok := generatePresenceWidget(p).(*gtk.Box)
ba, ok := generatePresenceWidget(p).(*gtk.Label)
if ok { if ok {
b = ba b = ba
} }
@@ -320,6 +327,26 @@ func main() {
} else { } else {
fmt.Println("Got message when the tab does not exist!") fmt.Println("Got message when the tab does not exist!")
} }
})
}
typed_unit.Members.Store(id, presence)
} else {
typed_unit.Members.Delete(id)
glib.IdleAdd(func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generatePresenceWidget(p).(*gtk.Box)
if ok {
b = ba
}
tab, ok := tabs.Load(muc)
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
} }
}) })
} }
@@ -332,20 +359,24 @@ func main() {
_, ok := userdevices.Load(user) _, ok := userdevices.Load(user)
_, mok := mucmembers.Load(user) _, mok := mucmembers.Load(user)
if !ok && !mok { // FIXME: The initial muc presence gets picked up from this check if !ok && !mok { // FIXME: The initial muc presence gets picked up from this check
ok := createTab(user, false)
if ok {
userdevices.Store(user, userUnit{}) userdevices.Store(user, userUnit{})
createTab(user, false)
b := gtk.NewButtonWithLabel(user) b := gtk.NewLabel(user)
b.ConnectClicked(func() { gesture1 := gtk.NewGestureClick()
b.AddCSSClass("accent") gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(user, &window.Window) switchToTab(user, &window.Window)
}) })
b.AddController(gesture1)
menu.Append(b) menu.Append(b)
} }
}
unit, ok := userdevices.Load(user) unit, ok := userdevices.Load(user)
if !ok { if !ok {
fmt.Println("Could not load user presence even after recreating it! Something weird is going on!")
return return
} }
@@ -361,12 +392,14 @@ func main() {
userdevices.Store(user, typed_unit) userdevices.Store(user, typed_unit)
} }
time.Sleep(1 * time.Second)
}) })
c, err := xmpp.NewClient(&config, router, func(err error) { c, err := xmpp.NewClient(&config, router, func(err error) {
showErrorDialog(err) connectionStatus.SetText(fmt.Sprintf("Disconnected: %s", err.Error()))
panic(err) connectionIcon.SetFromPaintable(clientAssets["disconnect"])
}) })
if err != nil { if err != nil {
showErrorDialog(err) showErrorDialog(err)
panic(err) panic(err)
@@ -376,21 +409,114 @@ func main() {
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) { cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
fmt.Println("XMPP client connected") fmt.Println("XMPP client connected")
/*
*/
})
go func() { go func() {
time.Sleep(3 * time.Second) for {
err = cm.Run() time.Sleep(5 * time.Second)
pingStatus.AddCSSClass("pending")
before := time.Now()
iq := new(stanza.IQ)
iq.From = clientroot.Session.BindJid
iq.To = iq.From
iq.Type = "get"
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
mychan, err := client.SendIQ(ctx, iq)
if err != nil { if err != nil {
showErrorDialog(err) continue
panic(err) }
_ = <-mychan
pingStatus.RemoveCSSClass("pending")
delay := time.Since(before) / time.Millisecond
pingStatus.SetText(fmt.Sprintf("%d ms", delay))
pingTimes[0] = append(pingTimes[0], float64(delay))
} }
}() }()
connectionStatus.SetText(fmt.Sprintf("Connected as %s", JidMustParse(clientroot.Session.BindJid).Bare()))
connectionStatus.SetTooltipText(fmt.Sprintf("Binded JID: %s\nUsing TLS: %t", clientroot.Session.BindJid, clientroot.Session.TlsEnabled))
connectionIcon.SetFromPaintable(clientAssets["connect"])
// Enable carbons
client.SendRaw(fmt.Sprintf(
`<iq xmlns='jabber:client'
from='%s'
id='enable1'
type='set'>
<enable xmlns='urn:xmpp:carbons:2'/>
</iq>
`, clientroot.Session.BindJid))
// Join rooms in bookmarks
if loadedConfig.JoinBookmarks {
books, err := stanza.NewItemsRequest("", "urn:xmpp:bookmarks:1", 0)
if err == nil {
mychan, err := c.SendIQ(context.TODO(), books)
result := <-mychan
if err == nil {
res, ok := result.Payload.(*stanza.PubSubGeneric)
if ok {
for _, item := range res.Items.List {
go func() {
jid := item.Id
node := item.Any
autojoin := false
nick := loadedConfig.Nick
for _, attr := range node.Attrs {
if attr.Name.Local == "autojoin" {
autojoin = attr.Value == "true"
}
}
for _, node := range node.Nodes {
if node.XMLName.Local == "nick" {
nick = node.Content
}
}
_, ok := tabs.Load(jid)
if !ok && autojoin {
err := joinMuc(client, clientroot.Session.BindJid, jid, nick)
if err != nil {
panic(err)
}
createTab(jid, true)
b := gtk.NewLabel(jid)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
b.AddController(gesture1)
menu.Append(b)
}
}()
}
}
}
}
}
})
conc := func() {
time.Sleep(3 * time.Second)
connectionStatus.SetText("Connecting...")
connectionIcon.SetFromPaintable(clientAssets["hourglass"])
err = cm.Run()
if err != nil {
fmt.Println(err.Error())
connectionStatus.SetText(fmt.Sprintf("Disconnected: %s", err.Error()))
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
}
}
app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone) app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
app.ConnectActivate(func() { activate(app) }) app.ConnectActivate(func() {
go conc()
activate(app)
})
if code := app.Run(os.Args); code > 0 { if code := app.Run(os.Args); code > 0 {
os.Exit(code) os.Exit(code)
@@ -405,40 +531,190 @@ func activate(app *gtk.Application) {
) )
window = gtk.NewApplicationWindow(app) window = gtk.NewApplicationWindow(app)
the_menu := gio.NewMenu()
fileMenu := gio.NewMenu()
fileMenu.Append("Join MUC", "app.join")
fileMenu.Append("Start DM", "app.dm")
fileMenu.Append("Destroy MUC", "app.destroymuc")
helpMenu := gio.NewMenu()
helpMenu.Append("About", "app.about")
aboutAction := gio.NewSimpleAction("about", nil)
aboutAction.ConnectActivate(func(p *glib.Variant) {
a := gtk.AboutDialog{}
a.SetVisible(true)
})
destroymucAction := gio.NewSimpleAction("destroymuc", nil)
destroymucAction.ConnectActivate(func(p *glib.Variant) {
cur, ok := tabs.Load(current)
if ok {
cur := cur.(*chatTab)
if cur.isMuc {
win := gtk.NewWindow()
win.SetTitle("Destroy MUC")
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel("Are you sure? This MUC will be gone forever! (a very long time)"))
box.Append(gtk.NewLabel("If you wish to continue, type 'I understand'"))
cancel := gtk.NewButtonWithLabel("Cancel")
cancel.ConnectClicked(func() {
win.SetVisible(false)
})
en := gtk.NewEntry()
en.SetPlaceholderText("...")
submit := gtk.NewButtonWithLabel("Destroy")
submit.ConnectClicked(func() {
fmt.Println(en.Text())
if en.Text() == "I understand" {
cur, ok := tabs.Load(current)
if ok {
cur := cur.(*chatTab)
if cur.isMuc {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='begone'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#owner'>
<destroy jid='%s'>
<reason>User requested</reason>
</destroy>
</query>
</iq>
`, clientroot.Session.BindJid, current, JidMustParse(clientroot.Session.BindJid).Bare()))
}
}
win.SetVisible(false)
}
})
box.Append(en)
box.Append(submit)
box.Append(cancel)
mu, ok := mucmembers.Load(current)
if ok {
typed_mu := mu.(mucUnit)
typed_mu.Members.Range(func(k, v any) bool {
user, ok := v.(stanza.Presence)
if ok {
mu := MucUser{}
ok := user.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
if JidMustParse(mu.MucUserItem.JID).Bare() == JidMustParse(clientroot.Session.BindJid).Bare() {
if mu.MucUserItem.Affiliation != "owner" {
box.Append(gtk.NewLabel("You are not an owner of this MUC and thus will most likely not be able to delete it"))
}
// return false
}
}
} else {
panic("not ok")
}
} else {
panic("not ok")
}
return true
})
} else {
panic("not ok")
}
win.SetChild(box)
win.SetVisible(true)
}
}
})
joinAction := gio.NewSimpleAction("join", nil)
joinAction.ConnectActivate(func(p *glib.Variant) {
box := gtk.NewBox(gtk.OrientationVertical, 0)
jid_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
nick_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
jid_entry := gtk.NewEntry()
nick_entry := gtk.NewEntry()
jid_entry.SetHAlign(gtk.AlignEnd)
jid_entry.SetHExpand(true)
nick_entry.SetHAlign(gtk.AlignEnd)
nick_entry.SetHExpand(true)
nick_entry.SetText(loadedConfig.Nick)
jid_box.Append(gtk.NewLabel("MUC JID:"))
jid_box.Append(jid_entry)
nick_box.Append(gtk.NewLabel("Nick:"))
nick_box.Append(nick_entry)
box.Append(jid_box)
box.Append(nick_box)
btn := gtk.NewButtonWithLabel("Submit")
btn.SetVAlign(gtk.AlignBaseline)
box.Append(btn)
win := gtk.NewWindow()
win.SetTitle("Join MUC")
win.SetDefaultSize(400, 1)
win.SetResizable(false)
win.SetChild(box)
btn.ConnectClicked(func() {
t := jid_entry.Text()
_, ok := tabs.Load(t)
if !ok {
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text())
if err != nil {
panic(err)
}
createTab(t, true)
b := gtk.NewLabel(t)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(t, &window.Window)
})
b.AddController(gesture1)
menu.Append(b)
}
win.SetVisible(false)
})
win.SetTransientFor(win)
win.Present()
})
app.AddAction(joinAction)
app.AddAction(aboutAction)
app.AddAction(destroymucAction)
the_menu.AppendSubmenu("File", fileMenu)
the_menu.AppendSubmenu("Help", helpMenu)
the_menuBar := gtk.NewPopoverMenuBarFromModel(the_menu)
app.SetMenubar(gio.NewMenu()) app.SetMenubar(gio.NewMenu())
window.SetTitle("Lambda") window.SetTitle("Lambda")
window.Window.AddCSSClass("ssd") window.Window.AddCSSClass("ssd")
window.Window.SetDefaultSize(500, 500)
menu = gtk.NewBox(gtk.OrientationVertical, 0) menu = gtk.NewBox(gtk.OrientationVertical, 0)
/*
f_menu := gtk.NewMenuButton()
f_menu.SetLabel("File")
f_menu.SetAlwaysShowArrow(false)
e_menu := gtk.NewMenuButton() empty_dialog = gtk.NewImageFromPaintable(clientAssets["disabled_logo"])
e_menu.SetLabel("Edit") empty_dialog.SetPixelSize(100)
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) empty_dialog.SetVExpand(true)
scroller = gtk.NewScrolledWindow() scroller = gtk.NewScrolledWindow()
@@ -448,6 +724,101 @@ func activate(app *gtk.Application) {
memberList.SetHExpand(true) memberList.SetHExpand(true)
box := gtk.NewBox(gtk.OrientationVertical, 0) box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(the_menuBar)
statBar := gtk.NewBox(gtk.OrientationHorizontal, 0)
cBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
connectionIcon = gtk.NewImageFromPaintable((clientAssets["disconnect"]))
connectionIcon.AddCSSClass("icon")
connectionStatus = gtk.NewLabel("Disconnected")
cBox.Append(connectionIcon)
cBox.Append(connectionStatus)
statBar.Append(cBox)
mBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
current = mStatus.Text()
switchToTab(current, &window.Window)
})
mIcon = gtk.NewImageFromPaintable((clientAssets["comment"]))
mIcon.AddCSSClass("icon")
mStatus = gtk.NewLabel("-")
mStatus.AddController(gesture1)
cBox.Append(mIcon)
cBox.Append(mStatus)
statBar.Append(mBox)
pBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
pBox.SetTooltipText("Ping between you and your XMPP server\nRight-click to see graph")
gesture := gtk.NewGestureClick()
gesture.SetButton(3)
gesture.Connect("pressed", func() {
opt := charts.NewLineChartOptionWithData(pingTimes)
opt.Title = charts.TitleOption{
Text: "Server latency",
}
/*
opt.XAxis.Labels = []string{
// The 7 labels here match to the 7 values above
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
}*/
opt.Legend = charts.LegendOption{
SeriesNames: []string{
"Ping (ms)",
},
}
opt.StrokeSmoothingTension = 0.9
p := charts.NewPainter(charts.PainterOptions{
Width: 600,
Height: 400,
})
err := p.LineChart(opt)
if err != nil {
panic(err)
}
buf, err := p.Bytes()
if err != nil {
panic(err)
}
loader := gdkpixbuf.NewPixbufLoader()
loader.Write(buf)
loader.Close()
i := gtk.NewPictureForPaintable(gdk.NewTextureForPixbuf(loader.Pixbuf()))
win := gtk.NewWindow()
win.SetDefaultSize(600, 400)
win.SetTitle("Server latency")
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(i)
win.SetChild(box)
win.SetVisible(true)
})
pBox.AddController(gesture)
i := (gtk.NewImageFromPaintable(clientAssets["chart_bar"]))
i.AddCSSClass("icon")
pBox.Append(i)
pingStatus = gtk.NewLabel("...")
pBox.Append(pingStatus)
statBar.Append(pBox)
scrollerStatBar := gtk.NewScrolledWindow()
scrollerStatBar.SetChild(statBar)
box.Append(scrollerStatBar)
// scroller.SetChild(empty_dialog) // scroller.SetChild(empty_dialog)
scroller.SetChild(empty_dialog) scroller.SetChild(empty_dialog)
menu_scroll := gtk.NewScrolledWindow() menu_scroll := gtk.NewScrolledWindow()
@@ -461,22 +832,27 @@ func activate(app *gtk.Application) {
chat_pane := gtk.NewPaned(gtk.OrientationHorizontal) chat_pane := gtk.NewPaned(gtk.OrientationHorizontal)
chat_pane.SetStartChild(scroller) chat_pane.SetStartChild(scroller)
chat_pane.SetEndChild(memberList) chat_pane.SetEndChild(memberList)
chat_pane.SetPosition(225)
main_pane := gtk.NewPaned(gtk.OrientationHorizontal) main_pane := gtk.NewPaned(gtk.OrientationHorizontal)
main_pane.SetStartChild(menu_scroll) main_pane.SetStartChild(menu_scroll)
main_pane.SetEndChild(chat_pane) main_pane.SetEndChild(chat_pane)
main_pane.SetPosition(135)
chatbox.Append(main_pane) chatbox.Append(main_pane)
box.Append(chatbox) box.Append(chatbox)
entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0) entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
en := gtk.NewEntry() oob_en := gtk.NewEntry()
en.SetPlaceholderText("Say something, what else are you gonna do here?") oob_en.SetPlaceholderText("Embed URL")
message_en = gtk.NewEntry()
message_en.SetPlaceholderText("Say something, what else are you gonna do here?")
b := gtk.NewButtonWithLabel("Send") b := gtk.NewButtonWithLabel("Send")
sendtxt := func() { sendtxt := func() {
t := en.Text() t := message_en.Text()
if t == "" { if t == "" {
dialog := &gtk.AlertDialog{} dialog := &gtk.AlertDialog{}
dialog.SetDetail("detail") dialog.SetDetail("detail")
@@ -496,51 +872,47 @@ func activate(app *gtk.Application) {
message_type = stanza.MessageTypeGroupchat message_type = stanza.MessageTypeGroupchat
} }
err := sendMessage(client, current, message_type, t, "", "") exts := []stanza.MsgExtension{}
if oob_en.Text() != "" {
new_oob := new(stanza.OOB)
new_oob.URL = oob_en.Text()
exts = append(exts, new_oob)
}
if strings.Contains(t, "@everyone") {
start := strings.Index(t, "@everyone")
end := start + len("@everyone")
new_mention := new(Mention)
new_mention.Type = "urn:xmpp:mentions:0#channel"
new_mention.Begin = start
new_mention.End = end
exts = append(exts, new_mention)
} else if strings.Contains(t, "@here") {
new_attention := new(Attention)
exts = append(exts, new_attention)
}
err := sendMessage(client, current, message_type, t, "", "", exts)
if err != nil { if err != nil {
panic(err) // TODO: Show error message via GTK panic(err) // TODO: Show error message via GTK
} }
en.SetText("") message_en.SetText("")
scrollToBottomAfterUpdate(scroller) scrollToBottomAfterUpdate(scroller)
} }
en.Connect("activate", sendtxt) message_en.Connect("activate", sendtxt)
b.ConnectClicked(sendtxt) b.ConnectClicked(sendtxt)
en.SetHExpand(true) message_en.SetHExpand(true)
m_entry := gtk.NewEntry() entry_box.Append(oob_en)
entry_box.Append(message_en)
entry_box.Append(en)
entry_box.Append(b) entry_box.Append(b)
entry_box.Append(m_entry)
debug_btn := gtk.NewButtonWithLabel("Join muc")
debug_btn.ConnectClicked(func() {
t := en.Text()
_, ok := tabs.Load(t)
if !ok {
err := joinMuc(client, clientroot.Session.BindJid, t, m_entry.Text())
if err != nil {
panic(err)
}
createTab(t, true)
b := gtk.NewButtonWithLabel(t)
b.ConnectClicked(func() {
b.AddCSSClass("accent")
switchToTab(t, &window.Window)
})
menu.Append(b)
}
})
entry_box.Append(debug_btn)
box.Append(entry_box) box.Append(entry_box)
window.SetChild(box) window.SetChild(box)

BIN
please_wait.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
rsrc_windows_amd64.syso Normal file

Binary file not shown.

View File

@@ -10,3 +10,54 @@
.author_img { .author_img {
border-radius 100%; border-radius 100%;
} }
.owner {
background-color: red;
color: white;
}
.admin {
background-color: orange;
color: white;
}
.member {
background-color: lime;
color: white;
}
.moderator {
color: magenta;
}
.visitor {
color: grey;
}
.pending {
color: grey;
transition-duration: 0.5s;
}
.hat {
background-color: orange;
color: black;
font-family: monospace;
}
.subject {
color: lime;
}
.icon {
padding: 2px;
}
.jid {
font-family: monospace;
}
.error {
color: white;
background-color: red;
}

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/diamondburned/gotk4/pkg/gtk/v4"
"mellium.im/xmpp/color"
"sync" "sync"
) )
@@ -13,13 +14,16 @@ type chatTab struct {
type lambdaConfig struct { type lambdaConfig struct {
Server string Server string
Username string Username string
Resource string
Password string Password string
Insecure bool Insecure bool
Nick string Nick string
JoinBookmarks bool
CVD color.CVD
} }
type mucUnit struct { type mucUnit struct {
// key: OccupantID // key: Resource
// value: last user presence // value: last user presence
Members sync.Map Members sync.Map
} }

View File

@@ -1,3 +1,3 @@
package main package main
var lambda_version string = "0.1.0" var lambda_version string = "26w11a"

17
xmpp-attention.go Normal file
View File

@@ -0,0 +1,17 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Implementation of XEP-0224: Attention
type Attention struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:attention:0 attention"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:attention:0", Local: "attention"}, Attention{})
}

22
xmpp-bookmarks.go Normal file
View File

@@ -0,0 +1,22 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Implementation of XEP-0402: PEP Native Bookmarks
// https://xmpp.org/extensions/xep-0402.html
type Bookmark struct {
XMLName xml.Name `xml:"urn:xmpp:bookmarks:1 conference"`
Name string `xml:"name,attr,omitempty"`
Autojoin bool `xml:"autojoin,attr,omitempty"`
Nick string `xml:"nick,omitempty"`
Password string `xml:"password,omitempty"`
Extensions []any `xml:"extensions,omitempty"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "urn:xmpp:bookmarks:1", Local: "conference"}, Bookmark{})
}

26
xmpp-carbons.go Normal file
View File

@@ -0,0 +1,26 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Implementation of XEP-0280: Message Carbons
// https://xmpp.org/extensions/xep-0280.html
type ReceivedCarbon struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:carbons:2 received"`
Forwarded stanza.Forwarded
}
type SentCarbon struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:carbons:2 sent"`
Forwarded stanza.Forwarded
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:carbons:2", Local: "received"}, ReceivedCarbon{})
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:carbons:2", Local: "sent"}, SentCarbon{})
}

19
xmpp-displayed_markers.go Normal file
View File

@@ -0,0 +1,19 @@
package main
// Partial implementation of XEP-0333: Displayed Markers
// https://xmpp.org/extensions/xep-0333.html
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
type Marker struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 displayed"`
ID string `xml:"id,attr"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:chat-markers:0", Local: "displayed"}, Marker{})
}

View File

@@ -9,7 +9,7 @@ import (
// This file has small functions that can be used to do XMPP stuff without writing tons of boilerplate // 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 // 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 { func sendMessage(c xmpp.Sender, sendTo string, msgType stanza.StanzaType, body string, subject string, thread string, exts []stanza.MsgExtension) error {
m := stanza.Message{ m := stanza.Message{
Attrs: stanza.Attrs{ Attrs: stanza.Attrs{
To: sendTo, To: sendTo,
@@ -18,6 +18,7 @@ func sendMessage(c xmpp.Sender, sendTo string, msgType stanza.StanzaType, body s
Body: body, Body: body,
Subject: subject, Subject: subject,
Thread: thread, Thread: thread,
Extensions: exts,
} }
err := c.Send(m) err := c.Send(m)
if err != nil { if err != nil {
@@ -46,3 +47,14 @@ func joinMuc(c xmpp.Sender, jid string, muc string, nick string) error {
} }
return nil return nil
} }
// jid MustParse but using gosrc's instead of mellium
// This function will panic if its an invalid JID
func JidMustParse(s string) *stanza.Jid {
j, err := stanza.NewJid(s)
if err != nil {
panic(err)
}
return j
}

23
xmpp-mentions.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Experimental implementation of XEP-XXXX: Explicit Mentions
// https://git.isekai.rocks/snit/protoxeps/tree/explicit-mentions.xml
type Mention struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:mentions:0 mention"`
URI string `xml:"uri,attr,omitempty"`
Begin int `xml:"begin,attr,omitempty"`
End int `xml:"end,attr,omitempty"`
Type string `xml:"type,attr"`
Target string `xml:"target,attr,omitempty"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:mentions:0", Local: "mention"}, Mention{})
}

View File

@@ -10,6 +10,7 @@ import (
type MucUser struct { type MucUser struct {
stanza.PresExtension stanza.PresExtension
stanza.MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"`
MucUserItem MucUserItem `xml:"item,omitempty"` MucUserItem MucUserItem `xml:"item,omitempty"`
} }
@@ -20,8 +21,15 @@ type MucUserItem struct {
Role string `xml:"role,attr,omitempty"` // TODO: Use enum Role string `xml:"role,attr,omitempty"` // TODO: Use enum
JID string `xml:"jid,attr,omitempty"` JID string `xml:"jid,attr,omitempty"`
Reason string `xml:"reason,omitempty"` Reason string `xml:"reason,omitempty"`
Actor Actor `xml:"actor,omitempty"`
}
type Actor struct {
JID string `xml:"jid,attr"`
Nick string `xml:"nick,attr"`
} }
func init() { func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc#user", Local: "x"}, MucUser{}) stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc#user", Local: "x"}, MucUser{})
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "http://jabber.org/protocol/muc#user", Local: "x"}, MucUser{})
} }

View File

@@ -1,20 +0,0 @@
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{})
}

21
xmpp-webxdc.go Normal file
View File

@@ -0,0 +1,21 @@
package main
// Implementation of XEP-0491: WebXDC
// https://xmpp.org/extensions/xep-0491.html
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
type XDCEl struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:webxdc:0 x"`
Document string `xml:"document"`
Summary string `xml:"summary"`
Payload string `xml:"urn:xmpp:json:0 json"` // TODO: Independent JSOn container type (XEP-0335)
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:webxdc:0", Local: "x"}, XDCEl{})
}