from evennia.commands.default.muxcommand import MuxCommand from evennia.server.sessionhandler import SESSIONS from evennia.utils import utils, evtable import time class CmdWho(MuxCommand): """ who [prefix] doing [prefix] doing/set [pithy quote] This command shows who is presently online. If a prefix is given, it will only show the accounts whose names start with that prefix. For admins, if you wish to see the player-side list, type 'doing'. For all players, if you'd like to set a pithy quote to show up in the last column, use 'doing/set'. If you don't provide a value to doing/set, it will clear yours. """ key = "who" aliases = ["doing", ] locks = "cmd:all()" account_caller = True def func(self): """ The MuxCommand class puts its main functionality in a function called, redundantly, 'func'. You don't need to implement the parser or anything else, but you DO need to implement 'func' or your command won't do anything. Also, since this multi-line comment occurs right below the function definition, it would be used as automatic documentation by most IDEs, such as PyCharm. The documentation right below the class definition will actually be used by Evennia for the helpfiles; when you type 'help who' or 'help doing' you'll get that comment right below CmdWho. This is nice, since it means your documentation stays right with the code. :return: """ # First, let's create an inner function. This is defined /within/ our func function, so it doesn't # clutter up anything outside of func. But within func, we'll be able to call get_sessions() # # For more information on inner functions in Python, it's worth reading up a bit about functional # programming. A useful place to start might be: # https://www.protechtraining.com/content/python_fundamentals_tutorial-functional_programming # # Technically this code doesn't need to be an inner function, but it's a nice place to demonstrate # how inner functions work. # def get_sessions(prefix=None): """ Given an optional prefix for account names, return a list of currently connected sessions. This is just a convenience function since we might use it several places. :param prefix: A prefix that the account name must start with to be included in the list. :return: """ # Get the raw list of sessions from Evennia sessions = SESSIONS.get_sessions() # If our prefix is None -- Python's equivalent of NULL or nil -- then # it counts as false. if prefix: # The following lambda is a bit complex; the short form is that for every session # it checks if the session key begins with the prefix we were passed, but only # if the session's get_account() is not None (empty). If the get_account() were # empty, then get_account().key.startswith() would be trying to call startswith on # a None value, and the program would crash. # # To read more on lambdas, check out: # https://www.programiz.com/python-programming/anonymous-function # # To read more on ternary conditional operators, like the use of if/else here, # check out: # https://www.pythoncentral.io/one-line-if-statement-in-python-ternary-conditional-operator/ # sessions = \ filter(lambda sess: sess.get_account().key.startswith( prefix) if sess.get_account() is not None else False, sessions) # We have our session list, so return it to the user. return sessions # Now we're into the main body of the func method again, having finished with our # embedded get_sessions function. # First, let's check if someone is setting their "doing" value. # # The cmdstring is what was typed to run this command; since we are aliased to # 'doing', you can run this either by typing 'who' or 'doing'. We only want to set # a value if they used "doing/set", though. # # The switches list contains a list of all switches used on the command. For # instance, if I did 'foo/bar/baz test' then switches would contain 'bar' and 'baz'. # # So this if statement checks that we ran 'doing' and have 'set' in our switches. # if self.cmdstring == "doing" and "set" in self.switches: # The 'db' field on Evennia Accounts, Players, and Objects contains # a special object that contains all the attributes you've set on-game # using the @set command. Think of it like the attributes on objects in # MUSH. # # We'll set an attribute called 'who_doing' on the account with whatever they # passed in. In this case, the 'args' field on a MuxCommand contains any arguments # passed to the command. # # The account field on a MuxCommand contains the Account that ran this command, so # we can just set the who_doing attribute directly on their 'db' record. # # This could also be done on-game by doing @set */who_doing=A doing! # if self.args == "": self.msg("Doing field cleared.") self.account.db.who_doing = None else: # The 'format' function on a string will take a list of parameters, and # replace any instances of {} in the string one-by-one. The {} can contain # various formatting guidelines, but that's beyond our scope here; you can # look up 'format' in the documentation for the Python string class. self.msg("Doing field set: {}".format(self.args)) self.account.db.who_doing = self.args # If we were just setting our doing, we're done now, so return return # Get the list of sessions, using the session_list function we defined above. # The 'args' field on a MuxCommand contains the entire argument passed to the command, # without using the = sign to split it into the lhs (left hand side) and rhs (right hand # side) values. In this case, we'll use it as the prefix we pass in to our function. # # This means if I typed 'who P' it should give me all the sessions where the account # starts with a P, because 'P' will be passed in as a prefix. If I typed 'WHO Pax' # it would return a list of all the accounts starting with 'Pax'. # session_list = get_sessions(self.args) # Sort the list by the time connected # # The 'key' parameter is a reference to a function that takes one # argument and returns a value that the list should be sorted by. # In this case, there's no convenient function to reference, so # we'll define what's called a lambda function: an anonymous inline # function. In this case, given a single 'sess' argument, it just # returns the conn_time field from sess. # # To read more on lambdas, check out: # https://www.programiz.com/python-programming/anonymous-function # session_list = sorted(session_list, key=lambda sess: sess.conn_time) # If our command was 'doing', we never show the admin version of things. # Otherwise, we check if the account has Developer or Admins permissions. # if self.cmdstring == "doing": show_admin_data = False else: show_admin_data= self.account.check_permstring("Developer") or self.account.check_permstring("Admins") if show_admin_data: # Create an instance of our Evennia table helper class with six # columns. table = evtable.EvTable("Account Name", "On For", "Idle", "Location", "Client", "Address") else: # Create an instance of our Evennia table helper class with four # columns. table = evtable.EvTable("Account Name", "On For", "Idle", "Doing") # Iterate across the sessions for session in session_list: # If this session isn't logged in -- i.e. is at the login screen # or something -- just skip it. if not session.logged_in: continue # How long has it been since their last command? # # time.time() returns the current UNIX time -- in the same format # as MUSH softcode's secs() function -- while ServerSession's # cmd_last_visible field is the timestamp of when we saw the last # command. We'll store the difference between the two. # delta_cmd = time.time() - session.cmd_last_visible # How long has this session been connected? # # Once again, we'll store the difference between right now # and the time they first connected. # delta_conn = time.time() - session.conn_time # Let's store the account just for easy reference, so we don't # have to do get_account() everywhere. Saves on typing. account = session.get_account() # An account's 'key' is the name of the account. There's no # reason to store this in a string, really, instead of using # account.key when we wanted to reference it, but this lets # me comment on it. account_name = account.key # If the 'who_doing' Evennia attribute on the account isn't empty, # let's store it in a new variable called doing_string, otherwise # let's store an empty string. # # The format we're using here is called a 'ternary conditional operator', # and you can read a bit more about it at: # https://www.pythoncentral.io/one-line-if-statement-in-python-ternary-conditional-operator/ # doing_string = account.db.who_doing if account.db.who_doing else "" # Get the Character that this Account is using. For MUSH-style games, # there's a one-to-one mapping of Account to Character, but you can set # up Evennia to have a single Account, and then let you pick from among the # characters tied to that Account. character = session.get_puppet() # Now we have all our data gathered! Let's add a row to the table for # this session, using a few Evennia utilities to crop strings and format # times. # # crop takes a string and a maximum length, to crop it to. # # time_format takes a value in seconds, and a style; 0 is hours:minutes, # 1 is a natural language string like '50m' or '2h'. # # For the admin, we have a few other things. 'id' on anything descended from # Object will be a number that is what MUSHers would think of as the dbref; we # use format here again to just add a hash on the beginning. In this case, # we check if the Character object (the session's puppet) that we got earlier # is not None, and use the string 'format' function to add a '#' to the beginning # of the number. Otherwise, we return an empty string. # # The 'protocol_key' field on a session will be 'websocket', 'telnet', or 'ssh'. # In this case, if they're using the websocket we show the string "Web Client", # otherwise we read the 'CLIENTNAME' value out of the session's 'protocol_flags' # table. The protocol_flags table contains everything the client has negotiated; # name of client, the size of the window, and so on. # # And lastly, it's possible for a session to have multiple addresses in some cases, # so we just make sure that if there are multiple addresses connected we only show # the first one. # if show_admin_data: table.add_row(utils.crop(account_name, 25), utils.time_format(delta_conn, 0), utils.time_format(delta_conn, 1), "#{}".format(character.location.id) if character else "", "Web Client" if session.protocol_key == "websocket" else session.protocol_flags['CLIENTNAME'], session.address[0] if isinstance(session.address, tuple) else session.address) else: table.add_row(utils.crop(account_name, 25), utils.time_format(delta_conn, 0), utils.time_format(delta_cmd, 1), utils.crop(doing_string, 35)) # Send the table to our user. self.msg(table)