Commit 8decfa6d authored by Mo Firouz's avatar Mo Firouz
Browse files

Ban users and create groups from within the Runtime environment. Merged #88

parent 9bd122f3
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
@@ -6,14 +6,13 @@ The format is based on [keep a changelog](http://keepachangelog.com/) and this p
## [Unreleased]
### Added
- New storage list feature.
- Ban users and create groups from within the Runtime environment.

### Changed
- Run Facebook friends import after registration completes.
- Streamline command line flags to be inline with the config file.

### Changed
- Restructure and stabilize API messages.
- Add batch operations Friends social graph.
- Update Runtime modules to use plural function names for batch operations. (`users_fetch_id` and `users_fetch_handle`)

### Fixed
- Invocation type was always set to "Before" in After Runtime scripts.
+43 −52
Original line number Diff line number Diff line
@@ -213,50 +213,48 @@ message Envelope {
    TGroupUsersAdd group_users_add = 26;
    TGroupUsersKick group_users_kick = 27;
    TGroupUsersPromote group_users_promote = 28;
    TGroup group = 29;
    TGroups groups = 30;
    TGroupUsers group_users = 31;

    TTopicsJoin topics_join = 32;
    TTopicsLeave topics_leave = 33;
    TTopicMessageSend topic_message_send = 34;
    TTopicMessagesList topic_messages_list = 35;
    TTopics topics = 36;
    TTopicMessageAck topic_message_ack = 37;
    TopicMessage topic_message = 38;
    TTopicMessages topic_messages = 39;
    TopicPresence topic_presence = 40;

    TMatchCreate match_create = 41;
    TMatchesJoin matches_join = 42;
    TMatchesLeave matches_leave = 43;
    MatchDataSend match_data_send = 44;
    TMatch match = 45;
    TMatches matches = 46;
    MatchData match_data = 47;
    MatchPresence match_presence = 48;

    TStorageList storage_list = 49;
    TStorageFetch storage_fetch = 50;
    TStorageWrite storage_write = 51;
    TStorageRemove storage_remove = 52;
    TStorageData storage_data = 53;
    TStorageKeys storage_keys = 54;

    TLeaderboardsList leaderboards_list = 55;
    TLeaderboardRecordsWrite leaderboard_records_write = 56;
    TLeaderboardRecordsFetch leaderboard_records_fetch = 57;
    TLeaderboardRecordsList leaderboard_records_list = 58;
    TLeaderboards leaderboards = 59;

    TLeaderboardRecords leaderboard_records = 60;

    TMatchmakeAdd matchmake_add = 61;
    TMatchmakeRemove matchmake_remove = 62;
    TMatchmakeTicket matchmake_ticket = 63;
    MatchmakeMatched matchmake_matched = 64;

    TRpc rpc = 65;
    TGroups groups = 29;
    TGroupUsers group_users = 30;

    TTopicsJoin topics_join = 31;
    TTopicsLeave topics_leave = 32;
    TTopicMessageSend topic_message_send = 33;
    TTopicMessagesList topic_messages_list = 34;
    TTopics topics = 35;
    TTopicMessageAck topic_message_ack = 36;
    TopicMessage topic_message = 37;
    TTopicMessages topic_messages = 38;
    TopicPresence topic_presence = 39;

    TMatchCreate match_create = 40;
    TMatchesJoin matches_join = 41;
    TMatchesLeave matches_leave = 42;
    MatchDataSend match_data_send = 43;
    TMatch match = 44;
    TMatches matches = 45;
    MatchData match_data = 46;
    MatchPresence match_presence = 47;

    TStorageList storage_list = 48;
    TStorageFetch storage_fetch = 49;
    TStorageWrite storage_write = 50;
    TStorageRemove storage_remove = 51;
    TStorageData storage_data = 52;
    TStorageKeys storage_keys = 53;

    TLeaderboardsList leaderboards_list = 54;
    TLeaderboardRecordsWrite leaderboard_records_write = 55;
    TLeaderboardRecordsFetch leaderboard_records_fetch = 56;
    TLeaderboardRecordsList leaderboard_records_list = 57;
    TLeaderboards leaderboards = 58;
    TLeaderboardRecords leaderboard_records = 59;

    TMatchmakeAdd matchmake_add = 60;
    TMatchmakeRemove matchmake_remove = 61;
    TMatchmakeTicket matchmake_ticket = 62;
    MatchmakeMatched matchmake_matched = 63;

    TRpc rpc = 64;
  }
}

