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 | ||
|
|
359e8ed63e | ||
|
|
3b9df94cf6 | ||
|
|
634f451595 | ||
|
|
00d7945f70 |
No files matched your search
@@ -0,0 +1,15 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
As you might have gathered, this Gitea instance does not allow registrations. If you want to have an account on this instance in order to contribute to this project, you must contact me for an account. I am available on:
|
||||||
|
|
||||||
|
- XMPP: `x@sunglocto.net`
|
||||||
|
- Matrix: `@x:sunglocto.net`
|
||||||
|
- E-Mail: sunglocto <img width=16px height=16px src="https://sunglocto.net/icons/email.gif"/> protonmail.com
|
||||||
|
|
||||||
|
For actually submitting your patches, you can just fork this repo and submit a pull request. I currently have no contribution policy that you have to follow however if you just use common sense you should be good.
|
||||||
|
|
||||||
|
## AI contributions
|
||||||
|
|
||||||
|
Obviously vibe-coded or fully AI-generated contributions will be immediately rejected. This is in order to ensure the stability of the project and decrease the chance of significant bugs or vulnerabilities.
|
||||||
|
|
||||||
|
AI contributions that you have reviewed, edited and **ensured that they compile, run and work successfully** are acceptable. AI is a tool, and like any other tools, can be used for good if used in moderation.
|
||||||
@@ -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: 1.1 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)
|
||||||
|
}
|
||||||