فهرست منبع

webapi: remove feedbag adapter, WebPermitDenyManager, chat, redundant presence code

Mike 2 هفته پیش
والد
کامیت
58c11db418

+ 1 - 11
cmd/server/factory.go

@@ -516,11 +516,6 @@ func TOC(deps Container) *toc.Server {
 func WebAPI(deps Container) *webapi.Server {
 	logger := deps.logger.With("svc", "webapi")
 
-	// Create feedbag adapter for WebAPI
-	feedbagAdapter := &webapi.FeedbagAdapter{
-		Store: deps.sqLiteUserStore,
-	}
-
 	// Create WebAPI buddy list manager (local to WebAPI)
 	buddyListManager := handlers.NewBuddyListManager(
 		deps.feedbagSvc,
@@ -611,8 +606,6 @@ func WebAPI(deps Container) *webapi.Server {
 		SNACRateLimits: deps.snacRateLimits,
 		// New fields for WebAPI handlers
 		SessionRetriever: deps.inMemorySessionManager,
-		FeedbagRetriever: feedbagAdapter,
-		FeedbagManager:   feedbagAdapter,
 		// Phase 2 additions
 		MessageRelayer:        deps.inMemorySessionManager,
 		OfflineMessageManager: deps.sqLiteUserStore,
@@ -621,14 +614,11 @@ func WebAPI(deps Container) *webapi.Server {
 		RelationshipFetcher:   deps.sqLiteUserStore,
 		// Phase 3 additions
 		PreferenceManager: deps.sqLiteUserStore.NewWebPreferenceManager(),
-		PermitDenyManager: deps.sqLiteUserStore.NewWebPermitDenyManager(),
 		// Phase 4 additions for OSCAR Bridge
 		OSCARBridgeStore: deps.sqLiteUserStore.NewOSCARBridgeStore(),
 		OSCARConfig:      webapi.NewOSCARConfigAdapter(deps.cfg),
 		// Phase 5 additions for buddy list and messaging
-		BuddyListManager: buddyListManager,
-		// Phase 5 additions for chat rooms
-		ChatManager:        deps.sqLiteUserStore.NewWebAPIChatManager(logger, deps.webAPISessionManager),
+		BuddyListManager:   buddyListManager,
 		ChatSessionManager: deps.chatSessionManager,
 		RecalcWarning:      deps.icbmSvc.RestoreWarningLevel,
 		LowerWarnLevel:     deps.icbmSvc.UpdateWarnLevel,

+ 0 - 136
server/webapi/adapters.go

@@ -1,136 +0,0 @@
-package webapi
-
-import (
-	"bytes"
-	"context"
-	"crypto/rand"
-	"encoding/binary"
-	"time"
-
-	"github.com/mk6i/open-oscar-server/server/webapi/types"
-	"github.com/mk6i/open-oscar-server/state"
-	"github.com/mk6i/open-oscar-server/wire"
-)
-
-// FeedbagAdapter wraps SQLiteUserStore to implement FeedbagRetriever and FeedbagManager interfaces
-type FeedbagAdapter struct {
-	Store *state.SQLiteUserStore
-}
-
-// RetrieveFeedbag implements FeedbagRetriever interface
-func (f *FeedbagAdapter) RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error) {
-	return f.Store.Feedbag(ctx, screenName)
-}
-
-// RelationshipsByUser implements FeedbagRetriever interface
-// Returns the list of users who have this user in their buddy list
-func (f *FeedbagAdapter) RelationshipsByUser(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error) {
-	// Get all relationships where this user is involved
-	relationships, err := f.Store.AllRelationships(ctx, screenName, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	// Extract unique screen names from relationships
-	uniqueUsers := make(map[state.IdentScreenName]bool)
-	for _, rel := range relationships {
-		// Add the user from the relationship
-		uniqueUsers[rel.User] = true
-	}
-
-	// Convert map to slice
-	users := make([]state.IdentScreenName, 0, len(uniqueUsers))
-	for user := range uniqueUsers {
-		users = append(users, user)
-	}
-
-	return users, nil
-}
-
-// InsertItem implements FeedbagManager interface
-func (f *FeedbagAdapter) InsertItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error {
-	// Use FeedbagUpsert to insert a new item
-	return f.Store.FeedbagUpsert(ctx, screenName, []wire.FeedbagItem{item})
-}
-
-// UpdateItem implements FeedbagManager interface
-func (f *FeedbagAdapter) UpdateItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error {
-	// Use FeedbagUpsert to update an existing item
-	return f.Store.FeedbagUpsert(ctx, screenName, []wire.FeedbagItem{item})
-}
-
-// DeleteItem implements FeedbagManager interface
-func (f *FeedbagAdapter) DeleteItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error {
-	// Use FeedbagDelete to remove an item
-	return f.Store.FeedbagDelete(ctx, screenName, []wire.FeedbagItem{item})
-}
-
-// Message Conversion Functions
-
-// WebAPIToICBM converts a Web API message to OSCAR ICBM format
-func WebAPIToICBM(sender state.IdentScreenName, recipient string, message string, autoResponse bool) (wire.SNAC_0x04_0x06_ICBMChannelMsgToHost, error) {
-	// Generate message cookie
-	var cookie [8]byte
-	if _, err := rand.Read(cookie[:]); err != nil {
-		return wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{}, err
-	}
-	cookieUint64 := binary.BigEndian.Uint64(cookie[:])
-
-	// Create ICBM fragment list for the message
-	frags, err := wire.ICBMFragmentList(message)
-	if err != nil {
-		return wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{}, err
-	}
-
-	// Marshal the fragments
-	buf := &bytes.Buffer{}
-	for _, frag := range frags {
-		if err := wire.MarshalBE(frag, buf); err != nil {
-			return wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{}, err
-		}
-	}
-
-	// Build ICBM message
-	icbmMsg := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
-		Cookie:     cookieUint64,
-		ChannelID:  wire.ICBMChannelIM,
-		ScreenName: recipient,
-		TLVRestBlock: wire.TLVRestBlock{
-			TLVList: wire.TLVList{
-				wire.NewTLVBE(wire.ICBMTLVAOLIMData, buf.Bytes()),
-			},
-		},
-	}
-
-	// Add auto-response flag if applicable
-	if autoResponse {
-		icbmMsg.Append(wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}))
-	}
-
-	return icbmMsg, nil
-}
-
-// PresenceUpdateToWebAPIEvent converts OSCAR buddy arrival/departure to WebAPI event
-func PresenceUpdateToWebAPIEvent(screenName string, online bool, awayMsg string, statusBitmask uint32) types.Event {
-	stateStr := "offline"
-	if online {
-		stateStr = "online"
-		if statusBitmask&wire.OServiceUserStatusAway != 0 {
-			stateStr = "away"
-		} else if statusBitmask&wire.OServiceUserStatusDND != 0 {
-			stateStr = "dnd"
-		} else if statusBitmask&wire.OServiceUserStatusInvisible != 0 {
-			stateStr = "invisible"
-		}
-	}
-
-	return types.Event{
-		Type:      types.EventTypePresence,
-		Timestamp: time.Now().Unix(),
-		Data: types.PresenceEvent{
-			AimID:   screenName,
-			State:   stateStr,
-			AwayMsg: awayMsg,
-		},
-	}
-}

+ 1 - 6
server/webapi/handler.go

@@ -29,8 +29,6 @@ type Handler struct {
 	SNACRateLimits    wire.SNACRateLimits
 	// New fields for WebAPI handlers
 	SessionRetriever SessionRetriever
-	FeedbagRetriever FeedbagRetriever
-	FeedbagManager   FeedbagManager
 	// Phase 2 additions
 	MessageRelayer        MessageRelayer
 	OfflineMessageManager OfflineMessageManager
@@ -41,14 +39,11 @@ type Handler struct {
 	}
 	// Phase 3 additions
 	PreferenceManager PreferenceManager
-	PermitDenyManager PermitDenyManager
 	// Phase 4 additions for OSCAR Bridge
 	OSCARBridgeStore OSCARBridgeStore
 	OSCARConfig      OSCARConfig
 	// Phase 5 additions for buddy list and messaging
-	BuddyListManager interface{}
-	// Phase 5 additions for chat rooms
-	ChatManager        *state.WebAPIChatManager
+	BuddyListManager   interface{}
 	RecalcWarning      func(ctx context.Context, instance *state.SessionInstance) error
 	LowerWarnLevel     func(ctx context.Context, instance *state.SessionInstance)
 	ChatSessionManager ChatSessionManager

+ 3 - 4
server/webapi/handlers/buddyfeed.go

@@ -14,10 +14,9 @@ import (
 
 // BuddyFeedHandler handles Web AIM API buddy feed endpoints.
 type BuddyFeedHandler struct {
-	SessionManager   *state.WebAPISessionManager
-	FeedManager      *state.BuddyFeedManager
-	SessionRetriever SessionRetriever
-	Logger           *slog.Logger
+	SessionManager *state.WebAPISessionManager
+	FeedManager    *state.BuddyFeedManager
+	Logger         *slog.Logger
 }
 
 // GetUser handles GET /buddyfeed/getUser requests to retrieve a user's feed.

+ 0 - 282
server/webapi/handlers/chat.go

@@ -1,282 +0,0 @@
-package handlers
-
-import (
-	"log/slog"
-	"net/http"
-	"strings"
-
-	"github.com/mk6i/open-oscar-server/state"
-)
-
-// ChatHandler handles Web API chat endpoints
-type ChatHandler struct {
-	SessionManager *state.WebAPISessionManager
-	ChatManager    *state.WebAPIChatManager
-	Logger         *slog.Logger
-}
-
-// CreateAndJoinChat creates (if needed) and joins a chat room
-// GET /chat/createAndJoinChat
-func (h *ChatHandler) CreateAndJoinChat(w http.ResponseWriter, r *http.Request) {
-	// Extract parameters
-	aimsid := r.URL.Query().Get("aimsid")
-	roomID := r.URL.Query().Get("roomId")
-	roomName := r.URL.Query().Get("roomName")
-
-	// Validate session
-	session, err := h.SessionManager.GetSession(r.Context(), aimsid)
-	if err != nil {
-		h.Logger.Error("invalid session", "aimsid", aimsid, "error", err)
-		SendError(w, http.StatusUnauthorized, "Authentication Required")
-		return
-	}
-
-	// Validate parameters - exactly one of roomId or roomName must be provided
-	if (roomID == "" && roomName == "") || (roomID != "" && roomName != "") {
-		SendError(w, http.StatusBadRequest, "Exactly one of roomId or roomName must be provided")
-		return
-	}
-
-	// Create or join the chat room
-	chatSession, room, err := h.ChatManager.CreateAndJoinChat(r.Context(), aimsid, roomID, roomName, string(session.ScreenName))
-	if err != nil {
-		h.Logger.Error("failed to create/join chat", "error", err, "aimsid", aimsid)
-
-		// Determine appropriate error code
-		statusCode := http.StatusInternalServerError
-		message := "Internal Server Error"
-		if strings.Contains(err.Error(), "maximum capacity") {
-			statusCode = http.StatusServiceUnavailable
-			message = "Room is at maximum capacity"
-		} else if strings.Contains(err.Error(), "must be provided") {
-			statusCode = http.StatusBadRequest
-			message = err.Error()
-		}
-
-		SendError(w, statusCode, message)
-		return
-	}
-
-	// Build response
-	roomData := map[string]interface{}{
-		"roomName":    room.RoomName,
-		"roomId":      room.RoomID,
-		"instanceId":  room.InstanceID,
-		"description": room.Description,
-		"roomType":    string(room.RoomType),
-	}
-
-	// Add category ID if present
-	if room.CategoryID != "" {
-		roomData["categoryId"] = room.CategoryID
-	}
-
-	response := BaseResponse{
-		Response: ResponseBody{
-			StatusCode: 200,
-			StatusText: "OK",
-			Data: map[string]interface{}{
-				"chatsid": chatSession.ChatSID,
-				"room":    roomData,
-			},
-		},
-	}
-
-	// Send response
-	SendResponse(w, r, response, h.Logger)
-
-	h.Logger.Info("user joined chat room",
-		"screenName", session.ScreenName,
-		"roomName", room.RoomName,
-		"roomID", room.RoomID,
-		"chatsid", chatSession.ChatSID)
-}
-
-// SendMessage sends a message to a chat room
-// GET /chat/sendMessage
-func (h *ChatHandler) SendMessage(w http.ResponseWriter, r *http.Request) {
-	// Extract parameters
-	aimsid := r.URL.Query().Get("aimsid")
-	chatsid := r.URL.Query().Get("chatsid")
-	message := r.URL.Query().Get("message")
-	whisperTarget := r.URL.Query().Get("whisperTarget")
-
-	// Validate session
-	_, err := h.SessionManager.GetSession(r.Context(), aimsid)
-	if err != nil {
-		h.Logger.Error("invalid session", "aimsid", aimsid, "error", err)
-		SendError(w, http.StatusUnauthorized, "Authentication Required")
-		return
-	}
-
-	// Validate required parameters
-	if chatsid == "" {
-		SendError(w, http.StatusBadRequest, "chatsid is required")
-		return
-	}
-
-	if message == "" {
-		SendError(w, http.StatusBadRequest, "message is required")
-		return
-	}
-
-	// Send the message
-	err = h.ChatManager.SendMessage(r.Context(), chatsid, message, whisperTarget)
-	if err != nil {
-		h.Logger.Error("failed to send message", "error", err, "chatsid", chatsid)
-
-		// Determine appropriate error code
-		statusCode := http.StatusInternalServerError
-		message := "Internal Server Error"
-		if strings.Contains(err.Error(), "invalid chat session") || strings.Contains(err.Error(), "user has left") {
-			statusCode = http.StatusNotFound
-			message = "Chat session not found"
-		}
-
-		SendError(w, statusCode, message)
-		return
-	}
-
-	// Build response
-	response := BaseResponse{
-		Response: ResponseBody{
-			StatusCode: 200,
-			StatusText: "OK",
-			Data:       map[string]interface{}{},
-		},
-	}
-
-	// Send response
-	SendResponse(w, r, response, h.Logger)
-
-	logMsg := "message sent to chat room"
-	if whisperTarget != "" {
-		logMsg = "whisper sent in chat room"
-	}
-	h.Logger.Debug(logMsg, "chatsid", chatsid, "whisperTarget", whisperTarget)
-}
-
-// SetTyping sets typing status for a chat room
-// GET /chat/setTyping
-func (h *ChatHandler) SetTyping(w http.ResponseWriter, r *http.Request) {
-	// Extract parameters
-	aimsid := r.URL.Query().Get("aimsid")
-	chatsid := r.URL.Query().Get("chatsid")
-	typingStatus := r.URL.Query().Get("typingStatus")
-
-	// Validate session
-	_, err := h.SessionManager.GetSession(r.Context(), aimsid)
-	if err != nil {
-		h.Logger.Error("invalid session", "aimsid", aimsid, "error", err)
-		SendError(w, http.StatusUnauthorized, "Authentication Required")
-		return
-	}
-
-	// Validate required parameters
-	if chatsid == "" {
-		SendError(w, http.StatusBadRequest, "chatsid is required")
-		return
-	}
-
-	if typingStatus == "" {
-		SendError(w, http.StatusBadRequest, "typingStatus is required")
-		return
-	}
-
-	// Validate typing status value
-	validStatuses := map[string]bool{
-		"none":   true,
-		"typing": true,
-		"typed":  true,
-	}
-	if !validStatuses[typingStatus] {
-		SendError(w, http.StatusBadRequest, "Invalid typingStatus value")
-		return
-	}
-
-	// Set typing status
-	err = h.ChatManager.SetTyping(r.Context(), chatsid, typingStatus)
-	if err != nil {
-		h.Logger.Error("failed to set typing status", "error", err, "chatsid", chatsid)
-
-		// Determine appropriate error code
-		statusCode := http.StatusInternalServerError
-		errMessage := "Internal Server Error"
-		if strings.Contains(err.Error(), "invalid chat session") || strings.Contains(err.Error(), "user has left") {
-			statusCode = http.StatusNotFound
-			errMessage = "Chat session not found"
-		}
-
-		SendError(w, statusCode, errMessage)
-		return
-	}
-
-	// Build response
-	response := BaseResponse{
-		Response: ResponseBody{
-			StatusCode: 200,
-			StatusText: "OK",
-			Data:       map[string]interface{}{},
-		},
-	}
-
-	// Send response
-	SendResponse(w, r, response, h.Logger)
-
-	h.Logger.Debug("typing status updated", "chatsid", chatsid, "status", typingStatus)
-}
-
-// LeaveChat leaves the current chat room
-// GET /chat/leaveChat
-func (h *ChatHandler) LeaveChat(w http.ResponseWriter, r *http.Request) {
-	// Extract parameters
-	aimsid := r.URL.Query().Get("aimsid")
-	chatsid := r.URL.Query().Get("chatsid")
-
-	// Validate session
-	session, err := h.SessionManager.GetSession(r.Context(), aimsid)
-	if err != nil {
-		h.Logger.Error("invalid session", "aimsid", aimsid, "error", err)
-		SendError(w, http.StatusUnauthorized, "Authentication Required")
-		return
-	}
-
-	// Validate required parameters
-	if chatsid == "" {
-		SendError(w, http.StatusBadRequest, "chatsid is required")
-		return
-	}
-
-	// Leave the chat room
-	err = h.ChatManager.LeaveChat(r.Context(), chatsid)
-	if err != nil {
-		h.Logger.Error("failed to leave chat", "error", err, "chatsid", chatsid)
-
-		// Determine appropriate error code
-		statusCode := http.StatusInternalServerError
-		message := "Internal Server Error"
-		if strings.Contains(err.Error(), "invalid chat session") {
-			statusCode = http.StatusNotFound
-			message = "Chat session not found"
-		}
-
-		SendError(w, statusCode, message)
-		return
-	}
-
-	// Build response
-	response := BaseResponse{
-		Response: ResponseBody{
-			StatusCode: 200,
-			StatusText: "OK",
-			Data:       map[string]interface{}{},
-		},
-	}
-
-	// Send response
-	SendResponse(w, r, response, h.Logger)
-
-	h.Logger.Info("user left chat room",
-		"screenName", session.ScreenName,
-		"chatsid", chatsid)
-}

+ 0 - 7
server/webapi/handlers/common.go

@@ -12,7 +12,6 @@ import (
 	"strings"
 
 	"github.com/mk6i/open-oscar-server/state"
-	"github.com/mk6i/open-oscar-server/wire"
 )
 
 // SessionRetriever provides methods to retrieve OSCAR sessions.
@@ -21,12 +20,6 @@ type SessionRetriever interface {
 	RetrieveSession(screenName state.IdentScreenName) *state.Session
 }
 
-// FeedbagRetriever provides methods to retrieve feedbag data.
-type FeedbagRetriever interface {
-	RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error)
-	RelationshipsByUser(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error)
-}
-
 // CommonHandler provides shared utilities for all Web API handlers.
 type CommonHandler struct {
 	Logger *slog.Logger

+ 14 - 5
server/webapi/handlers/presence.go

@@ -2,6 +2,7 @@ package handlers
 
 import (
 	"context"
+	"fmt"
 	"log/slog"
 	"net/http"
 	"strings"
@@ -16,7 +17,7 @@ import (
 type PresenceHandler struct {
 	SessionManager      *state.WebAPISessionManager
 	SessionRetriever    SessionRetriever
-	FeedbagRetriever    FeedbagRetriever
+	FeedbagService      FeedbagService
 	BuddyBroadcaster    BuddyBroadcaster
 	ProfileManager      ProfileManager
 	RelationshipFetcher RelationshipFetcher
@@ -106,7 +107,7 @@ func (h *PresenceHandler) GetPresence(w http.ResponseWriter, r *http.Request) {
 
 	if getBuddyList {
 		// Retrieve buddy list from feedbag
-		groups, err := h.getBuddyListGroups(ctx, session.ScreenName.IdentScreenName())
+		groups, err := h.getBuddyListGroups(ctx, session)
 		if err != nil {
 			h.Logger.ErrorContext(ctx, "failed to get buddy list", "err", err.Error())
 			// Return empty buddy list on error instead of failing
@@ -179,12 +180,20 @@ func (h *PresenceHandler) GetPresence(w http.ResponseWriter, r *http.Request) {
 }
 
 // getBuddyListGroups retrieves the buddy list organized by groups.
-func (h *PresenceHandler) getBuddyListGroups(ctx context.Context, screenName state.IdentScreenName) ([]BuddyGroupInfo, error) {
-	// Get feedbag items
-	items, err := h.FeedbagRetriever.RetrieveFeedbag(ctx, screenName)
+func (h *PresenceHandler) getBuddyListGroups(ctx context.Context, session *state.WebAPISession) ([]BuddyGroupInfo, error) {
+	screenName := session.ScreenName.IdentScreenName()
+
+	// Get feedbag items via the feedbag service
+	frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
+	reply, err := h.FeedbagService.Query(ctx, session.OSCARSession, frame)
 	if err != nil {
 		return nil, err
 	}
+	body, ok := reply.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
+	if !ok {
+		return nil, fmt.Errorf("unexpected feedbag reply body type %T", reply.Body)
+	}
+	items := body.Items
 
 	// Organize items into groups
 	groupMap := make(map[uint16]*BuddyGroupInfo)

+ 17 - 34
server/webapi/handlers/presence_test.go

@@ -15,27 +15,6 @@ import (
 	"github.com/mk6i/open-oscar-server/wire"
 )
 
-// MockFeedbagRetriever is a mock implementation of FeedbagRetriever
-type MockFeedbagRetriever struct {
-	mock.Mock
-}
-
-func (m *MockFeedbagRetriever) RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error) {
-	args := m.Called(ctx, screenName)
-	if items := args.Get(0); items != nil {
-		return items.([]wire.FeedbagItem), args.Error(1)
-	}
-	return nil, args.Error(1)
-}
-
-func (m *MockFeedbagRetriever) RelationshipsByUser(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error) {
-	args := m.Called(ctx, screenName)
-	if names := args.Get(0); names != nil {
-		return names.([]state.IdentScreenName), args.Error(1)
-	}
-	return nil, args.Error(1)
-}
-
 // MockFeedbagService is a mock implementation of FeedbagService
 type MockFeedbagService struct {
 	mock.Mock
@@ -125,19 +104,23 @@ func TestPresenceHandler_GetPresence(t *testing.T) {
 	tests := []struct {
 		name               string
 		queryParams        string
-		setupMocks         func(*MockSessionRetriever, *MockFeedbagRetriever, *MockRelationshipFetcher)
+		setupMocks         func(*MockSessionRetriever, *MockFeedbagService, *MockRelationshipFetcher)
 		expectedStatusCode int
 		checkResponse      func(*testing.T, string)
 	}{
 		{
 			name:        "Success_BuddyList",
 			queryParams: "bl=1",
-			setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagRetriever, rf *MockRelationshipFetcher) {
+			setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagService, rf *MockRelationshipFetcher) {
 				// Return feedbag with a group and buddy
-				fr.On("RetrieveFeedbag", mock.Anything, state.NewIdentScreenName("testuser")).
-					Return([]wire.FeedbagItem{
-						{ItemID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends", GroupID: 0},
-						{ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "buddy1", GroupID: 1},
+				fr.On("Query", mock.Anything, mock.Anything, mock.Anything).
+					Return(wire.SNACMessage{
+						Body: wire.SNAC_0x13_0x06_FeedbagReply{
+							Items: []wire.FeedbagItem{
+								{ItemID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends", GroupID: 0},
+								{ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "buddy1", GroupID: 1},
+							},
+						},
 					}, nil)
 				rf.On("Relationship", mock.Anything, state.NewIdentScreenName("testuser"), state.NewIdentScreenName("buddy1")).
 					Return(state.Relationship{}, nil)
@@ -156,7 +139,7 @@ func TestPresenceHandler_GetPresence(t *testing.T) {
 		{
 			name:        "Success_TargetUsers",
 			queryParams: "t=user1,user2",
-			setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagRetriever, rf *MockRelationshipFetcher) {
+			setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagService, rf *MockRelationshipFetcher) {
 				rf.On("Relationship", mock.Anything, state.NewIdentScreenName("testuser"), state.NewIdentScreenName("user1")).
 					Return(state.Relationship{}, nil)
 				rf.On("Relationship", mock.Anything, state.NewIdentScreenName("testuser"), state.NewIdentScreenName("user2")).
@@ -177,7 +160,7 @@ func TestPresenceHandler_GetPresence(t *testing.T) {
 		{
 			name:        "Success_BlockedUserOffline",
 			queryParams: "t=blockeduser",
-			setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagRetriever, rf *MockRelationshipFetcher) {
+			setupMocks: func(sr *MockSessionRetriever, fr *MockFeedbagService, rf *MockRelationshipFetcher) {
 				rf.On("Relationship", mock.Anything, state.NewIdentScreenName("testuser"), state.NewIdentScreenName("blockeduser")).
 					Return(state.Relationship{YouBlock: true}, nil)
 				// RetrieveSession should NOT be called for blocked users
@@ -192,7 +175,7 @@ func TestPresenceHandler_GetPresence(t *testing.T) {
 		{
 			name:               "Success_EmptyRequest",
 			queryParams:        "",
-			setupMocks:         func(sr *MockSessionRetriever, fr *MockFeedbagRetriever, rf *MockRelationshipFetcher) {},
+			setupMocks:         func(sr *MockSessionRetriever, fr *MockFeedbagService, rf *MockRelationshipFetcher) {},
 			expectedStatusCode: http.StatusOK,
 			checkResponse: func(t *testing.T, body string) {
 				assert.Contains(t, body, `"statusCode":200`)
@@ -203,7 +186,7 @@ func TestPresenceHandler_GetPresence(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			sessionRetriever := &MockSessionRetriever{}
-			feedbagRetriever := &MockFeedbagRetriever{}
+			feedbagService := &MockFeedbagService{}
 			relFetcher := &MockRelationshipFetcher{}
 
 			sessionMgr, aimsid := createTestSessionManager("testuser")
@@ -211,12 +194,12 @@ func TestPresenceHandler_GetPresence(t *testing.T) {
 			handler := &PresenceHandler{
 				SessionManager:      sessionMgr,
 				SessionRetriever:    sessionRetriever,
-				FeedbagRetriever:    feedbagRetriever,
+				FeedbagService:      feedbagService,
 				RelationshipFetcher: relFetcher,
 				Logger:              slog.Default(),
 			}
 
-			tt.setupMocks(sessionRetriever, feedbagRetriever, relFetcher)
+			tt.setupMocks(sessionRetriever, feedbagService, relFetcher)
 
 			reqURL := "/presence/get?aimsid=" + aimsid
 			if tt.queryParams != "" {
@@ -237,7 +220,7 @@ func TestPresenceHandler_GetPresence(t *testing.T) {
 			}
 
 			sessionRetriever.AssertExpectations(t)
-			feedbagRetriever.AssertExpectations(t)
+			feedbagService.AssertExpectations(t)
 			relFetcher.AssertExpectations(t)
 		})
 	}

+ 0 - 38
server/webapi/handlers/session.go

@@ -24,12 +24,9 @@ type SessionHandler struct {
 	SessionManager      *state.WebAPISessionManager
 	OSCARSessionManager SessionManager
 	OSCARAuthService    AuthService
-	BuddyListService    BuddyListService
 	BuddyListRegistry   BuddyListRegistry
 	BuddyBroadcaster    BuddyBroadcaster
-	FeedbagRetriever    FeedbagRetriever
 	FeedbagService      FeedbagService
-	OSCARBuddyService   OSCARBuddyService
 	BuddyListManager    *BuddyListManager
 	Logger              *slog.Logger
 	OServiceService     OServiceService
@@ -62,16 +59,6 @@ type BuddyListRegistry interface {
 	UnregisterBuddyList(ctx context.Context, screenName state.IdentScreenName) error
 }
 
-// BuddyListService defines methods for buddy list operations.
-type BuddyListService interface {
-	GetBuddyList(ctx context.Context, screenName state.IdentScreenName) ([]BuddyGroup, error)
-}
-
-// OSCARBuddyService defines the OSCAR buddy-list operations we need to emulate an OSCAR client.
-type OSCARBuddyService interface {
-	AddBuddies(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x03_0x04_BuddyAddBuddies) (*wire.SNACMessage, error)
-}
-
 type ChatSessionManager interface {
 	RemoveUserFromAllChats(user state.IdentScreenName)
 }
@@ -317,31 +304,6 @@ func (h *SessionHandler) StartSession(w http.ResponseWriter, r *http.Request) {
 
 			oscarInstance.SetSignonComplete()
 
-			// Emulate an OSCAR client buddy watch list.
-			if h.FeedbagRetriever != nil && h.OSCARBuddyService != nil {
-				if items, err := h.FeedbagRetriever.RetrieveFeedbag(ctx, screenName.IdentScreenName()); err != nil {
-					h.Logger.ErrorContext(ctx, "failed to retrieve feedbag for buddy watch list", "err", err.Error())
-				} else {
-					var b wire.SNAC_0x03_0x04_BuddyAddBuddies
-					for _, item := range items {
-						if item.ClassID != wire.FeedbagClassIdBuddy {
-							continue
-						}
-						if strings.TrimSpace(item.Name) == "" {
-							continue
-						}
-						b.Buddies = append(b.Buddies, struct {
-							ScreenName string `oscar:"len_prefix=uint8"`
-						}{ScreenName: item.Name})
-					}
-					if len(b.Buddies) > 0 {
-						if _, err := h.OSCARBuddyService.AddBuddies(ctx, oscarInstance, wire.SNACFrame{}, b); err != nil {
-							h.Logger.ErrorContext(ctx, "failed to add OSCAR buddy watch list", "err", err.Error())
-						}
-					}
-				}
-			}
-
 			if err := h.OServiceService.ClientOnline(ctx, wire.BOS, wire.SNAC_0x01_0x02_OServiceClientOnline{}, oscarInstance); err != nil {
 				h.Logger.ErrorContext(ctx, "failed to set client online", "err", err.Error())
 				h.sendError(w, r, http.StatusInternalServerError, "internal server error")

+ 1 - 29
server/webapi/server.go

@@ -32,12 +32,9 @@ func NewServer(listeners []string, logger *slog.Logger, handler Handler, apiKeyV
 		SessionManager:      sessionManager,
 		OSCARSessionManager: handler.SessionRetriever.(handlers.SessionManager),
 		OSCARAuthService:    handler.AuthService,
-		BuddyListService:    nil,
 		BuddyListRegistry:   handler.BuddyListRegistry,
 		BuddyBroadcaster:    handler.BuddyBroadcaster,
-		FeedbagRetriever:    handler.FeedbagRetriever,
 		FeedbagService:      handler.FeedbagService,
-		OSCARBuddyService:   handler.BuddyService,
 		BuddyListManager:    handler.BuddyListManager.(*handlers.BuddyListManager),
 		Logger:              logger,
 		OServiceService:     handler.OServiceService,
@@ -54,7 +51,7 @@ func NewServer(listeners []string, logger *slog.Logger, handler Handler, apiKeyV
 	presenceHandler := &handlers.PresenceHandler{
 		SessionManager:      sessionManager,
 		SessionRetriever:    handler.SessionRetriever,
-		FeedbagRetriever:    handler.FeedbagRetriever,
+		FeedbagService:      handler.FeedbagService,
 		BuddyBroadcaster:    handler.BuddyBroadcaster,
 		ProfileManager:      handler.ProfileManager,
 		RelationshipFetcher: handler.RelationshipFetcher,
@@ -93,13 +90,6 @@ func NewServer(listeners []string, logger *slog.Logger, handler Handler, apiKeyV
 		Logger:           logger,
 	}
 
-	// Phase 5: Chat handler
-	chatHandler := &handlers.ChatHandler{
-		SessionManager: sessionManager,
-		ChatManager:    handler.ChatManager,
-		Logger:         logger,
-	}
-
 	for _, l := range listeners {
 		mux := http.NewServeMux()
 
@@ -292,24 +282,6 @@ func NewServer(listeners []string, logger *slog.Logger, handler Handler, apiKeyV
 			mux.Handle("GET /lifestream/"+p, lifestreamRoute(lifestreamStub.EmptyOK))
 		}
 
-		// Phase 5: Chat room endpoints
-		// All chat endpoints use aimsid for authentication
-		mux.Handle("GET /chat/createAndJoinChat", authMiddleware.AuthenticateFlexible(
-			authMiddleware.CORSMiddleware(
-				http.HandlerFunc(chatHandler.CreateAndJoinChat))))
-
-		mux.Handle("GET /chat/sendMessage", authMiddleware.AuthenticateFlexible(
-			authMiddleware.CORSMiddleware(
-				http.HandlerFunc(chatHandler.SendMessage))))
-
-		mux.Handle("GET /chat/setTyping", authMiddleware.AuthenticateFlexible(
-			authMiddleware.CORSMiddleware(
-				http.HandlerFunc(chatHandler.SetTyping))))
-
-		mux.Handle("GET /chat/leaveChat", authMiddleware.AuthenticateFlexible(
-			authMiddleware.CORSMiddleware(
-				http.HandlerFunc(chatHandler.LeaveChat))))
-
 		// Unmatched paths (pattern "/" matches anything not covered by routes above).
 		mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 			logger.Debug("webapi 404", "method", r.Method, "path", r.URL.Path)

+ 0 - 26
server/webapi/types.go

@@ -111,20 +111,6 @@ type SessionRetriever interface {
 	RetrieveSession(screenName state.IdentScreenName) *state.Session
 }
 
-// FeedbagRetriever provides methods to retrieve buddy list data.
-type FeedbagRetriever interface {
-	RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error)
-	RelationshipsByUser(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error)
-}
-
-// FeedbagManager provides methods to manage buddy lists.
-type FeedbagManager interface {
-	RetrieveFeedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error)
-	InsertItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error
-	UpdateItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error
-	DeleteItem(ctx context.Context, screenName state.IdentScreenName, item wire.FeedbagItem) error
-}
-
 type FeedbagService interface {
 	DeleteItem(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x0A_FeedbagDeleteItem) (*wire.SNACMessage, error)
 	Query(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) (wire.SNACMessage, error)
@@ -171,18 +157,6 @@ type PreferenceManager interface {
 	GetPreferences(ctx context.Context, screenName state.IdentScreenName) (map[string]interface{}, error)
 }
 
-// PermitDenyManager provides methods to manage permit/deny lists.
-type PermitDenyManager interface {
-	SetPDMode(ctx context.Context, screenName state.IdentScreenName, mode wire.FeedbagPDMode) error
-	GetPDMode(ctx context.Context, screenName state.IdentScreenName) (wire.FeedbagPDMode, error)
-	GetPermitList(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error)
-	GetDenyList(ctx context.Context, screenName state.IdentScreenName) ([]state.IdentScreenName, error)
-	AddPermitBuddy(ctx context.Context, me state.IdentScreenName, them state.IdentScreenName) error
-	RemovePermitBuddy(ctx context.Context, me state.IdentScreenName, them state.IdentScreenName) error
-	AddDenyBuddy(ctx context.Context, me state.IdentScreenName, them state.IdentScreenName) error
-	RemoveDenyBuddy(ctx context.Context, me state.IdentScreenName, them state.IdentScreenName) error
-}
-
 // Phase 4: OSCAR Bridge interfaces
 
 // OSCARBridgeStore manages the persistence of OSCAR bridge sessions.

+ 62 - 0
state/migrations/0038_drop_web_chat.down.sql

@@ -0,0 +1,62 @@
+-- Rollback: recreate Web API chat room tables
+
+CREATE TABLE IF NOT EXISTS web_chat_rooms (
+    room_id VARCHAR(255) PRIMARY KEY,
+    room_name VARCHAR(255) NOT NULL,
+    description TEXT,
+    room_type VARCHAR(50) DEFAULT 'userCreated',
+    category_id VARCHAR(50),
+    creator_screen_name VARCHAR(16) NOT NULL,
+    created_at INTEGER NOT NULL,
+    closed_at INTEGER,
+    max_participants INTEGER DEFAULT 100
+);
+
+CREATE INDEX IF NOT EXISTS idx_web_chat_rooms_name ON web_chat_rooms(room_name);
+CREATE INDEX IF NOT EXISTS idx_web_chat_rooms_creator ON web_chat_rooms(creator_screen_name);
+CREATE INDEX IF NOT EXISTS idx_web_chat_rooms_created ON web_chat_rooms(created_at);
+CREATE INDEX IF NOT EXISTS idx_web_chat_rooms_closed ON web_chat_rooms(closed_at);
+
+CREATE TABLE IF NOT EXISTS web_chat_sessions (
+    chat_sid VARCHAR(255) PRIMARY KEY,
+    aimsid VARCHAR(255) NOT NULL,
+    room_id VARCHAR(255) NOT NULL,
+    screen_name VARCHAR(16) NOT NULL,
+    instance_id INTEGER NOT NULL,
+    joined_at INTEGER NOT NULL,
+    left_at INTEGER,
+    FOREIGN KEY (room_id) REFERENCES web_chat_rooms(room_id) ON DELETE CASCADE
+);
+
+CREATE INDEX IF NOT EXISTS idx_web_chat_sessions_aimsid ON web_chat_sessions(aimsid);
+CREATE INDEX IF NOT EXISTS idx_web_chat_sessions_room ON web_chat_sessions(room_id);
+CREATE INDEX IF NOT EXISTS idx_web_chat_sessions_user ON web_chat_sessions(screen_name);
+CREATE INDEX IF NOT EXISTS idx_web_chat_sessions_joined ON web_chat_sessions(joined_at);
+
+CREATE TABLE IF NOT EXISTS web_chat_messages (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    room_id VARCHAR(255) NOT NULL,
+    screen_name VARCHAR(16) NOT NULL,
+    message TEXT NOT NULL,
+    whisper_target VARCHAR(16),
+    timestamp INTEGER NOT NULL,
+    FOREIGN KEY (room_id) REFERENCES web_chat_rooms(room_id) ON DELETE CASCADE
+);
+
+CREATE INDEX IF NOT EXISTS idx_web_chat_messages_room ON web_chat_messages(room_id);
+CREATE INDEX IF NOT EXISTS idx_web_chat_messages_timestamp ON web_chat_messages(timestamp);
+CREATE INDEX IF NOT EXISTS idx_web_chat_messages_user ON web_chat_messages(screen_name);
+
+CREATE TABLE IF NOT EXISTS web_chat_participants (
+    room_id VARCHAR(255) NOT NULL,
+    screen_name VARCHAR(16) NOT NULL,
+    chat_sid VARCHAR(255) NOT NULL,
+    joined_at INTEGER NOT NULL,
+    typing_status VARCHAR(20) DEFAULT 'none',
+    typing_updated_at INTEGER,
+    PRIMARY KEY (room_id, screen_name),
+    FOREIGN KEY (room_id) REFERENCES web_chat_rooms(room_id) ON DELETE CASCADE
+);
+
+CREATE INDEX IF NOT EXISTS idx_web_chat_participants_room ON web_chat_participants(room_id);
+CREATE INDEX IF NOT EXISTS idx_web_chat_participants_user ON web_chat_participants(screen_name);

+ 4 - 0
state/migrations/0038_drop_web_chat.up.sql

@@ -0,0 +1,4 @@
+DROP TABLE IF EXISTS web_chat_messages;
+DROP TABLE IF EXISTS web_chat_participants;
+DROP TABLE IF EXISTS web_chat_sessions;
+DROP TABLE IF EXISTS web_chat_rooms;

+ 0 - 721
state/webapi_chat.go

@@ -1,721 +0,0 @@
-package state
-
-import (
-	"context"
-	"crypto/rand"
-	"database/sql"
-	"encoding/hex"
-	"errors"
-	"fmt"
-	"log/slog"
-	"sync"
-	"time"
-)
-
-// ChatRoomType represents the type of chat room
-type ChatRoomType string
-
-const (
-	ChatRoomTypeUserCreated ChatRoomType = "userCreated"
-)
-
-// ChatEventType represents the type of chat event
-type ChatEventType string
-
-const (
-	ChatEventUserInRoom  ChatEventType = "userInRoom"
-	ChatEventUserEntered ChatEventType = "userEntered"
-	ChatEventUserLeft    ChatEventType = "userLeft"
-	ChatEventMessage     ChatEventType = "message"
-	ChatEventTyping      ChatEventType = "typing"
-	ChatEventClosed      ChatEventType = "closed"
-)
-
-// WebAPIChatRoom represents a chat room for Web API
-type WebAPIChatRoom struct {
-	RoomID            string       `json:"roomId"`
-	RoomName          string       `json:"roomName"`
-	Description       string       `json:"description,omitempty"`
-	RoomType          ChatRoomType `json:"roomType"`
-	CategoryID        string       `json:"categoryId,omitempty"`
-	CreatorScreenName string       `json:"-"` // Internal only
-	CreatedAt         int64        `json:"-"`
-	ClosedAt          *int64       `json:"-"`
-	MaxParticipants   int          `json:"-"`
-	InstanceID        int          `json:"instanceId"`
-}
-
-// ChatSession represents a user's session in a chat room
-type ChatSession struct {
-	ChatSID    string
-	AIMSid     string
-	RoomID     string
-	ScreenName string
-	InstanceID int
-	JoinedAt   int64
-	LeftAt     *int64
-}
-
-// ChatMessage represents a message sent in a chat room
-type ChatMessage struct {
-	ID            int64
-	RoomID        string
-	ScreenName    string
-	Message       string
-	WhisperTarget string
-	Timestamp     int64
-}
-
-// ChatParticipant represents a participant in a chat room
-type ChatParticipant struct {
-	RoomID          string
-	ScreenName      string
-	ChatSID         string
-	JoinedAt        int64
-	TypingStatus    string
-	TypingUpdatedAt *int64
-}
-
-// ChatEventData represents data for a chat event
-type ChatEventData struct {
-	ChatSID   string        `json:"chatsid"`
-	EventType ChatEventType `json:"eventType"`
-	EventData interface{}   `json:"eventData"`
-}
-
-// ChatMessageEventData represents chat message event data
-type ChatMessageEventData struct {
-	ScreenName    string `json:"screenName"`
-	Message       string `json:"message"`
-	Timestamp     int64  `json:"timestamp"`
-	WhisperTarget string `json:"whisperTarget,omitempty"`
-}
-
-// ChatUserEventData represents user join/leave event data
-type ChatUserEventData struct {
-	ScreenName string `json:"screenName"`
-	Timestamp  int64  `json:"timestamp"`
-}
-
-// ChatTypingEventData represents typing status event data
-type ChatTypingEventData struct {
-	ScreenName   string `json:"screenName"`
-	TypingStatus string `json:"typingStatus"`
-}
-
-// ChatParticipantList represents a list of participants in the room
-type ChatParticipantList struct {
-	Participants []string `json:"participants"`
-}
-
-// WebAPIChatManager manages Web API chat rooms
-type WebAPIChatManager struct {
-	store    *SQLiteUserStore
-	logger   *slog.Logger
-	sessions *WebAPISessionManager
-	mu       sync.RWMutex
-	// In-memory cache for active rooms
-	activeRooms map[string]*WebAPIChatRoom
-	// Track typing timeouts
-	typingTimers map[string]*time.Timer
-}
-
-// NewWebAPIChatManager creates a new WebAPIChatManager
-func (s *SQLiteUserStore) NewWebAPIChatManager(logger *slog.Logger, sessions *WebAPISessionManager) *WebAPIChatManager {
-	return &WebAPIChatManager{
-		store:        s,
-		logger:       logger,
-		sessions:     sessions,
-		activeRooms:  make(map[string]*WebAPIChatRoom),
-		typingTimers: make(map[string]*time.Timer),
-	}
-}
-
-// CreateAndJoinChat creates a new chat room or joins an existing one
-func (m *WebAPIChatManager) CreateAndJoinChat(ctx context.Context, aimsid, roomID, roomName, screenName string) (*ChatSession, *WebAPIChatRoom, error) {
-	m.mu.Lock()
-	defer m.mu.Unlock()
-
-	var room *WebAPIChatRoom
-	var err error
-
-	// Determine which identifier to use
-	if roomID != "" {
-		room, err = m.getRoomByID(ctx, roomID)
-		if err != nil {
-			return nil, nil, fmt.Errorf("failed to get room by ID: %w", err)
-		}
-	} else if roomName != "" {
-		room, err = m.getRoomByName(ctx, roomName)
-		if err != nil && !errors.Is(err, sql.ErrNoRows) {
-			return nil, nil, fmt.Errorf("failed to get room by name: %w", err)
-		}
-		// If room doesn't exist, create it
-		if room == nil {
-			room, err = m.createRoom(ctx, roomName, screenName)
-			if err != nil {
-				return nil, nil, fmt.Errorf("failed to create room: %w", err)
-			}
-		}
-	} else {
-		return nil, nil, errors.New("either roomId or roomName must be provided")
-	}
-
-	// Check if user is already in the room
-	existingSession, _ := m.getUserSessionInRoom(ctx, aimsid, room.RoomID)
-	if existingSession != nil {
-		return existingSession, room, nil
-	}
-
-	// Check room capacity
-	count, err := m.getParticipantCount(ctx, room.RoomID)
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to get participant count: %w", err)
-	}
-	if count >= room.MaxParticipants {
-		return nil, nil, errors.New("room is at maximum capacity")
-	}
-
-	// Create chat session
-	session := &ChatSession{
-		ChatSID:    m.generateChatSID(),
-		AIMSid:     aimsid,
-		RoomID:     room.RoomID,
-		ScreenName: screenName,
-		InstanceID: room.InstanceID,
-		JoinedAt:   time.Now().Unix(),
-	}
-
-	// Insert session into database
-	_, err = m.store.db.ExecContext(ctx, `
-		INSERT INTO web_chat_sessions (chat_sid, aimsid, room_id, screen_name, instance_id, joined_at)
-		VALUES (?, ?, ?, ?, ?, ?)`,
-		session.ChatSID, session.AIMSid, session.RoomID, session.ScreenName, session.InstanceID, session.JoinedAt)
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to create chat session: %w", err)
-	}
-
-	// Add participant to room
-	_, err = m.store.db.ExecContext(ctx, `
-		INSERT INTO web_chat_participants (room_id, screen_name, chat_sid, joined_at, typing_status)
-		VALUES (?, ?, ?, ?, 'none')`,
-		room.RoomID, screenName, session.ChatSID, session.JoinedAt)
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to add participant: %w", err)
-	}
-
-	// Broadcast user joined event
-	// Note: Broadcasting doesn't need context as it's fire-and-forget
-	m.broadcastChatEvent(room.RoomID, ChatEventData{
-		ChatSID:   session.ChatSID,
-		EventType: ChatEventUserEntered,
-		EventData: ChatUserEventData{
-			ScreenName: screenName,
-			Timestamp:  session.JoinedAt,
-		},
-	})
-
-	// Send current participant list to the new user
-	participants, _ := m.getParticipants(ctx, room.RoomID)
-	m.sendChatEventToUser(aimsid, ChatEventData{
-		ChatSID:   session.ChatSID,
-		EventType: ChatEventUserInRoom,
-		EventData: ChatParticipantList{
-			Participants: participants,
-		},
-	})
-
-	return session, room, nil
-}
-
-// SendMessage sends a message to a chat room
-func (m *WebAPIChatManager) SendMessage(ctx context.Context, chatsid, message, whisperTarget string) error {
-	m.mu.Lock()
-	defer m.mu.Unlock()
-
-	// Get session
-	session, err := m.getSessionByChatSID(ctx, chatsid)
-	if err != nil {
-		return fmt.Errorf("invalid chat session: %w", err)
-	}
-
-	// Verify user is still in room
-	if session.LeftAt != nil {
-		return errors.New("user has left the chat room")
-	}
-
-	// Store message in database
-	timestamp := time.Now().Unix()
-	_, err = m.store.db.ExecContext(ctx, `
-		INSERT INTO web_chat_messages (room_id, screen_name, message, whisper_target, timestamp)
-		VALUES (?, ?, ?, ?, ?)`,
-		session.RoomID, session.ScreenName, message, whisperTarget, timestamp)
-	if err != nil {
-		return fmt.Errorf("failed to store message: %w", err)
-	}
-
-	// Broadcast message event
-	eventData := ChatMessageEventData{
-		ScreenName:    session.ScreenName,
-		Message:       message,
-		Timestamp:     timestamp,
-		WhisperTarget: whisperTarget,
-	}
-
-	if whisperTarget != "" {
-		// For whispers, only send to sender and target
-		m.sendChatEventToUser(session.AIMSid, ChatEventData{
-			ChatSID:   chatsid,
-			EventType: ChatEventMessage,
-			EventData: eventData,
-		})
-		// Find target's session and send to them
-		targetSession, _ := m.getUserSessionInRoomByScreenName(ctx, session.RoomID, whisperTarget)
-		if targetSession != nil {
-			m.sendChatEventToUser(targetSession.AIMSid, ChatEventData{
-				ChatSID:   targetSession.ChatSID,
-				EventType: ChatEventMessage,
-				EventData: eventData,
-			})
-		}
-	} else {
-		// Broadcast to all participants
-		m.broadcastChatEvent(session.RoomID, ChatEventData{
-			ChatSID:   chatsid,
-			EventType: ChatEventMessage,
-			EventData: eventData,
-		})
-	}
-
-	return nil
-}
-
-// SetTyping sets the typing status for a user in a chat room
-func (m *WebAPIChatManager) SetTyping(ctx context.Context, chatsid, typingStatus string) error {
-	m.mu.Lock()
-	defer m.mu.Unlock()
-
-	// Get session
-	session, err := m.getSessionByChatSID(ctx, chatsid)
-	if err != nil {
-		return fmt.Errorf("invalid chat session: %w", err)
-	}
-
-	// Verify user is still in room
-	if session.LeftAt != nil {
-		return errors.New("user has left the chat room")
-	}
-
-	// Update typing status
-	now := time.Now().Unix()
-	_, err = m.store.db.ExecContext(ctx, `
-		UPDATE web_chat_participants 
-		SET typing_status = ?, typing_updated_at = ?
-		WHERE room_id = ? AND screen_name = ?`,
-		typingStatus, now, session.RoomID, session.ScreenName)
-	if err != nil {
-		return fmt.Errorf("failed to update typing status: %w", err)
-	}
-
-	// Cancel existing typing timer for this user
-	timerKey := fmt.Sprintf("%s:%s", session.RoomID, session.ScreenName)
-	if timer, exists := m.typingTimers[timerKey]; exists {
-		timer.Stop()
-		delete(m.typingTimers, timerKey)
-	}
-
-	// If status is "typing" or "typed", set a timer to reset it
-	if typingStatus == "typing" || typingStatus == "typed" {
-		timer := time.AfterFunc(10*time.Second, func() {
-			m.mu.Lock()
-			defer m.mu.Unlock()
-			// Reset typing status to none
-			// Using background context here since this is an async timer callback
-			// and the original context may have expired
-			if _, err := m.store.db.ExecContext(context.Background(), `
-				UPDATE web_chat_participants 
-				SET typing_status = 'none', typing_updated_at = ?
-				WHERE room_id = ? AND screen_name = ?`,
-				time.Now().Unix(), session.RoomID, session.ScreenName); err != nil {
-				m.logger.Error("failed to reset typing status", "error", err)
-			}
-			// Broadcast the reset
-			m.broadcastChatEvent(session.RoomID, ChatEventData{
-				ChatSID:   chatsid,
-				EventType: ChatEventTyping,
-				EventData: ChatTypingEventData{
-					ScreenName:   session.ScreenName,
-					TypingStatus: "none",
-				},
-			})
-			delete(m.typingTimers, timerKey)
-		})
-		m.typingTimers[timerKey] = timer
-	}
-
-	// Broadcast typing event
-	m.broadcastChatEvent(session.RoomID, ChatEventData{
-		ChatSID:   chatsid,
-		EventType: ChatEventTyping,
-		EventData: ChatTypingEventData{
-			ScreenName:   session.ScreenName,
-			TypingStatus: typingStatus,
-		},
-	})
-
-	return nil
-}
-
-// LeaveChat removes a user from a chat room
-func (m *WebAPIChatManager) LeaveChat(ctx context.Context, chatsid string) error {
-	m.mu.Lock()
-	defer m.mu.Unlock()
-
-	// Get session
-	session, err := m.getSessionByChatSID(ctx, chatsid)
-	if err != nil {
-		return fmt.Errorf("invalid chat session: %w", err)
-	}
-
-	// Mark session as left
-	now := time.Now().Unix()
-	_, err = m.store.db.ExecContext(ctx, `
-		UPDATE web_chat_sessions 
-		SET left_at = ?
-		WHERE chat_sid = ?`,
-		now, chatsid)
-	if err != nil {
-		return fmt.Errorf("failed to update session: %w", err)
-	}
-
-	// Remove from participants
-	_, err = m.store.db.ExecContext(ctx, `
-		DELETE FROM web_chat_participants
-		WHERE room_id = ? AND screen_name = ?`,
-		session.RoomID, session.ScreenName)
-	if err != nil {
-		return fmt.Errorf("failed to remove participant: %w", err)
-	}
-
-	// Cancel any typing timer
-	timerKey := fmt.Sprintf("%s:%s", session.RoomID, session.ScreenName)
-	if timer, exists := m.typingTimers[timerKey]; exists {
-		timer.Stop()
-		delete(m.typingTimers, timerKey)
-	}
-
-	// Broadcast user left event
-	// Note: Broadcasting doesn't need context as it's fire-and-forget
-	m.broadcastChatEvent(session.RoomID, ChatEventData{
-		ChatSID:   chatsid,
-		EventType: ChatEventUserLeft,
-		EventData: ChatUserEventData{
-			ScreenName: session.ScreenName,
-			Timestamp:  now,
-		},
-	})
-
-	// Check if room should be closed (no participants left)
-	count, _ := m.getParticipantCount(ctx, session.RoomID)
-	if count == 0 {
-		m.closeRoom(ctx, session.RoomID)
-	}
-
-	return nil
-}
-
-// Helper methods
-
-func (m *WebAPIChatManager) getRoomByID(ctx context.Context, roomID string) (*WebAPIChatRoom, error) {
-	var room WebAPIChatRoom
-	err := m.store.db.QueryRowContext(ctx, `
-		SELECT room_id, room_name, description, room_type, category_id, 
-		       creator_screen_name, created_at, closed_at, max_participants
-		FROM web_chat_rooms
-		WHERE room_id = ? AND closed_at IS NULL`,
-		roomID).Scan(
-		&room.RoomID, &room.RoomName, &room.Description, &room.RoomType,
-		&room.CategoryID, &room.CreatorScreenName, &room.CreatedAt,
-		&room.ClosedAt, &room.MaxParticipants)
-	if err != nil {
-		return nil, err
-	}
-	room.InstanceID = m.generateInstanceID()
-	return &room, nil
-}
-
-func (m *WebAPIChatManager) getRoomByName(ctx context.Context, roomName string) (*WebAPIChatRoom, error) {
-	var room WebAPIChatRoom
-	err := m.store.db.QueryRowContext(ctx, `
-		SELECT room_id, room_name, description, room_type, category_id, 
-		       creator_screen_name, created_at, closed_at, max_participants
-		FROM web_chat_rooms
-		WHERE room_name = ? AND closed_at IS NULL`,
-		roomName).Scan(
-		&room.RoomID, &room.RoomName, &room.Description, &room.RoomType,
-		&room.CategoryID, &room.CreatorScreenName, &room.CreatedAt,
-		&room.ClosedAt, &room.MaxParticipants)
-	if err != nil {
-		return nil, err
-	}
-	room.InstanceID = m.generateInstanceID()
-	return &room, nil
-}
-
-func (m *WebAPIChatManager) createRoom(ctx context.Context, roomName, creatorScreenName string) (*WebAPIChatRoom, error) {
-	room := &WebAPIChatRoom{
-		RoomID:            m.generateRoomID(),
-		RoomName:          roomName,
-		Description:       fmt.Sprintf("Chat room created by %s", creatorScreenName),
-		RoomType:          ChatRoomTypeUserCreated,
-		CreatorScreenName: creatorScreenName,
-		CreatedAt:         time.Now().Unix(),
-		MaxParticipants:   100,
-		InstanceID:        m.generateInstanceID(),
-	}
-
-	_, err := m.store.db.ExecContext(ctx, `
-		INSERT INTO web_chat_rooms (room_id, room_name, description, room_type, 
-		                            category_id, creator_screen_name, created_at, max_participants)
-		VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
-		room.RoomID, room.RoomName, room.Description, room.RoomType,
-		room.CategoryID, room.CreatorScreenName, room.CreatedAt, room.MaxParticipants)
-	if err != nil {
-		return nil, err
-	}
-
-	// Cache the room
-	m.activeRooms[room.RoomID] = room
-
-	return room, nil
-}
-
-func (m *WebAPIChatManager) getSessionByChatSID(ctx context.Context, chatsid string) (*ChatSession, error) {
-	var session ChatSession
-	err := m.store.db.QueryRowContext(ctx, `
-		SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
-		FROM web_chat_sessions
-		WHERE chat_sid = ?`,
-		chatsid).Scan(
-		&session.ChatSID, &session.AIMSid, &session.RoomID,
-		&session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
-	if err != nil {
-		return nil, err
-	}
-	return &session, nil
-}
-
-func (m *WebAPIChatManager) getUserSessionInRoom(ctx context.Context, aimsid, roomID string) (*ChatSession, error) {
-	var session ChatSession
-	err := m.store.db.QueryRowContext(ctx, `
-		SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
-		FROM web_chat_sessions
-		WHERE aimsid = ? AND room_id = ? AND left_at IS NULL`,
-		aimsid, roomID).Scan(
-		&session.ChatSID, &session.AIMSid, &session.RoomID,
-		&session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
-	if err != nil {
-		return nil, err
-	}
-	return &session, nil
-}
-
-func (m *WebAPIChatManager) getUserSessionInRoomByScreenName(ctx context.Context, roomID, screenName string) (*ChatSession, error) {
-	var session ChatSession
-	err := m.store.db.QueryRowContext(ctx, `
-		SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
-		FROM web_chat_sessions
-		WHERE room_id = ? AND screen_name = ? AND left_at IS NULL`,
-		roomID, screenName).Scan(
-		&session.ChatSID, &session.AIMSid, &session.RoomID,
-		&session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
-	if err != nil {
-		return nil, err
-	}
-	return &session, nil
-}
-
-func (m *WebAPIChatManager) getParticipantCount(ctx context.Context, roomID string) (int, error) {
-	var count int
-	err := m.store.db.QueryRowContext(ctx, `
-		SELECT COUNT(*) FROM web_chat_participants WHERE room_id = ?`,
-		roomID).Scan(&count)
-	return count, err
-}
-
-func (m *WebAPIChatManager) getParticipants(ctx context.Context, roomID string) ([]string, error) {
-	rows, err := m.store.db.QueryContext(ctx, `
-		SELECT screen_name FROM web_chat_participants WHERE room_id = ?`,
-		roomID)
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
-	var participants []string
-	for rows.Next() {
-		var screenName string
-		if err := rows.Scan(&screenName); err != nil {
-			continue
-		}
-		participants = append(participants, screenName)
-	}
-	return participants, nil
-}
-
-func (m *WebAPIChatManager) closeRoom(ctx context.Context, roomID string) {
-	now := time.Now().Unix()
-	if _, err := m.store.db.ExecContext(ctx, `
-		UPDATE web_chat_rooms SET closed_at = ? WHERE room_id = ?`,
-		now, roomID); err != nil {
-		m.logger.Error("failed to close chat room", "roomID", roomID, "error", err)
-	}
-
-	// Remove from cache
-	delete(m.activeRooms, roomID)
-
-	// Broadcast room closed event
-	// Note: Broadcasting doesn't need context as it's fire-and-forget
-	m.broadcastChatEvent(roomID, ChatEventData{
-		EventType: ChatEventClosed,
-	})
-}
-
-func (m *WebAPIChatManager) broadcastChatEvent(roomID string, event ChatEventData) {
-	// Get all active sessions in the room
-	rows, err := m.store.db.Query(`
-		SELECT aimsid, chat_sid FROM web_chat_sessions 
-		WHERE room_id = ? AND left_at IS NULL`,
-		roomID)
-	if err != nil {
-		m.logger.Error("failed to get sessions for broadcast", "error", err, "roomID", roomID)
-		return
-	}
-	defer rows.Close()
-
-	for rows.Next() {
-		var aimsid, chatsid string
-		if err := rows.Scan(&aimsid, &chatsid); err != nil {
-			continue
-		}
-		// Update event with the recipient's chat session ID if not set
-		if event.ChatSID == "" {
-			event.ChatSID = chatsid
-		}
-		m.sendChatEventToUser(aimsid, event)
-	}
-}
-
-func (m *WebAPIChatManager) sendChatEventToUser(aimsid string, event ChatEventData) {
-	// Get the user's Web API session
-	// Using background context for async event sending
-	session, err := m.sessions.GetSession(context.Background(), aimsid)
-	if err != nil {
-		m.logger.Error("failed to get session for chat event", "error", err, "aimsid", aimsid)
-		return
-	}
-
-	// Queue the chat event
-	session.EventQueue.Push("chat", event)
-}
-
-func (m *WebAPIChatManager) generateRoomID() string {
-	b := make([]byte, 16)
-	rand.Read(b)
-	return hex.EncodeToString(b)
-}
-
-func (m *WebAPIChatManager) generateChatSID() string {
-	b := make([]byte, 16)
-	rand.Read(b)
-	return hex.EncodeToString(b)
-}
-
-func (m *WebAPIChatManager) generateInstanceID() int {
-	// In production, this might be based on server instance or other factors
-	// For now, use a simple random number
-	return int(time.Now().Unix() % 1000000)
-}
-
-// GetRecentMessages returns recent messages from a chat room (for history)
-func (m *WebAPIChatManager) GetRecentMessages(ctx context.Context, roomID string, limit int) ([]*ChatMessage, error) {
-	rows, err := m.store.db.QueryContext(ctx, `
-		SELECT id, room_id, screen_name, message, whisper_target, timestamp
-		FROM web_chat_messages
-		WHERE room_id = ?
-		ORDER BY timestamp DESC
-		LIMIT ?`,
-		roomID, limit)
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
-	var messages []*ChatMessage
-	for rows.Next() {
-		var msg ChatMessage
-		err := rows.Scan(&msg.ID, &msg.RoomID, &msg.ScreenName,
-			&msg.Message, &msg.WhisperTarget, &msg.Timestamp)
-		if err != nil {
-			continue
-		}
-		messages = append(messages, &msg)
-	}
-
-	// Reverse to get chronological order
-	for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
-		messages[i], messages[j] = messages[j], messages[i]
-	}
-
-	return messages, nil
-}
-
-// CleanupInactiveSessions removes sessions that have been inactive for too long
-func (m *WebAPIChatManager) CleanupInactiveSessions(ctx context.Context) {
-	m.mu.Lock()
-	defer m.mu.Unlock()
-
-	// Mark sessions as left if they've been inactive for more than 30 minutes
-	cutoff := time.Now().Add(-30 * time.Minute).Unix()
-
-	rows, err := m.store.db.QueryContext(ctx, `
-		SELECT chat_sid, room_id, screen_name 
-		FROM web_chat_sessions 
-		WHERE left_at IS NULL AND joined_at < ?`,
-		cutoff)
-	if err != nil {
-		m.logger.Error("failed to get inactive sessions", "error", err)
-		return
-	}
-	defer rows.Close()
-
-	for rows.Next() {
-		var chatsid, roomID, screenName string
-		if err := rows.Scan(&chatsid, &roomID, &screenName); err != nil {
-			continue
-		}
-
-		// Mark as left
-		now := time.Now().Unix()
-		if _, err := m.store.db.ExecContext(ctx, `UPDATE web_chat_sessions SET left_at = ? WHERE chat_sid = ?`, now, chatsid); err != nil {
-			m.logger.Error("failed to mark inactive chat session left", "chatsid", chatsid, "error", err)
-			continue
-		}
-		if _, err := m.store.db.ExecContext(ctx, `DELETE FROM web_chat_participants WHERE room_id = ? AND screen_name = ?`,
-			roomID, screenName); err != nil {
-			m.logger.Error("failed to remove inactive chat participant", "roomID", roomID, "screenName", screenName, "error", err)
-			continue
-		}
-
-		// Broadcast user left
-		// Note: Broadcasting doesn't need context as it's fire-and-forget
-		m.broadcastChatEvent(roomID, ChatEventData{
-			ChatSID:   chatsid,
-			EventType: ChatEventUserLeft,
-			EventData: ChatUserEventData{
-				ScreenName: screenName,
-				Timestamp:  now,
-			},
-		})
-	}
-}

+ 0 - 135
state/webapi_preferences.go

@@ -6,8 +6,6 @@ import (
 	"encoding/json"
 	"errors"
 	"time"
-
-	"github.com/mk6i/open-oscar-server/wire"
 )
 
 // WebPreferenceManager handles Web API user preferences.
@@ -62,136 +60,3 @@ func (m *WebPreferenceManager) GetPreferences(ctx context.Context, screenName Id
 
 	return prefs, nil
 }
-
-// WebPermitDenyManager handles Web API permit/deny list management.
-type WebPermitDenyManager struct {
-	store *SQLiteUserStore
-}
-
-// NewWebPermitDenyManager creates a new WebPermitDenyManager.
-func (s *SQLiteUserStore) NewWebPermitDenyManager() *WebPermitDenyManager {
-	return &WebPermitDenyManager{store: s}
-}
-
-// SetPDMode sets the permit/deny mode for a user.
-func (m *WebPermitDenyManager) SetPDMode(ctx context.Context, screenName IdentScreenName, mode wire.FeedbagPDMode) error {
-	q := `
-		INSERT INTO buddyListMode (screenName, clientSidePDMode)
-		VALUES (?, ?)
-		ON CONFLICT (screenName)
-		DO UPDATE SET clientSidePDMode = excluded.clientSidePDMode
-	`
-	_, err := m.store.db.ExecContext(ctx, q, screenName.String(), int(mode))
-	return err
-}
-
-// GetPDMode retrieves the permit/deny mode for a user.
-func (m *WebPermitDenyManager) GetPDMode(ctx context.Context, screenName IdentScreenName) (wire.FeedbagPDMode, error) {
-	q := `
-		SELECT clientSidePDMode
-		FROM buddyListMode
-		WHERE screenName = ?
-	`
-	var mode int
-	err := m.store.db.QueryRowContext(ctx, q, screenName.String()).Scan(&mode)
-	if err != nil {
-		if errors.Is(err, sql.ErrNoRows) {
-			// Default to PermitAll if not set
-			return wire.FeedbagPDModePermitAll, nil
-		}
-		return 0, err
-	}
-	return wire.FeedbagPDMode(mode), nil
-}
-
-// GetPermitList retrieves the permit list for a user.
-func (m *WebPermitDenyManager) GetPermitList(ctx context.Context, screenName IdentScreenName) ([]IdentScreenName, error) {
-	q := `
-		SELECT them
-		FROM clientSideBuddyList
-		WHERE me = ? AND isPermit = 1
-	`
-	rows, err := m.store.db.QueryContext(ctx, q, screenName.String())
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
-	var users []IdentScreenName
-	for rows.Next() {
-		var user string
-		if err := rows.Scan(&user); err != nil {
-			return nil, err
-		}
-		users = append(users, NewIdentScreenName(user))
-	}
-	return users, rows.Err()
-}
-
-// GetDenyList retrieves the deny list for a user.
-func (m *WebPermitDenyManager) GetDenyList(ctx context.Context, screenName IdentScreenName) ([]IdentScreenName, error) {
-	q := `
-		SELECT them
-		FROM clientSideBuddyList
-		WHERE me = ? AND isDeny = 1
-	`
-	rows, err := m.store.db.QueryContext(ctx, q, screenName.String())
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
-	var users []IdentScreenName
-	for rows.Next() {
-		var user string
-		if err := rows.Scan(&user); err != nil {
-			return nil, err
-		}
-		users = append(users, NewIdentScreenName(user))
-	}
-	return users, rows.Err()
-}
-
-// AddPermitBuddy adds a user to the permit list.
-func (m *WebPermitDenyManager) AddPermitBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
-	q := `
-		INSERT INTO clientSideBuddyList (me, them, isPermit)
-		VALUES (?, ?, 1)
-		ON CONFLICT (me, them) DO UPDATE SET isPermit = 1
-	`
-	_, err := m.store.db.ExecContext(ctx, q, me.String(), them.String())
-	return err
-}
-
-// RemovePermitBuddy removes a user from the permit list.
-func (m *WebPermitDenyManager) RemovePermitBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
-	q := `
-		UPDATE clientSideBuddyList
-		SET isPermit = 0
-		WHERE me = ? AND them = ?
-	`
-	_, err := m.store.db.ExecContext(ctx, q, me.String(), them.String())
-	return err
-}
-
-// AddDenyBuddy adds a user to the deny list.
-func (m *WebPermitDenyManager) AddDenyBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
-	q := `
-		INSERT INTO clientSideBuddyList (me, them, isDeny)
-		VALUES (?, ?, 1)
-		ON CONFLICT (me, them) DO UPDATE SET isDeny = 1
-	`
-	_, err := m.store.db.ExecContext(ctx, q, me.String(), them.String())
-	return err
-}
-
-// RemoveDenyBuddy removes a user from the deny list.
-func (m *WebPermitDenyManager) RemoveDenyBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
-	q := `
-		UPDATE clientSideBuddyList
-		SET isDeny = 0
-		WHERE me = ? AND them = ?
-	`
-	_, err := m.store.db.ExecContext(ctx, q, me.String(), them.String())
-	return err
-}