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/color" "image/png" xmpp_color "mellium.im/xmpp/color" "strconv" ) func init() { } func showUserWindow(u stanza.Presence, custom_nick string) { var ocu OccupantID u.Get(&ocu) var id string id = JidMustParse(u.From).Resource win := gtk.NewWindow() win.SetDefaultSize(400, 400) win.SetResizable(false) profile_box := gtk.NewBox(gtk.OrientationVertical, 0) nick := gtk.NewLabel(JidMustParse(u.From).Resource) if custom_nick != "" { nick.SetText(custom_nick) } 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) if u.Status != "" { status_message := gtk.NewLabel(u.Status) status_message.AddCSSClass("status") profile_box.Append(status_message) } 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)) var affil string if mu.MucUserItem.Affiliation == "none" { affil = loadedLocale["unaffiliated"] } else { affil = loadedLocale["affiliatedAs"] + mu.MucUserItem.Affiliation } profile_box.Append(gtk.NewLabel(affil)) } if ocu.ID != "" { ocu_label := gtk.NewLabel(ocu.ID) ocu_label.AddCSSClass("jid") ocu_label.SetSelectable(true) profile_box.Append(ocu_label) } 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() } 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() glib.IdleAdd(func() { newTab.msgs.SetVExpand(true) newTab.msgs.SetShowSeparators(true) newTab.msgs.Append(gtk.NewButtonWithLabel(loadedLocale["getPastMessages"])) }) newTab.name = name tabs.Store(jid, newTab) return true } return false } func removeTab(jid string, w *gtk.Window) { t, ok := tabs.Load(jid) if ok { tab := t.(*chatTab) tab.msgs.RemoveAll() if tab.isMuc { client.SendRaw(fmt.Sprintf(` `, clientroot.Session.BindJid, jid+"/"+tab.current_nick)) } tabs.Delete(jid) mucmembers.Delete(jid) userdevices.Delete(jid) if current == jid { current = "" scroller.SetChild(empty_dialog) typingStatus.SetText("") memberList.SetChild(gtk.NewLabel("")) } mucmembers.Delete(jid) } } 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(clientAssetsLoad("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(clientAssetsLoad("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(gtk.NewSeparator(gtk.OrientationHorizontal)) gen.Prepend(userbox) } else { gen.Append(userbox) gen.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) } //id := ocu.ID //if id == "" { // id := JidMustParse(u.From).Resource //} nick_label := gtk.NewLabel(JidMustParse(u.From).Resource) var custom_nick string var vcu VCardUpdate ok = u.Get(&vcu) if ok { av_nick, ok := loadedConfig.CustomNicksAV[vcu.Photo] if ok { custom_nick = av_nick } } ocu_nick, ok := loadedConfig.CustomNicks[ocu.ID] if ok { custom_nick = ocu_nick } if custom_nick != "" { nick_label.SetText(custom_nick) } 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(clientAssetsLoad("status_" + string(u.Show))) status.SetTooltipText(string(u.Show)) status.SetHAlign(gtk.AlignEnd) // medal.SetHExpand(true) userbox.Prepend(status) if u.Status != "" { status_message := gtk.NewImageFromPaintable(clientAssetsLoad("comment")) status_message.SetTooltipText(u.Status) status_message.SetHAlign(gtk.AlignEnd) userbox.Append(status_message) } medal := gtk.NewImageFromPaintable(clientAssetsLoad(mu.MucUserItem.Affiliation)) medal.SetTooltipText(mu.MucUserItem.Affiliation) medal.SetHAlign(gtk.AlignEnd) medal.SetHExpand(true) userbox.Append(medal) if loadedConfig.ShowAvatarsInMemberList { 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.Destroy() }) 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) { showUserWindow(u, custom_nick) }) userbox.AddController(gesture) userbox.AddController(mod_gesture) return true })) headerBox := gtk.NewBox(gtk.OrientationHorizontal, 0) if i >= 1000 { headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("solar"))) } else if i >= 500 { headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("world"))) } else if i >= 50 { headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("large_group"))) } else { headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("group"))) } headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, loadedLocale["participants"]))) gen.Prepend(headerBox) muc_presence := typed_tab.muc_presence if muc_presence != nil { vc := &VCardUpdate{} ok = muc_presence.Get(vc) if ok { muci := getAvatar(jid, vc.Photo) muci.SetPixelSize(80) gen.Prepend(muci) } } muc_name := gtk.NewLabel(typed_tab.name) muc_name.AddCSSClass("author") muc_name.SetWrap(true) gen.Prepend(muc_name) memberList.SetChild(gen) } else { memberList.SetChild(gtk.NewLabel(jid)) } } func showErrorDialog(err error, w *gtk.Window) { 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.Destroy() }) box.Append(close_btn) err_win.SetChild(box) err_win.SetTransientFor(w) err_win.Present() } func createIdenticon(word string, always_create bool) *gtk.Image { // This function generates an identicon if !loadedConfig.Identicons && !always_create { return gtk.NewImageFromPaintable(clientAssetsLoad("DefaultAvatar")) } 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() p := loader.Pixbuf() gt := gdk.NewTextureForPixbuf(p) i := gtk.NewImageFromPaintable(gt) 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.Destroy() }) box.Append(submit) win.SetChild(box) win.SetVisible(true) }