Compare commits

...

9 Commits

Author SHA1 Message Date
Mickaël Rémond
7186c058fd
Update test.yaml 2024-05-16 17:41:31 +02:00
Mickaël Rémond
87fb1dfe78
Update test.yaml 2024-05-16 16:51:07 +02:00
Bohdan Horbeshko
655f875918 Support multiple command elements 2024-05-16 16:47:09 +02:00
Bohdan Horbeshko
9af32ad7e0 Fix marshalling/unmarshalling of command children 2024-05-16 16:47:09 +02:00
bodqhrohro
5f99e1cd06 Support partial JIDs in Bare/Full methods 2021-12-14 12:01:36 +01:00
remicorniere
ac5b066815
Merge pull request #164 from remicorniere/XEP-0082
Support for XEP-0082.
2020-05-07 00:10:55 +00:00
CORNIERE Rémi
17d561f829 Support for XEP-0082.
Parsing of times with an offset does not work for now (should it ?)
2020-04-29 10:13:31 +02:00
remicorniere
ce71bc5c76
Merge pull request #163 from remicorniere/XEP-0334
Support for XEP-0334Support for XEP-0334 (Message Hints)
2020-04-16 17:26:46 +02:00
CORNIERE Rémi
6a3ee5b0a5 Support for XEP-0334 2020-04-09 10:02:11 +02:00
10 changed files with 439 additions and 34 deletions

View File

@ -27,12 +27,12 @@ jobs:
run: | run: |
go test ./... -v -race -coverprofile cover.out -covermode=atomic go test ./... -v -race -coverprofile cover.out -covermode=atomic
- name: Convert coverage to lcov - name: Convert coverage to lcov
uses: jandelgado/gcov2lcov-action@v1.0.0 uses: jandelgado/gcov2lcov-action@v1
with: with:
infile: cover.out infile: cover.out
outfile: coverage.lcov outfile: coverage.lcov
- name: Coveralls - name: Coveralls
uses: coverallsapp/github-action@v1.0.1 uses: coverallsapp/github-action@v1
with: with:
github-token: ${{ secrets.github_token }} github-token: ${{ secrets.github_token }}
path-to-lcov: coverage.lcov path-to-lcov: coverage.lcov

2
go.sum
View File

