~samwhited/xmpp

d68b809926a4a70f2effe9e0461564f4b6576797 — Sam Whited 2 years ago 93053ec disco
disco: add XEP-0030 support

[ci skip]
5 files changed, 783 insertions(+), 0 deletions(-)

A disco/categories.go
A disco/disco.go
A disco/disco_test.go
A disco/gen.go
A disco/option.go
A disco/categories.go => disco/categories.go +325 -0
@@ 0,0 1,325 @@
// Code generated by running "go generate" in mellium.im/xmpp/disco. DO NOT EDIT.

package disco

// Predefined identities generated from the Service Discovery Identities
// Registry as registered with the XMPP Registrar.
var (
	// Category: The "account" category is to be used by a server when responding to a disco request sent to the bare JID (user@host addresss) of an account hosted by the server.
	// Type: The user@host is an administrative account
	AdminAccount func(name, lang string) Option = newIdentity("account", "admin")

	// Category: The "account" category is to be used by a server when responding to a disco request sent to the bare JID (user@host addresss) of an account hosted by the server.
	// Type: The user@host is a "guest" account that allows anonymous login by any user
	AnonymousAccount func(name, lang string) Option = newIdentity("account", "anonymous")

	// Category: The "account" category is to be used by a server when responding to a disco request sent to the bare JID (user@host addresss) of an account hosted by the server.
	// Type: The user@host is a registered or provisioned account associated with a particular non-administrative user
	RegisteredAccount func(name, lang string) Option = newIdentity("account", "registered")

	// Category: The "auth" category consists of server components that provide authentication services within a server implementation.
	// Type: A server component that authenticates based on external certificates
	CertAuth func(name, lang string) Option = newIdentity("auth", "cert")

	// Category: The "auth" category consists of server components that provide authentication services within a server implementation.
	// Type: A server authentication component other than one of the registered types
	GenericAuth func(name, lang string) Option = newIdentity("auth", "generic")

	// Category: The "auth" category consists of server components that provide authentication services within a server implementation.
	// Type: A server component that authenticates against an LDAP database
	LDAPAuth func(name, lang string) Option = newIdentity("auth", "ldap")

	// Category: The "auth" category consists of server components that provide authentication services within a server implementation.
	// Type: A server component that authenticates against an NT domain
	NTLMAuth func(name, lang string) Option = newIdentity("auth", "ntlm")

	// Category: The "auth" category consists of server components that provide authentication services within a server implementation.
	// Type: A server component that authenticates against a PAM system
	PAMAuth func(name, lang string) Option = newIdentity("auth", "pam")

	// Category: The "auth" category consists of server components that provide authentication services within a server implementation.
	// Type: A server component that authenticates against a Radius system
	RadiusAuth func(name, lang string) Option = newIdentity("auth", "radius")

	// Category: The "automation" category consists of entities and nodes that provide automated or programmed interaction.
	// Type: The node for a list of commands; valid only for the node "http://jabber.org/protocol/commands"
	CommandListAutomation func(name, lang string) Option = newIdentity("automation", "command-list")

	// Category: The "automation" category consists of entities and nodes that provide automated or programmed interaction.
	// Type: A node for a specific command; the "node" attribute uniquely identifies the command
	CommandNodeAutomation func(name, lang string) Option = newIdentity("automation", "command-node")

	// Category: The "automation" category consists of entities and nodes that provide automated or programmed interaction.
	// Type: An entity that supports Jabber-RPC.
	RpcAutomation func(name, lang string) Option = newIdentity("automation", "rpc")

	// Category: The "automation" category consists of entities and nodes that provide automated or programmed interaction.
	// Type: An entity that supports the SOAP XMPP Binding.
	SOAPAutomation func(name, lang string) Option = newIdentity("automation", "soap")

	// Category: The "automation" category consists of entities and nodes that provide automated or programmed interaction.
	// Type: An entity that provides automated translation services.
	TranslationAutomation func(name, lang string) Option = newIdentity("automation", "translation")

	// Category: The "client" category consists of different types of clients, mostly for instant messaging.
	// Type: An automated client that is not controlled by a human user
	BotClient func(name, lang string) Option = newIdentity("client", "bot")

	// Category: The "client" category consists of different types of clients, mostly for instant messaging.
	// Type: Minimal non-GUI client used on dumb terminals or text-only screens
	ConsoleClient func(name, lang string) Option = newIdentity("client", "console")

	// Category: The "client" category consists of different types of clients, mostly for instant messaging.
	// Type: A client running on a gaming console
	GameClient func(name, lang string) Option = newIdentity("client", "game")

	// Category: The "client" category consists of different types of clients, mostly for instant messaging.
	// Type: A client running on a PDA, RIM device, or other handheld
	HandheldClient func(name, lang string) Option = newIdentity("client", "handheld")

	// Category: The "client" category consists of different types of clients, mostly for instant messaging.
	// Type: Standard full-GUI client used on desktops and laptops
	PCClient func(name, lang string) Option = newIdentity("client", "pc")

	// Category: The "client" category consists of different types of clients, mostly for instant messaging.
	// Type: A client running on a mobile phone or other telephony device
	PhoneClient func(name, lang string) Option = newIdentity("client", "phone")

	// Category: The "client" category consists of different types of clients, mostly for instant messaging.
	// Type: A client that is not actually using an instant messaging client; however, messages sent to this contact will be delivered as Short Message Service (SMS) messages
	SMSClient func(name, lang string) Option = newIdentity("client", "sms")

	// Category: The "client" category consists of different types of clients, mostly for instant messaging.
	// Type: A client operated from within a web browser
	WebClient func(name, lang string) Option = newIdentity("client", "web")

	// Category: The "collaboration" category consists of services that enable multiple individuals to work together in real time.
	// Type: Multi-user whiteboarding service
	WhiteboardCollaboration func(name, lang string) Option = newIdentity("collaboration", "whiteboard")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that archives traffic
	ArchiveComponent func(name, lang string) Option = newIdentity("component", "archive")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that handles client connections
	C2SComponent func(name, lang string) Option = newIdentity("component", "c2s")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component other than one of the registered types
	GenericComponent func(name, lang string) Option = newIdentity("component", "generic")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that handles load balancing
	LoadComponent func(name, lang string) Option = newIdentity("component", "load")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that logs server information
	LogComponent func(name, lang string) Option = newIdentity("component", "log")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that provides presence information
	PresenceComponent func(name, lang string) Option = newIdentity("component", "presence")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that handles core routing logic
	RouterComponent func(name, lang string) Option = newIdentity("component", "router")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that handles server connections
	S2SComponent func(name, lang string) Option = newIdentity("component", "s2s")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that manages user sessions
	SMComponent func(name, lang string) Option = newIdentity("component", "sm")

	// Category: The "component" category consists of services that are internal to server implementations and not normally exposed outside a server.
	// Type: A server component that provides server statistics
	StatsComponent func(name, lang string) Option = newIdentity("component", "stats")

	// Category: The "conference" category consists of online conference services such as multi-user chatroom services.
	// Type: Internet Relay Chat service
	IRCConference func(name, lang string) Option = newIdentity("conference", "irc")

	// Category: The "conference" category consists of online conference services such as multi-user chatroom services.
	// Type: Text conferencing service
	TextConference func(name, lang string) Option = newIdentity("conference", "text")

	// Category: The "directory" category consists of information retrieval services that enable users to search online directories or otherwise be informed about the existence of other XMPP entities.
	// Type: A directory of chatrooms
	ChatroomDirectory func(name, lang string) Option = newIdentity("directory", "chatroom")

	// Category: The "directory" category consists of information retrieval services that enable users to search online directories or otherwise be informed about the existence of other XMPP entities.
	// Type: A directory that provides shared roster groups
	GroupDirectory func(name, lang string) Option = newIdentity("directory", "group")

	// Category: The "directory" category consists of information retrieval services that enable users to search online directories or otherwise be informed about the existence of other XMPP entities.
	// Type: A directory of end users (e.g., JUD)
	UserDirectory func(name, lang string) Option = newIdentity("directory", "user")

	// Category: The "directory" category consists of information retrieval services that enable users to search online directories or otherwise be informed about the existence of other XMPP entities.
	// Type: A directory of waiting list entries
	WaitinglistDirectory func(name, lang string) Option = newIdentity("directory", "waitinglist")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to AOL Instant Messenger
	AIMGateway func(name, lang string) Option = newIdentity("gateway", "aim")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the Facebook IM service
	FacebookGateway func(name, lang string) Option = newIdentity("gateway", "facebook")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the Gadu-Gadu IM service
	GaduGaduGateway func(name, lang string) Option = newIdentity("gateway", "gadu-gadu")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway that provides HTTP Web Services access
	HTTPWSGateway func(name, lang string) Option = newIdentity("gateway", "http-ws")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to ICQ
	ICQGateway func(name, lang string) Option = newIdentity("gateway", "icq")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to IRC
	IRCGateway func(name, lang string) Option = newIdentity("gateway", "irc")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to Microsoft Live Communications Server
	LCSGateway func(name, lang string) Option = newIdentity("gateway", "lcs")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the mail.ru IM service
	MRIMGateway func(name, lang string) Option = newIdentity("gateway", "mrim")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to MSN Messenger
	MSNGateway func(name, lang string) Option = newIdentity("gateway", "msn")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the MySpace IM service
	MyspaceimGateway func(name, lang string) Option = newIdentity("gateway", "myspaceim")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to Microsoft Office Communications Server
	OCSGateway func(name, lang string) Option = newIdentity("gateway", "ocs")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the QQ IM service
	QQGateway func(name, lang string) Option = newIdentity("gateway", "qq")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to IBM Lotus Sametime
	SametimeGateway func(name, lang string) Option = newIdentity("gateway", "sametime")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to SIP for Instant Messaging and Presence Leveraging Extensions (SIMPLE)
	SimpleGateway func(name, lang string) Option = newIdentity("gateway", "simple")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the Skype service
	SkypeGateway func(name, lang string) Option = newIdentity("gateway", "skype")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to Short Message Service
	SMSGateway func(name, lang string) Option = newIdentity("gateway", "sms")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the SMTP (email) network
	SMTPGateway func(name, lang string) Option = newIdentity("gateway", "smtp")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the Tlen IM service
	TlenGateway func(name, lang string) Option = newIdentity("gateway", "tlen")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to the Xfire gaming and IM service
	XfireGateway func(name, lang string) Option = newIdentity("gateway", "xfire")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to another XMPP service (NOT via native server-to-server communication)
	XMPPGateway func(name, lang string) Option = newIdentity("gateway", "xmpp")

	// Category: The "gateway" category consists of translators between Jabber/XMPP services and non-XMPP services.
	// Type: Gateway to Yahoo! Instant Messenger
	YahooGateway func(name, lang string) Option = newIdentity("gateway", "yahoo")

	// Category: The "headline" category consists of services that provide real-time news or information (often but not necessarily in a message of type "headline").
	// Type: Service that notifies a user of new email messages.
	NewmailHeadline func(name, lang string) Option = newIdentity("headline", "newmail")

	// Category: The "headline" category consists of services that provide real-time news or information (often but not necessarily in a message of type "headline").
	// Type: RSS notification service.
	RSSHeadline func(name, lang string) Option = newIdentity("headline", "rss")

	// Category: The "headline" category consists of services that provide real-time news or information (often but not necessarily in a message of type "headline").
	// Type: Service that provides weather alerts.
	WeatherHeadline func(name, lang string) Option = newIdentity("headline", "weather")

	// Category: The "hierarchy" category is used to describe nodes within a hierarchy of nodes; the "branch" and "leaf" types are exhaustive.
	// Type: A service discovery node that contains further nodes in the hierarchy.
	BranchHierarchy func(name, lang string) Option = newIdentity("hierarchy", "branch")

	// Category: The "hierarchy" category is used to describe nodes within a hierarchy of nodes; the "branch" and "leaf" types are exhaustive.
	// Type: A service discovery node that does not contain further nodes in the hierarchy.
	LeafHierarchy func(name, lang string) Option = newIdentity("hierarchy", "leaf")

	// Category: The "proxy" category consists of servers or services that act as special-purpose proxies or intermediaries between two or more XMPP endpoints.
	// Type: SOCKS5 bytestreams proxy service
	BytestreamsProxy func(name, lang string) Option = newIdentity("proxy", "bytestreams")

	// Category: Services and nodes that adhere to XEP-0060.
	// Type: A pubsub node of the "collection" type.
	CollectionPubsub func(name, lang string) Option = newIdentity("pubsub", "collection")

	// Category: Services and nodes that adhere to XEP-0060.
	// Type: A pubsub node of the "leaf" type.
	LeafPubsub func(name, lang string) Option = newIdentity("pubsub", "leaf")

	// Category: Services and nodes that adhere to XEP-0060.
	// Type: A personal eventing service that supports the publish-subscribe subset defined in XEP-0163.
	PEPPubsub func(name, lang string) Option = newIdentity("pubsub", "pep")

	// Category: Services and nodes that adhere to XEP-0060.
	// Type: A pubsub service that supports the functionality defined in XEP-0060.
	ServicePubsub func(name, lang string) Option = newIdentity("pubsub", "service")

	// Category: The "server" category consists of any Jabber/XMPP server.
	// Type: Standard Jabber/XMPP server used for instant messaging and presence
	IMServer func(name, lang string) Option = newIdentity("server", "im")

	// Category: The "store" category consists of internal server components that provide data storage and retrieval services.
	// Type: A server component that stores data in a Berkeley database
	BerkeleyStore func(name, lang string) Option = newIdentity("store", "berkeley")

	// Category: The "store" category consists of internal server components that provide data storage and retrieval services.
	// Type: A server component that stores data on the file system
	FileStore func(name, lang string) Option = newIdentity("store", "file")

	// Category: The "store" category consists of internal server components that provide data storage and retrieval services.
	// Type: A server data storage component other than one of the registered types
	GenericStore func(name, lang string) Option = newIdentity("store", "generic")

	// Category: The "store" category consists of internal server components that provide data storage and retrieval services.
	// Type: A server component that stores data in an LDAP database
	LDAPStore func(name, lang string) Option = newIdentity("store", "ldap")

	// Category: The "store" category consists of internal server components that provide data storage and retrieval services.
	// Type: A server component that stores data in a MySQL database
	MysqlStore func(name, lang string) Option = newIdentity("store", "mysql")

	// Category: The "store" category consists of internal server components that provide data storage and retrieval services.
	// Type: A server component that stores data in an Oracle database
	OracleStore func(name, lang string) Option = newIdentity("store", "oracle")

	// Category: The "store" category consists of internal server components that provide data storage and retrieval services.
	// Type: A server component that stores data in a PostgreSQL database
	PostgresStore func(name, lang string) Option = newIdentity("store", "postgres")
)

