Files
lambda/main.go
2026-02-01 09:17:57 +00:00

486 lines
10 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/kr/pretty"
"path/filepath"
"github.com/BurntSushi/toml"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"mellium.im/xmpp/jid"
"time"
_ "embed"
"encoding/xml"
"math/rand/v2"
"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)
var window *gtk.ApplicationWindow
// 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() {
p, err := ensureConfig()
if err != nil {
panic(err)
}
b, err := os.ReadFile(filepath.Join(p, "lambda.toml"))
if err != nil {
dropToSignInPage(err)
return
// panic(err)
}
_, err = toml.Decode(string(b), &loadedConfig)
if err != nil {
panic(err)
}
// Put 4 random characters in front of lambda
chars := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZλ"
str := ""
for range 4 {
str = str + string(chars[rand.IntN(len(chars))])
}
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server,
},
Jid: loadedConfig.Username + "/lambda."+str,
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"},
{Var: "λ"},
},
}
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
}
pretty.Println(presence)
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)
glib.IdleAdd(func() {
uiQueue <- func() {
b := gtk.NewLabel("")
ba, ok := generatePresenceWidget(p).(*gtk.Label)
if ok {
b = ba
}
tab, ok := tabs.Load(muc)
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
scrollToBottomAfterUpdate(scroller)
} else {
fmt.Println("Got message when the tab does not exist!")
}
}
})
}
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)
_, mok := mucmembers.Load(user)
if !ok && !mok { // FIXME: The initial muc presence gets picked up from this check
userdevices.Store(user, userUnit{})
createTab(user, false)
b := gtk.NewButtonWithLabel(user)
b.ConnectClicked(func() {
b.AddCSSClass("accent")
switchToTab(user, &window.Window)
})
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() {
time.Sleep(3 * time.Second)
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")
window.Window.SetDefaultSize(500, 500)
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.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.SetHExpand(true)
menu_scroll.SetChild(menu)
// box.Append(menu_scroll)
chatbox := gtk.NewBox(gtk.OrientationHorizontal, 0)
// chatbox.Append(menu_scroll)
chat_pane := gtk.NewPaned(gtk.OrientationHorizontal)
chat_pane.SetStartChild(scroller)
chat_pane.SetEndChild(memberList)
chat_pane.SetPosition(225)
main_pane := gtk.NewPaned(gtk.OrientationHorizontal)
main_pane.SetStartChild(menu_scroll)
main_pane.SetEndChild(chat_pane)
main_pane.SetPosition(135)
chatbox.Append(main_pane)
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)
}
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, &window.Window)
})
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
}