Loading CHANGELOG.md +2 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr - Allow runtime email authentication calls to authenticate with username/password instead of email/password. - New authoritative match dispatcher function to defer message broadcasts until the end of the tick. - New runtime function to retrieve multiple user accounts by user ID. - Send notifications to admins of non-open groups when a user requests to join. - Send notifications to users when their request to join a group is accepted. ### Changed - Replace standard logger supplied to the Go runtime with a more powerful interface. Loading server/api_group.go +5 −4 Original line number Diff line number Diff line Loading @@ -244,6 +244,7 @@ func (s *ApiServer) DeleteGroup(ctx context.Context, in *api.DeleteGroupRequest) func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*empty.Empty, error) { userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) username := ctx.Value(ctxUsernameKey{}).(string) // Before hook. if fn := s.runtime.BeforeJoinGroup(); fn != nil { Loading @@ -256,7 +257,7 @@ func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*e // Extract request information and execute the hook. clientIP, clientPort := extractClientAddress(s.logger, ctx) result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) result, err, code := fn(ctx, s.logger, userID.String(), username, ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return nil, status.Error(code, err.Error()) } Loading @@ -281,7 +282,7 @@ func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*e return nil, status.Error(codes.InvalidArgument, "Group ID must be a valid ID.") } err = JoinGroup(ctx, s.logger, s.db, groupID, userID) err = JoinGroup(ctx, s.logger, s.db, s.router, groupID, userID, username) if err != nil { if err == ErrGroupNotFound { return nil, status.Error(codes.NotFound, "Group not found.") Loading @@ -301,7 +302,7 @@ func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*e // Extract request information and execute the hook. clientIP, clientPort := extractClientAddress(s.logger, ctx) fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) fn(ctx, s.logger, userID.String(), username, ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) // Stats measurement end boundary. span.End() Loading Loading @@ -430,7 +431,7 @@ func (s *ApiServer) AddGroupUsers(ctx context.Context, in *api.AddGroupUsersRequ userIDs = append(userIDs, uid) } err = AddGroupUsers(ctx, s.logger, s.db, userID, groupID, userIDs) err = AddGroupUsers(ctx, s.logger, s.db, s.router, userID, groupID, userIDs) if err != nil { if err == ErrGroupPermissionDenied { return nil, status.Error(codes.NotFound, "Group not found or permission denied.") Loading server/core_authenticate.go +1 −1 Original line number Diff line number Diff line Loading @@ -771,7 +771,7 @@ AND EXISTS Subject: subject, Content: string(content), SenderId: userID.String(), Code: NOTIFICATION_FRIEND_JOIN_GAME, Code: NotificationCodeFriendJoinGame, Persistent: true, CreateTime: ×tamp.Timestamp{Seconds: createTime}, }} Loading server/core_friend.go +2 −2 Original line number Diff line number Diff line Loading @@ -178,10 +178,10 @@ func AddFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, messageRout content, _ := json.Marshal(map[string]interface{}{"username": username}) for id, isFriendAccept := range notificationToSend { uid := uuid.FromStringOrNil(id) code := NOTIFICATION_FRIEND_REQUEST code := NotificationCodeFriendRequest subject := fmt.Sprintf("%v wants to add you as a friend", username) if isFriendAccept { code = NOTIFICATION_FRIEND_ACCEPT code = NotificationCodeFriendAccept subject = fmt.Sprintf("%v accepted your friend request", username) } notifications[uid] = []*api.Notification{{ Loading server/core_group.go +86 −10 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ import ( "database/sql" "encoding/base64" "encoding/gob" "encoding/json" "fmt" "strconv" "strings" "time" Loading Loading @@ -268,7 +270,7 @@ func DeleteGroup(ctx context.Context, logger *zap.Logger, db *sql.DB, groupID uu return nil } func JoinGroup(ctx context.Context, logger *zap.Logger, db *sql.DB, groupID uuid.UUID, userID uuid.UUID) error { func JoinGroup(ctx context.Context, logger *zap.Logger, db *sql.DB, router MessageRouter, groupID uuid.UUID, userID uuid.UUID, username string) error { query := ` SELECT id, creator_id, name, description, avatar_url, state, edge_count, lang_tag, max_count, metadata, create_time, update_time FROM groups Loading Loading @@ -311,6 +313,52 @@ WHERE (id = $1) AND (disable_time = '1970-01-01 00:00:00')` return err } // If it's a private group notify superadmins/admins that someone has requested to join. // Prepare notification data. notificationContentBytes, err := json.Marshal(map[string]string{"username": username}) if err != nil { logger.Error("Could not encode notification content.", zap.Error(err)) } else { notificationContent := string(notificationContentBytes) notificationSubject := fmt.Sprintf("User %v wants to join your group", username) notifications := make(map[uuid.UUID][]*api.Notification) query = "SELECT destination_id FROM group_edge WHERE source_id = $1::UUID AND (state = 0 OR state = 1)" rows, err := db.QueryContext(ctx, query, groupID) if err != nil { // Errors here will not cause the join operation to fail. logger.Error("Error looking up group admins to notify of join request.", zap.Error(err)) } else { defer rows.Close() for rows.Next() { var id string if err = rows.Scan(&id); err != nil { // Errors here will not cause the join operation to fail. logger.Error("Error reading up group admins to notify of join request.", zap.Error(err)) break } adminID := uuid.FromStringOrNil(id) notifications[adminID] = []*api.Notification{ &api.Notification{ Id: uuid.Must(uuid.NewV4()).String(), Subject: notificationSubject, Content: notificationContent, SenderId: userID.String(), Code: NotificationCodeGroupJoinRequest, Persistent: true, CreateTime: ×tamp.Timestamp{Seconds: time.Now().UTC().Unix()}, }, } } } if len(notifications) > 0 { NotificationSend(ctx, logger, db, router, notifications) } } logger.Info("Added join request to group.", zap.String("group_id", groupID.String()), zap.String("user_id", userID.String())) return nil } Loading Loading @@ -421,7 +469,7 @@ func LeaveGroup(ctx context.Context, logger *zap.Logger, db *sql.DB, groupID uui return nil } func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, caller uuid.UUID, groupID uuid.UUID, userIDs []uuid.UUID) error { func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, router MessageRouter, caller uuid.UUID, groupID uuid.UUID, userIDs []uuid.UUID) error { if caller != uuid.Nil { var dbState sql.NullInt64 query := "SELECT state FROM group_edge WHERE source_id = $1::UUID AND destination_id = $2::UUID" Loading @@ -440,17 +488,16 @@ func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, caller u } } var groupExists sql.NullBool query := "SELECT EXISTS (SELECT id FROM groups WHERE id = $1 AND disable_time = '1970-01-01 00:00:00')" err := db.QueryRowContext(ctx, query, groupID).Scan(&groupExists) if err != nil { logger.Error("Could not look up group when adding users.", zap.Error(err), zap.String("group_id", groupID.String())) return err } if !groupExists.Bool { var groupName sql.NullString query := "SELECT name FROM groups WHERE id = $1 AND disable_time = '1970-01-01 00:00:00'" if err := db.QueryRowContext(ctx, query, groupID).Scan(&groupName); err != nil { if err == sql.ErrNoRows { logger.Info("Cannot add users to disabled group.", zap.String("group_id", groupID.String())) return ErrGroupNotFound } logger.Error("Could not look up group when adding users.", zap.Error(err), zap.String("group_id", groupID.String())) return err } tx, err := db.BeginTx(ctx, nil) if err != nil { Loading @@ -458,7 +505,20 @@ func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, caller u return err } // Prepare notification data. notificationContentBytes, err := json.Marshal(map[string]string{"name": groupName.String}) if err != nil { logger.Error("Could not encode notification content.", zap.Error(err)) return err } notificationContent := string(notificationContentBytes) notificationSubject := fmt.Sprintf("You've been added to group %v", groupName.String) var notifications map[uuid.UUID][]*api.Notification if err := crdb.ExecuteInTx(ctx, tx, func() error { // If the transaction is retried ensure we wipe any notifications that may have been prepared by previous attempts. notifications = make(map[uuid.UUID][]*api.Notification, len(userIDs)) for _, uid := range userIDs { if uid == caller { continue Loading Loading @@ -504,12 +564,28 @@ func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, caller u return ErrGroupFull } } notifications[uid] = []*api.Notification{ &api.Notification{ Id: uuid.Must(uuid.NewV4()).String(), Subject: notificationSubject, Content: notificationContent, SenderId: caller.String(), Code: NotificationCodeGroupAdd, Persistent: true, CreateTime: ×tamp.Timestamp{Seconds: time.Now().UTC().Unix()}, }, } } return nil }); err != nil { return err } if len(notifications) > 0 { NotificationSend(ctx, logger, db, router, notifications) } return nil } Loading Loading
CHANGELOG.md +2 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr - Allow runtime email authentication calls to authenticate with username/password instead of email/password. - New authoritative match dispatcher function to defer message broadcasts until the end of the tick. - New runtime function to retrieve multiple user accounts by user ID. - Send notifications to admins of non-open groups when a user requests to join. - Send notifications to users when their request to join a group is accepted. ### Changed - Replace standard logger supplied to the Go runtime with a more powerful interface. Loading
server/api_group.go +5 −4 Original line number Diff line number Diff line Loading @@ -244,6 +244,7 @@ func (s *ApiServer) DeleteGroup(ctx context.Context, in *api.DeleteGroupRequest) func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*empty.Empty, error) { userID := ctx.Value(ctxUserIDKey{}).(uuid.UUID) username := ctx.Value(ctxUsernameKey{}).(string) // Before hook. if fn := s.runtime.BeforeJoinGroup(); fn != nil { Loading @@ -256,7 +257,7 @@ func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*e // Extract request information and execute the hook. clientIP, clientPort := extractClientAddress(s.logger, ctx) result, err, code := fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) result, err, code := fn(ctx, s.logger, userID.String(), username, ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) if err != nil { return nil, status.Error(code, err.Error()) } Loading @@ -281,7 +282,7 @@ func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*e return nil, status.Error(codes.InvalidArgument, "Group ID must be a valid ID.") } err = JoinGroup(ctx, s.logger, s.db, groupID, userID) err = JoinGroup(ctx, s.logger, s.db, s.router, groupID, userID, username) if err != nil { if err == ErrGroupNotFound { return nil, status.Error(codes.NotFound, "Group not found.") Loading @@ -301,7 +302,7 @@ func (s *ApiServer) JoinGroup(ctx context.Context, in *api.JoinGroupRequest) (*e // Extract request information and execute the hook. clientIP, clientPort := extractClientAddress(s.logger, ctx) fn(ctx, s.logger, userID.String(), ctx.Value(ctxUsernameKey{}).(string), ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) fn(ctx, s.logger, userID.String(), username, ctx.Value(ctxExpiryKey{}).(int64), clientIP, clientPort, in) // Stats measurement end boundary. span.End() Loading Loading @@ -430,7 +431,7 @@ func (s *ApiServer) AddGroupUsers(ctx context.Context, in *api.AddGroupUsersRequ userIDs = append(userIDs, uid) } err = AddGroupUsers(ctx, s.logger, s.db, userID, groupID, userIDs) err = AddGroupUsers(ctx, s.logger, s.db, s.router, userID, groupID, userIDs) if err != nil { if err == ErrGroupPermissionDenied { return nil, status.Error(codes.NotFound, "Group not found or permission denied.") Loading
server/core_authenticate.go +1 −1 Original line number Diff line number Diff line Loading @@ -771,7 +771,7 @@ AND EXISTS Subject: subject, Content: string(content), SenderId: userID.String(), Code: NOTIFICATION_FRIEND_JOIN_GAME, Code: NotificationCodeFriendJoinGame, Persistent: true, CreateTime: ×tamp.Timestamp{Seconds: createTime}, }} Loading
server/core_friend.go +2 −2 Original line number Diff line number Diff line Loading @@ -178,10 +178,10 @@ func AddFriends(ctx context.Context, logger *zap.Logger, db *sql.DB, messageRout content, _ := json.Marshal(map[string]interface{}{"username": username}) for id, isFriendAccept := range notificationToSend { uid := uuid.FromStringOrNil(id) code := NOTIFICATION_FRIEND_REQUEST code := NotificationCodeFriendRequest subject := fmt.Sprintf("%v wants to add you as a friend", username) if isFriendAccept { code = NOTIFICATION_FRIEND_ACCEPT code = NotificationCodeFriendAccept subject = fmt.Sprintf("%v accepted your friend request", username) } notifications[uid] = []*api.Notification{{ Loading
server/core_group.go +86 −10 Original line number Diff line number Diff line Loading @@ -20,6 +20,8 @@ import ( "database/sql" "encoding/base64" "encoding/gob" "encoding/json" "fmt" "strconv" "strings" "time" Loading Loading @@ -268,7 +270,7 @@ func DeleteGroup(ctx context.Context, logger *zap.Logger, db *sql.DB, groupID uu return nil } func JoinGroup(ctx context.Context, logger *zap.Logger, db *sql.DB, groupID uuid.UUID, userID uuid.UUID) error { func JoinGroup(ctx context.Context, logger *zap.Logger, db *sql.DB, router MessageRouter, groupID uuid.UUID, userID uuid.UUID, username string) error { query := ` SELECT id, creator_id, name, description, avatar_url, state, edge_count, lang_tag, max_count, metadata, create_time, update_time FROM groups Loading Loading @@ -311,6 +313,52 @@ WHERE (id = $1) AND (disable_time = '1970-01-01 00:00:00')` return err } // If it's a private group notify superadmins/admins that someone has requested to join. // Prepare notification data. notificationContentBytes, err := json.Marshal(map[string]string{"username": username}) if err != nil { logger.Error("Could not encode notification content.", zap.Error(err)) } else { notificationContent := string(notificationContentBytes) notificationSubject := fmt.Sprintf("User %v wants to join your group", username) notifications := make(map[uuid.UUID][]*api.Notification) query = "SELECT destination_id FROM group_edge WHERE source_id = $1::UUID AND (state = 0 OR state = 1)" rows, err := db.QueryContext(ctx, query, groupID) if err != nil { // Errors here will not cause the join operation to fail. logger.Error("Error looking up group admins to notify of join request.", zap.Error(err)) } else { defer rows.Close() for rows.Next() { var id string if err = rows.Scan(&id); err != nil { // Errors here will not cause the join operation to fail. logger.Error("Error reading up group admins to notify of join request.", zap.Error(err)) break } adminID := uuid.FromStringOrNil(id) notifications[adminID] = []*api.Notification{ &api.Notification{ Id: uuid.Must(uuid.NewV4()).String(), Subject: notificationSubject, Content: notificationContent, SenderId: userID.String(), Code: NotificationCodeGroupJoinRequest, Persistent: true, CreateTime: ×tamp.Timestamp{Seconds: time.Now().UTC().Unix()}, }, } } } if len(notifications) > 0 { NotificationSend(ctx, logger, db, router, notifications) } } logger.Info("Added join request to group.", zap.String("group_id", groupID.String()), zap.String("user_id", userID.String())) return nil } Loading Loading @@ -421,7 +469,7 @@ func LeaveGroup(ctx context.Context, logger *zap.Logger, db *sql.DB, groupID uui return nil } func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, caller uuid.UUID, groupID uuid.UUID, userIDs []uuid.UUID) error { func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, router MessageRouter, caller uuid.UUID, groupID uuid.UUID, userIDs []uuid.UUID) error { if caller != uuid.Nil { var dbState sql.NullInt64 query := "SELECT state FROM group_edge WHERE source_id = $1::UUID AND destination_id = $2::UUID" Loading @@ -440,17 +488,16 @@ func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, caller u } } var groupExists sql.NullBool query := "SELECT EXISTS (SELECT id FROM groups WHERE id = $1 AND disable_time = '1970-01-01 00:00:00')" err := db.QueryRowContext(ctx, query, groupID).Scan(&groupExists) if err != nil { logger.Error("Could not look up group when adding users.", zap.Error(err), zap.String("group_id", groupID.String())) return err } if !groupExists.Bool { var groupName sql.NullString query := "SELECT name FROM groups WHERE id = $1 AND disable_time = '1970-01-01 00:00:00'" if err := db.QueryRowContext(ctx, query, groupID).Scan(&groupName); err != nil { if err == sql.ErrNoRows { logger.Info("Cannot add users to disabled group.", zap.String("group_id", groupID.String())) return ErrGroupNotFound } logger.Error("Could not look up group when adding users.", zap.Error(err), zap.String("group_id", groupID.String())) return err } tx, err := db.BeginTx(ctx, nil) if err != nil { Loading @@ -458,7 +505,20 @@ func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, caller u return err } // Prepare notification data. notificationContentBytes, err := json.Marshal(map[string]string{"name": groupName.String}) if err != nil { logger.Error("Could not encode notification content.", zap.Error(err)) return err } notificationContent := string(notificationContentBytes) notificationSubject := fmt.Sprintf("You've been added to group %v", groupName.String) var notifications map[uuid.UUID][]*api.Notification if err := crdb.ExecuteInTx(ctx, tx, func() error { // If the transaction is retried ensure we wipe any notifications that may have been prepared by previous attempts. notifications = make(map[uuid.UUID][]*api.Notification, len(userIDs)) for _, uid := range userIDs { if uid == caller { continue Loading Loading @@ -504,12 +564,28 @@ func AddGroupUsers(ctx context.Context, logger *zap.Logger, db *sql.DB, caller u return ErrGroupFull } } notifications[uid] = []*api.Notification{ &api.Notification{ Id: uuid.Must(uuid.NewV4()).String(), Subject: notificationSubject, Content: notificationContent, SenderId: caller.String(), Code: NotificationCodeGroupAdd, Persistent: true, CreateTime: ×tamp.Timestamp{Seconds: time.Now().UTC().Unix()}, }, } } return nil }); err != nil { return err } if len(notifications) > 0 { NotificationSend(ctx, logger, db, router, notifications) } return nil } Loading