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" "github.com/kr/pretty" ) var loadedConfig lambdaConfig var empty_dialog *gtk.Label // var msgs *gtk.ListBox var content *gtk.Widgetter var tabs map[string]*chatTab = make(map[string]*chatTab) var current string var scroller *gtk.ScrolledWindow var memberList *gtk.ScrolledWindow //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 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 } _, ok = tabs[originator] if ok { tabs[originator].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 } pretty.Println(presence) 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 typed_unit.Members == nil { typed_unit.Members = make(map[string]stanza.Presence) mucmembers.Store(muc, typed_unit) } */ if presence.Type != "unavailable" { typed_unit.Members.Store(ocu.ID, presence) } else { typed_unit.Members.Delete(ocu.ID) // delete(typed_unit.Members, ocu.ID) } mucmembers.Store(muc, 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) } err := sendMessage(client, current, stanza.MessageTypeGroupchat, t, "", "") if err != nil { panic(err) // TODO: Show error message via GTK } en.SetText("") scrollToBottomAfterUpdate(scroller) } en.Connect("activate", sendtxt) b.ConnectClicked(sendtxt) en.SetHExpand(true) 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[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 }