github.com/mattermost/mattermost-server/v5@v5.39.3/store/sqlstore/channel_member_history_store.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package sqlstore
     5  
     6  import (
     7  	"database/sql"
     8  	"fmt"
     9  
    10  	sq "github.com/Masterminds/squirrel"
    11  	"github.com/pkg/errors"
    12  
    13  	"github.com/mattermost/mattermost-server/v5/model"
    14  	"github.com/mattermost/mattermost-server/v5/shared/mlog"
    15  	"github.com/mattermost/mattermost-server/v5/store"
    16  )
    17  
    18  type SqlChannelMemberHistoryStore struct {
    19  	*SqlStore
    20  }
    21  
    22  func newSqlChannelMemberHistoryStore(sqlStore *SqlStore) store.ChannelMemberHistoryStore {
    23  	s := &SqlChannelMemberHistoryStore{
    24  		SqlStore: sqlStore,
    25  	}
    26  
    27  	for _, db := range sqlStore.GetAllConns() {
    28  		table := db.AddTableWithName(model.ChannelMemberHistory{}, "ChannelMemberHistory").SetKeys(false, "ChannelId", "UserId", "JoinTime")
    29  		table.ColMap("ChannelId").SetMaxSize(26)
    30  		table.ColMap("UserId").SetMaxSize(26)
    31  		table.ColMap("JoinTime").SetNotNull(true)
    32  	}
    33  
    34  	return s
    35  }
    36  
    37  func (s SqlChannelMemberHistoryStore) LogJoinEvent(userId string, channelId string, joinTime int64) error {
    38  	channelMemberHistory := &model.ChannelMemberHistory{
    39  		UserId:    userId,
    40  		ChannelId: channelId,
    41  		JoinTime:  joinTime,
    42  	}
    43  
    44  	if err := s.GetMaster().Insert(channelMemberHistory); err != nil {
    45  		return errors.Wrapf(err, "LogJoinEvent userId=%s channelId=%s joinTime=%d", userId, channelId, joinTime)
    46  	}
    47  	return nil
    48  }
    49  
    50  func (s SqlChannelMemberHistoryStore) LogLeaveEvent(userId string, channelId string, leaveTime int64) error {
    51  	query, params, err := s.getQueryBuilder().
    52  		Update("ChannelMemberHistory").
    53  		Set("LeaveTime", leaveTime).
    54  		Where(sq.And{
    55  			sq.Eq{"UserId": userId},
    56  			sq.Eq{"ChannelId": channelId},
    57  			sq.Eq{"LeaveTime": nil},
    58  		}).ToSql()
    59  	if err != nil {
    60  		return errors.Wrap(err, "channel_member_history_to_sql")
    61  	}
    62  	sqlResult, err := s.GetMaster().Exec(query, params...)
    63  	if err != nil {
    64  		return errors.Wrapf(err, "LogLeaveEvent userId=%s channelId=%s leaveTime=%d", userId, channelId, leaveTime)
    65  	}
    66  
    67  	if rows, err := sqlResult.RowsAffected(); err == nil && rows != 1 {
    68  		// there was no join event to update - this is best effort, so no need to raise an error
    69  		mlog.Warn("Channel join event for user and channel not found", mlog.String("user", userId), mlog.String("channel", channelId))
    70  	}
    71  	return nil
    72  }
    73  
    74  func (s SqlChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
    75  	useChannelMemberHistory, err := s.hasDataAtOrBefore(startTime)
    76  	if err != nil {
    77  		return nil, errors.Wrapf(err, "hasDataAtOrBefore startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
    78  	}
    79  
    80  	if useChannelMemberHistory {
    81  		// the export period starts after the ChannelMemberHistory table was first introduced, so we can use the
    82  		// data from it for our export
    83  		channelMemberHistories, err2 := s.getFromChannelMemberHistoryTable(startTime, endTime, channelId)
    84  		if err2 != nil {
    85  			return nil, errors.Wrapf(err2, "getFromChannelMemberHistoryTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
    86  		}
    87  		return channelMemberHistories, nil
    88  	}
    89  	// the export period starts before the ChannelMemberHistory table was introduced, so we need to fake the
    90  	// data by assuming that anybody who has ever joined the channel in question was present during the export period.
    91  	// this may not always be true, but it's better than saying that somebody wasn't there when they were
    92  	channelMemberHistories, err := s.getFromChannelMembersTable(startTime, endTime, channelId)
    93  	if err != nil {
    94  		return nil, errors.Wrapf(err, "getFromChannelMembersTable startTime=%d endTime=%d channelId=%s", startTime, endTime, channelId)
    95  	}
    96  	return channelMemberHistories, nil
    97  }
    98  
    99  func (s SqlChannelMemberHistoryStore) hasDataAtOrBefore(time int64) (bool, error) {
   100  	type NullableCountResult struct {
   101  		Min sql.NullInt64
   102  	}
   103  	query, _, err := s.getQueryBuilder().Select("MIN(JoinTime) as Min").From("ChannelMemberHistory").ToSql()
   104  	if err != nil {
   105  		return false, errors.Wrap(err, "channel_member_history_to_sql")
   106  	}
   107  	var result NullableCountResult
   108  	if err := s.GetReplica().SelectOne(&result, query); err != nil {
   109  		return false, err
   110  	} else if result.Min.Valid {
   111  		return result.Min.Int64 <= time, nil
   112  	} else {
   113  		// if the result was null, there are no rows in the table, so there is no data from before
   114  		return false, nil
   115  	}
   116  }
   117  
   118  func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
   119  	query, args, err := s.getQueryBuilder().
   120  		Select("cmh.*, u.Email, u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt").
   121  		From("ChannelMemberHistory cmh").
   122  		Join("Users u ON cmh.UserId = u.Id").
   123  		LeftJoin("Bots ON Bots.UserId = u.Id").
   124  		Where(sq.And{
   125  			sq.Eq{"cmh.ChannelId": channelId},
   126  			sq.LtOrEq{"cmh.JoinTime": endTime},
   127  			sq.Or{
   128  				sq.Eq{"cmh.LeaveTime": nil},
   129  				sq.GtOrEq{"cmh.LeaveTime": startTime},
   130  			},
   131  		}).
   132  		OrderBy("cmh.JoinTime ASC").ToSql()
   133  	if err != nil {
   134  		return nil, errors.Wrap(err, "channel_member_history_to_sql")
   135  	}
   136  	var histories []*model.ChannelMemberHistoryResult
   137  	if _, err := s.GetReplica().Select(&histories, query, args...); err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	return histories, nil
   142  }
   143  
   144  func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistoryResult, error) {
   145  	query, args, err := s.getQueryBuilder().
   146  		Select("ch.ChannelId, ch.UserId, u.Email, u.Username, Bots.UserId IS NOT NULL AS IsBot, u.DeleteAt AS UserDeleteAt").
   147  		Distinct().
   148  		From("ChannelMembers ch").
   149  		Join("Users u ON ch.UserId = u.id").
   150  		LeftJoin("Bots ON Bots.UserId = u.id").
   151  		Where(sq.Eq{"ch.ChannelId": channelId}).ToSql()
   152  	if err != nil {
   153  		return nil, errors.Wrap(err, "channel_member_history_to_sql")
   154  	}
   155  
   156  	var histories []*model.ChannelMemberHistoryResult
   157  	if _, err := s.GetReplica().Select(&histories, query, args...); err != nil {
   158  		return nil, err
   159  	}
   160  	// we have to fill in the join/leave times, because that data doesn't exist in the channel members table
   161  	for _, channelMemberHistory := range histories {
   162  		channelMemberHistory.JoinTime = startTime
   163  		channelMemberHistory.LeaveTime = model.NewInt64(endTime)
   164  	}
   165  	return histories, nil
   166  }
   167  
   168  // PermanentDeleteBatchForRetentionPolicies deletes a batch of records which are affected by
   169  // the global or a granular retention policy.
   170  // See `genericPermanentDeleteBatchForRetentionPolicies` for details.
   171  func (s SqlChannelMemberHistoryStore) PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) {
   172  	builder := s.getQueryBuilder().
   173  		Select("ChannelMemberHistory.ChannelId, ChannelMemberHistory.UserId, ChannelMemberHistory.JoinTime").
   174  		From("ChannelMemberHistory")
   175  	return genericPermanentDeleteBatchForRetentionPolicies(RetentionPolicyBatchDeletionInfo{
   176  		BaseBuilder:         builder,
   177  		Table:               "ChannelMemberHistory",
   178  		TimeColumn:          "LeaveTime",
   179  		PrimaryKeys:         []string{"ChannelId", "UserId", "JoinTime"},
   180  		ChannelIDTable:      "ChannelMemberHistory",
   181  		NowMillis:           now,
   182  		GlobalPolicyEndTime: globalPolicyEndTime,
   183  		Limit:               limit,
   184  	}, s.SqlStore, cursor)
   185  }
   186  
   187  // DeleteOrphanedRows removes entries from ChannelMemberHistory when a corresponding channel no longer exists.
   188  func (s SqlChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (deleted int64, err error) {
   189  	// We need the extra level of nesting to deal with MySQL's locking
   190  	const query = `
   191  	DELETE FROM ChannelMemberHistory WHERE (ChannelId, UserId, JoinTime) IN (
   192  		SELECT * FROM (
   193  			SELECT ChannelId, UserId, JoinTime FROM ChannelMemberHistory
   194  			LEFT JOIN Channels ON ChannelMemberHistory.ChannelId = Channels.Id
   195  			WHERE Channels.Id IS NULL
   196  			LIMIT :Limit
   197  		) AS A
   198  	)`
   199  	props := map[string]interface{}{"Limit": limit}
   200  	result, err := s.GetMaster().Exec(query, props)
   201  	if err != nil {
   202  		return
   203  	}
   204  	deleted, err = result.RowsAffected()
   205  	return
   206  }
   207  
   208  func (s SqlChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
   209  	var (
   210  		query string
   211  		args  []interface{}
   212  		err   error
   213  	)
   214  
   215  	if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
   216  		var innerSelect string
   217  		innerSelect, args, err = s.getQueryBuilder().
   218  			Select("ctid").
   219  			From("ChannelMemberHistory").
   220  			Where(sq.And{
   221  				sq.NotEq{"LeaveTime": nil},
   222  				sq.LtOrEq{"LeaveTime": endTime},
   223  			}).Limit(uint64(limit)).
   224  			ToSql()
   225  		if err != nil {
   226  			return 0, errors.Wrap(err, "channel_member_history_to_sql")
   227  		}
   228  		query, _, err = s.getQueryBuilder().
   229  			Delete("ChannelMemberHistory").
   230  			Where(fmt.Sprintf(
   231  				"ctid IN (%s)", innerSelect,
   232  			)).ToSql()
   233  	} else {
   234  		query, args, err = s.getQueryBuilder().
   235  			Delete("ChannelMemberHistory").
   236  			Where(sq.And{
   237  				sq.NotEq{"LeaveTime": nil},
   238  				sq.LtOrEq{"LeaveTime": endTime},
   239  			}).
   240  			Limit(uint64(limit)).ToSql()
   241  	}
   242  	if err != nil {
   243  		return 0, errors.Wrap(err, "channel_member_history_to_sql")
   244  	}
   245  	sqlResult, err := s.GetMaster().Exec(query, args...)
   246  	if err != nil {
   247  		return 0, errors.Wrapf(err, "PermanentDeleteBatch endTime=%d limit=%d", endTime, limit)
   248  	}
   249  
   250  	rowsAffected, err := sqlResult.RowsAffected()
   251  	if err != nil {
   252  		return 0, errors.Wrapf(err, "PermanentDeleteBatch endTime=%d limit=%d", endTime, limit)
   253  	}
   254  	return rowsAffected, nil
   255  }