Implement soulseek bridge

This commit is contained in:
7x11x13 2024-07-12 13:15:24 -04:00
parent d16645c952
commit cbae29b5dd
4 changed files with 695 additions and 0 deletions

View File

@ -0,0 +1,87 @@
package bsoulseek
import (
"fmt"
"strings"
"github.com/42wim/matterbridge/bridge/config"
)
func (b *Bsoulseek) handleMessage(msg soulseekMessageResponse) {
if msg != nil {
b.Log.Debugf("Handling message: %v", msg)
}
switch msg := msg.(type) {
case loginMessageResponseSuccess, loginMessageResponseFailure:
b.loginResponse <- msg
case joinRoomMessageResponse:
b.joinRoomResponse <- msg
case kickedMessageResponse:
b.fatalErrors <- fmt.Errorf("Logged in somewhere else")
case privateMessageReceive:
b.handleDM(msg)
case sayChatroomMessageReceive:
b.handleChatMessage(msg)
case userJoinedRoomMessage:
b.handleJoinMessage(msg)
case userLeftRoomMessage:
b.handleLeaveMessage(msg)
default:
// do nothing
}
}
func (b *Bsoulseek) handleChatMessage(msg sayChatroomMessageReceive) {
b.Log.Debugf("Handle chat message: %v", msg)
if msg.Username == b.Config.GetString("Nick") {
return
}
bridgeMessage := config.Message{
Account: b.Account,
Text: msg.Message,
Channel: msg.Room,
Username: msg.Username,
}
b.local <- bridgeMessage
}
func (b *Bsoulseek) handleJoinMessage(msg userJoinedRoomMessage) {
b.Log.Debugf("Handle join message: %v", msg)
if msg.Username == b.Config.GetString("Nick") {
return
}
bridgeMessage := config.Message{
Account: b.Account,
Event: config.EventJoinLeave,
Text: fmt.Sprintf("%s has joined the room", msg.Username),
Channel: msg.Room,
Username: "system",
}
b.local <- bridgeMessage
}
func (b *Bsoulseek) handleLeaveMessage(msg userLeftRoomMessage) {
b.Log.Debugf("Handle leave message: %v", msg)
if msg.Username == b.Config.GetString("Nick") {
return
}
bridgeMessage := config.Message{
Account: b.Account,
Event: config.EventJoinLeave,
Text: fmt.Sprintf("%s has left the room", msg.Username),
Channel: msg.Room,
Username: "system",
}
b.local <- bridgeMessage
}
func (b *Bsoulseek) handleDM(msg privateMessageReceive) {
b.Log.Debugf("Received private message: %+v", msg)
if msg.Username == "server" {
b.Log.Infof("Received system message: %s", msg.Message)
if strings.HasPrefix(msg.Message, "System Message: You have been banned") {
b.Log.Errorf("Banned from server. Message: %s", msg.Message)
b.doDisconnect()
}
}
}

366
bridge/soulseek/messages.go Normal file
View File