func newIdentity(cat, typ string) func(string, string) Option {
	return func(name, lang string) Option {
		return Identity(cat, typ, name, lang)
	}
}

A disco/disco.go => disco/disco.go +107 -0
@@ 0,0 1,107 @@
// Copyright 2017 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 disco implements XEP-0030: Service Discovery.
package disco

//go:generate go run gen.go

import (
	"encoding/xml"
	"errors"

	"mellium.im/xmlstream"
	"mellium.im/xmpp/internal/ns"
)

// Namespaces used by this package.
const (
	NSInfo  = `http://jabber.org/protocol/disco#info`
	NSItems = `http://jabber.org/protocol/disco#items`
)

type identity struct {
	Category string
	Type     string
	XMLLang  string
}

// A Registry is used to register features supported by a server.
type Registry struct {
	identities map[identity]string
	features   map[string]struct{}
}

// NewRegistry creates a new feature registry with the provided identities and
// features.
// If multiple identities are specified, the name of the registry will be used
// for all of them.
func NewRegistry(options ...Option) *Registry {
	registry := &Registry{
		features: map[string]struct{}{
			NSInfo:  struct{}{},
			NSItems: struct{}{},
		},
	}
	for _, o := range options {
		o(registry)
	}
	return registry
}

// HandleXMPP handles disco info requests.
func (r *Registry) HandleXMPP(t xmlstream.TokenReadWriter, start *xml.StartElement) error {
	// TODO: Handle the IQ and IQ semantics, not the payload.
	switch {
	case r == nil:
		return NewRegistry().HandleXMPP(t, start)
	case start == nil || start.Name.Local != "query" || start.Name.Space != NSInfo:
		return errors.New("disco: bad info query payload")
	}

	if err := xmlstream.Skip(t); err != nil {
		return err
	}

	resp := xml.StartElement{
		Name: xml.Name{Space: NSInfo, Local: "query"},
	}
	if err := t.EncodeToken(resp); err != nil {
		return err
	}

	for feature := range r.features {
		start := xml.StartElement{
			Name: xml.Name{Local: "feature"},
			Attr: []xml.Attr{
				{Name: xml.Name{Local: "var"}, Value: feature},
			},
		}
		if err := t.EncodeToken(start); err != nil {
			return err
		}
		if err := t.EncodeToken(start.End()); err != nil {
			return err
		}
	}
	for ident, name := range r.identities {
		start := xml.StartElement{
			Name: xml.Name{Local: "identity"},
			Attr: []xml.Attr{
				{Name: xml.Name{Local: "category"}, Value: ident.Category},
				{Name: xml.Name{Local: "type"}, Value: ident.Type},
				{Name: xml.Name{Local: "name"}, Value: name},
				{Name: xml.Name{Space: ns.XML, Local: "lang"}, Value: ident.XMLLang},
			},
		}
		if err := t.EncodeToken(start); err != nil {
			return err
		}
		if err := t.EncodeToken(start.End()); err != nil {
			return err
		}
	}

	return t.EncodeToken(resp.End())
}

A disco/disco_test.go => disco/disco_test.go +136 -0
@@ 0,0 1,136 @@
// Copyright 2017 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 disco_test

import (
	"bytes"
	"encoding/xml"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"testing"

	"mellium.im/xmlstream"
	"mellium.im/xmpp/disco"
)

type Feature struct {
	Var string `xml:"var,attr"`
}

type Ident struct {
	Cat  string `xml:"category,attr"`
	Type string `xml:"type,attr"`
	Name string `xml:"name,attr"`
}

type Query struct {
	XMLName  xml.Name  `xml:"http://jabber.org/protocol/disco#info query"`
	Feature  []Feature `xml:"feature"`
	Identity []Ident   `xml:"identity"`
}

var registryTests = [...]struct {
	r   *disco.Registry
	q   Query
	err error
}{
	0: {
		q: Query{
			Feature: []Feature{
				{Var: disco.NSInfo},
				{Var: disco.NSItems},
			},
		},
	},
	1: {
		r: disco.NewRegistry(),
		q: Query{
			Feature: []Feature{
				{Var: disco.NSInfo},
				{Var: disco.NSItems},
			},
		},
	},
	2: {
		r: disco.NewRegistry(disco.Feature(disco.NSInfo), disco.Feature("porticulus")),
		q: Query{
			Feature: []Feature{
				{Var: disco.NSInfo},
				{Var: disco.NSItems},
				{Var: "porticulus"},
			},
		},
	},
	3: {
		r: disco.NewRegistry(disco.Feature(disco.NSItems), disco.AdminAccount("my service", "en"), disco.Feature("porticulus")),
		q: Query{
			Identity: []Ident{
				{"account", "admin", "my service"},
			},
			Feature: []Feature{
				{Var: disco.NSInfo},
				{Var: disco.NSItems},
				{Var: "porticulus"},
			},
		},
	},
}

