package main import ( "bytes" "context" "fmt" "github.com/boxes-ltd/imaging" "github.com/crazy3lf/colorconv" "github.com/diamondburned/gotk4/pkg/gdk/v4" "github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/diamondburned/gotk4/pkg/pango" "github.com/rrivera/identicon" "gosrc.io/xmpp/stanza" "image" "image/png" xmpp_color "mellium.im/xmpp/color" "image/color" "strconv" ) func init() { } func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) { glib.IdleAdd(func() bool { vAdj := scrolledWindow.VAdjustment() vAdj.SetValue(vAdj.Upper() - vAdj.PageSize()) return false // Return false to run only once }) } func createTab(jid string, isMuc bool, name string) bool { if name == "" { name = jid } _, ok := tabs.Load(jid) _, uok := userdevices.Load(jid) _, mok := mucmembers.Load(jid) if !ok && !uok && !mok { newTab := new(chatTab) newTab.isMuc = isMuc newTab.msgs = gtk.NewListBox() newTab.msgs.SetVExpand(true) newTab.msgs.SetShowSeparators(true) newTab.name = name newTab.msgs.Append(gtk.NewButtonWithLabel(loadedLocale["getPastMessages"])) tabs.Store(jid, newTab) return true } return false } func switchToTab(jid string, w *gtk.Window) { current = jid tab, ok := tabs.Load(current) if !ok { return } typed_tab := tab.(*chatTab) scroller.SetChild(typed_tab.msgs) typingStatus.SetText("") if typed_tab.isMuc { m, ok := mucmembers.Load(jid) if !ok { box := gtk.NewBox(gtk.OrientationVertical, 10) failed_icon := gtk.NewImageFromPaintable(clientAssets["fail"]) failed_icon.SetPixelSize(100) box.Append(failed_icon) label := gtk.NewLabel("There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.") label.SetWrap(true) box.Append(label) memberList.SetChild(box) return } ma, ok := m.(mucUnit) if !ok { box := gtk.NewBox(gtk.OrientationVertical, 10) failed_icon := gtk.NewImageFromPaintable(clientAssets["fail"]) failed_icon.SetPixelSize(100) box.Append(failed_icon) label := gtk.NewLabel("There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.") label.SetWrap(true) box.Append(label) memberList.SetChild(box) return } mm := ma.Members gen := gtk.NewBox(gtk.OrientationVertical, 10) i := 0 rangeOrdered(&mm, (func(k, v any) bool { i++ u, ok := v.(stanza.Presence) if !ok { return true } userbox := gtk.NewBox(gtk.OrientationHorizontal, 2) var mu MucUser var ocu OccupantID u.Get(&mu) u.Get(&ocu) if mu.MucUserItem.Role == "moderator" { gen.Prepend(userbox) } else { gen.Append(userbox) } //id := ocu.ID //if id == "" { id := JidMustParse(u.From).Resource //} nick_label := gtk.NewLabel(JidMustParse(u.From).Resource) nick_label.SetEllipsize(pango.EllipsizeEnd) nick_label.AddCSSClass(mu.MucUserItem.Role) if mu.MucUserItem.Role == "visitor" { nick_label.SetOpacity(0.5) } userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\n%s", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation, loadedLocale["clickForMoreInfo"])) userbox.Append(nick_label) var hats Hats ok = u.Get(&hats) if ok { for _, hat := range hats.Hats { var val float64 if hat.Hue != "" { tval, _ := strconv.Atoi(hat.Hue) val = float64(tval) } else { xc := xmpp_color.String(hat.URI, 255, loadedConfig.CVD) r, g, b, _ := xc.RGBA() val, _, _ = colorconv.RGBToHSV(uint8(r), uint8(g), uint8(b)) } tB := tagBytes img, _, _ := image.Decode(bytes.NewReader(tB)) i_rgba := imaging.AdjustHue(img, val) var buf bytes.Buffer png.Encode(&buf, i_rgba) tB = buf.Bytes() loader := gdkpixbuf.NewPixbufLoader() loader.Write(tB) loader.Close() tag := gtk.NewPictureForPaintable(gdk.NewTextureForPixbuf(loader.Pixbuf())) tag.SetTooltipText(hat.Title) userbox.Prepend(tag) } } status := gtk.NewImageFromPaintable(clientAssets["status_"+string(u.Show)]) status.SetTooltipText(string(u.Show)) status.SetHAlign(gtk.AlignEnd) // medal.SetHExpand(true) userbox.Prepend(status) medal := gtk.NewImageFromPaintable(clientAssets[mu.MucUserItem.Affiliation]) medal.SetTooltipText(mu.MucUserItem.Affiliation) medal.SetHAlign(gtk.AlignEnd) medal.SetHExpand(true) userbox.Append(medal) default_av := createIdenticon(u.From, false) userbox.Prepend(default_av) var vcu VCardUpdate ok = u.Get(&vcu) if ok { photo := vcu.Photo go func() { new_im := getAvatar(u.From, photo) glib.IdleAdd(func() { userbox.Remove(default_av) userbox.Prepend(new_im) }) }() } gesture := gtk.NewGestureClick() gesture.SetButton(1) mod_gesture := gtk.NewGestureClick() mod_gesture.SetButton(3) popover := gtk.NewPopover() popover.SetHasArrow(false) popover.SetParent(userbox) rc_box := gtk.NewBox(gtk.OrientationVertical, 0) bb := gtk.NewButtonWithLabel(loadedLocale["ban"]) kb := gtk.NewButtonWithLabel(loadedLocale["kick"]) ab := gtk.NewButtonWithLabel(loadedLocale["setAffil"]) rb := gtk.NewButtonWithLabel(loadedLocale["setRole"]) kb.ConnectClicked(func() { client.SendRaw(fmt.Sprintf(` `, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource)) }) bb.ConnectClicked(func() { var mu MucUser ok = u.Get(&mu) if ok { if mu.MucUserItem.JID != "" { client.SendRaw(fmt.Sprintf(` `, clientroot.Session.BindJid, jid, JidMustParse(mu.MucUserItem.JID).Bare())) } } }) ab.ConnectClicked(func() { var mu MucUser ok = u.Get(&mu) if ok { if mu.MucUserItem.JID != "" { win := gtk.NewWindow() win.SetDefaultSize(400, 1) win.SetResizable(false) box := gtk.NewBox(gtk.OrientationVertical, 0) box.Append(gtk.NewLabel(loadedLocale["setAffilDescPartOne"] + JidMustParse(u.From).Resource + loadedLocale["setAffilDescPartTwo"])) the_entry := gtk.NewEntry() the_entry.SetText(mu.MucUserItem.Affiliation) submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) submit.ConnectClicked(func() { client.SendRaw(fmt.Sprintf(` `, clientroot.Session.BindJid, jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare())) win.SetVisible(false) }) box.Append(the_entry) box.Append(submit) win.SetChild(box) win.SetVisible(true) } } }) rb.ConnectClicked(func() { var mu MucUser ok = u.Get(&mu) if ok { if mu.MucUserItem.JID != "" { win := gtk.NewWindow() win.SetDefaultSize(400, 1) win.SetResizable(false) box := gtk.NewBox(gtk.OrientationVertical, 0) box.Append(gtk.NewLabel(loadedLocale["setRoleDescPartOne"] + JidMustParse(u.From).Resource + loadedLocale["setRoleDescPartTwo"])) box.Append(gtk.NewLabel(loadedLocale["setRoleWarning"])) the_entry := gtk.NewEntry() the_entry.SetText(mu.MucUserItem.Role) submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) submit.ConnectClicked(func() { client.SendRaw(fmt.Sprintf(` `, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource, the_entry.Text())) }) box.Append(the_entry) box.Append(submit) win.SetChild(box) win.SetVisible(true) } } }) rc_box.Append(bb) rc_box.Append(kb) rc_box.Append(ab) rc_box.Append(rb) popover.SetChild(rc_box) mod_gesture.Connect("pressed", func(n_press, x, y int) { rect := gdk.NewRectangle(x, y, 1, 1) popover.SetPointingTo(&rect) popover.Popup() }) gesture.Connect("pressed", func(n_press, x, y int) { win := gtk.NewWindow() win.SetDefaultSize(400, 400) profile_box := gtk.NewBox(gtk.OrientationVertical, 0) nick := gtk.NewLabel(JidMustParse(u.From).Resource) ver_text := gtk.NewLabel(loadedLocale["gettingVersion"]) ver_text.AddCSSClass("visitor") win.SetTitle(JidMustParse(u.From).Resource) nick.AddCSSClass("author") nick.SetSelectable(true) profile_box.Append(nick) profile_box.Append(ver_text) fr := gtk.NewLabel(u.From) fr.AddCSSClass("jid") fr.SetSelectable(true) profile_box.Append(fr) profile_box.Append(ver_text) iqResp, err := stanza.NewIQ(stanza.Attrs{ Type: "get", From: clientroot.Session.BindJid, To: u.From, Id: "vc2", Lang: "en", }) if err != nil { panic(err) } iqResp.Payload = &stanza.Version{} loading_version_text := gtk.NewLabel("...") var hats Hats ok := u.Get(&hats) if ok { for _, hat := range hats.Hats { l := gtk.NewLabel(hat.Title) l.AddCSSClass("hat") profile_box.Append(l) } } var mu MucUser ok = u.Get(&mu) if ok { if mu.MucUserItem.JID != "" { ji := (gtk.NewLabel(mu.MucUserItem.JID)) ji.AddCSSClass("jid") ji.SetSelectable(true) profile_box.Append(ji) } profile_box.Append(gtk.NewLabel(loadedLocale["connectedWithRole"] + mu.MucUserItem.Role)) profile_box.Append(gtk.NewLabel(loadedLocale["affiliatedAs"] + mu.MucUserItem.Affiliation)) } go func() { myIQ, err := stanza.NewIQ(stanza.Attrs{ Type: "get", From: clientroot.Session.BindJid, To: u.From, Id: "dicks", Lang: "en", }) if err != nil { panic(err) } myIQ.Payload = &stanza.DiscoInfo{} ctx := context.TODO() mychan, err := client.SendIQ(ctx, myIQ) if err == nil { result := <-mychan res, ok := result.Payload.(*stanza.DiscoInfo) if ok { idents := res.Identity for i, ident := range idents { 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)) */ sw := gtk.NewScrolledWindow() s := "" for _, feature := range res.Features { s = s + feature.Var + "\n" } sw.SetChild(gtk.NewLabel(s)) profile_box.Append(sw) } } }() go func() { ctx := context.TODO() mychan, err := client.SendIQ(ctx, iqResp) if err == nil { result := <-mychan ver, ok := result.Payload.(*stanza.Version) if ok { loading_version_text.SetVisible(false) name := ver.Name version := ver.Version os := ver.OS vr := fmt.Sprintf("%s %s %s", name, version, os) if name == "" && version == "" && os == "" { ver_text.SetText(loadedLocale["versionQueryEmpty"]) } else { ver_text.SetText(vr) ver_text.RemoveCSSClass("visitor") } } else if result.Error != nil && result.Error.Type != "" { ver_text.SetText(loadedLocale["versionQueryError"]) ver_text.SetTooltipText(result.Error.Reason + ": " + result.Error.Text) ver_text.RemoveCSSClass("visitor") ver_text.AddCSSClass("error") } } }() go func() { mo, _ := mucmembers.Load(JidMustParse(u.From).Bare()) mm := mo.(mucUnit) mmm := mm.Members mmmm, ok := mmm.Load(id) if ok { pres := mmmm.(stanza.Presence) var vu VCardUpdate pres.Get(&vu) if vu.Photo != "" { im := getAvatar(u.From, vu.Photo) im.SetPixelSize(80) im.AddCSSClass("author_img") profile_box.Prepend(im) } else { im := createIdenticon(u.From, false) im.SetPixelSize(80) im.AddCSSClass("author_img") profile_box.Prepend(im) } } else { im := createIdenticon(u.From, false) im.SetPixelSize(80) im.AddCSSClass("author_img") profile_box.Prepend(im) } }() win.SetChild(profile_box) win.SetTransientFor(win) win.Present() }) userbox.AddController(gesture) userbox.AddController(mod_gesture) return true })) headerBox := gtk.NewBox(gtk.OrientationHorizontal, 0) if i >= 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 %s", i, loadedLocale["participants"]))) gen.Prepend(headerBox) muci := getAvatar(jid, jid) muci.SetPixelSize(80) gen.Prepend(muci) muc_name := gtk.NewLabel(typed_tab.name) muc_name.AddCSSClass("author") gen.Prepend(muc_name) memberList.SetChild(gen) } else { memberList.SetChild(gtk.NewLabel(jid)) } } func showErrorDialog(err error) { err_win := gtk.NewWindow() err_win.SetTitle(loadedLocale["error"]) err_win.SetDefaultSize(400, 200) err_win.SetResizable(false) box := gtk.NewBox(gtk.OrientationVertical, 0) err_label := gtk.NewLabel(err.Error()) err_label.SetSelectable(true) box.Append(err_label) close_btn := gtk.NewButtonWithLabel(loadedLocale["close"]) close_btn.ConnectClicked(func() { err_win.SetVisible(false) }) box.Append(close_btn) err_win.SetChild(box) err_win.Present() } func createIdenticon(word string, always_create bool) *gtk.Image { // This function generates an identicon if !loadedConfig.Identicons && !always_create { i := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") return i } identicon.SetBackgroundColorFunction(func([]byte, color.Color) color.Color { return color.Transparent }) gen, _ := identicon.New("", 5, 3) ii, _ := gen.Draw(word) im := ii.Image(250) buf := new(bytes.Buffer) err := png.Encode(buf, im) if err != nil { panic(err) } loader := gdkpixbuf.NewPixbufLoader() loader.Write(buf.Bytes()) loader.Close() i := gtk.NewImageFromPaintable(gdk.NewTextureForPixbuf(loader.Pixbuf())) i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") return i } func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows the user to interactively build a JID // TODO: Localise this win := gtk.NewWindow() win.SetTitle("Build-A-JID") win.SetDefaultSize(400, 1) win.SetResizable(false) box := gtk.NewBox(gtk.OrientationVertical, 2) header := gtk.NewLabel("Build-A-JID") header.AddCSSClass("author") box.Append(header) box.Append(gtk.NewLabel("All fields except for domain are optional")) jid_builder := gtk.NewBox(gtk.OrientationHorizontal, 2) localPartEntry := gtk.NewEntry() localPartEntry.SetPlaceholderText("localpart") jid_builder.Append(localPartEntry) at_sign := gtk.NewLabel("@") at_sign.AddCSSClass("author") jid_builder.Append(at_sign) domainEntry := gtk.NewEntry() domainEntry.SetPlaceholderText("domain") jid_builder.Append(domainEntry) resource_sign := gtk.NewLabel("/") resource_sign.AddCSSClass("author") jid_builder.Append(resource_sign) resourceEntry := gtk.NewEntry() resourceEntry.SetPlaceholderText("resource") jid_builder.Append(resourceEntry) box.Append(jid_builder) submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) submit.ConnectClicked(func() { localPart := localPartEntry.Text() domain := domainEntry.Text() resource := resourceEntry.Text() at := "@" slash := "/" if localPart == "" { at = "" } if resource == "" { slash = "" } jid := localPart + at + domain + slash + resource en.SetText(jid) win.SetVisible(false) }) box.Append(submit) win.SetChild(box) win.SetVisible(true) }