@ -0,0 +1,366 @@
package bsoulseek
import (
"bytes"
"crypto/md5"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"reflect"
)
type soulseekMessage interface {}
type soulseekMessageResponse interface {}
const (
loginMessageCode uint32 = 1
sayInChatRoomMessageCode uint32 = 13
joinRoomMessageCode uint32 = 14
userJoinedRoomMessageCode uint32 = 16
userLeftRoomMessageCode uint32 = 17
privateMessageCode uint32 = 22
kickedMessageCode uint32 = 41
changePasswordMessageCode uint32 = 142
)
var ignoreMessageCodes = map[uint32]bool {
7: true,
64: true,
69: true,
83: true,
84: true,
104: true,
113: true,
114: true,
115: true,
130: true,
133: true,
139: true,
140: true,
145: true,
146: true,
148: true,
160: true,
1003: true,
}
// 1: Login
type loginMessage struct {
Code uint32
Username string
Password string
Version uint32
Hash string
MinorVersion uint32
}
type loginMessageResponseSuccess struct {
Greet string
Address uint32
Hash string
IsSupporter bool
}
type loginMessageResponseFailure struct {
Reason string
}
// 13: Say in chatroom
type sayChatroomMessage struct {
Code uint32
Room string
Message string
}
type sayChatroomMessageReceive struct {
Room string
Username string
Message string
}
// 14: Join room
type joinRoomMessage struct {
Code uint32
Room string
Private uint32
}
type userStat struct {
AvgSpeed uint32
UploadNum uint64
Files uint32
Dirs uint32
}
type joinRoomMessageResponse struct {
Room string
Users []string
Statuses []uint32
Stats []userStat
SlotsFree []uint32
Countries []uint32
Owner string
Operators []string
}
// 16: User joined room
type userJoinedRoomMessage struct {
Room string
Username string
Status uint32
AvgSpeed uint32
UploadNum uint64
Files uint32
Dirs uint32
SlotsFree uint32
CountryCode string
}
// 16: User left room
type userLeftRoomMessage struct {
Room string
Username string
}
// 22: Private message (sometimes used by server to tell us errors)
type privateMessageReceive struct {
ID uint32
Timestamp uint32
Username string
Message string
NewMessage bool
}
// 41: Kicked from server (relogged)
type kickedMessageResponse struct {}
// 142: Change password
type changePasswordMessage struct {
Code uint32
Password string
}
type changePasswordMessageResponse struct {
Password string
}
func packMessage(message soulseekMessage) ([]byte, error) {
buf := &bytes.Buffer{}
var length uint32 = 0
binary.Write(buf, binary.LittleEndian, length) // placeholder
v := reflect.ValueOf(message)
var err error
for i := range v.NumField() {
val := v.Field(i).Interface()
switch val := val.(type) {
case string:
s_len := uint32(len(val))
err = binary.Write(buf, binary.LittleEndian, s_len)
buf.WriteString(val)
length += s_len + 4
case bool, uint8:
length += 1
err = binary.Write(buf, binary.LittleEndian, val)
case uint16:
length += 2
err = binary.Write(buf, binary.LittleEndian, val)
case uint32:
length += 4
err = binary.Write(buf, binary.LittleEndian, val)
case uint64:
length += 8
err = binary.Write(buf, binary.LittleEndian, val)
default:
panic("Unsupported struct field type")
}
if err != nil {
return nil, err
}
}
bytes := buf.Bytes()
binary.LittleEndian.PutUint32(bytes, length)
return bytes, nil
}
func unpackStructField(reader io.Reader, field reflect.Value) error {
switch field.Kind() {
case reflect.Struct:
for i := range field.NumField() {
field := field.Field(i)
err := unpackStructField(reader, field)
if err != nil {
return err
}
}
case reflect.Slice:
var length uint32
err := binary.Read(reader, binary.LittleEndian, &length)
if err != nil {
return err
}
ilen := int(length)
newval := reflect.MakeSlice(field.Type(), ilen, ilen)
field.Set(newval)
for j := range ilen {
err := unpackStructField(reader, field.Index(j))
if err != nil {
return err
}
}
case reflect.String:
var length uint32
err := binary.Read(reader, binary.LittleEndian, &length)
if err != nil {
return err
}
val := make([]byte, length)
_, err = reader.Read(val)
if err != nil {
return err
}
field.SetString(string(val))
case reflect.Bool:
var val bool
err := binary.Read(reader, binary.LittleEndian, &val)
if err != nil {
return err
}
field.SetBool(val)
case reflect.Uint8:
var val uint8
err := binary.Read(reader, binary.LittleEndian, &val)
if err != nil {
return err
}
field.SetUint(uint64(val))
case reflect.Uint16:
var val uint16
err := binary.Read(reader, binary.LittleEndian, &val)
if err != nil {
return err
}
field.SetUint(uint64(val))
case reflect.Uint32:
var val uint32
err := binary.Read(reader, binary.LittleEndian, &val)
if err != nil {
return err
}
field.SetUint(uint64(val))
case reflect.Uint64:
var val uint64
err := binary.Read(reader, binary.LittleEndian, &val)
if err != nil {
return err
}
field.SetUint(val)
default:
panic(fmt.Sprintf("Unsupported struct field type: %d", field.Kind()))
}
return nil
}
func unpackMessage[T soulseekMessage](reader io.Reader) (T, error) {
var data T
v := reflect.ValueOf(&data).Elem()
err := unpackStructField(reader, v)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return data, err
}
return data, nil
}
func readMessage(reader io.Reader) (soulseekMessage, error) {
var length uint32
err := binary.Read(reader, binary.LittleEndian, &length)
if err != nil {
return nil, err
}
buf := make([]byte, int(length))
_, err = io.ReadAtLeast(reader, buf, len(buf))
if err != nil {
return nil, err
}
reader = bytes.NewReader(buf)
var code uint32
err = binary.Read(reader, binary.LittleEndian, &code)
if err != nil {
return nil, err
}
switch code {
case loginMessageCode:
// login message is special; has two possible responses
var success bool
err := binary.Read(reader, binary.LittleEndian, &success)
if err != nil {
return nil, err
}
if success {
return unpackMessage[loginMessageResponseSuccess](reader)
} else {
return unpackMessage[loginMessageResponseFailure](reader)
}
case sayInChatRoomMessageCode:
return unpackMessage[sayChatroomMessageReceive](reader)
case joinRoomMessageCode:
return unpackMessage[joinRoomMessageResponse](reader)
case kickedMessageCode:
return unpackMessage[kickedMessageResponse](reader)
case userJoinedRoomMessageCode:
return unpackMessage[userJoinedRoomMessage](reader)
case userLeftRoomMessageCode:
return unpackMessage[userLeftRoomMessage](reader)
case changePasswordMessageCode:
return unpackMessage[changePasswordMessageResponse](reader)
case privateMessageCode:
return unpackMessage[privateMessageReceive](reader)
default:
_, ignore := ignoreMessageCodes[code]
if ignore {
return nil, nil
}
return nil, fmt.Errorf("Unknown message code: %d", code)
}
}
func makeLoginMessage(username string, password string) soulseekMessage {
hash := md5.Sum([]byte(username + password))
msg := loginMessage{
loginMessageCode,
username,
password,
160,
hex.EncodeToString(hash[:]),
1,
}
return msg
}
func makeJoinRoomMessage(room string) joinRoomMessage {
return joinRoomMessage{
joinRoomMessageCode,
room,
0,
}
}
func makeSayChatroomMessage(room string, text string) sayChatroomMessage {
return sayChatroomMessage{
sayInChatRoomMessageCode,
room,
text,
}
}