func TestDisco(t *testing.T) {
	for i, tc := range registryTests {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			d := xml.NewDecoder(strings.NewReader(`<query xmlns='http://jabber.org/protocol/disco#info'/>`))
			tok, _ := d.Token()
			start := tok.(xml.StartElement)
			buf := new(bytes.Buffer)
			e := xml.NewEncoder(buf)

			err := tc.r.HandleXMPP(struct {
				xml.TokenReader
				xmlstream.TokenWriter
			}{
				TokenReader: d,
				TokenWriter: e,
			}, &start)
			if err != tc.err {
				t.Fatalf("Unexpected error: want=`%v', got=`%v'", tc.err, err)
			}
			if err := e.Flush(); err != nil {
				t.Fatalf("Unexpected error while flushing: `%v'", err)
			}

			q := Query{}
			err = xml.Unmarshal(buf.Bytes(), &q)
			if err != nil {
				t.Fatalf("Unexpected error: `%v'", err)
			}

			// Clear the name so we don't have to set it in the test cases.
			q.XMLName = xml.Name{}

			sort.Slice(q.Feature, func(i, j int) bool {
				return q.Feature[i].Var < q.Feature[j].Var
			})
			sort.Slice(tc.q.Feature, func(i, j int) bool {
				return tc.q.Feature[i].Var < tc.q.Feature[j].Var
			})
			if !reflect.DeepEqual(q.Feature, tc.q.Feature) {
				t.Errorf("Features list did not match: want=`%+v', got=`%+v'", tc.q, q)
			}

			sort.Slice(q.Identity, func(i, j int) bool {
				return q.Identity[i].Cat+q.Identity[i].Type+q.Identity[i].Name < q.Identity[j].Cat+q.Identity[j].Type+q.Identity[j].Name
			})
			sort.Slice(tc.q.Identity, func(i, j int) bool {
				return tc.q.Identity[i].Cat+tc.q.Identity[i].Type+tc.q.Identity[i].Name < tc.q.Identity[j].Cat+tc.q.Identity[j].Type+tc.q.Identity[j].Name
			})
			if !reflect.DeepEqual(q.Identity, tc.q.Identity) {
				t.Errorf("Identity list did not match: want=`%+v', got=`%+v'", tc.q, q)
			}
		})
	}
}

