2021-07-02 23:32:03 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2021-07-15 04:34:43 +00:00
|
|
|
"encoding/base64"
|
2021-07-02 23:32:03 +00:00
|
|
|
"encoding/json"
|
|
|
|
"flag"
|
2021-07-15 04:34:43 +00:00
|
|
|
"image"
|
|
|
|
_ "image/gif"
|
|
|
|
_ "image/jpeg"
|
|
|
|
_ "image/png"
|
2021-07-02 23:32:03 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
2021-07-15 04:34:43 +00:00
|
|
|
"strings"
|
2021-07-02 23:32:03 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.sr.ht/~thrrgilag/woodstock"
|
2021-07-15 04:34:43 +00:00
|
|
|
"github.com/gabriel-vasile/mimetype"
|
2021-07-02 23:32:03 +00:00
|
|
|
"github.com/gorilla/websocket"
|
2021-07-24 15:27:41 +00:00
|
|
|
"github.com/mitchellh/go-wordwrap"
|
2021-07-02 23:32:03 +00:00
|
|
|
"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"`
|
2022-11-27 20:36:24 +00:00
|
|
|
MbURL string `ini:"matterbridge_url"`
|
2021-07-02 23:32:03 +00:00
|
|
|
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"`
|
2022-11-27 20:36:24 +00:00
|
|
|
ID string `json:"id"`
|
2021-07-02 23:32:03 +00:00
|
|
|
ChannelType string `json:"channel_type"`
|
|
|
|
SubscriptionIds []string `json:"subscription_ids"`
|
|
|
|
Stream Stream `json:"stream"`
|
2022-11-27 20:36:24 +00:00
|
|
|
ConnectionID string `json:"connection_id"`
|
2021-07-02 23:32:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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"`
|
2022-11-27 20:36:24 +00:00
|
|
|
Extra MbExtra `json:"extra"`
|
2021-07-02 23:32:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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"`
|
2022-11-27 20:36:24 +00:00
|
|
|
URL string `json:"url"`
|
2021-07-15 04:34:43 +00:00
|
|
|
Width int `json:"width"`
|
|
|
|
Height int `json:"height"`
|
2021-07-02 23:32:03 +00:00
|
|
|
}
|
|
|
|
|
2022-11-27 20:36:24 +00:00
|
|
|
func subscribe(connectionID string, accessToken string, rooms []Room) {
|
2021-07-02 23:32:03 +00:00
|
|
|
params := url.Values{}
|
2022-11-27 20:36:24 +00:00
|
|
|
params.Set("connection_id", connectionID)
|
2021-07-02 23:32:03 +00:00
|
|
|
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")
|
2022-11-27 20:36:24 +00:00
|
|
|
req.Header.Add("Authorization", "Bearer "+accessToken)
|
2021-07-02 23:32:03 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2022-11-27 20:36:24 +00:00
|
|
|
if len(m.Meta.ConnectionID) > 0 {
|
|
|
|
log.Println("<< connection", m.Meta.ConnectionID)
|
|
|
|
subscribe(m.Meta.ConnectionID, conf.PnutAccessToken, conf.Rooms)
|
2021-07-02 23:32:03 +00:00
|
|
|
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
|
|
|
|
}
|
2022-11-27 20:36:24 +00:00
|
|
|
url := conf.MbURL + "/api/message"
|
2021-07-02 23:32:03 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-11-27 20:36:24 +00:00
|
|
|
func getRoomGateway(rooms []Room, chanID string) string {
|
2021-07-02 23:32:03 +00:00
|
|
|
for _, v := range rooms {
|
2022-11-27 20:36:24 +00:00
|
|
|
if v.ChannelID == chanID {
|
2021-07-02 23:32:03 +00:00
|
|
|
return v.Gateway
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func bridgeMsgHandler(conf *Config, msg MbMessage) {
|
|
|
|
client := woodstock.NewClient("", "")
|
|
|
|
client.SetAccessToken(conf.PnutAccessToken)
|
2022-11-27 20:36:24 +00:00
|
|
|
channelID := getPnutChannel(conf.Rooms, msg.Gateway)
|
2021-07-24 15:27:41 +00:00
|
|
|
|
|
|
|
text := msg.Text
|
2021-07-15 04:34:43 +00:00
|
|
|
var raw []woodstock.Raw
|
2021-07-02 23:32:03 +00:00
|
|
|
for _, file := range msg.Extra.File {
|
2021-07-15 04:34:43 +00:00
|
|
|
fdata, err := base64.StdEncoding.DecodeString(file.Data)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("!! error decoding file", err)
|
|
|
|
}
|
|
|
|
mtype := mimetype.Detect(fdata)
|
|
|
|
ir := bytes.NewReader(fdata)
|
|
|
|
im, _, err := image.DecodeConfig(ir)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Couldn't decode image?", err)
|
|
|
|
}
|
|
|
|
if strings.HasPrefix(mtype.String(), "image") {
|
|
|
|
oembed := PnutOembed{
|
|
|
|
Version: "1.0",
|
|
|
|
Type: "photo",
|
|
|
|
Title: file.Name,
|
2022-11-27 20:36:24 +00:00
|
|
|
URL: file.URL,
|
2021-07-15 04:34:43 +00:00
|
|
|
Width: im.Width,
|
|
|
|
Height: im.Height,
|
|
|
|
}
|
|
|
|
raw = append(raw, woodstock.Raw{Type: "io.pnut.core.oembed", Value: oembed})
|
|
|
|
} else {
|
|
|
|
text = text + "\n" + file.URL
|
|
|
|
}
|
2021-07-02 23:32:03 +00:00
|
|
|
}
|
2021-07-24 15:27:41 +00:00
|
|
|
|
|
|
|
maxlen := 2048
|
|
|
|
prelen := len(msg.Username) + 1
|
|
|
|
wrapped := wordwrap.WrapString(text, uint(maxlen-prelen))
|
|
|
|
lines := strings.Split(wrapped, "\n")
|
|
|
|
for _, line := range lines {
|
|
|
|
line := msg.Username + " " + line
|
2022-11-27 20:36:24 +00:00
|
|
|
log.Printf(">> sending message from %s to channel %s ...", msg.Gateway, channelID)
|
2021-07-24 15:27:41 +00:00
|
|
|
newmessage := woodstock.NewMessage{Text: line, Raw: raw}
|
2022-11-27 20:36:24 +00:00
|
|
|
_, err := client.CreateMessage(channelID, newmessage)
|
2021-07-24 15:27:41 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println("!! problem posting message to pnut.io", err)
|
|
|
|
}
|
|
|
|
log.Println("<< ok")
|
|
|
|
raw = nil
|
2021-07-02 23:32:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2022-11-27 20:36:24 +00:00
|
|
|
socketURL := "wss://stream.pnut.io/v1/user?" + params.Encode()
|
|
|
|
conn, _, err := websocket.DefaultDialer.Dial(socketURL, nil)
|
2021-07-02 23:32:03 +00:00
|
|
|
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 {
|
2021-08-14 04:31:26 +00:00
|
|
|
log.Fatal("!! error during write to websocket:", err)
|
2021-07-02 23:32:03 +00:00
|
|
|
}
|
|
|
|
|
2022-11-27 20:36:24 +00:00
|
|
|
url := conf.MbURL + "/api/messages"
|
2021-07-02 23:32:03 +00:00
|
|
|
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)
|
2021-11-23 14:35:30 +00:00
|
|
|
client := &http.Client{
|
|
|
|
Timeout: 10 * time.Second,
|
|
|
|
}
|
2021-07-02 23:32:03 +00:00
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(">> error on reponse", err)
|
|
|
|
}
|
|
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
2021-11-23 14:35:30 +00:00
|
|
|
resp.Body.Close()
|
2021-07-02 23:32:03 +00:00
|
|
|
//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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|