Attention and experimental mentions impl

This commit is contained in:
2026-03-10 16:35:56 +00:00
parent bf1685a382
commit 77e4e444d4
10 changed files with 215 additions and 24 deletions

View File

@@ -87,6 +87,11 @@ var connectB64 string = base64.StdEncoding.EncodeToString(connectBytes)
var commentBytes []byte
var commentB64 string = base64.StdEncoding.EncodeToString(commentBytes)
//go:embed assets/information.png
var informationBytes []byte
var informationB64 string = base64.StdEncoding.EncodeToString(informationBytes)
func init() {
loader := gdkpixbuf.NewPixbufLoader()
@@ -248,4 +253,13 @@ func init() {
loader.Close()
clientAssets["comment"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
informationData, _ := base64.StdEncoding.DecodeString(informationB64)
loader.Write(informationData)
loader.Close()
clientAssets["information"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
}

2
go.mod
View File

@@ -12,6 +12,7 @@ require (
github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/kr/pretty v0.2.0
golang.org/x/net v0.29.0
gosrc.io/xmpp v0.5.1
mellium.im/xmpp v0.22.0
)
@@ -33,7 +34,6 @@ require (
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect

View File

@@ -2,8 +2,6 @@ package main
import (
"context"
"crypto/sha1"
"encoding/hex"
"fmt"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -112,11 +110,17 @@ func switchToTab(jid string, w *gtk.Window) {
win.SetDefaultSize(400, 400)
profile_box := gtk.NewBox(gtk.OrientationVertical, 0)
nick := gtk.NewLabel(JidMustParse(u.From).Resource)
ver_text := gtk.NewLabel("Getting version...")
ver_text.AddCSSClass("visitor")
win.SetTitle(JidMustParse(u.From).Resource)
nick.AddCSSClass("author")
profile_box.Append(nick)
profile_box.Append(gtk.NewLabel(u.From))
profile_box.Append(ver_text)
fr := gtk.NewLabel(u.From)
fr.AddCSSClass("jid")
profile_box.Append(fr)
profile_box.Append(ver_text)
iqResp, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
@@ -147,7 +151,9 @@ func switchToTab(jid string, w *gtk.Window) {
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
profile_box.Append(gtk.NewLabel(mu.MucUserItem.JID))
ji := (gtk.NewLabel(mu.MucUserItem.JID))
ji.AddCSSClass("jid")
profile_box.Append(ji)
}
profile_box.Append(gtk.NewLabel("Connected with role " + mu.MucUserItem.Role))
profile_box.Append(gtk.NewLabel("Affiliated as " + mu.MucUserItem.Affiliation))
@@ -178,14 +184,24 @@ func switchToTab(jid string, w *gtk.Window) {
if ok {
idents := res.Identity
for i, ident := range idents {
profile_box.Append(gtk.NewLabel(fmt.Sprintf("Identity %d: Name: %s, Category: %s, Type: %s", i+1, ident.Name, ident.Category, ident.Type)))
profile_box.Append(gtk.NewLabel(fmt.Sprintf("%d: Name: %s, Category: %s, Type: %s", i+1, ident.Name, ident.Category, ident.Type)))
}
/*
s := fmt.Sprintf("%v", res.Features)
h := sha1.New()
h.Write([]byte(s))
sha1_hash := hex.EncodeToString(h.Sum(nil))
profile_box.Append(gtk.NewLabel(fmt.Sprintf("The hash of this user's Disco features is:\n%s\nUse the disco feature to view them", sha1_hash)))
*/
sw := gtk.NewScrolledWindow()
s := ""
for _, feature := range res.Features {
s = s + feature.Var + "\n"
}
sw.SetChild(gtk.NewLabel(s))
profile_box.Append(sw)
}
}
}()
@@ -202,8 +218,14 @@ func switchToTab(jid string, w *gtk.Window) {
version := ver.Version
os := ver.OS
profile_box.Append(gtk.NewLabel(fmt.Sprintf("%s %s %s", name, version, os)))
}
ver_text.SetText(fmt.Sprintf("%s %s %s", name, version, os))
ver_text.RemoveCSSClass("visitor")
} else if result.Error != nil && result.Error.Type != "" {
ver_text.SetText("Got error trying to get version")
ver_text.SetTooltipText(result.Error.Reason + ": "+result.Error.Text)
ver_text.RemoveCSSClass("visitor")
ver_text.AddCSSClass("error")
}
}
}()
@@ -221,18 +243,18 @@ func switchToTab(jid string, w *gtk.Window) {
im := getAvatar(u.From, vu.Photo)
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Append(im)
profile_box.Prepend(im)
} else {
im := newImageFromPath("debug.png")
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Append(im)
profile_box.Prepend(im)
}
} else {
im := newImageFromPath("debug.png")
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Append(im)
profile_box.Prepend(im)
}
}()