A disco/gen.go => disco/gen.go +169 -0
@@ 0,0 1,169 @@
// Copyright 2017 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

// +build ignore

// The gen command creates disco identity options from the disco-categories
// registry located at https://xmpp.org/registrar/disco-categories.html.
package main

import (
	"bytes"
	"encoding/xml"
	"flag"
	"go/format"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"text/template"
)

type registry struct {
	XMLName    xml.Name `xml:"registry"`
	Categories []struct {
		Name string `xml:"name"`
		Desc string `xml:"desc"`
		Type []struct {
			Name    string `xml:"name"`
			XMLLang string `xml:"http://www.w3.org/XML/1998/namespace lang"`
			Desc    string `xml:"desc"`
			Doc     string `xml:"doc"`
		} `xml:"type"`
	} `xml:"category"`
}

func main() {
	var (
		catURL  = `https://xmpp.org/registrar/disco-categories.xml`
		tmpDir  = os.TempDir()
		outPath = `./`
	)
	flag.StringVar(&catURL, "categories", catURL, "A link to the disco-categories registry")
	flag.StringVar(&tmpDir, "tmp", tmpDir, "A temporary directory to downlaod files to")
	flag.StringVar(&outPath, "out", outPath, "A directory to output Go files to")
	flag.Parse()

	ident := func(s string) string {
		return strings.Map(func(r rune) rune {
			switch r {
			case ' ', '-', '_':
				return rune(-1)
			}
			return r
		}, s)
	}

	tmpl := template.Must(template.New("categories").Funcs(map[string]interface{}{
		"ident": ident,
		"export": func(s string) string {
			s = strings.Title(s)
			// Manual tweaks to make the output more idiomatic Go.
			for _, cap := range []string{
				"Rss", "Xmpp", "Smtp", "Sms", "Qq", "Msn", "Lcs", "Irc", "Icq", "Aim",
				"S2s", "C2s", "Sm", "Pc", "Soap", "Pam", "Ntlm", "Ldap", "Http-Ws",
				"Mrim", "Ocs", "Pep", "Im",
			} {
				if strings.HasPrefix(s, cap) {
					s = strings.Replace(s, cap, strings.ToUpper(cap), 1)
					break
				}
			}
			return ident(s)
		},
	}).Parse(`// Code generated by running "go generate" in mellium.im/xmpp/disco. DO NOT EDIT.

package disco

// Predefined identities generated from the Service Discovery Identities
// Registry as registered with the XMPP Registrar.
var (
{{- range $cat := .Categories}}{{range .Type}}
	// Category: {{$cat.Desc}}
	// Type: {{.Desc}}
	{{export .Name}}{{export  $cat.Name}} func(name, lang string) Option = newIdentity({{printf "%q" $cat.Name}}, {{printf "%q" .Name}})
{{end}}{{end}}
)

func newIdentity(cat, typ string) func(string, string) Option {
	return func(name, lang string) Option {
		return Identity(cat, typ, name, lang)
	}
}
`))

	logger := log.New(os.Stderr, "", log.LstdFlags)
	if err := genFile(catURL, tmpDir, filepath.Join(outPath, "categories.go"), tmpl); err != nil {
		logger.Fatal(err)
	}
}

