Update examples
This commit is contained in:
parent
24502f7cd7
commit
3f81465c6c
|
@ -21,15 +21,15 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
router.HandleFunc("message", HandleMessage)
|
router.HandleFunc("message", handleMessage)
|
||||||
router.NewRoute().
|
router.NewRoute().
|
||||||
IQNamespaces(xmpp.NSDiscoInfo).
|
IQNamespaces(xmpp.NSDiscoInfo).
|
||||||
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
||||||
DiscoInfo(s, p, opts)
|
discoInfo(s, p, opts)
|
||||||
})
|
})
|
||||||
router.NewRoute().
|
router.NewRoute().
|
||||||
IQNamespaces("urn:xmpp:delegation:1").
|
IQNamespaces("urn:xmpp:delegation:1").
|
||||||
HandlerFunc(HandleDelegation)
|
HandlerFunc(handleDelegation)
|
||||||
|
|
||||||
component, err := xmpp.NewComponent(opts, router)
|
component, err := xmpp.NewComponent(opts, router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -43,7 +43,7 @@ func main() {
|
||||||
log.Fatal(cm.Run())
|
log.Fatal(cm.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleMessage(_ xmpp.Sender, p xmpp.Packet) {
|
func handleMessage(_ xmpp.Sender, p xmpp.Packet) {
|
||||||
msg, ok := p.(xmpp.Message)
|
msg, ok := p.(xmpp.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
@ -72,13 +72,13 @@ const (
|
||||||
// TODO: replace xmpp.Sender by ctx xmpp.Context ?
|
// TODO: replace xmpp.Sender by ctx xmpp.Context ?
|
||||||
// ctx.Stream.Send / SendRaw
|
// ctx.Stream.Send / SendRaw
|
||||||
// ctx.Opts
|
// ctx.Opts
|
||||||
func DiscoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(xmpp.IQ)
|
iq, ok := p.(xmpp.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info, ok := iq.Payload[0].(*xmpp.DiscoInfo)
|
info, ok := iq.Payload.(*xmpp.DiscoInfo)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -87,17 +87,17 @@ func DiscoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
||||||
|
|
||||||
switch info.Node {
|
switch info.Node {
|
||||||
case "":
|
case "":
|
||||||
DiscoInfoRoot(&iqResp, opts)
|
discoInfoRoot(&iqResp, opts)
|
||||||
case pubsubNode:
|
case pubsubNode:
|
||||||
DiscoInfoPubSub(&iqResp)
|
discoInfoPubSub(&iqResp)
|
||||||
case pepNode:
|
case pepNode:
|
||||||
DiscoInfoPEP(&iqResp)
|
discoInfoPEP(&iqResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DiscoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) {
|
func discoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) {
|
||||||
// Higher level discovery
|
// Higher level discovery
|
||||||
identity := xmpp.Identity{
|
identity := xmpp.Identity{
|
||||||
Name: opts.Name,
|
Name: opts.Name,
|
||||||
|
@ -117,10 +117,10 @@ func DiscoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) {
|
||||||
{Var: "urn:xmpp:delegation:1"},
|
{Var: "urn:xmpp:delegation:1"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
iqResp.AddPayload(&payload)
|
iqResp.Payload = &payload
|
||||||
}
|
}
|
||||||
|
|
||||||
func DiscoInfoPubSub(iqResp *xmpp.IQ) {
|
func discoInfoPubSub(iqResp *xmpp.IQ) {
|
||||||
payload := xmpp.DiscoInfo{
|
payload := xmpp.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: xmpp.NSDiscoInfo,
|
Space: xmpp.NSDiscoInfo,
|
||||||
|
@ -134,10 +134,10 @@ func DiscoInfoPubSub(iqResp *xmpp.IQ) {
|
||||||
{Var: "http://jabber.org/protocol/pubsub#publish-options"},
|
{Var: "http://jabber.org/protocol/pubsub#publish-options"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
iqResp.AddPayload(&payload)
|
iqResp.Payload = &payload
|
||||||
}
|
}
|
||||||
|
|
||||||
func DiscoInfoPEP(iqResp *xmpp.IQ) {
|
func discoInfoPEP(iqResp *xmpp.IQ) {
|
||||||
identity := xmpp.Identity{
|
identity := xmpp.Identity{
|
||||||
Category: "pubsub",
|
Category: "pubsub",
|
||||||
Type: "pep",
|
Type: "pep",
|
||||||
|
@ -163,18 +163,17 @@ func DiscoInfoPEP(iqResp *xmpp.IQ) {
|
||||||
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
|
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
iqResp.AddPayload(&payload)
|
iqResp.Payload = &payload
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleDelegation(s xmpp.Sender, p xmpp.Packet) {
|
func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(xmpp.IQ)
|
iq, ok := p.(xmpp.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
payload1 := iq.Payload[0]
|
delegation, ok := iq.Payload.(*xmpp.Delegation)
|
||||||
delegation, ok := payload1.(*xmpp.Delegation)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -184,12 +183,8 @@ func HandleDelegation(s xmpp.Sender, p xmpp.Packet) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload := forwardedIQ.Payload
|
|
||||||
if len(payload) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pubsub, ok := payload[0].(*xmpp.PubSub)
|
pubsub, ok := forwardedIQ.Payload.(*xmpp.PubSub)
|
||||||
if !ok {
|
if !ok {
|
||||||
// We only support pubsub delegation
|
// We only support pubsub delegation
|
||||||
return
|
return
|
||||||
|
@ -204,7 +199,7 @@ func HandleDelegation(s xmpp.Sender, p xmpp.Packet) {
|
||||||
Local: "pubsub",
|
Local: "pubsub",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
iqResp.AddPayload(&payload)
|
iqResp.Payload = &payload
|
||||||
// Wrap the reply in delegation 'forward'
|
// Wrap the reply in delegation 'forward'
|
||||||
iqForward := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
|
iqForward := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
|
||||||
delegPayload := xmpp.Delegation{
|
delegPayload := xmpp.Delegation{
|
||||||
|
@ -220,7 +215,7 @@ func HandleDelegation(s xmpp.Sender, p xmpp.Packet) {
|
||||||
Stanza: iqResp,
|
Stanza: iqResp,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
iqForward.AddPayload(&delegPayload)
|
iqForward.Payload = &delegPayload
|
||||||
_ = s.Send(iqForward)
|
_ = s.Send(iqForward)
|
||||||
// TODO: The component should actually broadcast the mood to subscribers
|
// TODO: The component should actually broadcast the mood to subscribers
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,18 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
router.HandleFunc("message", HandleMessage)
|
router.HandleFunc("message", handleMessage)
|
||||||
router.NewRoute().
|
router.NewRoute().
|
||||||
IQNamespaces(xmpp.NSDiscoInfo).
|
IQNamespaces(xmpp.NSDiscoInfo).
|
||||||
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
||||||
DiscoInfo(s, p, opts)
|
discoInfo(s, p, opts)
|
||||||
})
|
})
|
||||||
router.NewRoute().
|
router.NewRoute().
|
||||||
IQNamespaces(xmpp.NSDiscoItems).
|
IQNamespaces(xmpp.NSDiscoItems).
|
||||||
HandlerFunc(DiscoItems)
|
HandlerFunc(discoItems)
|
||||||
router.NewRoute().
|
router.NewRoute().
|
||||||
IQNamespaces("jabber:iq:version").
|
IQNamespaces("jabber:iq:version").
|
||||||
HandlerFunc(HandleVersion)
|
HandlerFunc(handleVersion)
|
||||||
|
|
||||||
component, err := xmpp.NewComponent(opts, router)
|
component, err := xmpp.NewComponent(opts, router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -44,7 +44,7 @@ func main() {
|
||||||
log.Fatal(cm.Run())
|
log.Fatal(cm.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleMessage(_ xmpp.Sender, p xmpp.Packet) {
|
func handleMessage(_ xmpp.Sender, p xmpp.Packet) {
|
||||||
msg, ok := p.(xmpp.Message)
|
msg, ok := p.(xmpp.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
@ -52,7 +52,7 @@ func HandleMessage(_ xmpp.Sender, p xmpp.Packet) {
|
||||||
fmt.Println("Received message:", msg.Body)
|
fmt.Println("Received message:", msg.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DiscoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(xmpp.IQ)
|
iq, ok := p.(xmpp.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -78,22 +78,19 @@ func DiscoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
||||||
{Var: "urn:xmpp:delegation:1"},
|
{Var: "urn:xmpp:delegation:1"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
iqResp.AddPayload(&payload)
|
iqResp.Payload = &payload
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle iq error responses
|
// TODO: Handle iq error responses
|
||||||
func DiscoItems(c xmpp.Sender, p xmpp.Packet) {
|
func discoItems(c xmpp.Sender, p xmpp.Packet) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(xmpp.IQ)
|
iq, ok := p.(xmpp.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(iq.Payload) == 0 {
|
discoItems, ok := iq.Payload.(*xmpp.DiscoItems)
|
||||||
return
|
|
||||||
}
|
|
||||||
discoItems, ok := iq.Payload[0].(*xmpp.DiscoItems)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -112,11 +109,11 @@ func DiscoItems(c xmpp.Sender, p xmpp.Packet) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iqResp.AddPayload(&payload)
|
iqResp.Payload = &payload
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleVersion(c xmpp.Sender, p xmpp.Packet) {
|
func handleVersion(c xmpp.Sender, p xmpp.Packet) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(xmpp.IQ)
|
iq, ok := p.(xmpp.IQ)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -127,6 +124,6 @@ func HandleVersion(c xmpp.Sender, p xmpp.Packet) {
|
||||||
var payload xmpp.Version
|
var payload xmpp.Version
|
||||||
payload.Name = "Fluux XMPP Component"
|
payload.Name = "Fluux XMPP Component"
|
||||||
payload.Version = "0.0.1"
|
payload.Version = "0.0.1"
|
||||||
iq.AddPayload(&payload)
|
iq.Payload = &payload
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
router.HandleFunc("message", HandleMessage)
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
client, err := xmpp.NewClient(config, router)
|
client, err := xmpp.NewClient(config, router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -35,7 +35,7 @@ func main() {
|
||||||
log.Fatal(cm.Run())
|
log.Fatal(cm.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleMessage(s xmpp.Sender, p xmpp.Packet) {
|
func handleMessage(s xmpp.Sender, p xmpp.Packet) {
|
||||||
msg, ok := p.(xmpp.Message)
|
msg, ok := p.(xmpp.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
|
|
@ -23,45 +23,61 @@ func main() {
|
||||||
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
|
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
var client *xmpp.Client
|
// 1. Create mpg player
|
||||||
var err error
|
player, err := mpg123.NewPlayer()
|
||||||
if client, err = connectXmpp(*jid, *password, *address); err != nil {
|
|
||||||
log.Fatal("Could not connect to XMPP: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := mpg123.NewPlayer()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterator to receive packets coming from our XMPP connection
|
// 2. Prepare XMPP client
|
||||||
for packet := range client.Recv() {
|
config := xmpp.Config{
|
||||||
|
Address: *address,
|
||||||
switch packet := packet.(type) {
|
Jid: *jid,
|
||||||
case xmpp.Message:
|
Password: *password,
|
||||||
processMessage(client, p, &packet)
|
Insecure: true,
|
||||||
case xmpp.IQ:
|
|
||||||
processIq(client, p, &packet)
|
|
||||||
case xmpp.Presence:
|
|
||||||
// Do nothing with received presence
|
|
||||||
default:
|
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.NewRoute().
|
||||||
|
Packet("message").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
||||||
|
handleMessage(s, p, player)
|
||||||
|
})
|
||||||
|
router.NewRoute().
|
||||||
|
Packet("message").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
||||||
|
handleIQ(s, p, player)
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(config, router)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
func processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.Message) {
|
func handleMessage(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
|
||||||
command := strings.Trim(packet.Body, " ")
|
msg, ok := p.(xmpp.Message)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command := strings.Trim(msg.Body, " ")
|
||||||
if command == "stop" {
|
if command == "stop" {
|
||||||
p.Stop()
|
player.Stop()
|
||||||
} else {
|
} else {
|
||||||
playSCURL(p, command)
|
playSCURL(player, command)
|
||||||
sendUserTune(client, "Radiohead", "Spectre")
|
sendUserTune(s, "Radiohead", "Spectre")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) {
|
func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
|
||||||
switch payload := packet.Payload[0].(type) {
|
iq, ok := p.(xmpp.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch payload := iq.Payload.(type) {
|
||||||
// We support IOT Control IQ
|
// We support IOT Control IQ
|
||||||
case *xmpp.ControlSet:
|
case *xmpp.ControlSet:
|
||||||
var url string
|
var url string
|
||||||
|
@ -72,24 +88,24 @@ func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playSCURL(p, url)
|
playSCURL(player, url)
|
||||||
setResponse := new(xmpp.ControlSetResponse)
|
setResponse := new(xmpp.ControlSetResponse)
|
||||||
// FIXME: Broken
|
// FIXME: Broken
|
||||||
reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: packet.From, Type: "result", Id: packet.Id}, Payload: []xmpp.IQPayload{setResponse}}
|
reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
|
||||||
_ = client.Send(reply)
|
_ = s.Send(reply)
|
||||||
// TODO add Soundclound artist / title retrieval
|
// TODO add Soundclound artist / title retrieval
|
||||||
sendUserTune(client, "Radiohead", "Spectre")
|
sendUserTune(s, "Radiohead", "Spectre")
|
||||||
default:
|
default:
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", packet.Payload)
|
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", iq.Payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendUserTune(client *xmpp.Client, artist string, title string) {
|
func sendUserTune(s xmpp.Sender, artist string, title string) {
|
||||||
tune := xmpp.Tune{Artist: artist, Title: title}
|
tune := xmpp.Tune{Artist: artist, Title: title}
|
||||||
iq := xmpp.NewIQ("set", "", "", "usertune-1", "en")
|
iq := xmpp.NewIQ("set", "", "", "usertune-1", "en")
|
||||||
payload := xmpp.PubSub{Publish: &xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: &tune}}}
|
payload := xmpp.PubSub{Publish: &xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: &tune}}}
|
||||||
iq.AddPayload(&payload)
|
iq.Payload = &payload
|
||||||
_ = client.Send(iq)
|
_ = s.Send(iq)
|
||||||
}
|
}
|
||||||
|
|
||||||
func playSCURL(p *mpg123.Player, rawURL string) {
|
func playSCURL(p *mpg123.Player, rawURL string) {
|
||||||
|
@ -100,20 +116,6 @@ func playSCURL(p *mpg123.Player, rawURL string) {
|
||||||
_ = p.Play(url)
|
_ = p.Play(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectXmpp(jid string, password string, address string) (client *xmpp.Client, err error) {
|
|
||||||
xmppConfig := xmpp.Config{Address: address,
|
|
||||||
Jid: jid, Password: password, PacketLogger: os.Stdout, Insecure: true}
|
|
||||||
|
|
||||||
if client, err = xmpp.NewClient(xmppConfig); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Connect(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// - Have a player API to play, play next, or add to queue
|
// - Have a player API to play, play next, or add to queue
|
||||||
// - Have the ability to parse custom packet to play sound
|
// - Have the ability to parse custom packet to play sound
|
||||||
|
|
Loading…
Reference in a new issue