From d369eb4420bb92debb97599279f195151ba4b645 Mon Sep 17 00:00:00 2001 From: Josh Knight Date: Wed, 28 Aug 2024 20:57:28 -0400 Subject: [PATCH] Mgmt api improvements Allow for fetching additional account, session, and buddy icon data using the mgmt api Centralize fetching of buddy icon BART items --- api.yml | 116 ++++++++++++++++++ cmd/server/main.go | 2 +- foodgroup/buddy.go | 61 +--------- foodgroup/types.go | 1 + server/http/mgmt_api.go | 264 ++++++++++++++++++++++++++++------------ server/http/types.go | 43 ++++++- state/session.go | 21 ++++ state/user_store.go | 42 +++++++ 8 files changed, 407 insertions(+), 143 deletions(-) diff --git a/api.yml b/api.yml index 39f889c3..e0e0a9fc 100644 --- a/api.yml +++ b/api.yml @@ -72,6 +72,66 @@ paths: '404': description: User not found. + /user/{screenname}/account: + get: + summary: Get account details for a specific screen name. + description: Retrieve account details for a specific screen name. + parameters: + - name: screenname + in: path + description: User's AIM screen name or ICQ UIN. + required: true + type: string + responses: + '200': + description: Successful response containing account details + content: + application/json: + schema: + type: object + properties: + id: + type: string + description: User's unique identifier. + screen_name: + type: string + description: User's AIM screen name or ICQ UIN. + profile: + type: string + description: User's AIM profile HTML. + email_address: + type: string + description: User's email address + confirmed: + type: bool + description: User's account confirmation status + is_icq: + type: boolean + description: If true, indicates an ICQ user instead of an AIM user. + '404': + description: User not found. + + /user/{screenname}/icon: + get: + summary: Get AIM buddy icon for a screen name + description: Retrieve account buddy icon for a specific screen name. + parameters: + - name: screenname + in: path + description: User's AIM screen name or ICQ UIN. + required: true + type: string + responses: + '200': + description: Successful response containing buddy icon bytes + content: + image/gif: + schema: + type: string + format: binary + '404': + description: User not found, or user has no buddy icon + /session: get: summary: Get active sessions @@ -98,10 +158,66 @@ paths: screen_name: type: string description: User's AIM screen name or ICQ UIN. + online_seconds: + type: float + description: Number of seconds this user session has been online. + away_message: + type: string + description: User's AIM away message HTML. Empty if the user is not away. + idle_seconds: + type: float + description: Number of seconds this user session has been idle. 0 if not idle. is_icq: type: boolean description: If true, indicates an ICQ user instead of an AIM user. + /session/{screenname}: + get: + summary: Get active sessions for a given screen name or UIN. + description: Retrieve a list of active sessions of a specific logged in user. + parameters: + - name: screenname + in: path + description: User's AIM screen name or ICQ UIN. + required: true + type: string + responses: + '200': + description: Successful response containing a list of active sessions for the given screen name + content: + application/json: + schema: + type: object + properties: + count: + type: integer + description: The number of active sessions. + sessions: + type: array + items: + type: object + properties: + id: + type: string + description: User's unique identifier. + screen_name: + type: string + description: User's AIM screen name or ICQ UIN. + online_seconds: + type: float + description: Number of seconds this user session has been online. + away_message: + type: string + description: User's AIM away message HTML. Empty if the user is not away. + idle_seconds: + type: float + description: Number of seconds this user session has been idle. 0 if not idle. + is_icq: + type: boolean + description: If true, indicates an ICQ user instead of an AIM user. + '404': + description: User not found. + /user/password: put: summary: Set a user's password diff --git a/cmd/server/main.go b/cmd/server/main.go index 93b530bf..9edc065d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -47,7 +47,7 @@ func main() { wg.Add(7) go func() { - http.StartManagementAPI(cfg, feedbagStore, sessionManager, feedbagStore, feedbagStore, chatSessionManager, sessionManager, logger) + http.StartManagementAPI(cfg, feedbagStore, sessionManager, feedbagStore, feedbagStore, chatSessionManager, sessionManager, feedbagStore, feedbagStore, feedbagStore, feedbagStore, logger) wg.Done() }() go func(logger *slog.Logger) { diff --git a/foodgroup/buddy.go b/foodgroup/buddy.go index 4770d8a7..f0d131fc 100644 --- a/foodgroup/buddy.go +++ b/foodgroup/buddy.go @@ -1,10 +1,7 @@ package foodgroup import ( - "bytes" "context" - "errors" - "strconv" "github.com/mk6i/retro-aim-server/state" "github.com/mk6i/retro-aim-server/wire" @@ -87,7 +84,7 @@ func (s BuddyService) DelBuddies(_ context.Context, sess *state.Session, inBody // buddy icons, warning levels, invisibility status, etc. func (s BuddyService) UnicastBuddyArrived(ctx context.Context, from *state.Session, to *state.Session) error { userInfo := from.TLVUserInfo() - icon, err := getBuddyIconRefFromFeedbag(from, s.feedbagManager) + icon, err := s.feedbagManager.BuddyIconRefByName(from.IdentScreenName()) switch { case err != nil: return err @@ -122,7 +119,7 @@ func (s BuddyService) BroadcastBuddyArrived(ctx context.Context, sess *state.Ses recipients = append(recipients, legacyUsers...) userInfo := sess.TLVUserInfo() - icon, err := getBuddyIconRefFromFeedbag(sess, s.feedbagManager) + icon, err := s.feedbagManager.BuddyIconRefByName(sess.IdentScreenName()) switch { case err != nil: return err @@ -143,60 +140,6 @@ func (s BuddyService) BroadcastBuddyArrived(ctx context.Context, sess *state.Ses return nil } -// getBuddyIconRefFromFeedbag retrieves a reference to the user's buddy icon -// from their feedbag. If it exists, the buddy icon is the feedbag item of -// class wire.FeedbagClassIdBart with BART type wire.BARTTypesBuddyIcon. -func getBuddyIconRefFromFeedbag(sess *state.Session, feedbagManager FeedbagManager) (*wire.BARTID, error) { - items, err := feedbagManager.Feedbag(sess.IdentScreenName()) - if err != nil { - return nil, err - } - - for _, item := range items { - if item.ClassID != wire.FeedbagClassIdBart { - continue - } - bartType, err := extractBARTItemType(item) - if err != nil { - return nil, err - } - if bartType != wire.BARTTypesBuddyIcon { - continue - } - b, hasBuf := item.Slice(wire.FeedbagAttributesBartInfo) - if !hasBuf { - return nil, errors.New("unable to extract icon payload") - } - bartInfo := wire.BARTInfo{} - if err := wire.UnmarshalBE(&bartInfo, bytes.NewBuffer(b)); err != nil { - return nil, err - } - return &wire.BARTID{ - Type: bartType, - BARTInfo: wire.BARTInfo{ - Flags: bartInfo.Flags, - Hash: bartInfo.Hash, - }, - }, nil - } - - return nil, nil -} - -// extractBARTItemType gets the BART type for item, which is stored in the -// "name" field. -func extractBARTItemType(item wire.FeedbagItem) (uint16, error) { - var bartType uint16 - // Feedbag items of type wire.FeedbagClassIdBart store the BART type in the - // name field. - if bt, err := strconv.ParseUint(item.Name, 10, 16); err != nil { - return 0, err - } else { - bartType = uint16(bt) - } - return bartType, nil -} - func (s BuddyService) BroadcastBuddyDeparted(ctx context.Context, sess *state.Session) error { recipients, err := s.feedbagManager.AdjacentUsers(sess.IdentScreenName()) if err != nil { diff --git a/foodgroup/types.go b/foodgroup/types.go index 307009d0..04df6d34 100644 --- a/foodgroup/types.go +++ b/foodgroup/types.go @@ -50,6 +50,7 @@ type FeedbagManager interface { FeedbagLastModified(screenName state.IdentScreenName) (time.Time, error) Feedbag(screenName state.IdentScreenName) ([]wire.FeedbagItem, error) FeedbagUpsert(screenName state.IdentScreenName, items []wire.FeedbagItem) error + BuddyIconRefByName(screenName state.IdentScreenName) (*wire.BARTID, error) } // LegacyBuddyListManager defines operations for tracking user relationships diff --git a/server/http/mgmt_api.go b/server/http/mgmt_api.go index c1edb0fd..12e19f10 100644 --- a/server/http/mgmt_api.go +++ b/server/http/mgmt_api.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/google/uuid" @@ -27,30 +28,72 @@ func StartManagementAPI( chatRoomCreator ChatRoomCreator, chatSessionRetriever ChatSessionRetriever, messageRelayer MessageRelayer, + bartRetriever BARTRetriever, + feedbagRetriever FeedBagRetriever, + accountRetriever AccountRetriever, + profileRetriever ProfileRetriever, logger *slog.Logger, ) { mux := http.NewServeMux() - mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { - userHandler(w, r, userManager, uuid.New, logger) + + // Handlers for '/user' route + mux.HandleFunc("DELETE /user", func(w http.ResponseWriter, r *http.Request) { + deleteUserHandler(w, r, userManager, logger) + }) + mux.HandleFunc("GET /user", func(w http.ResponseWriter, r *http.Request) { + getUserHandler(w, userManager, logger) + }) + mux.HandleFunc("POST /user", func(w http.ResponseWriter, r *http.Request) { + postUserHandler(w, r, userManager, uuid.New, logger) + }) + + // Handlers for '/user/password' route + mux.HandleFunc("PUT /user/password", func(w http.ResponseWriter, r *http.Request) { + putUserPasswordHandler(w, r, userManager, logger) + }) + + // Handlers for '/user/login' route + mux.HandleFunc("GET /user/login", func(w http.ResponseWriter, r *http.Request) { + getUserLoginHandler(w, r, userManager, logger) + }) + + // Handlers for '/user/{screenname}/account' route + mux.HandleFunc("GET /user/{screenname}/account", func(w http.ResponseWriter, r *http.Request) { + getUserAccountHandler(w, r, userManager, accountRetriever, profileRetriever, logger) + }) + + // Handlers for '/user/{screenname}/icon' route + mux.HandleFunc("GET /user/{screenname}/icon", func(w http.ResponseWriter, r *http.Request) { + getUserBuddyIconHandler(w, r, userManager, feedbagRetriever, bartRetriever, logger) }) - mux.HandleFunc("/user/password", func(w http.ResponseWriter, r *http.Request) { - userPasswordHandler(w, r, userManager, logger) + + // Handlers for '/session' route + mux.HandleFunc("GET /session", func(w http.ResponseWriter, r *http.Request) { + getSessionHandler(w, r, sessionRetriever, time.Since) }) - mux.HandleFunc("/user/login", func(w http.ResponseWriter, r *http.Request) { - loginHandler(w, r, userManager, logger) + + // Handlers for '/session/{screenname}' route + mux.HandleFunc("GET /session/{screenname}", func(w http.ResponseWriter, r *http.Request) { + getSessionHandler(w, r, sessionRetriever, time.Since) }) - mux.HandleFunc("/session", func(w http.ResponseWriter, r *http.Request) { - sessionHandler(w, r, sessionRetriever) + + // Handlers for '/chat/room/public' route + mux.HandleFunc("GET /chat/room/public", func(w http.ResponseWriter, r *http.Request) { + getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger) }) - mux.HandleFunc("/chat/room/public", func(w http.ResponseWriter, r *http.Request) { - publicChatHandler(w, r, chatRoomRetriever, chatRoomCreator, chatSessionRetriever, logger) + mux.HandleFunc("POST /chat/room/public", func(w http.ResponseWriter, r *http.Request) { + postPublicChatHandler(w, r, chatRoomCreator, logger) }) - mux.HandleFunc("/chat/room/private", func(w http.ResponseWriter, r *http.Request) { - privateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger) + + // Handlers for '/chat/room/private' route + mux.HandleFunc("GET /chat/room/private", func(w http.ResponseWriter, r *http.Request) { + getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger) }) - mux.HandleFunc("/instant-message", func(w http.ResponseWriter, r *http.Request) { - instantMessageHandler(w, r, messageRelayer, logger) + + // Handlers for '/instant-message' route + mux.HandleFunc("POST /instant-message", func(w http.ResponseWriter, r *http.Request) { + postInstantMessageHandler(w, r, messageRelayer, logger) }) addr := net.JoinHostPort(cfg.ApiHost, cfg.ApiPort) @@ -61,19 +104,7 @@ func StartManagementAPI( } } -func userHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) { - switch r.Method { - case http.MethodDelete: - deleteUserHandler(w, r, userManager, logger) - case http.MethodGet: - getUserHandler(w, r, userManager, logger) - case http.MethodPost: - postUserHandler(w, r, userManager, newUUID, logger) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - +// deleteUserHandler handles the DELETE /user endpoint. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) { user, err := userFromBody(r) if err != nil { @@ -96,15 +127,6 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManag _, _ = fmt.Fprintln(w, "User account successfully deleted.") } -func userPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) { - switch r.Method { - case http.MethodPut: - putUserPasswordHandler(w, r, userManager, logger) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - // putUserPasswordHandler handles the PUT /user/password endpoint. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) { input, err := userFromBody(r) @@ -123,7 +145,7 @@ func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager case errors.Is(err, state.ErrPasswordInvalid): http.Error(w, err.Error(), http.StatusBadRequest) return - case err != nil: + default: logger.Error("error updating user password PUT /user/password", "err", err.Error()) http.Error(w, "internal server error", http.StatusInternalServerError) return @@ -134,26 +156,43 @@ func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager _, _ = fmt.Fprintln(w, "Password successfully reset.") } -// sessionHandler handles GET /session -func sessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever) { +// getSessionHandler handles GET /session +func getSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever, funcTimeSince func(t time.Time) time.Duration) { w.Header().Set("Content-Type", "application/json") - if r.Method != http.MethodGet { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - allUsers := sessionRetriever.AllSessions() + var allUsers []*state.Session + + if screenName := r.PathValue("screenname"); screenName != "" { + session := sessionRetriever.RetrieveByScreenName(state.NewIdentScreenName(screenName)) + if session == nil { + http.Error(w, "session not found", http.StatusNotFound) + return + } + allUsers = append(allUsers, session) + } else { + allUsers = sessionRetriever.AllSessions() + } ou := onlineUsers{ Count: len(allUsers), - Sessions: make([]userHandle, len(allUsers)), + Sessions: make([]sessionHandle, len(allUsers)), } for i, s := range allUsers { - ou.Sessions[i] = userHandle{ - ID: s.IdentScreenName().String(), - ScreenName: s.DisplayScreenName().String(), - IsICQ: s.UIN() > 0, + // report 0 if the user is not idle + idleSeconds := funcTimeSince(s.IdleTime()).Seconds() + if !s.Idle() { + idleSeconds = 0 + } + onlineSeconds := funcTimeSince(s.SignonTime()).Seconds() + + ou.Sessions[i] = sessionHandle{ + ID: s.IdentScreenName().String(), + ScreenName: s.DisplayScreenName().String(), + OnlineSeconds: onlineSeconds, + AwayMessage: s.AwayMessage(), + IdleSeconds: idleSeconds, + IsICQ: s.UIN() > 0, } } @@ -164,7 +203,7 @@ func sessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever Ses } // getUserHandler handles the GET /user endpoint. -func getUserHandler(w http.ResponseWriter, _ *http.Request, userManager UserManager, logger *slog.Logger) { +func getUserHandler(w http.ResponseWriter, userManager UserManager, logger *slog.Logger) { w.Header().Set("Content-Type", "application/json") users, err := userManager.AllUsers() @@ -246,9 +285,9 @@ func userFromBody(r *http.Request) (userWithPassword, error) { return user, nil } -// loginHandler is a temporary endpoint for validating user credentials for +// getUserLoginHandler is a temporary endpoint for validating user credentials for // chivanet. do not rely on this endpoint, as it will be eventually removed. -func loginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) { +func getUserLoginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) { authHeader := r.Header.Get("Authorization") if authHeader == "" { // No authentication header found @@ -299,26 +338,6 @@ func loginHandler(w http.ResponseWriter, r *http.Request, userManager UserManage _, _ = w.Write([]byte("200 OK: Successfully Authenticated\n")) } -func publicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatRoomCreator ChatRoomCreator, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) { - switch r.Method { - case http.MethodGet: - getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger) - case http.MethodPost: - postPublicChatHandler(w, r, chatRoomCreator, logger) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - -func privateChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) { - switch r.Method { - case http.MethodGet: - getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - // getPublicChatHandler handles the GET /chat/room/public endpoint. func getPublicChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) { w.Header().Set("Content-Type", "application/json") @@ -421,15 +440,6 @@ func getPrivateChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetri } } -func instantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) { - switch r.Method { - case http.MethodPost: - postInstantMessageHandler(w, r, messageRelayer, logger) - default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - } -} - // postIMHandler handles the POST /instant-message endpoint. func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) { input := instantMessage{} @@ -467,3 +477,95 @@ func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRe w.WriteHeader(http.StatusOK) _, _ = fmt.Fprintln(w, "Message sent successfully.") } + +// getUserBuddyIconHandler handles the GET /user/{screenname}/icon endpoint. +func getUserBuddyIconHandler(w http.ResponseWriter, r *http.Request, u UserManager, f FeedBagRetriever, b BARTRetriever, logger *slog.Logger) { + w.Header().Set("Content-Type", "image/gif") + + screenName := state.NewIdentScreenName(r.PathValue("screenname")) + user, err := u.User(screenName) + if err != nil { + logger.Error("error retrieving user", "err", err.Error()) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + if user == nil { + http.Error(w, "user not found", http.StatusNotFound) + return + } + iconRef, err := f.BuddyIconRefByName(screenName) + if err != nil { + logger.Error("error retrieving buddy icon ref", "err", err.Error()) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + if iconRef == nil || iconRef.HasClearIconHash() { + http.Error(w, "icon not found", http.StatusNotFound) + return + } + icon, err := b.BARTRetrieve(iconRef.Hash) + if err != nil { + logger.Error("error retrieving buddy icon bart item", "err", err.Error()) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + w.Write(icon) +} + +// getUserAccountHandler handles the GET /user/{screenname}/account endpoint. +func getUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountRetriever, p ProfileRetriever, logger *slog.Logger) { + w.Header().Set("Content-Type", "application/json") + + screenName := r.PathValue("screenname") + user, err := userManager.User(state.NewIdentScreenName(screenName)) + if err != nil { + logger.Error("error in GET /user/{screenname}/account", "err", err.Error()) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + if user == nil { + http.Error(w, "user not found", http.StatusNotFound) + return + } + + emailAddress := "" + email, err := a.EmailAddressByName(user.IdentScreenName) + if err != nil { + emailAddress = "" + } else { + emailAddress = email.String() + } + regStatus, err := a.RegStatusByName(user.IdentScreenName) + if err != nil { + logger.Error("error in GET /user/*/account RegStatus", "err", err.Error()) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + confirmStatus, err := a.ConfirmStatusByName(user.IdentScreenName) + if err != nil { + logger.Error("error in GET /user/*/account ConfirmStatus", "err", err.Error()) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + profile, err := p.Profile(user.IdentScreenName) + if err != nil { + logger.Error("error in GET /user/*/account Profile", "err", err.Error()) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + out := userAccountHandle{ + ID: user.IdentScreenName.String(), + ScreenName: user.DisplayScreenName.String(), + EmailAddress: emailAddress, + RegStatus: regStatus, + Confirmed: confirmStatus, + Profile: profile, + IsICQ: user.IsICQ, + } + + if err := json.NewEncoder(w).Encode(out); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/server/http/types.go b/server/http/types.go index c528a8a8..e09b6569 100644 --- a/server/http/types.go +++ b/server/http/types.go @@ -2,6 +2,7 @@ package http import ( "context" + "net/mail" "time" "github.com/mk6i/retro-aim-server/state" @@ -22,6 +23,7 @@ type ChatSessionRetriever interface { type SessionRetriever interface { AllSessions() []*state.Session + RetrieveByScreenName(screenName state.IdentScreenName) *state.Session } type UserManager interface { @@ -36,14 +38,28 @@ type MessageRelayer interface { RelayToScreenName(ctx context.Context, screenName state.IdentScreenName, msg wire.SNACMessage) } +type AccountRetriever interface { + EmailAddressByName(screenName state.IdentScreenName) (*mail.Address, error) + RegStatusByName(screenName state.IdentScreenName) (uint16, error) + ConfirmStatusByName(screnName state.IdentScreenName) (bool, error) +} + +type BARTRetriever interface { + BARTRetrieve(itemHash []byte) ([]byte, error) +} + +type FeedBagRetriever interface { + BuddyIconRefByName(screenName state.IdentScreenName) (*wire.BARTID, error) +} + type userWithPassword struct { ScreenName string `json:"screen_name"` Password string `json:"password,omitempty"` } type onlineUsers struct { - Count int `json:"count"` - Sessions []userHandle `json:"sessions"` + Count int `json:"count"` + Sessions []sessionHandle `json:"sessions"` } type userHandle struct { @@ -57,6 +73,29 @@ type aimChatUserHandle struct { ScreenName string `json:"screen_name"` } +type userAccountHandle struct { + ID string `json:"id"` + ScreenName string `json:"screen_name"` + Profile string `json:"profile"` + EmailAddress string `json:"email_address"` + RegStatus uint16 `json:"reg_status"` + Confirmed bool `json:"confirmed"` + IsICQ bool `json:"is_icq"` +} + +type sessionHandle struct { + ID string `json:"id"` + ScreenName string `json:"screen_name"` + OnlineSeconds float64 `json:"online_seconds"` + AwayMessage string `json:"away_message"` + IdleSeconds float64 `json:"idle_seconds"` + IsICQ bool `json:"is_icq"` +} + +type ProfileRetriever interface { + Profile(screenName state.IdentScreenName) (string, error) +} + type chatRoomCreate struct { Name string `json:"name"` } diff --git a/state/session.go b/state/session.go index ebb5c06e..8e8d6b57 100644 --- a/state/session.go +++ b/state/session.go @@ -138,6 +138,27 @@ func (s *Session) SetSignonTime(t time.Time) { s.signonTime = t } +// SignonTime reports when the user signed on +func (s *Session) SignonTime() time.Time { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.signonTime +} + +// Idle reports the user's idle state. +func (s *Session) Idle() bool { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.idle +} + +// IdleTime reports when the user went idle +func (s *Session) IdleTime() time.Time { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.idleTime +} + // SetIdle sets the user's idle state. func (s *Session) SetIdle(dur time.Duration) { s.mutex.Lock() diff --git a/state/user_store.go b/state/user_store.go index 2122d1d5..4f08aed9 100644 --- a/state/user_store.go +++ b/state/user_store.go @@ -1281,3 +1281,45 @@ func (f SQLiteUserStore) DeleteMessages(recip IdentScreenName) error { _, err := f.db.Exec(q, recip.String()) return err } + +// BuddyIconRefByName retrieves the buddy icon reference for a given user +func (f SQLiteUserStore) BuddyIconRefByName(screenName IdentScreenName) (*wire.BARTID, error) { + q := ` + SELECT + groupID, + itemID, + classID, + name, + attributes + FROM feedBag + WHERE screenname = ? AND name = ? AND classID = ? + ` + var item wire.FeedbagItem + var attrs []byte + err := f.db.QueryRow(q, screenName.String(), wire.BARTTypesBuddyIcon, wire.FeedbagClassIdBart).Scan(&item.GroupID, &item.ItemID, &item.ClassID, &item.Name, &attrs) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, err + } + if err := wire.UnmarshalBE(&item.TLVLBlock, bytes.NewBuffer(attrs)); err != nil { + return nil, err + } + b, hasBuf := item.Slice(wire.FeedbagAttributesBartInfo) + if !hasBuf { + return nil, errors.New("unable to extract icon payload") + } + bartInfo := wire.BARTInfo{} + if err := wire.UnmarshalBE(&bartInfo, bytes.NewBuffer(b)); err != nil { + return nil, err + } + return &wire.BARTID{ + Type: wire.BARTTypesBuddyIcon, + BARTInfo: wire.BARTInfo{ + Flags: bartInfo.Flags, + Hash: bartInfo.Hash, + }, + }, nil + +}