Files
lambda/main.go
2026-01-30 15:56:58 +00:00

375 lines
7.8 KiB
Go

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 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
}
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.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 := &gtk.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)
entry_box.Append(en)
entry_box.Append(b)
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, loadedConfig.Nick)
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
}