2026-01-29 21:35:36 +00:00
package main
import (
2026-08-20 11:01:25 +01:00
"log"
"net/http"
_ "net/http/pprof"
2026-01-29 21:35:36 +00:00
"os"
2026-03-15 09:55:26 +00:00
"strings"
2026-01-30 10:40:38 +00:00
"sync"
2026-01-29 21:35:36 +00:00
"context"
2026-07-17 17:15:41 +01:00
"crypto/tls"
2026-01-29 21:35:36 +00:00
"fmt"
2026-07-14 07:51:46 +01:00
"path/filepath"
2026-01-29 21:35:36 +00:00
"github.com/diamondburned/gotk4/pkg/gdk/v4"
2026-02-22 08:16:14 +00:00
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
2026-01-29 21:35:36 +00:00
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
2026-10-11 12:08:38 +01:00
"github.com/diamondburned/gotk4/pkg/pango"
2026-02-15 13:08:52 +00:00
"github.com/gen2brain/beeep"
2026-02-20 06:28:31 +00:00
"github.com/go-analyze/charts"
2026-05-18 06:27:55 +01:00
"github.com/google/uuid"
2026-07-14 07:51:46 +01:00
"time"
2026-01-29 21:35:36 +00:00
"github.com/BurntSushi/toml"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
_ "embed"
"encoding/xml"
2026-03-10 16:48:10 +00:00
"runtime"
2026-07-14 07:51:46 +01:00
"github.com/kr/pretty"
2026-01-29 21:35:36 +00:00
)
var loadedConfig lambdaConfig
2026-02-15 13:08:52 +00:00
var empty_dialog * gtk . Image
2026-01-29 21:35:36 +00:00
2026-02-17 09:27:46 +00:00
var connectionStatus * gtk . Label
2026-02-19 14:30:04 +00:00
var connectionIcon * gtk . Image
2026-02-22 08:16:14 +00:00
var mStatus * gtk . Label
var mIcon * gtk . Image
2026-08-16 20:07:53 +01:00
var gStatus * gtk . Label
var gIcon * gtk . Image
2026-04-06 11:04:53 +01:00
var typingStatus * gtk . Label
2026-02-17 09:27:46 +00:00
var pingStatus * gtk . Label
2026-01-29 21:35:36 +00:00
var content * gtk . Widgetter
2026-01-30 21:31:33 +00:00
var tabs sync . Map
2026-01-29 21:35:36 +00:00
var current string
var scroller * gtk . ScrolledWindow
2026-01-30 10:40:38 +00:00
var memberList * gtk . ScrolledWindow
2026-01-30 21:31:33 +00:00
var menu * gtk . Box
2026-03-10 16:35:56 +00:00
var message_en * gtk . Entry
2026-01-29 21:35:36 +00:00
//go:embed style.css
var styleCSS string
var client xmpp . Sender
var clientroot * xmpp . Client
2026-01-31 15:38:02 +00:00
var window * gtk . ApplicationWindow
2026-01-30 10:40:38 +00:00
// stores members of mucs
var mucmembers sync . Map
2026-01-31 10:02:04 +00:00
// stores devices of users
2026-01-30 21:31:33 +00:00
var userdevices sync . Map
2026-02-20 06:28:31 +00:00
var pingTimes = [][] float64 {}
2026-02-17 09:27:46 +00:00
2026-04-26 10:40:13 +01:00
var xmlLog * os . File
2026-08-08 13:54:20 +01:00
// Basic roster
var Roster = make ( map [ string ] bool )
2026-10-11 12:08:38 +01:00
var config_filename = "lambda.toml"
var app_id = "net.sunglocto.lambda"
2026-01-29 21:35:36 +00:00
func init () {
2026-04-28 12:58:00 +01:00
beeep . AppName = loadedLocale [ "appName" ]
2026-01-29 21:35:36 +00:00
}
2026-01-31 21:35:26 +00:00
func main () {
2026-10-11 12:08:38 +01:00
if os . Getenv ( "PROFILE" ) != "" {
config_filename = fmt . Sprintf ( "lambda_%s.toml" , os . Getenv ( "PROFILE" ))
app_id = "net.sunglocto.lambda." + os . Getenv ( "PROFILE" )
}
2026-08-20 11:01:25 +01:00
2026-02-20 06:28:31 +00:00
pingTimes = append ( pingTimes , [] float64 {})
2026-02-01 09:05:20 +00:00
p , err := ensureConfig ()
if err != nil {
panic ( err )
}
2026-10-11 12:08:38 +01:00
b , err := os . ReadFile ( filepath . Join ( p , config_filename ))
2026-01-29 21:35:36 +00:00
if err != nil {
2026-01-31 21:35:26 +00:00
dropToSignInPage ( err )
return
2026-01-29 21:35:36 +00:00
}
_ , err = toml . Decode ( string ( b ), & loadedConfig )
2026-01-31 21:35:26 +00:00
if err != nil {
panic ( err )
}
2026-08-25 20:17:23 +01:00
if loadedConfig . Debug {
go func () {
log . Println ( http . ListenAndServe ( "localhost:6060" , nil ))
}()
log . Println ( "pprof debug server running at port 6060" )
}
2026-01-29 21:35:36 +00:00
2026-02-28 16:24:37 +00:00
if loadedConfig . Resource == "" {
2026-04-28 12:58:00 +01:00
fmt . Println ( loadedLocale [ "configResourceEmptyWarning" ])
2026-02-28 16:24:37 +00:00
loadedConfig . Resource = randomClientResource ()
2026-01-31 23:32:26 +00:00
}
2026-04-26 10:40:13 +01:00
if ! loadedConfig . Debug {
xmlLog , err = os . CreateTemp ( "" , "xmpp-log" )
if err != nil {
panic ( err )
}
defer os . Remove ( xmlLog . Name ())
} else {
xmlLog = os . Stdout
}
2026-08-08 13:54:20 +01:00
res := "/" + loadedConfig . Resource
if loadedConfig . RandomizeResource {
res = ""
}
2026-01-29 21:35:36 +00:00
config := xmpp . Config {
TransportConfiguration : xmpp . TransportConfiguration {
2026-08-16 20:07:53 +01:00
Address : loadedConfig . Server ,
2026-04-18 10:29:07 +01:00
ConnectTimeout : 300 ,
2026-07-17 17:15:41 +01:00
TLSConfig : & tls . Config { InsecureSkipVerify : loadedConfig . Insecure },
2026-01-29 21:35:36 +00:00
},
2026-08-08 13:54:20 +01:00
Jid : loadedConfig . Username + res ,
2026-04-18 10:29:07 +01:00
Credential : xmpp . Password ( loadedConfig . Password ),
Insecure : loadedConfig . Insecure ,
2026-02-19 11:28:18 +00:00
StreamManagementEnable : true ,
2026-04-18 10:29:07 +01:00
ConnectTimeout : 300 ,
StreamLogger : xmlLog ,
2026-01-29 21:35:36 +00:00
}
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 )
}
2026-08-08 13:54:20 +01:00
// All requests from either ourselves or our server will always be accepted regardless of user settings.
if !( loadedConfig . DiscoPrivacy == 0 ) && !(( JidMustParse ( iq . From ). Bare () == JidMustParse ( clientroot . Session . BindJid ). Bare ()) || ( JidMustParse ( iq . From ). Domain == JidMustParse ( clientroot . Session . BindJid ). Domain )) {
if loadedConfig . DiscoPrivacy == None || ( loadedConfig . DiscoPrivacy == OnlyRoster && ! Roster [ JidMustParse ( iq . From ). Bare ()]) {
if loadedConfig . IgnoreInsteadOfReturnErrorUponPrivacyViolation {
return
}
iqResp . Type = "error"
2026-08-16 20:07:53 +01:00
iqResp . From = clientroot . Session . BindJid
2026-10-11 12:08:38 +01:00
iqResp . Id = iq . Id
2026-08-08 13:54:20 +01:00
stanza_err := new ( stanza . Err )
stanza_err . Type = stanza . ErrorTypeCancel
stanza_err . Text = "service-unavailable"
iqResp . MakeError ( * stanza_err )
s . Send ( iqResp )
return
}
}
2026-01-29 21:35:36 +00:00
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" },
2026-01-31 15:08:54 +00:00
{ Var : "λ" },
2026-03-10 16:35:56 +00:00
{ Var : "urn:xmpp:attention:0" },
2026-05-10 14:46:19 +01:00
{ Var : "urn:xmpp:carbons:2" },
{ Var : "urn:xmpp:ping" },
2026-08-08 13:54:20 +01:00
{ Var : "jabber:iq:oob" },
{ Var : "jabber:x:oob" },
2026-10-11 12:08:38 +01:00
{ Var : "jabber:x:data" },
2026-01-29 21:35:36 +00:00
},
}
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 )
}
2026-08-08 13:54:20 +01:00
// All requests from either ourselves or our server will always be accepted regardless of user settings.
if !( loadedConfig . VersionPrivacy == 0 ) && !(( JidMustParse ( iq . From ). Bare () == JidMustParse ( clientroot . Session . BindJid ). Bare ()) || ( JidMustParse ( iq . From ). Domain == JidMustParse ( clientroot . Session . BindJid ). Domain )) {
if loadedConfig . VersionPrivacy == None || ( loadedConfig . VersionPrivacy == OnlyRoster && ! Roster [ JidMustParse ( iq . From ). Bare ()]) {
if loadedConfig . IgnoreInsteadOfReturnErrorUponPrivacyViolation {
return
}
iqResp . Type = "error"
2026-08-16 20:07:53 +01:00
iqResp . From = clientroot . Session . BindJid
2026-10-11 12:08:38 +01:00
iqResp . Id = iq . Id
2026-08-08 13:54:20 +01:00
stanza_err := new ( stanza . Err )
stanza_err . Type = stanza . ErrorTypeCancel
stanza_err . Text = "service-unavailable"
iqResp . MakeError ( * stanza_err )
s . Send ( iqResp )
return
}
}
2026-01-29 21:35:36 +00:00
v := & stanza . Version {}
2026-04-28 12:58:00 +01:00
v = v . SetInfo ( loadedLocale [ "appName" ], lambda_version , runtime . GOOS ) // TODO: Allow spoofing on user request
2026-01-29 21:35:36 +00:00
iqResp . Payload = v
s . Send ( iqResp )
})
router . HandleFunc ( "message" , func ( s xmpp . Sender , p stanza . Packet ) {
2026-07-30 01:39:31 +01:00
var blocked bool
2026-01-29 21:35:36 +00:00
m , ok := p .( stanza . Message )
if ! ok {
return
}
2026-08-25 20:17:23 +01:00
originator := JidMustParse ( m . From ). Bare ()
var typed_tab * chatTab
tab , ok := tabs . Load ( originator )
if ok {
typed_tab = tab .( * chatTab )
} else {
if m . Type != stanza . MessageTypeGroupchat {
createTab ( originator , false , originator )
tab , ok = tabs . Load ( originator )
if ok {
typed_tab = tab .( * chatTab )
glib . IdleAdd ( func () {
box := createTabBox ( originator , originator , originator , true )
menu . Append ( box )
menu . Append ( gtk . NewSeparator ( gtk . OrientationVertical ))
})
} else {
return
}
} else {
return
}
}
2026-07-30 01:39:31 +01:00
oi := new ( OccupantID )
ok = m . Get ( oi )
2026-02-04 10:12:49 +00:00
if ok {
2026-07-30 01:39:31 +01:00
id := oi . ID
va , ok := loadedConfig . BlockingOI [ id ]
if ok {
if va {
if loadedConfig . BlockingMode == Drop {
return
} else {
blocked = true
}
}
}
2026-02-04 10:12:49 +00:00
}
2026-08-20 11:01:25 +01:00
sid := new ( StanzaID )
m . Get ( sid )
2026-03-10 16:35:56 +00:00
at := new ( Attention )
ok = m . Get ( at )
if ok {
2026-04-28 12:58:00 +01:00
beeep . Notify ( loadedLocale [ "attention" ], fmt . Sprintf ( "%s: %s" , JidMustParse ( m . From ). Resource , m . Body ), commentBytes ) // TODO: Use localpart if DM
2026-03-10 16:35:56 +00:00
}
2026-03-15 09:55:26 +00:00
// Handle mentions
for _ , ext := range m . Extensions {
mention , ok := ext .( * Mention )
if ok {
pretty . Println ( mention )
}
}
2026-03-10 16:35:56 +00:00
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
2026-04-06 11:04:53 +01:00
orig := m . To
2026-03-10 16:35:56 +00:00
m = sc . Forwarded . Stanza .( stanza . Message )
2026-04-06 11:04:53 +01:00
m . To = orig
2026-03-10 16:35:56 +00:00
} 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 ))
}
}
}
2026-04-06 11:04:53 +01:00
composing := stanza . StateComposing {}
ok = m . Get ( & composing )
if ok && current == JidMustParse ( m . From ). Bare () {
2026-05-02 06:56:08 +01:00
typingStatus . SetText ( fmt . Sprintf ( "%s%s" , m . From , loadedLocale [ "isTyping" ]))
2026-04-06 11:04:53 +01:00
return
}
inactive := stanza . StateInactive {}
ok = m . Get ( & inactive )
if ok && current == JidMustParse ( m . From ). Bare () {
typingStatus . SetText ( "" )
return
}
2026-03-10 16:35:56 +00:00
2026-08-25 20:17:23 +01:00
// Handle read indicators
marker := new ( Marker )
ok = m . Get ( marker )
2026-10-11 12:08:38 +01:00
if ok && ! loadedConfig . CompactMode {
_ , ok := typed_tab . msg_refsOSID . Load ( marker . ID )
2026-08-25 20:17:23 +01:00
if ok {
im := gtk . NewImage ()
im . SetPixelSize ( 20 )
im . SetTooltipText ( JidMustParse ( m . From ). Resource )
glib . IdleAdd ( func () {
createIdenticon ( m . From , false , im )
})
pres , err := getPresenceFromJIDMuc ( JidMustParse ( m . From ))
if err != nil {
return
}
var vu VCardUpdate
ok = pres . Get ( & vu )
if ok {
go getAvatar ( m . From , vu . Photo , im )
}
2026-10-11 12:08:38 +01:00
uind , loaded := typed_tab . msg_refsOSID . Load ( marker . ID )
if loaded {
ind , ok := uind .( * abMessage )
if ok {
if ind . read_indicator != nil {
ind . read_indicator . Append ( im )
}
2026-08-25 20:17:23 +01:00
}
}
return
}
return
}
2026-08-16 20:07:53 +01:00
// Handle corrections BEFORE attempting to generate a message
correction := new ( Correction )
ok = m . Get ( correction )
if ok {
tab , ok := tabs . Load ( originator )
if ok {
typed_tab := tab .( * chatTab )
if ok {
msg , ok := typed_tab . msg_refsID [ correction . ID ]
if ok {
if msg . ocu_id == oi . ID && msg . ocu_id != "" {
lab := msg . label_ref
2026-08-25 20:17:23 +01:00
if lab != nil {
glib . IdleAdd ( func () {
lab . SetMarkup ( XEPToPango ( m . Body , true ))
})
}
2026-08-16 20:07:53 +01:00
}
return
}
}
}
}
2026-08-20 11:01:25 +01:00
// The same, however with retractions
retraction := new ( Retraction )
ok = m . Get ( retraction )
if ok {
tab , ok := tabs . Load ( originator )
if ok {
typed_tab := tab .( * chatTab )
2026-10-11 12:08:38 +01:00
umsg , ok := typed_tab . msg_refsOSID . Load ( retraction . ID )
2026-08-20 11:01:25 +01:00
if ok {
2026-10-11 12:08:38 +01:00
msg , ok := umsg .( * abMessage )
if ok {
lab := msg . label_ref
glib . IdleAdd ( func () {
GenerateRetraction ( lab , * retraction )
})
return
}
2026-08-25 20:17:23 +01:00
}
2026-08-20 11:01:25 +01:00
}
}
2026-10-11 12:08:38 +01:00
glib . IdleAddPriority ( glib . PriorityLow , func () {
2026-02-01 19:22:43 +00:00
b := gtk . NewBox ( gtk . OrientationVertical , 0 )
2026-01-29 21:35:36 +00:00
2026-02-01 19:22:43 +00:00
tab , ok := tabs . Load ( originator )
2026-03-10 16:35:56 +00:00
if ! ok {
return
}
2026-08-25 20:17:23 +01:00
typed_tab , ok := tab .( * chatTab )
if ! ok {
return
}
2026-01-29 21:35:36 +00:00
2026-02-01 19:22:43 +00:00
if ok {
2026-04-18 10:29:07 +01:00
if current == JidMustParse ( m . From ). Bare () {
scrollToBottomAfterUpdate ( scroller )
}
2026-02-01 19:22:43 +00:00
} else {
fmt . Println ( "Got message when the tab does not exist!" )
}
2026-02-14 22:38:32 +00:00
2026-08-16 20:07:53 +01:00
untyped_box , valid := generateMessageWidget ( p , blocked , originator )
2026-07-17 18:38:35 +01:00
ba , converted_successfully := untyped_box .( * gtk . Box )
if valid && converted_successfully {
2026-08-25 20:17:23 +01:00
mStatus . SetText ( originator )
2026-10-11 12:08:38 +01:00
if typed_tab . msgs != nil {
typed_tab . msgs . Append ( b )
b . Append ( ba )
b . Append ( gtk . NewSeparator ( gtk . OrientationVertical ))
}
2026-07-30 01:39:31 +01:00
}
2026-01-29 21:35:36 +00:00
})
})
2026-01-30 10:40:38 +00:00
router . HandleFunc ( "presence" , func ( s xmpp . Sender , p stanza . Packet ) {
presence , ok := p .( stanza . Presence )
if ! ok {
return
}
2026-02-15 13:08:52 +00:00
if presence . Error . Reason != "" {
2026-07-14 07:51:46 +01:00
// beeep.Notify(fmt.Sprintf("%s : %s", presence.From, presence.Error.Reason), presence.Error.Text, cancelBytes)
2026-01-30 21:31:33 +00:00
return
}
2026-01-30 10:40:38 +00:00
var mu MucUser
var ocu OccupantID
ok = presence . Get ( & mu )
if ok { // This is a presence stanza from a user in a MUC
2026-10-11 12:08:38 +01:00
from_self , created := false , false
for _ , status := range mu . StatusCodes {
if status . Code == "110" {
from_self = true
}
if status . Code == "201" {
created = true
}
}
if from_self && created {
showDialog ( "You have created a new MUC. In order for people to be able to join this MUC, you must first configure it. You can configure this MUC by clicking on the MUC button in the top right and then clicking 'Configre MUC'." , & window . Window )
}
2026-05-09 07:33:47 +01:00
2026-01-30 10:40:38 +00:00
presence . Get ( & ocu )
2026-03-12 16:09:22 +00:00
id := JidMustParse ( presence . From ). Resource
2026-02-01 13:49:30 +00:00
from , _ := stanza . NewJid ( presence . From )
muc := from . Bare ()
2026-01-30 10:40:38 +00:00
_ , ok = mucmembers . Load ( muc )
2026-05-09 07:33:47 +01:00
if ! ok && presence . Type != "unavailable" {
2026-01-30 10:40:38 +00:00
mucmembers . Store ( muc , mucUnit {})
}
unit , ok := mucmembers . Load ( muc )
if ! ok {
return
}
typed_unit := unit .( mucUnit )
2026-05-09 07:33:47 +01:00
if mu . MucUserItem . JID == clientroot . Session . BindJid {
tab , ok := tabs . Load ( muc )
if ok {
typed_tab := tab .( * chatTab )
typed_tab . current_nick = id
}
}
2026-01-30 10:40:38 +00:00
if presence . Type != "unavailable" {
2026-02-14 22:38:32 +00:00
_ , ok := typed_unit . Members . Load ( id )
2026-05-08 06:56:51 +01:00
if ! ok && loadedConfig . ShowPresenceUpdates {
2026-10-11 12:08:38 +01:00
glib . IdleAddPriority ( glib . PriorityLow , func () {
2026-02-16 09:52:25 +00:00
b := gtk . NewBox ( gtk . OrientationVertical , 0 )
ba , ok := generatePresenceWidget ( p ).( * gtk . Box )
2026-02-14 22:38:32 +00:00
if ok {
b = ba
}
tab , ok := tabs . Load ( muc )
2026-07-14 16:25:18 +01:00
typed_tab , ok2 := tab .( * chatTab )
2026-02-14 22:38:32 +00:00
2026-07-14 16:25:18 +01:00
if ok && ok2 {
2026-02-14 22:38:32 +00:00
typed_tab . msgs . Append ( b )
2026-04-18 10:29:07 +01:00
if current == muc {
scrollToBottomAfterUpdate ( scroller )
}
2026-02-14 22:38:32 +00:00
} else {
fmt . Println ( "Got message when the tab does not exist!" )
}
})
}
2026-02-04 17:57:24 +00:00
typed_unit . Members . Store ( id , presence )
2026-01-30 10:40:38 +00:00
} else {
2026-02-04 17:57:24 +00:00
typed_unit . Members . Delete ( id )
2026-05-08 06:56:51 +01:00
if loadedConfig . ShowPresenceUpdates {
2026-10-11 12:08:38 +01:00
glib . IdleAddPriority ( glib . PriorityLow , func () {
2026-05-08 06:56:51 +01:00
b := gtk . NewBox ( gtk . OrientationVertical , 0 )
ba , ok := generatePresenceWidget ( p ).( * gtk . Box )
if ok {
b = ba
2026-05-01 21:05:14 +01:00
}
2026-05-08 06:56:51 +01:00
tab , ok := tabs . Load ( muc )
typed_tab := tab .( * chatTab )
if ok {
2026-08-07 12:50:35 +01:00
b . Append ( gtk . NewSeparator ( gtk . OrientationVertical ))
2026-05-08 06:56:51 +01:00
typed_tab . msgs . Append ( b )
if current == muc {
scrollToBottomAfterUpdate ( scroller )
}
} else {
fmt . Println ( "Got message when the tab does not exist!" )
}
})
}
2026-01-30 10:40:38 +00:00
}
mucmembers . Store ( muc , typed_unit )
2026-01-30 21:31:33 +00:00
} else { // This is a presence stanza from a regular user
2026-01-31 10:02:04 +00:00
// The code is basically the exact same as above, we just don't check for mucuser
2026-01-30 21:31:33 +00:00
2026-05-03 11:10:15 +01:00
// TODO: Presence handling code goes here
2026-05-19 14:07:21 +01:00
j := presence . From
2026-05-18 06:27:55 +01:00
tab , ok := tabs . Load ( j )
if ok {
typed_tab , ok := tab .( * chatTab )
if ok {
if typed_tab . isMuc {
typed_tab . muc_presence = & presence
}
}
}
2026-01-30 10:40:38 +00:00
}
})
2026-01-29 21:35:36 +00:00
c , err := xmpp . NewClient ( & config , router , func ( err error ) {
2026-04-28 12:58:00 +01:00
connectionStatus . SetText ( fmt . Sprintf ( "%s%s" , loadedLocale [ "disconnected" ], err . Error ()))
2026-05-23 06:45:18 +01:00
connectionIcon . SetFromPaintable ( clientAssetsLoad ( "disconnect" ))
2026-01-29 21:35:36 +00:00
})
2026-02-19 11:28:18 +00:00
2026-01-29 21:35:36 +00:00
if err != nil {
2026-05-09 07:33:47 +01:00
showErrorDialog ( err , & window . Window )
2026-01-29 21:35:36 +00:00
panic ( err )
}
client = c
clientroot = c
cm := xmpp . NewStreamManager ( c , func ( c xmpp . Sender ) {
fmt . Println ( "XMPP client connected" )
2026-10-11 12:08:38 +01:00
client = c
2026-08-07 12:50:35 +01:00
// clientroot = c
2026-02-17 09:27:46 +00:00
go func () {
for {
time . Sleep ( 5 * time . Second )
2026-04-18 10:29:07 +01:00
go func () {
2026-08-16 20:07:53 +01:00
// Goroutine counter
goroutines := runtime . NumGoroutine ()
glib . IdleAdd ( func () {
gStatus . SetText ( fmt . Sprintf ( "%d goroutines" , goroutines ))
})
}()
2026-08-20 11:01:25 +01:00
2026-08-16 20:07:53 +01:00
go func () {
// Ping
2026-04-18 10:29:07 +01:00
pingStatus . AddCSSClass ( "pending" )
before := time . Now ()
iq := new ( stanza . IQ )
2026-08-25 20:17:23 +01:00
iq . Id = uuid . NewString ()
2026-04-18 10:29:07 +01:00
iq . From = clientroot . Session . BindJid
iq . Type = "get"
2026-05-10 14:46:19 +01:00
iq . Payload = & Ping {}
2026-02-17 09:27:46 +00:00
2026-04-18 10:29:07 +01:00
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 ))
2026-04-30 14:55:17 +01:00
glib . IdleAdd ( func () {
pingStatus . RemoveCSSClass ( "pending" )
pingStatus . SetText ( fmt . Sprintf ( "%d %s" , delay , loadedLocale [ "milliseconds" ]))
})
2026-04-18 10:29:07 +01:00
}()
}
}()
2026-04-30 14:55:17 +01:00
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 ))
2026-05-23 06:45:18 +01:00
connectionIcon . SetFromPaintable ( clientAssetsLoad ( "connect" ))
2026-07-14 07:51:46 +01:00
if loadedConfig . CheckUpdate == "" {
showInitialConsent ()
}
if loadedConfig . CheckUpdate == "yes" {
err := updateCheck ()
if err != nil {
2026-08-03 19:39:57 +01:00
showErrorDialog ( err , & window . Window )
2026-07-14 07:51:46 +01:00
}
}
2026-04-30 14:55:17 +01:00
})
2026-03-10 16:35:56 +00:00
// 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 ))
2026-07-30 01:39:31 +01:00
// Create note to self
jid := JidMustParse ( clientroot . Session . BindJid ). Bare ()
name := "Note To Self"
createTab ( jid , false , name )
glib . IdleAdd ( func () {
2026-08-25 20:17:23 +01:00
b := createTabBox ( name , jid , jid , false )
menu . Append ( b )
2026-07-30 01:39:31 +01:00
menu . Append ( gtk . NewSeparator ( gtk . OrientationHorizontal ))
})
2026-05-03 11:10:15 +01:00
// 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
2026-08-08 13:54:20 +01:00
Roster [ jid ] = true
2026-05-03 11:10:15 +01:00
if name == "" {
2026-05-08 06:56:51 +01:00
name = jid
2026-05-03 11:10:15 +01:00
}
createTab ( jid , false , name )
glib . IdleAdd ( func () {
2026-08-25 20:17:23 +01:00
box := createTabBox ( name , jid , jid , false )
2026-05-03 11:10:15 +01:00
menu . Append ( box )
menu . Append ( gtk . NewSeparator ( gtk . OrientationHorizontal ))
})
}
}
}
2026-02-17 09:27:46 +00:00
// Join rooms in bookmarks
2026-02-19 11:28:18 +00:00
if loadedConfig . JoinBookmarks {
books , err := stanza . NewItemsRequest ( "" , "urn:xmpp:bookmarks:1" , 0 )
2026-02-16 09:52:25 +00:00
if err == nil {
2026-02-19 11:28:18 +00:00
mychan , err := c . SendIQ ( context . TODO (), books )
result := <- mychan
if err == nil {
res , ok := result . Payload .( * stanza . PubSubGeneric )
if ok {
2026-07-17 17:15:41 +01:00
fmt . Printf ( "%d rooms in bookmarks\n" , len ( res . Items . List ))
2026-02-19 11:28:18 +00:00
for _ , item := range res . Items . List {
2026-04-18 10:29:07 +01:00
jid := item . Id
node := item . Any
autojoin := false
2026-05-03 11:10:15 +01:00
name := jid
2026-04-30 14:55:17 +01:00
password := ""
nick := loadedConfig . Nick
2026-04-18 10:29:07 +01:00
for _ , attr := range node . Attrs {
if attr . Name . Local == "autojoin" {
autojoin = attr . Value == "true"
2026-04-26 10:40:13 +01:00
break
}
}
for _ , attr := range node . Attrs {
if attr . Name . Local == "name" {
name = attr . Value
break
2026-02-17 09:27:46 +00:00
}
2026-04-18 10:29:07 +01:00
}
2026-02-17 09:27:46 +00:00
2026-04-18 10:29:07 +01:00
for _ , attr := range node . Attrs {
if attr . Name . Local == "autojoin" {
autojoin = attr . Value == "true"
2026-04-26 10:40:13 +01:00
break
2026-02-19 11:28:18 +00:00
}
2026-04-18 10:29:07 +01:00
}
2026-02-19 11:28:18 +00:00
2026-04-18 10:29:07 +01:00
for _ , node := range node . Nodes {
if node . XMLName . Local == "nick" {
nick = node . Content
2026-04-26 10:40:13 +01:00
break
2026-02-19 11:28:18 +00:00
}
2026-04-18 10:29:07 +01:00
}
2026-04-30 14:55:17 +01:00
for _ , node := range node . Nodes {
if node . XMLName . Local == "password" {
password = node . Content
break
2026-04-18 10:29:07 +01:00
}
}
2026-04-30 14:55:17 +01:00
_ , ok := tabs . Load ( jid )
if ! ok && autojoin {
joinMuc ( client , clientroot . Session . BindJid , jid , nick , password )
createTab ( jid , true , name )
glib . IdleAdd ( func () {
2026-08-25 20:17:23 +01:00
box := createTabBox ( name , jid , jid , true )
2026-05-03 11:10:15 +01:00
menu . Append ( box )
menu . Append ( gtk . NewSeparator ( gtk . OrientationHorizontal ))
2026-04-30 14:55:17 +01:00
})
}
2026-02-19 11:28:18 +00:00
}
2026-02-16 09:52:25 +00:00
}
2026-04-30 14:55:17 +01:00
2026-02-16 09:52:25 +00:00
}
}
2026-08-20 11:01:25 +01:00
// Local bookmarks
for jid , nick := range loadedConfig . LocalBookmarks {
joinMuc ( client , clientroot . Session . BindJid , jid , nick , "" )
createTab ( jid , true , jid )
glib . IdleAdd ( func () {
2026-08-25 20:17:23 +01:00
box := createTabBox ( jid , jid , jid , true )
2026-08-20 11:01:25 +01:00
menu . Append ( box )
menu . Append ( gtk . NewSeparator ( gtk . OrientationHorizontal ))
})
}
2026-02-16 09:52:25 +00:00
}
2026-01-29 21:35:36 +00:00
})
2026-03-15 09:55:26 +00:00
conc := func () {
2026-04-28 12:58:00 +01:00
connectionStatus . SetText ( loadedLocale [ "connecting" ])
2026-05-23 06:45:18 +01:00
connectionIcon . SetFromPaintable ( clientAssetsLoad ( "hourglass" ))
2026-02-19 14:30:04 +00:00
2026-01-29 21:35:36 +00:00
err = cm . Run ()
if err != nil {
2026-02-19 11:28:18 +00:00
fmt . Println ( err . Error ())
2026-04-28 12:58:00 +01:00
connectionStatus . SetText ( fmt . Sprintf ( "%s%s" , loadedLocale [ "disconnected" ], err . Error ()))
2026-05-23 06:45:18 +01:00
connectionIcon . SetFromPaintable ( clientAssetsLoad ( "disconnect" ))
2026-01-29 21:35:36 +00:00
}
2026-03-15 09:55:26 +00:00
}
2026-01-29 21:35:36 +00:00
2026-10-11 12:08:38 +01:00
app := gtk . NewApplication ( app_id , gio . ApplicationDefaultFlags )
2026-03-15 09:55:26 +00:00
app . ConnectActivate ( func () {
2026-03-15 09:57:08 +00:00
activate ( app )
2026-04-30 14:55:17 +01:00
go conc ()
2026-03-15 09:55:26 +00:00
})
2026-01-29 21:35:36 +00:00
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 ,
)
2026-01-31 15:38:02 +00:00
window = gtk . NewApplicationWindow ( app )
2026-02-01 19:22:43 +00:00
the_menu := gio . NewMenu ()
fileMenu := gio . NewMenu ()
2026-04-28 12:58:00 +01:00
fileMenu . Append ( loadedLocale [ "joinMUCMenu" ], "app.join" )
fileMenu . Append ( loadedLocale [ "destroyMUCMenu" ], "app.destroymuc" )
2026-10-11 12:08:38 +01:00
fileMenu . Append ( "MUC Affiliations" , "app.mucaffils" )
fileMenu . Append ( "Configure MUC" , "app.configuremuc" )
viewAffilsMucAction := gio . NewSimpleAction ( "mucaffils" , nil )
viewAffilsMucAction . ConnectActivate ( func ( p * glib . Variant ) {
go func () {
cur , ok := tabs . Load ( current )
if ok {
cur := cur .( * chatTab )
if cur . isMuc {
affils := [] string { "owner" , "admin" , "member" , "outcast" }
items := [] MucAdminItem {}
for _ , affil := range affils {
iq , err := stanza . NewIQ ( stanza . Attrs { Type : stanza . IQTypeGet , To : current , Id : uuid . NewString (), From : clientroot . Session . BindJid })
if err != nil {
return
}
q := new ( MucAdminQuery )
q . MucAdminItems = append ( q . MucAdminItems , MucAdminItem { Affiliation : affil })
iq . Payload = q
ctx := context . TODO ()
mychan , err := client . SendIQ ( ctx , iq )
result := <- mychan
res , ok := result . Payload .( * MucAdminQuery )
if ok {
for _ , item := range res . MucAdminItems {
items = append ( items , item )
}
}
}
glib . IdleAdd ( func () {
affil_win := gtk . NewWindow ()
affil_box := gtk . NewBox ( gtk . OrientationVertical , 2 )
affil_box . Append ( gtk . NewLabel ( "Affiliations" ))
scroller := gtk . NewScrolledWindow ()
scroller_grid := gtk . NewGrid ()
scroller_grid . SetVExpand ( true )
scroller . SetChild ( scroller_grid )
scroller . SetVExpand ( true )
affil_box . Append ( scroller )
affil_win . SetChild ( affil_box )
scroller_grid . Attach ( gtk . NewLabel ( "Affiliation" ), 0 , 0 , 1 , 1 )
scroller_grid . Attach ( gtk . NewLabel ( "JID" ), 1 , 0 , 1 , 1 )
scroller_grid . Attach ( gtk . NewLabel ( "Nick" ), 2 , 0 , 1 , 1 )
scroller_grid . Attach ( gtk . NewLabel ( "Reason" ), 3 , 0 , 1 , 1 )
for i , v := range items {
medal_box := gtk . NewBox ( gtk . OrientationHorizontal , 0 )
medal_box . Append ( gtk . NewImageFromPaintable ( clientAssetsLoad ( v . Affiliation )))
medal_box . Append ( gtk . NewLabel ( v . Affiliation ))
scroller_grid . Attach ( medal_box , 0 , i + 1 , 1 , 1 )
jid := gtk . NewLabel ( v . JID )
jid . AddCSSClass ( "jid" )
jid . SetSelectable ( true )
scroller_grid . Attach ( jid , 1 , i + 1 , 1 , 1 )
nick := gtk . NewLabel ( v . Nick )
nick . SetSelectable ( true )
scroller_grid . Attach ( nick , 2 , i + 1 , 1 , 1 )
reason := gtk . NewLabel ( v . Reason )
reason . SetSelectable ( true )
reason . SetEllipsize ( pango . EllipsizeEnd )
scroller_grid . Attach ( reason , 3 , i + 1 , 1 , 1 )
}
affil_win . SetVisible ( true )
})
}
}
}()
})
2026-02-01 19:22:43 +00:00
2026-08-03 19:39:57 +01:00
dmMenu := gio . NewMenu ()
dmMenu . Append ( loadedLocale [ "startDMMenu" ], "app.dm" )
startDmAction := gio . NewSimpleAction ( "dm" , nil )
startDmAction . ConnectActivate ( func ( p * glib . Variant ) {
mainBox := gtk . NewBox ( gtk . OrientationVertical , 5 )
dm_window := gtk . NewWindow ()
dm_window . SetTransientFor ( & window . Window )
dm_window . SetTitle ( "Start a DM" )
jid_box := gtk . NewBox ( gtk . OrientationHorizontal , 10 )
jid_entry := gtk . NewEntry ()
create_jid := gtk . NewImageFromPaintable ( clientAssetsLoad ( "jabber" ))
gesture := gtk . NewGestureClick ()
gesture . SetButton ( 1 )
gesture . Connect ( "pressed" , func () {
jidBuilder ( jid_entry )
})
create_jid . AddController ( gesture )
jid_box . Append ( gtk . NewLabel ( "JID: " ))
jid_box . Append ( create_jid )
jid_box . Append ( jid_entry )
mainBox . Append ( jid_box )
submit := gtk . NewButtonWithLabel ( loadedLocale [ "submit" ])
submit . ConnectClicked ( func () {
t := jid_entry . Text ()
2026-08-25 20:17:23 +01:00
createTab ( t , false , t )
box := createTabBox ( t , t , t , true )
2026-08-03 19:39:57 +01:00
menu . Append ( box )
menu . Append ( gtk . NewSeparator ( gtk . OrientationHorizontal ))
dm_window . SetVisible ( false )
})
mainBox . Append ( submit )
dm_window . SetChild ( mainBox )
dm_window . SetDefaultSize ( 100 , 1 )
dm_window . SetVisible ( true )
})
2026-02-04 17:57:24 +00:00
helpMenu := gio . NewMenu ()
helpMenu . Append ( "About" , "app.about" )
aboutAction := gio . NewSimpleAction ( "about" , nil )
aboutAction . ConnectActivate ( func ( p * glib . Variant ) {
2026-04-30 14:55:17 +01:00
a := gtk . NewAboutDialog ()
about_window := gtk . NewWindow ()
about_window . SetTransientFor ( & window . Window )
about_window . SetTitle ( fmt . Sprintf ( "%s %s" , "About" , loadedLocale [ "appName" ]))
2026-08-20 11:01:25 +01:00
a . SetProgramName ( loadedLocale [ "appName" ])
a . SetVersion ( fmt . Sprintf ( "Version %s" , lambda_version ))
2026-04-30 14:55:17 +01:00
a . SetComments ( "yet another XMPP client" )
2026-08-20 11:01:25 +01:00
a . SetAuthors ([] string { "Sunglocto" , "Hydrogen" , "Kuyuhi" })
a . SetLicenseType ( gtk . LicenseGPL30 )
a . SetLogo ( clientAssetsLoad ( "logo" ))
2026-10-11 12:08:38 +01:00
a . SetWebsite ( "https://sunglocto.net/lambda" )
2026-04-30 14:55:17 +01:00
a . SetWebsiteLabel ( "Website" )
2026-08-20 11:01:25 +01:00
a . SetVisible ( true )
2026-02-04 17:57:24 +00:00
})
2026-10-11 12:08:38 +01:00
configureMucAction := gio . NewSimpleAction ( "configuremuc" , nil )
configureMucAction . ConnectActivate ( func ( p * glib . Variant ) {
go func () {
cur , ok := tabs . Load ( current )
if ok {
cur := cur .( * chatTab )
if cur . isMuc {
iq , err := stanza . NewIQ ( stanza . Attrs {
Type : stanza . IQTypeGet ,
Id : uuid . NewString (),
To : current ,
})
if err != nil {
glib . IdleAdd ( func () { showErrorDialog ( err , & window . Window ) })
return
}
iq . Payload = & MucOwnerQuery {}
ctx := context . TODO ()
mychan , err := client . SendIQ ( ctx , iq )
if err != nil {
glib . IdleAdd ( func () { showErrorDialog ( err , & window . Window ) })
return
}
result := <- mychan
res , ok := result . Payload .( * MucOwnerQuery )
if ! ok {
return
}
glib . IdleAdd ( func () {
f , win , _ := showFormDialog ( res . Form )
win . ConnectCloseRequest ( func () ( ok bool ) {
go func () {
iq , err := stanza . NewIQ ( stanza . Attrs {
Id : result . Id ,
To : result . From ,
Type : stanza . IQTypeSet ,
})
if err != nil {
glib . IdleAdd ( func () { showErrorDialog ( err , & window . Window ) })
return
}
f . Type = "submit"
mo := new ( MucOwnerQuery )
mo . Form = * f
iq . Payload = mo
ctx := context . TODO ()
_ , err = client . SendIQ ( ctx , iq )
if err != nil {
glib . IdleAdd ( func () { showErrorDialog ( err , & window . Window ) })
return
}
}()
return false
})
})
}
}
}()
})
2026-02-15 13:08:52 +00:00
destroymucAction := gio . NewSimpleAction ( "destroymuc" , nil )
destroymucAction . ConnectActivate ( func ( p * glib . Variant ) {
2026-02-16 09:52:25 +00:00
cur , ok := tabs . Load ( current )
2026-02-15 13:08:52 +00:00
if ok {
cur := cur .( * chatTab )
if cur . isMuc {
2026-02-19 11:28:18 +00:00
win := gtk . NewWindow ()
2026-04-28 12:58:00 +01:00
win . SetTitle ( loadedLocale [ "destroyMUCMenu" ])
2026-02-19 11:28:18 +00:00
win . SetDefaultSize ( 400 , 1 )
win . SetResizable ( false )
2026-04-28 12:58:00 +01:00
box := gtk . NewBox ( gtk . OrientationVertical , 10 )
box . Append ( gtk . NewLabel ( loadedLocale [ "destroyMUCWarningOne" ]))
box . Append ( gtk . NewLabel ( loadedLocale [ "destroyMUCWarningTwo" ]))
cancel := gtk . NewButtonWithLabel ( loadedLocale [ "cancel" ])
2026-02-19 11:28:18 +00:00
cancel . ConnectClicked ( func () {
win . SetVisible ( false )
})
en := gtk . NewEntry ()
en . SetPlaceholderText ( "..." )
2026-04-28 12:58:00 +01:00
submit := gtk . NewButtonWithLabel ( loadedLocale [ "destroyMUCActionButton" ])
2026-02-19 11:28:18 +00:00
submit . ConnectClicked ( func () {
2026-04-28 12:58:00 +01:00
if en . Text () == loadedLocale [ "destroyMUCPassword" ] {
2026-02-19 11:28:18 +00:00
cur , ok := tabs . Load ( current )
if ok {
cur := cur .( * chatTab )
if cur . isMuc {
client . SendRaw ( fmt . Sprintf ( `
2026-02-15 13:08:52 +00:00
<iq from='%s'
id='begone'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#owner'>
<destroy jid='%s'>
2026-04-28 12:58:00 +01:00
<reason>%s</reason>
2026-02-15 13:08:52 +00:00
</destroy>
</query>
</iq>
2026-04-28 12:58:00 +01:00
` , clientroot . Session . BindJid , current , JidMustParse ( clientroot . Session . BindJid ). Bare (), loadedLocale [ "userRequested" ]))
2026-02-19 11:28:18 +00:00
}
}
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" {
2026-04-28 12:58:00 +01:00
box . Append ( gtk . NewLabel ( loadedLocale [ "destroyMUCNotOwnerWarning" ]))
2026-08-25 20:17:23 +01:00
return false
2026-02-19 11:28:18 +00:00
}
}
}
}
}
return true
})
}
win . SetChild ( box )
win . SetVisible ( true )
2026-02-15 13:08:52 +00:00
}
}
2026-02-19 11:28:18 +00:00
2026-02-15 13:08:52 +00:00
})
2026-02-01 19:22:43 +00:00
joinAction := gio . NewSimpleAction ( "join" , nil )
joinAction . ConnectActivate ( func ( p * glib . Variant ) {
2026-04-28 12:58:00 +01:00
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 )
2026-08-16 20:07:53 +01:00
bookmark_box := gtk . NewBox ( gtk . OrientationHorizontal , 10 )
2026-02-01 19:22:43 +00:00
jid_entry := gtk . NewEntry ()
nick_entry := gtk . NewEntry ()
2026-04-26 10:40:13 +01:00
disco_check := gtk . NewCheckButton ()
2026-08-16 20:07:53 +01:00
bookmark_check := gtk . NewCheckButton ()
2026-02-01 19:22:43 +00:00
jid_entry . SetHAlign ( gtk . AlignEnd )
jid_entry . SetHExpand ( true )
nick_entry . SetHAlign ( gtk . AlignEnd )
nick_entry . SetHExpand ( true )
nick_entry . SetText ( loadedConfig . Nick )
2026-05-23 06:45:18 +01:00
create_jid := gtk . NewImageFromPaintable ( clientAssetsLoad ( "jabber" ))
2026-04-30 14:55:17 +01:00
gesture := gtk . NewGestureClick ()
gesture . SetButton ( 1 )
gesture . Connect ( "pressed" , func () {
jidBuilder ( jid_entry )
})
create_jid . AddController ( gesture )
2026-04-28 12:58:00 +01:00
jid_box . Append ( gtk . NewLabel ( loadedLocale [ "joinMUCJIDEntry" ]))
2026-04-30 14:55:17 +01:00
jid_box . Append ( create_jid )
2026-02-01 19:22:43 +00:00
jid_box . Append ( jid_entry )
2026-04-28 12:58:00 +01:00
nick_box . Append ( gtk . NewLabel ( loadedLocale [ "joinMUCNickEntry" ]))
2026-02-01 19:22:43 +00:00
nick_box . Append ( nick_entry )
2026-04-26 10:40:13 +01:00
disco_check . SetActive ( true )
2026-04-28 12:58:00 +01:00
disco_box . Append ( gtk . NewLabel ( loadedLocale [ "joinMUCDiscoCheck" ]))
2026-04-26 10:40:13 +01:00
disco_box . Append ( disco_check )
2026-04-28 12:58:00 +01:00
disco_box . SetTooltipText ( loadedLocale [ "joinMUCDiscoCheckTooltip" ])
2026-04-26 10:40:13 +01:00
2026-08-16 20:07:53 +01:00
bookmark_check . SetActive ( true )
bookmark_box . Append ( gtk . NewLabel ( loadedLocale [ "joinMUCBookmarkCheck" ]))
bookmark_box . Append ( bookmark_check )
2026-02-01 19:22:43 +00:00
box . Append ( jid_box )
box . Append ( nick_box )
2026-04-26 10:40:13 +01:00
box . Append ( disco_box )
2026-08-16 20:07:53 +01:00
box . Append ( bookmark_box )
2026-02-01 19:22:43 +00:00
2026-04-28 12:58:00 +01:00
btn := gtk . NewButtonWithLabel ( loadedLocale [ "submit" ])
2026-08-20 11:01:25 +01:00
btn . SetVAlign ( gtk . AlignBaselineFill )
2026-02-01 19:22:43 +00:00
box . Append ( btn )
win := gtk . NewWindow ()
2026-04-28 12:58:00 +01:00
win . SetTitle ( loadedLocale [ "joinMUCMenu" ])
2026-02-04 17:57:24 +00:00
win . SetDefaultSize ( 400 , 1 )
2026-02-19 11:28:18 +00:00
win . SetResizable ( false )
2026-02-01 19:22:43 +00:00
win . SetChild ( box )
btn . ConnectClicked ( func () {
t := jid_entry . Text ()
_ , ok := tabs . Load ( t )
2026-04-26 10:40:13 +01:00
jm := func ( n string , pw string ) {
err := joinMuc ( client , clientroot . Session . BindJid , t , nick_entry . Text (), pw )
2026-04-18 10:29:07 +01:00
if err != nil {
2026-05-09 07:33:47 +01:00
showErrorDialog ( err , win )
2026-04-26 10:40:13 +01:00
return
2026-04-18 10:29:07 +01:00
}
2026-04-06 11:04:53 +01:00
2026-04-26 10:40:13 +01:00
createTab ( t , true , n )
2026-08-25 20:17:23 +01:00
box := createTabBox ( n , t , t , true )
2026-05-03 11:10:15 +01:00
menu . Append ( box )
menu . Append ( gtk . NewSeparator ( gtk . OrientationHorizontal ))
2026-08-16 20:07:53 +01:00
if bookmark_check . Active () {
_ , err := addPEPBookmark ( client , t , n , nick_entry . Text (), true , pw )
if err != nil {
panic ( err )
}
}
2026-04-06 11:04:53 +01:00
}
2026-02-01 19:22:43 +00:00
if ! ok {
2026-04-26 10:40:13 +01:00
if ! disco_check . Active () {
jm ( t , "" )
win . SetVisible ( false )
return
}
var res * stanza . DiscoInfo
2026-04-06 11:04:53 +01:00
allowed := true
myIQ , err := stanza . NewIQ ( stanza . Attrs {
Type : "get" ,
From : clientroot . Session . BindJid ,
To : t ,
2026-08-25 20:17:23 +01:00
Id : uuid . NewString (),
2026-04-06 11:04:53 +01:00
Lang : "en" ,
})
2026-02-01 19:22:43 +00:00
if err != nil {
panic ( err )
}
2026-04-06 11:04:53 +01:00
myIQ . Payload = & stanza . DiscoInfo {}
2026-02-22 08:16:14 +00:00
2026-08-16 20:07:53 +01:00
ctx := context . Background ()
2026-04-26 10:40:13 +01:00
2026-08-25 20:17:23 +01:00
var mychan chan stanza . IQ
//go func() {
mychan , err = client . SendIQ ( ctx , myIQ )
if err != nil {
showErrorDialog ( err , & window . Window )
}
//}()
2026-04-28 12:58:00 +01:00
2026-08-25 20:17:23 +01:00
result := <- mychan
res , ok = result . Payload .( * stanza . DiscoInfo )
if ok {
if len ( res . Identity ) >= 1 {
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 )
2026-04-26 10:40:13 +01:00
2026-08-25 20:17:23 +01:00
buttons := gtk . NewBox ( gtk . OrientationHorizontal , 10 )
2026-08-16 20:07:53 +01:00
2026-08-25 20:17:23 +01:00
join_button := gtk . NewButtonWithLabel ( "Join" )
join_button . ConnectClicked ( func () {
warning_win . SetVisible ( false )
if password_protected {
allowed = false
2026-08-16 20:07:53 +01:00
2026-08-25 20:17:23 +01:00
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 )
2026-04-26 10:40:13 +01:00
}
2026-08-25 20:17:23 +01:00
jm ( res . Identity [ 0 ]. Name , password )
})
2026-04-06 11:04:53 +01:00
2026-08-25 20:17:23 +01:00
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 ( clientAssetsLoad ( icon )))
box . Append ( gtk . NewLabel ( description ))
warning_box . Append ( box )
2026-08-16 20:07:53 +01:00
}
2026-08-25 20:17:23 +01:00
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 ()
2026-04-26 10:40:13 +01:00
} else {
allowed = false
2026-08-25 20:17:23 +01:00
showErrorDialog ( fmt . Errorf ( loadedLocale [ "discoFail" ]), win )
}
} else {
allowed = false
if result . Error != nil {
showErrorDialog ( fmt . Errorf ( "%s: %s - %s" , loadedLocale [ "discoFail" ], result . Error . Reason , result . Error . Text ), win )
} else {
showErrorDialog ( fmt . Errorf ( loadedLocale [ "discoFail" ]), win )
2026-04-06 11:04:53 +01:00
}
}
if allowed {
2026-04-26 10:40:13 +01:00
jm ( res . Identity [ 0 ]. Name , "" )
2026-04-06 11:04:53 +01:00
}
2026-02-01 19:22:43 +00:00
}
win . SetVisible ( false )
})
win . SetTransientFor ( win )
win . Present ()
})
app . AddAction ( joinAction )
2026-02-04 17:57:24 +00:00
app . AddAction ( aboutAction )
2026-02-15 13:08:52 +00:00
app . AddAction ( destroymucAction )
2026-10-11 12:08:38 +01:00
app . AddAction ( configureMucAction )
2026-08-03 19:39:57 +01:00
app . AddAction ( startDmAction )
2026-10-11 12:08:38 +01:00
app . AddAction ( viewAffilsMucAction )
2026-02-01 19:22:43 +00:00
2026-04-30 14:55:17 +01:00
the_menu . AppendSubmenu ( "MUC" , fileMenu )
2026-08-03 19:39:57 +01:00
the_menu . AppendSubmenu ( "DM" , dmMenu )
2026-02-04 17:57:24 +00:00
the_menu . AppendSubmenu ( "Help" , helpMenu )
2026-02-01 19:22:43 +00:00
the_menuBar := gtk . NewPopoverMenuBarFromModel ( the_menu )
2026-01-29 21:35:36 +00:00
app . SetMenubar ( gio . NewMenu ())
2026-08-08 13:54:20 +01:00
window . SetTitle ( loadedLocale [ "appName" ])
2026-01-30 19:08:18 +00:00
window . Window . AddCSSClass ( "ssd" )
2026-02-01 09:17:57 +00:00
window . Window . SetDefaultSize ( 500 , 500 )
2026-01-31 10:02:04 +00:00
menu = gtk . NewBox ( gtk . OrientationVertical , 0 )
2026-01-29 21:35:36 +00:00
2026-05-23 06:45:18 +01:00
empty_dialog = gtk . NewImageFromPaintable ( clientAssetsLoad ( "disabled_logo" ))
2026-02-15 13:08:52 +00:00
empty_dialog . SetPixelSize ( 100 )
2026-01-29 21:35:36 +00:00
empty_dialog . SetVExpand ( true )
scroller = gtk . NewScrolledWindow ()
2026-01-30 10:40:38 +00:00
memberList = gtk . NewScrolledWindow ()
scroller . SetHExpand ( true )
memberList . SetHExpand ( true )
2026-01-29 21:35:36 +00:00
box := gtk . NewBox ( gtk . OrientationVertical , 0 )
2026-02-01 19:22:43 +00:00
box . Append ( the_menuBar )
2026-02-17 09:27:46 +00:00
statBar := gtk . NewBox ( gtk . OrientationHorizontal , 0 )
cBox := gtk . NewBox ( gtk . OrientationHorizontal , 0 )
2026-05-23 06:45:18 +01:00
connectionIcon = gtk . NewImageFromPaintable (( clientAssetsLoad ( "disconnect" )))
2026-02-22 08:16:14 +00:00
connectionIcon . AddCSSClass ( "icon" )
2026-04-28 12:58:00 +01:00
connectionStatus = gtk . NewLabel ( loadedLocale [ "disconnected" ])
2026-02-19 14:30:04 +00:00
cBox . Append ( connectionIcon )
2026-02-17 09:27:46 +00:00
cBox . Append ( connectionStatus )
2026-02-19 14:30:04 +00:00
2026-02-17 09:27:46 +00:00
statBar . Append ( cBox )
2026-02-22 08:16:14 +00:00
mBox := gtk . NewBox ( gtk . OrientationHorizontal , 0 )
gesture1 := gtk . NewGestureClick ()
gesture1 . SetButton ( 1 )
gesture1 . Connect ( "pressed" , func () {
current = mStatus . Text ()
switchToTab ( current , & window . Window )
})
2026-05-23 06:45:18 +01:00
mIcon = gtk . NewImageFromPaintable (( clientAssetsLoad ( "comment" )))
2026-02-22 08:16:14 +00:00
mIcon . AddCSSClass ( "icon" )
mStatus = gtk . NewLabel ( "-" )
mStatus . AddController ( gesture1 )
2026-08-16 20:07:53 +01:00
gIcon = gtk . NewImageFromPaintable (( clientAssetsLoad ( "timeline_marker" )))
gIcon . AddCSSClass ( "icon" )
gStatus = gtk . NewLabel ( "..." )
2026-02-22 08:16:14 +00:00
cBox . Append ( mIcon )
cBox . Append ( mStatus )
2026-08-16 20:07:53 +01:00
cBox . Append ( gIcon )
cBox . Append ( gStatus )
2026-02-22 08:16:14 +00:00
statBar . Append ( mBox )
2026-02-17 09:27:46 +00:00
pBox := gtk . NewBox ( gtk . OrientationHorizontal , 0 )
2026-04-28 12:58:00 +01:00
pBox . SetTooltipText ( loadedLocale [ "pingBarTooltip" ])
2026-02-20 06:28:31 +00:00
gesture := gtk . NewGestureClick ()
gesture . SetButton ( 3 )
gesture . Connect ( "pressed" , func () {
opt := charts . NewLineChartOptionWithData ( pingTimes )
opt . Title = charts . TitleOption {
2026-04-28 12:58:00 +01:00
Text : loadedLocale [ "pingGraphTitle" ],
2026-02-20 06:28:31 +00:00
}
2026-08-20 11:01:25 +01:00
2026-02-20 06:28:31 +00:00
opt . Legend = charts . LegendOption {
SeriesNames : [] string {
2026-04-28 12:58:00 +01:00
loadedLocale [ "pingGraphYAxis" ],
2026-02-20 06:28:31 +00:00
},
}
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 )
2026-07-14 16:25:18 +01:00
win . SetResizable ( false )
2026-04-28 12:58:00 +01:00
win . SetTitle ( loadedLocale [ "pingGraphTitle" ])
2026-02-20 06:28:31 +00:00
box := gtk . NewBox ( gtk . OrientationVertical , 0 )
box . Append ( i )
win . SetChild ( box )
win . SetVisible ( true )
})
pBox . AddController ( gesture )
2026-05-23 06:45:18 +01:00
i := ( gtk . NewImageFromPaintable ( clientAssetsLoad ( "chart_bar" )))
2026-02-17 09:27:46 +00:00
i . AddCSSClass ( "icon" )
2026-05-10 14:46:19 +01:00
2026-02-17 09:27:46 +00:00
pBox . Append ( i )
2026-02-19 11:28:18 +00:00
pingStatus = gtk . NewLabel ( "..." )
2026-02-17 09:27:46 +00:00
pBox . Append ( pingStatus )
statBar . Append ( pBox )
2026-02-22 08:16:14 +00:00
scrollerStatBar := gtk . NewScrolledWindow ()
scrollerStatBar . SetChild ( statBar )
box . Append ( scrollerStatBar )
2026-02-17 09:27:46 +00:00
2026-01-29 21:35:36 +00:00
scroller . SetChild ( empty_dialog )
menu_scroll := gtk . NewScrolledWindow ()
2026-01-31 10:02:04 +00:00
menu_scroll . SetHExpand ( true )
2026-01-29 21:35:36 +00:00
menu_scroll . SetChild ( menu )
2026-01-30 10:40:38 +00:00
chatbox := gtk . NewBox ( gtk . OrientationHorizontal , 0 )
2026-01-31 10:02:04 +00:00
chat_pane := gtk . NewPaned ( gtk . OrientationHorizontal )
chat_pane . SetStartChild ( scroller )
chat_pane . SetEndChild ( memberList )
2026-02-01 09:17:57 +00:00
chat_pane . SetPosition ( 225 )
2026-01-31 10:02:04 +00:00
main_pane := gtk . NewPaned ( gtk . OrientationHorizontal )
main_pane . SetStartChild ( menu_scroll )
main_pane . SetEndChild ( chat_pane )
2026-02-01 09:17:57 +00:00
main_pane . SetPosition ( 135 )
2026-01-31 10:02:04 +00:00
chatbox . Append ( main_pane )
2026-01-30 10:40:38 +00:00
box . Append ( chatbox )
2026-01-29 21:35:36 +00:00
entry_box := gtk . NewBox ( gtk . OrientationHorizontal , 0 )
2026-03-10 16:35:56 +00:00
oob_en := gtk . NewEntry ()
2026-04-28 12:58:00 +01:00
oob_en . SetPlaceholderText ( "URL" )
2026-03-10 16:35:56 +00:00
message_en = gtk . NewEntry ()
2026-04-28 12:58:00 +01:00
message_en . SetPlaceholderText ( loadedLocale [ "messageEntryPlaceholder" ])
b := gtk . NewButtonWithLabel ( loadedLocale [ "send" ])
2026-01-29 21:35:36 +00:00
sendtxt := func () {
2026-03-10 16:35:56 +00:00
t := message_en . Text ()
2026-01-29 21:35:36 +00:00
2026-01-30 21:31:33 +00:00
message_type := stanza . MessageTypeChat
tab , ok := tabs . Load ( current )
if ! ok {
return
}
typed_tab := tab .( * chatTab )
if typed_tab . isMuc {
2026-01-31 10:02:04 +00:00
message_type = stanza . MessageTypeGroupchat
2026-07-14 16:25:18 +01:00
// First check if the user is a visitor
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 {
var mui MucUser
ok = user . Get ( & mui )
if ok && mui . MucUserItem . JID != "" {
if JidMustParse ( mui . MucUserItem . JID ). Bare () == JidMustParse ( clientroot . Session . BindJid ). Bare () {
if mui . MucUserItem . Role == "visitor" {
win := gtk . NewWindow ()
2026-07-17 17:15:41 +01:00
win . SetDefaultSize ( 1 , 1 )
2026-07-14 16:25:18 +01:00
box := gtk . NewBox ( gtk . OrientationVertical , 5 )
txt := gtk . NewLabel ( "You are a visitor and do not have voice.\nWould you like to request voice from the moderators of this MUC?" )
btn := gtk . NewButtonWithLabel ( "Request voice" )
2026-07-17 17:15:41 +01:00
btn . ConnectClicked ( func () {
2026-07-14 16:25:18 +01:00
sendVoiceRequest ( client , current )
win . SetVisible ( false )
})
dismiss := gtk . NewButtonWithLabel ( "Dismiss" )
dismiss . ConnectClicked ( func () {
win . SetVisible ( false )
})
box . Append ( txt )
box . Append ( btn )
box . Append ( dismiss )
win . SetChild ( box )
win . SetResizable ( false )
win . SetVisible ( true )
}
return false
}
}
}
return true
})
}
2026-01-31 10:02:04 +00:00
}
2026-01-30 21:31:33 +00:00
2026-03-10 16:35:56 +00:00
exts := [] stanza . MsgExtension {}
if oob_en . Text () != "" {
new_oob := new ( stanza . OOB )
new_oob . URL = oob_en . Text ()
exts = append ( exts , new_oob )
}
2026-03-12 16:09:22 +00:00
if strings . Contains ( t , "@everyone" ) {
2026-03-15 09:55:26 +00:00
start := strings . Index ( t , "@everyone" )
end := start + len ( "@everyone" )
2026-03-12 16:09:22 +00:00
new_mention := new ( Mention )
2026-04-06 11:04:53 +01:00
new_mention . Mentions = "urn:xmpp:mentions:0#channel"
2026-03-15 09:55:26 +00:00
new_mention . Begin = start
new_mention . End = end
2026-03-12 16:09:22 +00:00
exts = append ( exts , new_mention )
} else if strings . Contains ( t , "@here" ) {
new_attention := new ( Attention )
exts = append ( exts , new_attention )
}
2026-03-10 16:35:56 +00:00
err := sendMessage ( client , current , message_type , t , "" , "" , exts )
2026-01-29 21:35:36 +00:00
if err != nil {
2026-05-09 07:33:47 +01:00
showErrorDialog ( err , & window . Window )
2026-01-29 21:35:36 +00:00
}
2026-03-10 16:35:56 +00:00
message_en . SetText ( "" )
2026-01-29 21:35:36 +00:00
scrollToBottomAfterUpdate ( scroller )
}
2026-03-10 16:35:56 +00:00
message_en . Connect ( "activate" , sendtxt )
2026-01-29 21:35:36 +00:00
b . ConnectClicked ( sendtxt )
2026-03-10 16:35:56 +00:00
message_en . SetHExpand ( true )
2026-01-29 21:35:36 +00:00
2026-03-10 16:35:56 +00:00
entry_box . Append ( oob_en )
entry_box . Append ( message_en )
2026-01-29 21:35:36 +00:00
entry_box . Append ( b )
box . Append ( entry_box )
2026-04-06 11:04:53 +01:00
typingStatus = gtk . NewLabel ( "" )
box . Append ( typingStatus )
2026-01-29 21:35:36 +00:00
window . SetChild ( box )
window . SetVisible ( true )
}
func loadCSS ( content string ) * gtk . CSSProvider {
prov := gtk . NewCSSProvider ()
prov . LoadFromString ( content )
return prov
}