From e9ce35d0f292fe39ca7ce2fc83d64d069ef2cb44 Mon Sep 17 00:00:00 2001 From: wuriyanto Date: Tue, 5 Dec 2023 12:59:31 +0700 Subject: [PATCH 1/8] feat: account management: create, remove, search, change password --- xmpp_account.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 xmpp_account.go diff --git a/xmpp_account.go b/xmpp_account.go new file mode 100644 index 0000000..df6f8a0 --- /dev/null +++ b/xmpp_account.go @@ -0,0 +1,105 @@ +package xmpp + +import ( + "encoding/xml" + "fmt" + "strings" +) + +const ( + nsRegister = "jabber:iq:register" + nsSearch = "jabber:iq:search" +) + +type clientSearchAccountItem struct { + Jid string `xml:"jid,attr"` + First string `xml:"first"` + Last string `xml:"last"` + Nick string `xml:"nick"` + Email string `xml:"email"` +} + +type clientSearchAccountQuery struct { + XMLName xml.Name `xml:"query"` + Xmlns string `xml:"xmlns,attr"` + Items []clientSearchAccountItem `xml:"item"` +} + +// SearchAccountResultItem represent search account item result +type SearchAccountResultItem struct { + Jid string + FirstName string + LastName string + NickName string + Email string +} + +// SearchAccountResult represent search account item result +type SearchAccountResult struct { + Jid string + Accounts []SearchAccountResultItem +} + +func clientSearchAccountItemToReturn(accounts []clientSearchAccountItem) []SearchAccountResultItem { + var ret []SearchAccountResultItem + for _, account := range accounts { + ret = append(ret, SearchAccountResultItem{ + Jid: account.Jid, + FirstName: account.First, + LastName: account.Last, + NickName: account.Nick, + Email: account.Email, + }) + } + + return ret +} + +func buildAccountAttr(username string, password string, attributes map[string]string) string { + var attrBuilder strings.Builder + attrBuilder.WriteString(fmt.Sprintf("%s", username)) + attrBuilder.WriteString(fmt.Sprintf("%s", password)) + + if attributes != nil { + for k, v := range attributes { + attrBuilder.WriteString(fmt.Sprintf("<%s>%s", k, v, k)) + } + } + + return attrBuilder.String() +} + +// CreateAccount Creates a new account using the specified username, password and extra attributes +func (c *Client) CreateAccount(username string, password string, attributes map[string]string) error { + attrBuilder := buildAccountAttr(username, password, attributes) + const xmlIQ = "%s" + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, nsRegister, attrBuilder) + return err +} + +// ChangePassword enables a user to change his or her password with a server or service +func (c *Client) ChangePassword(username string, password string) error { + attrBuilder := buildAccountAttr(username, password, nil) + const xmlIQ = "%s" + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, c.domain, nsRegister, attrBuilder) + return err +} + +// RemoveAccount cancel a registration with a host by sending a element in an IQ set +func (c *Client) RemoveAccount(username string) error { + from := c.jid + const xmlIQ = "" + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, from, nsRegister) + return err +} + +// SearchAccount search information repositories on the Jabber network +// searchServiceName is the Search Service Properties Name from your server +func (c *Client) SearchAccount(searchServiceName, username, fieldName, fieldValue string) error { + from := c.jid + searchService := fmt.Sprintf("%s.%s", searchServiceName, c.domain) + searchQuery := fmt.Sprintf("<%s>%s", fieldName, fieldValue, fieldName) + const xmlIQ = "%s\n" + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, from, searchService, nsSearch, searchQuery) + return err +} From 73a532e3b4d0625dc4908103532ecc0914373284 Mon Sep 17 00:00:00 2001 From: wuriyanto Date: Tue, 5 Dec 2023 13:00:36 +0700 Subject: [PATCH 2/8] feat: account management: handle search account results in recv --- xmpp.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/xmpp.go b/xmpp.go index 051c11a..3f8dfc6 100644 --- a/xmpp.go +++ b/xmpp.go @@ -1096,6 +1096,19 @@ func (c *Client) Recv() (stanza interface{}, err error) { return handleAvatarMetadata(p.Items[0].Body, v }*/ + case "searchAccount1": + if v.Query.XMLName.Space == nsSearch { + var accountQuery clientSearchAccountQuery + err := xml.Unmarshal(v.InnerXML, &accountQuery) + if err != nil { + return PubsubSubscription{}, err + } + + return SearchAccountResult{ + Jid: v.From, + Accounts: clientSearchAccountItemToReturn(accountQuery.Items), + }, nil + } default: res, err := xml.Marshal(v.Query) if err != nil { From ab9cce729dce6ae58cd4f2834bbf9e4190c6de95 Mon Sep 17 00:00:00 2001 From: wuriyanto Date: Tue, 5 Dec 2023 13:11:02 +0700 Subject: [PATCH 3/8] feat: account management: add more comment --- xmpp_account.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/xmpp_account.go b/xmpp_account.go index df6f8a0..a0a4850 100644 --- a/xmpp_account.go +++ b/xmpp_account.go @@ -77,15 +77,17 @@ func (c *Client) CreateAccount(username string, password string, attributes map[ return err } -// ChangePassword enables a user to change his or her password with a server or service -func (c *Client) ChangePassword(username string, password string) error { - attrBuilder := buildAccountAttr(username, password, nil) +// ChangePassword enables a user to change his or her password with a server or service. +// With the user's username parameter which will change the password and new password +func (c *Client) ChangePassword(username string, newPassword string) error { + attrBuilder := buildAccountAttr(username, newPassword, nil) const xmlIQ = "%s" _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, c.domain, nsRegister, attrBuilder) return err } -// RemoveAccount cancel a registration with a host by sending a element in an IQ set +// RemoveAccount cancel a registration with a host by sending a element in an IQ set. +// With the username parameter belonging to the user to be deleted func (c *Client) RemoveAccount(username string) error { from := c.jid const xmlIQ = "" @@ -93,8 +95,8 @@ func (c *Client) RemoveAccount(username string) error { return err } -// SearchAccount search information repositories on the Jabber network -// searchServiceName is the Search Service Properties Name from your server +// SearchAccount search information repositories on the Jabber network. +// searchServiceName is the Search Service Properties Name from your server. func (c *Client) SearchAccount(searchServiceName, username, fieldName, fieldValue string) error { from := c.jid searchService := fmt.Sprintf("%s.%s", searchServiceName, c.domain) From 3f333bc66644ae7a3c53bf211ee639141507c5bf Mon Sep 17 00:00:00 2001 From: wuriyanto Date: Tue, 5 Dec 2023 13:14:19 +0700 Subject: [PATCH 4/8] feat: account management: remove unnecessary username parameter --- xmpp_account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmpp_account.go b/xmpp_account.go index a0a4850..237e9c6 100644 --- a/xmpp_account.go +++ b/xmpp_account.go @@ -97,7 +97,7 @@ func (c *Client) RemoveAccount(username string) error { // SearchAccount search information repositories on the Jabber network. // searchServiceName is the Search Service Properties Name from your server. -func (c *Client) SearchAccount(searchServiceName, username, fieldName, fieldValue string) error { +func (c *Client) SearchAccount(searchServiceName, fieldName, fieldValue string) error { from := c.jid searchService := fmt.Sprintf("%s.%s", searchServiceName, c.domain) searchQuery := fmt.Sprintf("<%s>%s", fieldName, fieldValue, fieldName) From a67513080ca186009fc14b1d1ece20b3f00d34d5 Mon Sep 17 00:00:00 2001 From: wuriyanto Date: Tue, 5 Dec 2023 13:17:22 +0700 Subject: [PATCH 5/8] fix example: the DefaultConfig should use the tls.Config pointer --- _example/example.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_example/example.go b/_example/example.go index 136af5a..1639714 100644 --- a/_example/example.go +++ b/_example/example.go @@ -40,7 +40,7 @@ func main() { } if !*notls { - xmpp.DefaultConfig = tls.Config{ + xmpp.DefaultConfig = &tls.Config{ ServerName: serverName(*server), InsecureSkipVerify: false, } From cd280be4226fa8b099eef061d24f36991cc99ac7 Mon Sep 17 00:00:00 2001 From: wuriyanto Date: Wed, 6 Dec 2023 11:23:44 +0700 Subject: [PATCH 6/8] fix: searchAccount1 case should return an empty SearchAccountResult when an error is detected --- xmpp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmpp.go b/xmpp.go index 3f8dfc6..1ca3f95 100644 --- a/xmpp.go +++ b/xmpp.go @@ -1101,7 +1101,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { var accountQuery clientSearchAccountQuery err := xml.Unmarshal(v.InnerXML, &accountQuery) if err != nil { - return PubsubSubscription{}, err + return SearchAccountResult{}, err } return SearchAccountResult{ From d8370c1ea31021175c68a4ac7bdd37ce8a160027 Mon Sep 17 00:00:00 2001 From: wuriyanto Date: Sat, 9 Dec 2023 21:38:28 +0700 Subject: [PATCH 7/8] refactor: remove unnecessary username parameters from RemoveAccount, from field is determined from current session --- xmpp_account.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xmpp_account.go b/xmpp_account.go index 237e9c6..df57c75 100644 --- a/xmpp_account.go +++ b/xmpp_account.go @@ -86,9 +86,9 @@ func (c *Client) ChangePassword(username string, newPassword string) error { return err } -// RemoveAccount cancel a registration with a host by sending a element in an IQ set. -// With the username parameter belonging to the user to be deleted -func (c *Client) RemoveAccount(username string) error { +// RemoveAccount cancel or delete the current session registration +// with a host by sending a element in an IQ set. +func (c *Client) RemoveAccount() error { from := c.jid const xmlIQ = "" _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, from, nsRegister) From 855180418f34f3c9d95718b43286bebb0d8c838d Mon Sep 17 00:00:00 2001 From: wuriyanto Date: Sun, 10 Dec 2023 00:55:00 +0700 Subject: [PATCH 8/8] feat: account management: request last activity information from another entity --- xmpp.go | 10 +++++++++ xmpp_account.go | 59 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/xmpp.go b/xmpp.go index 1ca3f95..4dd48a6 100644 --- a/xmpp.go +++ b/xmpp.go @@ -1109,6 +1109,16 @@ func (c *Client) Recv() (stanza interface{}, err error) { Accounts: clientSearchAccountItemToReturn(accountQuery.Items), }, nil } + case "requestLastActivity1": + if v.Query.XMLName.Space == nsLastActivity { + var lastActivity lastActivity + err := xml.Unmarshal(v.InnerXML, &lastActivity) + if err != nil { + return LastActivityResult{}, err + } + + return handleLastActivityResult(v.From, lastActivity) + } default: res, err := xml.Marshal(v.Query) if err != nil { diff --git a/xmpp_account.go b/xmpp_account.go index df57c75..2a78483 100644 --- a/xmpp_account.go +++ b/xmpp_account.go @@ -3,12 +3,14 @@ package xmpp import ( "encoding/xml" "fmt" + "strconv" "strings" ) const ( - nsRegister = "jabber:iq:register" - nsSearch = "jabber:iq:search" + nsRegister = "jabber:iq:register" + nsSearch = "jabber:iq:search" + nsLastActivity = "jabber:iq:last" ) type clientSearchAccountItem struct { @@ -40,6 +42,21 @@ type SearchAccountResult struct { Accounts []SearchAccountResultItem } +type lastActivity struct { + XMLName xml.Name `xml:"query"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + Seconds string `xml:"seconds,attr"` +} + +// LastActivityResult represent last activity response +// when current entity session is authorized to view the user's presence information +type LastActivityResult struct { + From string + Text string + LastActiveSeconds int +} + func clientSearchAccountItemToReturn(accounts []clientSearchAccountItem) []SearchAccountResultItem { var ret []SearchAccountResultItem for _, account := range accounts { @@ -55,10 +72,23 @@ func clientSearchAccountItemToReturn(accounts []clientSearchAccountItem) []Searc return ret } +func handleLastActivityResult(from string, lastActivity lastActivity) (LastActivityResult, error) { + lastActiveSeconds, err := strconv.Atoi(lastActivity.Seconds) + if err != nil { + return LastActivityResult{}, err + } + + return LastActivityResult{ + From: from, + Text: lastActivity.Text, + LastActiveSeconds: lastActiveSeconds, + }, nil +} + func buildAccountAttr(username string, password string, attributes map[string]string) string { var attrBuilder strings.Builder - attrBuilder.WriteString(fmt.Sprintf("%s", username)) - attrBuilder.WriteString(fmt.Sprintf("%s", password)) + attrBuilder.WriteString(fmt.Sprintf("%s", xmlEscape(username))) + attrBuilder.WriteString(fmt.Sprintf("%s", xmlEscape(password))) if attributes != nil { for k, v := range attributes { @@ -82,7 +112,7 @@ func (c *Client) CreateAccount(username string, password string, attributes map[ func (c *Client) ChangePassword(username string, newPassword string) error { attrBuilder := buildAccountAttr(username, newPassword, nil) const xmlIQ = "%s" - _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, c.domain, nsRegister, attrBuilder) + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(c.domain), nsRegister, attrBuilder) return err } @@ -91,7 +121,7 @@ func (c *Client) ChangePassword(username string, newPassword string) error { func (c *Client) RemoveAccount() error { from := c.jid const xmlIQ = "" - _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, from, nsRegister) + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), nsRegister) return err } @@ -100,8 +130,21 @@ func (c *Client) RemoveAccount() error { func (c *Client) SearchAccount(searchServiceName, fieldName, fieldValue string) error { from := c.jid searchService := fmt.Sprintf("%s.%s", searchServiceName, c.domain) - searchQuery := fmt.Sprintf("<%s>%s", fieldName, fieldValue, fieldName) + searchQuery := fmt.Sprintf("<%s>%s", fieldName, xmlEscape(fieldValue), fieldName) const xmlIQ = "%s\n" - _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, from, searchService, nsSearch, searchQuery) + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), searchService, nsSearch, searchQuery) + return err +} + +// RequestLastActivity request last activity information regarding another entity +func (c *Client) RequestLastActivity(to string) error { + targetEntity := strings.SplitN(to, "@", 2) + if len(targetEntity) < 2 { + to = fmt.Sprintf("%s@%s", to, c.domain) + } + + from := c.jid + const xmlIQ = "" + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), nsLastActivity) return err }