<commit_before>#!/usr/bin/ruby
# 
# This file contains the MUES::User class. Instances of this class represent a
# remote client who has connected to the MUES and provided the proper
# authentication.
# 
# == Synopsis
# 
#   require "mues/User"
# 
# == To Do
#
# * Remove the hard-coded AccountTypes and replace it with configurable
#   types. This will require some deep thought about what to do when types change
#   between multiple runs of the environment, how to specify conversion functions
#   for modified types when there are already users who are of old types, etc.
#
# == Rcsid
# 
# $Id: user.rb,v 1.27 2002/10/18 00:35:28 stillflame Exp $
# 
# == Authors
# 
# * <NAME> <<EMAIL>>
# * <NAME> <<EMAIL>>
# 
#:include: COPYRIGHT
#
#---
#
# Please see the file COPYRIGHT for licensing details.
#

require "date"
require "digest/md5"

require "mues/Object"
require "mues/StorableObject"
require "mues/Events"
require "mues/Exceptions"
require "mues/filters/MacroFilter"
require "mues/filters/Questionnaire"

module MUES

	# A user connection class for the MUES::Engine. A user object is an
	# abstraction which contains the data describing a connected user and
	# methods for interacting with that data, but does not contain any IO
	# functionality itself. That task is delegated to a MUES::IOEventStream
	# object which is joined with the user object at activation time. Activation
	# is the process of associating a user object with a particular IO
	# connection and a "command shell" after the user has provided valid
	# authentication. The command shell is an instance of MUES::CommandShell (or
	# a derivative) which the user can use to perform tasks in the MUES such as
	# creating new characters, assuming the roles of existing characters,
	# creating macros, etc.
	class User < MUES::StorableObject ; implements MUES::Debuggable

		include MUES::Event::Handler, MUES::TypeCheckFunctions

		### Class constants
		Version			= /([\d\.]+)/.match( %q$Revision: 1.27 $ )[1]
		Rcsid			= %q$Id: user.rb,v 1.27 2002/10/18 00:35:28 stillflame Exp $

		# Account type constants module for the MUES::User class. Contains the
		# following constants:
		# 
		# [USER]
		#   Normal user. No special permissions.
		# 
		# [CREATOR]
		#   Creator permissions allow the user to start and stop their own Environments,
		#   examine the state of any object inside an Environment which they have
		#   started, and fetch limited runtime statistics from the Engine.
		# 
		# [IMPLEMENTOR]
		#   Implementor permissions allow the user to start and stop any Environment,
		#   view the state of any object in any Environment, interact with the Engine to
		#   a limited degree (shutdown, restart, reload config), view the banlist, etc.
		# 
		# [ADMIN]
		#   Unlimited permissions.
		module AccountType
			USER		= 0		# Regular user
			CREATOR		= 1		# Can world-interaction access
			IMPLEMENTOR	= 2		# Has server-interaction access
			ADMIN		= 3		# Unrestricted access

			Name = %w{User Creator Implementor Admin}
			Default = Name.first
			
			Map = begin
				hash = {}
				Name.each_with_index {|name,i| hash[name.downcase] = i}
				hash
			end

		end
		AccountType.freeze


		### Class methods

		### Return a MUES::Questionnaire IOEventFilter object suitable for
		### insertion into a user's IOEventStream to query for the information
		### necessary to create a new user. 
		def self.getCreationQuestionnaire
			qnaire = Questionnaire::new( "Create User", *CreateUserQuestions ) {|qnaire|
				user = MUES::User::new( qnaire.answers )
				MUES::ServerFunctions::registerUser( user )

				qnaire.message( "User '%s' created." % user.login.capitalize )
			}
			qnaire.debugLevel = 5
			return qnaire
		end


		### Return a MUES::Questionnaire IOEventFilter object suitable for
		### insertion into a user's IOEventStream to query for a new password.
		def self.getPasswordQuestionnaire( user, isOther )
			questions = nil

			# If we're changing another user's password, don't prompt for the
			# old password
			if isOther
				questions = ChangePasswordQuestions[1..-1]
			else
				questions = ChangePasswordQuestions
			end

			name = "Changing %s's password" % user.login.capitalize
			qnaire = Questionnaire::new( name, *questions ) {|qnaire|
				user.password = qnaire.answers[:newPassword]
				qnaire.message( "Password changed.\n\n" )
			}

			# Add the user to the support data so the questionnaire's validators
			# can get at it
			qnaire.debugLevel = 5
			qnaire.supportData[:user] = user

			return qnaire
		end


		### Return a MUES::Questionnaire IOEventFilter object suitable for
		### insertion into a user's IOEventStream to query for altering an
		### attribute of a user.
		def self.getAttributeQuestionnaire(user, attribute)
			question = CreateUserQuestions.find {|question| question[:name] == attribute} or
				raise RuntimeError, "Can't build a questionnaire for '%s': No question with that name." %
				attribute
			question[:question] = ("Currently: %s\n" % user.send(attribute.intern)) + question[:question]

			qnaire = Questionnaire::new( "Change #{attribute}", question ) {|qnaire|
				MUES::ServerFunctions::unregisterUser( user )
				user.send("#{attribute}=", qnaire.answers[attribute.intern])
				MUES::ServerFunctions::registerUser( user )
				qnaire.message "Changed '%s' for %s to '%s'\n\n" %
					[ attribute, user.login.capitalize, user.send(attribute.intern) ]
			}

			qnaire.debugLevel = 5
			return qnaire
		end



		### Create a new user object with the hash of attributes specified. The
		### valid attributes are:
		###
		### [<tt>:accountType</tt>]
		###   The AccountType of the user. One of the constants listed in
		###   MUES::User::AccountType, or a stringified version of one of the
		###   constants (eg., 'creator' or 'CREATOR'); defaults to
		###   MUES::User::AccountType::USER.
		### [<tt>:username</tt>]
		###   The login name of the user. Defaults to 'guest'.
		### [<tt>:realname</tt>]
		###   The real name of the user. Defaults to 'Guest User'.
		### [<tt>:emailAddress</tt>]
		###   The user's email address. Defaults to 'guestAccount@localhost'.
		### [<tt>:lastLoginDate</tt>]
		###   The date of the user's last connection.
		### [<tt>:lastHost</tt>]
		###   The hostname or IP of host the user last connected from.
		### [<tt>:password</tt>]
		###   The user's unencrypted password.
		def initialize( attributes={} )
			checkResponse( attributes, '[]', '[]=', 'has_key?' )
			super()

			@remoteHost			= nil
			@ioEventStream		= nil
			@activated			= false

			# Translate stringified account types
			if attributes[:accountType].kind_of? String
				type = attributes[:accountType].downcase
				raise MUES::Exception, "No such account type #{type}" unless
					AccountType::Map.key?( type )
				attributes[:accountType] = AccountType::Map[ type ]
			end

			@accountType		= attributes[:accountType]	|| AccountType::USER
			@accountType.freeze

			self.taint unless @accountType >= AccountType::IMPLEMENTOR

			@username			= attributes[:username]		|| 'guest'
			@realname			= attributes[:realname]		|| 'Guest User'
			@emailAddress		= attributes[:emailAddress] || 'guestAccount@localhost'
			@lastLoginDate		= attributes[:lastLoginDate]
			@lastHost			= attributes[:lastHost]

			@timeCreated		= Time.now
			@firstLoginTick		= 0

			@flags				= 0
			@preferences		= {}

			@cryptedPass		= '*'
			self.password		= attributes[:password] if attributes.has_key?( :password )
		end



		######
		public
		######

		### :FIXME: Do these need to be accessors? Or can they be readers?

		# The IOEventStream object belonging to this user
		attr_reader		:ioEventStream

		# The name/IP of the host the user is connected from
		attr_reader		:remoteHost

		# The username of the user
		attr_accessor	:username
		alias :login :username

		# The real name of the player
		attr_accessor	:realname

		# The player's email address
		attr_accessor	:emailAddress

		# The Date the user last logged in
		attr_accessor	:lastLoginDate

		# The host the user last logged in from
		attr_accessor	:lastHost

		# The type of account the user has (one of MUES::User::AccountType)
		attr_reader		:accountType

		# Bitflags for the user (currently unused)
		attr_accessor	:flags

		# Hash of preferences for the user
		attr_accessor	:preferences

		# Tick number of the user's first login
		attr_reader		:firstLoginTick

		# Date from when the user was created
		attr_reader		:timeCreated

		# The user's encrypted password
		attr_reader		:cryptedPass


		### Returns true if the engine has activated this user object
		def activated? 
			@activated
		end


		### Sets the remote IP that the client is connected from, and sets the
		### user's 'lastHost' attribute.
		def remoteHost=( newIp )
			checkType( newIp, ::String )

			@remoteHost = @lastHost = newIp
		end


		### Returns true if this user has creator permissions
		def isCreator? 
			return @accountType >= AccountType::CREATOR
		end


		### Returns true if this user has implementor permissions
		def isImplementor? 
			return @accountType >= AccountType::IMPLEMENTOR
		end


		### Returns true if this user has admin permissions
		def isAdmin? 
			return @accountType >= AccountType::ADMIN
		end


		### Returns a stringified version of the user object
		def to_s 
			if self.isCreator?
				return "%s - %s <%s> (%s)" % [
					@realname,
					@username.capitalize,
					@emailAddress,
					AccountType::Name[@accountType]
				]
			else
				return "%s - %s <%s>" % [
					@realname,
					@username.capitalize,
					@emailAddress
				]
			end
		end


		### Comparison operator
		def <=>( otherUser )

			# Return either the accounttype comparison (since #nonzero? returns
			# the numeric value itself if it is true), or the comparison of
			# usernames, which should always be unique.
			(@accountType <=> otherUser.accounttype).nonzero? ||
				@username <=> otherUser.username
		end


		### Reset the user's password to ((|newPassword|)). The password will be
		### encrypted before being stored.
		def password=( newPassword )
			newPassword = <PASSWORD>.to_s
			@cryptedPass = Digest::MD5::hexdigest( newPassword )
		end


		### Returns true if the specified password matches the user's.
		def passwordMatches?( pass )
			return Digest::MD5::hexdigest( pass ) == @cryptedPass
		end


		### Activate the user, set up their environment with the given stream,
		### and output the specified 'message of the day', if given.
		def activate( stream, cshell, motd=nil )
			checkType( stream, MUES::IOEventStream )
			checkType( cshell, MUES::CommandShell )

			# Create the command shell and macro filters and add them
			macros = MUES::MacroFilter.new( self )
			stream.addFilters( cshell, macros )

			# Set the stream, send the MOTD if it was specified, and flag the
			# object as activated
			@ioEventStream = stream
			@ioEventStream.addEvents( MUES::OutputEvent::new("\n\n" + motd.strip + "\n\n") ) if motd
			@ioEventStream.unpause

			@activated = true

			debugMsg( 1, "MOTD is: #{motd.inspect}" )

			MUES::OutputEvent::RegisterHandlers( self )
			#TickEvent.RegisterHandlers( self )
			return []
		end
