Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34b885a770 | ||
|
|
7a875f84a5 | ||
|
|
a6e74ec808 | ||
|
|
2717137270 | ||
|
|
58ae55e061 | ||
|
|
eebb6906c6 | ||
|
|
0179d614b9 | ||
|
|
2794adaa21 | ||
|
|
37266ab8d4 | ||
|
|
74f5761e06 | ||
|
|
546234eacf | ||
|
|
7a822e8eaf | ||
|
|
b07f92853d | ||
|
|
3bb74eb672 | ||
|
|
8cf0f84ddc | ||
|
|
006b43a590 | ||
|
|
120b0c2647 | ||
|
|
a3702a900c | ||
|
|
ede81d551c | ||
|
|
7c19abb6ae | ||
|
|
8f93dde120 | ||
|
|
ca7b82dab5 | ||
|
|
fce8df9e84 | ||
|
|
1597f99eef | ||
|
|
9ad48ff310 | ||
|
|
af0ec78eae | ||
|
|
5f95df147f | ||
|
|
98cd855349 | ||
|
|
1aa14f9fd5 | ||
|
|
39cdd5e6c1 | ||
|
|
9bbefbcea8 | ||
|
|
9593e7c041 | ||
|
|
82aa2abfbd | ||
|
|
58c7165ce5 | ||
|
|
5bcf0eda0b | ||
|
|
0bfb140dc7 | ||
|
|
7b63799f0b | ||
|
|
a97c42323c | ||
|
|
fc0ed5ac2c | ||
|
|
69994d9856 | ||
|
|
843687ff2b | ||
|
|
7416aa37e0 | ||
|
|
ce83000e0c |
No files matched your search
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
an XMPP client
|
an XMPP client
|
||||||
|
|
||||||
|
Experimental XMPP client written in Go, with the GTK4 UI toolkit and the Fluux IO XMPP libraries.
|
||||||
|
|
||||||
icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im))
|
icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im))
|
||||||
|
|
||||||
additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons)
|
additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) and the [Haiku project](https://www.haiku-os.org/)
|
||||||
|
|
||||||
|
pepper & carrot default avatar is from [Pepper&Carrot](https://peppercarrot.com) - created by David Revoy, licensed under Creative Commons Attribution 4.0. Change made: cropped and made black & white
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
# TODO
|
# TODO
|
||||||
- XEP-0393: Message Styling 0%
|
|
||||||
- XEP-0402: PEP Native Bookmarks 50% (often lags out client if enabled)
|
|
||||||
- XEP-0461: Message Replies 0%
|
- XEP-0461: Message Replies 0%
|
||||||
- XEP-0444: Message Reactions partial
|
- XEP-0444: Message Reactions partial
|
||||||
|
|
||||||
@@ -2,333 +2,309 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/base64"
|
|
||||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||||
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
|
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed debug.png
|
// If you want to change the default profile picture used when Identicons are disabled, you can change the below line to an image in the repository and recompile Lambda.
|
||||||
|
|
||||||
|
//go:embed debug_psi.png
|
||||||
var defaultAvatarBytes []byte
|
var defaultAvatarBytes []byte
|
||||||
var defaultAvatarB64 string = base64.StdEncoding.EncodeToString(defaultAvatarBytes)
|
|
||||||
|
|
||||||
//go:embed failed_load.png
|
//go:embed failed_load.png
|
||||||
var failedBytes []byte
|
var failedBytes []byte
|
||||||
var failedB64 string = base64.StdEncoding.EncodeToString(failedBytes)
|
|
||||||
|
|
||||||
//go:embed assets/owner.png
|
//go:embed assets/owner.png
|
||||||
var ownerMedalBytes []byte
|
var ownerMedalBytes []byte
|
||||||
var ownerMedalB64 string = base64.StdEncoding.EncodeToString(ownerMedalBytes)
|
|
||||||
|
|
||||||
//go:embed assets/admin.png
|
//go:embed assets/admin.png
|
||||||
var adminMedalBytes []byte
|
var adminMedalBytes []byte
|
||||||
var adminMedalB64 string = base64.StdEncoding.EncodeToString(adminMedalBytes)
|
|
||||||
|
|
||||||
//go:embed assets/member.png
|
//go:embed assets/member.png
|
||||||
var memberMedalBytes []byte
|
var memberMedalBytes []byte
|
||||||
var memberMedalB64 string = base64.StdEncoding.EncodeToString(memberMedalBytes)
|
|
||||||
|
|
||||||
//go:embed assets/noaffiliation.png
|
//go:embed assets/noaffiliation.png
|
||||||
var noneMedalBytes []byte
|
var noneMedalBytes []byte
|
||||||
var noneMedalB64 string = base64.StdEncoding.EncodeToString(noneMedalBytes)
|
|
||||||
|
|
||||||
//go:embed assets/outcast.png
|
//go:embed assets/outcast.png
|
||||||
var outcastMedalBytes []byte
|
var outcastMedalBytes []byte
|
||||||
var outcastMedalB64 string = base64.StdEncoding.EncodeToString(outcastMedalBytes)
|
|
||||||
|
|
||||||
//go:embed assets/cancel.png
|
//go:embed assets/cancel.png
|
||||||
var cancelBytes []byte
|
var cancelBytes []byte
|
||||||
var cancelB64 string = base64.StdEncoding.EncodeToString(cancelBytes)
|
|
||||||
|
|
||||||
//go:embed assets/status_away.png
|
//go:embed assets/status_away.png
|
||||||
var sABytes []byte
|
var sABytes []byte
|
||||||
var sAB64 string = base64.StdEncoding.EncodeToString(sABytes)
|
|
||||||
|
|
||||||
//go:embed assets/status_busy.png
|
//go:embed assets/status_busy.png
|
||||||
var sBBytes []byte
|
var sBBytes []byte
|
||||||
var sBB64 string = base64.StdEncoding.EncodeToString(sBBytes)
|
|
||||||
|
|
||||||
//go:embed assets/status_chatty.png
|
//go:embed assets/status_chatty.png
|
||||||
var sCBytes []byte
|
var sCBytes []byte
|
||||||
var sCB64 string = base64.StdEncoding.EncodeToString(sCBytes)
|
|
||||||
|
|
||||||
//go:embed assets/status_online.png
|
//go:embed assets/status_online.png
|
||||||
var sOBytes []byte
|
var sOBytes []byte
|
||||||
var sOB64 string = base64.StdEncoding.EncodeToString(sOBytes)
|
|
||||||
|
|
||||||
//go:embed assets/status_xa.png
|
//go:embed assets/status_xa.png
|
||||||
var xaBytes []byte
|
var xaBytes []byte
|
||||||
var xaB64 string = base64.StdEncoding.EncodeToString(xaBytes)
|
|
||||||
|
|
||||||
//go:embed assets/tag.png
|
//go:embed assets/tag.png
|
||||||
var tagBytes []byte
|
var tagBytes []byte
|
||||||
var tagB64 string = base64.StdEncoding.EncodeToString(tagBytes)
|
|
||||||
|
|
||||||
//go:embed assets/lambda-disabled.png
|
//go:embed assets/lambda-disabled.png
|
||||||
var logoDisabledBytes []byte
|
var logoDisabledBytes []byte
|
||||||
var logoDisabledB64 string = base64.StdEncoding.EncodeToString(logoDisabledBytes)
|
|
||||||
|
//go:embed assets/icon.png
|
||||||
|
var logoBytes []byte
|
||||||
|
|
||||||
//go:embed assets/group.png
|
//go:embed assets/group.png
|
||||||
var groupBytes []byte
|
var groupBytes []byte
|
||||||
var groupB64 string = base64.StdEncoding.EncodeToString(groupBytes)
|
|
||||||
|
|
||||||
//go:embed assets/door_in.png
|
//go:embed assets/door_in.png
|
||||||
var doorInBytes []byte
|
var doorInBytes []byte
|
||||||
var doorInB64 string = base64.StdEncoding.EncodeToString(doorInBytes)
|
|
||||||
|
|
||||||
//go:embed assets/door_out.png
|
//go:embed assets/door_out.png
|
||||||
var doorOutBytes []byte
|
var doorOutBytes []byte
|
||||||
var doorOutB64 string = base64.StdEncoding.EncodeToString(doorOutBytes)
|
|
||||||
|
|
||||||
//go:embed assets/large_group.png
|
//go:embed assets/large_group.png
|
||||||
var largeGroupBytes []byte
|
var largeGroupBytes []byte
|
||||||
var largeGroupB64 string = base64.StdEncoding.EncodeToString(largeGroupBytes)
|
|
||||||
|
|
||||||
//go:embed assets/world.png
|
//go:embed assets/world.png
|
||||||
var worldBytes []byte
|
var worldBytes []byte
|
||||||
var worldB64 string = base64.StdEncoding.EncodeToString(worldBytes)
|
|
||||||
|
|
||||||
//go:embed assets/disconnect.png
|
//go:embed assets/disconnect.png
|
||||||
var disconnectBytes []byte
|
var disconnectBytes []byte
|
||||||
var disconnectB64 string = base64.StdEncoding.EncodeToString(disconnectBytes)
|
|
||||||
|
|
||||||
//go:embed assets/chart_bar.png
|
//go:embed assets/chart_bar.png
|
||||||
var barBytes []byte
|
var barBytes []byte
|
||||||
var barB64 string = base64.StdEncoding.EncodeToString(barBytes)
|
|
||||||
|
//go:embed assets/chart_bar_laggy.png
|
||||||
|
var barLaggyBytes []byte
|
||||||
|
|
||||||
//go:embed assets/ok.png
|
//go:embed assets/ok.png
|
||||||
var okBytes []byte
|
var okBytes []byte
|
||||||
var okB64 string = base64.StdEncoding.EncodeToString(okBytes)
|
|
||||||
|
|
||||||
//go:embed assets/hourglass.png
|
//go:embed assets/hourglass.png
|
||||||
var hourglassBytes []byte
|
var hourglassBytes []byte
|
||||||
var hourglassB64 string = base64.StdEncoding.EncodeToString(hourglassBytes)
|
|
||||||
|
|
||||||
//go:embed assets/connect_tls.png
|
//go:embed assets/connect_tls.png
|
||||||
var connectBytes []byte
|
var connectBytes []byte
|
||||||
var connectB64 string = base64.StdEncoding.EncodeToString(connectBytes)
|
|
||||||
|
|
||||||
//go:embed assets/comment.png
|
//go:embed assets/comment.png
|
||||||
var commentBytes []byte
|
var commentBytes []byte
|
||||||
var commentB64 string = base64.StdEncoding.EncodeToString(commentBytes)
|
|
||||||
|
|
||||||
//go:embed assets/information.png
|
//go:embed assets/information.png
|
||||||
var informationBytes []byte
|
var informationBytes []byte
|
||||||
var informationB64 string = base64.StdEncoding.EncodeToString(informationBytes)
|
|
||||||
|
//go:embed assets/car.png
|
||||||
|
var carBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/car_high.png
|
||||||
|
var carHighBytes []byte
|
||||||
|
|
||||||
|
// muc icons
|
||||||
|
|
||||||
|
//go:embed assets/muc_open.png
|
||||||
|
var mucOpenBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_membersonly.png
|
||||||
|
var mucMembersOnlyBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_passwordprotected.png
|
||||||
|
var mucPasswordProtectedBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_unsecured.png
|
||||||
|
var mucUnsecuredBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_hidden.png
|
||||||
|
var mucHiddenBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_public.png
|
||||||
|
var mucPublicBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_unmoderated.png
|
||||||
|
var mucUnmoderatedBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_moderated.png
|
||||||
|
var mucModeratedBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_nonanonymous.png
|
||||||
|
var mucNonAnonymousBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_semianonymous.png
|
||||||
|
var mucSemiAnonymousBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_persistent.png
|
||||||
|
var mucPersistentBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/muc_temporary.png
|
||||||
|
var mucTemporaryBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/moderate.png
|
||||||
|
var moderateBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/jabber.png
|
||||||
|
var jabberBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/fail.png
|
||||||
|
var failBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/please_wait.gif
|
||||||
|
var pleaseWaitBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/update.png
|
||||||
|
var updateBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/solar.png
|
||||||
|
var solarBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/timeline_marker.png
|
||||||
|
var timelineMarkerBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/cap.png
|
||||||
|
var capIconBytes []byte
|
||||||
|
|
||||||
|
// client icons
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/monocles.png
|
||||||
|
var monoclesCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/gajim.png
|
||||||
|
var gajimCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/conversations.png
|
||||||
|
var conversationsCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/dino.png
|
||||||
|
var dinoCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/blabber.png
|
||||||
|
var blabberCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/profanity.png
|
||||||
|
var profanityCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/poezio.png
|
||||||
|
var poezioCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/mcabber.png
|
||||||
|
var mcabberCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/converse.png
|
||||||
|
var converseJSCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/pidgin.png
|
||||||
|
var pidginCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/anotherim.png
|
||||||
|
var anotherImCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/movim.png
|
||||||
|
var movimCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/yaxim.png
|
||||||
|
var yaximCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/psi.png
|
||||||
|
var psiCapBytes []byte
|
||||||
|
|
||||||
|
//go:embed assets/client_icons/spark.png
|
||||||
|
var sparkCapBytes []byte
|
||||||
|
|
||||||
|
var clientAssets sync.Map
|
||||||
|
|
||||||
|
func loadAsset(key string, data []byte) {
|
||||||
|
loader := gdkpixbuf.NewPixbufLoader()
|
||||||
|
loader.Write(data)
|
||||||
|
loader.Close()
|
||||||
|
clientAssets.Store(key, gdk.NewTextureForPixbuf(loader.Pixbuf()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientAssetsLoad(key string) gdk.Paintabler {
|
||||||
|
dat, ok := clientAssets.Load(key)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
paintable, ok := dat.(gdk.Paintabler)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return paintable
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
for key, data := range map[string][]byte{
|
||||||
|
"logo": logoBytes,
|
||||||
|
"DefaultAvatar": defaultAvatarBytes,
|
||||||
|
"FailedAvatar": failedBytes,
|
||||||
|
"owner": ownerMedalBytes,
|
||||||
|
"admin": adminMedalBytes,
|
||||||
|
"member": memberMedalBytes,
|
||||||
|
"none": noneMedalBytes,
|
||||||
|
"outcast": outcastMedalBytes,
|
||||||
|
"cancel": cancelBytes,
|
||||||
|
"tag": tagBytes,
|
||||||
|
"disabled_logo": logoDisabledBytes,
|
||||||
|
"group": groupBytes,
|
||||||
|
"door_in": doorInBytes,
|
||||||
|
"door_out": doorOutBytes,
|
||||||
|
"large_group": largeGroupBytes,
|
||||||
|
"world": worldBytes,
|
||||||
|
"disconnect": disconnectBytes,
|
||||||
|
"chart_bar": barBytes,
|
||||||
|
"chart_bar_laggy": barLaggyBytes,
|
||||||
|
"ok": okBytes,
|
||||||
|
"hourglass": hourglassBytes,
|
||||||
|
"connect": connectBytes,
|
||||||
|
"comment": commentBytes,
|
||||||
|
"information": informationBytes,
|
||||||
|
"status_away": sABytes,
|
||||||
|
"status_dnd": sBBytes,
|
||||||
|
"status_chat": sCBytes,
|
||||||
|
"status_xa": xaBytes,
|
||||||
|
"status_": sOBytes,
|
||||||
|
"car": carBytes,
|
||||||
|
"car_high": carHighBytes,
|
||||||
|
"muc_open": mucOpenBytes,
|
||||||
|
"muc_membersonly": mucMembersOnlyBytes,
|
||||||
|
"muc_passwordprotected": mucPasswordProtectedBytes,
|
||||||
|
"muc_unsecured": mucUnsecuredBytes,
|
||||||
|
"muc_hidden": mucHiddenBytes,
|
||||||
|
"muc_public": mucPublicBytes,
|
||||||
|
"muc_unmoderated": mucUnmoderatedBytes,
|
||||||
|
"muc_moderated": mucModeratedBytes,
|
||||||
|
"muc_nonanonymous": mucNonAnonymousBytes,
|
||||||
|
"muc_semianonymous": mucSemiAnonymousBytes,
|
||||||
|
"muc_persistent": mucPersistentBytes,
|
||||||
|
"muc_temporary": mucTemporaryBytes,
|
||||||
|
"moderate": moderateBytes,
|
||||||
|
"jabber": jabberBytes,
|
||||||
|
"fail": failBytes,
|
||||||
|
"please_wait": pleaseWaitBytes,
|
||||||
|
"update": updateBytes,
|
||||||
|
"solar": solarBytes,
|
||||||
|
"timeline_marker": timelineMarkerBytes,
|
||||||
|
"cap": capIconBytes,
|
||||||
|
|
||||||
|
"https://monocles.eu": monoclesCapBytes,
|
||||||
|
"https://gajim.org": gajimCapBytes,
|
||||||
|
"http://conversations.im": conversationsCapBytes,
|
||||||
|
"https://dino.im": dinoCapBytes,
|
||||||
|
"http://blabber.im": blabberCapBytes,
|
||||||
|
"http://profanity-im.github.io": profanityCapBytes,
|
||||||
|
"https://poez.io": poezioCapBytes,
|
||||||
|
"http://mcabber.com/caps": mcabberCapBytes,
|
||||||
|
"https://conversejs.org": converseJSCapBytes,
|
||||||
|
"http://pidgin.im/": pidginCapBytes,
|
||||||
|
"https://dev.narayana.im/narayana/Conversations-Classic": anotherImCapBytes,
|
||||||
|
"https://movim.eu/": movimCapBytes,
|
||||||
|
"https://yaxim.org/": yaximCapBytes,
|
||||||
|
"https://psi-im.org": psiCapBytes,
|
||||||
|
"https://igniterealtime.org/projects/smack": sparkCapBytes,
|
||||||
|
} {
|
||||||
|
loadAsset(key, data)
|
||||||
|
}
|
||||||
|
|
||||||
loader := gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
defaultAvatarData, _ := base64.StdEncoding.DecodeString(defaultAvatarB64)
|
|
||||||
loader.Write(defaultAvatarData)
|
|
||||||
loader.Close()
|
|
||||||
clientAssets["DefaultAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
failedData, _ := base64.StdEncoding.DecodeString(failedB64)
|
|
||||||
loader.Write(failedData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["FailedAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
ownerMedalData, _ := base64.StdEncoding.DecodeString(ownerMedalB64)
|
|
||||||
loader.Write(ownerMedalData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["owner"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
cancelData, _ := base64.StdEncoding.DecodeString(cancelB64)
|
|
||||||
loader.Write(cancelData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["cancel"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
tagData, _ := base64.StdEncoding.DecodeString(tagB64)
|
|
||||||
loader.Write(tagData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["tag"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
adminMedalData, _ := base64.StdEncoding.DecodeString(adminMedalB64)
|
|
||||||
loader.Write(adminMedalData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["admin"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
memberMedalData, _ := base64.StdEncoding.DecodeString(memberMedalB64)
|
|
||||||
loader.Write(memberMedalData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["member"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
noneMedalData, _ := base64.StdEncoding.DecodeString(noneMedalB64)
|
|
||||||
loader.Write(noneMedalData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["none"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
outcastMedalData, _ := base64.StdEncoding.DecodeString(outcastMedalB64)
|
|
||||||
loader.Write(outcastMedalData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["outcast"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
disabledLogoData, _ := base64.StdEncoding.DecodeString(logoDisabledB64)
|
|
||||||
loader.Write(disabledLogoData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["disabled_logo"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
groupData, _ := base64.StdEncoding.DecodeString(groupB64)
|
|
||||||
loader.Write(groupData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["group"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
doorInData, _ := base64.StdEncoding.DecodeString(doorInB64)
|
|
||||||
loader.Write(doorInData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["door_in"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
doorOutData, _ := base64.StdEncoding.DecodeString(doorOutB64)
|
|
||||||
loader.Write(doorOutData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["door_out"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
largeGroupData, _ := base64.StdEncoding.DecodeString(largeGroupB64)
|
|
||||||
loader.Write(largeGroupData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["large_group"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
worldData, _ := base64.StdEncoding.DecodeString(worldB64)
|
|
||||||
loader.Write(worldData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["world"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
disconnectData, _ := base64.StdEncoding.DecodeString(disconnectB64)
|
|
||||||
loader.Write(disconnectData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["disconnect"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
barData, _ := base64.StdEncoding.DecodeString(barB64)
|
|
||||||
loader.Write(barData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["chart_bar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
okData, _ := base64.StdEncoding.DecodeString(okB64)
|
|
||||||
loader.Write(okData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["ok"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
hourglassData, _ := base64.StdEncoding.DecodeString(hourglassB64)
|
|
||||||
loader.Write(hourglassData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["hourglass"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
connectData, _ := base64.StdEncoding.DecodeString(connectB64)
|
|
||||||
loader.Write(connectData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["connect"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
commentData, _ := base64.StdEncoding.DecodeString(commentB64)
|
|
||||||
loader.Write(commentData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["comment"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
informationData, _ := base64.StdEncoding.DecodeString(informationB64)
|
|
||||||
loader.Write(informationData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["information"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
sAData, _ := base64.StdEncoding.DecodeString(sAB64)
|
|
||||||
loader.Write(sAData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["status_away"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
sBData, _ := base64.StdEncoding.DecodeString(sBB64)
|
|
||||||
loader.Write(sBData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["status_dnd"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
sCData, _ := base64.StdEncoding.DecodeString(sCB64)
|
|
||||||
loader.Write(sCData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["status_chat"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
xaData, _ := base64.StdEncoding.DecodeString(xaB64)
|
|
||||||
loader.Write(xaData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["status_xa"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
|
|
||||||
loader = gdkpixbuf.NewPixbufLoader()
|
|
||||||
|
|
||||||
sOData, _ := base64.StdEncoding.DecodeString(sOB64)
|
|
||||||
loader.Write(sOData)
|
|
||||||
loader.Close()
|
|
||||||
|
|
||||||
clientAssets["status_"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 287 B |
|
After Width: | Height: | Size: 671 B |
|
After Width: | Height: | Size: 656 B |
|
After Width: | Height: | Size: 456 B |
|
After Width: | Height: | Size: 641 B |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 783 B |
|
After Width: | Height: | Size: 196 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 246 B |
|
After Width: | Height: | Size: 551 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 362 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 588 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 263 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 793 B |
|
After Width: | Height: | Size: 593 B |
|
After Width: | Height: | Size: 935 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 970 B |
|
After Width: | Height: | Size: 508 B |
|
After Width: | Height: | Size: 612 B |
|
After Width: | Height: | Size: 806 B |
|
After Width: | Height: | Size: 622 B |
|
After Width: | Height: | Size: 924 B |
|
After Width: | Height: | Size: 882 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 543 B |
|
Before Width: | Height: | Size: 666 B After Width: | Height: | Size: 910 B |
|
After Width: | Height: | Size: 327 B |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -5,6 +5,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||||
@@ -13,13 +14,17 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// global or app-level map/cache
|
// global or app-level map/cache
|
||||||
var textureCache = make(map[string]gdk.Paintabler)
|
// var textureCache = make(map[string]gdk.Paintabler)
|
||||||
|
var textureCache sync.Map
|
||||||
|
|
||||||
// Invalid images, if an image/avatar cannot be loaded on the system (e.g: incompatible format) it's put here
|
// Invalid images, if an image/avatar cannot be loaded on the system (e.g: incompatible format) it's put here
|
||||||
var invalidImages = make(map[string]bool)
|
// var invalidImages = make(map[string]bool)
|
||||||
|
|
||||||
|
var invalidImages sync.Map
|
||||||
|
|
||||||
func ensureCache() (string, error) {
|
func ensureCache() (string, error) {
|
||||||
cachePath := configdir.LocalCache("lambda-im")
|
cachePath := configdir.LocalCache("lambda-im")
|
||||||
@@ -31,49 +36,70 @@ func ensureCache() (string, error) {
|
|||||||
return cachePath, nil
|
return cachePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTexture(path string) gdk.Paintabler {
|
func getTexture(path string) (gdk.Paintabler, error) {
|
||||||
if tex, exists := textureCache[path]; exists {
|
tex, exists := textureCache.Load(path)
|
||||||
return tex
|
if exists {
|
||||||
|
return tex.(gdk.Paintabler), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tex, err := gdk.NewTextureFromFilename(path) // load once
|
tex, err := gdk.NewTextureFromFilename(path) // load once
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
textureCache[path] = tex
|
textureCache.Store(path, tex)
|
||||||
return tex
|
t, ok := tex.(gdk.Paintabler)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid type")
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPictureFromPath(path string) *gtk.Picture {
|
func newPictureFromPath(path string) (*gtk.Picture, error) {
|
||||||
tex := getTexture(path)
|
tex, err := getTexture(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
img := gtk.NewPictureForPaintable(tex)
|
img := gtk.NewPictureForPaintable(tex)
|
||||||
return img
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newImageFromPath(path string) *gtk.Image {
|
func newImageFromPath(path string) (*gtk.Image, error) {
|
||||||
tex := getTexture(path)
|
tex, err := getTexture(path)
|
||||||
img := gtk.NewImageFromPaintable(tex)
|
if err != nil {
|
||||||
return img
|
return nil, err
|
||||||
|
}
|
||||||
|
return gtk.NewImageFromPaintable(tex), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPictureFromWeb(url string) *gtk.Picture {
|
func setImageFromPath(path string, i *gtk.Image) error {
|
||||||
|
tex, err := getTexture(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.SetFromPaintable(tex)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPictureFromWeb(url string) (*gtk.Picture, error) {
|
||||||
pa, _ := ensureCache()
|
pa, _ := ensureCache()
|
||||||
// step 1: get a sha256 sum of the URL
|
// step 1: get a sha256 sum of the URL
|
||||||
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
|
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
|
||||||
|
|
||||||
p, ok := textureCache[sum]
|
p, ok := textureCache.Load(sum)
|
||||||
if ok {
|
if ok {
|
||||||
return gtk.NewPictureForPaintable(p)
|
return gtk.NewPictureForPaintable(p.(gdk.Paintabler)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// step 2: download it
|
// step 2: download it
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := io.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fullpath := filepath.Join(pa, sum)
|
fullpath := filepath.Join(pa, sum)
|
||||||
@@ -81,31 +107,31 @@ func newPictureFromWeb(url string) *gtk.Picture {
|
|||||||
// step 3: save it
|
// step 3: save it
|
||||||
err = os.WriteFile(fullpath, b, 0644)
|
err = os.WriteFile(fullpath, b, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPictureFromPath(fullpath)
|
return newPictureFromPath(fullpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newImageFromWeb(url string) *gtk.Image {
|
func newImageFromWeb(url string) (*gtk.Image, error) {
|
||||||
pa, _ := ensureCache()
|
pa, _ := ensureCache()
|
||||||
// step 1: get a sha256 sum of the URL
|
// step 1: get a sha256 sum of the URL
|
||||||
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
|
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
|
||||||
|
|
||||||
p, ok := textureCache[sum]
|
p, ok := textureCache.Load(sum)
|
||||||
if ok {
|
if ok {
|
||||||
return gtk.NewImageFromPaintable(p)
|
return gtk.NewImageFromPaintable(p.(gdk.Paintabler)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// step 2: download it
|
// step 2: download it
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := io.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fullpath := filepath.Join(pa, sum)
|
fullpath := filepath.Join(pa, sum)
|
||||||
@@ -113,7 +139,7 @@ func newImageFromWeb(url string) *gtk.Image {
|
|||||||
// step 3: save it
|
// step 3: save it
|
||||||
err = os.WriteFile(fullpath, b, 0644)
|
err = os.WriteFile(fullpath, b, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newImageFromPath(fullpath)
|
return newImageFromPath(fullpath)
|
||||||
|
|||||||
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
name: Run tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- .github/workflows/test.yaml
|
||||||
|
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.*'
|
||||||
|
- .github/workflows/test.yaml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.25
|
||||||
|
uses: actions/setup-go@v1
|
||||||
|
with:
|
||||||
|
go-version: 1.25
|
||||||
|
id: go
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
go test ./... -v -race -coverprofile cover.out -covermode=atomic
|
||||||
|
- name: Convert coverage to lcov
|
||||||
|
uses: jandelgado/gcov2lcov-action@v1
|
||||||
|
with:
|
||||||
|
infile: cover.out
|
||||||
|
outfile: coverage.lcov
|
||||||
|
- name: Coveralls
|
||||||
|
uses: coverallsapp/github-action@v1
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.github_token }}
|
||||||
|
path-to-lcov: coverage.lcov
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
coverage.out
|
||||||
|
coverage.txt
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Do not commit codeship key
|
||||||
|
codeship.aes
|
||||||
|
codeship.env
|
||||||
|
|
||||||
|
priv/
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Fluux XMPP Changelog
|
||||||
|
|
||||||
|
## v0.5.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Added support for XEP-0198 (Stream management)
|
||||||
|
- Added message queue : when using "SendX" methods on a client, messages are also stored in a queue. When requesting
|
||||||
|
acks from the server, sent messages will be discarded, and unsent ones will be sent again. (see https://xmpp.org/extensions/xep-0198.html#acking)
|
||||||
|
- Added support for stanza_errors (see https://xmpp.org/rfcs/rfc3920.html#def C.2. Stream error namespace and https://xmpp.org/rfcs/rfc6120.html#schemas-streamerror)
|
||||||
|
- Added separate hooks for connection and reconnection on the client. One can now specify different actions to get triggered on client connect
|
||||||
|
and reconnect, at client init time.
|
||||||
|
- Client state update is now thread safe
|
||||||
|
- Changed the Config struct to use pointer semantics
|
||||||
|
- Tests
|
||||||
|
- Refactoring, including removing some Fprintf statements in favor of Marshal + Write and using structs from the library
|
||||||
|
instead of strings
|
||||||
|
|
||||||
|
## v0.4.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Added support for XEP-0060 (PubSub)
|
||||||
|
(no support for 6.5.4 Returning Some Items yet as it needs XEP-0059, Result Sets)
|
||||||
|
- Added support for XEP-0050 (Commands)
|
||||||
|
- Added support for XEP-0004 (Forms)
|
||||||
|
- Updated the client example with a TUI
|
||||||
|
- Make keepalive interval configurable #134
|
||||||
|
- Fix updating of EventManager.CurrentState #136
|
||||||
|
- Added callbacks for error management in Component and Client. Users must now provide a callback function when using NewClient/Component.
|
||||||
|
- Moved JID from xmpp package to stanza package
|
||||||
|
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Update requirements to go1.13
|
||||||
|
- Add a websocket transport
|
||||||
|
- Add Client.SendIQ method
|
||||||
|
- Add IQ result routes to the Router
|
||||||
|
- Fix SIGSEGV in xmpp_component (#126)
|
||||||
|
- Add tests for Component and code style fixes
|
||||||
|
|
||||||
|
## v0.2.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- XMPP Over Websocket support
|
||||||
|
- Add support for getting IQ responses to client IQ queries (synchronously or asynchronously, passing an handler
|
||||||
|
function).
|
||||||
|
- Implement X-OAUTH2 authentication method. You can read more details here:
|
||||||
|
[Understanding ejabberd OAuth Support & Roadmap: Step 4](https://blog.process-one.net/understanding-ejabberd-oauth-support-roadmap/)
|
||||||
|
- Fix issues in the stanza builder when trying to add text inside and XMPP node.
|
||||||
|
- Fix issues with unescaped % characters in XMPP payload.
|
||||||
|
|
||||||
|
### Code migration guide
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at contact@process-one.net. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
We'd love for you to contribute to our source code and to make our project even better than it is
|
||||||
|
today! Here are the guidelines we'd like you to follow:
|
||||||
|
|
||||||
|
* [Code of Conduct](#coc)
|
||||||
|
* [Questions and Problems](#question)
|
||||||
|
* [Issues and Bugs](#issue)
|
||||||
|
* [Feature Requests](#feature)
|
||||||
|
* [Issue Submission Guidelines](#submit)
|
||||||
|
* [Pull Request Submission Guidelines](#submit-pr)
|
||||||
|
* [Signing the CLA](#cla)
|
||||||
|
|
||||||
|
## <a name="coc"></a> Code of Conduct
|
||||||
|
|
||||||
|
Help us keep our community open-minded and inclusive. Please read and follow our [Code of Conduct][coc].
|
||||||
|
|
||||||
|
## <a name="requests"></a> Questions, Bugs, Features
|
||||||
|
|
||||||
|
### <a name="question"></a> Got a Question or Problem?
|
||||||
|
|
||||||
|
Do not open issues for general support questions as we want to keep GitHub issues for bug reports
|
||||||
|
and feature requests. You've got much better chances of getting your question answered on dedicated
|
||||||
|
support platforms, the best being [Stack Overflow][stackoverflow].
|
||||||
|
|
||||||
|
Stack Overflow is a much better place to ask questions since:
|
||||||
|
|
||||||
|
- there are thousands of people willing to help on Stack Overflow
|
||||||
|
- questions and answers stay available for public viewing so your question / answer might help
|
||||||
|
someone else
|
||||||
|
- Stack Overflow's voting system assures that the best answers are prominently visible.
|
||||||
|
|
||||||
|
To save your and our time, we will systematically close all issues that are requests for general
|
||||||
|
support and redirect people to the section you are reading right now.
|
||||||
|
|
||||||
|
### <a name="issue"></a> Found an Issue or Bug?
|
||||||
|
|
||||||
|
If you find a bug in the source code, you can help us by submitting an issue to our
|
||||||
|
[GitHub Repository][github]. Even better, you can submit a Pull Request with a fix.
|
||||||
|
|
||||||
|
### <a name="feature"></a> Missing a Feature?
|
||||||
|
|
||||||
|
You can request a new feature by submitting an issue to our [GitHub Repository][github-issues].
|
||||||
|
|
||||||
|
If you would like to implement a new feature then consider what kind of change it is:
|
||||||
|
|
||||||
|
* **Major Changes** that you wish to contribute to the project should be discussed first in an
|
||||||
|
[GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature.
|
||||||
|
* **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github]
|
||||||
|
as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr).
|
||||||
|
|
||||||
|
## <a name="submit"></a> Issue Submission Guidelines
|
||||||
|
|
||||||
|
Before you submit your issue search the archive, maybe your question was already answered.
|
||||||
|
|
||||||
|
If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize
|
||||||
|
the effort we can spend fixing issues and adding new features, by not reporting duplicate issues.
|
||||||
|
|
||||||
|
The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to
|
||||||
|
make it easier to understand and categorize the issue.
|
||||||
|
|
||||||
|
## <a name="submit-pr"></a> Pull Request Submission Guidelines
|
||||||
|
|
||||||
|
By submitting a pull request for a code or doc contribution, you need to have the right
|
||||||
|
to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla]
|
||||||
|
for details.
|
||||||
|
|
||||||
|
Before you submit your pull request consider the following guidelines:
|
||||||
|
|
||||||
|
* Search [GitHub][github-pr] for an open or closed Pull Request
|
||||||
|
that relates to your submission. You don't want to duplicate effort.
|
||||||
|
* Make your changes in a new git branch:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git checkout -b my-fix-branch master
|
||||||
|
```
|
||||||
|
* Test your changes and, if relevant, expand the automated test suite.
|
||||||
|
* Create your patch commit, including appropriate test cases.
|
||||||
|
* If the changes affect public APIs, change or add relevant documentation.
|
||||||
|
* Commit your changes using a descriptive commit message.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git commit -a
|
||||||
|
```
|
||||||
|
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
|
||||||
|
|
||||||
|
* Push your branch to GitHub:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git push origin my-fix-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
* In GitHub, send a pull request to `master` branch. This will trigger the continuous integration and run the test.
|
||||||
|
We will also notify you if you have not yet signed the [contribution agreement][cla].
|
||||||
|
|
||||||
|
* If you find that the continunous integration has failed, look into the logs to find out
|
||||||
|
if your changes caused test failures, the commit message was malformed etc. If you find that the
|
||||||
|
tests failed or times out for unrelated reasons, you can ping a team member so that the build can be
|
||||||
|
restarted.
|
||||||
|
|
||||||
|
* If we suggest changes, then:
|
||||||
|
|
||||||
|
* Make the required updates.
|
||||||
|
* Test your changes and test cases.
|
||||||
|
* Commit your changes to your branch (e.g. `my-fix-branch`).
|
||||||
|
* Push the changes to your GitHub repository (this will update your Pull Request).
|
||||||
|
|
||||||
|
You can also amend the initial commits and force push them to the branch.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git rebase master -i
|
||||||
|
git push origin my-fix-branch -f
|
||||||
|
```
|
||||||
|
|
||||||
|
This is generally easier to follow, but separate commits are useful if the Pull Request contains
|
||||||
|
iterations that might be interesting to see side-by-side.
|
||||||
|
|
||||||
|
That's it! Thank you for your contribution!
|
||||||
|
|
||||||
|
## <a name="cla"></a> Signing the Contributor License Agreement (CLA)
|
||||||
|
|
||||||
|
Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done
|
||||||
|
so before. It's a quick process, we promise, and you will be able to do it all online
|
||||||
|
|
||||||
|
You can read [ProcessOne Contribution License Agreement][cla] in PDF.
|
||||||
|
|
||||||
|
This is part of the legal framework of the open-source ecosystem that adds some red tape,
|
||||||
|
but protects both the contributor and the company / foundation behind the project. It also
|
||||||
|
gives us the option to relicense the code with a more permissive license in the future.
|
||||||
|
|
||||||
|
|
||||||
|
[coc]: https://github.com/FluuxIO/go-xmpp/blob/master/CODE_OF_CONDUCT.md
|
||||||
|
[stackoverflow]: https://stackoverflow.com/
|
||||||
|
[github]: https://github.com/FluuxIO/go-xmpp
|
||||||
|
[github-issues]: https://github.com/FluuxIO/go-xmpp/issues
|
||||||
|
[github-new-issue]: https://github.com/FluuxIO/go-xmpp/issues/new
|
||||||
|
[github-pr]: https://github.com/FluuxIO/go-xmpp/pulls
|
||||||
|
[cla]: https://www.process-one.net/resources/ejabberd-cla.pdf
|
||||||
|
[license]: https://github.com/FluuxIO/go-xmpp/blob/master/LICENSE
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2017, ProcessOne SARL
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# Fluux XMPP
|
||||||
|
|
||||||
|
[](https://godoc.org/gosrc.io/xmpp) [](https://goreportcard.com/report/fluux.io/xmpp) [](https://coveralls.io/github/FluuxIO/go-xmpp?branch=master)
|
||||||
|
|
||||||
|
Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT.
|
||||||
|
|
||||||
|
The goal is to make simple to write simple XMPP clients and components:
|
||||||
|
|
||||||
|
- For automation (like for example monitoring of an XMPP service),
|
||||||
|
- For building connected "things" by plugging them on an XMPP server,
|
||||||
|
- For writing simple chatbot to control a service or a thing,
|
||||||
|
- For writing XMPP servers components.
|
||||||
|
|
||||||
|
The library is designed to have minimal dependencies. Currently it requires at least Go 1.13.
|
||||||
|
|
||||||
|
## Configuration and connection
|
||||||
|
|
||||||
|
### Allowing Insecure TLS connection during development
|
||||||
|
|
||||||
|
It is not recommended to disable the check for domain name and certificate chain. Doing so would open your client
|
||||||
|
to man-in-the-middle attacks.
|
||||||
|
|
||||||
|
However, in development, XMPP servers often use self-signed certificates. In that situation, it is better to add the
|
||||||
|
root CA that signed the certificate to your trusted list of root CA. It avoids changing the code and limit the risk
|
||||||
|
of shipping an insecure client to production.
|
||||||
|
|
||||||
|
That said, if you really want to allow your client to trust any TLS certificate, you can customize Go standard
|
||||||
|
`tls.Config` and set it in Config struct.
|
||||||
|
|
||||||
|
Here is an example code to configure a client to allow connecting to a server with self-signed certificate. Note the
|
||||||
|
`InsecureSkipVerify` option. When using this `tls.Config` option, all the checks on the certificate are skipped.
|
||||||
|
|
||||||
|
```go
|
||||||
|
config := xmpp.Config{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("Test"),
|
||||||
|
TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported specifications
|
||||||
|
|
||||||
|
### Clients
|
||||||
|
|
||||||
|
- [RFC 6120: XMPP Core](https://xmpp.org/rfcs/rfc6120.html)
|
||||||
|
- [RFC 6121: XMPP Instant Messaging and Presence](https://xmpp.org/rfcs/rfc6121.html)
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- [XEP-0114: Jabber Component Protocol](https://xmpp.org/extensions/xep-0114.html)
|
||||||
|
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
|
||||||
|
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
|
||||||
|
|
||||||
|
### Extensions
|
||||||
|
- [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
|
||||||
|
Note : "6.5.4 Returning Some Items" requires support for [XEP-0059: Result Set Management](https://xmpp.org/extensions/xep-0059.html),
|
||||||
|
and is therefore not supported yet.
|
||||||
|
- [XEP-0004: Data Forms](https://xmpp.org/extensions/xep-0004.html)
|
||||||
|
- [XEP-0050: Ad-Hoc Commands](https://xmpp.org/extensions/xep-0050.html)
|
||||||
|
|
||||||
|
## Package overview
|
||||||
|
|
||||||
|
### Stanza subpackage
|
||||||
|
|
||||||
|
XMPP stanzas are basic and extensible XML elements. Stanzas (or sometimes special stanzas called 'nonzas') are used to
|
||||||
|
leverage the XMPP protocol features. During a session, a client (or a component) and a server will be exchanging stanzas
|
||||||
|
back and forth.
|
||||||
|
|
||||||
|
At a low-level, stanzas are XML fragments. However, Fluux XMPP library provides the building blocks to interact with
|
||||||
|
stanzas at a high-level, providing a Go-friendly API.
|
||||||
|
|
||||||
|
The `stanza` subpackage provides support for XMPP stream parsing, marshalling and unmarshalling of XMPP stanza. It is a
|
||||||
|
bridge between high-level Go structure and low-level XMPP protocol.
|
||||||
|
|
||||||
|
Parsing, marshalling and unmarshalling is automatically handled by Fluux XMPP client library. As a developer, you will
|
||||||
|
generally manipulates only the high-level structs provided by the stanza package.
|
||||||
|
|
||||||
|
The XMPP protocol, as the name implies is extensible. If your application is using custom stanza extensions, you can
|
||||||
|
implement your own extensions directly in your own application.
|
||||||
|
|
||||||
|
To learn more about the stanza package, you can read more in the
|
||||||
|
[stanza package documentation](https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md).
|
||||||
|
|
||||||
|
### Router
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Getting IQ response from server
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
We have several [examples](https://github.com/FluuxIO/go-xmpp/tree/master/_examples) to help you get started using
|
||||||
|
Fluux XMPP library.
|
||||||
|
|
||||||
|
Here is the demo "echo" client:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("test"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
|
_ = s.Send(reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference documentation
|
||||||
|
|
||||||
|
The code documentation is available on GoDoc: [gosrc.io/xmpp](https://godoc.org/gosrc.io/xmpp)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Custom Stanza example
|
||||||
|
|
||||||
|
This module show how to implement a custom extension for your own client, without having to modify or fork Fluux XMPP.
|
||||||
|
|
||||||
|
It help integrating your custom extension in the standard stream parsing, marshalling and unmarshalling workflow.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "service.localhost", Id: "custom-pl-1"})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
payload := CustomPayload{XMLName: xml.Name{Space: "my:custom:payload", Local: "query"}, Node: "test"}
|
||||||
|
iq.Payload = payload
|
||||||
|
|
||||||
|
data, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Cannot marshal iq with custom payload: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedIQ stanza.IQ
|
||||||
|
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||||
|
log.Fatalf("Cannot unmarshal(%s): %s", data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedPayload, ok := parsedIQ.Payload.(*CustomPayload)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("Incorrect payload type: %#v", parsedIQ.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Parsed Payload: %#v", parsedPayload)
|
||||||
|
|
||||||
|
if parsedPayload.Node != "test" {
|
||||||
|
log.Fatalf("Incorrect node value: %s", parsedPayload.Node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomPayload struct {
|
||||||
|
XMLName xml.Name `xml:"my:custom:payload query"`
|
||||||
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomPayload) Namespace() string {
|
||||||
|
return c.XMLName.Space
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CustomPayload) GetSet() *stanza.ResultSet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func init() {
|
||||||
|
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "my:custom:payload", Local: "query"}, CustomPayload{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Advanced component: delegation
|
||||||
|
|
||||||
|
`delegation` is an example of advanced component supporting Namespace Delegation
|
||||||
|
([XEP-0355](https://xmpp.org/extensions/xep-0355.html)) and privileged entity
|
||||||
|
([XEP-356](https://xmpp.org/extensions/xep-0356.html)).
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:9999",
|
||||||
|
Domain: "service.localhost",
|
||||||
|
},
|
||||||
|
Domain: "service.localhost",
|
||||||
|
Secret: "mypass",
|
||||||
|
|
||||||
|
// TODO: Move that part to a component discovery handler
|
||||||
|
Name: "Test Component",
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces(stanza.NSDiscoInfo).
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
discoInfo(s, p, opts)
|
||||||
|
})
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces("urn:xmpp:delegation:1").
|
||||||
|
HandlerFunc(handleDelegation)
|
||||||
|
|
||||||
|
component, err := xmpp.NewComponent(opts, router, func(err error) {
|
||||||
|
log.Println(err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the component to a stream manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
// TODO: Post Connect could be a feature of the router or the client. Move it somewhere else.
|
||||||
|
cm := xmpp.NewStreamManager(component, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msgProcessed bool
|
||||||
|
for _, ext := range msg.Extensions {
|
||||||
|
delegation, ok := ext.(*stanza.Delegation)
|
||||||
|
if ok {
|
||||||
|
msgProcessed = true
|
||||||
|
fmt.Printf("Delegation confirmed for namespace %s\n", delegation.Delegated.Namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Decode privilege message
|
||||||
|
// <message to='service.localhost' from='localhost'><privilege xmlns='urn:xmpp:privilege:1'><perm type='outgoing' access='message'/><perm type='get' access='roster'/><perm type='managed_entity' access='presence'/></privilege></message>
|
||||||
|
|
||||||
|
if !msgProcessed {
|
||||||
|
fmt.Printf("Ignored received message, not related to delegation: %v\n", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
pubsubNode = "urn:xmpp:delegation:1::http://jabber.org/protocol/pubsub"
|
||||||
|
pepNode = "urn:xmpp:delegation:1:bare:http://jabber.org/protocol/pubsub"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: replace xmpp.Sender by ctx xmpp.Context ?
|
||||||
|
// ctx.Stream.Send / SendRaw
|
||||||
|
// ctx.Opts
|
||||||
|
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info, ok := iq.Payload.(*stanza.DiscoInfo)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create IQ response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch info.Node {
|
||||||
|
case "":
|
||||||
|
discoInfoRoot(iqResp, opts)
|
||||||
|
case pubsubNode:
|
||||||
|
discoInfoPubSub(iqResp)
|
||||||
|
case pepNode:
|
||||||
|
discoInfoPEP(iqResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = c.Send(iqResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
|
||||||
|
disco := iqResp.DiscoInfo()
|
||||||
|
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||||
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoInfoPubSub(iqResp *stanza.IQ) {
|
||||||
|
payload := stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: stanza.NSDiscoInfo,
|
||||||
|
Local: "query",
|
||||||
|
},
|
||||||
|
Node: pubsubNode,
|
||||||
|
Features: []stanza.Feature{
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#publish"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#publish-options"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqResp.Payload = &payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoInfoPEP(iqResp *stanza.IQ) {
|
||||||
|
identity := stanza.Identity{
|
||||||
|
Category: "pubsub",
|
||||||
|
Type: "pep",
|
||||||
|
}
|
||||||
|
payload := stanza.DiscoInfo{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: stanza.NSDiscoInfo,
|
||||||
|
Local: "query",
|
||||||
|
},
|
||||||
|
Identity: []stanza.Identity{identity},
|
||||||
|
Node: pepNode,
|
||||||
|
Features: []stanza.Feature{
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#access-presence"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#auto-create"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#auto-subscribe"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#config-node"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#create-and-configure"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#create-nodes"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#filtered-notifications"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#persistent-items"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#publish"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#retrieve-items"},
|
||||||
|
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqResp.Payload = &payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delegation, ok := iq.Payload.(*stanza.Delegation)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forwardedPacket := delegation.Forwarded.Stanza
|
||||||
|
fmt.Println(forwardedPacket)
|
||||||
|
forwardedIQ, ok := forwardedPacket.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pubsub, ok := forwardedIQ.Payload.(*stanza.PubSubGeneric)
|
||||||
|
if !ok {
|
||||||
|
// We only support pubsub delegation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubsub.Publish.XMLName.Local == "publish" {
|
||||||
|
// Prepare pubsub IQ reply
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create iqResp: %v", err)
|
||||||
|
}
|
||||||
|
payload := stanza.PubSubGeneric{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "http://jabber.org/protocol/pubsub",
|
||||||
|
Local: "pubsub",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqResp.Payload = &payload
|
||||||
|
// Wrap the reply in delegation 'forward'
|
||||||
|
iqForward, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create iqForward: %v", err)
|
||||||
|
}
|
||||||
|
delegPayload := stanza.Delegation{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "urn:xmpp:delegation:1",
|
||||||
|
Local: "delegation",
|
||||||
|
},
|
||||||
|
Forwarded: &stanza.Forwarded{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "urn:xmpp:forward:0",
|
||||||
|
Local: "forward",
|
||||||
|
},
|
||||||
|
Stanza: iqResp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iqForward.Payload = &delegPayload
|
||||||
|
_ = s.Send(iqForward)
|
||||||
|
// TODO: The component should actually broadcast the mood to subscribers
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module gosrc.io/xmpp/_examples
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/processone/mpg123 v1.0.0
|
||||||
|
github.com/processone/soundcloud v1.0.0
|
||||||
|
gosrc.io/xmpp v0.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
replace gosrc.io/xmpp => ./../
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA=
|
||||||
|
github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
|
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA=
|
||||||
|
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
||||||
|
github.com/processone/soundcloud v1.0.0 h1:/+i6+Yveb7Y6IFGDSkesYI+HddblzcRTQClazzVHxoE=
|
||||||
|
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
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=
|
||||||
|
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||||
|
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||||
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||||
|
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# XMPP Multi-User (MUC) chat bot example
|
||||||
|
|
||||||
|
This code shows how to build a simple basic XMPP Multi-User chat bot using Fluux Go XMPP library.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Chat TUI example
|
||||||
|
This is a simple chat example, with a TUI.
|
||||||
|
It shows the library usage and a few of its capabilities.
|
||||||
|
## How to run
|
||||||
|
### Build
|
||||||
|
You can build the client using :
|
||||||
|
```
|
||||||
|
go build -o example_client
|
||||||
|
```
|
||||||
|
and then run with (on unix for example):
|
||||||
|
```
|
||||||
|
./example_client
|
||||||
|
```
|
||||||
|
or you can simply build + run in one command while at the example directory root, like this:
|
||||||
|
```
|
||||||
|
go run xmpp_chat_client.go interface.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
The example needs a configuration file to run. A sample file is provided.
|
||||||
|
By default, the example will look for a file named "config" in the current directory.
|
||||||
|
To provide a different configuration file, pass the following argument to the example :
|
||||||
|
```
|
||||||
|
go run xmpp_chat_client.go interface.go -c /path/to/config
|
||||||
|
```
|
||||||
|
where /path/to/config is the path to the directory containing the configuration file. The configuration file must be named
|
||||||
|
"config" and be using the yaml format.
|
||||||
|
|
||||||
|
Required fields are :
|
||||||
|
```yaml
|
||||||
|
Server :
|
||||||
|
- full_address: "localhost:5222"
|
||||||
|
Client : # This is you
|
||||||
|
- jid: "testuser2@localhost"
|
||||||
|
- pass: "pass123" #Password in a config file yay
|
||||||
|
|
||||||
|
# Contacts list, ";" separated
|
||||||
|
Contacts : "testuser1@localhost;testuser3@localhost"
|
||||||
|
# Should we log stanzas ?
|
||||||
|
LogStanzas:
|
||||||
|
- logger_on: "true"
|
||||||
|
- logfile_path: "./logs" # Path to directory, not file.
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
Shortcuts :
|
||||||
|
- ctrl+space : switch between input window and menu window.
|
||||||
|
- While in input window :
|
||||||
|
- enter : sends a message if in message mode (see menu options)
|
||||||
|
- ctrl+e : sends a raw stanza when in raw mode (see menu options)
|
||||||
|
- ctrl+c : quit
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Sample config for the client
|
||||||
|
Server :
|
||||||
|
- full_address: "localhost:5222"
|
||||||
|
Client :
|
||||||
|
- jid: "testuser2@localhost"
|
||||||
|
- pass: "pass123" #Password in a config file yay
|
||||||
|
|
||||||
|
Contacts : "testuser1@localhost;testuser3@localhost"
|
||||||
|
|
||||||
|
LogStanzas:
|
||||||
|
- logger_on: "true"
|
||||||
|
- logfile_path: "./logs"
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module go-xmpp/_examples/xmpp_chat_client
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
|
github.com/spf13/viper v1.6.1
|
||||||
|
gosrc.io/xmpp v0.4.0
|
||||||
|
)
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/awesome-gocui/gocui"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Windows
|
||||||
|
chatLogWindow = "clw" // Where (received and sent) messages are logged
|
||||||
|
chatInputWindow = "iw" // Where messages are written
|
||||||
|
rawInputWindow = "rw" // Where raw stanzas are written
|
||||||
|
contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable
|
||||||
|
menuWindow = "mw" // Where the menu is shown
|
||||||
|
disconnectMsg = "msg"
|
||||||
|
|
||||||
|
// Windows titles
|
||||||
|
chatLogWindowTitle = "Chat log"
|
||||||
|
menuWindowTitle = "Menu"
|
||||||
|
chatInputWindowTitle = "Write a message :"
|
||||||
|
rawInputWindowTitle = "Write or paste a raw stanza. Press \"Ctrl+E\" to send :"
|
||||||
|
contactsListWindowTitle = "Contacts"
|
||||||
|
|
||||||
|
// Menu options
|
||||||
|
disconnect = "Disconnect"
|
||||||
|
askServerForRoster = "Ask server for roster"
|
||||||
|
rawMode = "Switch to Send Raw Mode"
|
||||||
|
messageMode = "Switch to Send Message Mode"
|
||||||
|
contactList = "Contacts list"
|
||||||
|
backFromContacts = "<- Go back"
|
||||||
|
)
|
||||||
|
|
||||||
|
// To store names of views on top
|
||||||
|
type viewsState struct {
|
||||||
|
input string // Which input view is on top
|
||||||
|
side string // Which side view is on top
|
||||||
|
contacts []string // Contacts list
|
||||||
|
currentContact string // Contact we are currently messaging
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Which window is on top currently on top of the other.
|
||||||
|
// This is the init setup
|
||||||
|
viewState = viewsState{
|
||||||
|
input: chatInputWindow,
|
||||||
|
side: menuWindow,
|
||||||
|
}
|
||||||
|
menuOptions = []string{contactList, rawMode, askServerForRoster, disconnect}
|
||||||
|
// Errors
|
||||||
|
servConnFail = errors.New("failed to connect to server. Check your configuration ? Exiting")
|
||||||
|
)
|
||||||
|
|
||||||
|
func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) {
|
||||||
|
if _, err := g.SetCurrentView(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return g.SetViewOnTop(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func layout(g *gocui.Gui) error {
|
||||||
|
maxX, maxY := g.Size()
|
||||||
|
|
||||||
|
if v, err := g.SetView(chatLogWindow, maxX/5, 0, maxX-1, 5*maxY/6-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = chatLogWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
v.Autoscroll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(contactsListWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = contactsListWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
// If we set this to true, the contacts list will "fit" in the window but if the number
|
||||||
|
// of contacts exceeds the maximum height, some contacts will be hidden...
|
||||||
|
// If set to false, we can scroll up and down the contact list... infinitely. Meaning lower lines
|
||||||
|
// will be unlimited and empty... Didn't find a way to quickfix yet.
|
||||||
|
v.Autoscroll = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = menuWindowTitle
|
||||||
|
v.Wrap = true
|
||||||
|
v.Autoscroll = true
|
||||||
|
fmt.Fprint(v, strings.Join(menuOptions, "\n"))
|
||||||
|
if _, err = setCurrentViewOnTop(g, menuWindow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(rawInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = rawInputWindowTitle
|
||||||
|
v.Editable = true
|
||||||
|
v.Wrap = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := g.SetView(chatInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Title = chatInputWindowTitle
|
||||||
|
v.Editable = true
|
||||||
|
v.Wrap = true
|
||||||
|
|
||||||
|
if _, err = setCurrentViewOnTop(g, chatInputWindow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gocui.ErrQuit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an input text from the user to the backend while also printing it in the chatlog window.
|
||||||
|
// KeyEnter is viewed as "\n" by gocui, so messages should only be one line, whereas raw sending has a different key
|
||||||
|
// binding and therefor should work with this too (for multiple lines stanzas)
|
||||||
|
func writeInput(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
chatLogWindow, _ := g.View(chatLogWindow)
|
||||||
|
|
||||||
|
input := strings.Join(v.ViewBufferLines(), "\n")
|
||||||
|
|
||||||
|
fmt.Fprintln(chatLogWindow, "Me : ", input)
|
||||||
|
if viewState.input == rawInputWindow {
|
||||||
|
rawTextChan <- input
|
||||||
|
} else {
|
||||||
|
textChan <- input
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setKeyBindings(g *gocui.Gui) {
|
||||||
|
// ==========================
|
||||||
|
// All views
|
||||||
|
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Chat input
|
||||||
|
if err := g.SetKeybinding(chatInputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.SetKeybinding(chatInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Raw input
|
||||||
|
if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlE, gocui.ModNone, writeInput); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Menu
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(menuWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Contacts list
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if err := g.SetKeybinding(contactsListWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Disconnect message
|
||||||
|
if err := g.SetKeybinding(disconnectMsg, gocui.KeyEnter, gocui.ModNone, delMsg); err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// General
|
||||||
|
// Used to handle menu selections and navigations
|
||||||
|
func getLine(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
var l string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, cy := v.Cursor()
|
||||||
|
if l, err = v.Line(cy); err != nil {
|
||||||
|
l = ""
|
||||||
|
}
|
||||||
|
if viewState.side == menuWindow {
|
||||||
|
if l == contactList {
|
||||||
|
cv, _ := g.View(contactsListWindow)
|
||||||
|
viewState.side = contactsListWindow
|
||||||
|
g.SetViewOnTop(contactsListWindow)
|
||||||
|
g.SetCurrentView(contactsListWindow)
|
||||||
|
if len(cv.ViewBufferLines()) == 0 {
|
||||||
|
printContactsToWindow(g, viewState.contacts)
|
||||||
|
}
|
||||||
|
} else if l == disconnect {
|
||||||
|
maxX, maxY := g.Size()
|
||||||
|
msg := "You disconnected from the server. Press enter to quit."
|
||||||
|
if v, err := g.SetView(disconnectMsg, maxX/2-30, maxY/2, maxX/2-29+len(msg), maxY/2+2, 0); err != nil {
|
||||||
|
if !gocui.IsUnknownView(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(v, msg)
|
||||||
|
if _, err := g.SetCurrentView(disconnectMsg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
killChan <- disconnectErr
|
||||||
|
} else if l == askServerForRoster {
|
||||||
|
chlw, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Asking server for contacts list...")
|
||||||
|
rosterChan <- struct{}{}
|
||||||
|
} else if l == rawMode {
|
||||||
|
mw, _ := g.View(menuWindow)
|
||||||
|
viewState.input = rawInputWindow
|
||||||
|
g.SetViewOnTop(rawInputWindow)
|
||||||
|
g.SetCurrentView(rawInputWindow)
|
||||||
|
menuOptions[1] = messageMode
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
fmt.Fprintln(mw, strings.Join(menuOptions, "\n"))
|
||||||
|
message := "Now sending in raw stanza mode"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
} else if l == messageMode {
|
||||||
|
mw, _ := g.View(menuWindow)
|
||||||
|
viewState.input = chatInputWindow
|
||||||
|
g.SetViewOnTop(chatInputWindow)
|
||||||
|
g.SetCurrentView(chatInputWindow)
|
||||||
|
menuOptions[1] = rawMode
|
||||||
|
v.Clear()
|
||||||
|
v.EditDeleteToStartOfLine()
|
||||||
|
fmt.Fprintln(mw, strings.Join(menuOptions, "\n"))
|
||||||
|
message := "Now sending in messages mode"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
}
|
||||||
|
} else if viewState.side == contactsListWindow {
|
||||||
|
if l == backFromContacts {
|
||||||
|
viewState.side = menuWindow
|
||||||
|
g.SetViewOnTop(menuWindow)
|
||||||
|
g.SetCurrentView(menuWindow)
|
||||||
|
} else if l == "" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// Updating the current correspondent, back-end side.
|
||||||
|
CorrespChan <- l
|
||||||
|
viewState.currentContact = l
|
||||||
|
// Showing the selected contact in contacts list
|
||||||
|
cl, _ := g.View(contactsListWindow)
|
||||||
|
cts := cl.ViewBufferLines()
|
||||||
|
cl.Clear()
|
||||||
|
printContactsToWindow(g, cts)
|
||||||
|
// Showing a message to the user, and switching back to input after the new contact is selected.
|
||||||
|
message := "Now sending messages to : " + l + " in a private conversation"
|
||||||
|
clv, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintln(clv, infoFormat+message)
|
||||||
|
g.SetCurrentView(chatInputWindow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printContactsToWindow(g *gocui.Gui, contactsList []string) {
|
||||||
|
cl, _ := g.View(contactsListWindow)
|
||||||
|
for _, c := range contactsList {
|
||||||
|
c = strings.ReplaceAll(c, " *", "")
|
||||||
|
if c == viewState.currentContact {
|
||||||
|
fmt.Fprintf(cl, c+" *\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(cl, c+"\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing view between input and "menu/contacts" when pressing the specific key.
|
||||||
|
func nextView(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v == nil || v.Name() == chatInputWindow || v.Name() == rawInputWindow {
|
||||||
|
_, err := g.SetCurrentView(viewState.side)
|
||||||
|
return err
|
||||||
|
} else if v.Name() == menuWindow || v.Name() == contactsListWindow {
|
||||||
|
_, err := g.SetCurrentView(viewState.input)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not be reached right now
|
||||||
|
_, err := g.SetCurrentView(chatInputWindow)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v != nil {
|
||||||
|
cx, cy := v.Cursor()
|
||||||
|
// Avoid going below the list of contacts. Although lines are stored in the view as a slice
|
||||||
|
// in the used lib. Therefor, if the number of lines is too big, the cursor will go past the last line since
|
||||||
|
// increasing slice capacity is done by doubling it. Last lines will be "nil" and reachable by the cursor
|
||||||
|
// in a dynamic context (such as contacts list)
|
||||||
|
cv := g.CurrentView()
|
||||||
|
h := cv.LinesHeight()
|
||||||
|
if cy+1 >= h {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Lower cursor
|
||||||
|
if err := v.SetCursor(cx, cy+1); err != nil {
|
||||||
|
ox, oy := v.Origin()
|
||||||
|
if err := v.SetOrigin(ox, oy+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if v != nil {
|
||||||
|
ox, oy := v.Origin()
|
||||||
|
cx, cy := v.Cursor()
|
||||||
|
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
|
||||||
|
if err := v.SetOrigin(ox, oy-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func delMsg(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := g.DeleteView(disconnectMsg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
errChan <- gocui.ErrQuit // Quit the program
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
xmpp_chat_client is a demo client that connect on an XMPP server to chat with other members
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/awesome-gocui/gocui"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
infoFormat = "====== "
|
||||||
|
// Default configuration
|
||||||
|
defaultConfigFilePath = "./"
|
||||||
|
|
||||||
|
configFileName = "config"
|
||||||
|
configType = "yaml"
|
||||||
|
logStanzasOn = "logger_on"
|
||||||
|
logFilePath = "logfile_path"
|
||||||
|
// Keys in config
|
||||||
|
serverAddressKey = "full_address"
|
||||||
|
clientJid = "jid"
|
||||||
|
clientPass = "pass"
|
||||||
|
configContactSep = ";"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CorrespChan = make(chan string, 1)
|
||||||
|
textChan = make(chan string, 5)
|
||||||
|
rawTextChan = make(chan string, 5)
|
||||||
|
killChan = make(chan error, 1)
|
||||||
|
errChan = make(chan error)
|
||||||
|
rosterChan = make(chan struct{})
|
||||||
|
|
||||||
|
logger *log.Logger
|
||||||
|
disconnectErr = errors.New("disconnecting client")
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Server map[string]string `mapstructure:"server"`
|
||||||
|
Client map[string]string `mapstructure:"client"`
|
||||||
|
Contacts string `string:"contact"`
|
||||||
|
LogStanzas map[string]string `mapstructure:"logstanzas"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Parse the flag with the config directory path as argument
|
||||||
|
flag.String("c", defaultConfigFilePath, "Provide a path to the directory that contains the configuration"+
|
||||||
|
" file you want to use. Config file should be named \"config\" and be in YAML format..")
|
||||||
|
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Read configuration
|
||||||
|
c := readConfig()
|
||||||
|
|
||||||
|
//================================
|
||||||
|
// Setup logger
|
||||||
|
on, err := strconv.ParseBool(c.LogStanzas[logStanzasOn])
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if on {
|
||||||
|
f, err := os.OpenFile(path.Join(c.LogStanzas[logFilePath], "logs.txt"), os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
logger = log.New(f, "", log.Lshortfile|log.Ldate|log.Ltime)
|
||||||
|
logger.SetOutput(f)
|
||||||
|
defer f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Create TUI
|
||||||
|
g, err := gocui.NewGui(gocui.OutputNormal, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
defer g.Close()
|
||||||
|
g.Highlight = true
|
||||||
|
g.Cursor = true
|
||||||
|
g.SelFgColor = gocui.ColorGreen
|
||||||
|
g.SetManagerFunc(layout)
|
||||||
|
setKeyBindings(g)
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Run TUI
|
||||||
|
go func() {
|
||||||
|
errChan <- g.MainLoop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Start XMPP client
|
||||||
|
go startClient(g, c)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
if err == gocui.ErrQuit {
|
||||||
|
log.Println("Closing client.")
|
||||||
|
} else {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startClient(g *gocui.Gui, config *config) {
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client setup
|
||||||
|
clientCfg := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: config.Server[serverAddressKey],
|
||||||
|
},
|
||||||
|
Jid: config.Client[clientJid],
|
||||||
|
Credential: xmpp.Password(config.Client[clientPass]),
|
||||||
|
Insecure: true}
|
||||||
|
|
||||||
|
var client *xmpp.Client
|
||||||
|
var err error
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
|
||||||
|
handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(msg)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := g.View(chatLogWindow)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(v, "%sIgnoring packet: %T\n", infoFormat, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
if msg.Error.Code != 0 {
|
||||||
|
_, err := fmt.Fprintf(v, "Error from server : %s : %s \n", msg.Error.Reason, msg.XMLName.Space)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(strings.TrimSpace(msg.Body)) != 0 {
|
||||||
|
_, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
router.HandleFunc("message", handlerWithGui)
|
||||||
|
if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil {
|
||||||
|
log.Panicln(fmt.Sprintf("Could not create a new client ! %s", err))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client connection
|
||||||
|
if err = client.Connect(); err != nil {
|
||||||
|
msg := fmt.Sprintf("%sXMPP connection failed: %s", infoFormat, err)
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
v, err := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintf(v, msg)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
fmt.Println("Failed to connect to server. Exiting...")
|
||||||
|
errChan <- servConnFail
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Start working
|
||||||
|
updateRosterFromConfig(config)
|
||||||
|
// Sending the default contact in a channel. Default value is the first contact in the list from the config.
|
||||||
|
viewState.currentContact = strings.Split(config.Contacts, configContactSep)[0]
|
||||||
|
// Informing user of the default contact
|
||||||
|
clw, _ := g.View(chatLogWindow)
|
||||||
|
fmt.Fprintf(clw, infoFormat+"Now sending messages to "+viewState.currentContact+" in a private conversation\n")
|
||||||
|
CorrespChan <- viewState.currentContact
|
||||||
|
startMessaging(client, config, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMessaging(client xmpp.Sender, config *config, g *gocui.Gui) {
|
||||||
|
var text string
|
||||||
|
var correspondent string
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-killChan:
|
||||||
|
if err == disconnectErr {
|
||||||
|
sc := client.(xmpp.StreamClient)
|
||||||
|
sc.Disconnect()
|
||||||
|
} else {
|
||||||
|
logger.Println(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case text = <-textChan:
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, Type: stanza.MessageTypeChat}, Body: text}
|
||||||
|
if logger != nil {
|
||||||
|
raw, _ := xml.Marshal(reply)
|
||||||
|
logger.Println(string(raw))
|
||||||
|
}
|
||||||
|
err := client.Send(reply)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("There was a problem sending the message : %v", reply)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case text = <-rawTextChan:
|
||||||
|
if logger != nil {
|
||||||
|
logger.Println(text)
|
||||||
|
}
|
||||||
|
err := client.SendRaw(text)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("There was a problem sending the message : %v", text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case crrsp := <-CorrespChan:
|
||||||
|
correspondent = crrsp
|
||||||
|
case <-rosterChan:
|
||||||
|
askForRoster(client, g, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only reads and parses the configuration
|
||||||
|
func readConfig() *config {
|
||||||
|
viper.SetConfigName(configFileName) // name of config file (without extension)
|
||||||
|
viper.BindPFlags(pflag.CommandLine)
|
||||||
|
viper.AddConfigPath(viper.GetString("c")) // path to look for the config file in
|
||||||
|
err := viper.ReadInConfig() // Find and read the config file
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
|
log.Fatalf("%s %s", err, "Please make sure you give a path to the directory of the config and not to the config itself.")
|
||||||
|
} else {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigType(configType)
|
||||||
|
var config config
|
||||||
|
err = viper.Unmarshal(&config)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("Unable to decode Config: %s \n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have contacts to message
|
||||||
|
if len(strings.TrimSpace(config.Contacts)) == 0 {
|
||||||
|
log.Panicln("You appear to have no contacts to message !")
|
||||||
|
}
|
||||||
|
// Check logging
|
||||||
|
config.LogStanzas[logFilePath] = path.Clean(config.LogStanzas[logFilePath])
|
||||||
|
on, err := strconv.ParseBool(config.LogStanzas[logStanzasOn])
|
||||||
|
if err != nil {
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
if d, e := isDirectory(config.LogStanzas[logFilePath]); (e != nil || !d) && on {
|
||||||
|
log.Panicln("The log file path could not be found or is not a directory.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an error occurs, this is used to kill the client
|
||||||
|
func errorHandler(err error) {
|
||||||
|
killChan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the client roster from the config. This does not check with the server that the roster is correct.
|
||||||
|
// If user tries to send a message to someone not registered with the server, the server will return an error.
|
||||||
|
func updateRosterFromConfig(config *config) {
|
||||||
|
viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts)
|
||||||
|
// Put a "go back" button at the end of the list
|
||||||
|
viewState.contacts = append(viewState.contacts, backFromContacts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the menu panel of the view with the current user's roster, by asking the server.
|
||||||
|
func askForRoster(client xmpp.Sender, g *gocui.Gui, config *config) {
|
||||||
|
// Craft a roster request
|
||||||
|
req := stanza.NewIQ(stanza.Attrs{From: config.Client[clientJid], Type: stanza.IQTypeGet})
|
||||||
|
req.RosterItems()
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(req)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
|
||||||
|
// Send the roster request to the server
|
||||||
|
c, err := client.SendIQ(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending a IQ has a channel spawned to process the response once we receive it.
|
||||||
|
// In order not to block the client, we spawn a goroutine to update the TUI once the server has responded.
|
||||||
|
go func() {
|
||||||
|
serverResp := <-c
|
||||||
|
if logger != nil {
|
||||||
|
m, _ := xml.Marshal(serverResp)
|
||||||
|
logger.Println(string(m))
|
||||||
|
}
|
||||||
|
// Update contacts with the response from the server
|
||||||
|
chlw, _ := g.View(chatLogWindow)
|
||||||
|
if rosterItems, ok := serverResp.Payload.(*stanza.RosterItems); ok {
|
||||||
|
viewState.contacts = []string{}
|
||||||
|
for _, item := range rosterItems.Items {
|
||||||
|
viewState.contacts = append(viewState.contacts, item.Jid)
|
||||||
|
}
|
||||||
|
// Put a "go back" button at the end of the list
|
||||||
|
viewState.contacts = append(viewState.contacts, backFromContacts)
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Contacts list updated !")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(chlw, infoFormat+"Failed to update contact list !")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDirectory(path string) (bool, error) {
|
||||||
|
fileInfo, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return fileInfo.IsDir(), err
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# xmpp_component
|
||||||
|
|
||||||
|
This component will connect to ejabberd and act as a subdomain "service" of your primary XMPP domain
|
||||||
|
(in that case localhost).
|
||||||
|
|
||||||
|
This component does nothing expect connect and show up in service discovery.
|
||||||
|
|
||||||
|
To be able to connect this component, you need to add a listener to your XMPP server.
|
||||||
|
|
||||||
|
Here is an example ejabberd configuration for that component listener:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
listen:
|
||||||
|
...
|
||||||
|
-
|
||||||
|
port: 8888
|
||||||
|
module: ejabberd_service
|
||||||
|
password: "mypass"
|
||||||
|
```
|
||||||
|
|
||||||
|
ejabberd will listen for a component (service) on port 8888 and allows it to connect using the
|
||||||
|
secret "mypass".
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:8888",
|
||||||
|
Domain: "service2.localhost",
|
||||||
|
},
|
||||||
|
Domain: "service2.localhost",
|
||||||
|
Secret: "mypass",
|
||||||
|
Name: "Test Component",
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces(stanza.NSDiscoInfo).
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
discoInfo(s, p, opts)
|
||||||
|
})
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces(stanza.NSDiscoItems).
|
||||||
|
HandlerFunc(discoItems)
|
||||||
|
router.NewRoute().
|
||||||
|
IQNamespaces("jabber:iq:version").
|
||||||
|
HandlerFunc(handleVersion)
|
||||||
|
|
||||||
|
component, err := xmpp.NewComponent(opts, router, handleError)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the component to a stream manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
// TODO: Post Connect could be a feature of the router or the client. Move it somewhere else.
|
||||||
|
cm := xmpp.NewStreamManager(component, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Received message:", msg.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok || iq.Type != stanza.IQTypeGet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
|
// TODO: fix this...
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
disco := iqResp.DiscoInfo()
|
||||||
|
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||||
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
|
_ = c.Send(iqResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle iq error responses
|
||||||
|
func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok || iq.Type != stanza.IQTypeGet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
discoItems, ok := iq.Payload.(*stanza.DiscoItems)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix this...
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := iqResp.DiscoItems()
|
||||||
|
|
||||||
|
if discoItems.Node == "" {
|
||||||
|
items.AddItem("service.localhost", "node1", "test node")
|
||||||
|
}
|
||||||
|
_ = c.Send(iqResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleVersion(c xmpp.Sender, p stanza.Packet) {
|
||||||
|
// Type conversion & sanity checks
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
|
||||||
|
_ = c.Send(iqResp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# xmpp_component2
|
||||||
|
|
||||||
|
|
||||||
|
This program is an example of the simplest XMPP component: it connects to an XMPP server using XEP 114 protocol, perform a discovery query on the server and print the response.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Connect to an XMPP server using XEP 114 protocol, perform a discovery query on the server and print the response
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
domain = "mycomponent.localhost"
|
||||||
|
address = "build.vpn.p1:8888"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init and return a component
|
||||||
|
func makeComponent() *xmpp.Component {
|
||||||
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: address,
|
||||||
|
Domain: domain,
|
||||||
|
},
|
||||||
|
Domain: domain,
|
||||||
|
Secret: "secret",
|
||||||
|
}
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
c, err := xmpp.NewComponent(opts, router, handleError)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := makeComponent()
|
||||||
|
|
||||||
|
// Connect Component to the server
|
||||||
|
fmt.Printf("Connecting to %v\n", address)
|
||||||
|
err := c.Connect()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a disco iq
|
||||||
|
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet,
|
||||||
|
From: domain,
|
||||||
|
To: "localhost",
|
||||||
|
Id: "my-iq1"})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create IQ: %v", err)
|
||||||
|
}
|
||||||
|
disco := iqReq.DiscoInfo()
|
||||||
|
iqReq.Payload = disco
|
||||||
|
|
||||||
|
// res is the channel used to receive the result iq
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
res, _ := c.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case iqResponse := <-res:
|
||||||
|
// Got response from server
|
||||||
|
fmt.Print(iqResponse.Payload)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
panic("No iq response was received in time")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
xmpp_echo is a demo client that connect on an XMPP server and echo message received back to original sender.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("test"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
|
_ = s.Send(reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Jukebox example
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- You need mpg123 installed on your computer because the example runs it as a command :
|
||||||
|
[Official MPG123 website](https://mpg123.de/)
|
||||||
|
Most linux distributions have a package for it.
|
||||||
|
- You need a soundcloud ID to play a music from the website through mpg123. You currently cannot play music files with this example.
|
||||||
|
Your user ID is available in your account settings on the [soundcloud website](https://soundcloud.com/)
|
||||||
|
**One is provided for convenience.**
|
||||||
|
- You need a running jabber server. You can run your local instance of [ejabberd](https://www.ejabberd.im/) for example.
|
||||||
|
- You need a registered user on the running jabber server.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
You can edit the soundcloud ID in the example file with your own, or use the provided one :
|
||||||
|
```go
|
||||||
|
const scClientID = "dde6a0075614ac4f3bea423863076b22"
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the example, build it with (while in the example directory) :
|
||||||
|
```
|
||||||
|
go build xmpp_jukebox.go
|
||||||
|
```
|
||||||
|
|
||||||
|
then run it with (update the command arguments accordingly):
|
||||||
|
```
|
||||||
|
./xmpp_jukebox -jid=MY_USERE@MY_DOMAIN/jukebox -password=MY_PASSWORD -address=MY_SERVER:MY_SERVER_PORT
|
||||||
|
```
|
||||||
|
Make sure to have a resource, for instance "/jukebox", on your jid.
|
||||||
|
|
||||||
|
Then you can send the following stanza to "MY_USERE@MY_DOMAIN/jukebox" (with the resource) to play a song (update the soundcloud URL accordingly) :
|
||||||
|
```xml
|
||||||
|
<iq id="1" to="MY_USERE@MY_DOMAIN/jukebox" type="set">
|
||||||
|
<set xml:lang="en" xmlns="urn:xmpp:iot:control">
|
||||||
|
<string name="url" value="https://soundcloud.com/UPDATE/ME"/>
|
||||||
|
</set>
|
||||||
|
</iq>
|
||||||
|
```
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// Can be launched with:
|
||||||
|
// ./xmpp_jukebox -jid=test@localhost/jukebox -password=test -address=localhost:5222
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/processone/mpg123"
|
||||||
|
"github.com/processone/soundcloud"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the actual song Stream URL from SoundCloud website song URL and play it with mpg123 player.
|
||||||
|
const scClientID = "dde6a0075614ac4f3bea423863076b22"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
jid := flag.String("jid", "", "jukebok XMPP Jid, resource is optional")
|
||||||
|
password := flag.String("password", "", "XMPP account password")
|
||||||
|
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 1. Create mpg player
|
||||||
|
player, err := mpg123.NewPlayer()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Prepare XMPP client
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: *address,
|
||||||
|
},
|
||||||
|
Jid: *jid,
|
||||||
|
Credential: xmpp.Password(*password),
|
||||||
|
// StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.NewRoute().
|
||||||
|
Packet("message").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
handleMessage(s, p, player)
|
||||||
|
})
|
||||||
|
router.NewRoute().
|
||||||
|
Packet("iq").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
handleIQ(s, p, player)
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command := strings.Trim(msg.Body, " ")
|
||||||
|
if command == "stop" {
|
||||||
|
player.Stop()
|
||||||
|
} else {
|
||||||
|
playSCURL(player, command)
|
||||||
|
sendUserTune(s, "Radiohead", "Spectre")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||||
|
iq, ok := p.(*stanza.IQ)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch payload := iq.Payload.(type) {
|
||||||
|
// We support IOT Control IQ
|
||||||
|
case *stanza.ControlSet:
|
||||||
|
var url string
|
||||||
|
for _, element := range payload.Fields {
|
||||||
|
if element.XMLName.Local == "string" && element.Name == "url" {
|
||||||
|
url = strings.Trim(element.Value, " ")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playSCURL(player, url)
|
||||||
|
setResponse := new(stanza.ControlSetResponse)
|
||||||
|
// FIXME: Broken
|
||||||
|
reply := stanza.IQ{Attrs: stanza.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
|
||||||
|
_ = s.Send(&reply)
|
||||||
|
// TODO add Soundclound artist / title retrieval
|
||||||
|
sendUserTune(s, "Radiohead", "Spectre")
|
||||||
|
default:
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", iq.Payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendUserTune(s xmpp.Sender, artist string, title string) {
|
||||||
|
rq, err := stanza.NewPublishItemRq("localhost",
|
||||||
|
"http://jabber.org/protocol/tune",
|
||||||
|
"",
|
||||||
|
stanza.Item{
|
||||||
|
XMLName: xml.Name{Space: "http://jabber.org/protocol/tune", Local: "tune"},
|
||||||
|
Any: &stanza.Node{
|
||||||
|
Nodes: []stanza.Node{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Local: "artist"},
|
||||||
|
Content: artist,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Local: "title"},
|
||||||
|
Content: title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to build the publish request : %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.Send(rq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func playSCURL(p *mpg123.Player, rawURL string) {
|
||||||
|
songID, _ := soundcloud.GetSongID(rawURL)
|
||||||
|
// TODO: Maybe we need to check the track itself to get the stream URL from reply ?
|
||||||
|
url := soundcloud.FormatStreamURL(songID)
|
||||||
|
|
||||||
|
_ = p.Play(strings.ReplaceAll(url, "YOUR_SOUNDCLOUD_CLIENTID", scClientID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// - Have a player API to play, play next, or add to queue
|
||||||
|
// - Have the ability to parse custom packet to play sound
|
||||||
|
// - Use PEP to display tunes status
|
||||||
|
// - Ability to "speak" messages
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
xmpp_oauth2 is a demo client that connect on an XMPP server using OAuth2 and prints received messages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.OAuthToken("OdAIsBlY83SLBaqQoClAn7vrZSHxixT8"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
// Insecure: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(err error) {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# PubSub client example
|
||||||
|
|
||||||
|
## Description
|
||||||
|
This is a simple example of a client that :
|
||||||
|
* Creates a node on a service
|
||||||
|
* Subscribes to that node
|
||||||
|
* Publishes to that node
|
||||||
|
* Gets the notification from the publication and prints it on screen
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
You need to have a running jabber server, like [ejabberd](https://www.ejabberd.im/) that supports [XEP-0060](https://xmpp.org/extensions/xep-0060.html).
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
Just run :
|
||||||
|
```
|
||||||
|
go run xmpp_ps_client.go
|
||||||
|
```
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userJID = "testuser2@localhost"
|
||||||
|
serverAddress = "localhost:5222"
|
||||||
|
nodeName = "lel_node"
|
||||||
|
serviceName = "pubsub.localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
var invalidResp = errors.New("invalid response")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: serverAddress,
|
||||||
|
},
|
||||||
|
Jid: userJID,
|
||||||
|
Credential: xmpp.Password("pass123"),
|
||||||
|
// StreamLogger: os.Stdout,
|
||||||
|
Insecure: true,
|
||||||
|
}
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.NewRoute().Packet("message").
|
||||||
|
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
data, _ := xml.Marshal(p)
|
||||||
|
log.Println("Received a message ! => \n" + string(data))
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(&config, router, func(err error) { log.Println(err) })
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Client connection
|
||||||
|
err = client.Connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Create a node
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
createNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ================================================================================
|
||||||
|
// Configure the node. This can also be done in a single message with the creation
|
||||||
|
configureNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ====================================
|
||||||
|
// Subscribe to this node :
|
||||||
|
subToNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Publish to that node
|
||||||
|
pubToNode(ctx, cancel, client)
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// Let's purge the node :
|
||||||
|
purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName)
|
||||||
|
purgeCh, err := client.SendIQ(ctx, purgeRq)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not send purge request: %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case purgeResp := <-purgeCh:
|
||||||
|
|
||||||
|
if purgeResp.Type == stanza.IQTypeError {
|
||||||
|
cancel()
|
||||||
|
if vld, err := purgeResp.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", purgeResp, err)
|
||||||
|
}
|
||||||
|
log.Fatalf("error while purging node : %s", purgeResp.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println("node successfully purged")
|
||||||
|
case <-time.After(1000 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while purging node")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
rqCreate, err := stanza.NewCreateNode(serviceName, nodeName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
createCh, err := client.SendIQ(ctx, rqCreate)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if createCh != nil {
|
||||||
|
select {
|
||||||
|
case respCr := <-createCh:
|
||||||
|
// Got response from server
|
||||||
|
if respCr.Type == stanza.IQTypeError {
|
||||||
|
if vld, err := respCr.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %+v"+" reason: %s", respCr, err)
|
||||||
|
}
|
||||||
|
if respCr.Error.Reason != "conflict" {
|
||||||
|
log.Fatalf("%+v", respCr.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println(respCr.Error.Text)
|
||||||
|
} else {
|
||||||
|
fmt.Print("successfully created channel")
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while creating node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
// First, ask for a form with the config options
|
||||||
|
confRq, _ := stanza.NewConfigureNode(serviceName, nodeName)
|
||||||
|
confReqCh, err := client.SendIQ(ctx, confRq)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not send iq : %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case confForm := <-confReqCh:
|
||||||
|
// If the request was successful, we now have a form with configuration options to update
|
||||||
|
fields, err := confForm.GetFormFields()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("No config fields found !")
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are some common fields expected to be present. Change processing to your liking
|
||||||
|
if fields["pubsub#max_payload_size"] != nil {
|
||||||
|
fields["pubsub#max_payload_size"].ValuesList[0] = "100000"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fields["pubsub#notification_type"] != nil {
|
||||||
|
fields["pubsub#notification_type"].ValuesList[0] = "headline"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the modified fields as a form
|
||||||
|
submitConf, err := stanza.NewFormSubmissionOwner(serviceName,
|
||||||
|
nodeName,
|
||||||
|
[]*stanza.Field{
|
||||||
|
fields["pubsub#max_payload_size"],
|
||||||
|
fields["pubsub#notification_type"],
|
||||||
|
})
|
||||||
|
|
||||||
|
c, _ := client.SendIQ(ctx, submitConf)
|
||||||
|
select {
|
||||||
|
case confResp := <-c:
|
||||||
|
if confResp.Type == stanza.IQTypeError {
|
||||||
|
cancel()
|
||||||
|
if vld, err := confResp.IsValid(); !vld {
|
||||||
|
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", confResp, err)
|
||||||
|
}
|
||||||
|
log.Fatalf("node configuration failed : %s", confResp.Error.Text)
|
||||||
|
}
|
||||||
|
log.Println("node configuration was successful")
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while configuring the node")
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while asking for the config form")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func subToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
rqSubscribe, err := stanza.NewSubRq(serviceName, stanza.SubInfo{
|
||||||
|
Node: nodeName,
|
||||||
|
Jid: userJID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
subRespCh, _ := client.SendIQ(ctx, rqSubscribe)
|
||||||
|
if subRespCh != nil {
|
||||||
|
select {
|
||||||
|
case <-subRespCh:
|
||||||
|
log.Println("Subscribed to the service")
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while subscribing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
|
||||||
|
pub, err := stanza.NewPublishItemRq(serviceName, nodeName, "", stanza.Item{
|
||||||
|
Publisher: "testuser2",
|
||||||
|
Any: &stanza.Node{
|
||||||
|
XMLName: xml.Name{
|
||||||
|
Space: "http://www.w3.org/2005/Atom",
|
||||||
|
Local: "entry",
|
||||||
|
},
|
||||||
|
Nodes: []stanza.Node{
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "title"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item title",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "summary"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item content summary",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "link"},
|
||||||
|
Attrs: []xml.Attr{
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "rel"},
|
||||||
|
Value: "alternate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "type"},
|
||||||
|
Value: "text/html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: xml.Name{Space: "", Local: "href"},
|
||||||
|
Value: "http://denmark.lit/2003/12/13/atom03",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "id"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "My pub item content ID",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "published"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "2003-12-13T18:30:02Z",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
XMLName: xml.Name{Space: "", Local: "updated"},
|
||||||
|
Attrs: nil,
|
||||||
|
Content: "2003-12-13T18:30:02Z",
|
||||||
|
Nodes: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
pubRespCh, _ := client.SendIQ(ctx, pub)
|
||||||
|
if pubRespCh != nil {
|
||||||
|
select {
|
||||||
|
case <-pubRespCh:
|
||||||
|
log.Println("Published item to the service")
|
||||||
|
case <-time.After(300 * time.Millisecond):
|
||||||
|
cancel()
|
||||||
|
log.Fatal("No iq response was received in time while publishing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||