@ -201,7 +201,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -23,7 +23,7 @@ const (
type Command struct { type Command struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"` XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"`
CommandElement CommandElement CommandElements []CommandElement
BadAction *struct{} `xml:"bad-action,omitempty"` BadAction *struct{} `xml:"bad-action,omitempty"`
BadLocale *struct{} `xml:"bad-locale,omitempty"` BadLocale *struct{} `xml:"bad-locale,omitempty"`
@ -56,6 +56,8 @@ type CommandElement interface {
} }
type Actions struct { type Actions struct {
XMLName xml.Name `xml:"actions"`
Prev *struct{} `xml:"prev,omitempty"` Prev *struct{} `xml:"prev,omitempty"`
Next *struct{} `xml:"next,omitempty"` Next *struct{} `xml:"next,omitempty"`
Complete *struct{} `xml:"complete,omitempty"` Complete *struct{} `xml:"complete,omitempty"`
@ -68,6 +70,8 @@ func (a *Actions) Ref() string {
} }
type Note struct { type Note struct {
XMLName xml.Name `xml:"note"`
Text string `xml:",cdata"` Text string `xml:",cdata"`
Type string `xml:"type,attr,omitempty"` Type string `xml:"type,attr,omitempty"`
} }
@ -117,22 +121,22 @@ func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var err error var err error
switch tt.Name.Local { switch tt.Name.Local {
case "affiliations": case "actions":
a := Actions{} a := Actions{}
err = d.DecodeElement(&a, &tt) err = d.DecodeElement(&a, &tt)
c.CommandElement = &a c.CommandElements = append(c.CommandElements, &a)
case "configure": case "note":
nt := Note{} nt := Note{}
err = d.DecodeElement(&nt, &tt) err = d.DecodeElement(&nt, &tt)
c.CommandElement = &nt c.CommandElements = append(c.CommandElements, &nt)
case "x": case "x":
f := Form{} f := Form{}
err = d.DecodeElement(&f, &tt) err = d.DecodeElement(&f, &tt)
c.CommandElement = &f c.CommandElements = append(c.CommandElements, &f)
default: default:
n := Node{} n := Node{}
err = d.DecodeElement(&n, &tt) err = d.DecodeElement(&n, &tt)
c.CommandElement = &n c.CommandElements = append(c.CommandElements, &n)
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,70 @@
package stanza
import (
"errors"
"strings"
"time"
)
// Helper structures and functions to manage dates and timestamps as defined in
// XEP-0082: XMPP Date and Time Profiles (https://xmpp.org/extensions/xep-0082.html)
const dateLayoutXEP0082 = "2006-01-02"
const timeLayoutXEP0082 = "15:04:05+00:00"
var InvalidDateInput = errors.New("could not parse date. Input might not be in a supported format")
var InvalidDateOutput = errors.New("could not format date as desired")
type JabberDate struct {
value time.Time
}
func (d JabberDate) DateToString() string {
return d.value.Format(dateLayoutXEP0082)
}
func (d JabberDate) DateTimeToString(nanos bool) string {
if nanos {
return d.value.Format(time.RFC3339Nano)
}
return d.value.Format(time.RFC3339)
}
func (d JabberDate) TimeToString(nanos bool) (string, error) {
if nanos {
spl := strings.Split(d.value.Format(time.RFC3339Nano), "T")
if len(spl) != 2 {
return "", InvalidDateOutput
}
return spl[1], nil
}
spl := strings.Split(d.value.Format(time.RFC3339), "T")
if len(spl) != 2 {
return "", InvalidDateOutput
}
return spl[1], nil
}
func NewJabberDateFromString(strDate string) (JabberDate, error) {
t, err := time.Parse(time.RFC3339, strDate)
if err == nil {
return JabberDate{value: t}, nil
}
t, err = time.Parse(time.RFC3339Nano, strDate)
if err == nil {
return JabberDate{value: t}, nil
}
t, err = time.Parse(dateLayoutXEP0082, strDate)
if err == nil {
return JabberDate{value: t}, nil
}
t, err = time.Parse(timeLayoutXEP0082, strDate)
if err == nil {
return JabberDate{value: t}, nil
}
return JabberDate{}, InvalidDateInput
}

View File

@ -0,0 +1,191 @@
package stanza
import (
"testing"
"time"
)
func TestDateToString(t *testing.T) {
t1 := JabberDate{value: time.Now()}
t2 := JabberDate{value: time.Now().Add(24 * time.Hour)}
t1Str := t1.DateToString()
t2Str := t2.DateToString()
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestDateToStringOracle(t *testing.T) {
expected := "2009-11-10"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatalf(err.Error())
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str := t1.DateToString()
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestDateTimeToString(t *testing.T) {
t1 := JabberDate{value: time.Now()}
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
t1Str := t1.DateTimeToString(false)
t2Str := t2.DateTimeToString(false)
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestDateTimeToStringOracle(t *testing.T) {
expected := "2009-11-10T23:03:22+08:00"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatalf(err.Error())
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str := t1.DateTimeToString(false)
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestDateTimeToStringNanos(t *testing.T) {
t1 := JabberDate{value: time.Now()}
time.After(10 * time.Millisecond)
t2 := JabberDate{value: time.Now()}
t1Str := t1.DateTimeToString(true)
t2Str := t2.DateTimeToString(true)
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestDateTimeToStringNanosOracle(t *testing.T) {
expected := "2009-11-10T23:03:22.000000089+08:00"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatalf(err.Error())
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str := t1.DateTimeToString(true)
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestTimeToString(t *testing.T) {
t1 := JabberDate{value: time.Now()}
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
t1Str, err := t1.TimeToString(false)
if err != nil {
t.Fatalf(err.Error())
}
t2Str, err := t2.TimeToString(false)
if err != nil {
t.Fatalf(err.Error())
}
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestTimeToStringOracle(t *testing.T) {
expected := "23:03:22+08:00"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatalf(err.Error())
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str, err := t1.TimeToString(false)
if err != nil {
t.Fatalf(err.Error())
}
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestTimeToStringNanos(t *testing.T) {
t1 := JabberDate{value: time.Now()}
time.After(10 * time.Millisecond)
t2 := JabberDate{value: time.Now()}
t1Str, err := t1.TimeToString(true)
if err != nil {
t.Fatalf(err.Error())
}
t2Str, err := t2.TimeToString(true)
if err != nil {
t.Fatalf(err.Error())
}
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestTimeToStringNanosOracle(t *testing.T) {
expected := "23:03:22.000000089+08:00"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatalf(err.Error())
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str, err := t1.TimeToString(true)
if err != nil {
t.Fatalf(err.Error())
}
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestJabberDateParsing(t *testing.T) {
date := "2009-11-10"
_, err := NewJabberDateFromString(date)
if err != nil {
t.Fatalf(err.Error())
}
dateTime := "2009-11-10T23:03:22+08:00"
_, err = NewJabberDateFromString(dateTime)
if err != nil {
t.Fatalf(err.Error())
}
dateTimeNanos := "2009-11-10T23:03:22.000000089+08:00"
_, err = NewJabberDateFromString(dateTimeNanos)
if err != nil {
t.Fatalf(err.Error())
}
// TODO : fix these. Parsing a time with an offset doesn't work
//time := "23:03:22+08:00"
//_, err = NewJabberDateFromString(time)
//if err != nil {
// t.Fatalf(err.Error())
//}
//timeNanos := "23:03:22.000000089+08:00"
//_, err = NewJabberDateFromString(timeNanos)
//if err != nil {
// t.Fatalf(err.Error())
//}
}

View File

@ -51,11 +51,21 @@ func NewJid(sjid string) (*Jid, error) {
} }
func (j *Jid) Full() string { func (j *Jid) Full() string {
return j.Node + "@" + j.Domain + "/" + j.Resource if j.Resource == "" {
return j.Bare()
} else if j.Node == "" {
return j.Node + "/" + j.Resource
} else {
return j.Node + "@" + j.Domain + "/" + j.Resource
}
} }
func (j *Jid) Bare() string { func (j *Jid) Bare() string {
return j.Node + "@" + j.Domain if j.Node == "" {
return j.Domain
} else {
return j.Node + "@" + j.Domain
}
} }
// ============================================================================ // ============================================================================

View File

@ -61,26 +61,41 @@ func TestIncorrectJids(t *testing.T) {
} }
func TestFull(t *testing.T) { func TestFull(t *testing.T) {
jid := "test@domain.com/my resource" fullJids := []string{
parsedJid, err := NewJid(jid) "test@domain.com/my resource",
if err != nil { "test@domain.com",
t.Errorf("could not parse jid: %v", err) "domain.com",
} }
fullJid := parsedJid.Full() for _, sjid := range fullJids {
if fullJid != jid { parsedJid, err := NewJid(sjid)
t.Errorf("incorrect full jid: %s", fullJid) if err != nil {
t.Errorf("could not parse jid: %v", err)
}
fullJid := parsedJid.Full()
if fullJid != sjid {
t.Errorf("incorrect full jid: %s", fullJid)
}
} }
} }
func TestBare(t *testing.T) { func TestBare(t *testing.T) {
jid := "test@domain.com" tests := []struct {
fullJid := jid + "/my resource" jidstr string
parsedJid, err := NewJid(fullJid) expected string
if err != nil { }{
t.Errorf("could not parse jid: %v", err) {jidstr: "test@domain.com", expected: "test@domain.com"},
{jidstr: "test@domain.com/resource", expected: "test@domain.com"},
{jidstr: "domain.com", expected: "domain.com"},
} }
bareJid := parsedJid.Bare()
if bareJid != jid { for _, tt := range tests {
t.Errorf("incorrect bare jid: %s", bareJid) parsedJid, err := NewJid(tt.jidstr)
if err != nil {
t.Errorf("could not parse jid: %v", err)
}
bareJid := parsedJid.Bare()
if bareJid != tt.expected {
t.Errorf("incorrect bare jid: %s", bareJid)
}
} }
} }

36
stanza/msg_hint.go Normal file
View File

@ -0,0 +1,36 @@
package stanza
import "encoding/xml"
/*
Support for:
- XEP-0334: Message Processing Hints: https://xmpp.org/extensions/xep-0334.html
Pointers should be used to keep consistent with unmarshal. Eg :
msg.Extensions = append(msg.Extensions, &stanza.HintNoCopy{}, &stanza.HintStore{})
*/
type HintNoPermanentStore struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:hints no-permanent-store"`
}
type HintNoStore struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:hints no-store"`
}
type HintNoCopy struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:hints no-copy"`
}
type HintStore struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:hints store"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-permanent-store"}, HintNoPermanentStore{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-store"}, HintNoStore{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-copy"}, HintNoCopy{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "store"}, HintStore{})
}

