diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..74fe979 --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# TODO +- XEP-0153: vCard-Based Avatars 0% +- XEP-0393: Message Styling 0% +- XEP-0402: PEP Native Bookmarks 0% +- XEP-0066: Out of Band Data partial +- XEP-0461: Message Replies partial +- XEP-0444: Message Reactions partial + diff --git a/cache.go b/cache.go index e400ba4..ad10da2 100644 --- a/cache.go +++ b/cache.go @@ -4,19 +4,29 @@ package main // It also does the same for images that need to be grabbed from HTTP. import ( - "github.com/diamondburned/gotk4/pkg/gdk/v4" - "github.com/diamondburned/gotk4/pkg/gtk/v4" "crypto/sha256" "fmt" - "net/http" + "github.com/diamondburned/gotk4/pkg/gdk/v4" + "github.com/diamondburned/gotk4/pkg/gtk/v4" "io" + "net/http" "os" + "path/filepath" + "github.com/kirsle/configdir" ) // global or app-level map/cache var textureCache = make(map[string]gdk.Paintabler) +func ensureCache() (string, error) { + cachePath := configdir.LocalCache("lambda-im") + err := configdir.MakePath(cachePath) // Ensure it exists. + if err != nil { + return "", err + } + return cachePath, nil +} func getTexture(path string) gdk.Paintabler { if tex, exists := textureCache[path]; exists { @@ -43,6 +53,7 @@ func newImageFromPath(path string) *gtk.Image { } func newPictureFromWeb(url string) *gtk.Picture { + pa, _ := ensureCache() // step 1: get a sha256 sum of the URL sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url))) @@ -62,16 +73,19 @@ func newPictureFromWeb(url string) *gtk.Picture { return nil } + fullpath := filepath.Join(pa, sum) + // step 3: save it - err = os.WriteFile(sum, b, 0644) + err = os.WriteFile(fullpath, b, 0644) if err != nil { return nil } - return newPictureFromPath(sum) + return newPictureFromPath(fullpath) } func newImageFromWeb(url string) *gtk.Image { + pa, _ := ensureCache() // step 1: get a sha256 sum of the URL sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url))) @@ -91,11 +105,14 @@ func newImageFromWeb(url string) *gtk.Image { return nil } + + fullpath := filepath.Join(pa, sum) + // step 3: save it - err = os.WriteFile(sum, b, 0644) + err = os.WriteFile(fullpath, b, 0644) if err != nil { return nil } - return newImageFromPath(sum) + return newImageFromPath(fullpath) } diff --git a/go.mod b/go.mod index fbe0091..dc27008 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/diamondburned/gotk4/pkg v0.3.1 github.com/google/uuid v1.1.1 github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030 + github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f + github.com/kr/pretty v0.1.0 github.com/sqweek/dialog v0.0.0-20260123140253-64c163d53aac gosrc.io/xmpp v0.5.1 mellium.im/xmpp v0.22.0 @@ -15,6 +17,7 @@ require ( require ( github.com/KarpelesLab/weak v0.1.1 // indirect github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect + github.com/kr/text v0.1.0 // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/go.sum b/go.sum index afc599c..e53cb4d 100644 --- a/go.sum +++ b/go.sum @@ -38,11 +38,15 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030 h1:NFCJG3BerP/5ZLXwu08x9xDs+9p7AYFMeo5IXjGANxw= github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030/go.mod h1:+YdGDBjXJho3QTsEntqzdm0YaiALOsz3sL6b67QLC8M= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= +github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= diff --git a/gtk-helpers.go b/gtk-helpers.go index 4bd4989..ff42189 100644 --- a/gtk-helpers.go +++ b/gtk-helpers.go @@ -4,6 +4,8 @@ import ( "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/sqweek/dialog" + "gosrc.io/xmpp/stanza" + Jid "mellium.im/xmpp/jid" ) func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) { @@ -31,6 +33,19 @@ func createTab(jid string, isMuc bool) { func switchToTab(jid string) { current = jid scroller.SetChild(tabs[current].msgs) + m, _ := mucmembers.Load(jid) + ma := m.(mucUnit) + mm := ma.Members + gen := gtk.NewBox(gtk.OrientationVertical, 0) + + mm.Range(func(k, v any) bool { + u := v.(stanza.Presence) + gen.Append(gtk.NewLabel(Jid.MustParse(u.From).Resourcepart())) + return true + }) + + memberList.SetChild(gen) + } func showErrorDialog(err error) { diff --git a/gtk-message.go b/gtk-message.go index d8a89cf..2f7db74 100644 --- a/gtk-message.go +++ b/gtk-message.go @@ -97,6 +97,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { // mainBox.Append(media) // media.AddCSSClass("chat_image") mbtn := gtk.NewButtonWithLabel("🖼️") + // mbtn.SetChild(newImageFromWeb(oob.URL)) mbtn.ConnectClicked(func(){ gopen.Open(oob.URL) }) diff --git a/main.go b/main.go index 5ad8118..d5a225f 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "os" + "sync" "context" "fmt" @@ -18,7 +19,7 @@ import ( _ "embed" "encoding/xml" - "errors" + "github.com/kr/pretty" ) var loadedConfig lambdaConfig @@ -32,6 +33,7 @@ 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 @@ -40,6 +42,9 @@ 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 { @@ -66,10 +71,10 @@ func main() { TransportConfiguration: xmpp.TransportConfiguration{ Address: loadedConfig.Server, }, - Jid: loadedConfig.Username, - Credential: xmpp.Password(loadedConfig.Password), - Insecure: loadedConfig.Insecure, - StreamLogger: os.Stdout, + Jid: loadedConfig.Username, + Credential: xmpp.Password(loadedConfig.Password), + Insecure: loadedConfig.Insecure, + // StreamLogger: os.Stdout, } router := xmpp.NewRouter() @@ -159,6 +164,52 @@ func main() { } }) }) + + router.HandleFunc("presence", func(s xmpp.Sender, p stanza.Packet) { + presence, ok := p.(stanza.Presence) + pretty.Println(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) @@ -236,6 +287,10 @@ func activate(app *gtk.Application) { 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) @@ -243,7 +298,11 @@ func activate(app *gtk.Application) { menu_scroll := gtk.NewScrolledWindow() menu_scroll.SetChild(menu) box.Append(menu_scroll) - box.Append(scroller) + + chatbox := gtk.NewBox(gtk.OrientationHorizontal, 0) + chatbox.Append(scroller) + chatbox.Append(memberList) + box.Append(chatbox) entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0) @@ -281,7 +340,6 @@ func activate(app *gtk.Application) { debug_btn := gtk.NewButtonWithLabel("Join muc") debug_btn.ConnectClicked(func() { - showErrorDialog(errors.New("test error")) t := en.Text() _, ok := tabs[t] if !ok { diff --git a/types.go b/types.go index 960bd21..22f9e79 100644 --- a/types.go +++ b/types.go @@ -1,7 +1,7 @@ package main import ( - + "sync" "github.com/diamondburned/gotk4/pkg/gtk/v4" ) @@ -17,3 +17,10 @@ type lambdaConfig struct { Insecure bool Nick string } + + +type mucUnit struct { + // key: OccupantID + // value: last user presence + Members sync.Map +} diff --git a/xmpp-vcard.go b/xmpp-vcard.go new file mode 100644 index 0000000..9cecbae --- /dev/null +++ b/xmpp-vcard.go @@ -0,0 +1,27 @@ +package main + +// Implementation of XEP-0054 +// https://xmpp.org/extensions/xep-0054.html + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" +) + +type VCard struct { + XMLName xml.Name `xml:"vcard-temp vCard"` + Photo Photo `xml:"PHOTO"` +} + +func (v *VCard) Namespace() string { + return v.XMLName.Space +} + +type Photo struct { + Type string `xml:"TYPE"` + Binval string `xml:"BINVAL"` +} + +func init() { + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "vcard-temp", Local: "vCard"}, VCard{}) +}