Esempio n. 1
0
File: room.go Progetto: robot0x/heim
func (rb *RoomBinding) RenameUser(ctx scope.Context, session proto.Session, formerName string) (
	*proto.NickEvent, error) {

	presence := &Presence{
		Room:      rb.Name,
		ServerID:  rb.desc.ID,
		ServerEra: rb.desc.Era,
		SessionID: session.ID(),
		Updated:   time.Now(),
	}
	err := presence.SetFact(&proto.Presence{
		SessionView:    *session.View(),
		LastInteracted: presence.Updated,
	})
	if err != nil {
		return nil, fmt.Errorf("presence marshal error: %s", err)
	}
	if _, err := rb.DbMap.Update(presence); err != nil {
		return nil, fmt.Errorf("presence update error: %s", err)
	}

	event := &proto.NickEvent{
		SessionID: session.ID(),
		ID:        session.Identity().ID(),
		From:      formerName,
		To:        session.Identity().Name(),
	}
	return event, rb.Backend.broadcast(ctx, rb.Room, proto.NickEventType, event, session)
}
Esempio n. 2
0
File: room.go Progetto: logan/heim
func (rb *RoomBinding) Snapshot(
	ctx scope.Context, session proto.Session, level proto.PrivilegeLevel, numMessages int) (*proto.SnapshotEvent, error) {

	snapshot := &proto.SnapshotEvent{
		Identity:  session.Identity().ID(),
		Nick:      session.Identity().Name(),
		SessionID: session.ID(),
		Version:   rb.Version(),
	}

	// TODO: do all this in a transaction

	listing, err := rb.Listing(ctx, level, session)
	if err != nil {
		return nil, err
	}
	snapshot.Listing = listing

	log, err := rb.Latest(ctx, numMessages, 0)
	if err != nil {
		return nil, err
	}
	snapshot.Log = log

	return snapshot, nil
}
Esempio n. 3
0
func (r *RoomBase) Snapshot(
	ctx scope.Context, session proto.Session, level proto.PrivilegeLevel, numMessages int) (*proto.SnapshotEvent, error) {

	snapshot := &proto.SnapshotEvent{
		Identity:  session.Identity().ID(),
		Nick:      session.Identity().Name(),
		SessionID: session.ID(),
		Version:   r.Version(),
	}

	listing, err := r.Listing(ctx, level, session)
	if err != nil {
		return nil, err
	}
	snapshot.Listing = listing

	log, err := r.Latest(ctx, numMessages, 0)
	if err != nil {
		return nil, err
	}
	snapshot.Log = log
	if level == proto.General {
		for i := range snapshot.Log {
			snapshot.Log[i].Sender.ClientAddress = ""
		}
	}

	return snapshot, nil
}
Esempio n. 4
0
func isExcluded(toCheck proto.Session, excluding []proto.Session) bool {
	for _, excSess := range excluding {
		if toCheck.ID() == excSess.ID() {
			return true
		}
	}
	return false
}
Esempio n. 5
0
func (r *memRoom) RenameUser(
	ctx scope.Context, session proto.Session, formerName string) (*proto.NickEvent, error) {

	backend.Logger(ctx).Printf(
		"renaming %s from %s to %s\n", session.ID(), formerName, session.Identity().Name())
	payload := &proto.NickEvent{
		SessionID: session.ID(),
		ID:        session.Identity().ID(),
		From:      formerName,
		To:        session.Identity().Name(),
	}
	return payload, r.broadcast(ctx, proto.NickType, payload, session)
}
Esempio n. 6
0
func (b *Backend) part(ctx scope.Context, rb *RoomBinding, session proto.Session) error {
	t, err := b.DbMap.Begin()
	if err != nil {
		return err
	}

	_, err = t.Exec(
		"DELETE FROM presence WHERE room = $1 AND server_id = $2 AND server_era = $3 AND session_id = $4",
		rb.RoomName, b.desc.ID, b.desc.Era, session.ID())
	if err != nil {
		rollback(ctx, t)
		logging.Logger(ctx).Printf("failed to persist departure: %s", err)
		return err
	}

	// Broadcast a presence event.
	// TODO: make this an explicit action via the Room protocol, to support encryption
	event := proto.PresenceEvent(session.View(proto.Staff))
	if err := rb.broadcast(ctx, t, proto.PartEventType, event, session); err != nil {
		rollback(ctx, t)
		return err
	}

	if err := t.Commit(); err != nil {
		return err
	}

	b.Lock()
	if lm, ok := b.listeners[rb.RoomName]; ok {
		delete(lm, session.ID())
	}
	b.Unlock()

	return nil
}
Esempio n. 7
0
File: room.go Progetto: logan/heim
func (r *RoomBase) Join(ctx scope.Context, session proto.Session) (string, error) {
	client := &proto.Client{}
	if !client.FromContext(ctx) {
		return "", fmt.Errorf("client data not found in scope")
	}

	r.m.Lock()
	defer r.m.Unlock()

	if r.identities == nil {
		r.identities = map[proto.UserID]proto.Identity{}
	}
	if r.nicks == nil {
		r.nicks = map[proto.UserID]string{}
	}
	if r.live == nil {
		r.live = map[proto.UserID][]proto.Session{}
	}
	if r.clients == nil {
		r.clients = map[string]*proto.Client{}
	}

	ident := session.Identity()
	id := ident.ID()

	if banned, ok := r.agentBans[ident.ID()]; ok && banned.After(time.Now()) {
		return "", proto.ErrAccessDenied
	}

	if banned, ok := r.ipBans[client.IP]; ok && banned.After(time.Now()) {
		return "", proto.ErrAccessDenied
	}

	if _, ok := r.identities[id]; !ok {
		r.identities[id] = ident
	}

	r.live[id] = append(r.live[id], session)
	r.clients[session.ID()] = client

	event := proto.PresenceEvent(session.View(proto.Staff))
	return "virt:" + event.RealClientAddress, r.broadcast(ctx, proto.JoinType, &event, session)
}
Esempio n. 8
0
File: room.go Progetto: logan/heim
func (r *RoomBase) Part(ctx scope.Context, session proto.Session) error {
	r.m.Lock()
	defer r.m.Unlock()

	ident := session.Identity()
	id := ident.ID()
	live := r.live[id]
	for i, s := range live {
		if s == session {
			copy(live[i:], live[i+1:])
			r.live[id] = live[:len(live)-1]
		}
	}
	if len(r.live[id]) == 0 {
		delete(r.live, id)
		delete(r.identities, id)
	}
	delete(r.clients, session.ID())
	event := proto.PresenceEvent(session.View(proto.Staff))
	return r.broadcast(ctx, proto.PartEventType, &event, session)
}
Esempio n. 9
0
func (b *Backend) part(ctx scope.Context, room *Room, session proto.Session) error {
	b.Lock()
	defer b.Unlock()

	if lm, ok := b.listeners[room.Name]; ok {
		delete(lm, session.ID())
	}

	_, err := b.DbMap.Exec(
		"DELETE FROM presence"+
			" WHERE room = $1 AND server_id = $2 AND server_era = $3 AND session_id = $4",
		room.Name, b.desc.ID, b.desc.Era, session.ID())
	if err != nil {
		backend.Logger(ctx).Printf("failed to persist departure: %s", err)
	}

	// Broadcast a presence event.
	// TODO: make this an explicit action via the Room protocol, to support encryption
	return b.broadcast(ctx, room,
		proto.PartEventType, proto.PresenceEvent(*session.View()), session)
}
Esempio n. 10
0
func (r *memRoom) Join(ctx scope.Context, session proto.Session) error {
	client := &proto.Client{}
	if !client.FromContext(ctx) {
		return fmt.Errorf("client data not found in scope")
	}

	r.m.Lock()
	defer r.m.Unlock()

	if r.identities == nil {
		r.identities = map[proto.UserID]proto.Identity{}
	}
	if r.live == nil {
		r.live = map[proto.UserID][]proto.Session{}
	}
	if r.clients == nil {
		r.clients = map[string]*proto.Client{}
	}

	ident := session.Identity()
	id := ident.ID()

	if banned, ok := r.agentBans[ident.ID()]; ok && banned.After(time.Now()) {
		return proto.ErrAccessDenied
	}

	if banned, ok := r.ipBans[client.IP]; ok && banned.After(time.Now()) {
		return proto.ErrAccessDenied
	}

	if _, ok := r.identities[id]; !ok {
		r.identities[id] = ident
	}

	r.live[id] = append(r.live[id], session)
	r.clients[session.ID()] = client

	return r.broadcast(ctx, proto.JoinType,
		proto.PresenceEvent(*session.View()), session)
}
Esempio n. 11
0
File: room.go Progetto: logan/heim
func (r *RoomBase) RenameUser(
	ctx scope.Context, session proto.Session, formerName string) (*proto.NickEvent, error) {

	r.m.Lock()
	defer r.m.Unlock()

	logging.Logger(ctx).Printf(
		"renaming %s from %s to %s\n", session.ID(), formerName, session.Identity().Name())
	r.nicks[session.Identity().ID()] = session.Identity().Name()
	payload := &proto.NickEvent{
		SessionID: session.ID(),
		ID:        session.Identity().ID(),
		From:      formerName,
		To:        session.Identity().Name(),
	}
	return payload, r.broadcast(ctx, proto.NickType, payload, session)
}
Esempio n. 12
0
File: room.go Progetto: logan/heim
func (rb *RoomBinding) RenameUser(ctx scope.Context, session proto.Session, formerName string) (
	*proto.NickEvent, error) {

	presence := &Presence{
		Room:      rb.RoomName,
		ServerID:  rb.desc.ID,
		ServerEra: rb.desc.Era,
		SessionID: session.ID(),
		Updated:   time.Now(),
	}
	err := presence.SetFact(&proto.Presence{
		SessionView:    session.View(proto.Staff),
		LastInteracted: presence.Updated,
	})
	if err != nil {
		return nil, fmt.Errorf("presence marshal error: %s", err)
	}

	t, err := rb.DbMap.Begin()
	if err != nil {
		return nil, err
	}

	if _, err := t.Update(presence); err != nil {
		rollback(ctx, t)
		return nil, fmt.Errorf("presence update error: %s", err)
	}

	// Store latest nick.
	// Loop in read-committed mode to simulate UPSERT.
	for {
		// Try to insert; if this fails due to duplicate key value, try to update.
		nick := &Nick{
			Room:   rb.RoomName,
			UserID: string(session.Identity().ID()),
			Nick:   session.Identity().Name(),
		}
		if err := rb.DbMap.Insert(nick); err != nil {
			if !strings.HasPrefix(err.Error(), "pq: duplicate key value") {
				rollback(ctx, t)
				return nil, err
			}
		} else {
			break
		}
		n, err := rb.DbMap.Update(nick)
		if err != nil {
			rollback(ctx, t)
			return nil, err
		}
		if n > 0 {
			break
		}
	}

	event := &proto.NickEvent{
		SessionID: session.ID(),
		ID:        session.Identity().ID(),
		From:      formerName,
		To:        session.Identity().Name(),
	}
	if err := rb.broadcast(ctx, t, proto.NickEventType, event, session); err != nil {
		rollback(ctx, t)
		return nil, err
	}

	if err := t.Commit(); err != nil {
		return nil, err
	}

	return event, nil
}
Esempio n. 13
0
File: room.go Progetto: logan/heim
func (rb *RoomBinding) EditMessage(
	ctx scope.Context, session proto.Session, edit proto.EditMessageCommand) (
	proto.EditMessageReply, error) {

	var reply proto.EditMessageReply

	editID, err := snowflake.New()
	if err != nil {
		return reply, err
	}

	cols, err := allColumns(rb.DbMap, Message{}, "")
	if err != nil {
		return reply, err
	}

	t, err := rb.DbMap.Begin()
	if err != nil {
		return reply, err
	}

	rollback := func() {
		if err := t.Rollback(); err != nil {
			logging.Logger(ctx).Printf("rollback error: %s", err)
		}
	}

	var msg Message
	err = t.SelectOne(&msg, fmt.Sprintf("SELECT %s FROM message WHERE room = $1 AND id = $2", cols), rb.RoomName, edit.ID.String())
	if err != nil {
		rollback()
		return reply, err
	}

	if msg.PreviousEditID.Valid && msg.PreviousEditID.String != edit.PreviousEditID.String() {
		rollback()
		return reply, proto.ErrEditInconsistent
	}

	entry := &MessageEditLog{
		EditID:          editID.String(),
		Room:            rb.RoomName,
		MessageID:       edit.ID.String(),
		PreviousEditID:  msg.PreviousEditID,
		PreviousContent: msg.Content,
		PreviousParent: sql.NullString{
			String: msg.Parent,
			Valid:  true,
		},
	}
	// TODO: tests pass in a nil session, until we add support for the edit command
	if session != nil {
		entry.EditorID = sql.NullString{
			String: string(session.Identity().ID()),
			Valid:  true,
		}
	}
	if err := t.Insert(entry); err != nil {
		rollback()
		return reply, err
	}

	now := time.Time(proto.Now())
	sets := []string{"edited = $3", "previous_edit_id = $4"}
	args := []interface{}{rb.RoomName, edit.ID.String(), now, editID.String()}
	msg.Edited = gorp.NullTime{Valid: true, Time: now}
	if edit.Content != "" {
		args = append(args, edit.Content)
		sets = append(sets, fmt.Sprintf("content = $%d", len(args)))
		msg.Content = edit.Content
	}
	if edit.Parent != 0 {
		args = append(args, edit.Parent.String())
		sets = append(sets, fmt.Sprintf("parent = $%d", len(args)))
		msg.Parent = edit.Parent.String()
	}
	if edit.Delete != msg.Deleted.Valid {
		if edit.Delete {
			args = append(args, now)
			sets = append(sets, fmt.Sprintf("deleted = $%d", len(args)))
			msg.Deleted = gorp.NullTime{Valid: true, Time: now}
		} else {
			sets = append(sets, "deleted = NULL")
			msg.Deleted.Valid = false
		}
	}
	query := fmt.Sprintf("UPDATE message SET %s WHERE room = $1 AND id = $2", strings.Join(sets, ", "))
	if _, err := t.Exec(query, args...); err != nil {
		rollback()
		return reply, err
	}

	if edit.Announce {
		event := &proto.EditMessageEvent{
			EditID:  editID,
			Message: msg.ToTransmission(),
		}
		err = rb.broadcast(ctx, t, proto.EditMessageEventType, event, session)
		if err != nil {
			rollback()
			return reply, err
		}
	}

	if err := t.Commit(); err != nil {
		return reply, err
	}

	reply.EditID = editID
	reply.Message = msg.ToTransmission()
	return reply, nil
}
Esempio n. 14
0
func (b *Backend) join(ctx scope.Context, rb *RoomBinding, session proto.Session) (string, error) {
	client := &proto.Client{}
	if !client.FromContext(ctx) {
		return "", fmt.Errorf("client data not found in scope")
	}

	bannedAgentCols, err := allColumns(b.DbMap, BannedAgent{}, "")
	if err != nil {
		return "", err
	}

	bannedIPCols, err := allColumns(b.DbMap, BannedIP{}, "")
	if err != nil {
		return "", err
	}

	t, err := b.DbMap.Begin()
	if err != nil {
		return "", err
	}

	// Check for agent ID bans.
	agentBans, err := t.Select(
		BannedAgent{},
		fmt.Sprintf(
			"SELECT %s FROM banned_agent WHERE agent_id = $1 AND (room IS NULL OR room = $2) AND (expires IS NULL OR expires > NOW())",
			bannedAgentCols),
		session.Identity().ID().String(), rb.RoomName)
	if err != nil {
		rollback(ctx, t)
		return "", err
	}
	if len(agentBans) > 0 {
		logging.Logger(ctx).Printf("access denied to %s: %#v", session.Identity().ID(), agentBans)
		if err := t.Rollback(); err != nil {
			return "", err
		}
		return "", proto.ErrAccessDenied
	}

	// Check for IP bans.
	ipBans, err := t.Select(
		BannedIP{},
		fmt.Sprintf(
			"SELECT %s FROM banned_ip WHERE ip = $1 AND (room IS NULL OR room = $2) AND (expires IS NULL OR expires > NOW())",
			bannedIPCols),
		client.IP, rb.RoomName)
	if err != nil {
		rollback(ctx, t)
		return "", err
	}
	if len(ipBans) > 0 {
		logging.Logger(ctx).Printf("access denied to %s: %#v", client.IP, ipBans)
		if err := t.Rollback(); err != nil {
			return "", err
		}
		return "", proto.ErrAccessDenied
	}

	// Virtualize the session's client address.
	var row struct {
		Address string `db:"address"`
	}
	if err := t.SelectOne(&row, "SELECT virtualize_address($1, $2::inet) AS address", rb.RoomName, client.IP); err != nil {
		return "", err
	}
	virtualAddress := row.Address

	// Look up session's nick.
	nickRow, err := t.Get(Nick{}, string(session.Identity().ID()), rb.RoomName)
	if err != nil && err != sql.ErrNoRows {
		return "", err
	}
	if nickRow != nil {
		session.SetName(nickRow.(*Nick).Nick)
	}

	// Write to session log.
	// TODO: do proper upsert simulation
	entry := &SessionLog{
		SessionID: session.ID(),
		IP:        client.IP,
		Room:      rb.RoomName,
		UserAgent: client.UserAgent,
		Connected: client.Connected,
	}
	if _, err := t.Delete(entry); err != nil {
		if rerr := t.Rollback(); rerr != nil {
			logging.Logger(ctx).Printf("rollback error: %s", rerr)
		}
		return "", err
	}
	if err := t.Insert(entry); err != nil {
		if rerr := t.Rollback(); rerr != nil {
			logging.Logger(ctx).Printf("rollback error: %s", rerr)
		}
		return "", err
	}

	// Broadcast a presence event.
	// TODO: make this an explicit action via the Room protocol, to support encryption

	presence := &Presence{
		Room:      rb.RoomName,
		ServerID:  b.desc.ID,
		ServerEra: b.desc.Era,
		SessionID: session.ID(),
		Updated:   time.Now(),
	}
	sessionView := session.View(proto.Staff)
	sessionView.ClientAddress = virtualAddress
	err = presence.SetFact(&proto.Presence{
		SessionView:    sessionView,
		LastInteracted: presence.Updated,
	})
	if err != nil {
		rollback(ctx, t)
		return "", fmt.Errorf("presence marshal error: %s", err)
	}
	if err := t.Insert(presence); err != nil {
		return "", fmt.Errorf("presence insert error: %s", err)
	}

	b.Lock()
	// Add session to listeners.
	lm, ok := b.listeners[rb.RoomName]
	if !ok {
		lm = ListenerMap{}
		b.listeners[rb.RoomName] = lm
	}
	lm[session.ID()] = Listener{Session: session, Client: client}
	b.Unlock()

	event := proto.PresenceEvent(session.View(proto.Staff))
	event.ClientAddress = virtualAddress
	if err := rb.broadcast(ctx, t, proto.JoinEventType, event, session); err != nil {
		rollback(ctx, t)
		return "", err
	}

	if err := t.Commit(); err != nil {
		return "", err
	}

	return virtualAddress, nil
}
Esempio n. 15
0
func (b *Backend) join(ctx scope.Context, room *Room, session proto.Session) error {
	client := &proto.Client{}
	if !client.FromContext(ctx) {
		return fmt.Errorf("client data not found in scope")
	}

	bannedAgentCols, err := allColumns(b.DbMap, BannedAgent{}, "")
	if err != nil {
		return err
	}

	bannedIPCols, err := allColumns(b.DbMap, BannedIP{}, "")
	if err != nil {
		return err
	}

	t, err := b.DbMap.Begin()
	if err != nil {
		return err
	}

	rb := func() { rollback(ctx, t) }

	// Check for agent ID bans.
	agentBans, err := t.Select(
		BannedAgent{},
		fmt.Sprintf(
			"SELECT %s FROM banned_agent WHERE agent_id = $1 AND (room IS NULL OR room = $2) AND (expires IS NULL OR expires > NOW())",
			bannedAgentCols),
		session.Identity().ID().String(), room.Name)
	if err != nil {
		rb()
		return err
	}
	if len(agentBans) > 0 {
		logging.Logger(ctx).Printf("access denied to %s: %#v", session.Identity().ID(), agentBans)
		if err := t.Rollback(); err != nil {
			return err
		}
		return proto.ErrAccessDenied
	}

	// Check for IP bans.
	ipBans, err := t.Select(
		BannedIP{},
		fmt.Sprintf(
			"SELECT %s FROM banned_ip WHERE ip = $1 AND (room IS NULL OR room = $2) AND (expires IS NULL OR expires > NOW())",
			bannedIPCols),
		client.IP, room.Name)
	if err != nil {
		rb()
		return err
	}
	if len(ipBans) > 0 {
		logging.Logger(ctx).Printf("access denied to %s: %#v", client.IP, ipBans)
		if err := t.Rollback(); err != nil {
			return err
		}
		return proto.ErrAccessDenied
	}

	// Write to session log.
	// TODO: do proper upsert simulation
	entry := &SessionLog{
		SessionID: session.ID(),
		IP:        client.IP,
		Room:      room.Name,
		UserAgent: client.UserAgent,
		Connected: client.Connected,
	}
	if _, err := t.Delete(entry); err != nil {
		if rerr := t.Rollback(); rerr != nil {
			logging.Logger(ctx).Printf("rollback error: %s", rerr)
		}
		return err
	}
	if err := t.Insert(entry); err != nil {
		if rerr := t.Rollback(); rerr != nil {
			logging.Logger(ctx).Printf("rollback error: %s", rerr)
		}
		return err
	}

	// Broadcast a presence event.
	// TODO: make this an explicit action via the Room protocol, to support encryption

	presence := &Presence{
		Room:      room.Name,
		ServerID:  b.desc.ID,
		ServerEra: b.desc.Era,
		SessionID: session.ID(),
		Updated:   time.Now(),
	}
	err = presence.SetFact(&proto.Presence{
		SessionView:    *session.View(),
		LastInteracted: presence.Updated,
	})
	if err != nil {
		rb()
		return fmt.Errorf("presence marshal error: %s", err)
	}
	if err := t.Insert(presence); err != nil {
		return fmt.Errorf("presence insert error: %s", err)
	}

	b.Lock()
	// Add session to listeners.
	lm, ok := b.listeners[room.Name]
	if !ok {
		lm = ListenerMap{}
		b.listeners[room.Name] = lm
	}
	lm[session.ID()] = Listener{Session: session, Client: client}
	b.Unlock()

	if err := room.broadcast(ctx, t, proto.JoinEventType, proto.PresenceEvent(*session.View()), session); err != nil {
		rb()
		return err
	}

	if err := t.Commit(); err != nil {
		return err
	}

	return nil
}
Esempio n. 16
0
func (b *Backend) join(ctx scope.Context, room *Room, session proto.Session) error {
	client := &proto.Client{}
	if !client.FromContext(ctx) {
		return fmt.Errorf("client data not found in scope")
	}

	// Check for agent ID bans.
	agentBans, err := b.DbMap.Select(
		BannedAgent{},
		"SELECT agent_id, room, created, expires, room_reason, agent_reason, private_reason"+
			" FROM banned_agent"+
			" WHERE agent_id = $1 AND (room IS NULL OR room = $2)"+
			" AND (expires IS NULL OR expires > NOW())",
		session.Identity().ID().String(), room.Name)
	if err != nil {
		return err
	}
	if len(agentBans) > 0 {
		backend.Logger(ctx).Printf("access denied to %s: %#v", session.Identity().ID(), agentBans)
		return proto.ErrAccessDenied
	}

	// Check for IP bans.
	ipBans, err := b.DbMap.Select(
		BannedIP{},
		"SELECT ip, room, created, expires, reason FROM banned_ip"+
			" WHERE ip = $1 AND (room IS NULL OR room = $2)"+
			" AND (expires IS NULL OR expires > NOW())",
		client.IP, room.Name)
	if err != nil {
		return err
	}
	if len(ipBans) > 0 {
		backend.Logger(ctx).Printf("access denied to %s: %#v", client.IP, ipBans)
		return proto.ErrAccessDenied
	}

	// Write to session log.
	entry := &SessionLog{
		SessionID: session.ID(),
		IP:        client.IP,
		Room:      room.Name,
		UserAgent: client.UserAgent,
		Connected: client.Connected,
	}
	t, err := b.DbMap.Begin()
	if err != nil {
		return err
	}
	if _, err := t.Delete(entry); err != nil {
		if rerr := t.Rollback(); rerr != nil {
			backend.Logger(ctx).Printf("rollback error: %s", rerr)
		}
		return err
	}
	if err := t.Insert(entry); err != nil {
		if rerr := t.Rollback(); rerr != nil {
			backend.Logger(ctx).Printf("rollback error: %s", rerr)
		}
		return err
	}
	if err := t.Commit(); err != nil {
		return err
	}

	b.Lock()
	defer b.Unlock()

	// Add session to listeners.
	lm, ok := b.listeners[room.Name]
	if !ok {
		b.debug("registering listener for %s", room.Name)
		lm = ListenerMap{}
		b.listeners[room.Name] = lm
	}
	lm[session.ID()] = Listener{Session: session, Client: client}

	// Broadcast a presence event.
	// TODO: make this an explicit action via the Room protocol, to support encryption

	presence := &Presence{
		Room:      room.Name,
		ServerID:  b.desc.ID,
		ServerEra: b.desc.Era,
		SessionID: session.ID(),
		Updated:   time.Now(),
	}
	err = presence.SetFact(&proto.Presence{
		SessionView:    *session.View(),
		LastInteracted: presence.Updated,
	})
	if err != nil {
		return fmt.Errorf("presence marshal error: %s", err)
	}
	if err := b.DbMap.Insert(presence); err != nil {
		return fmt.Errorf("presence insert error: %s", err)
	}
	b.debug("joining session: %#v", session)
	b.debug(" -> %#v", session.View())
	return b.broadcast(ctx, room,
		proto.JoinEventType, proto.PresenceEvent(*session.View()), session)
}