package main import ( "os" "sync" "context" "fmt" "github.com/diamondburned/gotk4/pkg/gdk/v4" "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/BurntSushi/toml" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" "mellium.im/xmpp/jid" "time" _ "embed" "encoding/xml" "runtime" ) var loadedConfig lambdaConfig var empty_dialog *gtk.Label // var msgs *gtk.ListBox var content *gtk.Widgetter // var tabs map[string]*chatTab = make(map[string]*chatTab) var tabs sync.Map var current string var scroller *gtk.ScrolledWindow var memberList *gtk.ScrolledWindow var menu *gtk.Box //go:embed style.css var styleCSS string var client xmpp.Sender var clientroot *xmpp.Client var uiQueue = make(chan func(), 100) // stores members of mucs var mucmembers sync.Map // stores devices of users var userdevices sync.Map func init() { go func() { for fn := range uiQueue { glib.IdleAdd(func() bool { fn() return false }) time.Sleep(10 * time.Millisecond) // Small delay between updates } }() } func main() { b, err := os.ReadFile("lambda.toml") if err != nil { showErrorDialog(err) panic(err) } _, err = toml.Decode(string(b), &loadedConfig) config := xmpp.Config{ TransportConfiguration: xmpp.TransportConfiguration{ Address: loadedConfig.Server, }, Jid: loadedConfig.Username, Credential: xmpp.Password(loadedConfig.Password), Insecure: loadedConfig.Insecure, StreamLogger: os.Stdout, } router := xmpp.NewRouter() router.NewRoute().IQNamespaces(stanza.NSDiscoInfo).HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { iq, ok := p.(*stanza.IQ) if !ok || iq.Type != stanza.IQTypeGet { return } iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) if err != nil { panic(err) } identity := stanza.Identity{ Name: "Lambda", Category: "client", // TODO: Allow spoofing on user request Type: "pc", } payload := stanza.DiscoInfo{ XMLName: xml.Name{ Space: stanza.NSDiscoInfo, Local: "query", }, Identity: []stanza.Identity{identity}, Features: []stanza.Feature{ {Var: stanza.NSDiscoInfo}, {Var: stanza.NSDiscoItems}, {Var: "jabber:iq:version"}, {Var: "urn:xmpp:delegation:1"}, {Var: "http://jabber.org/protocol/muc"}, {Var: "urn:xmpp:reply:0"}, }, } iqResp.Payload = &payload s.Send(iqResp) }) router.NewRoute().IQNamespaces("jabber:iq:version").HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { iq, ok := p.(*stanza.IQ) if !ok || iq.Type != stanza.IQTypeGet { return } iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) if err != nil { panic(err) } v := &stanza.Version{} v = v.SetInfo("Lambda", lambda_version, runtime.GOOS) // TODO: Allow spoofing on user request iqResp.Payload = v s.Send(iqResp) }) router.HandleFunc("message", func(s xmpp.Sender, p stanza.Packet) { m, ok := p.(stanza.Message) if !ok { return } if m.Body == "" { return } originator := jid.MustParse(m.From).Bare().String() glib.IdleAdd(func() { uiQueue <- func() { b := gtk.NewBox(gtk.OrientationVertical, 0) ba, ok := generateMessageWidget(p).(*gtk.Box) if ok { b = ba } 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!") } } }) }) router.HandleFunc("presence", func(s xmpp.Sender, p stanza.Packet) { presence, ok := p.(stanza.Presence) if !ok { return } if presence.Error != *new(stanza.Err) { return } 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) muc := jid.MustParse(presence.From).Bare().String() _, 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" { typed_unit.Members.Store(ocu.ID, presence) } else { typed_unit.Members.Delete(ocu.ID) } mucmembers.Store(muc, typed_unit) } else { // This is a presence stanza from a regular user // 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) if !ok { userdevices.Store(user, userUnit{}) createTab(user, false) b := gtk.NewButtonWithLabel(user) b.ConnectClicked(func() { b.AddCSSClass("accent") switchToTab(user) }) menu.Append(b) } unit, ok := userdevices.Load(user) if !ok { fmt.Println("Could not load user presence even after recreating it! Something weird is going on!") 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) } }) c, err := xmpp.NewClient(&config, router, func(err error) { showErrorDialog(err) panic(err) }) if err != nil { showErrorDialog(err) panic(err) } client = c clientroot = c cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) { fmt.Println("XMPP client connected") /* */ }) go func() { err = cm.Run() if err != nil { showErrorDialog(err) panic(err) } }() app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone) app.ConnectActivate(func() { activate(app) }) if code := app.Run(os.Args); code > 0 { os.Exit(code) } } func activate(app *gtk.Application) { // Load the CSS and apply it globally. gtk.StyleContextAddProviderForDisplay( gdk.DisplayGetDefault(), loadCSS(styleCSS), gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) window := gtk.NewApplicationWindow(app) app.SetMenubar(gio.NewMenu()) window.SetTitle("Lambda") window.Window.AddCSSClass("ssd") menu = gtk.NewBox(gtk.OrientationHorizontal, 0) /* f_menu := gtk.NewMenuButton() f_menu.SetLabel("File") f_menu.SetAlwaysShowArrow(false) e_menu := gtk.NewMenuButton() e_menu.SetLabel("Edit") e_menu.SetAlwaysShowArrow(false) v_menu := gtk.NewMenuButton() v_menu.SetLabel("View") v_menu.SetAlwaysShowArrow(false) b_menu := gtk.NewMenuButton() b_menu.SetLabel("Bookmarks") b_menu.SetAlwaysShowArrow(false) h_menu := gtk.NewMenuButton() h_menu.SetLabel("Help") h_menu.SetAlwaysShowArrow(false) menu.Append(f_menu) menu.Append(e_menu) menu.Append(v_menu) menu.Append(b_menu) menu.Append(h_menu) */ empty_dialog = gtk.NewLabel("You are not focused on any chats.") empty_dialog.SetVExpand(true) scroller = gtk.NewScrolledWindow() memberList = gtk.NewScrolledWindow() scroller.SetHExpand(true) memberList.SetHExpand(true) box := gtk.NewBox(gtk.OrientationVertical, 0) // scroller.SetChild(empty_dialog) scroller.SetChild(empty_dialog) menu_scroll := gtk.NewScrolledWindow() menu_scroll.SetChild(menu) box.Append(menu_scroll) chatbox := gtk.NewBox(gtk.OrientationHorizontal, 0) chatbox.Append(scroller) chatbox.Append(memberList) box.Append(chatbox) entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0) en := gtk.NewEntry() en.SetPlaceholderText("Say something, what else are you gonna do here?") b := gtk.NewButtonWithLabel("Send") sendtxt := func() { t := en.Text() if t == "" { dialog := >k.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 { message_type = stanza.MessageTypeGroupchat } err := sendMessage(client, current, message_type, t, "", "") if err != nil { panic(err) // TODO: Show error message via GTK } en.SetText("") scrollToBottomAfterUpdate(scroller) } en.Connect("activate", sendtxt) b.ConnectClicked(sendtxt) en.SetHExpand(true) m_entry := gtk.NewEntry() entry_box.Append(en) 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) }) menu.Append(b) } }) entry_box.Append(debug_btn) box.Append(entry_box) window.SetChild(box) window.SetVisible(true) } func loadCSS(content string) *gtk.CSSProvider { prov := gtk.NewCSSProvider() prov.LoadFromString(content) return prov }