<commit_msg>- Converted event registration to use the MUES::Event::Handler function instead   of manually registering.<commit_after>#!/usr/bin/ruby
# 
# This file contains the MUES::User class. Instances of this class represent a
# remote client who has connected to the MUES and provided the proper
# authentication.
# 
# == Synopsis
# 
#   require "mues/User"
# 
# == To Do
#
# * Remove the hard-coded AccountTypes and replace it with configurable
#   types. This will require some deep thought about what to do when types change
#   between multiple runs of the environment, how to specify conversion functions
#   for modified types when there are already users who are of old types, etc.
#
# == Rcsid
# 
# $Id: user.rb,v 1.28 2002/10/23 04:58:29 deveiant Exp $
# 
# == Authors
# 
# * <NAME> <<EMAIL>>
# * <NAME> <<EMAIL>>
# 
#:include: COPYRIGHT
#
#---
#
# Please see the file COPYRIGHT for licensing details.
#

require "date"
require "digest/md5"

require "mues/Object"
require "mues/StorableObject"
require "mues/Events"
require "mues/Exceptions"
require "mues/filters/MacroFilter"
require "mues/filters/Questionnaire"

module MUES

	# A user connection class for the MUES::Engine. A user object is an
	# abstraction which contains the data describing a connected user and
	# methods for interacting with that data, but does not contain any IO
	# functionality itself. That task is delegated to a MUES::IOEventStream
	# object which is joined with the user object at activation time. Activation
	# is the process of associating a user object with a particular IO
	# connection and a "command shell" after the user has provided valid
	# authentication. The command shell is an instance of MUES::CommandShell (or
	# a derivative) which the user can use to perform tasks in the MUES such as
	# creating new characters, assuming the roles of existing characters,
	# creating macros, etc.
	class User < MUES::StorableObject ; implements MUES::Debuggable

		include MUES::Event::Handler, MUES::TypeCheckFunctions

		### Class constants
		Version			= /([\d\.]+)/.match( %q$Revision: 1.28 $ )[1]
		Rcsid			= %q$Id: user.rb,v 1.28 2002/10/23 04:58:29 deveiant Exp $

		# Account type constants module for the MUES::User class. Contains the
		# following constants:
		# 
		# [USER]
		#   Normal user. No special permissions.
		# 
		# [CREATOR]
		#   Creator permissions allow the user to start and stop their own Environments,
		#   examine the state of any object inside an Environment which they have
		#   started, and fetch limited runtime statistics from the Engine.
		# 
		# [IMPLEMENTOR]
		#   Implementor permissions allow the user to start and stop any Environment,
		#   view the state of any object in any Environment, interact with the Engine to
		#   a limited degree (shutdown, restart, reload config), view the banlist, etc.
		# 
		# [ADMIN]
		#   Unlimited permissions.
		module AccountType
			USER		= 0		# Regular user
			CREATOR		= 1		# Can world-interaction access
			IMPLEMENTOR	= 2		# Has server-interaction access
			ADMIN		= 3		# Unrestricted access

			Name = %w{User Creator Implementor Admin}
			Default = Name.first
			
			Map = begin
				hash = {}
				Name.each_with_index {|name,i| hash[name.downcase] = i}
				hash
			end

		end
		AccountType.freeze


		### Class methods

		### Return a MUES::Questionnaire IOEventFilter object suitable for
		### insertion into a user's IOEventStream to query for the information
		### necessary to create a new user. 
		def self.getCreationQuestionnaire
			qnaire = Questionnaire::new( "Create User", *CreateUserQuestions ) {|qnaire|
				user = MUES::User::new( qnaire.answers )
				MUES::ServerFunctions::registerUser( user )

				qnaire.message( "User '%s' created." % user.login.capitalize )
			}
			qnaire.debugLevel = 5
			return qnaire
		end


		### Return a MUES::Questionnaire IOEventFilter object suitable for
		### insertion into a user's IOEventStream to query for a new password.
		def self.getPasswordQuestionnaire( user, isOther )
			questions = nil

			# If we're changing another user's password, don't prompt for the
			# old password
			if isOther
				questions = ChangePasswordQuestions[1..-1]
			else
				questions = ChangePasswordQuestions
			end

			name = "Changing %s's password" % user.login.capitalize
			qnaire = Questionnaire::new( name, *questions ) {|qnaire|
				user.password = qnaire.answers[:newPassword]
				qnaire.message( "Password changed.\n\n" )
			}

			# Add the user to the support data so the questionnaire's validators
			# can get at it
			qnaire.debugLevel = 5
			qnaire.supportData[:user] = user

			return qnaire
		end


		### Return a MUES::Questionnaire IOEventFilter object suitable for
		### insertion into a user's IOEventStream to query for altering an
		### attribute of a user.
		def self.getAttributeQuestionnaire(user, attribute)
			question = CreateUserQuestions.find {|question| question[:name] == attribute} or
				raise RuntimeError, "Can't build a questionnaire for '%s': No question with that name." %
				attribute
			question[:question] = ("Currently: %s\n" % user.send(attribute.intern)) + question[:question]

			qnaire = Questionnaire::new( "Change #{attribute}", question ) {|qnaire|
				MUES::ServerFunctions::unregisterUser( user )
				user.send("#{attribute}=", qnaire.answers[attribute.intern])
				MUES::ServerFunctions::registerUser( user )
				qnaire.message "Changed '%s' for %s to '%s'\n\n" %
					[ attribute, user.login.capitalize, user.send(attribute.intern) ]
			}

			qnaire.debugLevel = 5
			return qnaire
		end



		### Create a new user object with the hash of attributes specified. The
		### valid attributes are:
		###
		### [<tt>:accountType</tt>]
		###   The AccountType of the user. One of the constants listed in
		###   MUES::User::AccountType, or a stringified version of one of the
		###   constants (eg., 'creator' or 'CREATOR'); defaults to
		###   MUES::User::AccountType::USER.
		### [<tt>:username</tt>]
		###   The login name of the user. Defaults to 'guest'.
		### [<tt>:realname</tt>]
		###   The real name of the user. Defaults to 'Guest User'.
		### [<tt>:emailAddress</tt>]
		###   The user's email address. Defaults to 'guestAccount@localhost'.
		### [<tt>:lastLoginDate</tt>]
		###   The date of the user's last connection.
		### [<tt>:lastHost</tt>]
		###   The hostname or IP of host the user last connected from.
		### [<tt>:password</tt>]
		###   The user's unencrypted password.
		def initialize( attributes={} )
			checkResponse( attributes, '[]', '[]=', 'has_key?' )
			super()

			@remoteHost			= nil
			@ioEventStream		= nil
			@activated			= false

			# Translate stringified account types
			if attributes[:accountType].kind_of? String
				type = attributes[:accountType].downcase
				raise MUES::Exception, "No such account type #{type}" unless
					AccountType::Map.key?( type )
				attributes[:accountType] = AccountType::Map[ type ]
			end

			@accountType		= attributes[:accountType]	|| AccountType::USER
			@accountType.freeze

			self.taint unless @accountType >= AccountType::IMPLEMENTOR

			@username			= attributes[:username]		|| 'guest'
			@realname			= attributes[:realname]		|| 'Guest User'
			@emailAddress		= attributes[:emailAddress] || 'guestAccount@localhost'
			@lastLoginDate		= attributes[:lastLoginDate]
			@lastHost			= attributes[:lastHost]

			@timeCreated		= Time.now
			@firstLoginTick		= 0

			@flags				= 0
			@preferences		= {}

			@cryptedPass		= '*'
			self.password		= attributes[:password] if attributes.has_key?( :password )
		end



		######
		public
		######

		### :FIXME: Do these need to be accessors? Or can they be readers?

		# The IOEventStream object belonging to this user
		attr_reader		:ioEventStream

		# The name/IP of the host the user is connected from
		attr_reader		:remoteHost

		# The username of the user
		attr_accessor	:username
		alias :login :username

		# The real name of the player
		attr_accessor	:realname

		# The player's email address
		attr_accessor	:emailAddress

		# The Date the user last logged in
		attr_accessor	:lastLoginDate

		# The host the user last logged in from
		attr_accessor	:lastHost

		# The type of account the user has (one of MUES::User::AccountType)
		attr_reader		:accountType

		# Bitflags for the user (currently unused)
		attr_accessor	:flags

		# Hash of preferences for the user
		attr_accessor	:preferences

		# Tick number of the user's first login
		attr_reader		:firstLoginTick

		# Date from when the user was created
		attr_reader		:timeCreated

		# The user's encrypted password
		attr_reader		:cryptedPass


		### Returns true if the engine has activated this user object
		def activated? 
			@activated
		end


		### Sets the remote IP that the client is connected from, and sets the
		### user's 'lastHost' attribute.
		def remoteHost=( newIp )
			checkType( newIp, ::String )

			@remoteHost = @lastHost = newIp
		end


		### Returns true if this user has creator permissions
		def isCreator? 
			return @accountType >= AccountType::CREATOR
		end


		### Returns true if this user has implementor permissions
		def isImplementor? 
			return @accountType >= AccountType::IMPLEMENTOR
		end


		### Returns true if this user has admin permissions
		def isAdmin? 
			return @accountType >= AccountType::ADMIN
		end


		### Returns a stringified version of the user object
		def to_s 
			if self.isCreator?
				return "%s - %s <%s> (%s)" % [
					@realname,
					@username.capitalize,
					@emailAddress,
					AccountType::Name[@accountType]
				]
			else
				return "%s - %s <%s>" % [
					@realname,
					@username.capitalize,
					@emailAddress
				]
			end
		end


		### Comparison operator
		def <=>( otherUser )

			# Return either the accounttype comparison (since #nonzero? returns
			# the numeric value itself if it is true), or the comparison of
			# usernames, which should always be unique.
			(@accountType <=> otherUser.accounttype).nonzero? ||
				@username <=> otherUser.username
		end


		### Reset the user's password to ((|newPassword|)). The password will be
		### encrypted before being stored.
		def password=( newPassword )
			newPassword = <PASSWORD>
			@cryptedPass = Digest::MD5::hexdigest( newPassword )
		end


		### Returns true if the specified password matches the user's.
		def passwordMatches?( pass )
			return Digest::MD5::hexdigest( pass ) == @cryptedPass
		end


		### Activate the user, set up their environment with the given stream,
		### and output the specified 'message of the day', if given.
		def activate( stream, cshell, motd=nil )
			checkType( stream, MUES::IOEventStream )
			checkType( cshell, MUES::CommandShell )

			# Create the command shell and macro filters and add them
			macros = MUES::MacroFilter.new( self )
			stream.addFilters( cshell, macros )

			# Set the stream, send the MOTD if it was specified, and flag the
			# object as activated
			@ioEventStream = stream
			@ioEventStream.addEvents( MUES::OutputEvent::new("\n\n" + motd.strip + "\n\n") ) if motd
			@ioEventStream.unpause

			@activated = true

			debugMsg( 1, "MOTD is: #{motd.inspect}" )

			registerHandlerForEvents( self, MUES::OutputEvent ) #MUES::TickEvent )
			return []
		end

{'fraction_non_alphanumeric': 0.10419655667144907, 'fraction_numerical': 0.004752510760401721, 'mean_word_length': 3.581963845521775, 'pattern_counts': {'":': 0, '<': 57, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2591661', 'n_tokens_mistral': 7830, 'n_tokens_neox': 6643, 'n_words': 2344}