@@ -509,7 +507,7 @@ message Group {
/**
 * TGroupsCreate creates a new group.
 *
 * @returns TGroup
 * @returns TGroups
 *
 * NOTE: The server only processes the first item of the list, and will ignore and logs a warning message for other items.
 */
@@ -529,13 +527,6 @@ message TGroupsCreate {
  repeated GroupCreate groups = 1;
}

/**
 * TGroup contains the group information.
 */
message TGroup {
  Group group = 1;
}

/**
 * TGroupsUpdate updates the group with matching Group ID.
 * Only group admins can update group information.

server/core_group.go

0 → 100644
+214 −0
Original line number Diff line number Diff line
// Copyright 2017 The Nakama Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
	"database/sql"
	"errors"
	"strconv"
	"strings"

	"github.com/satori/go.uuid"
	"go.uber.org/zap"
)

type GroupCreateParam struct {
	Name        string    // mandatory
	Creator     uuid.UUID // mandatory
	Description string
	AvatarURL   string
	Lang        string
	Metadata    []byte
	Private     bool
}

func extractGroup(r scanner) (*Group, error) {
	var id []byte
	var creatorID []byte
	var name sql.NullString
	var description sql.NullString
	var avatarURL sql.NullString
	var lang sql.NullString
	var utcOffsetMs sql.NullInt64
	var metadata []byte
	var state sql.NullInt64
	var count sql.NullInt64
	var createdAt sql.NullInt64
	var updatedAt sql.NullInt64

	err := r.Scan(&id, &creatorID, &name,
		&description, &avatarURL, &lang,
		&utcOffsetMs, &metadata, &state,
		&count, &createdAt, &updatedAt)

	if err != nil {
		return nil, err
	}

	desc := ""
	if description.Valid {
		desc = description.String
	}

	avatar := ""
	if avatarURL.Valid {
		avatar = avatarURL.String
	}

	private := state.Int64 == 1

	return &Group{
		Id:          id,
		CreatorId:   creatorID,
		Name:        name.String,
		Description: desc,
		AvatarUrl:   avatar,
		Lang:        lang.String,
		UtcOffsetMs: utcOffsetMs.Int64,
		Metadata:    metadata,
		Private:     private,
		Count:       count.Int64,
		CreatedAt:   createdAt.Int64,
		UpdatedAt:   updatedAt.Int64,
	}, nil
}

func GroupsCreate(logger *zap.Logger, db *sql.DB, groupCreateParams []*GroupCreateParam) ([]*Group, error) {
	if groupCreateParams == nil || len(groupCreateParams) == 0 {
		return nil, errors.New("Could not create groups. At least one group param must be supplied")
	}

	groups := make([]*Group, 0)
	tx, err := db.Begin()
	if err != nil {
		logger.Error("Could not create groups", zap.Error(err))
		return nil, err
	}

	defer func() {
		if err != nil {
			logger.Error("Could not create groups", zap.Error(err))
			if tx != nil {
				txErr := tx.Rollback()
				if txErr != nil {
					logger.Error("Could not rollback transaction", zap.Error(txErr))
				}
			}
		} else {
			err = tx.Commit()
			if err != nil {
				logger.Error("Could not commit transaction", zap.Error(err))
			} else {
				groupNames := make([]string, 0)
				for _, p := range groups {
					groupNames = append(groupNames, p.Name)
				}
				logger.Debug("Created new groups", zap.Strings("names", groupNames))
			}
		}
	}()

	for _, g := range groupCreateParams {
		newGroup, err := groupCreate(tx, g)
		if err != nil {
			logger.Warn("Could not create group", zap.String("name", g.Name), zap.Error(err))
			return nil, err
		}

		groups = append(groups, newGroup)
	}

	return groups, err
}

func groupCreate(tx *sql.Tx, g *GroupCreateParam) (*Group, error) {
	if g.Name == "" {
		return nil, errors.New("Group name must not be empty")
	}
	if uuid.Equal(uuid.Nil, g.Creator) {
		return nil, errors.New("Group creator must be set")
	}

	state := 0
	if g.Private {
		state = 1
	}

	columns := make([]string, 0)
	params := make([]string, 0)

	values := []interface{}{
		uuid.NewV4().Bytes(),
		g.Creator.Bytes(),
		g.Name,
		state,
		nowMs(), // updated_at
	}

	if g.Description != "" {
		columns = append(columns, "description")
		params = append(params, "$"+strconv.Itoa(len(values)+1))
		values = append(values, g.Description)
	}

	if g.AvatarURL != "" {
		columns = append(columns, "avatar_url")
		params = append(params, "$"+strconv.Itoa(len(values)+1))
		values = append(values, g.AvatarURL)
	}

	if g.Lang != "" {
		columns = append(columns, "lang")
		params = append(params, "$"+strconv.Itoa(len(values)+1))
		values = append(values, g.Lang)
	}

	if g.Metadata != nil {
		columns = append(columns, "metadata")
		params = append(params, "$"+strconv.Itoa(len(values)+1))
		values = append(values, g.Metadata)
	}

	r := tx.QueryRow(`
INSERT INTO groups (id, creator_id, name, state, count, created_at, updated_at, `+strings.Join(columns, ", ")+")"+`
VALUES ($1, $2, $3, $4, 1, $5, $5, `+strings.Join(params, ",")+")"+`
RETURNING id, creator_id, name, description, avatar_url, lang, utc_offset_ms, metadata, state, count, created_at, updated_at
`, values...)

	group, err := extractGroup(r)
	if err != nil {
		return nil, err
	}

	res, err := tx.Exec(`
INSERT INTO group_edge (source_id, position, updated_at, destination_id, state)
VALUES ($1, $2, $2, $3, 0), ($3, $2, $2, $1, 0)`,
		group.Id, updatedAt, g.Creator.Bytes())

	if err != nil {
		return nil, err
	}

	rowAffected, err := res.RowsAffected()
	if err != nil {
		return nil, err
	}
	if rowAffected == 0 {
		err = errors.New("Could not insert into group_edge table")
		return nil, err
	}

	return group, nil
}
+46 −5
Original line number Diff line number Diff line
@@ -128,33 +128,34 @@ func UsersFetchHandle(logger *zap.Logger, db *sql.DB, handles []string) ([]*User
}

func UsersFetchIdsHandles(logger *zap.Logger, db *sql.DB, userIds [][]byte, handles []string) ([]*User, error) {
	statements := make([]string, 0)
	idStatements := make([]string, 0)
	handleStatements := make([]string, 0)
	params := make([]interface{}, 0)

	counter := 1
	for _, userID := range userIds {
		statement := "$" + strconv.Itoa(counter)
		counter += 1
		statements = append(statements, statement)
		idStatements = append(idStatements, statement)
		params = append(params, userID)
	}
	for _, handle := range handles {
		statement := "$" + strconv.Itoa(counter)
		counter += 1
		statements = append(statements, statement)
		handleStatements = append(handleStatements, statement)
		params = append(params, handle)
	}

	query := "WHERE "
	if len(userIds) > 0 {
		query += "users.id IN (" + strings.Join(statements, ", ") + ")"
		query += "users.id IN (" + strings.Join(idStatements, ", ") + ")"
	}

	if len(handles) > 0 {
		if len(userIds) > 0 {
			query += " OR "
		}
		query += "users.handle IN (" + strings.Join(statements, ", ") + ")"
		query += "users.handle IN (" + strings.Join(handleStatements, ", ") + ")"
	}

	users, err := querySocialGraph(logger, db, query, params)
@@ -164,3 +165,43 @@ func UsersFetchIdsHandles(logger *zap.Logger, db *sql.DB, userIds [][]byte, hand

	return users, nil
}

func UsersBan(logger *zap.Logger, db *sql.DB, userIds [][]byte, handles []string) error {
	idStatements := make([]string, 0)
	handleStatements := make([]string, 0)
	params := []interface{}{nowMs()} // $1

	counter := 2
	for _, userID := range userIds {
		statement := "$" + strconv.Itoa(counter)
		idStatements = append(idStatements, statement)
		params = append(params, userID)
		counter++
	}
	for _, handle := range handles {
		statement := "$" + strconv.Itoa(counter)
		handleStatements = append(handleStatements, statement)
		params = append(params, handle)
		counter++
	}

	query := "UPDATE users SET disabled_at = $1 WHERE "
	if len(userIds) > 0 {
		query += "users.id IN (" + strings.Join(idStatements, ", ") + ")"
	}

	if len(handles) > 0 {
		if len(userIds) > 0 {
			query += " OR "
		}
		query += "users.handle IN (" + strings.Join(handleStatements, ", ") + ")"
	}

	logger.Debug("ban user query", zap.String("query", query))
	_, err := db.Exec(query, params...)
	if err != nil {
		logger.Error("Failed to ban users", zap.Error(err))
	}

	return err
}
+6 −57
Original line number Diff line number Diff line
@@ -38,57 +38,6 @@ type groupCursor struct {
	GroupID   []byte
}

func (p *pipeline) extractGroup(r scanner) (*Group, error) {
	var id []byte
	var creatorID []byte
	var name sql.NullString
	var description sql.NullString
	var avatarURL sql.NullString
	var lang sql.NullString
	var utcOffsetMs sql.NullInt64
	var metadata []byte
	var state sql.NullInt64
	var count sql.NullInt64
	var createdAt sql.NullInt64
	var updatedAt sql.NullInt64

	err := r.Scan(&id, &creatorID, &name,
		&description, &avatarURL, &lang,
		&utcOffsetMs, &metadata, &state,
		&count, &createdAt, &updatedAt)

	if err != nil {
		return &Group{}, err
	}

	desc := ""
	if description.Valid {
		desc = description.String
	}

	avatar := ""
	if avatarURL.Valid {
		avatar = avatarURL.String
	}

	private := state.Int64 == 1

	return &Group{
		Id:          id,
		CreatorId:   creatorID,
		Name:        name.String,
		Description: desc,
		AvatarUrl:   avatar,
		Lang:        lang.String,
		UtcOffsetMs: utcOffsetMs.Int64,
		Metadata:    metadata,
		Private:     private,
		Count:       count.Int64,
		CreatedAt:   createdAt.Int64,
		UpdatedAt:   updatedAt.Int64,
	}, nil
}

func (p *pipeline) groupCreate(logger *zap.Logger, session *session, envelope *Envelope) {
	e := envelope.GetGroupsCreate()

@@ -135,7 +84,7 @@ func (p *pipeline) groupCreate(logger *zap.Logger, session *session, envelope *E
				session.Send(ErrorMessageRuntimeException(envelope.CollationId, "Could not create group"))
			} else {
				logger.Info("Created new group", zap.String("name", group.Name))
				session.Send(&Envelope{CollationId: envelope.CollationId, Payload: &Envelope_Group{Group: &TGroup{Group: group}}})
				session.Send(&Envelope{CollationId: envelope.CollationId, Payload: &Envelope_Groups{&TGroups{Groups: []*Group{group}}}})
			}
		}
	}()
@@ -184,7 +133,7 @@ func (p *pipeline) groupCreate(logger *zap.Logger, session *session, envelope *E
		}

		columns = append(columns, "metadata")
		params = append(params, "$"+strconv.Itoa(len(values)))
		params = append(params, "$"+strconv.Itoa(len(values)+1))
		values = append(values, g.Metadata)
	}

@@ -194,7 +143,7 @@ VALUES ($1, $2, $3, $4, 1, $5, $5, `+strings.Join(params, ",")+")"+`
RETURNING id, creator_id, name, description, avatar_url, lang, utc_offset_ms, metadata, state, count, created_at, updated_at
`, values...)

	group, err = p.extractGroup(r)
	group, err = extractGroup(r)
	if err != nil {
		return
	}
@@ -421,7 +370,7 @@ FROM groups WHERE disabled_at = 0 AND ( `+strings.Join(statements, " OR ")+" )",

	groups := make([]*Group, 0)
	for rows.Next() {
		group, err := p.extractGroup(rows)
		group, err := extractGroup(rows)
		if err != nil {
			logger.Error("Could not get groups", zap.Error(err))
			session.Send(ErrorMessageRuntimeException(envelope.CollationId, "Could not get groups"))
@@ -530,7 +479,7 @@ LIMIT $` + strconv.Itoa(len(params))
			cursor = cursorBuf.Bytes()
			break
		}
		lastGroup, err = p.extractGroup(rows)
		lastGroup, err = extractGroup(rows)
		if err != nil {
			logger.Error("Could not list groups", zap.Error(err))
			session.Send(ErrorMessageRuntimeException(envelope.CollationId, "Could not list groups"))
@@ -564,7 +513,7 @@ WHERE group_edge.destination_id = $1 AND disabled_at = 0 AND (group_edge.state =
	groups := make([]*Group, 0)
	var lastGroup *Group
	for rows.Next() {
		lastGroup, err = p.extractGroup(rows)
		lastGroup, err = extractGroup(rows)
		if err != nil {
			logger.Error("Could not list joined groups", zap.Error(err))
			session.Send(ErrorMessageRuntimeException(envelope.CollationId, "Could not list joined groups"))
Loading