func genFile(regURL, tmpDir, outFile string, tmpl *template.Template) error {
	fd, err := openOrDownload(regURL, tmpDir)
	if err != nil {
		return err
	}
	defer fd.Close()

	out, err := os.Create(outFile)
	if err != nil {
		return err
	}
	defer fd.Close()

	reg := registry{}
	d := xml.NewDecoder(fd)
	if err = d.Decode(&reg); err != nil {
		return err
	}

	buf := new(bytes.Buffer)
	if err = tmpl.Execute(buf, reg); err != nil {
		return err
	}
	b, err := format.Source(buf.Bytes())
	if err != nil {
		return err
	}
	_, err = io.Copy(out, bytes.NewReader(b))
	return err
}

type writeCloser struct {
	io.Writer
	closer func() error
}

func (f writeCloser) Close() error {
	return f.closer()
}

// opens the provided registry URL (downloading it if it doesn't exist).
func openOrDownload(catURL, tmpDir string) (*os.File, error) {
	registryXML := filepath.Join(tmpDir, filepath.Base(catURL))
	fd, err := os.Open(registryXML)
	if err != nil {
		fd, err = os.Create(registryXML)
		if err != nil {
			return nil, err
		}
		// If we couldn't open it for reading, attempt to download it.
		resp, err := http.Get(catURL)
		if err != nil {
			return nil, err
		}
		_, err = io.Copy(fd, resp.Body)
		if err != nil {
			return nil, err
		}
		resp.Body.Close()
		_, err = fd.Seek(0, 0)
		if err != nil {
			return nil, err
		}
	}
	return fd, err
}

