~samwhited/xmpp

ref: 467e8c4a0b977d765eadf64f02ebb5edee0ce805 xmpp/xtime/time.go -rw-r--r-- 3.8 KiB
467e8c4aSam Whited component: add integration tests 1 year, 5 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// Copyright 2020 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

// Package xtime implements time related XMPP functionality.
//
// In particular, this package implements XEP-0202: Entity Time and XEP-0082:
// XMPP Date and Time Profiles.
package xtime // import "mellium.im/xmpp/xtime"

import (
	"context"
	"encoding/xml"
	"time"

	"mellium.im/xmlstream"
	"mellium.im/xmpp"
	"mellium.im/xmpp/jid"
	"mellium.im/xmpp/mux"
	"mellium.im/xmpp/stanza"
)

const (
	// NS is the XML namespace used by XMPP entity time requests.
	// It is provided as a convenience.
	NS = "urn:xmpp:time"

	// LegacyDateTime implements the legacy profile mentioned in XEP-0082.
	//
	// Unless you are implementing an older XEP that specifically calls for this
	// format, time.RFC3339 should be used instead.
	LegacyDateTime = "20060102T15:04:05"
)

const tzd = "Z07:00"

// Time is like a time.Time but it can be marshaled as an XEP-0202 time payload.
type Time struct {
	XMLName xml.Name `xml:"urn:xmpp:time time"`
	time.Time
}

// WriteXML satisfies the xmlstream.WriterTo interface.
// It is like MarshalXML except it writes tokens to w.
func (t Time) WriteXML(w xmlstream.TokenWriter) (n int, err error) {
	return xmlstream.Copy(w, t.TokenReader())
}

// TokenReader satisfies the xmlstream.Marshaler interface.
func (t Time) TokenReader() xml.TokenReader {
	tt := time.Time(t.Time)
	tzo := tt.Format(tzd)
	utcTime := tt.UTC().Format(time.RFC3339Nano)

	return xmlstream.Wrap(
		xmlstream.MultiReader(
			xmlstream.Wrap(xmlstream.Token(xml.CharData(tzo)), xml.StartElement{Name: xml.Name{Local: "tzo"}}),
			xmlstream.Wrap(xmlstream.Token(xml.CharData(utcTime)), xml.StartElement{Name: xml.Name{Local: "utc"}}),
		),
		xml.StartElement{Name: xml.Name{Local: "time", Space: NS}},
	)
}

// MarshalXML implements xml.Marshaler.
func (t Time) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
	_, err := t.WriteXML(e)
	return err
}

// UnmarshalXML implements xml.Unmarshaler.
func (t *Time) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	data := struct {
		XMLName xml.Name `xml:"urn:xmpp:time time"`

		Timezone string `xml:"tzo"`
		UTC      string `xml:"utc"`
	}{}
	err := d.DecodeElement(&data, &start)
	if err != nil {
		return err
	}

	zone, err := time.Parse(tzd, data.Timezone)
	if err != nil {
		return err
	}
	utcTime, err := time.Parse(time.RFC3339, data.UTC)
	if err != nil {
		return err
	}
	t.Time = utcTime.In(zone.Location())
	return nil
}

// Get sends a request to the provided JID asking for its time.
func Get(ctx context.Context, s *xmpp.Session, to jid.JID) (time.Time, error) {
	result, err := s.SendIQ(ctx, stanza.IQ{
		Type: stanza.GetIQ,
		To:   to,
	}.Wrap(xmlstream.Wrap(nil, xml.StartElement{Name: xml.Name{Local: "time", Space: NS}})))
	var t time.Time
	if err != nil {
		return t, err
	}
	d := xml.NewTokenDecoder(result)
	data := struct {
		stanza.IQ
		Time Time
	}{}
	err = d.Decode(&data)
	if err != nil {
		return t, err
	}
	return time.Time(data.Time.Time), nil
}

// Handle returns an option that registers a Handler for entity time requests.
func Handle(h Handler) mux.Option {
	return mux.IQ(stanza.GetIQ, xml.Name{Local: "time", Space: NS}, h)
}

// Handler responds to requests for our time.
// If timeFunc is nil, time.Now is used.
type Handler struct {
	TimeFunc func() time.Time
}

// HandleIQ responds to entity time requests.
func (h Handler) HandleIQ(iq stanza.IQ, t xmlstream.TokenReadEncoder, start *xml.StartElement) error {
	if iq.Type != stanza.GetIQ || start.Name.Local != "time" || start.Name.Space != NS {
		return nil
	}

	var tt Time
	if h.TimeFunc == nil {
		tt = Time{Time: time.Now()}
	} else {
		tt = Time{Time: h.TimeFunc()}
	}

	_, err := xmlstream.Copy(t, iq.Result(tt.TokenReader()))
	return err
}