~samwhited/xmpp

xmpp/xtime/time.go -rw-r--r-- 4.0 KiB
70cfc50dSam Whited internal/pubsub: fix publish payload 5 hours 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
144
145
146
147
148
149
150
151
152
// 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.

//go:generate go run ../internal/genfeature -receiver "h Handler"

// 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
}

// 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}},
	)
}

// 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())
}

// 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
}

// MarshalXMLAttr implements xml.MarshalerAttr.
func (t Time) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
	return xml.Attr{
		Name:  name,
		Value: time.Time(t.Time).UTC().Format(time.RFC3339Nano),
	}, nil
}

// UnmarshalXMLAttr implements xml.UnmarshalerAttr.
func (t *Time) UnmarshalXMLAttr(attr xml.Attr) error {
	var err error
	t.Time, err = time.Parse(time.RFC3339, attr.Value)
	return err
}

// 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) {
	data := Time{}
	err := s.UnmarshalIQ(
		ctx,
		stanza.IQ{
			Type: stanza.GetIQ,
			To:   to,
		}.Wrap(xmlstream.Wrap(nil, xml.StartElement{Name: xml.Name{Local: "time", Space: NS}})),
		&data,
	)
	return data.Time, err
}

// 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
}