A disco/option.go => disco/option.go +46 -0
@@ 0,0 1,46 @@
// Copyright 2017 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 disco

// An Option is used to configure new registries.
type Option func(*Registry)

// Identity adds an identity to the registry.
//
// Identities are described by XEP-0030:
//
//     An entity's identity is broken down into its category (server, client,
//     gateway, directory, etc.) and its particular type within that category
//     (IM server, phone vs. handheld client, MSN gateway vs. AIM gateway, user
//     directory vs. chatroom directory, etc.). This information helps
//     requesting entities to determine the group or "bucket" of services into
//     which the entity is most appropriately placed (e.g., perhaps the entity
//     is shown in a GUI with an appropriate icon).
func Identity(category, typ, name, lang string) Option {
	return func(r *Registry) {
		if r.identities == nil {
			r.identities = make(map[identity]string)
		}
		r.identities[identity{
			Category: category,
			Type:     typ,
			XMLLang:  lang,
		}] = name
	}
}

// Feature adds a feature to the registry.
//
// Features are described by XEP-0030:
//
//     This information helps requesting entities determine what actions are
//     possible with regard to this entity (registration, search, join, etc.),
//     what protocols the entity supports, and specific feature types of
//     interest, if any (e.g., for the purpose of feature negotiation).
func Feature(name string) Option {
	return func(r *Registry) {
		r.features[name] = struct{}{}
	}
}