From e026e777f68e82cdd5e88b0b1d6bc97070e4ca6e Mon Sep 17 00:00:00 2001 From: sunglocto Date: Mon, 16 Feb 2026 09:52:25 +0000 Subject: [PATCH] BOOKMARKS! BOOKMARKS! WE GOT BOOKMARKS PEOPLE --- README.md | 2 +- assets/ban.png | Bin 0 -> 762 bytes assets/door_in.png | Bin 0 -> 693 bytes assets/door_out.png | Bin 0 -> 643 bytes assets/large_group.png | Bin 0 -> 747 bytes assets/world.png | Bin 0 -> 923 bytes gtk-helpers.go | 8 ++- gtk-message.go | 28 +++++++++-- main.go | 107 +++++++++++++++++++++++++++++++++++++---- xmpp-bookmarks.go | 22 +++++++++ xmpp-mucuser.go | 10 +++- 11 files changed, 160 insertions(+), 17 deletions(-) create mode 100644 assets/ban.png create mode 100644 assets/door_in.png create mode 100644 assets/door_out.png create mode 100644 assets/large_group.png create mode 100644 assets/world.png create mode 100644 xmpp-bookmarks.go diff --git a/README.md b/README.md index 3dd08c0..ad41ac2 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ an XMPP client icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im)) -additional icons are by Mark James's Silk Icon Set [https://peacocksoftware.com/sites/peacocksoftware/silk_icons/comment.png](https://peacocksoftware.com/sites/peacocksoftware/silk_icons/comment.png) +additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) diff --git a/assets/ban.png b/assets/ban.png new file mode 100644 index 0000000000000000000000000000000000000000..b0c700695fb3dec02de9ed5a06561cf712aace04 GIT binary patch literal 762 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!to4tlychE&W+oqRr9*iq#8 z{@K2tH}5y;^<3E`^4hVZOKfdW&eVIW#Q!rn3J9)$by2QrqSCx>1tq13FDas47uZ@v zx&(bPgtBIB>-6Eu7D=efQdWeMN*)*9bGUv1pKaY62eotA~U)4pywzA2&o zWZHr|Q4jw~Mfgjcxfvrq%P-N*O1G>;=R?3H1A*-?epR1uR$s^?7caKpe)vJQimp`I zUq2@o^vN@b%r{MA;SiTJP}&^jX)H2b`{HZ9=i=Gv_Py3?!>w5~^AC8e^5*K9&UEF0 zAANLRMB*MX~kY z52X(+zir{V(;maoe)xx_$l=+${T0_!`C*55EMe?*4r#nC?e`pWb_CeX`<(ap-&FnSrxmps zrFf6-30;4^JHenMYHeF*Cud1{xpHc!)2m4vjvC!4P!)K~dpK{qbKo~qwFPnO*?I0+ z@4w%F@}%d4^{cd2U0r4Q)9TVX(RnKveJ}rGyyCzeusSqQYpR0hB+YB3cUdMlOiLAZ zn4E6PD589uZ9=k)fg95;k8T!4#=pUHYG#RbvrhGDRlks=>#$X;^;GKb844U}C#TF+ zNb$U~g{9O&MnkMS!H0kAtrN_jPo^08*~vC~E$RNVzb4RRc3X*+?DLI3!=yLg%sKb? zP+HO7P&NzmA+y3ouJ>$lw Xr_b^2|Gt%hfq}u()z4*}Q$iB}(#BjX literal 0 HcmV?d00001 diff --git a/assets/door_in.png b/assets/door_in.png new file mode 100644 index 0000000000000000000000000000000000000000..41676a0a5be0f026fb136315fafb6c4566522d7a GIT binary patch literal 693 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7SkfJR9T^zbpD<_bdda}R zAX(xXQ4*Y=R#Ki=l*-_klAn~S;F+74o*I;zm{M7IGS!BGfyv#|#WBR<^wh~`yU!+y z9JfEexBT7QG#|x$)9J^&jZQ?|xTz_9)UB(xRbWz5zM*%0!%>mQ8xvSnliWSICOf$u zIorcw{BB0){m*lzAAdCKbgYKo+U`bRyx=Nes}dFnsUz8wXh zrtV(5QZMeI@J33TOMMwOq(EK73YVmhg%1nK42)c_-fB zLS=|i;{YB9p=Ccg3WVx1bBzCGRi_%w;d@$Zc<*f*>yEd67&`}g;) z4`!WMwL`^6_=!WA`_ctTS#NG`lP+}j4Gcfh(tb2qZ|}|%`>y_;7=KP};u=N_-ANMQ>xt?A8AoPY27FOv?U9cWVv{U zc$;QD&C7b4cWU98#m5($sPEgDeAUl|D@Ivu;neW#Sx&NV^-mUQ*w4Vgz~JfX=d#Wzp$P!D CPdCm0 literal 0 HcmV?d00001 diff --git a/assets/door_out.png b/assets/door_out.png new file mode 100644 index 0000000000000000000000000000000000000000..de93a963f579b70343cd9f279e99a6f765800151 GIT binary patch literal 643 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!toygXeTLn`LX4f4$v4ivHd zJTafyH>3Be$JJv~5{|gu)okMU*QR%*iMv$wsO!Prlj;<3W0` zP>yd#jECy18I#VO`Tc2MrnPU(*4o2Q`TEPx+djXS{FouV^5Z#X&7ZM~71!_iKRoq# zXMRvuOodK!)#9ctlFt|&R)z!xdA(^7PW#igclXtAV>)y!rQp;iAxdvvC?xMhE!O4j3Ad>|sExkc9d zIv2hxzorln&A`~)x_bJ)lzutKe?AkfIG*_VUu3Y%{QBX|(c%qn&+RLFr(nFTG4$xe z%!XEN@%b{dEeg%zH4go}t=Mb(>QK$8G9^a6uNz9znZCrR2-i=L%T!GHdhx5wjL-S^ z&aGQ}O>4gWzV^iE4M~DGHfpTNIK!S~qH|0p>A^3aTl;lQq!ur+-?IDIZ}0bApQrmi z{qlRs%;dF!?BOP<3u_e~2Bxo!G~_*Wp=f2!>_~$ny`7p8uT3>BANq1ioV7YtN>{_;b zo#c(fl@-;WHe4)L%lyoGvNBF{n(3MoJf5%qF#qnG)wPLfVIKnn1B0ilpUXO@geCx0 CWh@c^ literal 0 HcmV?d00001 diff --git a/assets/large_group.png b/assets/large_group.png new file mode 100644 index 0000000000000000000000000000000000000000..4839312eae3f4d1795d3f7ebb744e896d46f354b GIT binary patch literal 747 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7SkfJR9T^yIvfSBpi;01O zX_2RkV~EA+y_2IoE;EW8<4@|ie$hpDx#hH0#U*NNu54CX+B&5(kBKOIp4yP1(PixF z?0vbCSKR0HoHXBq&w7_xE>FF+XIXJxn$n^KCDF`DFaN(*l|LoNwTS=R<%i#!neQw9 zl7DD9p`Pddm&7}oVISqD>&DfyZa&j7TRe611o_0yQ`y@x&V7t5D*e-&vRruQDiO0& z4;3#hTOKSVI8#8d()#k#e_V`TWQ|f2bRdii7u|vn~u4UlS_(ngrJ!{#Qy8Q z4<`Q;KC^LI>~_DP==@p2*Q$yd{xO{~cUh#_%e?ahYuAspE6!SN_^@tuTI$UOZe0;E zuim|@oX~N`Oh)Ya_u$lIlePY*&nK1|i1$jyy}q?+^O@%pe+S&-Y|%N$Y+L$uM)v#W zx-7xuw#X~CxyCmNcgUsqZR>NK*ZGMj@vC|77deB8{c8d=jUy)VzUXdPc|4;oq{r^4 z@tNypNeMY(f14*~guX6NQP7#*S2+1;(m#!)zOscbAMYl!*@VzS}-Ie<`^H2YK_w}S* z*B6S+wPK!i(A&*o40 z-?!a*sF(2hPSe}Uyn{LOq76>vfq}u()z4*} HQ$iB}J?I(vHnMIVa`ZoYUXup7+&$xx1#aqQZRrvMEXF$JE^2 z8~7WQ9mEV8wG5Ujl|H*Xar5-DPq&f}-gvTt(}!Vs{AtzX8Cdf7Mu3Hl? z5_DpI>xBHeucyA>{?C0(&0Y7w_JiC09@c*zcjRME4bwE4-Wi)TcM5v3s1!tgUvIeA ztk%2b(T(l-e*&(`>&}$OxNBUqC40gq9*$C8#gbP_ZQebb@`FDJtqIYc=Vf<3!TtWh z1N{4$4YbtdSW^>_!$9s2lu<5jWf|I;+`%_jIP;b6X) zqqWq;*YSb1M$wFEPaef9Mk$^AHOV2|wX+~K>&_Tb+A-WBGv6hC=vk2Wzcn{37vk`=n7W7RH>qi_Cxa-NrY zVq@40l}%xtl6DK93$>|jOkvxt+9!E^qe-Wr$jetb$(6a15BlWT9&P3>U*9z^MeK6y z(x_d2YLAn-}r%`Nh#7^a{Ir-di^4e>*r_2>uEgje*A1_YxVQz=XtBXM%^!- z`&(nTUg7V|MY}q4qa$V=5DLB2Yk$yQmF?U6r)hPKOM9oTHhx?zKa*jh{J!7Sb?^N5 z%gNYO{*dTq6<)kb;Feh866c^vuU{@*zkhms`ugIR*Fxkfes>zr{q<+*6po{nf~SPv z^X|`AvJP0q=zBfG+d5}T`p-w7`h|9|?J+gB+y1fTL1EkR_PFUzbqc9h= 500 { + headerBox.Append(gtk.NewImageFromPaintable(clientAssets["world"])) + } else if i >= 50 { + headerBox.Append(gtk.NewImageFromPaintable(clientAssets["large_group"])) + } else { + headerBox.Append(gtk.NewImageFromPaintable(clientAssets["group"])) + } headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d participant(s)", i))) gen.Prepend(headerBox) diff --git a/gtk-message.go b/gtk-message.go index fc263a4..d52a5a2 100644 --- a/gtk-message.go +++ b/gtk-message.go @@ -19,6 +19,7 @@ import ( ) func generatePresenceWidget(p stanza.Packet) gtk.Widgetter { + b := gtk.NewBox(gtk.OrientationHorizontal, 0) presence, ok := p.(stanza.Presence) if !ok { return gtk.NewLabel("Unsupported message.") @@ -29,14 +30,21 @@ func generatePresenceWidget(p stanza.Packet) gtk.Widgetter { ok := presence.Get(&mu) if ok { if mu.MucUserItem.Affiliation == "outcast" { - return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " has been banned!") + b.Append(gtk.NewImageFromPaintable(clientAssets["outcast"])) + b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource + " has been banned by " + mu.MucUserItem.Actor.Nick + "!")) + return b } } - return gtk.NewLabel(JidMustParse(presence.From).Resource + " left the MUC") + b.Append(gtk.NewImageFromPaintable(clientAssets["door_out"])) + b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource)) } else { - return gtk.NewLabel(JidMustParse(presence.From).Resource + " joined the MUC") + b.Append(gtk.NewImageFromPaintable(clientAssets["door_in"])) + b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource)) } + + b.SetTooltipText(presence.Status) + return b } func generateMessageWidget(p stanza.Packet) gtk.Widgetter { @@ -65,7 +73,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { if m.Error.Type != "" { error_box := gtk.NewBox(gtk.OrientationHorizontal, 0) cancel_img := gtk.NewImageFromPaintable(clientAssets["cancel"]) - error_label := gtk.NewLabel(m.Error.Text+ ": ") + error_label := gtk.NewLabel(m.Error.Text + ": ") error_box.Append(cancel_img) error_box.Append(error_label) @@ -147,7 +155,11 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { // authorBox.Append(im) - al := gtk.NewLabel(jid.MustParse(m.From).Resourcepart()) + n := jid.MustParse(m.From).Resourcepart() + if n == "" { + n = jid.MustParse(m.From).String() + } + al := gtk.NewLabel(n) al.AddCSSClass("author") al.SetSelectable(true) @@ -195,6 +207,12 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { mlabel.SetSelectable(true) mlabel.SetHAlign(gtk.AlignFill) + mum := MucUser{} + ok = m.Get(&mum) + if ok { + mlabel.SetText(fmt.Sprintf("%s's affiliation has been changed to %s", mum.MucUserItem.JID, mum.MucUserItem.Affiliation)) + } + contentBox.Append(mlabel) mainBox.Append(authorBox) diff --git a/main.go b/main.go index 35a2d34..b844b3b 100644 --- a/main.go +++ b/main.go @@ -90,7 +90,6 @@ var cancelB64 string = base64.StdEncoding.EncodeToString(cancelBytes) var tagBytes []byte var tagB64 string = base64.StdEncoding.EncodeToString(tagBytes) - //go:embed assets/lambda-disabled.png var logoDisabledBytes []byte var logoDisabledB64 string = base64.StdEncoding.EncodeToString(logoDisabledBytes) @@ -99,10 +98,28 @@ var logoDisabledB64 string = base64.StdEncoding.EncodeToString(logoDisabledBytes var groupBytes []byte var groupB64 string = base64.StdEncoding.EncodeToString(groupBytes) +//go:embed assets/door_in.png +var doorInBytes []byte +var doorInB64 string = base64.StdEncoding.EncodeToString(doorInBytes) + +//go:embed assets/door_out.png +var doorOutBytes []byte +var doorOutB64 string = base64.StdEncoding.EncodeToString(doorOutBytes) + +//go:embed assets/large_group.png +var largeGroupBytes []byte +var largeGroupB64 string = base64.StdEncoding.EncodeToString(largeGroupBytes) + +//go:embed assets/world.png +var worldBytes []byte +var worldB64 string = base64.StdEncoding.EncodeToString(worldBytes) + var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler) var lockedJIDs map[string]bool = make(map[string]bool) func init() { + beeep.AppName = "Lambda" + go func() { for fn := range uiQueue { glib.IdleAdd(func() bool { @@ -176,7 +193,6 @@ func init() { clientAssets["outcast"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) - loader = gdkpixbuf.NewPixbufLoader() disabledLogoData, _ := base64.StdEncoding.DecodeString(logoDisabledB64) @@ -192,6 +208,38 @@ func init() { loader.Close() clientAssets["group"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + doorInData, _ := base64.StdEncoding.DecodeString(doorInB64) + loader.Write(doorInData) + loader.Close() + + clientAssets["door_in"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + doorOutData, _ := base64.StdEncoding.DecodeString(doorOutB64) + loader.Write(doorOutData) + loader.Close() + + clientAssets["door_out"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + largeGroupData, _ := base64.StdEncoding.DecodeString(largeGroupB64) + loader.Write(largeGroupData) + loader.Close() + + clientAssets["large_group"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + worldData, _ := base64.StdEncoding.DecodeString(worldB64) + loader.Write(worldData) + loader.Close() + + clientAssets["world"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) } func main() { @@ -368,8 +416,8 @@ func main() { _, ok := typed_unit.Members.Load(id) if !ok { glib.IdleAdd(func() { - b := gtk.NewLabel("") - ba, ok := generatePresenceWidget(p).(*gtk.Label) + b := gtk.NewBox(gtk.OrientationVertical, 0) + ba, ok := generatePresenceWidget(p).(*gtk.Box) if ok { b = ba } @@ -389,8 +437,8 @@ func main() { } else { typed_unit.Members.Delete(id) glib.IdleAdd(func() { - b := gtk.NewLabel("") - ba, ok := generatePresenceWidget(p).(*gtk.Label) + b := gtk.NewBox(gtk.OrientationVertical, 0) + ba, ok := generatePresenceWidget(p).(*gtk.Box) if ok { b = ba } @@ -462,8 +510,49 @@ func main() { cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) { fmt.Println("XMPP client connected") - /* - */ + books, err := stanza.NewItemsRequest("", "urn:xmpp:bookmarks:1", 0) + if err == nil { + mychan, err := c.SendIQ(context.TODO(), books) + result := <-mychan + if err == nil { + res, ok := result.Payload.(*stanza.PubSubGeneric) + if ok { + for _, item := range res.Items.List { + jid := item.Id + node := item.Any + autojoin := false + nick := loadedConfig.Nick + for _, attr := range node.Attrs { + if attr.Name.Local == "autojoin" { + autojoin = attr.Value == "true" + } + } + + for _, node := range node.Nodes { + if node.XMLName.Local == "nick" { + nick = node.Content + } + } + + _, ok := tabs.Load(jid) + if !ok && autojoin { + err := joinMuc(client, clientroot.Session.BindJid, jid, nick) + if err != nil { + panic(err) + } + + createTab(jid, true) + b := gtk.NewButtonWithLabel(jid) + b.ConnectClicked(func() { + b.AddCSSClass("accent") + switchToTab(jid, &window.Window) + }) + menu.Append(b) + } + } + } + } + } }) go func() { @@ -509,7 +598,7 @@ func activate(app *gtk.Application) { destroymucAction := gio.NewSimpleAction("destroymuc", nil) destroymucAction.ConnectActivate(func(p *glib.Variant) { - cur, ok := tabs.Load(current) + cur, ok := tabs.Load(current) if ok { cur := cur.(*chatTab) if cur.isMuc { diff --git a/xmpp-bookmarks.go b/xmpp-bookmarks.go new file mode 100644 index 0000000..1f86565 --- /dev/null +++ b/xmpp-bookmarks.go @@ -0,0 +1,22 @@ +package main + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" +) + +// Implementation of XEP-0402: PEP Native Bookmarks +// https://xmpp.org/extensions/xep-0402.html + +type Bookmark struct { + XMLName xml.Name `xml:"urn:xmpp:bookmarks:1 conference"` + Name string `xml:"name,attr,omitempty"` + Autojoin bool `xml:"autojoin,attr,omitempty"` + Nick string `xml:"nick,omitempty"` + Password string `xml:"password,omitempty"` + Extensions []any `xml:"extensions,omitempty"` +} + +func init() { + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "urn:xmpp:bookmarks:1", Local: "conference"}, Bookmark{}) +} diff --git a/xmpp-mucuser.go b/xmpp-mucuser.go index f2b7c89..5a5a704 100644 --- a/xmpp-mucuser.go +++ b/xmpp-mucuser.go @@ -10,6 +10,7 @@ import ( type MucUser struct { stanza.PresExtension + stanza.MsgExtension XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` MucUserItem MucUserItem `xml:"item,omitempty"` } @@ -19,9 +20,16 @@ type MucUserItem struct { Affiliation string `xml:"affiliation,attr,omitempty"` // TODO: Use enum Role string `xml:"role,attr,omitempty"` // TODO: Use enum JID string `xml:"jid,attr,omitempty"` - Reason string `xml:"reason,omitempty"` + Reason string `xml:"reason,omitempty"` + Actor Actor `xml:"actor,omitempty"` +} + +type Actor struct { + JID string `xml:"jid,attr"` + Nick string `xml:"nick,attr"` } func init() { stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc#user", Local: "x"}, MucUser{}) + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "http://jabber.org/protocol/muc#user", Local: "x"}, MucUser{}) }