231
bridge/soulseek/soulseek.go Normal file
View File

@ -0,0 +1,231 @@
package bsoulseek
import (
"fmt"
"net"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
)
type Bsoulseek struct {
conn net.Conn
messagesToSend chan soulseekMessage
local chan config.Message
loginResponse chan soulseekMessageResponse
joinRoomResponse chan joinRoomMessageResponse
fatalErrors chan error
disconnect chan bool
firstConnectResponse chan error
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bsoulseek{}
b.Config = cfg
b.messagesToSend = make(chan soulseekMessage, 256)
b.local = make(chan config.Message, 256)
b.loginResponse = make(chan soulseekMessageResponse)
b.joinRoomResponse = make(chan joinRoomMessageResponse)
b.fatalErrors = make(chan error)
b.disconnect = make(chan bool)
b.firstConnectResponse = make(chan error)
return b
}
func (b *Bsoulseek) receiveMessages() {
for {
msg, err := readMessage(b.conn)
if err != nil {
b.fatalErrors <- fmt.Errorf("Reading message failed: %s", err)
return
}
b.handleMessage(msg)
}
}
func sliceEqual(s []string) bool {
// Return true if every element in s is equal to each other
if len(s) <= 1 {
return true
}
for _, x := range(s) {
if x != s[0] {
return false
}
}
return true
}
func (b *Bsoulseek) sendMessages() {
lastFourChatMessages := []string {"1", "2", "3", ""}
for {
message, more := <-b.messagesToSend
if !more {
return
}
msg, is_say := message.(sayChatroomMessage)
if is_say {
// can't send 5 of the same message in a row or we get banned
if (sliceEqual(append(lastFourChatMessages, msg.Message))) {
b.Log.Warnf("Dropping message: %s", msg.Message)
continue
}
}
data, err := packMessage(message)
if err != nil {
b.fatalErrors <- fmt.Errorf("Packing message failed: %s", err)
return
}
_, err = b.conn.Write(data)
if err != nil {
b.fatalErrors <- fmt.Errorf("Sending message failed: %s", err)
return
}
b.Log.Debugf("Sent message: %v", message)
if is_say {
lastFourChatMessages = append(lastFourChatMessages[1:], msg.Message)
time.Sleep(3500 * time.Millisecond) // rate limit so less than 20 can be sent per min
}
}
}
func (b *Bsoulseek) sendLocalToRemote() {
for {
message, more := <-b.local
if !more {
return
}
b.Remote <- message
}
}
func (b *Bsoulseek) loginLoop() {
firstConnect := true
for {
if !firstConnect {
// Cleanup as we are making new sender/receiver routines
b.fatalErrors = make(chan error)
close(b.messagesToSend)
}
// Connect to slsk server
server := b.GetString("Server")
b.Log.Infof("Connecting %s", server)
conn, err := net.Dial("tcp", server)
b.conn = conn
if err != nil {
if firstConnect {
b.firstConnectResponse <- err
return
}
}
// Init sender and receiver
go b.receiveMessages()
go b.sendMessages()
go b.sendLocalToRemote()
// Attempt login
b.messagesToSend <- makeLoginMessage(b.GetString("Nick"), b.GetString("Password"))
var msg soulseekMessageResponse
connected := false
select {
case msg = <-b.loginResponse:
switch msg := msg.(type) {
case loginMessageResponseSuccess:
if firstConnect {
b.firstConnectResponse <- nil
}
connected = true
case loginMessageResponseFailure:
if firstConnect {
b.firstConnectResponse <- fmt.Errorf("Login failed: %s", msg.Reason)
return
}
b.Log.Errorf("Login failed: %s", msg.Reason)
default:
panic("Unreachable")
}
case err := <-b.fatalErrors:
// error
if firstConnect {
b.firstConnectResponse <- fmt.Errorf("Login failed: %s", err)
return
}
b.Log.Errorf("Login failed: %s", err)
case <-time.After(30 * time.Second):
// timeout
if firstConnect {
b.firstConnectResponse <- fmt.Errorf("Login failed: timeout")
return
}
b.Log.Errorf("Login failed: timeout")
}
if !connected {
// If we reach here, we are not logged in and
// it is not the first connect, so we should try again
b.Log.Info("Retrying in 30s")
time.Sleep(30 * time.Second)
continue
}
// Now we are connected
select {
case err = <- b.fatalErrors:
b.Log.Errorf("%s", err)
// Retry connect
continue
case <- b.disconnect:
// We are done
return
}
}
}
func (b *Bsoulseek) Connect() error {
go b.loginLoop()
err := <-b.firstConnectResponse
return err
}
func (b *Bsoulseek) JoinChannel(channel config.ChannelInfo) error {
b.messagesToSend <- makeJoinRoomMessage(channel.Name)
select {
case <-b.joinRoomResponse:
b.Log.Infof("Joined room: '%s'", channel.Name)
return nil
case <-time.After(30 * time.Second):
return fmt.Errorf("Could not join room '%s': timeout", channel.Name)
}
}
func (b *Bsoulseek) Send(msg config.Message) (string, error) {
// Only process text messages
b.Log.Debugf("=> Received local message %v", msg)
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave {
return "", nil
}
b.messagesToSend <- makeSayChatroomMessage(msg.Channel, msg.Username + msg.Text)
return "", nil
}
func (b *Bsoulseek) doDisconnect() error {
b.disconnect <- true
close(b.messagesToSend)
close(b.joinRoomResponse)
close(b.loginResponse)
close(b.local)
return nil
}
func (b *Bsoulseek) Disconnect() error {
b.doDisconnect()
return nil
}

View File

@ -0,0 +1,11 @@
// +build !nosoulseek
package bridgemap
import (
bsoulseek "github.com/42wim/matterbridge/bridge/soulseek"
)
func init() {
FullMap["soulseek"] = bsoulseek.New
}