func TestRoomMapLoadsConfig(t *testing.T) {
	dir, err := ioutil.TempDir("", "testdb")
	if err != nil {
		t.Fatal(err)
	}
	file := path.Join(dir, "sqlite3.db")
	db := makeDBAt(t, file)
	rooms, err := NewRoomMap(db)
	if err != nil {
		t.Fatal(err)
	}
	matrixRoomID := "foo"
	slack := "bar"
	rooms.Link(matrix.NewRoom(matrixRoomID), slack)
	db.Close()

	db, err = sql.Open("sqlite3", file)
	if err != nil {
		t.Fatal(err)
	}
	rooms, err = NewRoomMap(db)
	if err != nil {
		t.Fatal(err)
	}
	if got := rooms.SlackForMatrix(matrixRoomID); got != slack {
		t.Errorf("want %q got %q", slack, got)
	}
}
func TestSlackMeMessage(t *testing.T) {
	mockMatrixClient := &MockMatrixClient{}
	mockSlackClient := &MockSlackClient{}

	db := makeDB(t)
	rooms, err := NewRoomMap(db)
	if err != nil {
		t.Fatal(err)
	}
	rooms.Link(matrix.NewRoom("!abc123:matrix.org"), "CANTINA")

	echoSuppresser := common.NewEchoSuppresser()
	users, err := NewUserMap(db, http.Client{}, rooms, echoSuppresser)
	if err != nil {
		t.Fatal(err)
	}
	matrixUser := matrix.NewUser("@nancy:st.andrews", mockMatrixClient)
	slackUser := &slack.User{"U34", mockSlackClient}
	users.Link(matrixUser, slackUser)

	bridge := Bridge{users, rooms, nil, nil, http.Client{}, echoSuppresser, Config{}}
	bridge.OnSlackMessage(slack.Message{
		Type:    "message",
		Channel: "CANTINA",
		User:    "******",
		Subtype: "me_message",
		Text:    "takes more chances",
	})

	want := []call{call{"SendEmote", []interface{}{"!abc123:matrix.org", "takes more chances"}}}
	if !reflect.DeepEqual(mockMatrixClient.calls, want) {
		t.Fatalf("Wrong Matrix calls, want %v got %v", want, mockMatrixClient.calls)
	}
}
func TestSlackMessageFilter(t *testing.T) {
	rooms, err := NewRoomMap(makeDB(t))
	if err != nil {
		t.Fatal(err)
	}
	receive := func(ts string) bool {
		return rooms.ShouldNotify(&slack.Message{
			Type:    "message",
			Channel: "CANTINA",
			User:    "******",
			Text:    "Take more chances",
			TS:      ts,
		})
	}

	if receive("1") {
		t.Fatalf("not linked: should have skipped")
	}

	rooms.Link(matrix.NewRoom("!abc123:matrix.org"), "CANTINA")

	if !receive("1") {
		t.Fatalf("should have notified")
	}

	if receive("0.5") {
		t.Fatalf("seen newer, should have skipped")
	}

	if !receive("2") {
		t.Fatalf("should have notified")
	}
}
func makeBridge(t *testing.T, db *sql.DB) *Bridge {
	mockMatrixClient := &MockMatrixClient{}
	mockSlackClient := &MockSlackClient{}

	rooms, err := NewRoomMap(db)
	if err != nil {
		t.Fatal(err)
	}

	echoSuppresser := common.NewEchoSuppresser()
	users, err := NewUserMap(db, http.Client{}, rooms, echoSuppresser)
	if err != nil {
		t.Fatal(err)
	}
	matrixUser := matrix.NewUser("@nancy:st.andrews", mockMatrixClient)
	slackUser := &slack.User{"U34", mockSlackClient}
	users.Link(matrixUser, slackUser)

	// Subsequent calls should load link from database, but don't yet
	if err := rooms.Link(matrix.NewRoom("!abc123:matrix.org"), "CANTINA"); err != nil {
		t.Fatalf("Error linking rooms: %v", err)
	}

	return &Bridge{users, rooms, nil, nil, http.Client{}, echoSuppresser, Config{}}
}
func TestMatrixImageMessage(t *testing.T) {
	mockMatrixClient := &MockMatrixClient{}
	mockSlackClient := &MockSlackClient{}

	db := makeDB(t)
	rooms, err := NewRoomMap(db)
	if err != nil {
		t.Fatal(err)
	}
	rooms.Link(matrix.NewRoom("!abc123:matrix.org"), "BOWLINGALLEY")

	echoSuppresser := common.NewEchoSuppresser()
	users, err := NewUserMap(db, http.Client{}, rooms, echoSuppresser)
	if err != nil {
		t.Fatal(err)
	}
	matrixUser := matrix.NewUser("@sean:st.andrews", mockMatrixClient)
	slackUser := &slack.User{"U35", mockSlackClient}
	users.Link(matrixUser, slackUser)

	bridge := Bridge{users, rooms, nil, nil, http.Client{}, echoSuppresser, Config{
		HomeserverBaseURL: "https://some.url:1234",
	}}
	bridge.OnMatrixRoomMessage(matrix.RoomMessage{
		Type:    "m.room.message",
		Content: []byte(`{"msgtype": "m.image", "body": "It's Nancy!", "url": "mxc://some.homeserver/abcDEF"}`),
		UserID:  "@sean:st.andrews",
		RoomID:  "!abc123:matrix.org",
	})

	want := []call{call{"SendImage", []interface{}{"BOWLINGALLEY", "It's Nancy!", "https://some.url:1234/_matrix/media/v1/download/some.homeserver/abcDEF"}}}
	if !reflect.DeepEqual(mockSlackClient.calls, want) {
		t.Fatalf("Wrong Slack calls, want %v got %v", want, mockSlackClient.calls)
	}
}
func TestSlackMessageWithImage(t *testing.T) {
	mockMatrixClient := &MockMatrixClient{}
	mockSlackClient := &MockSlackClient{}

	db := makeDB(t)
	rooms, err := NewRoomMap(db)
	if err != nil {
		t.Fatal(err)
	}
	rooms.Link(matrix.NewRoom("!abc123:matrix.org"), "CANTINA")

	echoSuppresser := common.NewEchoSuppresser()
	users, err := NewUserMap(db, http.Client{}, rooms, echoSuppresser)
	if err != nil {
		t.Fatal(err)
	}
	matrixUser := matrix.NewUser("@nancy:st.andrews", mockMatrixClient)
	slackUser := &slack.User{"U34", mockSlackClient}
	users.Link(matrixUser, slackUser)

	bridge := Bridge{users, rooms, nil, nil, http.Client{}, echoSuppresser, Config{}}

	imageURL := "https://slack-files.com/files-pub/T02TMLW97-F0D2M81QA-38528eaf47/otters.jpg"
	bridge.OnSlackMessage(slack.Message{
		Type:    "message",
		Channel: "CANTINA",
		User:    "******",
		Text:    "Cute otter",
		File: &slack.File{
			MIMEType:       "image/jpeg",
			URL:            imageURL,
			OriginalHeight: 768,
			OriginalWidth:  1024,
			Size:           90,
			CommentsCount:  1,
			InitialComment: &slack.Comment{
				Comment: "omg",
				User:    "******",
			},
		},
	})

	want := []call{
		call{"SendImage", []interface{}{"!abc123:matrix.org", "otters.jpg", matrix.Image{
			URL: imageURL,
			Info: &matrix.ImageInfo{
				Width:    1024,
				Height:   768,
				MIMEType: "image/jpeg",
				Size:     90,
			},
		}}},
		call{"SendText", []interface{}{"!abc123:matrix.org", "omg"}},
	}
	if !reflect.DeepEqual(mockMatrixClient.calls, want) {
		t.Fatalf("Wrong Matrix calls, want:\n%v\ngot:\n%v", want, mockMatrixClient.calls)
	}
}
Exemple #7
0
func NewRoomMap(db *sql.DB) (*RoomMap, error) {
	m := &RoomMap{
		matrixToSlack: make(map[string]string),
		slackToMatrix: make(map[string]*matrix.Room),
		rows:          make(map[string]*entry),

		/*
			CREATE TABLE IF NOT EXISTS rooms(
			id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
			slack_channel_id TEXT,
			matrix_room_id TEXT,
			last_slack_timestamp TEXT,
			last_matrix_stream_token TEXT)
		*/
		db: db,
	}

	rows, err := db.Query("SELECT id, slack_channel_id, matrix_room_id FROM rooms ORDER BY id ASC")
	if err != nil {
		return nil, err
	}
	for rows.Next() {
		var id int32
		var slack string
		var matrixID string
		if err := rows.Scan(&id, &slack, &matrixID); err != nil {
			return nil, err
		}
		matrixRoom := matrix.NewRoom(matrixID)
		if err := m.Link(matrixRoom, slack); err != nil {
			return nil, err
		}
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}
	return m, nil
}
func TestSlackMessageFromUnlinkedUser(t *testing.T) {
	db := makeDB(t)
	rooms, err := NewRoomMap(db)
	if err != nil {
		t.Fatal(err)
	}
	slackChannel := "BOWLINGALLEY"
	matrixRoom := matrix.NewRoom("!abc123:matrix.org")
	message := "Shhhhh"
	slackUser := "******"
	asToken := "abc123"

	rooms.Link(matrixRoom, slackChannel)

	echoSuppresser := common.NewEchoSuppresser()
	users, err := NewUserMap(db, http.Client{}, rooms, echoSuppresser)
	if err != nil {
		t.Fatal(err)
	}

	slackRoomMembers := slack.NewRoomMembers()
	slackRoomMembers.Add(slackChannel, &slack.User{"someone", &MockSlackClient{}})

	calledTwice := make(chan struct{}, 1)
	var invites int32
	var botJoins int32
	var joins int32
	var calls int32
	verify := func(req *http.Request) string {
		if req.URL.Path == "/api/users.info" {
			return `{"ok": true, "user": {"id": "` + slackUser + `", "name": "someoneonslack"}}`
		}

		if req.URL.Path == "/_matrix/client/api/v1/rooms/"+matrixRoom.ID+"/join" {
			if req.URL.Query().Get("user_id") == "" {
				atomic.AddInt32(&botJoins, 1)
			} else {
				atomic.AddInt32(&joins, 1)
			}
			return ""
		}
		if req.URL.Path == "/_matrix/client/api/v1/rooms/"+matrixRoom.ID+"/invite" {
			atomic.AddInt32(&invites, 1)
			return ""
		}
		if req.URL.Path != "/_matrix/client/api/v1/rooms/"+matrixRoom.ID+"/send/m.room.message" {
			t.Fatalf("Got request to unexpected path %q", req.URL.Path)
			return ""
		}

		if atomic.LoadInt32(&invites) == 0 {
			t.Errorf("Didn't get expected invite before message send")
		}
		if atomic.LoadInt32(&joins) == 0 {
			t.Errorf("Didn't get expected join before message send")
		}

		query := req.URL.Query()
		assertUrlValueEquals(t, query, "access_token", asToken)
		assertUrlValueEquals(t, query, "user_id", "@prefix_someoneonslack:my.server")

		b, err := ioutil.ReadAll(req.Body)
		if err != nil {
			t.Fatalf("Error reading request body: %v", err)
		}
		var content matrix.TextMessageContent
		if err := json.Unmarshal(b, &content); err != nil {
			t.Fatalf("Error unmarshaling json: %v", err)
		}
		if content.MsgType != "m.text" {
			t.Errorf("Msgtype: want %q got %q", "m.text", content.MsgType)
		}
		if content.Body != message {
			t.Errorf("Message: want %q got %q", message, content.Body)
		}
		callsAfter := atomic.AddInt32(&calls, 1)
		if callsAfter == 2 {
			calledTwice <- struct{}{}
		}
		return ""
	}
	client := http.Client{
		Transport: &spyRoundTripper{verify},
	}
	matrixUsers := matrix.NewUsers()
	bridge := Bridge{users, rooms, slackRoomMembers, matrixUsers, client, echoSuppresser, Config{
		MatrixASAccessToken: asToken,
		UserPrefix:          "@prefix_",
		HomeserverBaseURL:   "https://my.server",
		HomeserverName:      "my.server",
	}}
	bridge.OnSlackMessage(slack.Message{
		Type:    "message",
		Channel: "BOWLINGALLEY",
		TS:      "10",
		User:    slackUser,
		Text:    message,
	})
	bridge.OnSlackMessage(slack.Message{
		Type:    "message",
		Channel: "BOWLINGALLEY",
		TS:      "11",
		User:    slackUser,
		Text:    message,
	})
	select {
	case _ = <-calledTwice:
		if got := atomic.LoadInt32(&joins); got != 1 {
			t.Errorf("join count: want: %d, got: %d", 1, got)
		}
		if got := atomic.LoadInt32(&botJoins); got != 1 {
			t.Errorf("bot join count: want: %d, got: %d", 1, got)
		}
		return
	case _ = <-time.After(50 * time.Millisecond):
		t.Fatalf("Didn't get expected calls")
	}
}
func TestMatrixMessageFromUnlinkedUser(t *testing.T) {
	db := makeDB(t)
	rooms, err := NewRoomMap(db)
	if err != nil {
		t.Fatal(err)
	}
	slackChannel := "BOWLINGALLEY"
	message := "It's Nancy!"
	matrixUser := "******"
	matrixRoom := matrix.NewRoom("!abc123:matrix.org")

	rooms.Link(matrixRoom, slackChannel)

	echoSuppresser := common.NewEchoSuppresser()
	users, err := NewUserMap(db, http.Client{}, rooms, echoSuppresser)
	if err != nil {
		t.Fatal(err)
	}

	slackRoomMembers := slack.NewRoomMembers()
	slackRoomMembers.Add(slackChannel, &slack.User{"someone", &MockSlackClient{}})

	var calls int32
	called := make(chan struct{}, 1)
	verify := func(req *http.Request) string {
		b, err := ioutil.ReadAll(req.Body)
		if err != nil {
			t.Fatalf("Error reading request body: %v", err)
		}
		v, err := url.ParseQuery(string(b))
		if err != nil {
			t.Fatalf("Error parsing request body: %v", err)
		}
		assertUrlValueEquals(t, v, "token", "slack_access_token")
		assertUrlValueEquals(t, v, "channel", slackChannel)
		assertUrlValueEquals(t, v, "text", message)
		assertUrlValueEquals(t, v, "as_user", "false")
		assertUrlValueEquals(t, v, "username", matrixUser)
		if c := atomic.LoadInt32(&calls); c == 0 {
			if got, ok := v["icon_url"]; ok {
				t.Errorf("Want icon_url absent, got: %v", got)
			}
		} else if c == 1 {
			assertUrlValueEquals(t, v, "icon_url", "https://hs.url/_matrix/media/v1/download/st.andrews/sean.jpg")
		}
		atomic.AddInt32(&calls, 1)
		if calls == 2 {
			called <- struct{}{}
		}
		return ""
	}
	client := http.Client{
		Transport: &spyRoundTripper{verify},
	}
	bridge := Bridge{users, rooms, slackRoomMembers, nil, client, echoSuppresser, Config{
		HomeserverBaseURL: "https://hs.url",
	}}
	bridge.OnMatrixRoomMessage(matrix.RoomMessage{
		Type:    "m.room.message",
		Content: []byte(`{"msgtype": "m.text", "body": "` + message + `"}`),
		UserID:  matrixUser,
		RoomID:  matrixRoom.ID,
	})

	bridge.OnMatrixRoomMember(matrix.RoomMemberEvent{
		Type: "m.room.member",
		Content: matrix.UserInfo{
			Membership: "join",
			AvatarURL:  "mxc://st.andrews/sean.jpg",
		},
		StateKey: matrixUser,
		UserID:   matrixUser,
		RoomID:   matrixRoom.ID,
	})

	bridge.OnMatrixRoomMessage(matrix.RoomMessage{
		Type:    "m.room.message",
		Content: []byte(`{"msgtype": "m.text", "body": "` + message + `"}`),
		UserID:  matrixUser,
		RoomID:  matrixRoom.ID,
	})

	select {
	case _ = <-called:
		return
	case _ = <-time.After(50 * time.Millisecond):
		t.Fatalf("Didn't get expected calls (2), got: %d", atomic.LoadInt32(&calls))
	}
}