Compare commits

..
64 Commits
Author SHA1 Message Date
sunglocto 546234eacf sorry guys... lambda is now a snapchat client..... 2026-08-03 19:39:57 +01:00
sunglocto 7a822e8eaf Add blocking, add note to self to tabs and add snit's default avatar img 2026-07-30 01:39:31 +01:00
sunglocto b07f92853d NOBODYSETISGONELETSGO 2026-07-17 18:38:35 +01:00
sunglocto 3bb74eb672 i farted2 2026-07-17 17:20:18 +01:00
sunglocto 8cf0f84ddc uhh whats a submodule i have never heard of that bro 2026-07-17 17:18:21 +01:00
sunglocto 006b43a590 lambda is free of this insane bug 2026-07-17 17:15:41 +01:00
sunglocto 120b0c2647 Fix insecure TLS and bump version to 3 2026-07-16 17:08:43 +01:00
sunglocto a3702a900c AV hash custom nicks 2026-07-16 09:05:03 +01:00
sunglocto ede81d551c this commit was sponsored by mizkif 2026-07-14 16:25:18 +01:00
sunglocto 7c19abb6ae Add update check, upgrade dependencies and format code and change ver 2026-07-14 07:51:46 +01:00
sunglocto 8f93dde120 status msg in userbox 2026-07-10 09:52:18 +01:00
sunglocto ca7b82dab5 compact mode changes 2026-06-03 06:38:31 +01:00
sunglocto fce8df9e84 fixed member window 2026-06-01 10:37:22 +01:00
sunglocto 1597f99eef format code + make showing avatars in member lists optional 2026-05-31 16:44:04 +01:00
sunglocto 9ad48ff310 a 2026-05-23 06:45:18 +01:00
sunglocto af0ec78eae format code 2026-05-19 14:07:21 +01:00
sunglocto 5f95df147f h 2026-05-18 06:27:55 +01:00
sunglocto 98cd855349 some qol stuff 2026-05-10 14:46:19 +01:00
sunglocto 1aa14f9fd5 compact mode 2026-05-09 09:00:44 +01:00
sunglocto 39cdd5e6c1 format + allow closing 2026-05-09 07:33:47 +01:00
sunglocto 9bbefbcea8 t 2026-05-08 06:56:51 +01:00
sunglocto 9593e7c041 some lag fixes and begin work on jingle 2026-05-07 09:16:38 +01:00
sunglocto 82aa2abfbd t 2026-05-03 11:10:15 +01:00
sunglocto 58c7165ce5 fix avatars 2026-05-02 06:56:08 +01:00
sunglocto 5bcf0eda0b t 2026-05-01 21:05:14 +01:00
sunglocto 0bfb140dc7 Fix lag issues 2026-04-30 14:55:17 +01:00
sunglocto 7b63799f0b Add extra fields to vCard and format code 2026-04-28 13:08:16 +01:00
sunglocto a97c42323c Do not let ebassi see this code 2026-04-28 12:58:00 +01:00
sunglocto fc0ed5ac2c somehow sunglocto returned 2026-04-26 10:40:13 +01:00
sunglocto 69994d9856 remove base64 encoding/decoding which was not needed for embedding assets 2026-04-18 11:09:59 +01:00
sunglocto 843687ff2b add throughput, format code, and begin to add MUC preview window 2026-04-18 10:29:07 +01:00
sunglocto 7416aa37e0 Merge branch 'master' of https://forge.sunglocto.net/sunglocto/lambda 2026-04-06 11:05:49 +01:00
sunglocto ce83000e0c i forgor to commit sorry 2026-04-06 11:04:53 +01:00
sunglocto 359e8ed63e 'I like small icons, the big ones weird me out' 2026-03-17 19:04:43 +00:00
sunglocto 3b9df94cf6 add CONTRIBUTING.md 2026-03-17 19:02:52 +00:00
sunglocto 634f451595 Merge branch 'master' of https://forge.sunglocto.net/sunglocto/lambda 2026-03-17 18:56:02 +00:00
sunglocto 00d7945f70 add kick png (not initialised yet) 2026-03-17 18:53:08 +00:00
sunglocto 60d6a287e5 oops 2026-03-15 16:14:27 +00:00
sunglocto d5981b7475 icl i forgot i had this file 2026-03-15 09:58:27 +00:00
sunglocto 523722bae6 Format code 2026-03-15 09:57:08 +00:00
sunglocto 71e6a58fbd ban, kick and change role&affil menu, status icons in muc, blocking avatars that are invalid so we dont fetch them over and over again 2026-03-15 09:55:26 +00:00
sunglocto a7e90e4ae5 fix avatars in rc menu 2026-03-12 16:17:29 +00:00
sunglocto 654ab8b618 add some icons and only store members by resource 2026-03-12 16:09:22 +00:00
sunglocto 7a201808e3 format code & add additional assets 2026-03-10 16:48:10 +00:00
sunglocto 77e4e444d4 Attention and experimental mentions impl 2026-03-10 16:35:56 +00:00
sunglocto bf1685a382 MODS 2026-02-28 16:24:37 +00:00
sunglocto 0ac43946b1 this commit was sponsored by RAID SHADOW LEGENDS 2026-02-22 08:16:14 +00:00
sunglocto 15ff7a20db prevent loading svg and webp if on netbsd (thanks gtk) 2026-02-20 09:01:40 +00:00
sunglocto 1dd3f09fed ping graph 2026-02-20 06:28:31 +00:00
sunglocto 87bdbc440a i'll do as much, for my true love, as any young girl may 2026-02-19 14:30:37 +00:00
sunglocto 62b5a9db72 cold blows the wind over my true love, cold blows the drops of rain 2026-02-19 14:30:04 +00:00
sunglocto 39156af48a format code, add confirmation to destroy muc, implement some disconnect logic 2026-02-19 11:28:18 +00:00
sunglocto 3f40d3da29 stat bar 2026-02-17 09:27:46 +00:00
sunglocto a7a49f7441 format code 2026-02-16 09:54:37 +00:00
sunglocto e026e777f6 BOOKMARKS! BOOKMARKS! WE GOT BOOKMARKS PEOPLE 2026-02-16 09:52:25 +00:00
sunglocto 09a809c102 add Destroy room button, show error notification on presence error and show disabled lambda icon when not focused on any chat 2026-02-15 13:08:52 +00:00
sunglocto 29ef37e237 Format code, remove use of Mellium JID parser, and add indicator for messages originating from WebXDC apps 2026-02-14 22:38:32 +00:00
sunglocto 713cb24508 Add hat icon, change some CSS and remove all reply support 2026-02-08 09:44:45 +00:00
sunglocto 589101c292 add info to readme 2026-02-04 19:29:56 +00:00
sunglocto f807565cb2 Add nickname fallback for rooms that don't have XEP-0421 2026-02-04 17:57:24 +00:00
sunglocto 6626d35920 fix some crashes and debug smtn 2026-02-04 10:12:49 +00:00
sunglocto 5c76729a6b Attempt to fix duplicated user tabs 2026-02-03 10:38:41 +00:00
sunglocto c260b8b231 add more support for other message types 2026-02-03 10:07:33 +00:00
sunglocto 971147dcb8 add icon 2026-02-02 18:57:11 +00:00
223 changed files with 20588 additions and 692 deletions
+15
View File
@@ -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.
+5 -1
View File
@@ -1,3 +1,7 @@
# lambda
an XMPP client
an XMPP client
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)
+2 -3
View File
@@ -1,7 +1,6 @@
# TODO
- XEP-0393: Message Styling 0%
- XEP-0402: PEP Native Bookmarks 0%
- XEP-0066: Out of Band Data partial
- XEP-0461: Message Replies partial
- XEP-0402: PEP Native Bookmarks 50% (often lags out client if enabled)
- XEP-0461: Message Replies 0%
- XEP-0444: Message Reactions partial
+234
View File
@@ -0,0 +1,234 @@
package main
import (
_ "embed"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
"sync"
)
//go:embed debug.png
var defaultAvatarBytes []byte
//go:embed failed_load.png
var failedBytes []byte
//go:embed assets/owner.png
var ownerMedalBytes []byte
//go:embed assets/admin.png
var adminMedalBytes []byte
//go:embed assets/member.png
var memberMedalBytes []byte
//go:embed assets/noaffiliation.png
var noneMedalBytes []byte
//go:embed assets/outcast.png
var outcastMedalBytes []byte
//go:embed assets/cancel.png
var cancelBytes []byte
//go:embed assets/status_away.png
var sABytes []byte
//go:embed assets/status_busy.png
var sBBytes []byte
//go:embed assets/status_chatty.png
var sCBytes []byte
//go:embed assets/status_online.png
var sOBytes []byte
//go:embed assets/status_xa.png
var xaBytes []byte
//go:embed assets/tag.png
var tagBytes []byte
//go:embed assets/lambda-disabled.png
var logoDisabledBytes []byte
//go:embed assets/group.png
var groupBytes []byte
//go:embed assets/door_in.png
var doorInBytes []byte
//go:embed assets/door_out.png
var doorOutBytes []byte
//go:embed assets/large_group.png
var largeGroupBytes []byte
//go:embed assets/world.png
var worldBytes []byte
//go:embed assets/disconnect.png
var disconnectBytes []byte
//go:embed assets/chart_bar.png
var barBytes []byte
//go:embed assets/chart_bar_laggy.png
var barLaggyBytes []byte
//go:embed assets/ok.png
var okBytes []byte
//go:embed assets/hourglass.png
var hourglassBytes []byte
//go:embed assets/connect_tls.png
var connectBytes []byte
//go:embed assets/comment.png
var commentBytes []byte
//go:embed assets/information.png
var informationBytes []byte
//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
// var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler)
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() {
for key, data := range map[string][]byte{
"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,
} {
loadAsset(key, data)
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

+1
View File
@@ -0,0 +1 @@
All client assets are owned by their respective owners
Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

+45 -26
View File
@@ -5,6 +5,7 @@ package main
import (
"crypto/sha256"
"errors"
"fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
@@ -13,10 +14,17 @@ import (
"net/http"
"os"
"path/filepath"
"sync"
)
// 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
// var invalidImages = make(map[string]bool)
var invalidImages sync.Map
func ensureCache() (string, error) {
cachePath := configdir.LocalCache("lambda-im")
@@ -28,49 +36,60 @@ func ensureCache() (string, error) {
return cachePath, nil
}
func getTexture(path string) gdk.Paintabler {
if tex, exists := textureCache[path]; exists {
return tex
func getTexture(path string) (gdk.Paintabler, error) {
tex, exists := textureCache.Load(path)
if exists {
return tex.(gdk.Paintabler), nil
}
tex, err := gdk.NewTextureFromFilename(path) // load once
if err != nil {
panic(err)
return nil, err
}
textureCache[path] = tex
return tex
textureCache.Store(path, tex)
t, ok := tex.(gdk.Paintabler)
if !ok {
return nil, errors.New("invalid type")
}
return t, nil
}
func newPictureFromPath(path string) *gtk.Picture {
tex := getTexture(path)
func newPictureFromPath(path string) (*gtk.Picture, error) {
tex, err := getTexture(path)
if err != nil {
return nil, err
}
img := gtk.NewPictureForPaintable(tex)
return img
return img, nil
}
func newImageFromPath(path string) *gtk.Image {
tex := getTexture(path)
img := gtk.NewImageFromPaintable(tex)
return img
func newImageFromPath(path string) (*gtk.Image, error) {
tex, err := getTexture(path)
if err != nil {
return nil, err
}
return gtk.NewImageFromPaintable(tex), nil
}
func newPictureFromWeb(url string) *gtk.Picture {
func newPictureFromWeb(url string) (*gtk.Picture, error) {
pa, _ := ensureCache()
// step 1: get a sha256 sum of the URL
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
p, ok := textureCache[sum]
p, ok := textureCache.Load(sum)
if ok {
return gtk.NewPictureForPaintable(p)
return gtk.NewPictureForPaintable(p.(gdk.Paintabler)), nil
}
// step 2: download it
resp, err := http.Get(url)
if err != nil {
return nil
return nil, err
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil
return nil, err
}
fullpath := filepath.Join(pa, sum)
@@ -78,31 +97,31 @@ func newPictureFromWeb(url string) *gtk.Picture {
// step 3: save it
err = os.WriteFile(fullpath, b, 0644)
if err != nil {
return nil
return nil, err
}
return newPictureFromPath(fullpath)
}
func newImageFromWeb(url string) *gtk.Image {
func newImageFromWeb(url string) (*gtk.Image, error) {
pa, _ := ensureCache()
// step 1: get a sha256 sum of the URL
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
p, ok := textureCache[sum]
p, ok := textureCache.Load(sum)
if ok {
return gtk.NewImageFromPaintable(p)
return gtk.NewImageFromPaintable(p.(gdk.Paintabler)), nil
}
// step 2: download it
resp, err := http.Get(url)
if err != nil {
return nil
return nil, err
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil
return nil, err
}
fullpath := filepath.Join(pa, sum)
@@ -110,7 +129,7 @@ func newImageFromWeb(url string) *gtk.Image {
// step 3: save it
err = os.WriteFile(fullpath, b, 0644)
if err != nil {
return nil
return nil, err
}
return newImageFromPath(fullpath)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 91 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

+38
View File
@@ -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
+36
View File
@@ -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/
+58
View File
@@ -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
+76
View File
@@ -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
+139
View File
@@ -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
+29
View File
@@ -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.
+158
View File
@@ -0,0 +1,158 @@
# Fluux XMPP
[![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![Coverage Status](https://coveralls.io/repos/github/FluuxIO/go-xmpp/badge.svg?branch=master)](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{})
}
+5
View File
@@ -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)).
+220
View File
@@ -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
}
}
+11
View File
@@ -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 => ./../
+218
View File
@@ -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=
+3
View File
@@ -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"
+10
View File
@@ -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.
+79
View File
@@ -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")
}
}
+56
View File
@@ -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())
}
+37
View File
@@ -0,0 +1,37 @@
# Jukebox example
## Requirements
- You need mpg123 installed on your computer because the example runs it as a command :
[Official MPG123 website](https://mpg123.de/)
Most linux distributions have a package for it.
- You need a soundcloud ID to play a music from the website through mpg123. You currently cannot play music files with this example.
Your user ID is available in your account settings on the [soundcloud website](https://soundcloud.com/)
**One is provided for convenience.**
- You need a running jabber server. You can run your local instance of [ejabberd](https://www.ejabberd.im/) for example.
- You need a registered user on the running jabber server.
## Run
You can edit the soundcloud ID in the example file with your own, or use the provided one :
```go
const scClientID = "dde6a0075614ac4f3bea423863076b22"
```
To run the example, build it with (while in the example directory) :
```
go build xmpp_jukebox.go
```
then run it with (update the command arguments accordingly):
```
./xmpp_jukebox -jid=MY_USERE@MY_DOMAIN/jukebox -password=MY_PASSWORD -address=MY_SERVER:MY_SERVER_PORT
```
Make sure to have a resource, for instance "/jukebox", on your jid.
Then you can send the following stanza to "MY_USERE@MY_DOMAIN/jukebox" (with the resource) to play a song (update the soundcloud URL accordingly) :
```xml
<iq id="1" to="MY_USERE@MY_DOMAIN/jukebox" type="set">
<set xml:lang="en" xmlns="urn:xmpp:iot:control">
<string name="url" value="https://soundcloud.com/UPDATE/ME"/>
</set>
</iq>
```
@@ -0,0 +1,149 @@
// Can be launched with:
// ./xmpp_jukebox -jid=test@localhost/jukebox -password=test -address=localhost:5222
package main
import (
"encoding/xml"
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/processone/mpg123"
"github.com/processone/soundcloud"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
// Get the actual song Stream URL from SoundCloud website song URL and play it with mpg123 player.
const scClientID = "dde6a0075614ac4f3bea423863076b22"
func main() {
jid := flag.String("jid", "", "jukebok XMPP Jid, resource is optional")
password := flag.String("password", "", "XMPP account password")
address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)")
flag.Parse()
// 1. Create mpg player
player, err := mpg123.NewPlayer()
if err != nil {
log.Fatal(err)
}
// 2. Prepare XMPP client
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: *address,
},
Jid: *jid,
Credential: xmpp.Password(*password),
// StreamLogger: os.Stdout,
Insecure: true,
}
router := xmpp.NewRouter()
router.NewRoute().
Packet("message").
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
handleMessage(s, p, player)
})
router.NewRoute().
Packet("iq").
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
handleIQ(s, p, player)
})
client, err := xmpp.NewClient(&config, router, errorHandler)
if err != nil {
log.Fatalf("%+v", err)
}
cm := xmpp.NewStreamManager(client, nil)
log.Fatal(cm.Run())
}
func errorHandler(err error) {
fmt.Println(err.Error())
}
func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
msg, ok := p.(stanza.Message)
if !ok {
return
}
command := strings.Trim(msg.Body, " ")
if command == "stop" {
player.Stop()
} else {
playSCURL(player, command)
sendUserTune(s, "Radiohead", "Spectre")
}
}
func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
iq, ok := p.(*stanza.IQ)
if !ok {
return
}
switch payload := iq.Payload.(type) {
// We support IOT Control IQ
case *stanza.ControlSet:
var url string
for _, element := range payload.Fields {
if element.XMLName.Local == "string" && element.Name == "url" {
url = strings.Trim(element.Value, " ")
break
}
}
playSCURL(player, url)
setResponse := new(stanza.ControlSetResponse)
// FIXME: Broken
reply := stanza.IQ{Attrs: stanza.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
_ = s.Send(&reply)
// TODO add Soundclound artist / title retrieval
sendUserTune(s, "Radiohead", "Spectre")
default:
_, _ = fmt.Fprintf(os.Stdout, "Other IQ Payload: %T\n", iq.Payload)
}
}
func sendUserTune(s xmpp.Sender, artist string, title string) {
rq, err := stanza.NewPublishItemRq("localhost",
"http://jabber.org/protocol/tune",
"",
stanza.Item{
XMLName: xml.Name{Space: "http://jabber.org/protocol/tune", Local: "tune"},
Any: &stanza.Node{
Nodes: []stanza.Node{
{
XMLName: xml.Name{Local: "artist"},
Content: artist,
},
{
XMLName: xml.Name{Local: "title"},
Content: title,
},
},
},
})
if err != nil {
fmt.Printf("failed to build the publish request : %s", err.Error())
return
}
_ = s.Send(rq)
}
func playSCURL(p *mpg123.Player, rawURL string) {
songID, _ := soundcloud.GetSongID(rawURL)
// TODO: Maybe we need to check the track itself to get the stream URL from reply ?
url := soundcloud.FormatStreamURL(songID)
_ = p.Play(strings.ReplaceAll(url, "YOUR_SOUNDCLOUD_CLIENTID", scClientID))
}
// TODO
// - Have a player API to play, play next, or add to queue
// - Have the ability to parse custom packet to play sound
// - Use PEP to display tunes status
// - Ability to "speak" messages
@@ -0,0 +1,54 @@
/*
xmpp_oauth2 is a demo client that connect on an XMPP server using OAuth2 and prints received messages.
*/
package main
import (
"fmt"
"log"
"os"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: "localhost:5222",
// TLSConfig: tls.Config{InsecureSkipVerify: true},
},
Jid: "test@localhost",
Credential: xmpp.OAuthToken("OdAIsBlY83SLBaqQoClAn7vrZSHxixT8"),
StreamLogger: os.Stdout,
// Insecure: true,
}
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
client, err := xmpp.NewClient(&config, router, errorHandler)
if err != nil {
log.Fatalf("%+v", err)
}
// If you pass the client to a connection manager, it will handle the reconnect policy
// for you automatically.
cm := xmpp.NewStreamManager(client, nil)
log.Fatal(cm.Run())
}
func errorHandler(err error) {
fmt.Println(err.Error())
}
func handleMessage(s xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
return
}
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
}
@@ -0,0 +1,17 @@
# PubSub client example
## Description
This is a simple example of a client that :
* Creates a node on a service
* Subscribes to that node
* Publishes to that node
* Gets the notification from the publication and prints it on screen
## Requirements
You need to have a running jabber server, like [ejabberd](https://www.ejabberd.im/) that supports [XEP-0060](https://xmpp.org/extensions/xep-0060.html).
## How to use
Just run :
```
go run xmpp_ps_client.go
```
@@ -0,0 +1,278 @@
package main
import (
"context"
"encoding/xml"
"errors"
"fmt"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"log"
"time"
)
const (
userJID = "testuser2@localhost"
serverAddress = "localhost:5222"
nodeName = "lel_node"
serviceName = "pubsub.localhost"
)
var invalidResp = errors.New("invalid response")
func main() {
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: serverAddress,
},
Jid: userJID,
Credential: xmpp.Password("pass123"),
// StreamLogger: os.Stdout,
Insecure: true,
}
router := xmpp.NewRouter()
router.NewRoute().Packet("message").
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
data, _ := xml.Marshal(p)
log.Println("Received a message ! => \n" + string(data))
})
client, err := xmpp.NewClient(&config, router, func(err error) { log.Println(err) })
if err != nil {
log.Fatalf("%+v", err)
}
// ==========================
// Client connection
err = client.Connect()
if err != nil {
log.Fatalf("%+v", err)
}
// ==========================
// Create a node
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
createNode(ctx, cancel, client)
// ================================================================================
// Configure the node. This can also be done in a single message with the creation
configureNode(ctx, cancel, client)
// ====================================
// Subscribe to this node :
subToNode(ctx, cancel, client)
// ==========================
// Publish to that node
pubToNode(ctx, cancel, client)
// =============================
// Let's purge the node :
purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName)
purgeCh, err := client.SendIQ(ctx, purgeRq)
if err != nil {
log.Fatalf("could not send purge request: %v", err)
}
select {
case purgeResp := <-purgeCh:
if purgeResp.Type == stanza.IQTypeError {
cancel()
if vld, err := purgeResp.IsValid(); !vld {
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", purgeResp, err)
}
log.Fatalf("error while purging node : %s", purgeResp.Error.Text)
}
log.Println("node successfully purged")
case <-time.After(1000 * time.Millisecond):
cancel()
log.Fatal("No iq response was received in time while purging node")
}
cancel()
}
func createNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
rqCreate, err := stanza.NewCreateNode(serviceName, nodeName)
if err != nil {
log.Fatalf("%+v", err)
}
createCh, err := client.SendIQ(ctx, rqCreate)
if err != nil {
log.Fatalf("%+v", err)
} else {
if createCh != nil {
select {
case respCr := <-createCh:
// Got response from server
if respCr.Type == stanza.IQTypeError {
if vld, err := respCr.IsValid(); !vld {
log.Fatalf(invalidResp.Error()+" %+v"+" reason: %s", respCr, err)
}
if respCr.Error.Reason != "conflict" {
log.Fatalf("%+v", respCr.Error.Text)
}
log.Println(respCr.Error.Text)
} else {
fmt.Print("successfully created channel")
}
case <-time.After(100 * time.Millisecond):
cancel()
log.Fatal("No iq response was received in time while creating node")
}
}
}
}
func configureNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
// First, ask for a form with the config options
confRq, _ := stanza.NewConfigureNode(serviceName, nodeName)
confReqCh, err := client.SendIQ(ctx, confRq)
if err != nil {
log.Fatalf("could not send iq : %v", err)
}
select {
case confForm := <-confReqCh:
// If the request was successful, we now have a form with configuration options to update
fields, err := confForm.GetFormFields()
if err != nil {
log.Fatal("No config fields found !")
}
// These are some common fields expected to be present. Change processing to your liking
if fields["pubsub#max_payload_size"] != nil {
fields["pubsub#max_payload_size"].ValuesList[0] = "100000"
}
if fields["pubsub#notification_type"] != nil {
fields["pubsub#notification_type"].ValuesList[0] = "headline"
}
// Send the modified fields as a form
submitConf, err := stanza.NewFormSubmissionOwner(serviceName,
nodeName,
[]*stanza.Field{
fields["pubsub#max_payload_size"],
fields["pubsub#notification_type"],
})
c, _ := client.SendIQ(ctx, submitConf)
select {
case confResp := <-c:
if confResp.Type == stanza.IQTypeError {
cancel()
if vld, err := confResp.IsValid(); !vld {
log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", confResp, err)
}
log.Fatalf("node configuration failed : %s", confResp.Error.Text)
}
log.Println("node configuration was successful")
return
case <-time.After(300 * time.Millisecond):
cancel()
log.Fatal("No iq response was received in time while configuring the node")
}
case <-time.After(300 * time.Millisecond):
cancel()
log.Fatal("No iq response was received in time while asking for the config form")
}
}
func subToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
rqSubscribe, err := stanza.NewSubRq(serviceName, stanza.SubInfo{
Node: nodeName,
Jid: userJID,
})
if err != nil {
log.Fatalf("%+v", err)
}
subRespCh, _ := client.SendIQ(ctx, rqSubscribe)
if subRespCh != nil {
select {
case <-subRespCh:
log.Println("Subscribed to the service")
case <-time.After(300 * time.Millisecond):
cancel()
log.Fatal("No iq response was received in time while subscribing")
}
}
}
func pubToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) {
pub, err := stanza.NewPublishItemRq(serviceName, nodeName, "", stanza.Item{
Publisher: "testuser2",
Any: &stanza.Node{
XMLName: xml.Name{
Space: "http://www.w3.org/2005/Atom",
Local: "entry",
},
Nodes: []stanza.Node{
{
XMLName: xml.Name{Space: "", Local: "title"},
Attrs: nil,
Content: "My pub item title",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "summary"},
Attrs: nil,
Content: "My pub item content summary",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "link"},
Attrs: []xml.Attr{
{
Name: xml.Name{Space: "", Local: "rel"},
Value: "alternate",
},
{
Name: xml.Name{Space: "", Local: "type"},
Value: "text/html",
},
{
Name: xml.Name{Space: "", Local: "href"},
Value: "http://denmark.lit/2003/12/13/atom03",
},
},
},
{
XMLName: xml.Name{Space: "", Local: "id"},
Attrs: nil,
Content: "My pub item content ID",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "published"},
Attrs: nil,
Content: "2003-12-13T18:30:02Z",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "updated"},
Attrs: nil,
Content: "2003-12-13T18:30:02Z",
Nodes: nil,
},
},
},
})
if err != nil {
log.Fatalf("%+v", err)
}
pubRespCh, _ := client.SendIQ(ctx, pub)
if pubRespCh != nil {
select {
case <-pubRespCh:
log.Println("Published item to the service")
case <-time.After(300 * time.Millisecond):
cancel()
log.Fatal("No iq response was received in time while publishing")
}
}
}
@@ -0,0 +1,52 @@
/*
xmpp_websocket is a demo client that connect on an XMPP server using websocket 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: "wss://localhost:5443/ws",
},
Jid: "test@localhost",
Credential: xmpp.Password("test"),
StreamLogger: os.Stdout,
}
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)
}
+105
View File
@@ -0,0 +1,105 @@
package xmpp
import (
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"io"
"gosrc.io/xmpp/stanza"
)
// Credential is used to pass the type of secret that will be used to connect to XMPP server.
// It can be either a password or an OAuth 2 bearer token.
type Credential struct {
secret string
mechanisms []string
}
func Password(pwd string) Credential {
credential := Credential{
secret: pwd,
mechanisms: []string{"PLAIN"},
}
return credential
}
func OAuthToken(token string) Credential {
credential := Credential{
secret: token,
mechanisms: []string{"X-OAUTH2"},
}
return credential
}
// ============================================================================
// Authentication flow for SASL mechanisms
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, credential Credential) (err error) {
var matchingMech string
for _, mech := range credential.mechanisms {
if isSupportedMech(mech, f.Mechanisms.Mechanism) {
matchingMech = mech
break
}
}
switch matchingMech {
case "PLAIN", "X-OAUTH2":
// TODO: Implement other type of SASL mechanisms
return authPlain(socket, decoder, matchingMech, user, credential.secret)
default:
err := fmt.Errorf("no matching authentication (%v) supported by server: %v", credential.mechanisms, f.Mechanisms.Mechanism)
return NewConnError(err, true)
}
}
// Plain authentication: send base64-encoded \x00 user \x00 password
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user string, secret string) error {
raw := "\x00" + user + "\x00" + secret
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
a := stanza.SASLAuth{
Mechanism: mech,
Value: string(enc),
}
data, err := xml.Marshal(a)
if err != nil {
return err
}
n, err := socket.Write(data)
if err != nil {
return err
} else if n == 0 {
return errors.New("failed to write authSASL nonza to socket : wrote 0 bytes")
}
// Next message should be either success or failure.
val, err := stanza.NextPacket(decoder)
if err != nil {
return err
}
switch v := val.(type) {
case stanza.SASLSuccess:
case stanza.SASLFailure:
// v.Any is type of sub-element in failure, which gives a description of what failed.
err := errors.New("auth failure: " + v.Any.Local)
return NewConnError(err, true)
default:
return errors.New("expected SASL success or failure, got " + v.Name())
}
return err
}
// isSupportedMech returns true if the mechanism is supported in the provided list.
func isSupportedMech(mech string, mechanisms []string) bool {
for _, m := range mechanisms {
if mech == m {
return true
}
}
return false
}
+101
View File
@@ -0,0 +1,101 @@
/*
Interesting reference on backoff:
- Exponential Backoff And Jitter (AWS Blog):
https://www.awsarchitectureblog.com/2015/03/backoff.html
We use Jitter as a default for exponential backoff, as the goal of
this module is not to provide precise 'ticks', but good behaviour to
implement retries that are helping the server to recover faster in
case of congestion.
It can be used in several ways:
- Using duration to get next sleep time.
- Using ticker channel to trigger callback function on tick
The functions for Backoff are not threadsafe, but you can:
- Keep the attempt counter on your end and use durationForAttempt(int)
- Use lock in your own code to protect the Backoff structure.
TODO: Implement Backoff Ticker channel
TODO: Implement throttler interface. Throttler could be used to implement various reconnect strategies.
*/
package xmpp
import (
"math"
"math/rand"
"time"
)
const (
defaultBase int = 20 // Backoff base, in ms
defaultFactor int = 2
defaultCap int = 180000 // 3 minutes
)
// backoff provides increasing duration with the number of attempt
// performed. The structure is used to support exponential backoff on
// connection attempts to avoid hammering the server we are connecting
// to.
type backoff struct {
NoJitter bool
Base int
Factor int
Cap int
lastDuration int
attempt int
}
// duration returns the duration to apply to the current attempt.
func (b *backoff) duration() time.Duration {
d := b.durationForAttempt(b.attempt)
b.attempt++
return d
}
// wait sleeps for backoff duration for current attempt.
func (b *backoff) wait() {
time.Sleep(b.duration())
}
// durationForAttempt returns a duration for an attempt number, in a stateless way.
func (b *backoff) durationForAttempt(attempt int) time.Duration {
b.setDefault()
expBackoff := math.Min(float64(b.Cap), float64(b.Base)*math.Pow(float64(b.Factor), float64(b.attempt)))
d := int(math.Trunc(expBackoff))
if !b.NoJitter {
d = rand.Intn(d)
}
return time.Duration(d) * time.Millisecond
}
// reset sets back the number of attempts to 0. This is to be called after a successful operation has been performed,
// to reset the exponential backoff interval.
func (b *backoff) reset() {
b.attempt = 0
}
func (b *backoff) setDefault() {
if b.Base == 0 {
b.Base = defaultBase
}
if b.Cap == 0 {
b.Cap = defaultCap
}
if b.Factor == 0 {
b.Factor = defaultFactor
}
}
/*
We use full jitter as default for now as it seems to provide good behaviour for reconnect.
Base is the default interval between attempts (if backoff Factor was equal to 1)
Attempt is the number of retry for operation. If we start attempt at 0, first sleep equals base.
Cap is the maximum sleep time duration we tolerate between attempts
*/
+22
View File
@@ -0,0 +1,22 @@
package xmpp
import (
"testing"
"time"
)
func TestDurationForAttempt_NoJitter(t *testing.T) {
b := backoff{Base: 25, NoJitter: true}
bInMS := time.Duration(b.Base) * time.Millisecond
if b.durationForAttempt(0) != bInMS {
t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.durationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond)
}
var prevDuration, d time.Duration
for i := 0; i < 10; i++ {
d = b.durationForAttempt(i)
if !(d >= prevDuration) {
t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration)
}
prevDuration = d
}
}
+12
View File
@@ -0,0 +1,12 @@
package xmpp
type BiDirIterator interface {
// Next returns the next element of this iterator, if a response is available within t milliseconds
Next(t int) (BiDirIteratorElt, error)
// Previous returns the previous element of this iterator, if a response is available within t milliseconds
Previous(t int) (BiDirIteratorElt, error)
}
type BiDirIteratorElt interface {
NoOp()
}
+151
View File
@@ -0,0 +1,151 @@
package xmpp
import (
"crypto/tls"
"encoding/xml"
"errors"
"fmt"
"net"
"strings"
"time"
"gosrc.io/xmpp/stanza"
)
// TODO: Should I move this as an extension of the client?
// I should probably make the code more modular, but keep concern separated to keep it simple.
type ServerCheck struct {
address string
domain string
}
func NewChecker(address, domain string) (*ServerCheck, error) {
client := ServerCheck{}
var err error
var host string
if client.address, host, err = extractParams(address); err != nil {
return &client, err
}
if domain != "" {
client.domain = domain
} else {
client.domain = host
}
return &client, nil
}
// Check triggers actual TCP connection, based on previously defined parameters.
func (c *ServerCheck) Check() error {
var tcpconn net.Conn
var err error
timeout := 15 * time.Second
tcpconn, err = net.DialTimeout("tcp", c.address, timeout)
if err != nil {
return err
}
decoder := xml.NewDecoder(tcpconn)
// Send stream open tag
if _, err = fmt.Fprintf(tcpconn, clientStreamOpen, c.domain); err != nil {
return err
}
// Set xml decoder and extract streamID from reply (not used for now)
_, err = stanza.InitStream(decoder)
if err != nil {
return err
}
// extract stream features
var f stanza.StreamFeatures
packet, err := stanza.NextPacket(decoder)
if err != nil {
err = fmt.Errorf("stream open decode features: %s", err)
return err
}
switch p := packet.(type) {
case stanza.StreamFeatures:
f = p
case stanza.StreamError:
return errors.New("open stream error: " + p.Error.Local)
default:
return errors.New("expected packet received while expecting features, got " + p.Name())
}
if _, ok := f.DoesStartTLS(); ok {
_, err = fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
if err != nil {
return err
}
var k stanza.TLSProceed
if err = decoder.DecodeElement(&k, nil); err != nil {
return fmt.Errorf("expecting starttls proceed: %s", err)
}
var tlsConfig tls.Config
tlsConfig.ServerName = c.domain
tlsConn := tls.Client(tcpconn, &tlsConfig)
// We convert existing connection to TLS
if err = tlsConn.Handshake(); err != nil {
return err
}
// We check that cert matches hostname
if err = tlsConn.VerifyHostname(c.domain); err != nil {
return err
}
if err = checkExpiration(tlsConn); err != nil {
return err
}
return nil
}
return errors.New("TLS not supported on server")
}
// Check expiration date for the whole certificate chain and returns an error
// if the expiration date is in less than 48 hours.
func checkExpiration(tlsConn *tls.Conn) error {
checkedCerts := make(map[string]struct{})
for _, chain := range tlsConn.ConnectionState().VerifiedChains {
for _, cert := range chain {
if _, checked := checkedCerts[string(cert.Signature)]; checked {
continue
}
checkedCerts[string(cert.Signature)] = struct{}{}
// Check the expiration.
timeNow := time.Now()
expiresInHours := int64(cert.NotAfter.Sub(timeNow).Hours())
// fmt.Printf("Cert '%s' expires in %d days\n", cert.Subject.CommonName, expiresInHours/24)
if expiresInHours <= 48 {
return fmt.Errorf("certificate '%s' will expire on %s", cert.Subject.CommonName, cert.NotAfter)
}
}
}
return nil
}
func extractParams(addr string) (string, string, error) {
var err error
hostport := strings.Split(addr, ":")
if len(hostport) > 2 {
err = errors.New("too many colons in xmpp server address")
return addr, hostport[0], err
}
// Address is composed of two parts, we are good
if len(hostport) == 2 && hostport[1] != "" {
return addr, hostport[0], err
}
// Port was not passed, we append XMPP default port:
return strings.Join([]string{hostport[0], "5222"}, ":"), hostport[0], err
}
+449
View File
@@ -0,0 +1,449 @@
package xmpp
import (
"context"
"encoding/xml"
"errors"
"io"
"net"
"sync"
"time"
"gosrc.io/xmpp/stanza"
)
//=============================================================================
// EventManager
// SyncConnState represents the current connection state.
type SyncConnState struct {
sync.RWMutex
// Current state of the client. Please use the dedicated getter and setter for this field as they are thread safe.
state ConnState
}
type ConnState = uint8
// getState is a thread-safe getter for the current state
func (scs *SyncConnState) getState() ConnState {
var res ConnState
scs.RLock()
res = scs.state
scs.RUnlock()
return res
}
// setState is a thread-safe setter for the current
func (scs *SyncConnState) setState(cs ConnState) {
scs.Lock()
scs.state = cs
scs.Unlock()
}
// This is a the list of events happening on the connection that the
// client can be notified about.
const (
StateDisconnected ConnState = iota
StateResuming
StateSessionEstablished
StateStreamError
StatePermanentError
InitialPresence = "<presence/>"
)
// Event is a structure use to convey event changes related to client state. This
// is for example used to notify the client when the client get disconnected.
type Event struct {
State SyncConnState
Description string
StreamError string
SMState SMState
}
// SMState holds Stream Management information regarding the session that can be
// used to resume session after disconnect
type SMState struct {
// Stream Management ID
Id string
// Inbound stanza count
Inbound uint
// IP affinity
preferredReconAddr string
// Error
StreamErrorGroup stanza.StanzaErrorGroup
// Track sent stanzas
*stanza.UnAckQueue
// TODO Store max and timestamp, to check if we should retry resumption or not
}
// EventHandler is use to pass events about state of the connection to
// client implementation.
type EventHandler func(Event) error
type EventManager struct {
// Store current state. Please use "getState" and "setState" to access and/or modify this.
CurrentState SyncConnState
// Callback used to propagate connection state changes
Handler EventHandler
}
// updateState changes the CurrentState in the event manager. The state read is threadsafe but there is no guarantee
// regarding the triggered callback function.
func (em *EventManager) updateState(state ConnState) {
em.CurrentState.setState(state)
if em.Handler != nil {
em.Handler(Event{State: em.CurrentState})
}
}
// disconnected changes the CurrentState in the event manager to "disconnected". The state read is threadsafe but there is no guarantee
// regarding the triggered callback function.
func (em *EventManager) disconnected(state SMState) {
em.CurrentState.setState(StateDisconnected)
if em.Handler != nil {
em.Handler(Event{State: em.CurrentState, SMState: state})
}
}
// streamError changes the CurrentState in the event manager to "streamError". The state read is threadsafe but there is no guarantee
// regarding the triggered callback function.
func (em *EventManager) streamError(error, desc string) {
em.CurrentState.setState(StateStreamError)
if em.Handler != nil {
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
}
}
// Client
// ============================================================================
var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ stanzas")
// Client is the main structure used to connect as a client on an XMPP
// server.
type Client struct {
// Store user defined options and states
config *Config
// Session gather data that can be accessed by users of this library
Session *Session
transport Transport
// Router is used to dispatch packets
router *Router
// Track and broadcast connection state
EventManager
// Handle errors from client execution
ErrorHandler func(error)
// Post connection hook. This will be executed on first connection
PostConnectHook func() error
// Post resume hook. This will be executed after the client resumes a lost connection using StreamManagement (XEP-0198)
PostResumeHook func() error
}
/*
Setting up the client / Checking the parameters
*/
// NewClient generates a new XMPP client, based on Config passed as parameters.
// If host is not specified, the DNS SRV should be used to find the host from the domain part of the Jid.
// Default the port to 5222.
func NewClient(config *Config, r *Router, errorHandler func(error)) (c *Client, err error) {
if config.KeepaliveInterval == 0 {
config.KeepaliveInterval = time.Second * 30
}
// Parse Jid
if config.parsedJid, err = stanza.NewJid(config.Jid); err != nil {
err = errors.New("missing jid")
return nil, NewConnError(err, true)
}
if config.Credential.secret == "" {
err = errors.New("missing credential")
return nil, NewConnError(err, true)
}
// Fallback to jid domain
if config.Address == "" {
config.Address = config.parsedJid.Domain
// Fetch SRV DNS-Entries
_, srvEntries, err := net.LookupSRV("xmpp-client", "tcp", config.parsedJid.Domain)
if err == nil && len(srvEntries) > 0 {
// If we found matching DNS records, use the entry with highest weight
bestSrv := srvEntries[0]
for _, srv := range srvEntries {
if srv.Priority <= bestSrv.Priority && srv.Weight >= bestSrv.Weight {
bestSrv = srv
config.Address = ensurePort(srv.Target, int(srv.Port))
}
}
}
}
if config.Domain == "" {
// Fallback to jid domain
config.Domain = config.parsedJid.Domain
}
c = new(Client)
c.config = config
c.router = r
c.ErrorHandler = errorHandler
if c.config.ConnectTimeout == 0 {
c.config.ConnectTimeout = 15 // 15 second as default
}
if config.TransportConfiguration.Domain == "" {
config.TransportConfiguration.Domain = config.parsedJid.Domain
}
c.config.TransportConfiguration.ConnectTimeout = c.config.ConnectTimeout
c.transport = NewClientTransport(c.config.TransportConfiguration)
if config.StreamLogger != nil {
c.transport.LogTraffic(config.StreamLogger)
}
return
}
// Connect establishes a first time connection to a XMPP server.
// It calls the PostConnectHook
func (c *Client) Connect() error {
err := c.connect()
if err != nil {
return err
}
// TODO: Do we always want to send initial presence automatically ?
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
err = c.sendWithWriter(c.transport, []byte(InitialPresence))
// Execute the post first connection hook. Typically this holds "ask for roster" and this type of actions.
if c.PostConnectHook != nil {
err = c.PostConnectHook()
if err != nil {
return err
}
}
// Start the keepalive go routine
keepaliveQuit := make(chan struct{})
go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit)
// Start the receiver go routine
go c.recv(keepaliveQuit)
return err
}
// connect establishes an actual TCP connection, based on previously defined parameters, as well as a XMPP session
func (c *Client) connect() error {
var state SMState
var err error
// This is the TCP connection
streamId, err := c.transport.Connect()
if err != nil {
return err
}
// Client is ok, we now open XMPP session with TLS negotiation if possible and session resume or binding
// depending on state.
if c.Session, err = NewSession(c, state); err != nil {
// Try to get the stream close tag from the server.
go func() {
for {
val, err := stanza.NextPacket(c.transport.GetDecoder())
if err != nil {
c.ErrorHandler(err)
c.disconnected(state)
return
}
switch val.(type) {
case stanza.StreamClosePacket:
// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
c.transport.ReceivedStreamClose()
return
}
}
}()
c.Disconnect()
return err
}
c.Session.StreamId = streamId
c.updateState(StateSessionEstablished)
return err
}
// Resume attempts resuming a Stream Managed session, based on the provided stream management
// state. See XEP-0198
func (c *Client) Resume() error {
c.EventManager.updateState(StateResuming)
err := c.connect()
if err != nil {
return err
}
// Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrieval
// for example.
if c.PostResumeHook != nil {
err = c.PostResumeHook()
}
return err
}
// Disconnect disconnects the client from the server, sending a stream close nonza and closing the TCP connection.
func (c *Client) Disconnect() error {
if c.transport != nil {
err := c.transport.Close()
if c.Session != nil {
c.disconnected(c.Session.SMState)
}
return err
}
// No transport so no connection.
return nil
}
func (c *Client) SetHandler(handler EventHandler) {
c.Handler = handler
}
// Send marshals XMPP stanza and sends it to the server.
func (c *Client) Send(packet stanza.Packet) error {
conn := c.transport
if conn == nil {
return errors.New("client is not connected")
}
data, err := xml.Marshal(packet)
if err != nil {
return errors.New("cannot marshal packet " + err.Error())
}
// Store stanza as non-acked as part of stream management
// See https://xmpp.org/extensions/xep-0198.html#scenarios
if c.config.StreamManagementEnable {
if _, ok := packet.(stanza.SMRequest); !ok {
toStore := stanza.UnAckedStz{Stz: string(data)}
c.Session.SMState.UnAckQueue.Push(&toStore)
}
}
return c.sendWithWriter(c.transport, data)
}
// SendIQ sends an IQ set or get stanza to the server. If a result is received
// the provided handler function will automatically be called.
//
// The provided context should have a timeout to prevent the client from waiting
// forever for an IQ result. For example:
//
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
// result := <- client.SendIQ(ctx, iq)
//
func (c *Client) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
return nil, ErrCanOnlySendGetOrSetIq
}
if err := c.Send(iq); err != nil {
return nil, err
}
return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
}
// SendRaw sends an XMPP stanza as a string to the server.
// It can be invalid XML or XMPP content. In that case, the server will
// disconnect the client. It is up to the user of this method to
// carefully craft the XML content to produce valid XMPP.
func (c *Client) SendRaw(packet string) error {
conn := c.transport
if conn == nil {
return errors.New("client is not connected")
}
// Store stanza as non-acked as part of stream management
// See https://xmpp.org/extensions/xep-0198.html#scenarios
if c.config.StreamManagementEnable {
toStore := stanza.UnAckedStz{Stz: packet}
c.Session.SMState.UnAckQueue.Push(&toStore)
}
return c.sendWithWriter(c.transport, []byte(packet))
}
func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
var err error
_, err = writer.Write(packet)
return err
}
// ============================================================================
// Go routines
// Loop: Receive data from server
func (c *Client) recv(keepaliveQuit chan<- struct{}) {
defer close(keepaliveQuit)
for {
val, err := stanza.NextPacket(c.transport.GetDecoder())
if err != nil {
c.ErrorHandler(err)
c.disconnected(c.Session.SMState)
return
}
// Handle stream errors
switch packet := val.(type) {
case stanza.StreamError:
c.router.route(c, val)
c.streamError(packet.Error.Local, packet.Text)
c.ErrorHandler(errors.New("stream error: " + packet.Error.Local))
// We don't return here, because we want to wait for the stream close tag from the server, or timeout.
c.Disconnect()
// Process Stream management nonzas
case stanza.SMRequest:
answer := stanza.SMAnswer{XMLName: xml.Name{
Space: stanza.NSStreamManagement,
Local: "a",
}, H: c.Session.SMState.Inbound}
err = c.Send(answer)
if err != nil {
c.ErrorHandler(err)
return
}
case stanza.StreamClosePacket:
// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
c.transport.ReceivedStreamClose()
c.Disconnect()
continue
default:
c.Session.SMState.Inbound++
}
// Do normal route processing in a go-routine so we can immediately
// start receiving other stanzas. This also allows route handlers to
// send and receive more stanzas.
go c.router.route(c, val)
}
}
// Loop: send whitespace keepalive to server
// This is use to keep the connection open, but also to detect connection loss
// and trigger proper client connection shutdown.
func keepalive(transport Transport, interval time.Duration, quit <-chan struct{}) {
ticker := time.NewTicker(interval)
for {
select {
case <-ticker.C:
if err := transport.Ping(); err != nil {
// When keepalive fails, we force close the transport. In all cases, the recv will also fail.
ticker.Stop()
_ = transport.Close()
return
}
case <-quit:
ticker.Stop()
return
}
}
}
+608
View File
@@ -0,0 +1,608 @@
package xmpp
import (
"bytes"
"encoding/xml"
"fmt"
"gosrc.io/xmpp/stanza"
"strconv"
"testing"
"time"
)
const (
streamManagementID = "test-stream_management-id"
)
func TestClient_Send(t *testing.T) {
buffer := bytes.NewBufferString("")
client := Client{}
data := []byte("https://da.wikipedia.org/wiki/J%C3%A6vnd%C3%B8gn")
if err := client.sendWithWriter(buffer, data); err != nil {
t.Errorf("Writing failed: %v", err)
}
if buffer.String() != string(data) {
t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String())
}
}
// Stream management test.
// Connection is established, then the server sends supported features and so on.
// After the bind, client attempts a stream management enablement, and server replies in kind.
func Test_StreamManagement(t *testing.T) {
serverDone := make(chan struct{})
clientDone := make(chan struct{})
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesStreamManagment(t, sc) // Send post auth features
bind(t, sc)
enableStreamManagement(t, sc, false, true)
serverDone <- struct{}{}
}, testClientStreamManagement, true, true)
go func() {
var state SMState
var err error
// Client is ok, we now open XMPP session
if client.Session, err = NewSession(client, state); err != nil {
t.Fatalf("failed to open XMPP session: %s", err)
}
clientDone <- struct{}{}
}()
waitForEntity(t, clientDone)
waitForEntity(t, serverDone)
mock.Stop()
}
// Absence of stream management test.
// Connection is established, then the server sends supported features and so on.
// Client has stream management disabled in its config, and should not ask for it. Server is not set up to reply.
func Test_NoStreamManagement(t *testing.T) {
serverDone := make(chan struct{})
clientDone := make(chan struct{})
// Setup Mock server
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesNoStreamManagment(t, sc) // Send post auth features
bind(t, sc)
serverDone <- struct{}{}
}, testClientStreamManagement, true, false)
go func() {
var state SMState
// Client is ok, we now open XMPP session
var err error
if client.Session, err = NewSession(client, state); err != nil {
t.Fatalf("failed to open XMPP session: %s", err)
}
clientDone <- struct{}{}
}()
waitForEntity(t, clientDone)
waitForEntity(t, serverDone)
mock.Stop()
}
func Test_StreamManagementNotSupported(t *testing.T) {
serverDone := make(chan struct{})
clientDone := make(chan struct{})
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesNoStreamManagment(t, sc) // Send post auth features
bind(t, sc)
serverDone <- struct{}{}
}, testClientStreamManagement, true, true)
go func() {
var state SMState
var err error
// Client is ok, we now open XMPP session
if client.Session, err = NewSession(client, state); err != nil {
t.Fatalf("failed to open XMPP session: %s", err)
}
clientDone <- struct{}{}
}()
// Wait for client
waitForEntity(t, clientDone)
// Check if client got a positive stream management response from the server
if client.Session.Features.DoesStreamManagement() {
t.Fatalf("server does not provide stream management")
}
// Wait for server
waitForEntity(t, serverDone)
mock.Stop()
}
func Test_StreamManagementNoResume(t *testing.T) {
serverDone := make(chan struct{})
clientDone := make(chan struct{})
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesStreamManagment(t, sc) // Send post auth features
bind(t, sc)
enableStreamManagement(t, sc, false, false)
serverDone <- struct{}{}
}, testClientStreamManagement, true, true)
go func() {
var state SMState
var err error
// Client is ok, we now open XMPP session
if client.Session, err = NewSession(client, state); err != nil {
t.Fatalf("failed to open XMPP session: %s", err)
}
clientDone <- struct{}{}
}()
waitForEntity(t, clientDone)
if IsStreamResumable(client) {
t.Fatalf("server does not support resumption but client says stream is resumable")
}
waitForEntity(t, serverDone)
mock.Stop()
}
func Test_StreamManagementResume(t *testing.T) {
serverDone := make(chan struct{})
clientDone := make(chan struct{})
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesStreamManagment(t, sc) // Send post auth features
bind(t, sc)
enableStreamManagement(t, sc, false, true)
discardPresence(t, sc)
serverDone <- struct{}{}
})
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true,
StreamManagementEnable: true,
streamManagementResume: true} // Enable stream management
var client *Client
router := NewRouter()
client, err := NewClient(&config, router, clientDefaultErrorHandler)
if err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
// =================================================================
// Connect client, then disconnect it so we can resume the session
go func() {
err = client.Connect()
if err != nil {
t.Fatalf("could not connect client to mock server: %s", err)
}
clientDone <- struct{}{}
}()
waitForEntity(t, clientDone)
// ===========================================================================================
// Check that the client correctly went into "disconnected" state, after being disconnected
statusCorrectChan := make(chan struct{})
kill := make(chan struct{})
transp, ok := client.transport.(*XMPPTransport)
if !ok {
t.Fatalf("problem with client transport ")
}
transp.conn.Close()
waitForEntity(t, serverDone)
mock.Stop()
go checkClientResumeStatus(client, statusCorrectChan, kill)
select {
case <-statusCorrectChan:
// Test passed
case <-time.After(5 * time.Second):
kill <- struct{}{}
t.Fatalf("Client is not in disconnected state while it should be. Timed out")
}
// Check if the client can have its connection resumed using its state but also its configuration
if !IsStreamResumable(client) {
t.Fatalf("should support resumption")
}
// Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler
// and they should be different between a first connection and a stream resume since exchanged messages
// are different (See XEP-0198)
mock2 := ServerMock{}
mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
// Reconnect
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesStreamManagment(t, sc) // Send post auth features
resumeStream(t, sc)
serverDone <- struct{}{}
})
// Reconnect
go func() {
err = client.Resume()
if err != nil {
t.Fatalf("could not connect client to mock server: %s", err)
}
clientDone <- struct{}{}
}()
waitForEntity(t, clientDone)
waitForEntity(t, serverDone)
mock2.Stop()
}
func Test_StreamManagementFail(t *testing.T) {
serverDone := make(chan struct{})
clientDone := make(chan struct{})
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesStreamManagment(t, sc) // Send post auth features
bind(t, sc)
enableStreamManagement(t, sc, true, true)
serverDone <- struct{}{}
})
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true,
StreamManagementEnable: true,
streamManagementResume: true} // Enable stream management
var client *Client
router := NewRouter()
client, err := NewClient(&config, router, clientDefaultErrorHandler)
if err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
var state SMState
go func() {
_, err = client.transport.Connect()
if err != nil {
return
}
// Client is ok, we now open XMPP session
if client.Session, err = NewSession(client, state); err == nil {
t.Fatalf("test is supposed to err")
}
if client.Session.SMState.StreamErrorGroup == nil {
t.Fatalf("error was not stored correctly in session state")
}
clientDone <- struct{}{}
}()
waitForEntity(t, serverDone)
waitForEntity(t, clientDone)
mock.Stop()
}
func Test_SendStanzaQueueWithSM(t *testing.T) {
serverDone := make(chan struct{})
clientDone := make(chan struct{})
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesStreamManagment(t, sc) // Send post auth features
bind(t, sc)
enableStreamManagement(t, sc, false, true)
// Ignore the initial presence sent to the server by the client so we can move on to the next packet.
discardPresence(t, sc)
// Used here to silently discard the IQ sent by the client, in order to later trigger a resend
skipPacket(t, sc)
// Respond to the client ACK request with a number of processed stanzas of 0. This should trigger a resend
// of previously ignored stanza to the server, which this handler element will be expecting.
respondWithAck(t, sc, 0)
serverDone <- struct{}{}
})
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true,
StreamManagementEnable: true,
streamManagementResume: true} // Enable stream management
var client *Client
router := NewRouter()
client, err := NewClient(&config, router, clientDefaultErrorHandler)
if err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
go func() {
err = client.Connect()
client.SendRaw(`<iq id='ls72g593' type='get'>
<query xmlns='jabber:iq:roster'/>
</iq>
`)
// Last stanza was discarded silently by the server. Let's ask an ack for it. This should trigger resend as the server
// will respond with an acknowledged number of stanzas of 0.
r := stanza.SMRequest{}
client.Send(r)
clientDone <- struct{}{}
}()
waitForEntity(t, serverDone)
waitForEntity(t, clientDone)
mock.Stop()
}
//========================================================================
// Helper functions for tests
func skipPacket(t *testing.T, sc *ServerConn) {
var p stanza.IQ
se, err := stanza.NextStart(sc.decoder)
if err != nil {
t.Fatalf("cannot read packet: %s", err)
return
}
if err := sc.decoder.DecodeElement(&p, &se); err != nil {
t.Fatalf("cannot decode packet: %s", err)
return
}
}
func respondWithAck(t *testing.T, sc *ServerConn, h int) {
// Mock server reads the ack request
var p stanza.SMRequest
se, err := stanza.NextStart(sc.decoder)
if err != nil {
t.Fatalf("cannot read packet: %s", err)
return
}
if err := sc.decoder.DecodeElement(&p, &se); err != nil {
t.Fatalf("cannot decode packet: %s", err)
return
}
// Mock server sends the ack response
a := stanza.SMAnswer{
H: uint(h),
}
data, err := xml.Marshal(a)
_, err = sc.connection.Write(data)
if err != nil {
t.Fatalf("failed to send response ack")
}
// Mock server reads the re-sent stanza that was previously discarded intentionally
var p2 stanza.IQ
nse, err := stanza.NextStart(sc.decoder)
if err != nil {
t.Fatalf("cannot read packet: %s", err)
return
}
if err := sc.decoder.DecodeElement(&p2, &nse); err != nil {
t.Fatalf("cannot decode packet: %s", err)
return
}
}
func sendFeaturesStreamManagment(t *testing.T, sc *ServerConn) {
// This is a basic server, supporting only 2 features after auth: stream management & session binding
features := `<stream:features>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
<sm xmlns='urn:xmpp:sm:3'/>
</stream:features>`
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
t.Fatalf("cannot send stream feature: %s", err)
}
}
func sendFeaturesNoStreamManagment(t *testing.T, sc *ServerConn) {
// This is a basic server, supporting only 2 features after auth: stream management & session binding
features := `<stream:features>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
</stream:features>`
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
t.Fatalf("cannot send stream feature: %s", err)
}
}
// enableStreamManagement is a function for the mock server that can either mock a successful session, or fail depending on
// the value of the "fail" boolean. True means the session should fail.
func enableStreamManagement(t *testing.T, sc *ServerConn, fail bool, resume bool) {
// Decode element into pointer storage
var ed stanza.SMEnable
se, err := stanza.NextStart(sc.decoder)
if err != nil {
t.Fatalf("cannot read stream management enable: %s", err)
return
}
if err := sc.decoder.DecodeElement(&ed, &se); err != nil {
t.Fatalf("cannot decode stream management enable: %s", err)
return
}
if fail {
f := stanza.SMFailed{
H: nil,
StreamErrorGroup: &stanza.UnexpectedRequest{},
}
data, err := xml.Marshal(f)
if err != nil {
t.Fatalf("failed to marshall error response: %s", err)
}
sc.connection.Write(data)
} else {
e := &stanza.SMEnabled{
Resume: strconv.FormatBool(resume),
Id: streamManagementID,
}
data, err := xml.Marshal(e)
if err != nil {
t.Fatalf("failed to marshall error response: %s", err)
}
sc.connection.Write(data)
}
}
func resumeStream(t *testing.T, sc *ServerConn) {
h := uint(0)
response := stanza.SMResumed{
PrevId: streamManagementID,
H: &h,
}
data, err := xml.Marshal(response)
if err != nil {
t.Fatalf("failed to marshall stream management enabled response : %s", err)
}
writtenChan := make(chan struct{})
go func() {
sc.connection.Write(data)
writtenChan <- struct{}{}
}()
select {
case <-writtenChan:
// We're done here
return
case <-time.After(defaultTimeout):
t.Fatalf("failed to write enabled nonza to client")
}
}
func checkClientResumeStatus(client *Client, statusCorrectChan chan struct{}, killChan chan struct{}) {
for {
if client.CurrentState.getState() == StateDisconnected {
statusCorrectChan <- struct{}{}
}
select {
case <-killChan:
return
case <-time.After(time.Millisecond * 10):
// Keep checking status value
}
}
}
func initSrvCliForResumeTests(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int, StreamManagementEnable, StreamManagementResume bool) (*Client, *ServerMock) {
mock := &ServerMock{}
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
mock.Start(t, testServerAddress, serverHandler)
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testServerAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true,
StreamManagementEnable: StreamManagementEnable,
streamManagementResume: StreamManagementResume}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
t.Fatalf("connect create XMPP client: %s", err)
}
if _, err = client.transport.Connect(); err != nil {
t.Fatalf("XMPP connection failed: %s", err)
}
return client, mock
}
func waitForEntity(t *testing.T, entityDone chan struct{}) {
select {
case <-entityDone:
case <-time.After(defaultTimeout):
t.Fatalf("test timed out")
}
}
+659
View File
@@ -0,0 +1,659 @@
package xmpp
import (
"context"
"encoding/xml"
"errors"
"fmt"
"testing"
"time"
"gosrc.io/xmpp/stanza"
)
const (
// Default port is not standard XMPP port to avoid interfering
// with local running XMPP server
testXMPPAddress = "localhost:15222"
testClientDomain = "localhost"
)
func TestEventManager(t *testing.T) {
mgr := EventManager{}
mgr.updateState(StateResuming)
if mgr.CurrentState.getState() != StateResuming {
t.Fatal("CurrentState not updated by updateState()")
}
mgr.disconnected(SMState{})
if mgr.CurrentState.getState() != StateDisconnected {
t.Fatalf("CurrentState not reset by disconnected()")
}
mgr.streamError(ErrTLSNotSupported.Error(), "")
if mgr.CurrentState.getState() != StateStreamError {
t.Fatalf("CurrentState not set by streamError()")
}
}
func TestClient_Connect(t *testing.T) {
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, handlerClientConnectSuccess)
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
if err = client.Connect(); err != nil {
t.Errorf("XMPP connection failed: %s", err)
}
mock.Stop()
}
func TestClient_NoInsecure(t *testing.T) {
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
handlerAbortTLS(t, sc)
closeConn(t, sc)
})
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
t.Errorf("cannot create XMPP client: %s", err)
}
if err = client.Connect(); err == nil {
// When insecure is not allowed:
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
}
mock.Stop()
}
// Check that the client is properly tracking features, as session negotiation progresses.
func TestClient_FeaturesTracking(t *testing.T) {
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
handlerAbortTLS(t, sc)
closeConn(t, sc)
})
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
t.Errorf("cannot create XMPP client: %s", err)
}
if err = client.Connect(); err == nil {
// When insecure is not allowed:
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
}
mock.Stop()
}
func TestClient_RFC3921Session(t *testing.T) {
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, handlerClientConnectWithSession)
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true,
}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
if err = client.Connect(); err != nil {
t.Errorf("XMPP connection failed: %s", err)
}
mock.Stop()
}
// Testing sending an IQ to the mock server and reading its response.
func TestClient_SendIQ(t *testing.T) {
done := make(chan struct{})
// Handler for Mock server
h := func(t *testing.T, sc *ServerConn) {
handlerClientConnectSuccess(t, sc)
discardPresence(t, sc)
respondToIQ(t, sc)
done <- struct{}{}
}
client, mock := mockClientConnection(t, h, testClientIqPort)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
if err != nil {
t.Fatalf("failed to create the IQ request: %v", err)
}
disco := iqReq.DiscoInfo()
iqReq.Payload = disco
// Handle a possible error
errChan := make(chan error)
errorHandler := func(err error) {
errChan <- err
}
client.ErrorHandler = errorHandler
res, err := client.SendIQ(ctx, iqReq)
if err != nil {
t.Error(err)
}
select {
case <-res: // If the server responds with an IQ, we pass the test
case err := <-errChan: // If the server sends an error, or there is a connection error
cancel()
t.Fatal(err.Error())
case <-time.After(defaultChannelTimeout): // If we timeout
cancel()
t.Fatal("Failed to receive response, to sent IQ, from mock server")
}
select {
case <-done:
mock.Stop()
case <-time.After(defaultChannelTimeout):
cancel()
t.Fatal("The mock server failed to finish its job !")
}
cancel()
}
func TestClient_SendIQFail(t *testing.T) {
done := make(chan struct{})
// Handler for Mock server
h := func(t *testing.T, sc *ServerConn) {
handlerClientConnectSuccess(t, sc)
discardPresence(t, sc)
respondToIQ(t, sc)
done <- struct{}{}
}
client, mock := mockClientConnection(t, h, testClientIqFailPort)
//==================
// Create an IQ to send
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
if err != nil {
t.Fatalf("failed to create IQ request: %v", err)
}
disco := iqReq.DiscoInfo()
iqReq.Payload = disco
// Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified
// so we need to overwrite it.
iqReq.Id = ""
// Handle a possible error
errChan := make(chan error)
errorHandler := func(err error) {
errChan <- err
}
client.ErrorHandler = errorHandler
res, _ := client.SendIQ(ctx, iqReq)
// Test
select {
case <-res: // If the server responds with an IQ
t.Errorf("Server should not respond with an IQ since the request is expected to be invalid !")
case <-errChan: // If the server sends an error, the test passes
case <-time.After(defaultChannelTimeout): // If we timeout
t.Errorf("Failed to receive response, to sent IQ, from mock server")
}
select {
case <-done:
mock.Stop()
case <-time.After(defaultChannelTimeout):
cancel()
t.Errorf("The mock server failed to finish its job !")
}
cancel()
}
func TestClient_SendRaw(t *testing.T) {
done := make(chan struct{})
// Handler for Mock server
h := func(t *testing.T, sc *ServerConn) {
handlerClientConnectSuccess(t, sc)
discardPresence(t, sc)
respondToIQ(t, sc)
closeConn(t, sc)
done <- struct{}{}
}
type testCase struct {
req string
shouldErr bool
port int
}
testRequests := make(map[string]testCase)
// Sending a correct IQ of type get. Not supposed to err
testRequests["Correct IQ"] = testCase{
req: `<iq type="get" id="91bd0bba-012f-4d92-bb17-5fc41e6fe545" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
shouldErr: false,
port: testClientRawPort + 100,
}
// Sending an IQ with a missing ID. Should err
testRequests["IQ with missing ID"] = testCase{
req: `<iq type="get" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
shouldErr: true,
port: testClientRawPort,
}
// A handler for the client.
// In the failing test, the server returns a stream error, which triggers this handler, client side.
errChan := make(chan error)
errHandler := func(err error) {
errChan <- err
}
// Tests for all the IQs
for name, tcase := range testRequests {
t.Run(name, func(st *testing.T) {
//Connecting to a mock server, initialized with given port and handler function
c, m := mockClientConnection(t, h, tcase.port)
c.ErrorHandler = errHandler
// Sending raw xml from test case
err := c.SendRaw(tcase.req)
if err != nil {
t.Errorf("Error sending Raw string")
}
// Just wait a little so the message has time to arrive
select {
// We don't use the default "long" timeout here because waiting it out means passing the test.
case <-time.After(100 * time.Millisecond):
c.Disconnect()
case err = <-errChan:
if err == nil && tcase.shouldErr {
t.Errorf("Failed to get closing stream err")
} else if err != nil && !tcase.shouldErr {
t.Errorf("This test is not supposed to err !")
}
}
select {
case <-done:
m.Stop()
case <-time.After(defaultChannelTimeout):
t.Errorf("The mock server failed to finish its job !")
}
})
}
}
func TestClient_Disconnect(t *testing.T) {
c, m := mockClientConnection(t, func(t *testing.T, sc *ServerConn) {
handlerClientConnectSuccess(t, sc)
closeConn(t, sc)
}, testClientBasePort)
err := c.transport.Ping()
if err != nil {
t.Errorf("Could not ping but not disconnected yet")
}
c.Disconnect()
err = c.transport.Ping()
if err == nil {
t.Errorf("Did not disconnect properly")
}
m.Stop()
}
func TestClient_DisconnectStreamManager(t *testing.T) {
// Init mock server
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
handlerAbortTLS(t, sc)
closeConn(t, sc)
})
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
t.Errorf("cannot create XMPP client: %s", err)
}
sman := NewStreamManager(client, nil)
errChan := make(chan error)
runSMan := func(errChan chan error) {
errChan <- sman.Run()
}
go runSMan(errChan)
select {
case <-errChan:
case <-time.After(defaultChannelTimeout):
// When insecure is not allowed:
t.Errorf("should fail as insecure connection is not allowed and server does not support TLS")
}
mock.Stop()
}
func Test_ClientPostConnectHook(t *testing.T) {
done := make(chan struct{})
// Handler for Mock server
h := func(t *testing.T, sc *ServerConn) {
handlerClientConnectSuccess(t, sc)
done <- struct{}{}
}
hookChan := make(chan struct{})
mock := &ServerMock{}
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, testClientPostConnectHook)
mock.Start(t, testServerAddress, h)
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testServerAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
// The post connection client hook should just write to a channel that we will read later.
client.PostConnectHook = func() error {
go func() {
hookChan <- struct{}{}
}()
return nil
}
// Handle a possible error
errChan := make(chan error)
errorHandler := func(err error) {
errChan <- err
}
client.ErrorHandler = errorHandler
if err = client.Connect(); err != nil {
t.Errorf("XMPP connection failed: %s", err)
}
// Check if the post connection client hook was correctly called
select {
case err := <-errChan: // If the server sends an error, or there is a connection error
t.Fatal(err.Error())
case <-time.After(defaultChannelTimeout): // If we timeout
t.Fatal("Failed to call post connection client hook")
case <-hookChan:
// Test succeeded, channel was written to.
}
select {
case <-done:
mock.Stop()
case <-time.After(defaultChannelTimeout):
t.Fatal("The mock server failed to finish its job !")
}
}
func Test_ClientPostReconnectHook(t *testing.T) {
hookChan := make(chan struct{})
// Setup Mock server
mock := ServerMock{}
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesStreamManagment(t, sc) // Send post auth features
bind(t, sc)
enableStreamManagement(t, sc, false, true)
})
// Test / Check result
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testXMPPAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true,
StreamManagementEnable: true,
streamManagementResume: true} // Enable stream management
var client *Client
router := NewRouter()
client, err := NewClient(&config, router, clientDefaultErrorHandler)
if err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
client.PostResumeHook = func() error {
go func() {
hookChan <- struct{}{}
}()
return nil
}
err = client.Connect()
if err != nil {
t.Fatalf("could not connect client to mock server: %s", err)
}
transp, ok := client.transport.(*XMPPTransport)
if !ok {
t.Fatalf("problem with client transport ")
}
transp.conn.Close()
mock.Stop()
// Check if the client can have its connection resumed using its state but also its configuration
if !IsStreamResumable(client) {
t.Fatalf("should support resumption")
}
// Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler
// and they should be different between a first connection and a stream resume since exchanged messages
// are different (See XEP-0198)
mock2 := ServerMock{}
mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
// Reconnect
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendFeaturesStreamManagment(t, sc) // Send post auth features
resumeStream(t, sc)
})
// Reconnect
err = client.Resume()
if err != nil {
t.Fatalf("could not connect client to mock server: %s", err)
}
select {
case <-time.After(defaultChannelTimeout): // If we timeout
t.Fatal("Failed to call post connection client hook")
case <-hookChan:
// Test succeeded, channel was written to.
}
mock2.Stop()
}
//=============================================================================
// Basic XMPP Server Mock Handlers.
// Test connection with a basic straightforward workflow
func handlerClientConnectSuccess(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendBindFeature(t, sc) // Send post auth features
bind(t, sc)
}
// closeConn closes the connection on request from the client
func closeConn(t *testing.T, sc *ServerConn) {
for {
cls, err := stanza.NextPacket(sc.decoder)
if err != nil {
t.Errorf("cannot read from socket: %s", err)
return
}
switch cls.(type) {
case stanza.StreamClosePacket:
sc.connection.Write([]byte(stanza.StreamClose))
return
}
}
}
// We expect client will abort on TLS
func handlerAbortTLS(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
}
// Test connection with mandatory session (RFC-3921)
func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) {
checkClientOpenStream(t, sc)
sendStreamFeatures(t, sc) // Send initial features
readAuth(t, sc.decoder)
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
checkClientOpenStream(t, sc) // Reset stream
sendRFC3921Feature(t, sc) // Send post auth features
bind(t, sc)
session(t, sc)
}
func checkClientOpenStream(t *testing.T, sc *ServerConn) {
err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout))
if err != nil {
t.Fatalf("failed to set deadline: %v", err)
}
defer sc.connection.SetDeadline(time.Time{})
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
var token xml.Token
token, err := sc.decoder.Token()
if err != nil {
t.Fatalf("cannot read next token: %s", err)
}
switch elem := token.(type) {
// Wait for first startElement
case xml.StartElement:
if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" {
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
return
}
if _, err := fmt.Fprintf(sc.connection, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
t.Errorf("cannot write server stream open: %s", err)
}
return
}
}
}
func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int) (*Client, *ServerMock) {
mock := &ServerMock{}
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
mock.Start(t, testServerAddress, serverHandler)
config := Config{
TransportConfiguration: TransportConfiguration{
Address: testServerAddress,
},
Jid: "test@localhost",
Credential: Password("test"),
Insecure: true}
var client *Client
var err error
router := NewRouter()
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
t.Errorf("connect create XMPP client: %s", err)
}
if err = client.Connect(); err != nil {
t.Errorf("XMPP connection failed: %s", err)
}
return client, mock
}
// This really should not be used as is.
// It's just meant to be a placeholder when error handling is not needed at this level
func clientDefaultErrorHandler(err error) {
}
+198
View File
@@ -0,0 +1,198 @@
# fluuxmpp
fluuxIO's XMPP command-line tool
## Installation
To install `fluuxmpp` in your Go path:
```
$ go get -u gosrc.io/xmpp/cmd/fluuxmpp
```
## Usage
```
$ fluuxmpp --help
fluuxIO's xmpp comandline tool
Usage:
fluuxmpp [command]
Available Commands:
check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires
help Help about any command
send is a command-line tool to send to send XMPP messages to users
Flags:
-h, --help help for fluuxmpp
Use "fluuxmpp [command] --help" for more information about a command.
```
### check tls
```
$ fluuxmpp check --help
is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires
Usage:
fluuxmpp check <host[:port]> [flags]
Examples:
fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de
Flags:
-d, --domain string domain if host handle multiple domains
-h, --help help for check
```
### sending messages
```
$ fluuxmpp send --help
is a command-line tool to send to send XMPP messages to users
Usage:
fluuxmpp send <recipient,> [message] [flags]
Examples:
fluuxmpp send to@chat.sum7.eu "Hello World!"
Flags:
--addr string host[:port]
--config string config file (default is ~/.config/fluuxmpp.yml)
-h, --help help for send
--jid string using jid (required)
-m, --muc recipient is a muc (join it before sending messages)
--password string using password for your jid (required)
```
## Examples
### check tls
If you server is on standard port and XMPP domains matches the hostname you can simply use:
```
$ fluuxmpp check chat.sum7.eu
info All checks passed
⇢ address="chat.sum7.eu" domain=""
⇢ main.go:43 main.runCheck
⇢ 2019-07-16T22:01:39.765+02:00
```
You can also pass the port and the XMPP domain if different from the server hostname:
```
$ fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de
info All checks passed
⇢ address="chat.sum7.eu:5222" domain="meckerspace.de"
⇢ main.go:43 main.runCheck
⇢ 2019-07-16T22:01:33.270+02:00
```
Error code will be non-zero in case of error. You can thus use it directly with your usual
monitoring scripts.
### sending messages
Message from arguments:
```bash
$ fluuxmpp send to@example.org "Hello World!"
info client connected
⇢ cmd.go:56 main.glob..func1.1
⇢ 2019-07-17T23:42:43.310+02:00
info send message
muc=false text="Hello World!" to="to@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:42:43.310+02:00
```
Message from STDIN:
```bash
$ journalctl -f | fluuxmpp send to@example.org -
info client connected
⇢ cmd.go:56 main.glob..func1.1
⇢ 2019-07-17T23:40:03.177+02:00
info send message
muc=false text="-- Logs begin at Mon 2019-07-08 22:16:54 CEST. --" to="to@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:40:03.178+02:00
info send message
muc=false text="Jul 17 23:36:46 RECHNERNAME systemd[755]: Started Fetch mails." to="to@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:40:03.178+02:00
^C
```
Multiple recipients:
```bash
$ fluuxmpp send to1@example.org,to2@example.org "Multiple recipient"
info client connected
⇢ cmd.go:56 main.glob..func1.1
⇢ 2019-07-17T23:47:57.650+02:00
info send message
muc=false text="Multiple recipient" to="to1@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:47:57.651+02:00
info send message
muc=false text="Multiple recipient" to="to2@example.org"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:47:57.652+02:00
```
Send to MUC:
```bash
journalctl -f | fluuxmpp send testit@conference.chat.sum7.eu - --muc
info client connected
⇢ cmd.go:56 main.glob..func1.1
⇢ 2019-07-17T23:52:56.269+02:00
info send message
muc=true text="-- Logs begin at Mon 2019-07-08 22:16:54 CEST. --" to="testit@conference.chat.sum7.eu"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:52:56.270+02:00
info send message
muc=true text="Jul 17 23:48:58 RECHNERNAME systemd[755]: mail.service: Succeeded." to="testit@conference.chat.sum7.eu"
⇢ send.go:31 main.send
⇢ 2019-07-17T23:52:56.277+02:00
^C
```
## Authentification
### Configuration file
In `/etc/`, `~/.config` and `.` (here).
You could create the file name `fluuxmpp` with you favorite file extension (e.g. `toml`, `yml`).
e.g. ~/.config/fluuxmpp.toml
```toml
jid = "bot@example.org"
password = "secret"
addr = "example.com:5222"
```
### Environment variables
```bash
export FLUXXMPP_JID='bot@example.org';
export FLUXXMPP_PASSWORD='secret';
export FLUXXMPP_ADDR='example.com:5222';
fluuxmpp send to@example.org "Hello Welt";
```
### Parameters
Warning: This should not be used for production systems, as all users on the system
can read the running processes, and their parameters (and thus the password).
```bash
fluuxmpp send to@example.org "Hello World!" --jid bot@example.org --password secret --addr example.com:5222;
```
+21
View File
@@ -0,0 +1,21 @@
# TODO
## check
### Features
- Use a config file to define the checks to perform as client on an XMPP server.
## send
### Issues
- Remove global variable (like mucToleave)
- Does not report error when trying to connect to a non open port (for example localhost with no server running).
### Features
- configuration
- allow unencrypted
- skip tls verification
- support muc and single user at same time
- send html -> parse console colors to xhtml (is there a easy way or lib for it ?)
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"github.com/bdlm/log"
"github.com/spf13/cobra"
"gosrc.io/xmpp"
)
var domain = ""
var cmdCheck = &cobra.Command{
Use: "check <host[:port]>",
Short: "is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires",
Example: "fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
runCheck(args[0], domain)
},
}
func init() {
cmdRoot.AddCommand(cmdCheck)
cmdCheck.Flags().StringVarP(&domain, "domain", "d", "", "domain if host handle multiple domains")
}
func runCheck(address, domain string) {
logger := log.WithFields(map[string]interface{}{
"address": address,
"domain": domain,
})
client, err := xmpp.NewChecker(address, domain)
if err != nil {
log.Fatal("Error: ", err)
}
if err = client.Check(); err != nil {
logger.Fatal("Failed connection check: ", err)
}
logger.Println("All checks passed")
}
+5
View File
@@ -0,0 +1,5 @@
/*
fluuxmpp: fluuxIO's xmpp comandline tool
*/
package main
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"os"
"github.com/bdlm/log"
stdLogger "github.com/bdlm/std/logger"
)
type hook struct{}
func (h *hook) Fire(entry *log.Entry) error {
switch entry.Level {
case log.PanicLevel:
entry.Logger.Out = os.Stderr
case log.FatalLevel:
entry.Logger.Out = os.Stderr
case log.ErrorLevel:
entry.Logger.Out = os.Stderr
case log.WarnLevel:
entry.Logger.Out = os.Stdout
case log.InfoLevel:
entry.Logger.Out = os.Stdout
case log.DebugLevel:
entry.Logger.Out = os.Stdout
default:
}
return nil
}
func (h *hook) Levels() []stdLogger.Level {
return log.AllLevels
}
+19
View File
@@ -0,0 +1,19 @@
package main
import (
"github.com/bdlm/log"
"github.com/spf13/cobra"
)
// cmdRoot represents the base command when called without any subcommands
var cmdRoot = &cobra.Command{
Use: "fluuxmpp",
Short: "fluuxIO's xmpp comandline tool",
}
func main() {
log.AddHook(&hook{})
if err := cmdRoot.Execute(); err != nil {
log.Fatal(err)
}
}

Some files were not shown because too many files have changed in this diff Show More