72
stanza/msg_hint_test.go Normal file
View File

@ -0,0 +1,72 @@
package stanza_test
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
"reflect"
"strings"
"testing"
)
const msg_const = `
<message
from="romeo@montague.lit/laptop"
to="juliet@capulet.lit/laptop">
<body>V unir avtugf pybnx gb uvqr zr sebz gurve fvtug</body>
<no-copy xmlns="urn:xmpp:hints"></no-copy>
<no-permanent-store xmlns="urn:xmpp:hints"></no-permanent-store>
<no-store xmlns="urn:xmpp:hints"></no-store>
<store xmlns="urn:xmpp:hints"></store>
</message>`
func TestSerializationHint(t *testing.T) {
msg := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
msg.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
msg.Extensions = append(msg.Extensions, stanza.HintNoCopy{}, stanza.HintNoPermanentStore{}, stanza.HintNoStore{}, stanza.HintStore{})
data, _ := xml.Marshal(msg)
if strings.ReplaceAll(strings.Join(strings.Fields(msg_const), ""), "\n", "") != strings.Join(strings.Fields(string(data)), "") {
t.Fatalf("marshalled message does not match expected message")
}
}
func TestUnmarshalHints(t *testing.T) {
// Init message as in the const value
msgConst := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
msgConst.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
msgConst.Extensions = append(msgConst.Extensions, &stanza.HintNoCopy{}, &stanza.HintNoPermanentStore{}, &stanza.HintNoStore{}, &stanza.HintStore{})
// Compare message with the const value
msg := stanza.Message{}
err := xml.Unmarshal([]byte(msg_const), &msg)
if err != nil {
t.Fatal(err)
}
if msgConst.XMLName.Local != msg.XMLName.Local {
t.Fatalf("message tags do not match. Expected: %s, Actual: %s", msgConst.XMLName.Local, msg.XMLName.Local)
}
if msgConst.Body != msg.Body {
t.Fatalf("message bodies do not match. Expected: %s, Actual: %s", msgConst.Body, msg.Body)
}
if !reflect.DeepEqual(msgConst.Attrs, msg.Attrs) {
t.Fatalf("attributes do not match")
}
if !reflect.DeepEqual(msgConst.Error, msg.Error) {
t.Fatalf("attributes do not match")
}
var found bool
for _, ext := range msgConst.Extensions {
for _, strExt := range msg.Extensions {
if reflect.TypeOf(ext) == reflect.TypeOf(strExt) {
found = true
break
}
}
if !found {
t.Fatalf("extensions do not match")
}
found = false
}
}

