mirror of
https://github.com/42wim/matterbridge.git
synced 2024-11-28 21:52:08 -08:00
Implement soulseek bridge
This commit is contained in:
parent
d16645c952
commit
cbae29b5dd
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
|
||||
}
|
11
gateway/bridgemap/bsoulseek.go
Normal file
11
gateway/bridgemap/bsoulseek.go
Normal file
@ -0,0 +1,11 @@
|
||||
// +build !nosoulseek
|
||||
|
||||
package bridgemap
|
||||
|
||||
import (
|
||||
bsoulseek "github.com/42wim/matterbridge/bridge/soulseek"
|
||||
)
|
||||
|
||||
func init() {
|
||||
FullMap["soulseek"] = bsoulseek.New
|
||||
}
|
Loading…
Reference in New Issue
Block a user