Compare commits

...
47 Commits
Author SHA1 Message Date
Sunglocto 34b885a770 Add rendering forms to messages, add configuring mucs, add getting muc affiliations, add a bunch of other stuff idk man check the diff 2026-10-11 12:08:38 +01:00
Sunglocto 7a875f84a5 you can run from your demons until you are exhausted, one day you will have to stop and find out what they wanted; keep on running to those angels until you are exhausted 2026-08-25 20:21:43 +01:00
Sunglocto a6e74ec808 Rest in peace Dolly Patron 2026-08-25 20:17:23 +01:00
Sunglocto 2717137270 can we pretend that airplanes in the night sky are like shooting stars 2026-08-20 11:01:25 +01:00
Sunglocto 58ae55e061 rotund 2026-08-16 20:07:53 +01:00
Sunglocto eebb6906c6 6-7 2026-08-08 13:54:20 +01:00
Sunglocto 0179d614b9 you need a man, with, sen-si-ti-vi-ty. a man like me 2026-08-07 12:50:35 +01:00
Sunglocto 2794adaa21 Decrement version 2026-08-06 13:35:41 +01:00
Sunglocto 37266ab8d4 homer drops his donut, doh, homer spills his soda, doh, homer hits the doorway, doh, homer ruins his whole day, doh! 2026-08-06 13:33:55 +01:00
Sunglocto 74f5761e06 peanits... hehe 2026-08-06 07:53:46 +01:00
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
240 changed files with 21060 additions and 1060 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
im gonna make my own xmpp client with blackjack and hookers
+1
View File
@@ -0,0 +1 @@
i farted
+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
@@ -2,6 +2,10 @@
an XMPP client
Experimental XMPP client written in Go, with the GTK4 UI toolkit and the Fluux IO XMPP libraries.
icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im))
additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons)
additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) and the [Haiku project](https://www.haiku-os.org/)
pepper & carrot default avatar is from [Pepper&Carrot](https://peppercarrot.com) - created by David Revoy, licensed under Creative Commons Attribution 4.0. Change made: cropped and made black & white
-2
View File
@@ -1,6 +1,4 @@
# TODO
- XEP-0393: Message Styling 0%
- XEP-0402: PEP Native Bookmarks 50% (often lags out client if enabled)
- XEP-0461: Message Replies 0%
- XEP-0444: Message Reactions partial
+219 -243
View File
@@ -2,333 +2,309 @@ package main
import (
_ "embed"
"encoding/base64"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
"sync"
)
//go:embed debug.png
// If you want to change the default profile picture used when Identicons are disabled, you can change the below line to an image in the repository and recompile Lambda.
//go:embed debug_psi.png
var defaultAvatarBytes []byte
var defaultAvatarB64 string = base64.StdEncoding.EncodeToString(defaultAvatarBytes)
//go:embed failed_load.png
var failedBytes []byte
var failedB64 string = base64.StdEncoding.EncodeToString(failedBytes)
//go:embed assets/owner.png
var ownerMedalBytes []byte
var ownerMedalB64 string = base64.StdEncoding.EncodeToString(ownerMedalBytes)
//go:embed assets/admin.png
var adminMedalBytes []byte
var adminMedalB64 string = base64.StdEncoding.EncodeToString(adminMedalBytes)
//go:embed assets/member.png
var memberMedalBytes []byte
var memberMedalB64 string = base64.StdEncoding.EncodeToString(memberMedalBytes)
//go:embed assets/noaffiliation.png
var noneMedalBytes []byte
var noneMedalB64 string = base64.StdEncoding.EncodeToString(noneMedalBytes)
//go:embed assets/outcast.png
var outcastMedalBytes []byte
var outcastMedalB64 string = base64.StdEncoding.EncodeToString(outcastMedalBytes)
//go:embed assets/cancel.png
var cancelBytes []byte
var cancelB64 string = base64.StdEncoding.EncodeToString(cancelBytes)
//go:embed assets/status_away.png
var sABytes []byte
var sAB64 string = base64.StdEncoding.EncodeToString(sABytes)
//go:embed assets/status_busy.png
var sBBytes []byte
var sBB64 string = base64.StdEncoding.EncodeToString(sBBytes)
//go:embed assets/status_chatty.png
var sCBytes []byte
var sCB64 string = base64.StdEncoding.EncodeToString(sCBytes)
//go:embed assets/status_online.png
var sOBytes []byte
var sOB64 string = base64.StdEncoding.EncodeToString(sOBytes)
//go:embed assets/status_xa.png
var xaBytes []byte
var xaB64 string = base64.StdEncoding.EncodeToString(xaBytes)
//go:embed assets/tag.png
var tagBytes []byte
var tagB64 string = base64.StdEncoding.EncodeToString(tagBytes)
//go:embed assets/lambda-disabled.png
var logoDisabledBytes []byte
var logoDisabledB64 string = base64.StdEncoding.EncodeToString(logoDisabledBytes)
//go:embed assets/icon.png
var logoBytes []byte
//go:embed assets/group.png
var groupBytes []byte
var groupB64 string = base64.StdEncoding.EncodeToString(groupBytes)
//go:embed assets/door_in.png
var doorInBytes []byte
var doorInB64 string = base64.StdEncoding.EncodeToString(doorInBytes)
//go:embed assets/door_out.png
var doorOutBytes []byte
var doorOutB64 string = base64.StdEncoding.EncodeToString(doorOutBytes)
//go:embed assets/large_group.png
var largeGroupBytes []byte
var largeGroupB64 string = base64.StdEncoding.EncodeToString(largeGroupBytes)
//go:embed assets/world.png
var worldBytes []byte
var worldB64 string = base64.StdEncoding.EncodeToString(worldBytes)
//go:embed assets/disconnect.png
var disconnectBytes []byte
var disconnectB64 string = base64.StdEncoding.EncodeToString(disconnectBytes)
//go:embed assets/chart_bar.png
var barBytes []byte
var barB64 string = base64.StdEncoding.EncodeToString(barBytes)
//go:embed assets/chart_bar_laggy.png
var barLaggyBytes []byte
//go:embed assets/ok.png
var okBytes []byte
var okB64 string = base64.StdEncoding.EncodeToString(okBytes)
//go:embed assets/hourglass.png
var hourglassBytes []byte
var hourglassB64 string = base64.StdEncoding.EncodeToString(hourglassBytes)
//go:embed assets/connect_tls.png
var connectBytes []byte
var connectB64 string = base64.StdEncoding.EncodeToString(connectBytes)
//go:embed assets/comment.png
var commentBytes []byte
var commentB64 string = base64.StdEncoding.EncodeToString(commentBytes)
//go:embed assets/information.png
var informationBytes []byte
var informationB64 string = base64.StdEncoding.EncodeToString(informationBytes)
//go:embed assets/car.png
var carBytes []byte
//go:embed assets/car_high.png
var carHighBytes []byte
// muc icons
//go:embed assets/muc_open.png
var mucOpenBytes []byte
//go:embed assets/muc_membersonly.png
var mucMembersOnlyBytes []byte
//go:embed assets/muc_passwordprotected.png
var mucPasswordProtectedBytes []byte
//go:embed assets/muc_unsecured.png
var mucUnsecuredBytes []byte
//go:embed assets/muc_hidden.png
var mucHiddenBytes []byte
//go:embed assets/muc_public.png
var mucPublicBytes []byte
//go:embed assets/muc_unmoderated.png
var mucUnmoderatedBytes []byte
//go:embed assets/muc_moderated.png
var mucModeratedBytes []byte
//go:embed assets/muc_nonanonymous.png
var mucNonAnonymousBytes []byte
//go:embed assets/muc_semianonymous.png
var mucSemiAnonymousBytes []byte
//go:embed assets/muc_persistent.png
var mucPersistentBytes []byte
//go:embed assets/muc_temporary.png
var mucTemporaryBytes []byte
//go:embed assets/moderate.png
var moderateBytes []byte
//go:embed assets/jabber.png
var jabberBytes []byte
//go:embed assets/fail.png
var failBytes []byte
//go:embed assets/please_wait.gif
var pleaseWaitBytes []byte
//go:embed assets/update.png
var updateBytes []byte
//go:embed assets/solar.png
var solarBytes []byte
//go:embed assets/timeline_marker.png
var timelineMarkerBytes []byte
//go:embed assets/cap.png
var capIconBytes []byte
// client icons
//go:embed assets/client_icons/monocles.png
var monoclesCapBytes []byte
//go:embed assets/client_icons/gajim.png
var gajimCapBytes []byte
//go:embed assets/client_icons/conversations.png
var conversationsCapBytes []byte
//go:embed assets/client_icons/dino.png
var dinoCapBytes []byte
//go:embed assets/client_icons/blabber.png
var blabberCapBytes []byte
//go:embed assets/client_icons/profanity.png
var profanityCapBytes []byte
//go:embed assets/client_icons/poezio.png
var poezioCapBytes []byte
//go:embed assets/client_icons/mcabber.png
var mcabberCapBytes []byte
//go:embed assets/client_icons/converse.png
var converseJSCapBytes []byte
//go:embed assets/client_icons/pidgin.png
var pidginCapBytes []byte
//go:embed assets/client_icons/anotherim.png
var anotherImCapBytes []byte
//go:embed assets/client_icons/movim.png
var movimCapBytes []byte
//go:embed assets/client_icons/yaxim.png
var yaximCapBytes []byte
//go:embed assets/client_icons/psi.png
var psiCapBytes []byte
//go:embed assets/client_icons/spark.png
var sparkCapBytes []byte
var clientAssets sync.Map
func loadAsset(key string, data []byte) {
loader := gdkpixbuf.NewPixbufLoader()
loader.Write(data)
loader.Close()
clientAssets.Store(key, gdk.NewTextureForPixbuf(loader.Pixbuf()))
}
func clientAssetsLoad(key string) gdk.Paintabler {
dat, ok := clientAssets.Load(key)
if !ok {
return nil
}
paintable, ok := dat.(gdk.Paintabler)
if !ok {
return nil
}
return paintable
}
func init() {
for key, data := range map[string][]byte{
"logo": logoBytes,
"DefaultAvatar": defaultAvatarBytes,
"FailedAvatar": failedBytes,
"owner": ownerMedalBytes,
"admin": adminMedalBytes,
"member": memberMedalBytes,
"none": noneMedalBytes,
"outcast": outcastMedalBytes,
"cancel": cancelBytes,
"tag": tagBytes,
"disabled_logo": logoDisabledBytes,
"group": groupBytes,
"door_in": doorInBytes,
"door_out": doorOutBytes,
"large_group": largeGroupBytes,
"world": worldBytes,
"disconnect": disconnectBytes,
"chart_bar": barBytes,
"chart_bar_laggy": barLaggyBytes,
"ok": okBytes,
"hourglass": hourglassBytes,
"connect": connectBytes,
"comment": commentBytes,
"information": informationBytes,
"status_away": sABytes,
"status_dnd": sBBytes,
"status_chat": sCBytes,
"status_xa": xaBytes,
"status_": sOBytes,
"car": carBytes,
"car_high": carHighBytes,
"muc_open": mucOpenBytes,
"muc_membersonly": mucMembersOnlyBytes,
"muc_passwordprotected": mucPasswordProtectedBytes,
"muc_unsecured": mucUnsecuredBytes,
"muc_hidden": mucHiddenBytes,
"muc_public": mucPublicBytes,
"muc_unmoderated": mucUnmoderatedBytes,
"muc_moderated": mucModeratedBytes,
"muc_nonanonymous": mucNonAnonymousBytes,
"muc_semianonymous": mucSemiAnonymousBytes,
"muc_persistent": mucPersistentBytes,
"muc_temporary": mucTemporaryBytes,
"moderate": moderateBytes,
"jabber": jabberBytes,
"fail": failBytes,
"please_wait": pleaseWaitBytes,
"update": updateBytes,
"solar": solarBytes,
"timeline_marker": timelineMarkerBytes,
"cap": capIconBytes,
"https://monocles.eu": monoclesCapBytes,
"https://gajim.org": gajimCapBytes,
"http://conversations.im": conversationsCapBytes,
"https://dino.im": dinoCapBytes,
"http://blabber.im": blabberCapBytes,
"http://profanity-im.github.io": profanityCapBytes,
"https://poez.io": poezioCapBytes,
"http://mcabber.com/caps": mcabberCapBytes,
"https://conversejs.org": converseJSCapBytes,
"http://pidgin.im/": pidginCapBytes,
"https://dev.narayana.im/narayana/Conversations-Classic": anotherImCapBytes,
"https://movim.eu/": movimCapBytes,
"https://yaxim.org/": yaximCapBytes,
"https://psi-im.org": psiCapBytes,
"https://igniterealtime.org/projects/smack": sparkCapBytes,
} {
loadAsset(key, data)
}
loader := gdkpixbuf.NewPixbufLoader()
defaultAvatarData, _ := base64.StdEncoding.DecodeString(defaultAvatarB64)
loader.Write(defaultAvatarData)
loader.Close()
clientAssets["DefaultAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
failedData, _ := base64.StdEncoding.DecodeString(failedB64)
loader.Write(failedData)
loader.Close()
clientAssets["FailedAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
ownerMedalData, _ := base64.StdEncoding.DecodeString(ownerMedalB64)
loader.Write(ownerMedalData)
loader.Close()
clientAssets["owner"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
cancelData, _ := base64.StdEncoding.DecodeString(cancelB64)
loader.Write(cancelData)
loader.Close()
clientAssets["cancel"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
tagData, _ := base64.StdEncoding.DecodeString(tagB64)
loader.Write(tagData)
loader.Close()
clientAssets["tag"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
adminMedalData, _ := base64.StdEncoding.DecodeString(adminMedalB64)
loader.Write(adminMedalData)
loader.Close()
clientAssets["admin"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
memberMedalData, _ := base64.StdEncoding.DecodeString(memberMedalB64)
loader.Write(memberMedalData)
loader.Close()
clientAssets["member"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
noneMedalData, _ := base64.StdEncoding.DecodeString(noneMedalB64)
loader.Write(noneMedalData)
loader.Close()
clientAssets["none"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
outcastMedalData, _ := base64.StdEncoding.DecodeString(outcastMedalB64)
loader.Write(outcastMedalData)
loader.Close()
clientAssets["outcast"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
disabledLogoData, _ := base64.StdEncoding.DecodeString(logoDisabledB64)
loader.Write(disabledLogoData)
loader.Close()
clientAssets["disabled_logo"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
groupData, _ := base64.StdEncoding.DecodeString(groupB64)
loader.Write(groupData)
loader.Close()
clientAssets["group"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
doorInData, _ := base64.StdEncoding.DecodeString(doorInB64)
loader.Write(doorInData)
loader.Close()
clientAssets["door_in"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
doorOutData, _ := base64.StdEncoding.DecodeString(doorOutB64)
loader.Write(doorOutData)
loader.Close()
clientAssets["door_out"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
largeGroupData, _ := base64.StdEncoding.DecodeString(largeGroupB64)
loader.Write(largeGroupData)
loader.Close()
clientAssets["large_group"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
worldData, _ := base64.StdEncoding.DecodeString(worldB64)
loader.Write(worldData)
loader.Close()
clientAssets["world"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
disconnectData, _ := base64.StdEncoding.DecodeString(disconnectB64)
loader.Write(disconnectData)
loader.Close()
clientAssets["disconnect"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
barData, _ := base64.StdEncoding.DecodeString(barB64)
loader.Write(barData)
loader.Close()
clientAssets["chart_bar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
okData, _ := base64.StdEncoding.DecodeString(okB64)
loader.Write(okData)
loader.Close()
clientAssets["ok"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
hourglassData, _ := base64.StdEncoding.DecodeString(hourglassB64)
loader.Write(hourglassData)
loader.Close()
clientAssets["hourglass"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
connectData, _ := base64.StdEncoding.DecodeString(connectB64)
loader.Write(connectData)
loader.Close()
clientAssets["connect"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
commentData, _ := base64.StdEncoding.DecodeString(commentB64)
loader.Write(commentData)
loader.Close()
clientAssets["comment"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
informationData, _ := base64.StdEncoding.DecodeString(informationB64)
loader.Write(informationData)
loader.Close()
clientAssets["information"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sAData, _ := base64.StdEncoding.DecodeString(sAB64)
loader.Write(sAData)
loader.Close()
clientAssets["status_away"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sBData, _ := base64.StdEncoding.DecodeString(sBB64)
loader.Write(sBData)
loader.Close()
clientAssets["status_dnd"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sCData, _ := base64.StdEncoding.DecodeString(sCB64)
loader.Write(sCData)
loader.Close()
clientAssets["status_chat"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
xaData, _ := base64.StdEncoding.DecodeString(xaB64)
loader.Write(xaData)
loader.Close()
clientAssets["status_xa"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sOData, _ := base64.StdEncoding.DecodeString(sOB64)
loader.Write(sOData)
loader.Close()
clientAssets["status_"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 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: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

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: 1.1 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 B

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+53 -27
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,13 +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 = make(map[string]bool)
var invalidImages sync.Map
func ensureCache() (string, error) {
cachePath := configdir.LocalCache("lambda-im")
@@ -31,49 +36,70 @@ 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 setImageFromPath(path string, i *gtk.Image) error {
tex, err := getTexture(path)
if err != nil {
return err
}
i.SetFromPaintable(tex)
return nil
}
func newPictureFromWeb(url string) (*gtk.Picture, error) {
pa, _ := ensureCache()
// 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)
@@ -81,31 +107,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)
@@ -113,7 +139,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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File
File renamed without changes.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 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)
}
Loaded 100 of 240 files, more files were not shown because too many files have changed in this diff. Show more