View File

@ -237,10 +237,10 @@ func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (*IQ, erro
} }
iq.Payload = &Command{ iq.Payload = &Command{
// the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending" // the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending"
Node: "http://jabber.org/protocol/pubsub#get-pending", Node: "http://jabber.org/protocol/pubsub#get-pending",
Action: CommandActionExecute, Action: CommandActionExecute,
SessionId: sessionId, SessionId: sessionId,
CommandElement: &n, CommandElements: []CommandElement{&n},
} }
return iq, nil return iq, nil
} }
@ -353,11 +353,18 @@ func (iq *IQ) GetFormFields() (map[string]*Field, error) {
case *Command: case *Command:
fieldMap := make(map[string]*Field) fieldMap := make(map[string]*Field)
co, ok := payload.CommandElement.(*Form) var form *Form
if !ok { for _, ce := range payload.CommandElements {
fo, ok := ce.(*Form)
if ok {
form = fo
break
}
}
if form == nil {
return nil, errors.New("this IQ does not contain a command payload with a form") return nil, errors.New("this IQ does not contain a command payload with a form")
} }
for _, elt := range co.Fields { for _, elt := range form.Fields {
fieldMap[elt.Var] = elt fieldMap[elt.Var] = elt
} }
return fieldMap, nil return fieldMap, nil