Files
lambda/main.go
T

1184 lines
29 KiB
Go
Raw Normal View History

2026-01-29 21:35:36 +00:00
package main
import (
"os"
"strings"
2026-01-30 10:40:38 +00:00
"sync"
2026-01-29 21:35:36 +00:00
"context"
"fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
2026-01-29 21:35:36 +00:00
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/gen2brain/beeep"
2026-02-20 06:28:31 +00:00
"github.com/go-analyze/charts"
2026-03-10 16:35:56 +00:00
"golang.org/x/net/html/charset"
2026-01-31 21:35:26 +00:00
"path/filepath"
2026-01-29 21:35:36 +00:00
"github.com/BurntSushi/toml"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"mellium.im/xmpp/jid"
"time"
_ "embed"
"encoding/xml"
"github.com/kr/pretty"
2026-03-10 16:35:56 +00:00
"io"
2026-03-10 16:48:10 +00:00
"runtime"
2026-01-29 21:35:36 +00:00
)
var loadedConfig lambdaConfig
var empty_dialog *gtk.Image
2026-01-29 21:35:36 +00:00
2026-02-17 09:27:46 +00:00
var connectionStatus *gtk.Label
var connectionIcon *gtk.Image
var mStatus *gtk.Label
var mIcon *gtk.Image
var sStatus *gtk.Label
var sIcon *gtk.Image
2026-04-06 11:04:53 +01:00
var typingStatus *gtk.Label
2026-02-17 09:27:46 +00:00
var pingStatus *gtk.Label
2026-01-29 21:35:36 +00:00
var content *gtk.Widgetter
var tabs sync.Map
2026-01-29 21:35:36 +00:00
var current string
var scroller *gtk.ScrolledWindow
2026-01-30 10:40:38 +00:00
var memberList *gtk.ScrolledWindow
var menu *gtk.Box
2026-03-10 16:35:56 +00:00
var message_en *gtk.Entry
2026-01-29 21:35:36 +00:00
//go:embed style.css
var styleCSS string
var client xmpp.Sender
var clientroot *xmpp.Client
var uiQueue = make(chan func(), 100)
2026-01-31 15:38:02 +00:00
var window *gtk.ApplicationWindow
2026-01-30 10:40:38 +00:00
// stores members of mucs
var mucmembers sync.Map
2026-01-31 10:02:04 +00:00
// stores devices of users
var userdevices sync.Map
2026-02-20 06:28:31 +00:00
var pingTimes = [][]float64{}
2026-02-17 09:27:46 +00:00
2026-02-02 13:23:08 +00:00
var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler)
2026-04-26 10:40:13 +01:00
var xmlLog *os.File
2026-01-29 21:35:36 +00:00
func init() {
2026-04-28 12:58:00 +01:00
beeep.AppName = loadedLocale["appName"]
2026-01-29 21:35:36 +00:00
go func() {
for fn := range uiQueue {
glib.IdleAdd(func() bool {
fn()
return false
})
time.Sleep(10 * time.Millisecond) // Small delay between updates
}
}()
2026-02-02 13:23:08 +00:00
2026-01-29 21:35:36 +00:00
}
2026-01-31 21:35:26 +00:00
func main() {
2026-02-20 06:28:31 +00:00
pingTimes = append(pingTimes, []float64{})
p, err := ensureConfig()
if err != nil {
panic(err)
}
2026-01-31 21:35:26 +00:00
b, err := os.ReadFile(filepath.Join(p, "lambda.toml"))
2026-01-29 21:35:36 +00:00
if err != nil {
2026-01-31 21:35:26 +00:00
dropToSignInPage(err)
return
2026-01-29 21:35:36 +00:00
}
_, err = toml.Decode(string(b), &loadedConfig)
2026-01-31 21:35:26 +00:00
if err != nil {
panic(err)
}
2026-01-29 21:35:36 +00:00
2026-02-28 16:24:37 +00:00
if loadedConfig.Resource == "" {
2026-04-28 12:58:00 +01:00
fmt.Println(loadedLocale["configResourceEmptyWarning"])
2026-02-28 16:24:37 +00:00
loadedConfig.Resource = randomClientResource()
2026-01-31 23:32:26 +00:00
}
2026-04-26 10:40:13 +01:00
if !loadedConfig.Debug {
xmlLog, err = os.CreateTemp("", "xmpp-log")
if err != nil {
panic(err)
}
defer os.Remove(xmlLog.Name())
} else {
xmlLog = os.Stdout
}
2026-01-29 21:35:36 +00:00
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server,
2026-03-10 16:35:56 +00:00
CharsetReader: func(c string, input io.Reader) (io.Reader, error) {
return charset.NewReaderLabel(c, input)
},
ConnectTimeout: 300,
2026-01-29 21:35:36 +00:00
},
Jid: loadedConfig.Username + "/" + loadedConfig.Resource,
Credential: xmpp.Password(loadedConfig.Password),
Insecure: loadedConfig.Insecure,
StreamManagementEnable: true,
ConnectTimeout: 300,
StreamLogger: xmlLog,
2026-01-29 21:35:36 +00:00
}
router := xmpp.NewRouter()
router.NewRoute().IQNamespaces(stanza.NSDiscoInfo).HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
iq, ok := p.(*stanza.IQ)
if !ok || iq.Type != stanza.IQTypeGet {
return
}
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
if err != nil {
panic(err)
}
identity := stanza.Identity{
Name: "Lambda",
Category: "client", // TODO: Allow spoofing on user request
Type: "pc",
}
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
{Var: "http://jabber.org/protocol/muc"},
2026-01-31 15:08:54 +00:00
{Var: "λ"},
2026-03-10 16:35:56 +00:00
{Var: "urn:xmpp:attention:0"},
2026-01-29 21:35:36 +00:00
},
}
iqResp.Payload = &payload
s.Send(iqResp)
})
router.NewRoute().IQNamespaces("jabber:iq:version").HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
iq, ok := p.(*stanza.IQ)
if !ok || iq.Type != stanza.IQTypeGet {
return
}
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
if err != nil {
panic(err)
}
v := &stanza.Version{}
2026-04-28 12:58:00 +01:00
v = v.SetInfo(loadedLocale["appName"], lambda_version, runtime.GOOS) // TODO: Allow spoofing on user request
2026-01-29 21:35:36 +00:00
iqResp.Payload = v
s.Send(iqResp)
})
router.HandleFunc("message", func(s xmpp.Sender, p stanza.Packet) {
m, ok := p.(stanza.Message)
if !ok {
return
}
2026-02-04 10:12:49 +00:00
e := stanza.PubSubEvent{}
ok = m.Get(&e)
if ok {
fmt.Println(e)
}
2026-01-31 23:32:26 +00:00
/*
if m.Body == "" {
return
}
2026-01-31 23:32:26 +00:00
*/
2026-01-29 21:35:36 +00:00
originator := JidMustParse(m.From).Bare()
mStatus.SetText(originator)
2026-01-29 21:35:36 +00:00
2026-03-10 16:35:56 +00:00
at := new(Attention)
ok = m.Get(at)
if ok {
2026-04-28 12:58:00 +01:00
beeep.Notify(loadedLocale["attention"], fmt.Sprintf("%s: %s", JidMustParse(m.From).Resource, m.Body), commentBytes) // TODO: Use localpart if DM
2026-03-10 16:35:56 +00:00
}
// Handle mentions
for _, ext := range m.Extensions {
mention, ok := ext.(*Mention)
if ok {
pretty.Println(mention)
}
}
2026-03-10 16:35:56 +00:00
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
2026-04-06 11:04:53 +01:00
orig := m.To
2026-03-10 16:35:56 +00:00
m = sc.Forwarded.Stanza.(stanza.Message)
2026-04-06 11:04:53 +01:00
m.To = orig
2026-03-10 16:35:56 +00:00
} 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))
}
}
}
2026-04-06 11:04:53 +01:00
composing := stanza.StateComposing{}
ok = m.Get(&composing)
if ok && current == JidMustParse(m.From).Bare() {
2026-04-28 12:58:00 +01:00
typingStatus.SetText(fmt.Sprintf("%s %s", m.From, loadedLocale["isTyping"]))
2026-04-06 11:04:53 +01:00
return
}
inactive := stanza.StateInactive{}
ok = m.Get(&inactive)
if ok && current == JidMustParse(m.From).Bare() {
typingStatus.SetText("")
return
}
2026-03-10 16:35:56 +00:00
2026-01-29 21:35:36 +00:00
glib.IdleAdd(func() {
2026-02-01 18:16:55 +00:00
//uiQueue <- func() {
2026-02-01 19:22:43 +00:00
b := gtk.NewBox(gtk.OrientationVertical, 0)
2026-01-29 21:35:36 +00:00
2026-02-01 19:22:43 +00:00
tab, ok := tabs.Load(originator)
2026-03-10 16:35:56 +00:00
if !ok {
return
}
2026-02-01 19:22:43 +00:00
typed_tab := tab.(*chatTab)
2026-01-29 21:35:36 +00:00
2026-02-01 19:22:43 +00:00
if ok {
typed_tab.msgs.Append(b)
if current == JidMustParse(m.From).Bare() {
scrollToBottomAfterUpdate(scroller)
}
2026-02-01 19:22:43 +00:00
} else {
fmt.Println("Got message when the tab does not exist!")
}
ba, ok := generateMessageWidget(p).(*gtk.Box)
if ok {
b.Append(ba)
}
2026-02-01 18:16:55 +00:00
//}
2026-01-29 21:35:36 +00:00
})
})
2026-01-30 10:40:38 +00:00
router.HandleFunc("presence", func(s xmpp.Sender, p stanza.Packet) {
presence, ok := p.(stanza.Presence)
if !ok {
return
}
if presence.Error.Reason != "" {
beeep.Notify(fmt.Sprintf("%s : %s", presence.From, presence.Error.Reason), presence.Error.Text, cancelBytes)
return
}
2026-01-30 10:40:38 +00:00
var mu MucUser
var ocu OccupantID
ok = presence.Get(&mu)
if ok { // This is a presence stanza from a user in a MUC
presence.Get(&ocu)
// id := ocu.ID
// if id == "" {
id := JidMustParse(presence.From).Resource
// }
from, _ := stanza.NewJid(presence.From)
muc := from.Bare()
2026-01-30 10:40:38 +00:00
_, ok = mucmembers.Load(muc)
if !ok {
mucmembers.Store(muc, mucUnit{})
}
unit, ok := mucmembers.Load(muc)
if !ok {
return
}
typed_unit := unit.(mucUnit)
if presence.Type != "unavailable" {
_, ok := typed_unit.Members.Load(id)
if !ok {
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)
if current == muc {
scrollToBottomAfterUpdate(scroller)
}
} else {
fmt.Println("Got message when the tab does not exist!")
}
})
}
typed_unit.Members.Store(id, presence)
2026-01-30 10:40:38 +00:00
} else {
typed_unit.Members.Delete(id)
2026-01-31 10:02:04 +00:00
glib.IdleAdd(func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generatePresenceWidget(p).(*gtk.Box)
2026-02-01 19:22:43 +00:00
if ok {
b = ba
}
2026-01-31 10:02:04 +00:00
2026-02-01 19:22:43 +00:00
tab, ok := tabs.Load(muc)
typed_tab := tab.(*chatTab)
2026-01-31 10:02:04 +00:00
2026-02-01 19:22:43 +00:00
if ok {
typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
}
2026-01-31 10:02:04 +00:00
})
2026-01-30 10:40:38 +00:00
}
mucmembers.Store(muc, typed_unit)
} else { // This is a presence stanza from a regular user
2026-01-31 10:02:04 +00:00
// The code is basically the exact same as above, we just don't check for mucuser
user := jid.MustParse(presence.From).Bare().String()
_, ok := userdevices.Load(user)
2026-01-31 10:02:04 +00:00
_, mok := mucmembers.Load(user)
if !ok && !mok { // FIXME: The initial muc presence gets picked up from this check
2026-04-26 10:40:13 +01:00
ok := createTab(user, false, user)
if ok {
userdevices.Store(user, userUnit{})
b := gtk.NewLabel(user)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(user, &window.Window)
})
b.AddController(gesture1)
menu.Append(b)
}
}
unit, ok := userdevices.Load(user)
if !ok {
return
}
resource := jid.MustParse(presence.From).Resourcepart()
typed_unit := unit.(userUnit)
if presence.Type != "unavailable" {
typed_unit.Devices.Store(resource, presence)
} else {
typed_unit.Devices.Delete(resource)
}
userdevices.Store(user, typed_unit)
2026-01-30 10:40:38 +00:00
}
2026-02-03 10:07:33 +00:00
time.Sleep(1 * time.Second)
2026-01-30 10:40:38 +00:00
})
2026-01-29 21:35:36 +00:00
c, err := xmpp.NewClient(&config, router, func(err error) {
2026-04-28 12:58:00 +01:00
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["disconnected"], err.Error()))
2026-02-28 16:24:37 +00:00
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
2026-01-29 21:35:36 +00:00
})
2026-01-29 21:35:36 +00:00
if err != nil {
showErrorDialog(err)
panic(err)
}
client = c
clientroot = c
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
fmt.Println("XMPP client connected")
// Ping
2026-02-17 09:27:46 +00:00
go func() {
for {
time.Sleep(5 * time.Second)
go func() {
pingStatus.AddCSSClass("pending")
before := time.Now()
iq := new(stanza.IQ)
iq.From = clientroot.Session.BindJid
iq.To = iq.From
iq.Type = "get"
2026-02-17 09:27:46 +00:00
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
mychan, err := client.SendIQ(ctx, iq)
if err != nil {
return
}
_ = <-mychan
pingStatus.RemoveCSSClass("pending")
delay := time.Since(before) / time.Millisecond
2026-04-28 12:58:00 +01:00
pingStatus.SetText(fmt.Sprintf("%d %s", delay, loadedLocale["milliseconds"]))
pingTimes[0] = append(pingTimes[0], float64(delay))
}()
}
}()
// Throughput
var oldsize int64
var newsize int64
go func() {
for {
time.Sleep(1 * time.Second)
stat, err := xmlLog.Stat()
2026-02-17 09:27:46 +00:00
if err != nil {
panic(err)
2026-02-17 09:27:46 +00:00
}
newsize = stat.Size()
diff := float64(newsize-oldsize) / 1000
2026-04-26 10:40:13 +01:00
if diff > 100 {
sIcon.SetFromPaintable(clientAssets["car_high"])
} else {
sIcon.SetFromPaintable(clientAssets["car"])
}
2026-04-28 12:58:00 +01:00
sStatus.SetText(fmt.Sprintf("%.2f%s", diff, loadedLocale["KBPerSecond"]))
oldsize = stat.Size()
2026-02-17 09:27:46 +00:00
}
}()
2026-04-26 10:40:13 +01:00
2026-04-28 12:58:00 +01:00
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["connectedAs"], JidMustParse(clientroot.Session.BindJid).Bare()))
connectionStatus.SetTooltipText(fmt.Sprintf("%s%s\n%s%t", loadedLocale["bindedJid"], clientroot.Session.BindJid, loadedLocale["usingTLS"], clientroot.Session.TlsEnabled))
connectionIcon.SetFromPaintable(clientAssets["connect"])
2026-03-10 16:35:56 +00:00
// 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))
2026-02-17 09:27:46 +00:00
// 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 {
jid := item.Id
node := item.Any
autojoin := false
2026-04-26 10:40:13 +01:00
name := ""
for _, attr := range node.Attrs {
if attr.Name.Local == "autojoin" {
autojoin = attr.Value == "true"
2026-04-26 10:40:13 +01:00
break
}
}
for _, attr := range node.Attrs {
if attr.Name.Local == "name" {
name = attr.Value
break
2026-02-17 09:27:46 +00:00
}
}
2026-02-17 09:27:46 +00:00
_, ok := tabs.Load(jid)
if !ok && autojoin {
2026-04-26 10:40:13 +01:00
createTab(jid, true, name)
b := gtk.NewLabel(jid)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
b.AddController(gesture1)
menu.Append(b)
}
}
for _, item := range res.Items.List {
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"
2026-04-26 10:40:13 +01:00
break
}
}
for _, node := range node.Nodes {
if node.XMLName.Local == "nick" {
nick = node.Content
2026-04-26 10:40:13 +01:00
break
}
}
if autojoin {
2026-04-26 10:40:13 +01:00
err := joinMuc(client, clientroot.Session.BindJid, jid, nick, "")
if err != nil {
panic(err)
}
}
}
}
}
}
}
2026-01-29 21:35:36 +00:00
})
conc := func() {
2026-01-31 21:35:26 +00:00
time.Sleep(3 * time.Second)
2026-04-28 12:58:00 +01:00
connectionStatus.SetText(loadedLocale["connecting"])
connectionIcon.SetFromPaintable(clientAssets["hourglass"])
2026-01-29 21:35:36 +00:00
err = cm.Run()
if err != nil {
fmt.Println(err.Error())
2026-04-28 12:58:00 +01:00
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["disconnected"], err.Error()))
2026-02-28 16:24:37 +00:00
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
2026-01-29 21:35:36 +00:00
}
}
2026-01-29 21:35:36 +00:00
app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
app.ConnectActivate(func() {
2026-03-15 09:57:08 +00:00
go conc()
activate(app)
})
2026-01-29 21:35:36 +00:00
if code := app.Run(os.Args); code > 0 {
os.Exit(code)
}
}
func activate(app *gtk.Application) {
// Load the CSS and apply it globally.
gtk.StyleContextAddProviderForDisplay(
gdk.DisplayGetDefault(), loadCSS(styleCSS),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
2026-01-31 15:38:02 +00:00
window = gtk.NewApplicationWindow(app)
2026-02-01 19:22:43 +00:00
the_menu := gio.NewMenu()
fileMenu := gio.NewMenu()
2026-04-28 12:58:00 +01:00
fileMenu.Append(loadedLocale["joinMUCMenu"], "app.join")
fileMenu.Append(loadedLocale["startDMMenu"], "app.dm")
fileMenu.Append(loadedLocale["destroyMUCMenu"], "app.destroymuc")
2026-02-01 19:22:43 +00:00
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()
2026-04-28 12:58:00 +01:00
win.SetTitle(loadedLocale["destroyMUCMenu"])
win.SetDefaultSize(400, 1)
win.SetResizable(false)
2026-04-28 12:58:00 +01:00
box := gtk.NewBox(gtk.OrientationVertical, 10)
box.Append(gtk.NewLabel(loadedLocale["destroyMUCWarningOne"]))
box.Append(gtk.NewLabel(loadedLocale["destroyMUCWarningTwo"]))
cancel := gtk.NewButtonWithLabel(loadedLocale["cancel"])
cancel.ConnectClicked(func() {
win.SetVisible(false)
})
en := gtk.NewEntry()
en.SetPlaceholderText("...")
2026-04-28 12:58:00 +01:00
submit := gtk.NewButtonWithLabel(loadedLocale["destroyMUCActionButton"])
submit.ConnectClicked(func() {
fmt.Println(en.Text())
2026-04-28 12:58:00 +01:00
if en.Text() == loadedLocale["destroyMUCPassword"] {
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'>
2026-04-28 12:58:00 +01:00
<reason>%s</reason>
</destroy>
</query>
</iq>
2026-04-28 12:58:00 +01:00
`, clientroot.Session.BindJid, current, JidMustParse(clientroot.Session.BindJid).Bare(), loadedLocale["userRequested"]))
}
}
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" {
2026-04-28 12:58:00 +01:00
box.Append(gtk.NewLabel(loadedLocale["destroyMUCNotOwnerWarning"]))
}
// return false
}
}
} else {
panic("not ok")
}
} else {
panic("not ok")
}
return true
})
} else {
panic("not ok")
}
win.SetChild(box)
win.SetVisible(true)
}
}
})
2026-02-01 19:22:43 +00:00
joinAction := gio.NewSimpleAction("join", nil)
joinAction.ConnectActivate(func(p *glib.Variant) {
2026-04-28 12:58:00 +01:00
box := gtk.NewBox(gtk.OrientationVertical, 10)
jid_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
nick_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
disco_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
2026-02-01 19:22:43 +00:00
jid_entry := gtk.NewEntry()
nick_entry := gtk.NewEntry()
2026-04-26 10:40:13 +01:00
disco_check := gtk.NewCheckButton()
2026-02-01 19:22:43 +00:00
jid_entry.SetHAlign(gtk.AlignEnd)
jid_entry.SetHExpand(true)
nick_entry.SetHAlign(gtk.AlignEnd)
nick_entry.SetHExpand(true)
nick_entry.SetText(loadedConfig.Nick)
2026-04-28 12:58:00 +01:00
jid_box.Append(gtk.NewLabel(loadedLocale["joinMUCJIDEntry"]))
2026-02-01 19:22:43 +00:00
jid_box.Append(jid_entry)
2026-04-28 12:58:00 +01:00
nick_box.Append(gtk.NewLabel(loadedLocale["joinMUCNickEntry"]))
2026-02-01 19:22:43 +00:00
nick_box.Append(nick_entry)
2026-04-26 10:40:13 +01:00
disco_check.SetActive(true)
2026-04-28 12:58:00 +01:00
disco_box.Append(gtk.NewLabel(loadedLocale["joinMUCDiscoCheck"]))
2026-04-26 10:40:13 +01:00
disco_box.Append(disco_check)
2026-04-28 12:58:00 +01:00
disco_box.SetTooltipText(loadedLocale["joinMUCDiscoCheckTooltip"])
2026-04-26 10:40:13 +01:00
2026-02-01 19:22:43 +00:00
box.Append(jid_box)
box.Append(nick_box)
2026-04-26 10:40:13 +01:00
box.Append(disco_box)
2026-02-01 19:22:43 +00:00
2026-04-28 12:58:00 +01:00
btn := gtk.NewButtonWithLabel(loadedLocale["submit"])
2026-02-01 19:22:43 +00:00
btn.SetVAlign(gtk.AlignBaseline)
box.Append(btn)
win := gtk.NewWindow()
2026-04-28 12:58:00 +01:00
win.SetTitle(loadedLocale["joinMUCMenu"])
win.SetDefaultSize(400, 1)
win.SetResizable(false)
2026-02-01 19:22:43 +00:00
win.SetChild(box)
btn.ConnectClicked(func() {
t := jid_entry.Text()
_, ok := tabs.Load(t)
2026-04-26 10:40:13 +01:00
jm := func(n string, pw string) {
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text(), pw)
if err != nil {
2026-04-26 10:40:13 +01:00
showErrorDialog(err)
return
}
2026-04-06 11:04:53 +01:00
2026-04-26 10:40:13 +01:00
createTab(t, true, n)
b := gtk.NewLabel(t)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(t, &window.Window)
})
2026-04-06 11:04:53 +01:00
b.AddController(gesture1)
menu.Append(b)
2026-04-06 11:04:53 +01:00
}
2026-02-01 19:22:43 +00:00
if !ok {
2026-04-26 10:40:13 +01:00
if !disco_check.Active() {
jm(t, "")
win.SetVisible(false)
return
}
var res *stanza.DiscoInfo
2026-04-06 11:04:53 +01:00
allowed := true
fmt.Println("Attempting to get Disco info")
myIQ, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
From: clientroot.Session.BindJid,
To: t,
Id: "dicks",
Lang: "en",
})
2026-02-01 19:22:43 +00:00
if err != nil {
panic(err)
}
2026-04-06 11:04:53 +01:00
myIQ.Payload = &stanza.DiscoInfo{}
2026-04-06 11:04:53 +01:00
ctx := context.TODO()
mychan, err := client.SendIQ(ctx, myIQ)
if err == nil {
result := <-mychan
2026-04-26 10:40:13 +01:00
res, ok = result.Payload.(*stanza.DiscoInfo)
2026-04-06 11:04:53 +01:00
if ok {
features := res.Features
2026-04-26 10:40:13 +01:00
allowed = false
password_protected := false
password := ""
warning_win := gtk.NewWindow()
2026-04-28 12:58:00 +01:00
warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name))
warning_win.SetDefaultSize(400, 1)
2026-04-26 10:40:13 +01:00
warning_win.SetResizable(false)
2026-04-28 12:58:00 +01:00
buttons := gtk.NewBox(gtk.OrientationHorizontal, 10)
2026-04-26 10:40:13 +01:00
join_button := gtk.NewButtonWithLabel("Join")
join_button.ConnectClicked(func() {
warning_win.SetVisible(false)
if password_protected {
allowed = false
password_win := gtk.NewWindow()
2026-04-28 12:58:00 +01:00
password_win.SetTitle(loadedLocale["joinPasswordRequired"])
2026-04-26 10:40:13 +01:00
password_win.SetDefaultSize(400, 1)
password_win.SetResizable(false)
2026-04-28 12:58:00 +01:00
box := gtk.NewBox(gtk.OrientationVertical, 10)
en := gtk.NewPasswordEntry()
submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
2026-04-26 10:40:13 +01:00
submit.ConnectClicked(func() {
password = en.Text()
jm(res.Identity[0].Name, password)
password_win.SetVisible(false)
})
box.Append(en)
box.Append(submit)
password_win.SetChild(box)
password_win.SetVisible(true)
}
jm(res.Identity[0].Name, password)
})
2026-04-28 12:58:00 +01:00
cancel_button := gtk.NewButtonWithLabel(loadedLocale["cancel"])
2026-04-26 10:40:13 +01:00
cancel_button.ConnectClicked(func() {
warning_win.SetVisible(false)
})
2026-04-28 12:58:00 +01:00
join_button.SetHExpand(true)
cancel_button.SetHExpand(true)
2026-04-26 10:40:13 +01:00
buttons.Append(join_button)
buttons.Append(cancel_button)
2026-04-28 12:58:00 +01:00
warning_box := gtk.NewBox(gtk.OrientationVertical, 10)
2026-04-26 10:40:13 +01:00
header := gtk.NewLabel(res.Identity[0].Name)
warning_box.Append(header)
2026-04-28 12:58:00 +01:00
addFeature := func(icon string, description string) {
box := gtk.NewBox(gtk.OrientationHorizontal, 10)
box.Append(gtk.NewImageFromPaintable(clientAssets[icon]))
box.Append(gtk.NewLabel(description))
warning_box.Append(box)
}
2026-04-06 11:04:53 +01:00
for _, feature := range features {
2026-04-26 10:40:13 +01:00
switch feature.Var {
case "muc_passwordprotected":
password_protected = true
2026-04-28 12:58:00 +01:00
addFeature("muc_passwordprotected", loadedLocale["muc_passwordprotected_description"])
2026-04-26 10:40:13 +01:00
case "muc_unsecured":
2026-04-28 12:58:00 +01:00
addFeature("muc_unsecured", loadedLocale["muc_unsecured_description"])
2026-04-26 10:40:13 +01:00
case "muc_membersonly":
2026-04-28 12:58:00 +01:00
addFeature("muc_membersonly", loadedLocale["muc_membersonly_description"])
2026-04-26 10:40:13 +01:00
case "muc_open":
2026-04-28 12:58:00 +01:00
addFeature("muc_open", loadedLocale["muc_open_description"])
2026-04-26 10:40:13 +01:00
case "muc_moderated":
2026-04-28 13:08:16 +01:00
addFeature("muc_moderated", loadedLocale["muc_moderated_description"])
2026-04-26 10:40:13 +01:00
case "muc_unmoderated":
2026-04-28 12:58:00 +01:00
addFeature("muc_unmoderated", loadedLocale["muc_unmoderated_description"])
2026-04-26 10:40:13 +01:00
case "muc_nonanonymous":
2026-04-28 12:58:00 +01:00
addFeature("muc_nonanonymous", loadedLocale["muc_nonanonymous_description"])
2026-04-26 10:40:13 +01:00
case "muc_semianonymous":
2026-04-28 12:58:00 +01:00
addFeature("muc_semianonymous", loadedLocale["muc_semianonymous_description"])
2026-04-26 10:40:13 +01:00
case "muc_persistent":
2026-04-28 12:58:00 +01:00
addFeature("muc_persistent", loadedLocale["muc_persistent_description"])
2026-04-26 10:40:13 +01:00
case "muc_temporary":
2026-04-28 12:58:00 +01:00
addFeature("muc_temporary", loadedLocale["muc_temporary_description"])
2026-04-26 10:40:13 +01:00
case "muc_public":
2026-04-28 12:58:00 +01:00
addFeature("muc_public", loadedLocale["muc_public_description"])
2026-04-26 10:40:13 +01:00
case "muc_hidden":
2026-04-28 12:58:00 +01:00
addFeature("muc_hidden", loadedLocale["muc_hidden_description"])
case "urn:xmpp:mam:0":
addFeature("ok", loadedLocale["urn:xmpp:mam_description"])
case "urn:xmpp:message-moderate:0":
addFeature("moderate", loadedLocale["urn:xmpp:message-moderate_description"])
/*
2026-04-28 13:08:16 +01:00
default:
addFeature("comment", feature.Var)
2026-04-28 12:58:00 +01:00
*/
2026-04-06 11:04:53 +01:00
}
}
2026-04-26 10:40:13 +01:00
warning_box.Append(buttons)
warning_win.SetChild(warning_box)
warning_win.Present()
} else {
allowed = false
if result.Error != nil {
2026-04-28 12:58:00 +01:00
showErrorDialog(fmt.Errorf("%s: %s - %s", loadedLocale["discoFail"], result.Error.Reason, result.Error.Text))
2026-04-26 10:40:13 +01:00
} else {
2026-04-28 12:58:00 +01:00
showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]))
2026-04-06 11:04:53 +01:00
}
}
}
if allowed {
2026-04-26 10:40:13 +01:00
jm(res.Identity[0].Name, "")
2026-04-06 11:04:53 +01:00
}
2026-02-01 19:22:43 +00:00
}
win.SetVisible(false)
})
win.SetTransientFor(win)
win.Present()
})
app.AddAction(joinAction)
app.AddAction(aboutAction)
app.AddAction(destroymucAction)
2026-02-01 19:22:43 +00:00
the_menu.AppendSubmenu("File", fileMenu)
the_menu.AppendSubmenu("Help", helpMenu)
2026-02-01 19:22:43 +00:00
the_menuBar := gtk.NewPopoverMenuBarFromModel(the_menu)
2026-01-29 21:35:36 +00:00
app.SetMenubar(gio.NewMenu())
window.SetTitle("Lambda")
2026-01-30 19:08:18 +00:00
window.Window.AddCSSClass("ssd")
2026-02-01 09:17:57 +00:00
window.Window.SetDefaultSize(500, 500)
2026-01-31 10:02:04 +00:00
menu = gtk.NewBox(gtk.OrientationVertical, 0)
2026-01-29 21:35:36 +00:00
empty_dialog = gtk.NewImageFromPaintable(clientAssets["disabled_logo"])
empty_dialog.SetPixelSize(100)
2026-01-29 21:35:36 +00:00
empty_dialog.SetVExpand(true)
scroller = gtk.NewScrolledWindow()
2026-01-30 10:40:38 +00:00
memberList = gtk.NewScrolledWindow()
scroller.SetHExpand(true)
memberList.SetHExpand(true)
2026-01-29 21:35:36 +00:00
box := gtk.NewBox(gtk.OrientationVertical, 0)
2026-02-01 19:22:43 +00:00
box.Append(the_menuBar)
2026-02-17 09:27:46 +00:00
statBar := gtk.NewBox(gtk.OrientationHorizontal, 0)
cBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
connectionIcon = gtk.NewImageFromPaintable((clientAssets["disconnect"]))
connectionIcon.AddCSSClass("icon")
2026-04-28 12:58:00 +01:00
connectionStatus = gtk.NewLabel(loadedLocale["disconnected"])
cBox.Append(connectionIcon)
2026-02-17 09:27:46 +00:00
cBox.Append(connectionStatus)
2026-02-17 09:27:46 +00:00
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)
2026-02-17 09:27:46 +00:00
pBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
2026-04-28 12:58:00 +01:00
pBox.SetTooltipText(loadedLocale["pingBarTooltip"])
2026-02-20 06:28:31 +00:00
gesture := gtk.NewGestureClick()
gesture.SetButton(3)
gesture.Connect("pressed", func() {
opt := charts.NewLineChartOptionWithData(pingTimes)
opt.Title = charts.TitleOption{
2026-04-28 12:58:00 +01:00
Text: loadedLocale["pingGraphTitle"],
2026-02-20 06:28:31 +00:00
}
/*
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{
2026-04-28 12:58:00 +01:00
loadedLocale["pingGraphYAxis"],
2026-02-20 06:28:31 +00:00
},
}
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)
2026-04-28 12:58:00 +01:00
win.SetTitle(loadedLocale["pingGraphTitle"])
2026-02-20 06:28:31 +00:00
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(i)
win.SetChild(box)
win.SetVisible(true)
})
pBox.AddController(gesture)
2026-02-17 09:27:46 +00:00
i := (gtk.NewImageFromPaintable(clientAssets["chart_bar"]))
i.AddCSSClass("icon")
pBox.Append(i)
pingStatus = gtk.NewLabel("...")
2026-02-17 09:27:46 +00:00
pBox.Append(pingStatus)
statBar.Append(pBox)
sBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
sIcon = gtk.NewImageFromPaintable(clientAssets["car"])
sIcon.AddCSSClass("icon")
sStatus = gtk.NewLabel("-")
sBox.Append(sIcon)
sBox.Append(sStatus)
2026-04-28 12:58:00 +01:00
sStatus.SetTooltipText(loadedLocale["throughputTooltip"])
statBar.Append(sBox)
scrollerStatBar := gtk.NewScrolledWindow()
scrollerStatBar.SetChild(statBar)
box.Append(scrollerStatBar)
2026-02-17 09:27:46 +00:00
2026-01-29 21:35:36 +00:00
// scroller.SetChild(empty_dialog)
scroller.SetChild(empty_dialog)
menu_scroll := gtk.NewScrolledWindow()
2026-01-31 10:02:04 +00:00
menu_scroll.SetHExpand(true)
2026-01-29 21:35:36 +00:00
menu_scroll.SetChild(menu)
2026-01-31 10:02:04 +00:00
// box.Append(menu_scroll)
2026-01-30 10:40:38 +00:00
chatbox := gtk.NewBox(gtk.OrientationHorizontal, 0)
2026-01-31 10:02:04 +00:00
// chatbox.Append(menu_scroll)
chat_pane := gtk.NewPaned(gtk.OrientationHorizontal)
chat_pane.SetStartChild(scroller)
chat_pane.SetEndChild(memberList)
2026-02-01 09:17:57 +00:00
chat_pane.SetPosition(225)
2026-01-31 10:02:04 +00:00
main_pane := gtk.NewPaned(gtk.OrientationHorizontal)
main_pane.SetStartChild(menu_scroll)
main_pane.SetEndChild(chat_pane)
2026-02-01 09:17:57 +00:00
main_pane.SetPosition(135)
2026-01-31 10:02:04 +00:00
chatbox.Append(main_pane)
2026-01-30 10:40:38 +00:00
box.Append(chatbox)
2026-01-29 21:35:36 +00:00
entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
2026-03-10 16:35:56 +00:00
oob_en := gtk.NewEntry()
2026-04-28 12:58:00 +01:00
oob_en.SetPlaceholderText("URL")
2026-03-10 16:35:56 +00:00
message_en = gtk.NewEntry()
2026-04-28 12:58:00 +01:00
message_en.SetPlaceholderText(loadedLocale["messageEntryPlaceholder"])
b := gtk.NewButtonWithLabel(loadedLocale["send"])
2026-01-29 21:35:36 +00:00
sendtxt := func() {
2026-03-10 16:35:56 +00:00
t := message_en.Text()
2026-01-29 21:35:36 +00:00
if t == "" {
dialog := &gtk.AlertDialog{}
dialog.SetDetail("detail")
dialog.SetMessage("message")
dialog.SetButtons([]string{"yes, no"})
dialog.Choose(context.TODO(), &window.Window, nil)
}
message_type := stanza.MessageTypeChat
tab, ok := tabs.Load(current)
if !ok {
return
}
typed_tab := tab.(*chatTab)
if typed_tab.isMuc {
2026-01-31 10:02:04 +00:00
message_type = stanza.MessageTypeGroupchat
}
2026-03-10 16:35:56 +00:00
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)
2026-04-06 11:04:53 +01:00
new_mention.Mentions = "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)
}
2026-03-10 16:35:56 +00:00
err := sendMessage(client, current, message_type, t, "", "", exts)
2026-01-29 21:35:36 +00:00
if err != nil {
panic(err) // TODO: Show error message via GTK
}
2026-03-10 16:35:56 +00:00
message_en.SetText("")
2026-01-29 21:35:36 +00:00
scrollToBottomAfterUpdate(scroller)
}
2026-03-10 16:35:56 +00:00
message_en.Connect("activate", sendtxt)
2026-01-29 21:35:36 +00:00
b.ConnectClicked(sendtxt)
2026-03-10 16:35:56 +00:00
message_en.SetHExpand(true)
2026-01-29 21:35:36 +00:00
2026-03-10 16:35:56 +00:00
entry_box.Append(oob_en)
entry_box.Append(message_en)
2026-01-29 21:35:36 +00:00
entry_box.Append(b)
box.Append(entry_box)
2026-04-06 11:04:53 +01:00
typingStatus = gtk.NewLabel("")
box.Append(typingStatus)
2026-01-29 21:35:36 +00:00
window.SetChild(box)
window.SetVisible(true)
}
func loadCSS(content string) *gtk.CSSProvider {
prov := gtk.NewCSSProvider()
prov.LoadFromString(content)
return prov
}