Files
lambda/main.go
T
2026-05-03 11:10:15 +01:00

1259 lines
31 KiB
Go

package main
import (
"os"
"strings"
"sync"
"context"
"fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"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"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"time"
_ "embed"
"encoding/xml"
"github.com/kr/pretty"
"io"
"runtime"
)
var loadedConfig lambdaConfig
var empty_dialog *gtk.Image
var connectionStatus *gtk.Label
var connectionIcon *gtk.Image
var mStatus *gtk.Label
var mIcon *gtk.Image
/*
var sStatus *gtk.Label
var sIcon *gtk.Image
*/
var typingStatus *gtk.Label
var pingStatus *gtk.Label
var content *gtk.Widgetter
var tabs sync.Map
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
var client xmpp.Sender
var clientroot *xmpp.Client
var uiQueue = make(chan func(), 100)
var window *gtk.ApplicationWindow
// stores members of mucs
var mucmembers sync.Map
// stores devices of users
var userdevices sync.Map
var pingTimes = [][]float64{}
var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler)
var xmlLog *os.File
func init() {
beeep.AppName = loadedLocale["appName"]
go func() {
for fn := range uiQueue {
glib.IdleAdd(func() bool {
fn()
return false
})
time.Sleep(10 * time.Millisecond) // Small delay between updates
}
}()
}
func main() {
pingTimes = append(pingTimes, []float64{})
p, err := ensureConfig()
if err != nil {
panic(err)
}
b, err := os.ReadFile(filepath.Join(p, "lambda.toml"))
if err != nil {
dropToSignInPage(err)
return
}
_, err = toml.Decode(string(b), &loadedConfig)
if err != nil {
panic(err)
}
if loadedConfig.Resource == "" {
fmt.Println(loadedLocale["configResourceEmptyWarning"])
loadedConfig.Resource = randomClientResource()
}
if !loadedConfig.Debug {
xmlLog, err = os.CreateTemp("", "xmpp-log")
if err != nil {
panic(err)
}
defer os.Remove(xmlLog.Name())
} else {
xmlLog = os.Stdout
}
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server,
CharsetReader: func(c string, input io.Reader) (io.Reader, error) {
return charset.NewReaderLabel(c, input)
},
ConnectTimeout: 300,
},
Jid: loadedConfig.Username + "/" + loadedConfig.Resource,
Credential: xmpp.Password(loadedConfig.Password),
Insecure: loadedConfig.Insecure,
StreamManagementEnable: true,
ConnectTimeout: 300,
StreamLogger: xmlLog,
}
router := xmpp.NewRouter()
router.NewRoute().IQNamespaces(stanza.NSDiscoInfo).HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
iq, ok := p.(*stanza.IQ)
if !ok || iq.Type != stanza.IQTypeGet {
return
}
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
if err != nil {
panic(err)
}
identity := stanza.Identity{
Name: "Lambda",
Category: "client", // TODO: Allow spoofing on user request
Type: "pc",
}
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
{Var: "http://jabber.org/protocol/muc"},
{Var: "λ"},
{Var: "urn:xmpp:attention:0"},
},
}
iqResp.Payload = &payload
s.Send(iqResp)
})
router.NewRoute().IQNamespaces("jabber:iq:version").HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
iq, ok := p.(*stanza.IQ)
if !ok || iq.Type != stanza.IQTypeGet {
return
}
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
if err != nil {
panic(err)
}
v := &stanza.Version{}
v = v.SetInfo(loadedLocale["appName"], lambda_version, runtime.GOOS) // TODO: Allow spoofing on user request
iqResp.Payload = v
s.Send(iqResp)
})
router.HandleFunc("message", func(s xmpp.Sender, p stanza.Packet) {
m, ok := p.(stanza.Message)
if !ok {
return
}
e := stanza.PubSubEvent{}
ok = m.Get(&e)
if ok {
fmt.Println(e)
}
/*
if m.Body == "" {
return
}
*/
originator := JidMustParse(m.From).Bare()
glib.IdleAdd(func() {
mStatus.SetText(originator)
})
at := new(Attention)
ok = m.Get(at)
if ok {
beeep.Notify(loadedLocale["attention"], fmt.Sprintf("%s: %s", JidMustParse(m.From).Resource, m.Body), commentBytes) // TODO: Use localpart if DM
}
// Handle mentions
for _, ext := range m.Extensions {
mention, ok := ext.(*Mention)
if ok {
pretty.Println(mention)
}
}
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
orig := m.To
m = sc.Forwarded.Stanza.(stanza.Message)
m.To = orig
} 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))
}
}
}
composing := stanza.StateComposing{}
ok = m.Get(&composing)
if ok && current == JidMustParse(m.From).Bare() {
typingStatus.SetText(fmt.Sprintf("%s%s", m.From, loadedLocale["isTyping"]))
return
}
inactive := stanza.StateInactive{}
ok = m.Get(&inactive)
if ok && current == JidMustParse(m.From).Bare() {
typingStatus.SetText("")
return
}
glib.IdleAdd(func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
tab, ok := tabs.Load(originator)
if !ok {
return
}
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
if current == JidMustParse(m.From).Bare() {
scrollToBottomAfterUpdate(scroller)
}
} else {
fmt.Println("Got message when the tab does not exist!")
}
ba, ok := generateMessageWidget(p).(*gtk.Box)
if ok {
b.Append(ba)
}
})
})
router.HandleFunc("presence", func(s xmpp.Sender, p stanza.Packet) {
presence, ok := p.(stanza.Presence)
if !ok {
return
}
if presence.Error.Reason != "" {
beeep.Notify(fmt.Sprintf("%s : %s", presence.From, presence.Error.Reason), presence.Error.Text, cancelBytes)
return
}
var mu MucUser
var ocu OccupantID
ok = presence.Get(&mu)
if ok { // This is a presence stanza from a user in a MUC
presence.Get(&ocu)
// id := ocu.ID
// if id == "" {
id := JidMustParse(presence.From).Resource
// }
from, _ := stanza.NewJid(presence.From)
muc := from.Bare()
_, ok = mucmembers.Load(muc)
if !ok {
mucmembers.Store(muc, mucUnit{})
}
unit, ok := mucmembers.Load(muc)
if !ok {
return
}
typed_unit := unit.(mucUnit)
if presence.Type != "unavailable" {
_, ok := typed_unit.Members.Load(id)
if !ok {
glib.IdleAdd(func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generatePresenceWidget(p).(*gtk.Box)
if ok {
b = ba
}
tab, ok := tabs.Load(muc)
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
if current == muc {
scrollToBottomAfterUpdate(scroller)
}
} else {
fmt.Println("Got message when the tab does not exist!")
}
})
}
typed_unit.Members.Store(id, presence)
} else {
typed_unit.Members.Delete(id)
glib.IdleAdd(func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
ba, ok := generatePresenceWidget(p).(*gtk.Box)
if ok {
b = ba
}
tab, ok := tabs.Load(muc)
typed_tab := tab.(*chatTab)
if ok {
typed_tab.msgs.Append(b)
if current == muc {
scrollToBottomAfterUpdate(scroller)
}
} else {
fmt.Println("Got message when the tab does not exist!")
}
})
}
mucmembers.Store(muc, typed_unit)
} else { // This is a presence stanza from a regular user
// The code is basically the exact same as above, we just don't check for mucuser
// TODO: Presence handling code goes here
}
})
c, err := xmpp.NewClient(&config, router, func(err error) {
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["disconnected"], err.Error()))
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
})
if err != nil {
showErrorDialog(err)
panic(err)
}
client = c
clientroot = c
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
fmt.Println("XMPP client connected")
// Ping
go func() {
for {
time.Sleep(5 * time.Second)
go func() {
pingStatus.AddCSSClass("pending")
before := time.Now()
iq := new(stanza.IQ)
iq.From = clientroot.Session.BindJid
iq.To = iq.From
iq.Type = "get"
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
mychan, err := client.SendIQ(ctx, iq)
if err != nil {
return
}
_ = <-mychan
delay := time.Since(before) / time.Millisecond
pingTimes[0] = append(pingTimes[0], float64(delay))
glib.IdleAdd(func() {
pingStatus.RemoveCSSClass("pending")
pingStatus.SetText(fmt.Sprintf("%d %s", delay, loadedLocale["milliseconds"]))
})
}()
}
}()
// Throughput
/*
var oldsize int64
var newsize int64
var diff float64
go func() {
for {
time.Sleep(5 * time.Second)
stat, err := xmlLog.Stat()
if err != nil {
panic(err)
}
newsize = stat.Size()
diff = float64(newsize-oldsize) / 1000
ic := clientAssets["car"]
if diff >= 25 {
ic = clientAssets["car_high"]
}
glib.IdleAdd(func() {
sStatus.SetText(fmt.Sprintf("%.2f%s", diff, loadedLocale["KBPerSecond"]))
sIcon.SetFromPaintable(ic)
})
oldsize = newsize
}
}()
*/
glib.IdleAdd(func() {
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["connectedAs"], JidMustParse(clientroot.Session.BindJid).Bare()))
connectionStatus.SetTooltipText(fmt.Sprintf("%s%s\n%s%t", loadedLocale["bindedJid"], clientroot.Session.BindJid, loadedLocale["usingTLS"], 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))
// Fetch roster
i, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
})
if err != nil {
panic(err)
}
roster := i.RosterItems()
i.Payload = roster
mychan, err := c.SendIQ(context.TODO(), i)
result := <-mychan
if err == nil {
items, ok := result.Payload.(*stanza.RosterItems)
if ok {
for _, v := range items.Items {
name := v.Name
jid := v.Jid
if name == "" {
name = jid
}
createTab(jid, false, name)
glib.IdleAdd(func() {
box := gtk.NewBox(gtk.OrientationHorizontal, 10)
b := gtk.NewLabel(name)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
box.Append(b)
go func() {
new_im := getAvatar(jid, jid) // TODO: Use PEP avatar and do not use JID as hash
glib.IdleAdd(func() {
new_im.SetPixelSize(40)
box.Prepend(new_im)
})
}()
box.AddController(gesture1)
menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
})
}
}
}
// Join rooms in bookmarks
if loadedConfig.JoinBookmarks {
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
name := jid
password := ""
nick := loadedConfig.Nick
for _, attr := range node.Attrs {
if attr.Name.Local == "autojoin" {
autojoin = attr.Value == "true"
break
}
}
for _, attr := range node.Attrs {
if attr.Name.Local == "name" {
name = attr.Value
break
}
}
for _, attr := range node.Attrs {
if attr.Name.Local == "autojoin" {
autojoin = attr.Value == "true"
break
}
}
for _, node := range node.Nodes {
if node.XMLName.Local == "nick" {
nick = node.Content
break
}
}
for _, node := range node.Nodes {
if node.XMLName.Local == "password" {
password = node.Content
break
}
}
_, ok := tabs.Load(jid)
if !ok && autojoin {
joinMuc(client, clientroot.Session.BindJid, jid, nick, password)
createTab(jid, true, name)
glib.IdleAdd(func() {
box := gtk.NewBox(gtk.OrientationHorizontal, 10)
b := gtk.NewLabel(name)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
box.Append(b)
go func() {
new_im := getAvatar(jid, jid)
glib.IdleAdd(func() {
new_im.SetPixelSize(40)
box.Prepend(new_im)
})
}()
box.AddController(gesture1)
menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
})
}
}
}
}
}
}
})
conc := func() {
// time.Sleep(3 * time.Second)
connectionStatus.SetText(loadedLocale["connecting"])
connectionIcon.SetFromPaintable(clientAssets["hourglass"])
err = cm.Run()
if err != nil {
fmt.Println(err.Error())
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["disconnected"], err.Error()))
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
}
}
app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
app.ConnectActivate(func() {
activate(app)
go conc()
})
if code := app.Run(os.Args); code > 0 {
os.Exit(code)
}
}
func activate(app *gtk.Application) {
// Load the CSS and apply it globally.
gtk.StyleContextAddProviderForDisplay(
gdk.DisplayGetDefault(), loadCSS(styleCSS),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
window = gtk.NewApplicationWindow(app)
the_menu := gio.NewMenu()
fileMenu := gio.NewMenu()
fileMenu.Append(loadedLocale["joinMUCMenu"], "app.join")
fileMenu.Append(loadedLocale["startDMMenu"], "app.dm")
fileMenu.Append(loadedLocale["destroyMUCMenu"], "app.destroymuc")
helpMenu := gio.NewMenu()
helpMenu.Append("About", "app.about")
aboutAction := gio.NewSimpleAction("about", nil)
aboutAction.ConnectActivate(func(p *glib.Variant) {
a := gtk.NewAboutDialog()
about_window := gtk.NewWindow()
about_window.SetTransientFor(&window.Window)
about_window.SetTitle(fmt.Sprintf("%s %s", "About", loadedLocale["appName"]))
a.SetProgramName("Lambda")
a.SetVersion(lambda_version)
a.SetComments("yet another XMPP client")
a.SetAuthors([]string{"Sunglocto"})
a.SetLicense("GPL3")
a.SetWebsite("https://forge.sunglocto.net/sunglocto/lambda")
a.SetWebsiteLabel("Website")
/*
a.ConnectResponse(func() {
about_window.SetVisible(false)
})
*/
about_window.SetChild(a)
about_window.SetDefaultSize(400, 300)
about_window.SetVisible(true)
})
destroymucAction := gio.NewSimpleAction("destroymuc", nil)
destroymucAction.ConnectActivate(func(p *glib.Variant) {
cur, ok := tabs.Load(current)
if ok {
cur := cur.(*chatTab)
if cur.isMuc {
win := gtk.NewWindow()
win.SetTitle(loadedLocale["destroyMUCMenu"])
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 10)
box.Append(gtk.NewLabel(loadedLocale["destroyMUCWarningOne"]))
box.Append(gtk.NewLabel(loadedLocale["destroyMUCWarningTwo"]))
cancel := gtk.NewButtonWithLabel(loadedLocale["cancel"])
cancel.ConnectClicked(func() {
win.SetVisible(false)
})
en := gtk.NewEntry()
en.SetPlaceholderText("...")
submit := gtk.NewButtonWithLabel(loadedLocale["destroyMUCActionButton"])
submit.ConnectClicked(func() {
fmt.Println(en.Text())
if en.Text() == loadedLocale["destroyMUCPassword"] {
cur, ok := tabs.Load(current)
if ok {
cur := cur.(*chatTab)
if cur.isMuc {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='begone'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#owner'>
<destroy jid='%s'>
<reason>%s</reason>
</destroy>
</query>
</iq>
`, clientroot.Session.BindJid, current, JidMustParse(clientroot.Session.BindJid).Bare(), loadedLocale["userRequested"]))
}
}
win.SetVisible(false)
}
})
box.Append(en)
box.Append(submit)
box.Append(cancel)
mu, ok := mucmembers.Load(current)
if ok {
typed_mu := mu.(mucUnit)
typed_mu.Members.Range(func(k, v any) bool {
user, ok := v.(stanza.Presence)
if ok {
mu := MucUser{}
ok := user.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
if JidMustParse(mu.MucUserItem.JID).Bare() == JidMustParse(clientroot.Session.BindJid).Bare() {
if mu.MucUserItem.Affiliation != "owner" {
box.Append(gtk.NewLabel(loadedLocale["destroyMUCNotOwnerWarning"]))
}
// return false
}
}
} else {
panic("not ok")
}
} else {
panic("not ok")
}
return true
})
} else {
panic("not ok")
}
win.SetChild(box)
win.SetVisible(true)
}
}
})
joinAction := gio.NewSimpleAction("join", nil)
joinAction.ConnectActivate(func(p *glib.Variant) {
box := gtk.NewBox(gtk.OrientationVertical, 10)
jid_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
nick_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
disco_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
jid_entry := gtk.NewEntry()
nick_entry := gtk.NewEntry()
disco_check := gtk.NewCheckButton()
jid_entry.SetHAlign(gtk.AlignEnd)
jid_entry.SetHExpand(true)
nick_entry.SetHAlign(gtk.AlignEnd)
nick_entry.SetHExpand(true)
nick_entry.SetText(loadedConfig.Nick)
create_jid := gtk.NewImageFromPaintable(clientAssets["jabber"])
gesture := gtk.NewGestureClick()
gesture.SetButton(1)
gesture.Connect("pressed", func() {
jidBuilder(jid_entry)
})
create_jid.AddController(gesture)
jid_box.Append(gtk.NewLabel(loadedLocale["joinMUCJIDEntry"]))
jid_box.Append(create_jid)
jid_box.Append(jid_entry)
nick_box.Append(gtk.NewLabel(loadedLocale["joinMUCNickEntry"]))
nick_box.Append(nick_entry)
disco_check.SetActive(true)
disco_box.Append(gtk.NewLabel(loadedLocale["joinMUCDiscoCheck"]))
disco_box.Append(disco_check)
disco_box.SetTooltipText(loadedLocale["joinMUCDiscoCheckTooltip"])
box.Append(jid_box)
box.Append(nick_box)
box.Append(disco_box)
btn := gtk.NewButtonWithLabel(loadedLocale["submit"])
btn.SetVAlign(gtk.AlignBaseline)
box.Append(btn)
win := gtk.NewWindow()
win.SetTitle(loadedLocale["joinMUCMenu"])
win.SetDefaultSize(400, 1)
win.SetResizable(false)
win.SetChild(box)
btn.ConnectClicked(func() {
t := jid_entry.Text()
_, ok := tabs.Load(t)
jm := func(n string, pw string) {
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text(), pw)
if err != nil {
showErrorDialog(err)
return
}
createTab(t, true, n)
box := gtk.NewBox(gtk.OrientationHorizontal, 10)
go func() {
new_im := getAvatar(t, t)
glib.IdleAdd(func() {
new_im.SetPixelSize(40)
box.Prepend(new_im)
})
}()
b := gtk.NewLabel(n)
box.Append(b)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(t, &window.Window)
})
b.AddController(gesture1)
menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
}
if !ok {
if !disco_check.Active() {
jm(t, "")
win.SetVisible(false)
return
}
var res *stanza.DiscoInfo
allowed := true
fmt.Println("Attempting to get Disco info")
myIQ, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
From: clientroot.Session.BindJid,
To: t,
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 {
features := res.Features
allowed = false
password_protected := false
password := ""
warning_win := gtk.NewWindow()
warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name))
warning_win.SetDefaultSize(400, 1)
warning_win.SetResizable(false)
buttons := gtk.NewBox(gtk.OrientationHorizontal, 10)
join_button := gtk.NewButtonWithLabel("Join")
join_button.ConnectClicked(func() {
warning_win.SetVisible(false)
if password_protected {
allowed = false
password_win := gtk.NewWindow()
password_win.SetTitle(loadedLocale["joinPasswordRequired"])
password_win.SetDefaultSize(400, 1)
password_win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 10)
en := gtk.NewPasswordEntry()
submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() {
password = en.Text()
jm(res.Identity[0].Name, password)
password_win.SetVisible(false)
})
box.Append(en)
box.Append(submit)
password_win.SetChild(box)
password_win.SetVisible(true)
}
jm(res.Identity[0].Name, password)
})
cancel_button := gtk.NewButtonWithLabel(loadedLocale["cancel"])
cancel_button.ConnectClicked(func() {
warning_win.SetVisible(false)
})
join_button.SetHExpand(true)
cancel_button.SetHExpand(true)
buttons.Append(join_button)
buttons.Append(cancel_button)
warning_box := gtk.NewBox(gtk.OrientationVertical, 10)
header := gtk.NewLabel(res.Identity[0].Name)
warning_box.Append(header)
addFeature := func(icon string, description string) {
box := gtk.NewBox(gtk.OrientationHorizontal, 10)
box.Append(gtk.NewImageFromPaintable(clientAssets[icon]))
box.Append(gtk.NewLabel(description))
warning_box.Append(box)
}
for _, feature := range features {
switch feature.Var {
case "muc_passwordprotected":
password_protected = true
addFeature("muc_passwordprotected", loadedLocale["muc_passwordprotected_description"])
case "muc_unsecured":
addFeature("muc_unsecured", loadedLocale["muc_unsecured_description"])
case "muc_membersonly":
addFeature("muc_membersonly", loadedLocale["muc_membersonly_description"])
case "muc_open":
addFeature("muc_open", loadedLocale["muc_open_description"])
case "muc_moderated":
addFeature("muc_moderated", loadedLocale["muc_moderated_description"])
case "muc_unmoderated":
addFeature("muc_unmoderated", loadedLocale["muc_unmoderated_description"])
case "muc_nonanonymous":
addFeature("muc_nonanonymous", loadedLocale["muc_nonanonymous_description"])
case "muc_semianonymous":
addFeature("muc_semianonymous", loadedLocale["muc_semianonymous_description"])
case "muc_persistent":
addFeature("muc_persistent", loadedLocale["muc_persistent_description"])
case "muc_temporary":
addFeature("muc_temporary", loadedLocale["muc_temporary_description"])
case "muc_public":
addFeature("muc_public", loadedLocale["muc_public_description"])
case "muc_hidden":
addFeature("muc_hidden", loadedLocale["muc_hidden_description"])
case "urn:xmpp:mam:0":
addFeature("ok", loadedLocale["urn:xmpp:mam_description"])
case "urn:xmpp:message-moderate:0":
addFeature("moderate", loadedLocale["urn:xmpp:message-moderate_description"])
/*
default:
addFeature("comment", feature.Var)
*/
}
}
warning_box.Append(buttons)
warning_win.SetChild(warning_box)
warning_win.Present()
} else {
allowed = false
if result.Error != nil {
showErrorDialog(fmt.Errorf("%s: %s - %s", loadedLocale["discoFail"], result.Error.Reason, result.Error.Text))
} else {
showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]))
}
}
}
if allowed {
jm(res.Identity[0].Name, "")
}
}
win.SetVisible(false)
})
win.SetTransientFor(win)
win.Present()
})
app.AddAction(joinAction)
app.AddAction(aboutAction)
app.AddAction(destroymucAction)
the_menu.AppendSubmenu("MUC", fileMenu)
the_menu.AppendSubmenu("Help", helpMenu)
the_menuBar := gtk.NewPopoverMenuBarFromModel(the_menu)
app.SetMenubar(gio.NewMenu())
window.SetTitle("Lambda")
window.Window.AddCSSClass("ssd")
window.Window.SetDefaultSize(500, 500)
menu = gtk.NewBox(gtk.OrientationVertical, 0)
empty_dialog = gtk.NewImageFromPaintable(clientAssets["disabled_logo"])
empty_dialog.SetPixelSize(100)
empty_dialog.SetVExpand(true)
scroller = gtk.NewScrolledWindow()
memberList = gtk.NewScrolledWindow()
scroller.SetHExpand(true)
memberList.SetHExpand(true)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(the_menuBar)
statBar := gtk.NewBox(gtk.OrientationHorizontal, 0)
cBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
connectionIcon = gtk.NewImageFromPaintable((clientAssets["disconnect"]))
connectionIcon.AddCSSClass("icon")
connectionStatus = gtk.NewLabel(loadedLocale["disconnected"])
cBox.Append(connectionIcon)
cBox.Append(connectionStatus)
statBar.Append(cBox)
mBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
current = mStatus.Text()
switchToTab(current, &window.Window)
})
mIcon = gtk.NewImageFromPaintable((clientAssets["comment"]))
mIcon.AddCSSClass("icon")
mStatus = gtk.NewLabel("-")
mStatus.AddController(gesture1)
cBox.Append(mIcon)
cBox.Append(mStatus)
statBar.Append(mBox)
pBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
pBox.SetTooltipText(loadedLocale["pingBarTooltip"])
gesture := gtk.NewGestureClick()
gesture.SetButton(3)
gesture.Connect("pressed", func() {
opt := charts.NewLineChartOptionWithData(pingTimes)
opt.Title = charts.TitleOption{
Text: loadedLocale["pingGraphTitle"],
}
/*
opt.XAxis.Labels = []string{
// The 7 labels here match to the 7 values above
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
}*/
opt.Legend = charts.LegendOption{
SeriesNames: []string{
loadedLocale["pingGraphYAxis"],
},
}
opt.StrokeSmoothingTension = 0.9
p := charts.NewPainter(charts.PainterOptions{
Width: 600,
Height: 400,
})
err := p.LineChart(opt)
if err != nil {
panic(err)
}
buf, err := p.Bytes()
if err != nil {
panic(err)
}
loader := gdkpixbuf.NewPixbufLoader()
loader.Write(buf)
loader.Close()
i := gtk.NewPictureForPaintable(gdk.NewTextureForPixbuf(loader.Pixbuf()))
win := gtk.NewWindow()
win.SetDefaultSize(600, 400)
win.SetTitle(loadedLocale["pingGraphTitle"])
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(i)
win.SetChild(box)
win.SetVisible(true)
})
pBox.AddController(gesture)
i := (gtk.NewImageFromPaintable(clientAssets["chart_bar"]))
i.AddCSSClass("icon")
pBox.Append(i)
pingStatus = gtk.NewLabel("...")
pBox.Append(pingStatus)
statBar.Append(pBox)
/*
sBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
sIcon = gtk.NewImageFromPaintable(clientAssets["car"])
sIcon.AddCSSClass("icon")
sStatus = gtk.NewLabel("-")
sBox.Append(sIcon)
sBox.Append(sStatus)
sStatus.SetTooltipText(loadedLocale["throughputTooltip"])
statBar.Append(sBox)
*/
scrollerStatBar := gtk.NewScrolledWindow()
scrollerStatBar.SetChild(statBar)
box.Append(scrollerStatBar)
// scroller.SetChild(empty_dialog)
scroller.SetChild(empty_dialog)
menu_scroll := gtk.NewScrolledWindow()
menu_scroll.SetHExpand(true)
menu_scroll.SetChild(menu)
// box.Append(menu_scroll)
chatbox := gtk.NewBox(gtk.OrientationHorizontal, 0)
// chatbox.Append(menu_scroll)
chat_pane := gtk.NewPaned(gtk.OrientationHorizontal)
chat_pane.SetStartChild(scroller)
chat_pane.SetEndChild(memberList)
chat_pane.SetPosition(225)
main_pane := gtk.NewPaned(gtk.OrientationHorizontal)
main_pane.SetStartChild(menu_scroll)
main_pane.SetEndChild(chat_pane)
main_pane.SetPosition(135)
chatbox.Append(main_pane)
box.Append(chatbox)
entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
oob_en := gtk.NewEntry()
oob_en.SetPlaceholderText("URL")
message_en = gtk.NewEntry()
message_en.SetPlaceholderText(loadedLocale["messageEntryPlaceholder"])
b := gtk.NewButtonWithLabel(loadedLocale["send"])
sendtxt := func() {
t := message_en.Text()
if t == "" {
dialog := &gtk.AlertDialog{}
dialog.SetDetail("detail")
dialog.SetMessage("message")
dialog.SetButtons([]string{"yes, no"})
dialog.Choose(context.TODO(), &window.Window, nil)
}
message_type := stanza.MessageTypeChat
tab, ok := tabs.Load(current)
if !ok {
return
}
typed_tab := tab.(*chatTab)
if typed_tab.isMuc {
message_type = stanza.MessageTypeGroupchat
}
exts := []stanza.MsgExtension{}
if oob_en.Text() != "" {
new_oob := new(stanza.OOB)
new_oob.URL = oob_en.Text()
exts = append(exts, new_oob)
}
if strings.Contains(t, "@everyone") {
start := strings.Index(t, "@everyone")
end := start + len("@everyone")
new_mention := new(Mention)
new_mention.Mentions = "urn:xmpp:mentions:0#channel"
new_mention.Begin = start
new_mention.End = end
exts = append(exts, new_mention)
} else if strings.Contains(t, "@here") {
new_attention := new(Attention)
exts = append(exts, new_attention)
}
err := sendMessage(client, current, message_type, t, "", "", exts)
if err != nil {
showErrorDialog(err)
}
message_en.SetText("")
scrollToBottomAfterUpdate(scroller)
}
message_en.Connect("activate", sendtxt)
b.ConnectClicked(sendtxt)
message_en.SetHExpand(true)
entry_box.Append(oob_en)
entry_box.Append(message_en)
entry_box.Append(b)
box.Append(entry_box)
typingStatus = gtk.NewLabel("")
box.Append(typingStatus)
window.SetChild(box)
window.SetVisible(true)
}
func loadCSS(content string) *gtk.CSSProvider {
prov := gtk.NewCSSProvider()
prov.LoadFromString(content)
return prov
}