Compare commits

..

17 Commits

Author SHA1 Message Date
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: #1
2026-01-31 23:32:04 +00:00
f4eb50c741 One character patch to allow building with Go 1.25.5 2026-01-31 22:14:39 +00:00
18 changed files with 528 additions and 218 deletions

View File

@@ -1,12 +1,3 @@
# 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
Server = "example.com:5222"
Username = "user@example.com"
Password = "123456789"
Insecure = false
Nick = "User"
```

BIN
assets/admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

BIN
assets/cancel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 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/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/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

2
go.mod
View File

@@ -1,6 +1,6 @@
module lambda-im module lambda-im
go 1.25.6 go 1.25.5
require ( require (
github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/toml v1.6.0

View File

@@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"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 +17,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 +31,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,8 +48,14 @@ 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) m, ok := mucmembers.Load(jid)
ma := m.(mucUnit) 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)
@@ -57,10 +68,44 @@ func switchToTab(jid string, w *gtk.Window) {
u.Get(&mu) u.Get(&mu)
u.Get(&ocu) u.Get(&ocu)
nick_label := gtk.NewLabel(Jid.MustParse(u.From).Resourcepart()) nick_label := gtk.NewLabel(JidMustParse(u.From).Resource)
nick_label.SetEllipsize(pango.EllipsizeEnd)
/*
affil_label := gtk.NewLabel("")
switch mu.MucUserItem.Affiliation {
case "owner":
affil_label.SetText("O")
case "admin":
affil_label.SetText("A")
case "member":
affil_label.SetText("M")
case "none":
affil_label.SetText("-")
}
*/
nick_label.AddCSSClass(mu.MucUserItem.Role)
if mu.MucUserItem.Role == "visitor" {
nick_label.SetOpacity(0.5)
}
/*
affil_label.SetHAlign(gtk.AlignEnd)
affil_label.SetHExpand(true)
affil_label.AddCSSClass(mu.MucUserItem.Affiliation)
*/
userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\nRight-click for more information", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation))
userbox.Append(nick_label) userbox.Append(nick_label)
// userbox.Append(affil_label)
medal := gtk.NewImageFromPaintable(clientAssets[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(3) // Right click
@@ -68,7 +113,9 @@ func switchToTab(jid string, w *gtk.Window) {
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)
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(gtk.NewLabel(u.From))
@@ -96,6 +143,16 @@ func switchToTab(jid string, w *gtk.Window) {
} }
} }
var mu MucUser
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
profile_box.Append(gtk.NewLabel(mu.MucUserItem.JID))
}
profile_box.Append(gtk.NewLabel("Connected with role " + mu.MucUserItem.Role))
profile_box.Append(gtk.NewLabel("Affiliated as " + mu.MucUserItem.Affiliation))
}
go func() { go func() {
ctx := context.TODO() ctx := context.TODO()
mychan, err := client.SendIQ(ctx, iqResp) mychan, err := client.SendIQ(ctx, iqResp)
@@ -116,7 +173,7 @@ func switchToTab(jid string, w *gtk.Window) {
}() }()
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(ocu.ID)
@@ -149,8 +206,11 @@ func switchToTab(jid string, w *gtk.Window) {
win.Present() win.Present()
}) })
userbox.AddController(gesture) userbox.AddController(gesture)
if mu.MucUserItem.Role == "moderator" {
gen.Append(userbox) gen.Prepend(userbox)
} else {
gen.Append(userbox)
}
return true return true
}) })

View File

@@ -7,6 +7,7 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jacoblockett/sanitizefilename" "github.com/jacoblockett/sanitizefilename"
"github.com/jasonlovesdoggo/gopen" "github.com/jasonlovesdoggo/gopen"
@@ -32,9 +33,9 @@ func generatePresenceWidget(p stanza.Packet) gtk.Widgetter {
} }
} }
return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " left the room") return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " left the MUC")
} else { } else {
return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " joined the room") return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " joined the MUC")
} }
} }
@@ -43,6 +44,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
}
indicator := stanza.StateComposing{}
ok = m.Get(&indicator)
if ok { // TODO: Display typing indicator in a stat bar or something similar
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)
return error_box
}
sid := StanzaID{} sid := StanzaID{}
m.Get(&sid) m.Get(&sid)
@@ -50,17 +80,21 @@ 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
reactions := gtk.NewBox(gtk.OrientationHorizontal, 0)
reactions.SetVisible(false)
popover := gtk.NewPopover()
popover.SetParent(mainBox)
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,14 +106,30 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
reactions.Append(like) reactions.Append(like)
} }
rc_box.Append(reactions)
if m.Type == stanza.MessageTypeGroupchat {
moderate := gtk.NewButtonWithLabel("Moderate") // TODO: Implement proper support for moderations via extension
moderate.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
<iq type='set' to='%s' id='%s'>
<moderate id='%s' xmlns='urn:xmpp:message-moderate:1'>
<retract xmlns='urn:xmpp-message-retract:1'/>
<reason>Retracted</reason>
</moderate>
</iq>
`, jid.MustParse(m.From).Bare().String(), uuid.New().String(), sid.ID))
})
rc_box.Append(moderate)
}
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)
@@ -102,6 +152,9 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
// authorBox.Append(im) // authorBox.Append(im)
al := gtk.NewLabel(jid.MustParse(m.From).Resourcepart())
al.AddCSSClass("author")
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)
@@ -118,25 +171,24 @@ 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)
mlabel := gtk.NewLabel(m.Body) mlabel := gtk.NewLabel(m.Body)
// mlabel.SetMarkup(convertXEPToPango(m.Body))
mlabel.SetWrap(true) mlabel.SetWrap(true)
mlabel.SetSelectable(true) mlabel.SetSelectable(true)
mlabel.SetHAlign(gtk.AlignFill) mlabel.SetHAlign(gtk.AlignFill)
@@ -174,12 +226,12 @@ 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.
p, err := ensureCache() p, err := ensureCache()
if err != nil { if err != nil {
return newImageFromPath("debug.png") return gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
} }
if hash == "" { if hash == "" {
fmt.Println("Hash is nil!") fmt.Println("Hash is nil!")
return newImageFromPath("debug.png") return gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
} }
hash = filepath.Join(p, sanitizefilename.Sanitize(hash)) hash = filepath.Join(p, sanitizefilename.Sanitize(hash))
@@ -211,12 +263,12 @@ 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" && runtime.GOOS == "windows") {
return newImageFromPath("debug.png") return gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"])
} }
data, err := base64.StdEncoding.DecodeString(base64_data) data, err := base64.StdEncoding.DecodeString(base64_data)

