diff --git a/assets.go b/assets.go index 18e2ae7..cb14740 100644 --- a/assets.go +++ b/assets.go @@ -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()) } diff --git a/go.mod b/go.mod index 4ef2338..effcb3b 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/gtk-helpers.go b/gtk-helpers.go index bbabb3c..bf10f5c 100644 --- a/gtk-helpers.go +++ b/gtk-helpers.go @@ -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) } }() diff --git a/gtk-message.go b/gtk-message.go index 4019656..62f100c 100644 --- a/gtk-message.go +++ b/gtk-message.go @@ -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) diff --git a/main.go b/main.go index bcd0f26..1df73c2 100644 --- a/main.go +++ b/main.go @@ -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( + ` + + + `, 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 := >k.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) diff --git a/style.css b/style.css index 4b72d84..9356f90 100644 --- a/style.css +++ b/style.css @@ -52,3 +52,12 @@ .icon { padding: 2px; } + +.jid { + font-family: monospace; +} + +.error { + color: white; + background-color: red; +} diff --git a/xmpp-attention.go b/xmpp-attention.go new file mode 100644 index 0000000..0ddbac3 --- /dev/null +++ b/xmpp-attention.go @@ -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{}) +} diff --git a/xmpp-carbons.go b/xmpp-carbons.go new file mode 100644 index 0000000..f2695e5 --- /dev/null +++ b/xmpp-carbons.go @@ -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{}) +} diff --git a/xmpp-helpers.go b/xmpp-helpers.go index b68ea4d..1256939 100644 --- a/xmpp-helpers.go +++ b/xmpp-helpers.go @@ -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 { diff --git a/xmpp-mentions.go b/xmpp-mentions.go new file mode 100644 index 0000000..064cadd --- /dev/null +++ b/xmpp-mentions.go @@ -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{}) +}