View File

@@ -115,6 +115,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
rc_box.Append(reactions)
/*
if m.Type == stanza.MessageTypeGroupchat {
moderate := gtk.NewButtonWithLabel("Moderate") // TODO: Implement proper support for moderations via extension
moderate.ConnectClicked(func() {
@@ -129,6 +130,13 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
})
rc_box.Append(moderate)
}
*/
quote := gtk.NewButtonWithLabel("Quote")
quote.ConnectClicked(func() {
message_en.SetText("> " + m.Body + "\n")
})
rc_box.Append(quote)
popover.SetChild(rc_box)
@@ -203,6 +211,10 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
}
mlabel := gtk.NewLabel(m.Body)
if m.Body == "" {
mlabel.SetText("No body set")
mlabel.AddCSSClass("visitor")
}
mlabel.SetWrap(true)
mlabel.SetSelectable(true)
mlabel.SetHAlign(gtk.AlignFill)

88
main.go
View File

@@ -13,6 +13,7 @@ import (
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/gen2brain/beeep"
"github.com/go-analyze/charts"
"golang.org/x/net/html/charset"
"path/filepath"
"github.com/BurntSushi/toml"
@@ -25,6 +26,7 @@ import (
"encoding/xml"
"github.com/kr/pretty"
"runtime"
"io"
)
var loadedConfig lambdaConfig
@@ -49,6 +51,7 @@ var current string
var scroller *gtk.ScrolledWindow
var memberList *gtk.ScrolledWindow
var menu *gtk.Box
var message_en *gtk.Entry
//go:embed style.css
var styleCSS string
@@ -111,10 +114,13 @@ func main() {
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server,
CharsetReader: func(c string, input io.Reader) (io.Reader, error) {
return charset.NewReaderLabel(c, input)
},
},
Jid: loadedConfig.Username + "/" + loadedConfig.Resource,
Credential: xmpp.Password(loadedConfig.Password),
Insecure: loadedConfig.Insecure,
Jid: loadedConfig.Username + "/" + loadedConfig.Resource,
Credential: xmpp.Password(loadedConfig.Password),
Insecure: loadedConfig.Insecure,
// StreamLogger: os.Stdout,
StreamManagementEnable: true,
}
@@ -149,6 +155,7 @@ func main() {
{Var: "urn:xmpp:delegation:1"},
{Var: "http://jabber.org/protocol/muc"},
{Var: "λ"},
{Var: "urn:xmpp:attention:0"},
},
}
iqResp.Payload = &payload
@@ -197,11 +204,49 @@ func main() {
originator := JidMustParse(m.From).Bare()
mStatus.SetText(originator)
at := new(Attention)
ok = m.Get(at)
if ok {
beeep.Notify("Attention", fmt.Sprintf("%s: %s", JidMustParse(m.From).Resource, m.Body), commentBytes) // TODO: Use localpart if DM
}
sc := new(SentCarbon)
ok = m.Get(sc)
if ok {
fm, ok := sc.Forwarded.Stanza.(stanza.Message)
if ok {
if JidMustParse(fm.From).Bare() == JidMustParse(m.From).Bare() {
p = sc.Forwarded.Stanza
m = sc.Forwarded.Stanza.(stanza.Message)
} else {
panic(fmt.Sprintln("Impersonation: ", fm.From, m.From))
}
}
}
rc := new(SentCarbon)
ok = m.Get(rc)
if ok {
fm, ok := rc.Forwarded.Stanza.(stanza.Message)
if ok {
if JidMustParse(fm.From).Bare() == JidMustParse(m.From).Bare() {
p = rc.Forwarded.Stanza
m = rc.Forwarded.Stanza.(stanza.Message)
} else {
panic(fmt.Sprintln("Impersonation: ", fm.From, m.From))
}
}
}
glib.IdleAdd(func() {
//uiQueue <- func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
tab, ok := tabs.Load(originator)
if !ok {
return
}
typed_tab := tab.(*chatTab)
if ok {
@@ -383,6 +428,16 @@ func main() {
connectionStatus.SetText(fmt.Sprintf("Connected as %s", JidMustParse(clientroot.Session.BindJid).Bare()))
connectionStatus.SetTooltipText(fmt.Sprintf("Binded JID: %s\nUsing TLS: %t", clientroot.Session.BindJid, clientroot.Session.TlsEnabled))
connectionIcon.SetFromPaintable(clientAssets["connect"])
// Enable carbons
client.SendRaw(fmt.Sprintf(
`<iq xmlns='jabber:client'
from='%s'
id='enable1'
type='set'>
<enable xmlns='urn:xmpp:carbons:2'/>
</iq>
`, clientroot.Session.BindJid))
// Join rooms in bookmarks
if loadedConfig.JoinBookmarks {
books, err := stanza.NewItemsRequest("", "urn:xmpp:bookmarks:1", 0)
@@ -778,12 +833,15 @@ func activate(app *gtk.Application) {
entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
en := gtk.NewEntry()
en.SetPlaceholderText("Say something, what else are you gonna do here?")
oob_en := gtk.NewEntry()
oob_en.SetPlaceholderText("Embed URL")
message_en = gtk.NewEntry()
message_en.SetPlaceholderText("Say something, what else are you gonna do here?")
b := gtk.NewButtonWithLabel("Send")
sendtxt := func() {
t := en.Text()
t := message_en.Text()
if t == "" {
dialog := &gtk.AlertDialog{}
dialog.SetDetail("detail")
@@ -803,21 +861,29 @@ func activate(app *gtk.Application) {
message_type = stanza.MessageTypeGroupchat
}
err := sendMessage(client, current, message_type, t, "", "")
exts := []stanza.MsgExtension{}
if oob_en.Text() != "" {
new_oob := new(stanza.OOB)
new_oob.URL = oob_en.Text()
exts = append(exts, new_oob)
}
err := sendMessage(client, current, message_type, t, "", "", exts)
if err != nil {
panic(err) // TODO: Show error message via GTK
}
en.SetText("")
message_en.SetText("")
scrollToBottomAfterUpdate(scroller)
}
en.Connect("activate", sendtxt)
message_en.Connect("activate", sendtxt)
b.ConnectClicked(sendtxt)
en.SetHExpand(true)
message_en.SetHExpand(true)
entry_box.Append(en)
entry_box.Append(oob_en)
entry_box.Append(message_en)
entry_box.Append(b)
box.Append(entry_box)

View File

@@ -52,3 +52,12 @@
.icon {
padding: 2px;
}
.jid {
font-family: monospace;
}
.error {
color: white;
background-color: red;
}

17
xmpp-attention.go Normal file
View File

@@ -0,0 +1,17 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Implementation of XEP-0224: Attention
type Attention struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:attention:0 attention"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:attention:0", Local: "attention"}, Attention{})
}

27
xmpp-carbons.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Implementation of XEP-0280: Message Carbons
// https://xmpp.org/extensions/xep-0280.html
type ReceivedCarbon struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:carbons:2 received"`
Forwarded stanza.Forwarded
}
type SentCarbon struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:carbons:2 sent"`
Forwarded stanza.Forwarded
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:carbons:2", Local: "received"}, ReceivedCarbon{})
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:carbons:2", Local: "sent"}, SentCarbon{})
}

View File

@@ -9,7 +9,7 @@ import (
// This file has small functions that can be used to do XMPP stuff without writing tons of boilerplate
// Basic message sender. Anything more complex should be written by hand
func sendMessage(c xmpp.Sender, sendTo string, msgType stanza.StanzaType, body string, subject string, thread string) error {
func sendMessage(c xmpp.Sender, sendTo string, msgType stanza.StanzaType, body string, subject string, thread string, exts []stanza.MsgExtension) error {
m := stanza.Message{
Attrs: stanza.Attrs{
To: sendTo,
@@ -18,6 +18,7 @@ func sendMessage(c xmpp.Sender, sendTo string, msgType stanza.StanzaType, body s
Body: body,
Subject: subject,
Thread: thread,
Extensions: exts,
}
err := c.Send(m)
if err != nil {

23
xmpp-mentions.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
// Experimental implementation of XEP-XXXX: Explicit Mentions
// https://git.isekai.rocks/snit/protoxeps/tree/explicit-mentions.xml
type Mention struct {
stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:mentions:0 mention"`
Mentions string `xml:"mentions,attr,omitempty"`
URI string `xml:"uri,attr,omitempty"`
Begin int `xml:"begin,attr,omitempty"`
End int `xml:"end,attr,omitempty"`
OccupantID string `xml:"occupantid,attr,omitempty"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:mentions:0", Local: "mention"}, Mention{})
}