initial implementation
This commit is contained in:
commit
2217d19353
4 changed files with 395 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Morgan McMillian
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
10
go.mod
Normal file
10
go.mod
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module git.sr.ht/~thrrgilag/pnut-bridge
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.sr.ht/~thrrgilag/woodstock v0.0.0-20210613221914-3b9e0ef92799
|
||||||
|
github.com/gorilla/websocket v1.4.2
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.62.0
|
||||||
|
)
|
19
go.sum
Normal file
19
go.sum
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
git.sr.ht/~thrrgilag/woodstock v0.0.0-20210613221914-3b9e0ef92799 h1:iiUiZTW7yksLArreiNO1KcAkMmAvmMquz6etgaKzpCU=
|
||||||
|
git.sr.ht/~thrrgilag/woodstock v0.0.0-20210613221914-3b9e0ef92799/go.mod h1:giZm2mNVkz9BeA31LtUC2gwIYfUSUPQINp+tI8EGFTc=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||||
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
345
pnut-bridge.go
Normal file
345
pnut-bridge.go
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~thrrgilag/woodstock"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cfgfile *string
|
||||||
|
|
||||||
|
var done chan interface{}
|
||||||
|
var interrupt chan os.Signal
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PnutAccessToken string `ini:"pnut_access_token"`
|
||||||
|
PnutUsername string `ini:"pnut_username"`
|
||||||
|
MbUrl string `ini:"matterbridge_url"`
|
||||||
|
MbToken string `ini:"matterbridge_token"`
|
||||||
|
Rooms []Room
|
||||||
|
}
|
||||||
|
|
||||||
|
type Room struct {
|
||||||
|
ChannelID string `ini:"pnut_channel"`
|
||||||
|
Gateway string `ini:"gateway"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Meta struct {
|
||||||
|
Timestamp int `json:"timestamp"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
ChannelType string `json:"channel_type"`
|
||||||
|
SubscriptionIds []string `json:"subscription_ids"`
|
||||||
|
Stream Stream `json:"stream"`
|
||||||
|
ConnectionId string `json:"connection_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stream struct {
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Msg struct {
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Messages struct {
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
|
Data []woodstock.Message `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MbMessage struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
UserID string `json:"userid"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
ParentID string `json:"parent_id"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Extra MbExtra `json"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MbExtra struct {
|
||||||
|
File []MbFile `json:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MbFile struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Avatar bool `json:"avatar"`
|
||||||
|
SHA string `json:"sha"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MbOutMsg struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Gateway string `json:"gateway"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PnutOembed struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscribe(connection_id string, access_token string, rooms []Room) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("connection_id", connection_id)
|
||||||
|
for _, _room := range rooms {
|
||||||
|
log.Printf(">> connecting channel %s ...", _room.ChannelID)
|
||||||
|
url := "https://api.pnut.io/v1/channels/" + _room.ChannelID + "/messages?" + params.Encode()
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! unable to create request", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "pnut-bridge")
|
||||||
|
req.Header.Add("Authorization", "Bearer "+access_token)
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! error on reponse", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
log.Printf("<< %s", resp.Status)
|
||||||
|
//body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
//log.Println(string([]byte(body)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveHandler(connection *websocket.Conn, conf *Config) {
|
||||||
|
defer close(done)
|
||||||
|
for {
|
||||||
|
_, msg, err := connection.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! error in websocket receive:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("<< received: %s", msg)
|
||||||
|
var m Msg
|
||||||
|
if err := json.Unmarshal(msg, &m); err != nil {
|
||||||
|
log.Println("!! error unmarshaling msg", err)
|
||||||
|
}
|
||||||
|
if len(m.Meta.ConnectionId) > 0 {
|
||||||
|
log.Println("<< connection", m.Meta.ConnectionId)
|
||||||
|
subscribe(m.Meta.ConnectionId, conf.PnutAccessToken, conf.Rooms)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch m.Meta.ChannelType {
|
||||||
|
case "io.pnut.core.chat":
|
||||||
|
var messages Messages
|
||||||
|
if err := json.Unmarshal(msg, &messages); err != nil {
|
||||||
|
log.Println("!! error unmarshaling message objects", err)
|
||||||
|
}
|
||||||
|
for _, _message := range messages.Data {
|
||||||
|
pnutMsgHandler(conf, _message)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pnutMsgHandler(conf *Config, msg woodstock.Message) {
|
||||||
|
gateway := getRoomGateway(conf.Rooms, msg.ChannelID)
|
||||||
|
if len(gateway) < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msg.User.Username == conf.PnutUsername {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf(">> sending message from channel %s to %s ...", msg.ChannelID, gateway)
|
||||||
|
data := MbOutMsg{
|
||||||
|
Text: msg.Content.Text,
|
||||||
|
Username: msg.User.Username,
|
||||||
|
Gateway: gateway,
|
||||||
|
}
|
||||||
|
jsonStr, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! there is a problem creating json string", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
url := conf.MbUrl + "/api/message"
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! unable to create request", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "pnut-bridge")
|
||||||
|
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
req.Header.Add("Authorization", "Bearer "+conf.MbToken)
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! error on reponse", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
log.Printf("<< %s", resp.Status)
|
||||||
|
//body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
//log.Println(string([]byte(body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoomGateway(rooms []Room, chan_id string) string {
|
||||||
|
for _, v := range rooms {
|
||||||
|
if v.ChannelID == chan_id {
|
||||||
|
return v.Gateway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func bridgeMsgHandler(conf *Config, msg MbMessage) {
|
||||||
|
client := woodstock.NewClient("", "")
|
||||||
|
client.SetAccessToken(conf.PnutAccessToken)
|
||||||
|
channel_id := getPnutChannel(conf.Rooms, msg.Gateway)
|
||||||
|
text := msg.Username + " " + msg.Text
|
||||||
|
for _, file := range msg.Extra.File {
|
||||||
|
text = text + "\n" + file.URL
|
||||||
|
// log.Println("name, ", file.Name)
|
||||||
|
// log.Println("url, ", file.URL)
|
||||||
|
// fdata, err := base64.StdEncoding.DecodeString(file.Data)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Println("!! error decoding file", err)
|
||||||
|
// }
|
||||||
|
// mtype := mimetype.Detect(fdata)
|
||||||
|
// log.Println("type, ", mtype)
|
||||||
|
// if strings.HasPrefix(mtype, "image") {
|
||||||
|
// oembed := PnutOembed{
|
||||||
|
// Version: "1.0",
|
||||||
|
// Type: "photo",
|
||||||
|
// Title: file.Name,
|
||||||
|
// Url: file.URL,
|
||||||
|
// }
|
||||||
|
// // TODO finish this when woodstock is fixed
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
log.Printf(">> sending message from %s to channel %s ...", msg.Gateway, channel_id)
|
||||||
|
_, err := client.CreateMessage(channel_id, text)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! problem posting message to pnut.io", err)
|
||||||
|
}
|
||||||
|
log.Println("<< ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPnutChannel(rooms []Room, gateway string) string {
|
||||||
|
for _, v := range rooms {
|
||||||
|
if v.Gateway == gateway {
|
||||||
|
return v.ChannelID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfgfile = flag.String("c", "config.ini", "config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
done = make(chan interface{})
|
||||||
|
interrupt = make(chan os.Signal)
|
||||||
|
|
||||||
|
conf := new(Config)
|
||||||
|
file, err := ini.Load(*cfgfile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("!! there was a problem loading the config: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
var rooms []Room
|
||||||
|
for _, _sec := range file.SectionStrings() {
|
||||||
|
if _sec == "DEFAULT" {
|
||||||
|
err = file.Section("DEFAULT").MapTo(conf)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! unable to map default config section", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
room := new(Room)
|
||||||
|
err = file.Section(_sec).MapTo(room)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("!! unable to map config section %s: %v", _sec, err)
|
||||||
|
}
|
||||||
|
rooms = append(rooms, *room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conf.Rooms = rooms
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("access_token", conf.PnutAccessToken)
|
||||||
|
|
||||||
|
socketUrl := "wss://stream.pnut.io/v1/user?" + params.Encode()
|
||||||
|
conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("!! error connectiong to pnut.io:", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
go receiveHandler(conn, conf)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Duration(1) * time.Millisecond * 1000):
|
||||||
|
err := conn.WriteMessage(websocket.TextMessage, []byte("."))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! error during write to websocket:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url := conf.MbUrl + "/api/messages"
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("!! unable to create request", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "pnut-bridge")
|
||||||
|
req.Header.Add("Authorization", "Bearer "+conf.MbToken)
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(">> error on reponse", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
//log.Println("<< ", string([]byte(body)))
|
||||||
|
var m []MbMessage
|
||||||
|
jerr := json.Unmarshal(body, &m)
|
||||||
|
if jerr != nil {
|
||||||
|
log.Println("Error unmarshaling msg", jerr)
|
||||||
|
}
|
||||||
|
if len(m) > 0 {
|
||||||
|
for _, _m := range m {
|
||||||
|
bridgeMsgHandler(conf, _m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-interrupt:
|
||||||
|
log.Println("Received SIGINT.")
|
||||||
|
|
||||||
|
err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error during closing websocket:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
log.Println("Receiver Channel Closed! Exiting...")
|
||||||
|
case <-time.After(time.Duration(1) * time.Second):
|
||||||
|
log.Println("Timeout in closing receiving channel. Exiting...")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue