2021-07-20 05:22:29 +03:00
package user
import (
2022-04-22 00:55:26 +03:00
"context"
2021-07-20 05:22:29 +03:00
"database/sql"
"fmt"
"sort"
"strings"
"time"
2022-08-10 05:56:45 +03:00
"github.com/owncast/owncast/config"
2021-07-20 05:22:29 +03:00
"github.com/owncast/owncast/core/data"
2022-04-22 00:55:26 +03:00
"github.com/owncast/owncast/db"
2021-07-20 05:22:29 +03:00
"github.com/owncast/owncast/utils"
2022-04-22 00:55:26 +03:00
"github.com/pkg/errors"
2021-07-20 05:22:29 +03:00
"github.com/teris-io/shortid"
log "github.com/sirupsen/logrus"
)
var _datastore * data . Datastore
2022-03-07 07:09:55 +03:00
const (
moderatorScopeKey = "MODERATOR"
minSuggestedUsernamePoolLength = 10
)
2021-11-03 05:27:41 +03:00
2021-09-12 10:18:15 +03:00
// User represents a single chat user.
2021-07-20 05:22:29 +03:00
type User struct {
2022-04-22 00:55:26 +03:00
CreatedAt time . Time ` json:"createdAt" `
DisabledAt * time . Time ` json:"disabledAt,omitempty" `
NameChangedAt * time . Time ` json:"nameChangedAt,omitempty" `
2023-05-30 20:31:43 +03:00
AuthenticatedAt * time . Time ` json:"-" `
ID string ` json:"id" `
DisplayName string ` json:"displayName" `
PreviousNames [ ] string ` json:"previousNames" `
2022-04-22 00:55:26 +03:00
Scopes [ ] string ` json:"scopes,omitempty" `
2023-05-30 20:31:43 +03:00
DisplayColor int ` json:"displayColor" `
2022-04-22 00:55:26 +03:00
IsBot bool ` json:"isBot" `
Authenticated bool ` json:"authenticated" `
2021-07-20 05:22:29 +03:00
}
2021-09-12 10:18:15 +03:00
// IsEnabled will return if this single user is enabled.
2021-07-20 05:22:29 +03:00
func ( u * User ) IsEnabled ( ) bool {
return u . DisabledAt == nil
}
2021-11-03 05:27:41 +03:00
// IsModerator will return if the user has moderation privileges.
func ( u * User ) IsModerator ( ) bool {
_ , hasModerationScope := utils . FindInSlice ( u . Scopes , moderatorScopeKey )
return hasModerationScope
}
2021-09-12 10:18:15 +03:00
// SetupUsers will perform the initial initialization of the user package.
2021-07-20 05:22:29 +03:00
func SetupUsers ( ) {
_datastore = data . GetDatastore ( )
}
2022-12-29 21:03:15 +03:00
func generateDisplayName ( ) string {
suggestedUsernamesList := data . GetSuggestedUsernamesList ( )
if len ( suggestedUsernamesList ) >= minSuggestedUsernamePoolLength {
index := utils . RandomIndex ( len ( suggestedUsernamesList ) )
return suggestedUsernamesList [ index ]
} else {
return utils . GeneratePhrase ( )
}
}
2021-09-12 10:18:15 +03:00
// CreateAnonymousUser will create a new anonymous user with the provided display name.
2022-04-22 00:55:26 +03:00
func CreateAnonymousUser ( displayName string ) ( * User , string , error ) {
2022-12-29 21:03:15 +03:00
// Try to assign a name that was requested.
if displayName != "" {
// If name isn't available then generate a random one.
if available , _ := IsDisplayNameAvailable ( displayName ) ; ! available {
displayName = generateDisplayName ( )
2022-01-12 21:18:08 +03:00
}
2022-12-29 21:03:15 +03:00
} else {
displayName = generateDisplayName ( )
2021-07-20 05:22:29 +03:00
}
2022-08-10 05:56:45 +03:00
displayColor := utils . GenerateRandomDisplayColor ( config . MaxUserColor )
2021-07-20 05:22:29 +03:00
2022-12-29 21:03:15 +03:00
id := shortid . MustGenerate ( )
2021-07-20 05:22:29 +03:00
user := & User {
2021-09-12 10:18:15 +03:00
ID : id ,
2021-07-20 05:22:29 +03:00
DisplayName : displayName ,
DisplayColor : displayColor ,
CreatedAt : time . Now ( ) ,
}
2022-04-22 00:55:26 +03:00
// Create new user.
2021-07-20 05:22:29 +03:00
if err := create ( user ) ; err != nil {
2022-04-22 00:55:26 +03:00
return nil , "" , err
}
// Assign it an access token.
accessToken , err := utils . GenerateAccessToken ( )
if err != nil {
log . Errorln ( "Unable to create access token for new user" )
return nil , "" , err
}
if err := addAccessTokenForUser ( accessToken , id ) ; err != nil {
return nil , "" , errors . Wrap ( err , "unable to save access token for new user" )
2021-07-20 05:22:29 +03:00
}
2022-04-22 00:55:26 +03:00
return user , accessToken , nil
}
// IsDisplayNameAvailable will check if the proposed name is available for use.
func IsDisplayNameAvailable ( displayName string ) ( bool , error ) {
if available , err := _datastore . GetQueries ( ) . IsDisplayNameAvailable ( context . Background ( ) , displayName ) ; err != nil {
return false , errors . Wrap ( err , "unable to check if display name is available" )
} else if available != 0 {
return false , nil
}
return true , nil
2021-07-20 05:22:29 +03:00
}
2021-09-12 10:18:15 +03:00
// ChangeUsername will change the user associated to userID from one display name to another.
2022-04-22 00:55:26 +03:00
func ChangeUsername ( userID string , username string ) error {
2021-07-20 05:22:29 +03:00
_datastore . DbLock . Lock ( )
defer _datastore . DbLock . Unlock ( )
2022-04-22 00:55:26 +03:00
if err := _datastore . GetQueries ( ) . ChangeDisplayName ( context . Background ( ) , db . ChangeDisplayNameParams {
DisplayName : username ,
ID : userID ,
PreviousNames : sql . NullString { String : fmt . Sprintf ( ",%s" , username ) , Valid : true } ,
NamechangedAt : sql . NullTime { Time : time . Now ( ) , Valid : true } ,
} ) ; err != nil {
return errors . Wrap ( err , "unable to change display name" )
2021-07-20 05:22:29 +03:00
}
2022-04-22 00:55:26 +03:00
return nil
}
2021-07-20 05:22:29 +03:00
2022-08-10 05:56:45 +03:00
// ChangeUserColor will change the user associated to userID from one display name to another.
func ChangeUserColor ( userID string , color int ) error {
_datastore . DbLock . Lock ( )
defer _datastore . DbLock . Unlock ( )
if err := _datastore . GetQueries ( ) . ChangeDisplayColor ( context . Background ( ) , db . ChangeDisplayColorParams {
DisplayColor : int32 ( color ) ,
ID : userID ,
} ) ; err != nil {
return errors . Wrap ( err , "unable to change display color" )
}
return nil
}
2022-04-22 00:55:26 +03:00
func addAccessTokenForUser ( accessToken , userID string ) error {
return _datastore . GetQueries ( ) . AddAccessTokenForUser ( context . Background ( ) , db . AddAccessTokenForUserParams {
Token : accessToken ,
UserID : userID ,
} )
2021-07-20 05:22:29 +03:00
}
func create ( user * User ) error {
_datastore . DbLock . Lock ( )
defer _datastore . DbLock . Unlock ( )
tx , err := _datastore . DB . Begin ( )
if err != nil {
log . Debugln ( err )
}
defer func ( ) {
_ = tx . Rollback ( )
} ( )
2022-04-22 00:55:26 +03:00
stmt , err := tx . Prepare ( "INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)" )
2021-07-20 05:22:29 +03:00
if err != nil {
log . Debugln ( err )
}
defer stmt . Close ( )
2022-04-22 00:55:26 +03:00
_ , err = stmt . Exec ( user . ID , user . DisplayName , user . DisplayColor , user . DisplayName , user . CreatedAt )
2021-07-20 05:22:29 +03:00
if err != nil {
log . Errorln ( "error creating new user" , err )
2022-04-22 00:55:26 +03:00
return err
2021-07-20 05:22:29 +03:00
}
return tx . Commit ( )
}
2021-11-03 05:27:41 +03:00
// SetEnabled will set the enabled status of a single user by ID.
2021-07-20 05:22:29 +03:00
func SetEnabled ( userID string , enabled bool ) error {
_datastore . DbLock . Lock ( )
defer _datastore . DbLock . Unlock ( )
tx , err := _datastore . DB . Begin ( )
if err != nil {
return err
}
defer tx . Rollback ( ) //nolint
var stmt * sql . Stmt
if ! enabled {
stmt , err = tx . Prepare ( "UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?" )
} else {
stmt , err = tx . Prepare ( "UPDATE users SET disabled_at=null WHERE id IS ?" )
}
if err != nil {
return err
}
defer stmt . Close ( )
if _ , err := stmt . Exec ( userID ) ; err != nil {
return err
}
return tx . Commit ( )
}
// GetUserByToken will return a user by an access token.
func GetUserByToken ( token string ) * User {
2022-04-22 00:55:26 +03:00
u , err := _datastore . GetQueries ( ) . GetUserByAccessToken ( context . Background ( ) , token )
if err != nil {
return nil
}
2021-07-20 05:22:29 +03:00
2022-04-22 00:55:26 +03:00
var scopes [ ] string
if u . Scopes . Valid {
scopes = strings . Split ( u . Scopes . String , "," )
}
2021-07-20 05:22:29 +03:00
2022-04-22 00:55:26 +03:00
var disabledAt * time . Time
if u . DisabledAt . Valid {
disabledAt = & u . DisabledAt . Time
}
var authenticatedAt * time . Time
if u . AuthenticatedAt . Valid {
authenticatedAt = & u . AuthenticatedAt . Time
}
return & User {
ID : u . ID ,
DisplayName : u . DisplayName ,
DisplayColor : int ( u . DisplayColor ) ,
CreatedAt : u . CreatedAt . Time ,
DisabledAt : disabledAt ,
PreviousNames : strings . Split ( u . PreviousNames . String , "," ) ,
NameChangedAt : & u . NamechangedAt . Time ,
AuthenticatedAt : authenticatedAt ,
Authenticated : authenticatedAt != nil ,
Scopes : scopes ,
}
}
// SetAccessTokenToOwner will reassign an access token to be owned by a
// different user. Used for logging in with external auth.
func SetAccessTokenToOwner ( token , userID string ) error {
return _datastore . GetQueries ( ) . SetAccessTokenToOwner ( context . Background ( ) , db . SetAccessTokenToOwnerParams {
UserID : userID ,
Token : token ,
} )
}
// SetUserAsAuthenticated will mark that a user has been authenticated
// in some way.
func SetUserAsAuthenticated ( userID string ) error {
return errors . Wrap ( _datastore . GetQueries ( ) . SetUserAsAuthenticated ( context . Background ( ) , userID ) , "unable to set user as authenticated" )
2021-07-20 05:22:29 +03:00
}
2021-11-03 05:27:41 +03:00
// SetModerator will add or remove moderator status for a single user by ID.
func SetModerator ( userID string , isModerator bool ) error {
if isModerator {
return addScopeToUser ( userID , moderatorScopeKey )
}
return removeScopeFromUser ( userID , moderatorScopeKey )
}
func addScopeToUser ( userID string , scope string ) error {
u := GetUserByID ( userID )
2022-04-22 00:55:26 +03:00
if u == nil {
return errors . New ( "user not found when modifying scope" )
}
2021-11-03 05:27:41 +03:00
scopesString := u . Scopes
scopes := utils . StringSliceToMap ( scopesString )
scopes [ scope ] = true
scopesSlice := utils . StringMapKeys ( scopes )
return setScopesOnUser ( userID , scopesSlice )
}
func removeScopeFromUser ( userID string , scope string ) error {
u := GetUserByID ( userID )
scopesString := u . Scopes
scopes := utils . StringSliceToMap ( scopesString )
delete ( scopes , scope )
scopesSlice := utils . StringMapKeys ( scopes )
return setScopesOnUser ( userID , scopesSlice )
}
func setScopesOnUser ( userID string , scopes [ ] string ) error {
_datastore . DbLock . Lock ( )
defer _datastore . DbLock . Unlock ( )
tx , err := _datastore . DB . Begin ( )
if err != nil {
return err
}
defer tx . Rollback ( ) //nolint
scopesSliceString := strings . TrimSpace ( strings . Join ( scopes , "," ) )
stmt , err := tx . Prepare ( "UPDATE users SET scopes=? WHERE id IS ?" )
if err != nil {
return err
}
defer stmt . Close ( )
var val * string
if scopesSliceString == "" {
val = nil
} else {
val = & scopesSliceString
}
if _ , err := stmt . Exec ( val , userID ) ; err != nil {
return err
}
return tx . Commit ( )
}
2021-09-12 10:18:15 +03:00
// GetUserByID will return a user by a user ID.
func GetUserByID ( id string ) * User {
2021-07-20 05:22:29 +03:00
_datastore . DbLock . Lock ( )
defer _datastore . DbLock . Unlock ( )
2021-11-03 05:27:41 +03:00
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?"
2021-07-20 05:22:29 +03:00
row := _datastore . DB . QueryRow ( query , id )
if row == nil {
log . Errorln ( row )
return nil
}
return getUserFromRow ( row )
}
// GetDisabledUsers will return back all the currently disabled users that are not API users.
func GetDisabledUsers ( ) [ ] * User {
2021-11-03 05:27:41 +03:00
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'"
2021-07-20 05:22:29 +03:00
rows , err := _datastore . DB . Query ( query )
if err != nil {
log . Errorln ( err )
return nil
}
defer rows . Close ( )
users := getUsersFromRows ( rows )
sort . Slice ( users , func ( i , j int ) bool {
return users [ i ] . DisabledAt . Before ( * users [ j ] . DisabledAt )
} )
return users
}
2021-11-03 05:27:41 +03:00
// GetModeratorUsers will return a list of users with moderator access.
func GetModeratorUsers ( ) [ ] * User {
query := ` SELECT id , display_name , scopes , display_color , created_at , disabled_at , previous_names , namechanged_at FROM (
WITH RECURSIVE split ( id , display_name , scopes , display_color , created_at , disabled_at , previous_names , namechanged_at , scope , rest ) AS (
SELECT id , display_name , scopes , display_color , created_at , disabled_at , previous_names , namechanged_at , ' ' , scopes || ',' FROM users
UNION ALL
SELECT id , display_name , scopes , display_color , created_at , disabled_at , previous_names , namechanged_at ,
substr ( rest , 0 , instr ( rest , ',' ) ) ,
substr ( rest , instr ( rest , ',' ) + 1 )
FROM split
WHERE rest < > ' ' )
2022-01-12 21:18:08 +03:00
SELECT id , display_name , scopes , display_color , created_at , disabled_at , previous_names , namechanged_at , scope
FROM split
2021-11-03 05:27:41 +03:00
WHERE scope < > ' '
ORDER BY created_at
) AS token WHERE token . scope = ? `
rows , err := _datastore . DB . Query ( query , moderatorScopeKey )
if err != nil {
log . Errorln ( err )
return nil
}
defer rows . Close ( )
users := getUsersFromRows ( rows )
return users
}
2021-07-20 05:22:29 +03:00
func getUsersFromRows ( rows * sql . Rows ) [ ] * User {
users := make ( [ ] * User , 0 )
for rows . Next ( ) {
var id string
var displayName string
var displayColor int
var createdAt time . Time
var disabledAt * time . Time
var previousUsernames string
var userNameChangedAt * time . Time
2021-11-03 05:27:41 +03:00
var scopesString * string
2021-07-20 05:22:29 +03:00
2021-11-03 05:27:41 +03:00
if err := rows . Scan ( & id , & displayName , & scopesString , & displayColor , & createdAt , & disabledAt , & previousUsernames , & userNameChangedAt ) ; err != nil {
2021-07-20 05:22:29 +03:00
log . Errorln ( "error creating collection of users from results" , err )
return nil
}
2021-11-03 05:27:41 +03:00
var scopes [ ] string
if scopesString != nil {
scopes = strings . Split ( * scopesString , "," )
}
2021-07-20 05:22:29 +03:00
user := & User {
2021-09-12 10:18:15 +03:00
ID : id ,
2021-07-20 05:22:29 +03:00
DisplayName : displayName ,
DisplayColor : displayColor ,
CreatedAt : createdAt ,
DisabledAt : disabledAt ,
PreviousNames : strings . Split ( previousUsernames , "," ) ,
NameChangedAt : userNameChangedAt ,
2021-11-03 05:27:41 +03:00
Scopes : scopes ,
2021-07-20 05:22:29 +03:00
}
users = append ( users , user )
}
sort . Slice ( users , func ( i , j int ) bool {
return users [ i ] . CreatedAt . Before ( users [ j ] . CreatedAt )
} )
return users
}
func getUserFromRow ( row * sql . Row ) * User {
var id string
var displayName string
var displayColor int
var createdAt time . Time
var disabledAt * time . Time
var previousUsernames string
var userNameChangedAt * time . Time
2021-11-03 05:27:41 +03:00
var scopesString * string
2021-07-20 05:22:29 +03:00
2021-11-03 05:27:41 +03:00
if err := row . Scan ( & id , & displayName , & displayColor , & createdAt , & disabledAt , & previousUsernames , & userNameChangedAt , & scopesString ) ; err != nil {
2021-07-20 05:22:29 +03:00
return nil
}
2021-11-03 05:27:41 +03:00
var scopes [ ] string
if scopesString != nil {
scopes = strings . Split ( * scopesString , "," )
}
2021-07-20 05:22:29 +03:00
return & User {
2021-09-12 10:18:15 +03:00
ID : id ,
2021-07-20 05:22:29 +03:00
DisplayName : displayName ,
DisplayColor : displayColor ,
CreatedAt : createdAt ,
DisabledAt : disabledAt ,
PreviousNames : strings . Split ( previousUsernames , "," ) ,
NameChangedAt : userNameChangedAt ,
2021-11-03 05:27:41 +03:00
Scopes : scopes ,
2021-07-20 05:22:29 +03:00
}
}