113
gtk-signin.go Normal file
View File

@@ -0,0 +1,113 @@
package main
import (
"os"
"bytes"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"path/filepath"
"github.com/BurntSushi/toml"
_ "embed"
)
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()
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)
}
}

391
main.go
View File

@@ -3,7 +3,6 @@ package main
import ( import (
"os" "os"
"sync" "sync"
"bytes"
"context" "context"
"fmt" "fmt"
@@ -11,7 +10,7 @@ import (
"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/diamondburned/gotk4/pkg/gdkpixbuf/v2"
"path/filepath" "path/filepath"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
@@ -22,7 +21,9 @@ import (
_ "embed" _ "embed"
"encoding/xml" "encoding/xml"
"math/rand/v2"
"runtime" "runtime"
"encoding/base64"
) )
var loadedConfig lambdaConfig var loadedConfig lambdaConfig
@@ -55,6 +56,37 @@ var mucmembers sync.Map
// stores devices of users // stores devices of users
var userdevices sync.Map var userdevices sync.Map
//go:embed debug.png
var defaultAvatarBytes []byte
var defaultAvatarB64 string = base64.StdEncoding.EncodeToString(defaultAvatarBytes)
//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)
var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler)
var lockedJIDs map[string]bool = make(map[string]bool)
func init() { func init() {
go func() { go func() {
for fn := range uiQueue { for fn := range uiQueue {
@@ -65,97 +97,70 @@ func init() {
time.Sleep(10 * time.Millisecond) // Small delay between updates time.Sleep(10 * time.Millisecond) // Small delay between updates
} }
}() }()
}
func dropToSignInPage(err error) { loader := gdkpixbuf.NewPixbufLoader()
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) defaultAvatarData, _ := base64.StdEncoding.DecodeString(defaultAvatarB64)
username_box := gtk.NewBox(gtk.OrientationHorizontal, 0) loader.Write(defaultAvatarData)
password_box := gtk.NewBox(gtk.OrientationHorizontal, 0) loader.Close()
nickname_box := gtk.NewBox(gtk.OrientationHorizontal, 0) clientAssets["DefaultAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
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) loader = gdkpixbuf.NewPixbufLoader()
server_box.Append(server_entry)
username_box.Append(username_label) ownerMedalData, _ := base64.StdEncoding.DecodeString(ownerMedalB64)
username_box.Append(username_entry) loader.Write(ownerMedalData)
loader.Close()
password_box.Append(password_label) clientAssets["owner"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
password_box.Append(password_entry)
nickname_box.Append(nickname_label) loader = gdkpixbuf.NewPixbufLoader()
nickname_box.Append(nickname_entry)
insecure_box.Append(insecure_label) cancelData, _ := base64.StdEncoding.DecodeString(cancelB64)
insecure_box.Append(insecure_check) loader.Write(cancelData)
loader.Close()
form_box.Append(server_box) clientAssets["cancel"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
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") loader = gdkpixbuf.NewPixbufLoader()
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 adminMedalData, _ := base64.StdEncoding.DecodeString(adminMedalB64)
e := toml.NewEncoder(&b) loader.Write(adminMedalData)
e.Encode(conf) loader.Close()
p, _ := ensureConfig() clientAssets["admin"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
os.WriteFile(filepath.Join(p, "lambda.toml"), b.Bytes(), 0644)
window.SetVisible(false)
main()
os.Exit(0)
})
form_box.Append(sumbit_btn) loader = gdkpixbuf.NewPixbufLoader()
window = gtk.NewApplicationWindow(app) memberMedalData, _ := base64.StdEncoding.DecodeString(memberMedalB64)
window.SetChild(form_box) loader.Write(memberMedalData)
window.SetResizable(false) loader.Close()
window.SetVisible(true)
})
if code := app.Run(os.Args); code == 0 { clientAssets["member"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
os.Exit(code)
} 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())
} }
func main() { func main() {
p, _ := ensureConfig() 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,11 +173,18 @@ func main() {
panic(err) panic(err)
} }
// Put 4 random characters at the end
chars := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZλ"
str := ""
for range 4 {
str = str + string(chars[rand.IntN(len(chars))])
}
config := xmpp.Config{ config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{ TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server, Address: loadedConfig.Server,
}, },
Jid: loadedConfig.Username, Jid: loadedConfig.Username + "/lambda." + str,
Credential: xmpp.Password(loadedConfig.Password), Credential: xmpp.Password(loadedConfig.Password),
Insecure: loadedConfig.Insecure, Insecure: loadedConfig.Insecure,
// StreamLogger: os.Stdout, // StreamLogger: os.Stdout,
@@ -240,30 +252,38 @@ func main() {
return return
} }
if m.Body == "" { e := stanza.PubSubEvent{}
return ok = m.Get(&e)
if ok {
fmt.Println(e)
} }
/*
if m.Body == "" {
return
}
*/
originator := jid.MustParse(m.From).Bare().String() originator := jid.MustParse(m.From).Bare().String()
glib.IdleAdd(func() { glib.IdleAdd(func() {
uiQueue <- func() { //uiQueue <- func() {
b := gtk.NewBox(gtk.OrientationVertical, 0) b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generateMessageWidget(p).(*gtk.Box) ba, ok := generateMessageWidget(p).(*gtk.Box)
if ok { if ok {
b = ba b = ba
}
tab, ok := tabs.Load(originator)
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
}
} }
tab, ok := tabs.Load(originator)
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
}
//}
}) })
}) })
@@ -273,8 +293,6 @@ func main() {
return return
} }
pretty.Println(presence)
if presence.Error != *new(stanza.Err) { if presence.Error != *new(stanza.Err) {
return return
} }
@@ -286,7 +304,8 @@ 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() 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{})
@@ -304,22 +323,20 @@ func main() {
} else { } else {
typed_unit.Members.Delete(ocu.ID) typed_unit.Members.Delete(ocu.ID)
glib.IdleAdd(func() { glib.IdleAdd(func() {
uiQueue <- func() { b := gtk.NewLabel("")
b := gtk.NewLabel("") ba, ok := generatePresenceWidget(p).(*gtk.Label)
ba, ok := generatePresenceWidget(p).(*gtk.Label) if ok {
if ok { b = ba
b = ba }
}
tab, ok := tabs.Load(muc) tab, ok := tabs.Load(muc)
typed_tab := tab.(*chatTab) typed_tab := tab.(*chatTab)
if ok { if ok {
typed_tab.msgs.Append(b) typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller) scrollToBottomAfterUpdate(scroller)
} else { } else {
fmt.Println("Got message when the tab does not exist!") fmt.Println("Got message when the tab does not exist!")
}
} }
}) })
} }
@@ -332,20 +349,21 @@ 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
userdevices.Store(user, userUnit{}) ok := createTab(user, false)
createTab(user, false) if ok && !lockedJIDs[user]{
userdevices.Store(user, userUnit{})
b := gtk.NewButtonWithLabel(user) b := gtk.NewButtonWithLabel(user)
b.ConnectClicked(func() { b.ConnectClicked(func() {
b.AddCSSClass("accent") b.AddCSSClass("accent")
switchToTab(user, &window.Window) switchToTab(user, &window.Window)
}) })
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
} }
@@ -360,7 +378,9 @@ func main() {
} }
userdevices.Store(user, typed_unit) userdevices.Store(user, typed_unit)
lockedJIDs[user] = true
} }
time.Sleep(1 * time.Second)
}) })
c, err := xmpp.NewClient(&config, router, func(err error) { c, err := xmpp.NewClient(&config, router, func(err error) {
@@ -405,38 +425,83 @@ 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")
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(200, 200)
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.NewButtonWithLabel(t)
b.ConnectClicked(func() {
b.AddCSSClass("accent")
switchToTab(t, &window.Window)
})
menu.Append(b)
}
win.SetVisible(false)
})
win.SetTransientFor(win)
win.Present()
})
app.AddAction(joinAction)
the_menu.AppendSubmenu("File", fileMenu)
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()
e_menu.SetLabel("Edit")
e_menu.SetAlwaysShowArrow(false)
v_menu := gtk.NewMenuButton()
v_menu.SetLabel("View")
v_menu.SetAlwaysShowArrow(false)
b_menu := gtk.NewMenuButton()
b_menu.SetLabel("Bookmarks")
b_menu.SetAlwaysShowArrow(false)
h_menu := gtk.NewMenuButton()
h_menu.SetLabel("Help")
h_menu.SetAlwaysShowArrow(false)
menu.Append(f_menu)
menu.Append(e_menu)
menu.Append(v_menu)
menu.Append(b_menu)
menu.Append(h_menu)
*/
empty_dialog = gtk.NewLabel("You are not focused on any chats.") empty_dialog = gtk.NewLabel("You are not focused on any chats.")
empty_dialog.SetVExpand(true) empty_dialog.SetVExpand(true)
@@ -448,6 +513,8 @@ 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)
// 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,10 +528,12 @@ 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)
@@ -510,37 +579,9 @@ func activate(app *gtk.Application) {
en.SetHExpand(true) en.SetHExpand(true)
m_entry := gtk.NewEntry()
entry_box.Append(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
rsrc_windows_amd64.syso Normal file

Binary file not shown.

View File

@@ -10,3 +10,26 @@
.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;
}

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

@@ -46,3 +46,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
}