Implement soulseek bridge
This commit is contained in:
		
							
								
								
									
										87
									
								
								bridge/soulseek/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								bridge/soulseek/handlers.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										366
									
								
								bridge/soulseek/messages.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										231
									
								
								bridge/soulseek/soulseek.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user